Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/lib/src/AppNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
* limitations under the License.
*/

export type { AppNode } from "./render-tree/AppNode.interface"
export {
NO_SCRIPT_RUN_ID,
type AppNode,
} from "./render-tree/AppNode.interface"
export { AppRoot } from "./render-tree/AppRoot"
export { BlockNode } from "./render-tree/BlockNode"
export { ElementNode } from "./render-tree/ElementNode"
export { TransientNode } from "./render-tree/TransientNode"
7 changes: 6 additions & 1 deletion frontend/lib/src/components/core/Block/RenderNodeVisitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { ReactElement } from "react"

import { BlockNode, ElementNode } from "~lib/AppNode"
import { BlockNode, ElementNode, TransientNode } from "~lib/AppNode"
import { AppNodeVisitor } from "~lib/render-tree/visitors/AppNodeVisitor.interface"
import { getElementId } from "~lib/util/utils"

Expand Down Expand Up @@ -82,6 +82,11 @@ export class RenderNodeVisitor
return renderer
}

visitTransientNode(_node: TransientNode): OptionalReactElement {
// Transient nodes are rendered outside of the context this visitor is used in
return null
}

visitElementNode(node: ElementNode): OptionalReactElement {
// Put node in childProps instead of passing as a node={node} prop in React to
// guarantee it doesn't get overwritten by {...childProps}.
Expand Down
2 changes: 1 addition & 1 deletion frontend/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import "@streamlit/utils"
// These imports are each exported specifically in order to minimize public apis.
export type { LibConfig } from "@streamlit/connection"
export { AppRoot, BlockNode, ElementNode } from "./AppNode"
export { AppRoot, BlockNode, ElementNode, TransientNode } from "./AppNode"
export {
ContainerContentsWrapper,
VerticalBlock,
Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/src/render-tree/BlockNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe("BlockNode", () => {
const mockVisitor = {
visitElementNode: vi.fn().mockReturnValue("element-result"),
visitBlockNode: vi.fn().mockReturnValue("block-result"),
visitTransientNode: vi.fn().mockReturnValue("transient-result"),
}

const result = node.accept(mockVisitor)
Expand All @@ -96,6 +97,7 @@ describe("BlockNode", () => {
const identityVisitor = {
visitElementNode: vi.fn(),
visitBlockNode: vi.fn().mockReturnValue(node),
visitTransientNode: vi.fn(),
}

const result = node.accept(identityVisitor)
Expand All @@ -108,6 +110,7 @@ describe("BlockNode", () => {
const transformVisitor = {
visitElementNode: vi.fn(),
visitBlockNode: vi.fn().mockReturnValue(block([text("transformed")])),
visitTransientNode: vi.fn(),
}

const result = originalNode.accept(transformVisitor)
Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/src/render-tree/ElementNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ describe("ElementNode.accept", () => {
const mockVisitor = {
visitElementNode: vi.fn().mockReturnValue("element-result"),
visitBlockNode: vi.fn().mockReturnValue("block-result"),
visitTransientNode: vi.fn().mockReturnValue("transient-result"),
}

const result = node.accept(mockVisitor)
Expand All @@ -440,6 +441,7 @@ describe("ElementNode.accept", () => {
const identityVisitor = {
visitElementNode: vi.fn().mockReturnValue(node),
visitBlockNode: vi.fn(),
visitTransientNode: vi.fn(),
}

const result = node.accept(identityVisitor)
Expand All @@ -452,6 +454,7 @@ describe("ElementNode.accept", () => {
const nullVisitor = {
visitElementNode: vi.fn().mockReturnValue(undefined),
visitBlockNode: vi.fn(),
visitTransientNode: vi.fn(),
}

const result = node.accept(nullVisitor)
Expand Down
87 changes: 87 additions & 0 deletions frontend/lib/src/render-tree/TransientNode.test.ts
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)
})
})
})
61 changes: 61 additions & 0 deletions frontend/lib/src/render-tree/TransientNode.ts
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
Expand Up @@ -16,8 +16,10 @@

import { BlockNode } from "~lib/render-tree/BlockNode"
import { ElementNode } from "~lib/render-tree/ElementNode"
import { TransientNode } from "~lib/render-tree/TransientNode"

export interface AppNodeVisitor<T> {
visitBlockNode(node: BlockNode): T
visitElementNode(node: ElementNode): T
visitTransientNode(node: TransientNode): T
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { AppNode, BlockNode, ElementNode } from "~lib/AppNode"
import { AppNode, BlockNode, ElementNode, TransientNode } from "~lib/AppNode"
import { AppNodeVisitor } from "~lib/render-tree/visitors/AppNodeVisitor.interface"

/**
Expand Down Expand Up @@ -138,4 +138,8 @@ export class ClearStaleNodeVisitor
}
return node.scriptRunId === this.currentScriptRunId ? node : undefined
}

visitTransientNode(_node: TransientNode): AppNode | undefined {
throw new Error("Method not implemented.")
}
}
30 changes: 28 additions & 2 deletions frontend/lib/src/render-tree/visitors/DebugVisitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@

import { Element, ForwardMsgMetadata } from "@streamlit/protobuf"

import { NO_SCRIPT_RUN_ID } from "~lib/render-tree/AppNode.interface"
import { ElementNode } from "~lib/render-tree/ElementNode"
import { ElementNode, NO_SCRIPT_RUN_ID, TransientNode } from "~lib/AppNode"
import { block, FAKE_SCRIPT_HASH, text } from "~lib/render-tree/test-utils"

import { DebugVisitor, MAX_HASH_LENGTH } from "./DebugVisitor"
Expand Down Expand Up @@ -130,3 +129,30 @@ describe("DebugVisitor.generateDebugString", () => {
expect(out.startsWith("X ├── BlockNode")).toBe(true)
})
})

describe("DebugVisitor.visitTransientNode", () => {
it("prints anchor and transient nodes with proper structure and truncated run id", () => {
const anchor = text("anchor")
const t1 = text("one")
const t2 = text("two")
const runId = "abcdef012345"

const transient = new TransientNode(runId, anchor, [t1, t2], 1)

const out = transient.accept(new DebugVisitor())

// Root line with truncated run id
expect(out.split("\n")[0]).toBe(
`└── TransientNode [2 transient] (run: ${runId.substring(0, MAX_HASH_LENGTH)})`
)

// Contains anchor section and its rendered element
expect(out).toContain("anchor:")
expect(out).toContain('ElementNode [text] "anchor"')

// Contains transient nodes section and both elements
expect(out).toContain("transient nodes:")
expect(out).toContain('ElementNode [text] "one"')
expect(out).toContain('ElementNode [text] "two"')
})
})
44 changes: 41 additions & 3 deletions frontend/lib/src/render-tree/visitors/DebugVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tree connector logic is incorrect. The anchor section always uses ├── (line 96), but if there are no transient nodes, the anchor should be the last child and use └── instead.

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
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`
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)
}
if (node.transientNodes.length > 0) {
result += `${childPrefix}└── transient nodes:\n`

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

node.transientNodes.forEach((transientNode, index) => {
const isLastTransient = index === node.transientNodes.length - 1
const transientConnector = isLastTransient ? "└── " : "├── "
const transientChildPrefix =
childPrefix + " " + (isLastTransient ? " " : "│ ")

result += `${childPrefix} ${transientConnector}:\n`
const transientVisitor = new DebugVisitor(transientChildPrefix, true)
result += transientNode.accept(transientVisitor)
})
}

return result
}

/**
* Static helper method to generate debug output for any AppNode.
*/
Expand Down
5 changes: 5 additions & 0 deletions frontend/lib/src/render-tree/visitors/ElementsSetVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Element } from "@streamlit/protobuf"
import { AppNode } from "~lib/render-tree/AppNode.interface"
import { BlockNode } from "~lib/render-tree/BlockNode"
import { ElementNode } from "~lib/render-tree/ElementNode"
import { TransientNode } from "~lib/render-tree/TransientNode"
import type { AppNodeVisitor } from "~lib/render-tree/visitors/AppNodeVisitor.interface"

/**
Expand Down Expand Up @@ -56,6 +57,10 @@ export class ElementsSetVisitor implements AppNodeVisitor<Set<Element>> {
return this.elements
}

visitTransientNode(_node: TransientNode): Set<Element> {
throw new Error("Method not implemented.")
}

/**
* Static convenience method to collect all elements from a node tree.
*/
Expand Down
Loading
Loading