mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 07:00:23 +00:00
fix: make SubgraphNode.graph nullable to allow proper cleanup (#8180)
## Summary Fix `SubgraphNode.graph` property to match `LGraphNode` lifecycle contract. Previously declared as `override readonly graph` via constructor parameter promotion, which prevented `LGraph.remove()` from setting `node.graph = null`. ## Changes - Remove `readonly` from `SubgraphNode.graph` constructor parameter - Add `override graph: GraphOrSubgraph | null` as class property - Add `NullGraphError` guard in `rootGraph` getter with node ID for debugging - Add null guards in `ExecutableNodeDTO.resolveInput` and `imagePreviewStore.revokeSubgraphPreviews` - Add test verifying `rootGraph` throws after node removal ## Testing - Existing subgraph tests pass - New test confirms `NullGraphError` is thrown when accessing `rootGraph` on removed node --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -31,7 +31,7 @@ function setupSubgraph(
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
subgraphNode._internalConfigureAfterSlots()
|
||||
const graph = subgraphNode.graph
|
||||
const graph = subgraphNode.graph!
|
||||
graph.add(subgraphNode)
|
||||
const innerNodes = []
|
||||
for (let i = 0; i < innerNodeCount; i++) {
|
||||
|
||||
@@ -194,6 +194,10 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
if (!subgraphNode.graph)
|
||||
throw new NullGraphError(
|
||||
`SubgraphNode ${subgraphNode.id} has no graph during input resolution`
|
||||
)
|
||||
const outerLink = subgraphNode.graph.getLink(linkId)
|
||||
if (!outerLink)
|
||||
throw new InvalidLinkError(
|
||||
|
||||
@@ -45,7 +45,7 @@ describe.skip('SubgraphConversion', () => {
|
||||
it('Should keep interior nodes and links', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
const graph = subgraphNode.graph!
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const node1 = createNode(subgraph, [], ['number'])
|
||||
@@ -63,7 +63,7 @@ describe.skip('SubgraphConversion', () => {
|
||||
outputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
const graph = subgraphNode.graph!
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const innerNode1 = createNode(subgraph, [], ['number'])
|
||||
@@ -86,7 +86,7 @@ describe.skip('SubgraphConversion', () => {
|
||||
outputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
const graph = subgraphNode.graph!
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner = createNode(subgraph, [], ['number'])
|
||||
@@ -117,7 +117,7 @@ describe.skip('SubgraphConversion', () => {
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
const graph = subgraphNode.graph!
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner = createNode(subgraph, [], ['number', 'number'])
|
||||
@@ -159,7 +159,7 @@ describe.skip('SubgraphConversion', () => {
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
const graph = subgraphNode.graph!
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner1 = createNode(subgraph, ['number', 'number'])
|
||||
|
||||
@@ -54,11 +54,23 @@ describe.skip('SubgraphNode Construction', () => {
|
||||
it('should maintain reference to root graph', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const parentGraph = subgraphNode.graph
|
||||
const parentGraph = subgraphNode.graph!
|
||||
|
||||
expect(subgraphNode.rootGraph).toBe(parentGraph.rootGraph)
|
||||
})
|
||||
|
||||
it('should throw NullGraphError when accessing rootGraph after removal', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const parentGraph = subgraphNode.graph!
|
||||
parentGraph.add(subgraphNode)
|
||||
|
||||
parentGraph.remove(subgraphNode)
|
||||
|
||||
expect(() => subgraphNode.rootGraph).toThrow()
|
||||
expect(subgraphNode.graph).toBeNull()
|
||||
})
|
||||
|
||||
subgraphTest(
|
||||
'should synchronize slots with subgraph definition',
|
||||
({ subgraphWithNode }) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { DrawTitleBoxOptions } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { ResolvedConnection } from '@/lib/litegraph/src/LLink'
|
||||
import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError'
|
||||
import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError'
|
||||
import type {
|
||||
ISubgraphInput,
|
||||
@@ -47,8 +48,11 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
override readonly type: UUID
|
||||
override readonly isVirtualNode = true as const
|
||||
override graph: GraphOrSubgraph | null
|
||||
|
||||
get rootGraph(): LGraph {
|
||||
if (!this.graph)
|
||||
throw new NullGraphError(`SubgraphNode ${this.id} has no graph`)
|
||||
return this.graph.rootGraph
|
||||
}
|
||||
|
||||
@@ -67,12 +71,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
constructor(
|
||||
/** The (sub)graph that contains this subgraph instance. */
|
||||
override readonly graph: GraphOrSubgraph,
|
||||
graph: GraphOrSubgraph,
|
||||
/** The definition of this subgraph; how its nodes are configured, etc. */
|
||||
readonly subgraph: Subgraph,
|
||||
instanceData: ExportedSubgraphInstance
|
||||
) {
|
||||
super(subgraph.name, subgraph.id)
|
||||
this.graph = graph
|
||||
|
||||
// Update this node when the subgraph input / output slots are changed
|
||||
const subgraphEvents = this.subgraph.events
|
||||
@@ -496,7 +501,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const subgraphInstanceIdPath = [...subgraphNodePath, this.id]
|
||||
|
||||
// Store the subgraph node DTO
|
||||
const parentSubgraphNode = this.graph.rootGraph
|
||||
const parentSubgraphNode = this.rootGraph
|
||||
.resolveSubgraphIdPath(subgraphNodePath)
|
||||
.at(-1)
|
||||
const subgraphNodeDto = new ExecutableNodeDTO(
|
||||
|
||||
@@ -299,9 +299,10 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
* Does not recurse to contents of nested subgraphs.
|
||||
*/
|
||||
function revokeSubgraphPreviews(subgraphNode: SubgraphNode) {
|
||||
const graphId = subgraphNode.graph.isRootGraph
|
||||
? ''
|
||||
: subgraphNode.graph.id + ':'
|
||||
const { graph } = subgraphNode
|
||||
if (!graph) return
|
||||
|
||||
const graphId = graph.isRootGraph ? '' : graph.id + ':'
|
||||
revokePreviewsByLocatorId(graphId + subgraphNode.id)
|
||||
for (const node of subgraphNode.subgraph.nodes) {
|
||||
revokePreviewsByLocatorId(subgraphNode.subgraph.id + node.id)
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('useSubgraphStore', () => {
|
||||
//mock canvas to provide a minimal subgraphNode
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
const graph = subgraphNode.graph!
|
||||
graph.add(subgraphNode)
|
||||
vi.mocked(comfyApp.canvas).selectedItems = new Set([subgraphNode])
|
||||
vi.mocked(comfyApp.canvas)._serializeItems = vi.fn(() => {
|
||||
|
||||
Reference in New Issue
Block a user