mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 13:32:11 +00:00
fix: prune ChangeTracker output cache so undo cannot resurrect stale entries
The ChangeTracker keeps its own per-workflow nodeOutputs cache that is populated by the 'executed' event and re-applied via restoreOutputs() during workflow load. Without pruning it on node removal, undo would clear app.nodeOutputs through the new onNodeRemoved hook only to have the same entry written back from the tracker's stale cache. Hook now also derives the execution id for the removed node and deletes it from the active workflow's tracker cache, recursing through subgraph contents.
This commit is contained in:
@@ -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<string, unknown> = {
|
||||
[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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user