mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 08:20:53 +00:00
## Summary Extract and harden subgraph node ID deduplication to prevent widget store key collisions when multiple subgraph copies share identical node IDs. ## Changes - **What**: Extract `deduplicateSubgraphNodeIds` from `LGraph.ts` into `utils/subgraphDeduplication.ts`, decomposed into focused helpers (`remapNodeIds`, `findNextAvailableId`, `patchSerialisedLinks`, `patchPromotedWidgets`, `patchProxyWidgets`). Clone inputs internally so caller data is never mutated. Add safety limit on ID search to prevent unbounded loops. Add `console.warn` on remapped IDs matching existing `ensureGlobalIdUniqueness` behavior. Add test fixture and 5 behavioral tests covering ID remapping, link patching, promoted widget patching, proxyWidget patching, and no-op when IDs are unique. ## Review Focus - The cloning strategy in `deduplicateSubgraphNodeIds` — it `structuredClone`s subgraphs and rootNodes, returning the clones. The caller uses `effectiveNodesData` to thread the patched root nodes through to node creation. - The `MAX_NODE_ID` safety limit (100M) — is this a reasonable ceiling? ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9510-fix-extract-and-harden-subgraph-node-ID-deduplication-31b6d73d365081f48c7de75e2bfc48b3) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
173 lines
3.9 KiB
TypeScript
173 lines
3.9 KiB
TypeScript
import type { SerialisableGraph } from '@/lib/litegraph/src/types/serialisation'
|
|
|
|
/**
|
|
* Workflow where lastNodeId is near the MAX_NODE_ID ceiling (100_000_000)
|
|
* and root node 100_000_000 reserves the only remaining candidate ID.
|
|
*
|
|
* Both subgraph definitions share node IDs [3, 8, 37]. When SubgraphB's
|
|
* duplicates need remapping, candidate 100_000_000 is already reserved,
|
|
* so the next candidate (100_000_001) exceeds MAX_NODE_ID and must throw.
|
|
*/
|
|
export const nodeIdSpaceExhausted = {
|
|
id: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
|
|
version: 1,
|
|
revision: 0,
|
|
state: {
|
|
lastNodeId: 99_999_999,
|
|
lastLinkId: 10,
|
|
lastGroupId: 0,
|
|
lastRerouteId: 0
|
|
},
|
|
nodes: [
|
|
{
|
|
id: 102,
|
|
type: '11111111-1111-4111-8111-111111111111',
|
|
pos: [0, 0],
|
|
size: [200, 100],
|
|
flags: {},
|
|
order: 0,
|
|
mode: 0,
|
|
properties: { proxyWidgets: [['3', 'seed']] }
|
|
},
|
|
{
|
|
id: 103,
|
|
type: '22222222-2222-4222-8222-222222222222',
|
|
pos: [300, 0],
|
|
size: [200, 100],
|
|
flags: {},
|
|
order: 1,
|
|
mode: 0,
|
|
properties: { proxyWidgets: [['8', 'prompt']] }
|
|
},
|
|
{
|
|
id: 100_000_000,
|
|
type: 'dummy',
|
|
pos: [600, 0],
|
|
size: [100, 50],
|
|
flags: {},
|
|
order: 2,
|
|
mode: 0
|
|
}
|
|
],
|
|
definitions: {
|
|
subgraphs: [
|
|
{
|
|
id: '11111111-1111-4111-8111-111111111111',
|
|
version: 1,
|
|
revision: 0,
|
|
state: {
|
|
lastNodeId: 0,
|
|
lastLinkId: 0,
|
|
lastGroupId: 0,
|
|
lastRerouteId: 0
|
|
},
|
|
name: 'SubgraphA',
|
|
config: {},
|
|
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
|
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
|
inputs: [],
|
|
outputs: [],
|
|
widgets: [{ id: 3, name: 'seed' }],
|
|
nodes: [
|
|
{
|
|
id: 3,
|
|
type: 'dummy',
|
|
pos: [0, 0],
|
|
size: [100, 50],
|
|
flags: {},
|
|
order: 0,
|
|
mode: 0
|
|
},
|
|
{
|
|
id: 8,
|
|
type: 'dummy',
|
|
pos: [0, 0],
|
|
size: [100, 50],
|
|
flags: {},
|
|
order: 1,
|
|
mode: 0
|
|
},
|
|
{
|
|
id: 37,
|
|
type: 'dummy',
|
|
pos: [0, 0],
|
|
size: [100, 50],
|
|
flags: {},
|
|
order: 2,
|
|
mode: 0
|
|
}
|
|
],
|
|
links: [
|
|
{
|
|
id: 1,
|
|
origin_id: 3,
|
|
origin_slot: 0,
|
|
target_id: 8,
|
|
target_slot: 0,
|
|
type: 'number'
|
|
}
|
|
],
|
|
groups: []
|
|
},
|
|
{
|
|
id: '22222222-2222-4222-8222-222222222222',
|
|
version: 1,
|
|
revision: 0,
|
|
state: {
|
|
lastNodeId: 0,
|
|
lastLinkId: 0,
|
|
lastGroupId: 0,
|
|
lastRerouteId: 0
|
|
},
|
|
name: 'SubgraphB',
|
|
config: {},
|
|
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
|
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
|
inputs: [],
|
|
outputs: [],
|
|
widgets: [{ id: 8, name: 'prompt' }],
|
|
nodes: [
|
|
{
|
|
id: 3,
|
|
type: 'dummy',
|
|
pos: [0, 0],
|
|
size: [100, 50],
|
|
flags: {},
|
|
order: 0,
|
|
mode: 0
|
|
},
|
|
{
|
|
id: 8,
|
|
type: 'dummy',
|
|
pos: [0, 0],
|
|
size: [100, 50],
|
|
flags: {},
|
|
order: 1,
|
|
mode: 0
|
|
},
|
|
{
|
|
id: 37,
|
|
type: 'dummy',
|
|
pos: [0, 0],
|
|
size: [100, 50],
|
|
flags: {},
|
|
order: 2,
|
|
mode: 0
|
|
}
|
|
],
|
|
links: [
|
|
{
|
|
id: 2,
|
|
origin_id: 3,
|
|
origin_slot: 0,
|
|
target_id: 37,
|
|
target_slot: 0,
|
|
type: 'string'
|
|
}
|
|
],
|
|
groups: []
|
|
}
|
|
]
|
|
}
|
|
} as const satisfies SerialisableGraph
|