Files
ComfyUI_frontend/src/lib/litegraph/src/__fixtures__/nodeIdSpaceExhausted.ts
Alexander Brown 0b73285ca1 fix: extract and harden subgraph node ID deduplication (#9510)
## 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>
2026-03-06 21:56:56 +00:00

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