diff --git a/src/composables/graph/useNodeOutputClearingHooks.test.ts b/src/composables/graph/useNodeOutputClearingHooks.test.ts index f66d5933f1..be010cd5e8 100644 --- a/src/composables/graph/useNodeOutputClearingHooks.test.ts +++ b/src/composables/graph/useNodeOutputClearingHooks.test.ts @@ -10,6 +10,7 @@ import { } from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers' import { app } from '@/scripts/app' import { useNodeOutputStore } from '@/stores/nodeOutputStore' +import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' function seedOutputForLocator(locatorId: string) { app.nodeOutputs[locatorId] = { @@ -139,6 +140,29 @@ describe('installNodeOutputClearingHooks', () => { expect(app.nodeOutputs[interiorLocator]).toBeUndefined() }) + it('also prunes the active workflow change tracker output cache so undo cannot resurrect the entry', () => { + const graph = new LGraph() + vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph) + + const node = new LGraphNode('LoadImage') + graph.add(node) + const locator = String(node.id) + seedOutputForLocator(locator) + + const trackerCache: Record = { + [locator]: { images: [{ filename: 'preview.png' }] } + } + vi.spyOn(useWorkflowStore(), 'activeWorkflow', 'get').mockReturnValue({ + changeTracker: { nodeOutputs: trackerCache } + } as never) + + installNodeOutputClearingHooks(graph) + graph.remove(node) + + expect(app.nodeOutputs[locator]).toBeUndefined() + expect(trackerCache[locator]).toBeUndefined() + }) + it('does not throw when the removal hook fires for an already-cleared node', () => { const graph = new LGraph() vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph) diff --git a/src/composables/graph/useNodeOutputClearingHooks.ts b/src/composables/graph/useNodeOutputClearingHooks.ts index 2ad6abbb3b..0042b08c85 100644 --- a/src/composables/graph/useNodeOutputClearingHooks.ts +++ b/src/composables/graph/useNodeOutputClearingHooks.ts @@ -2,18 +2,30 @@ import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph' import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph' import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import { app } from '@/scripts/app' import { useNodeOutputStore } from '@/stores/nodeOutputStore' +import { getExecutionIdForNodeInGraph } from '@/utils/graphTraversalUtil' import { isSubgraph } from '@/utils/typeGuardUtil' -function clearInteriorOutputs(subgraphNode: SubgraphNode) { +function dropTrackerCacheEntry(execId: string) { + const tracked = useWorkflowStore().activeWorkflow?.changeTracker?.nodeOutputs + if (tracked) delete tracked[execId] +} + +function clearInteriorOutputs( + subgraphNode: SubgraphNode, + execIdPrefix: string +) { const subgraph: Subgraph | undefined = subgraphNode.subgraph if (!subgraph) return const store = useNodeOutputStore() for (const interior of subgraph.nodes) { store.removeOutputsByLocatorId(`${subgraph.id}:${interior.id}`) + const interiorExecId = `${execIdPrefix}:${interior.id}` + dropTrackerCacheEntry(interiorExecId) if (interior.isSubgraphNode()) { - clearInteriorOutputs(interior) + clearInteriorOutputs(interior, interiorExecId) } } } @@ -29,8 +41,13 @@ export function installNodeOutputClearingHooks(graph: LGraph): () => void { : String(node.id) store.removeOutputsByLocatorId(locatorId) + const execId = app.rootGraph + ? getExecutionIdForNodeInGraph(app.rootGraph, graph, node.id) + : String(node.id) + dropTrackerCacheEntry(execId) + if (node.isSubgraphNode()) { - clearInteriorOutputs(node) + clearInteriorOutputs(node, execId) } originalOnNodeRemoved?.call(this, node)