-
Notifications
You must be signed in to change notification settings - Fork 4k
Introduce TransientNode #12822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce TransientNode #12822
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| /** | ||
| * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025) | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { text } from "~lib/render-tree/test-utils" | ||
| import { TransientNode } from "~lib/render-tree/TransientNode" | ||
| import { AppNodeVisitor } from "~lib/render-tree/visitors/AppNodeVisitor.interface" | ||
| import { | ||
| DebugVisitor, | ||
| MAX_HASH_LENGTH, | ||
| } from "~lib/render-tree/visitors/DebugVisitor" | ||
|
|
||
| describe("TransientNode", () => { | ||
| describe("constructor defaults", () => { | ||
| it("sets defaults for transientNodes and deltaMsgReceivedAt", () => { | ||
| const now = 123456789 | ||
| const spy = vi.spyOn(Date, "now").mockReturnValue(now) | ||
|
|
||
| const anchor = text("anchor") | ||
| const node = new TransientNode("run-1", anchor) | ||
|
|
||
| expect(node.scriptRunId).toBe("run-1") | ||
| expect(node.anchor).toBe(anchor) | ||
| expect(Array.isArray(node.transientNodes)).toBe(true) | ||
| expect(node.transientNodes.length).toBe(0) | ||
| expect(node.deltaMsgReceivedAt).toBe(now) | ||
|
|
||
| spy.mockRestore() | ||
| }) | ||
|
|
||
| it("uses provided transient nodes and timestamp", () => { | ||
| const t1 = text("t1") | ||
| const t2 = text("t2") | ||
| const node = new TransientNode("run-2", undefined, [t1, t2], 42) | ||
|
|
||
| expect(node.transientNodes).toEqual([t1, t2]) | ||
| expect(node.deltaMsgReceivedAt).toBe(42) | ||
| }) | ||
| }) | ||
|
|
||
| describe("accept + debug", () => { | ||
| it("accepts a visitor and returns its value", () => { | ||
| const node = new TransientNode("run-v", text("a"), [], 1) | ||
| const visitor: AppNodeVisitor<string> = { | ||
| visitBlockNode: vi.fn(), | ||
| visitElementNode: vi.fn(), | ||
| visitTransientNode: vi.fn().mockReturnValue("ok"), | ||
| } | ||
|
|
||
| const out = node.accept(visitor) | ||
| expect(out).toBe("ok") | ||
| expect(visitor.visitTransientNode).toHaveBeenCalledWith(node) | ||
| }) | ||
|
|
||
| it("produces a human-readable debug string including anchor and transients", () => { | ||
| const anchor = text("anchor-text") | ||
| const t1 = text("t1") | ||
| const t2 = text("t2") | ||
| const node = new TransientNode("run-xyz", anchor, [t1, t2], 5) | ||
|
|
||
| const debug = node.debug() | ||
|
|
||
| expect(debug.split("\n")[0]).toBe( | ||
| `└── TransientNode [2 transient] (run: ${"run-xyz".substring(0, MAX_HASH_LENGTH)})` | ||
| ) | ||
| expect(debug).toContain("anchor:") | ||
| expect(debug).toContain("ElementNode [text]") | ||
| expect(debug).toContain("transient nodes:") | ||
|
|
||
| // Also validate DebugVisitor can be used directly | ||
| const viaVisitor = node.accept(new DebugVisitor()) | ||
| expect(viaVisitor).toBe(debug) | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /** | ||
| * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025) | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { AppNode } from "./AppNode.interface" | ||
| import { ElementNode } from "./ElementNode" | ||
| import { AppNodeVisitor } from "./visitors/AppNodeVisitor.interface" | ||
| import { DebugVisitor } from "./visitors/DebugVisitor" | ||
|
|
||
| /** | ||
| * A TransientNode represents a transient Node in the tree that can hold | ||
| * multiple transient Elements. It maintains an anchor node, which is the node | ||
| * that would persist after the transient nodes are cleared. | ||
| */ | ||
|
|
||
| export class TransientNode implements AppNode { | ||
| readonly anchor?: AppNode | ||
| readonly transientNodes: ElementNode[] | ||
| readonly scriptRunId: string | ||
| readonly deltaMsgReceivedAt?: number | ||
| readonly fragmentId?: string | ||
| readonly activeScriptHash?: string | ||
|
|
||
| constructor( | ||
| scriptRunId: string, | ||
| anchor?: AppNode, | ||
| transientNodes?: ElementNode[], | ||
| deltaMsgReceivedAt?: number | ||
| ) { | ||
| this.scriptRunId = scriptRunId | ||
| this.anchor = anchor | ||
| this.transientNodes = transientNodes ?? [] | ||
| this.deltaMsgReceivedAt = deltaMsgReceivedAt ?? Date.now() | ||
|
|
||
| // We explicitly set these to undefined because transient nodes | ||
| // are not associated with a fragment or a script hash directly. | ||
| // The anchor node will have the fragmentId and activeScriptHash. | ||
| this.fragmentId = undefined | ||
| this.activeScriptHash = undefined | ||
| } | ||
|
|
||
| accept<T>(visitor: AppNodeVisitor<T>): T { | ||
| return visitor.visitTransientNode(this) | ||
| } | ||
|
|
||
| public debug(): string { | ||
| return this.accept(new DebugVisitor()) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,9 +14,13 @@ | |||||||||||||||||||||||||||||||||||||||
| * limitations under the License. | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import { AppNode, NO_SCRIPT_RUN_ID } from "~lib/render-tree/AppNode.interface" | ||||||||||||||||||||||||||||||||||||||||
| import { BlockNode } from "~lib/render-tree/BlockNode" | ||||||||||||||||||||||||||||||||||||||||
| import { ElementNode } from "~lib/render-tree/ElementNode" | ||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| AppNode, | ||||||||||||||||||||||||||||||||||||||||
| BlockNode, | ||||||||||||||||||||||||||||||||||||||||
| ElementNode, | ||||||||||||||||||||||||||||||||||||||||
| NO_SCRIPT_RUN_ID, | ||||||||||||||||||||||||||||||||||||||||
| TransientNode, | ||||||||||||||||||||||||||||||||||||||||
| } from "~lib/AppNode" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import { AppNodeVisitor } from "./AppNodeVisitor.interface" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -78,6 +82,40 @@ export class DebugVisitor implements AppNodeVisitor<string> { | |||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| visitTransientNode(node: TransientNode): string { | ||||||||||||||||||||||||||||||||||||||||
| const connector = this.isLast ? "└── " : "├── " | ||||||||||||||||||||||||||||||||||||||||
| const childPrefix = this.prefix + (this.isLast ? " " : "│ ") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| let result = `${this.prefix}${connector}TransientNode [${node.transientNodes.length} transient]` | ||||||||||||||||||||||||||||||||||||||||
| if (node.scriptRunId !== NO_SCRIPT_RUN_ID) { | ||||||||||||||||||||||||||||||||||||||||
| result += ` (run: ${node.scriptRunId.substring(0, MAX_HASH_LENGTH)})` | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| result += "\n" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (node.anchor) { | ||||||||||||||||||||||||||||||||||||||||
| result += `${childPrefix}├── anchor:\n` | ||||||||||||||||||||||||||||||||||||||||
| const anchorVisitor = new DebugVisitor(childPrefix + "│ ", true) | ||||||||||||||||||||||||||||||||||||||||
| result += node.anchor.accept(anchorVisitor) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (node.transientNodes.length > 0) { | ||||||||||||||||||||||||||||||||||||||||
| result += `${childPrefix}└── transient nodes:\n` | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+95
to
+103
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tree connector logic is incorrect. The anchor section always uses Impact: When a TransientNode has an anchor but no transient nodes, the debug output will show incorrect tree structure with dangling connectors. Fix: The connector should be conditional: if (node.anchor) {
const hasTransientNodes = node.transientNodes.length > 0
const anchorConnector = hasTransientNodes ? "├──" : "└──"
result += `${childPrefix}${anchorConnector} anchor:\n`
const anchorChildPrefix = childPrefix + (hasTransientNodes ? "│ " : " ")
const anchorVisitor = new DebugVisitor(anchorChildPrefix, true)
result += node.anchor.accept(anchorVisitor)
}
Suggested change
Spotted by Graphite Agent |
||||||||||||||||||||||||||||||||||||||||
| node.transientNodes.forEach((transientNode, index) => { | ||||||||||||||||||||||||||||||||||||||||
| const isLastTransient = index === node.transientNodes.length - 1 | ||||||||||||||||||||||||||||||||||||||||
| const transientConnector = isLastTransient ? "└── " : "├── " | ||||||||||||||||||||||||||||||||||||||||
| const transientChildPrefix = | ||||||||||||||||||||||||||||||||||||||||
| childPrefix + " " + (isLastTransient ? " " : "│ ") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| result += `${childPrefix} ${transientConnector}:\n` | ||||||||||||||||||||||||||||||||||||||||
kmcgrady marked this conversation as resolved.
Show resolved
Hide resolved
kmcgrady marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||
| const transientVisitor = new DebugVisitor(transientChildPrefix, true) | ||||||||||||||||||||||||||||||||||||||||
| result += transientNode.accept(transientVisitor) | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Static helper method to generate debug output for any AppNode. | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.