mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
refactor: extract PromotionEntryResolver from SubgraphNode
Move promotion entry resolution logic (linked/fallback merging, alias pruning, persistence decisions) into a standalone module to reduce SubgraphNode method count and improve readability. - Extract resolvePromotionEntries, buildLinkedReconcileEntries, buildDisplayNameByViewKey, makePromotionViewKey into PromotionEntryResolver.ts - Remove 12 private methods (~280 lines) from SubgraphNode - Eliminate _makePromotionEntryKey wrapper (use store export directly) - Add 10 characterization tests pinning promotion entry resolution behavior before refactoring
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
resetSubgraphFixtureState,
|
||||
setupComplexPromotionFixture
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
import * as PromotionEntryResolver from '@/lib/litegraph/src/subgraph/PromotionEntryResolver'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({})
|
||||
@@ -1451,29 +1452,16 @@ describe('widgets getter caching', () => {
|
||||
const fakeCanvas = { frame: 12 } as Pick<LGraphCanvas, 'frame'>
|
||||
subgraphNode.rootGraph.primaryCanvas = fakeCanvas as LGraphCanvas
|
||||
|
||||
const reconcileSpy = vi.spyOn(
|
||||
fromAny<
|
||||
{
|
||||
_buildPromotionReconcileState: (
|
||||
entries: Array<{ sourceNodeId: string; sourceWidgetName: string }>,
|
||||
linkedEntries: Array<{
|
||||
inputName: string
|
||||
inputKey: string
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
}>
|
||||
) => unknown
|
||||
},
|
||||
unknown
|
||||
>(subgraphNode),
|
||||
'_buildPromotionReconcileState'
|
||||
const resolveSpy = vi.spyOn(
|
||||
PromotionEntryResolver,
|
||||
'resolvePromotionEntries'
|
||||
)
|
||||
|
||||
void subgraphNode.widgets
|
||||
void subgraphNode.widgets
|
||||
void subgraphNode.widgets
|
||||
|
||||
expect(reconcileSpy).toHaveBeenCalledTimes(1)
|
||||
expect(resolveSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('does not re-run reconciliation when only canvas frame advances', () => {
|
||||
@@ -1484,29 +1472,17 @@ describe('widgets getter caching', () => {
|
||||
const fakeCanvas = { frame: 24 } as Pick<LGraphCanvas, 'frame'>
|
||||
subgraphNode.rootGraph.primaryCanvas = fakeCanvas as LGraphCanvas
|
||||
|
||||
const reconcileSpy = vi.spyOn(
|
||||
fromAny<
|
||||
{
|
||||
_buildPromotionReconcileState: (
|
||||
entries: Array<{ sourceNodeId: string; sourceWidgetName: string }>,
|
||||
linkedEntries: Array<{
|
||||
inputName: string
|
||||
inputKey: string
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
}>
|
||||
) => unknown
|
||||
},
|
||||
unknown
|
||||
>(subgraphNode),
|
||||
'_buildPromotionReconcileState'
|
||||
const resolveSpy = vi.spyOn(
|
||||
PromotionEntryResolver,
|
||||
'resolvePromotionEntries'
|
||||
)
|
||||
|
||||
void subgraphNode.widgets
|
||||
const callsAfterFirst = resolveSpy.mock.calls.length
|
||||
fakeCanvas.frame += 1
|
||||
void subgraphNode.widgets
|
||||
|
||||
expect(reconcileSpy).toHaveBeenCalledTimes(1)
|
||||
expect(resolveSpy.mock.calls.length - callsAfterFirst).toBe(0)
|
||||
})
|
||||
|
||||
test('does not re-resolve linked entries when linked input state is unchanged', () => {
|
||||
@@ -2441,3 +2417,387 @@ describe('DOM widget promotion', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('promotion entry resolution invariants', () => {
|
||||
test('syncPromotions preserves entry order: linked before fallback', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 200 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const linkedNode = new LGraphNode('LinkedNode')
|
||||
const linkedInput = linkedNode.addInput('string_a', '*')
|
||||
linkedNode.addWidget('text', 'string_a', 'linked', () => {})
|
||||
linkedInput.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNode)
|
||||
|
||||
const independentNode = new LGraphNode('IndependentNode')
|
||||
independentNode.addWidget('text', 'indep_widget', 'independent', () => {})
|
||||
subgraph.add(independentNode)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(linkedInput, linkedNode)
|
||||
|
||||
// Store entries in reverse order: independent first, then linked
|
||||
usePromotionStore().setPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id,
|
||||
[
|
||||
{
|
||||
sourceNodeId: String(independentNode.id),
|
||||
sourceWidgetName: 'indep_widget'
|
||||
},
|
||||
{ sourceNodeId: String(linkedNode.id), sourceWidgetName: 'string_a' }
|
||||
]
|
||||
)
|
||||
|
||||
callSyncPromotions(subgraphNode)
|
||||
|
||||
const promotions = usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
expect(promotions[0]).toStrictEqual({
|
||||
sourceNodeId: String(linkedNode.id),
|
||||
sourceWidgetName: 'string_a'
|
||||
})
|
||||
expect(promotions[1]).toStrictEqual({
|
||||
sourceNodeId: String(independentNode.id),
|
||||
sourceWidgetName: 'indep_widget'
|
||||
})
|
||||
})
|
||||
|
||||
test('syncPromotions produces stable state after convergence', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 201 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const linkedNode = new LGraphNode('LinkedNode')
|
||||
const linkedInput = linkedNode.addInput('string_a', '*')
|
||||
linkedNode.addWidget('text', 'string_a', 'val', () => {})
|
||||
linkedInput.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNode)
|
||||
subgraph.inputNode.slots[0].connect(linkedInput, linkedNode)
|
||||
|
||||
setPromotions(subgraphNode, [[String(linkedNode.id), 'string_a']])
|
||||
|
||||
// Multiple syncs should converge to same state
|
||||
callSyncPromotions(subgraphNode)
|
||||
const afterFirst = [
|
||||
...usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
]
|
||||
|
||||
callSyncPromotions(subgraphNode)
|
||||
const afterSecond = usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
|
||||
expect(afterSecond).toStrictEqual(afterFirst)
|
||||
|
||||
callSyncPromotions(subgraphNode)
|
||||
const afterThird = usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
|
||||
expect(afterThird).toStrictEqual(afterFirst)
|
||||
})
|
||||
|
||||
test('syncPromotions with disambiguatingSourceNodeId preserves it through round-trip', () => {
|
||||
const { subgraphNodeB } = createTwoLevelNestedSubgraph()
|
||||
|
||||
// Access widgets to trigger initial promotion setup
|
||||
const widgets = promotedWidgets(subgraphNodeB)
|
||||
expect(widgets).toHaveLength(1)
|
||||
|
||||
// Get the initial promotions (linked resolution may produce disambiguating IDs)
|
||||
const promotionsBefore = usePromotionStore().getPromotions(
|
||||
subgraphNodeB.rootGraph.id,
|
||||
subgraphNodeB.id
|
||||
)
|
||||
|
||||
callSyncPromotions(subgraphNodeB)
|
||||
|
||||
const promotionsAfter = usePromotionStore().getPromotions(
|
||||
subgraphNodeB.rootGraph.id,
|
||||
subgraphNodeB.id
|
||||
)
|
||||
|
||||
// Verify disambiguation info survives sync
|
||||
for (const entry of promotionsBefore) {
|
||||
const match = promotionsAfter.find(
|
||||
(e) =>
|
||||
e.sourceNodeId === entry.sourceNodeId &&
|
||||
e.sourceWidgetName === entry.sourceWidgetName
|
||||
)
|
||||
expect(match).toBeDefined()
|
||||
expect(match?.disambiguatingSourceNodeId).toBe(
|
||||
entry.disambiguatingSourceNodeId
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('widgets getter returns linked views with labels from input labels', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: 'Custom Label A', type: '*' },
|
||||
{ name: 'Custom Label B', type: '*' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 202 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const nodeA = new LGraphNode('NodeA')
|
||||
const inputA = nodeA.addInput('Custom Label A', '*')
|
||||
nodeA.addWidget('text', 'widgetA', 'a', () => {})
|
||||
inputA.widget = { name: 'widgetA' }
|
||||
subgraph.add(nodeA)
|
||||
|
||||
const nodeB = new LGraphNode('NodeB')
|
||||
const inputB = nodeB.addInput('Custom Label B', '*')
|
||||
nodeB.addWidget('text', 'widgetB', 'b', () => {})
|
||||
inputB.widget = { name: 'widgetB' }
|
||||
subgraph.add(nodeB)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(inputA, nodeA)
|
||||
subgraph.inputNode.slots[1].connect(inputB, nodeB)
|
||||
|
||||
const widgets = promotedWidgets(subgraphNode)
|
||||
expect(widgets).toHaveLength(2)
|
||||
expect(widgets[0].label).toBe('Custom Label A')
|
||||
expect(widgets[1].label).toBe('Custom Label B')
|
||||
})
|
||||
|
||||
test('widgets getter deduplicates by view key, not by source entry key', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: 'input_1', type: '*' },
|
||||
{ name: 'input_2', type: '*' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 203 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const sharedNode = new LGraphNode('SharedNode')
|
||||
const sharedInput1 = sharedNode.addInput('input_1', '*')
|
||||
const sharedInput2 = sharedNode.addInput('input_2', '*')
|
||||
sharedNode.addWidget('number', 'value', 1, () => {})
|
||||
sharedInput1.widget = { name: 'value' }
|
||||
sharedInput2.widget = { name: 'value' }
|
||||
subgraph.add(sharedNode)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(sharedInput1, sharedNode)
|
||||
subgraph.inputNode.slots[1].connect(sharedInput2, sharedNode)
|
||||
|
||||
const widgets = promotedWidgets(subgraphNode)
|
||||
// Two inputs linked to same concrete widget produce two separate views
|
||||
expect(widgets).toHaveLength(2)
|
||||
expect(widgets[0].sourceNodeId).toBe(String(sharedNode.id))
|
||||
expect(widgets[1].sourceNodeId).toBe(String(sharedNode.id))
|
||||
})
|
||||
|
||||
test('widgets getter excludes connected-but-not-linked entries from fallback', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 204 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const connectedNode = new LGraphNode('ConnectedNode')
|
||||
const connectedInput = connectedNode.addInput('string_a', '*')
|
||||
connectedNode.addWidget('text', 'string_a', 'val', () => {})
|
||||
connectedInput.widget = { name: 'string_a' }
|
||||
subgraph.add(connectedNode)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(connectedInput, connectedNode)
|
||||
|
||||
// Also add an independent store entry referencing the same widget
|
||||
usePromotionStore().promote(subgraphNode.rootGraph.id, subgraphNode.id, {
|
||||
sourceNodeId: String(connectedNode.id),
|
||||
sourceWidgetName: 'string_a'
|
||||
})
|
||||
|
||||
const widgets = promotedWidgets(subgraphNode)
|
||||
// Connected widget appears as linked view, independent duplicate is excluded
|
||||
expect(widgets).toHaveLength(1)
|
||||
expect(widgets[0].sourceNodeId).toBe(String(connectedNode.id))
|
||||
})
|
||||
|
||||
test('full linked coverage with valid fallback source widgets does NOT prune fallback', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 205 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const linkedNode = new LGraphNode('LinkedNode')
|
||||
const linkedInput = linkedNode.addInput('string_a', '*')
|
||||
linkedNode.addWidget('text', 'string_a', 'linked', () => {})
|
||||
linkedInput.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNode)
|
||||
|
||||
const independentNode = new LGraphNode('IndependentNode')
|
||||
independentNode.addWidget('text', 'other_widget', 'independent', () => {})
|
||||
subgraph.add(independentNode)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(linkedInput, linkedNode)
|
||||
|
||||
usePromotionStore().setPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id,
|
||||
[
|
||||
{ sourceNodeId: String(linkedNode.id), sourceWidgetName: 'string_a' },
|
||||
{
|
||||
sourceNodeId: String(independentNode.id),
|
||||
sourceWidgetName: 'other_widget'
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
callSyncPromotions(subgraphNode)
|
||||
|
||||
const promotions = usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
// Fallback entry has valid source widget → not pruned despite full linked coverage
|
||||
expect(promotions).toHaveLength(2)
|
||||
expect(
|
||||
promotions.some(
|
||||
(e) =>
|
||||
e.sourceNodeId === String(independentNode.id) &&
|
||||
e.sourceWidgetName === 'other_widget'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('alias pruning removes fallback entries whose concrete resolution matches a linked entry', () => {
|
||||
const nestedSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
|
||||
const concreteNode = new LGraphNode('ConcreteNode')
|
||||
const concreteInput = concreteNode.addInput('string_a', '*')
|
||||
concreteNode.addWidget('text', 'string_a', 'value', () => {})
|
||||
concreteInput.widget = { name: 'string_a' }
|
||||
nestedSubgraph.add(concreteNode)
|
||||
nestedSubgraph.inputNode.slots[0].connect(concreteInput, concreteNode)
|
||||
|
||||
const hostSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
|
||||
const activeAlias = createTestSubgraphNode(nestedSubgraph, { id: 210 })
|
||||
const staleAlias = createTestSubgraphNode(nestedSubgraph, { id: 211 })
|
||||
hostSubgraph.add(activeAlias)
|
||||
hostSubgraph.add(staleAlias)
|
||||
|
||||
activeAlias._internalConfigureAfterSlots()
|
||||
staleAlias._internalConfigureAfterSlots()
|
||||
hostSubgraph.inputNode.slots[0].connect(activeAlias.inputs[0], activeAlias)
|
||||
|
||||
const hostNode = createTestSubgraphNode(hostSubgraph, { id: 212 })
|
||||
hostNode.graph?.add(hostNode)
|
||||
|
||||
usePromotionStore().setPromotions(hostNode.rootGraph.id, hostNode.id, [
|
||||
{ sourceNodeId: String(activeAlias.id), sourceWidgetName: 'string_a' },
|
||||
{ sourceNodeId: String(staleAlias.id), sourceWidgetName: 'string_a' }
|
||||
])
|
||||
|
||||
callSyncPromotions(hostNode)
|
||||
|
||||
const promotions = usePromotionStore().getPromotions(
|
||||
hostNode.rootGraph.id,
|
||||
hostNode.id
|
||||
)
|
||||
// Stale alias resolves to same concrete as linked entry → pruned
|
||||
expect(promotions).toHaveLength(1)
|
||||
expect(promotions[0].sourceNodeId).toBe(String(activeAlias.id))
|
||||
})
|
||||
|
||||
test('alias pruning keeps fallback entries when concrete resolution fails', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 213 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const linkedNode = new LGraphNode('LinkedNode')
|
||||
const linkedInput = linkedNode.addInput('string_a', '*')
|
||||
linkedNode.addWidget('text', 'string_a', 'linked', () => {})
|
||||
linkedInput.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNode)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(linkedInput, linkedNode)
|
||||
|
||||
// Add an entry referencing a non-existent node (resolution will fail)
|
||||
usePromotionStore().setPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id,
|
||||
[
|
||||
{ sourceNodeId: String(linkedNode.id), sourceWidgetName: 'string_a' },
|
||||
{ sourceNodeId: '9999', sourceWidgetName: 'unknown_widget' }
|
||||
]
|
||||
)
|
||||
|
||||
callSyncPromotions(subgraphNode)
|
||||
|
||||
const promotions = usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
// Entry for non-existent node is filtered by _getFallbackStoredEntries
|
||||
// because the node doesn't exist, so _pruneStaleAliasFallbackEntries
|
||||
// removes it. Only the linked entry survives.
|
||||
expect(promotions).toStrictEqual([
|
||||
{ sourceNodeId: String(linkedNode.id), sourceWidgetName: 'string_a' }
|
||||
])
|
||||
})
|
||||
|
||||
test('widgets getter and syncPromotions agree on entry count for mixed scenario', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 214 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const linkedNode = new LGraphNode('LinkedNode')
|
||||
const linkedInput = linkedNode.addInput('string_a', '*')
|
||||
linkedNode.addWidget('text', 'string_a', 'linked', () => {})
|
||||
linkedInput.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNode)
|
||||
|
||||
const independentNode = new LGraphNode('IndependentNode')
|
||||
independentNode.addWidget('text', 'indep_widget', 'independent', () => {})
|
||||
subgraph.add(independentNode)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(linkedInput, linkedNode)
|
||||
|
||||
usePromotionStore().setPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id,
|
||||
[
|
||||
{ sourceNodeId: String(linkedNode.id), sourceWidgetName: 'string_a' },
|
||||
{
|
||||
sourceNodeId: String(independentNode.id),
|
||||
sourceWidgetName: 'indep_widget'
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
callSyncPromotions(subgraphNode)
|
||||
|
||||
const promotions = usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
const widgets = promotedWidgets(subgraphNode)
|
||||
expect(widgets).toHaveLength(promotions.length)
|
||||
})
|
||||
})
|
||||
|
||||
228
src/lib/litegraph/src/subgraph/PromotionEntryResolver.ts
Normal file
228
src/lib/litegraph/src/subgraph/PromotionEntryResolver.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { resolveConcretePromotedWidget } from '@/core/graph/subgraph/resolveConcretePromotedWidget'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import { makePromotionEntryKey } from '@/stores/promotionStore'
|
||||
|
||||
export type LinkedPromotionEntry = PromotedWidgetSource & {
|
||||
inputName: string
|
||||
inputKey: string
|
||||
slotName: string
|
||||
}
|
||||
|
||||
export interface ResolvedPromotionEntries {
|
||||
linkedPromotionEntries: PromotedWidgetSource[]
|
||||
fallbackStoredEntries: PromotedWidgetSource[]
|
||||
shouldPersistLinkedOnly: boolean
|
||||
}
|
||||
|
||||
export function resolvePromotionEntries(
|
||||
storeEntries: PromotedWidgetSource[],
|
||||
linkedEntries: LinkedPromotionEntry[],
|
||||
connectedEntryKeys: Set<string>,
|
||||
inputCount: number,
|
||||
subgraph: Subgraph,
|
||||
subgraphNode: SubgraphNode
|
||||
): ResolvedPromotionEntries {
|
||||
const linkedPromotionEntries = toPromotionEntries(linkedEntries)
|
||||
|
||||
const excludedEntryKeys = new Set(
|
||||
linkedPromotionEntries.map((e) => makePromotionEntryKey(e))
|
||||
)
|
||||
for (const key of connectedEntryKeys) {
|
||||
excludedEntryKeys.add(key)
|
||||
}
|
||||
|
||||
const prePruneFallback = storeEntries.filter(
|
||||
(e) => !excludedEntryKeys.has(makePromotionEntryKey(e))
|
||||
)
|
||||
|
||||
const fallbackStoredEntries = pruneStaleFallbackAliases(
|
||||
prePruneFallback,
|
||||
linkedPromotionEntries,
|
||||
subgraph,
|
||||
subgraphNode
|
||||
)
|
||||
|
||||
const persistLinkedOnly = shouldPersistLinkedOnly(
|
||||
linkedEntries,
|
||||
fallbackStoredEntries,
|
||||
inputCount,
|
||||
subgraph
|
||||
)
|
||||
|
||||
return {
|
||||
linkedPromotionEntries,
|
||||
fallbackStoredEntries,
|
||||
shouldPersistLinkedOnly: persistLinkedOnly
|
||||
}
|
||||
}
|
||||
|
||||
function toPromotionEntries(
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): PromotedWidgetSource[] {
|
||||
return linkedEntries.map(
|
||||
({ sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId }) => ({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
...(disambiguatingSourceNodeId && { disambiguatingSourceNodeId })
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function shouldPersistLinkedOnly(
|
||||
linkedEntries: LinkedPromotionEntry[],
|
||||
fallbackStoredEntries: PromotedWidgetSource[],
|
||||
inputCount: number,
|
||||
subgraph: Subgraph
|
||||
): boolean {
|
||||
if (!(inputCount > 0 && linkedEntries.length === inputCount)) return false
|
||||
|
||||
const linkedEntryKeys = new Set(
|
||||
linkedEntries.map((e) =>
|
||||
makePromotionEntryKey({
|
||||
sourceNodeId: e.sourceNodeId,
|
||||
sourceWidgetName: e.sourceWidgetName
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const linkedWidgetNames = new Set(
|
||||
linkedEntries.map((e) => e.sourceWidgetName)
|
||||
)
|
||||
|
||||
const hasFallbackToKeep = fallbackStoredEntries.some((entry) => {
|
||||
const sourceNode = subgraph.getNodeById(entry.sourceNodeId)
|
||||
if (!sourceNode) return linkedWidgetNames.has(entry.sourceWidgetName)
|
||||
|
||||
const hasSourceWidget =
|
||||
sourceNode.widgets?.some(
|
||||
(widget) => widget.name === entry.sourceWidgetName
|
||||
) === true
|
||||
if (hasSourceWidget) return true
|
||||
|
||||
return linkedEntryKeys.has(
|
||||
makePromotionEntryKey({
|
||||
sourceNodeId: entry.sourceNodeId,
|
||||
sourceWidgetName: entry.sourceWidgetName
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return !hasFallbackToKeep
|
||||
}
|
||||
|
||||
function pruneStaleFallbackAliases(
|
||||
fallbackEntries: PromotedWidgetSource[],
|
||||
linkedPromotionEntries: PromotedWidgetSource[],
|
||||
subgraph: Subgraph,
|
||||
subgraphNode: SubgraphNode
|
||||
): PromotedWidgetSource[] {
|
||||
if (fallbackEntries.length === 0 || linkedPromotionEntries.length === 0)
|
||||
return fallbackEntries
|
||||
|
||||
const linkedConcreteKeys = new Set(
|
||||
linkedPromotionEntries
|
||||
.map((e) => resolveConcreteEntryKey(e, subgraphNode))
|
||||
.filter((key): key is string => key !== undefined)
|
||||
)
|
||||
if (linkedConcreteKeys.size === 0) return fallbackEntries
|
||||
|
||||
const pruned: PromotedWidgetSource[] = []
|
||||
for (const entry of fallbackEntries) {
|
||||
if (!subgraph.getNodeById(entry.sourceNodeId)) continue
|
||||
|
||||
const concreteKey = resolveConcreteEntryKey(entry, subgraphNode)
|
||||
if (concreteKey && linkedConcreteKeys.has(concreteKey)) continue
|
||||
|
||||
pruned.push(entry)
|
||||
}
|
||||
|
||||
return pruned
|
||||
}
|
||||
|
||||
function resolveConcreteEntryKey(
|
||||
entry: PromotedWidgetSource,
|
||||
subgraphNode: SubgraphNode
|
||||
): string | undefined {
|
||||
const result = resolveConcretePromotedWidget(
|
||||
subgraphNode,
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
)
|
||||
if (result.status !== 'resolved') return undefined
|
||||
|
||||
return makePromotionEntryKey({
|
||||
sourceNodeId: String(result.resolved.node.id),
|
||||
sourceWidgetName: result.resolved.widget.name
|
||||
})
|
||||
}
|
||||
|
||||
export function buildLinkedReconcileEntries(
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): Array<{
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
viewKey: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
slotName: string
|
||||
}> {
|
||||
return linkedEntries.map(
|
||||
({
|
||||
inputKey,
|
||||
inputName,
|
||||
slotName,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
}) => ({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
slotName,
|
||||
disambiguatingSourceNodeId,
|
||||
viewKey: makePromotionViewKey(
|
||||
inputKey,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
inputName,
|
||||
disambiguatingSourceNodeId
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function buildDisplayNameByViewKey(
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): Map<string, string> {
|
||||
return new Map(
|
||||
linkedEntries.map((entry) => [
|
||||
makePromotionViewKey(
|
||||
entry.inputKey,
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.inputName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
),
|
||||
entry.inputName
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
export function makePromotionViewKey(
|
||||
inputKey: string,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
inputName = '',
|
||||
disambiguatingSourceNodeId?: string
|
||||
): string {
|
||||
return disambiguatingSourceNodeId
|
||||
? JSON.stringify([
|
||||
inputKey,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
inputName,
|
||||
disambiguatingSourceNodeId
|
||||
])
|
||||
: JSON.stringify([inputKey, sourceNodeId, sourceWidgetName, inputName])
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
* IO synchronization, and edge cases.
|
||||
*/
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
createTestSubgraphNode,
|
||||
resetSubgraphFixtureState
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
import { makePromotionViewKey } from './PromotionEntryResolver'
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
@@ -929,34 +929,8 @@ describe('Nested SubgraphNode duplicate input prevention', () => {
|
||||
|
||||
describe('SubgraphNode promotion view keys', () => {
|
||||
it('distinguishes tuples that differ only by colon placement', () => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const nodeWithKeyBuilder = fromAny<
|
||||
{
|
||||
_makePromotionViewKey: (
|
||||
inputKey: string,
|
||||
interiorNodeId: string,
|
||||
widgetName: string,
|
||||
inputName?: string
|
||||
) => string
|
||||
},
|
||||
unknown
|
||||
>(subgraphNode)
|
||||
|
||||
const firstKey = nodeWithKeyBuilder._makePromotionViewKey(
|
||||
'65',
|
||||
'18',
|
||||
'a:b',
|
||||
'c'
|
||||
)
|
||||
const secondKey = nodeWithKeyBuilder._makePromotionViewKey(
|
||||
'65',
|
||||
'18',
|
||||
'a',
|
||||
'b:c'
|
||||
)
|
||||
const firstKey = makePromotionViewKey('65', '18', 'a:b', 'c')
|
||||
const secondKey = makePromotionViewKey('65', '18', 'a', 'b:c')
|
||||
|
||||
expect(firstKey).not.toBe(secondKey)
|
||||
})
|
||||
|
||||
@@ -53,6 +53,13 @@ import {
|
||||
|
||||
import { ExecutableNodeDTO } from './ExecutableNodeDTO'
|
||||
import type { ExecutableLGraphNode, ExecutionId } from './ExecutableNodeDTO'
|
||||
import {
|
||||
buildDisplayNameByViewKey,
|
||||
buildLinkedReconcileEntries,
|
||||
makePromotionViewKey,
|
||||
resolvePromotionEntries
|
||||
} from './PromotionEntryResolver'
|
||||
import type { LinkedPromotionEntry } from './PromotionEntryResolver'
|
||||
import { PromotedWidgetViewManager } from './PromotedWidgetViewManager'
|
||||
import type { SubgraphInput } from './SubgraphInput'
|
||||
import { createBitmapCache } from './svgBitmapCache'
|
||||
@@ -61,12 +68,6 @@ const workflowSvg = new Image()
|
||||
workflowSvg.src =
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='white' stroke-linecap='round' stroke-width='1.3' d='M9.18613 3.09999H6.81377M9.18613 12.9H7.55288c-3.08678 0-5.35171-2.99581-4.60305-6.08843l.3054-1.26158M14.7486 2.1721l-.5931 2.45c-.132.54533-.6065.92789-1.1508.92789h-2.2993c-.77173 0-1.33797-.74895-1.1508-1.5221l.5931-2.45c.132-.54533.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.74896 1.1508 1.52211Zm-8.3033 0-.59309 2.45c-.13201.54533-.60646.92789-1.15076.92789H2.4021c-.7717 0-1.33793-.74895-1.15077-1.5221l.59309-2.45c.13201-.54533.60647-.9279 1.15077-.9279h2.29935c.77169 0 1.33792.74896 1.15076 1.52211Zm8.3033 9.8-.5931 2.45c-.132.5453-.6065.9279-1.1508.9279h-2.2993c-.77173 0-1.33797-.749-1.1508-1.5221l.5931-2.45c.132-.5453.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.7489 1.1508 1.5221Z'/%3E%3C/svg%3E %3C/svg%3E"
|
||||
|
||||
type LinkedPromotionEntry = PromotedWidgetSource & {
|
||||
inputName: string
|
||||
inputKey: string
|
||||
/** The subgraph input slot's internal name (stable identity). */
|
||||
slotName: string
|
||||
}
|
||||
// Pre-rasterize the SVG to a bitmap canvas to avoid Firefox re-processing
|
||||
// the SVG's internal stylesheet on every ctx.drawImage() call per frame.
|
||||
const workflowBitmapCache = createBitmapCache(workflowSvg, 32)
|
||||
@@ -217,7 +218,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
const seenEntryKeys = new Set<string>()
|
||||
const deduplicatedEntries = linkedEntries.filter((entry) => {
|
||||
const entryKey = this._makePromotionViewKey(
|
||||
const entryKey = makePromotionViewKey(
|
||||
entry.inputKey,
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
@@ -271,8 +272,35 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
const linkedEntries = this._getLinkedPromotionEntries()
|
||||
|
||||
const { displayNameByViewKey, reconcileEntries } =
|
||||
this._buildPromotionReconcileState(entries, linkedEntries)
|
||||
const resolved = resolvePromotionEntries(
|
||||
entries,
|
||||
linkedEntries,
|
||||
this._getConnectedPromotionEntryKeys(),
|
||||
this.inputs.length,
|
||||
this.subgraph,
|
||||
this
|
||||
)
|
||||
const linkedReconcileEntries = buildLinkedReconcileEntries(linkedEntries)
|
||||
const fallbackReconcileEntries: Array<{
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
viewKey?: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
slotName?: string
|
||||
}> = resolved.fallbackStoredEntries.map((e) =>
|
||||
e.disambiguatingSourceNodeId
|
||||
? {
|
||||
sourceNodeId: e.sourceNodeId,
|
||||
sourceWidgetName: e.sourceWidgetName,
|
||||
disambiguatingSourceNodeId: e.disambiguatingSourceNodeId,
|
||||
viewKey: `src:${e.sourceNodeId}:${e.sourceWidgetName}:${e.disambiguatingSourceNodeId}`
|
||||
}
|
||||
: e
|
||||
)
|
||||
const reconcileEntries = resolved.shouldPersistLinkedOnly
|
||||
? linkedReconcileEntries
|
||||
: [...linkedReconcileEntries, ...fallbackReconcileEntries]
|
||||
const displayNameByViewKey = buildDisplayNameByViewKey(linkedEntries)
|
||||
|
||||
const views = this._promotedViewManager.reconcile(
|
||||
reconcileEntries,
|
||||
@@ -307,12 +335,17 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const store = usePromotionStore()
|
||||
const entries = store.getPromotionsRef(this.rootGraph.id, this.id)
|
||||
const linkedEntries = this._getLinkedPromotionEntries(false)
|
||||
// Intentionally preserve independent store promotions when linked coverage is partial;
|
||||
// tests assert that mixed linked/independent states must not collapse to linked-only.
|
||||
const { mergedEntries } = this._buildPromotionPersistenceState(
|
||||
const resolved = resolvePromotionEntries(
|
||||
entries,
|
||||
linkedEntries
|
||||
linkedEntries,
|
||||
this._getConnectedPromotionEntryKeys(),
|
||||
this.inputs.length,
|
||||
this.subgraph,
|
||||
this
|
||||
)
|
||||
const mergedEntries = resolved.shouldPersistLinkedOnly
|
||||
? resolved.linkedPromotionEntries
|
||||
: [...resolved.linkedPromotionEntries, ...resolved.fallbackStoredEntries]
|
||||
|
||||
const hasChanged =
|
||||
mergedEntries.length !== entries.length ||
|
||||
@@ -329,221 +362,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
store.setPromotions(this.rootGraph.id, this.id, mergedEntries)
|
||||
}
|
||||
|
||||
private _buildPromotionReconcileState(
|
||||
entries: PromotedWidgetSource[],
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): {
|
||||
displayNameByViewKey: Map<string, string>
|
||||
reconcileEntries: Array<{
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
viewKey?: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
slotName?: string
|
||||
}>
|
||||
} {
|
||||
const { fallbackStoredEntries } = this._collectLinkedAndFallbackEntries(
|
||||
entries,
|
||||
linkedEntries
|
||||
)
|
||||
const linkedReconcileEntries =
|
||||
this._buildLinkedReconcileEntries(linkedEntries)
|
||||
const shouldPersistLinkedOnly = this._shouldPersistLinkedOnly(
|
||||
linkedEntries,
|
||||
fallbackStoredEntries
|
||||
)
|
||||
const fallbackReconcileEntries = fallbackStoredEntries.map((e) =>
|
||||
e.disambiguatingSourceNodeId
|
||||
? {
|
||||
sourceNodeId: e.sourceNodeId,
|
||||
sourceWidgetName: e.sourceWidgetName,
|
||||
disambiguatingSourceNodeId: e.disambiguatingSourceNodeId,
|
||||
viewKey: `src:${e.sourceNodeId}:${e.sourceWidgetName}:${e.disambiguatingSourceNodeId}`
|
||||
}
|
||||
: e
|
||||
)
|
||||
const reconcileEntries = shouldPersistLinkedOnly
|
||||
? linkedReconcileEntries
|
||||
: [...linkedReconcileEntries, ...fallbackReconcileEntries]
|
||||
|
||||
return {
|
||||
displayNameByViewKey: this._buildDisplayNameByViewKey(linkedEntries),
|
||||
reconcileEntries
|
||||
}
|
||||
}
|
||||
|
||||
private _buildPromotionPersistenceState(
|
||||
entries: PromotedWidgetSource[],
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): {
|
||||
mergedEntries: PromotedWidgetSource[]
|
||||
} {
|
||||
const { linkedPromotionEntries, fallbackStoredEntries } =
|
||||
this._collectLinkedAndFallbackEntries(entries, linkedEntries)
|
||||
const shouldPersistLinkedOnly = this._shouldPersistLinkedOnly(
|
||||
linkedEntries,
|
||||
fallbackStoredEntries
|
||||
)
|
||||
|
||||
return {
|
||||
mergedEntries: shouldPersistLinkedOnly
|
||||
? linkedPromotionEntries
|
||||
: [...linkedPromotionEntries, ...fallbackStoredEntries]
|
||||
}
|
||||
}
|
||||
|
||||
private _collectLinkedAndFallbackEntries(
|
||||
entries: PromotedWidgetSource[],
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): {
|
||||
linkedPromotionEntries: PromotedWidgetSource[]
|
||||
fallbackStoredEntries: PromotedWidgetSource[]
|
||||
} {
|
||||
const linkedPromotionEntries = this._toPromotionEntries(linkedEntries)
|
||||
const excludedEntryKeys = new Set(
|
||||
linkedPromotionEntries.map((entry) =>
|
||||
this._makePromotionEntryKey(
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
)
|
||||
)
|
||||
)
|
||||
const connectedEntryKeys = this._getConnectedPromotionEntryKeys()
|
||||
for (const key of connectedEntryKeys) {
|
||||
excludedEntryKeys.add(key)
|
||||
}
|
||||
|
||||
const prePruneFallbackStoredEntries = this._getFallbackStoredEntries(
|
||||
entries,
|
||||
excludedEntryKeys
|
||||
)
|
||||
const fallbackStoredEntries = this._pruneStaleAliasFallbackEntries(
|
||||
prePruneFallbackStoredEntries,
|
||||
linkedPromotionEntries
|
||||
)
|
||||
|
||||
return {
|
||||
linkedPromotionEntries,
|
||||
fallbackStoredEntries
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldPersistLinkedOnly(
|
||||
linkedEntries: LinkedPromotionEntry[],
|
||||
fallbackStoredEntries: PromotedWidgetSource[]
|
||||
): boolean {
|
||||
if (
|
||||
!(this.inputs.length > 0 && linkedEntries.length === this.inputs.length)
|
||||
)
|
||||
return false
|
||||
|
||||
const linkedEntryKeys = new Set(
|
||||
linkedEntries.map((entry) =>
|
||||
this._makePromotionEntryKey(entry.sourceNodeId, entry.sourceWidgetName)
|
||||
)
|
||||
)
|
||||
|
||||
const linkedWidgetNames = new Set(
|
||||
linkedEntries.map((entry) => entry.sourceWidgetName)
|
||||
)
|
||||
|
||||
const hasFallbackToKeep = fallbackStoredEntries.some((entry) => {
|
||||
const sourceNode = this.subgraph.getNodeById(entry.sourceNodeId)
|
||||
if (!sourceNode) return linkedWidgetNames.has(entry.sourceWidgetName)
|
||||
|
||||
const hasSourceWidget =
|
||||
sourceNode.widgets?.some(
|
||||
(widget) => widget.name === entry.sourceWidgetName
|
||||
) === true
|
||||
if (hasSourceWidget) return true
|
||||
|
||||
// If the fallback entry overlaps a linked entry, keep it
|
||||
// until aliasing can be positively proven.
|
||||
return linkedEntryKeys.has(
|
||||
this._makePromotionEntryKey(entry.sourceNodeId, entry.sourceWidgetName)
|
||||
)
|
||||
})
|
||||
|
||||
return !hasFallbackToKeep
|
||||
}
|
||||
|
||||
private _toPromotionEntries(
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): PromotedWidgetSource[] {
|
||||
return linkedEntries.map(
|
||||
({ sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId }) => ({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
...(disambiguatingSourceNodeId && { disambiguatingSourceNodeId })
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private _getFallbackStoredEntries(
|
||||
entries: PromotedWidgetSource[],
|
||||
excludedEntryKeys: Set<string>
|
||||
): PromotedWidgetSource[] {
|
||||
return entries.filter(
|
||||
(entry) =>
|
||||
!excludedEntryKeys.has(
|
||||
this._makePromotionEntryKey(
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private _pruneStaleAliasFallbackEntries(
|
||||
fallbackStoredEntries: PromotedWidgetSource[],
|
||||
linkedPromotionEntries: PromotedWidgetSource[]
|
||||
): PromotedWidgetSource[] {
|
||||
if (
|
||||
fallbackStoredEntries.length === 0 ||
|
||||
linkedPromotionEntries.length === 0
|
||||
)
|
||||
return fallbackStoredEntries
|
||||
|
||||
const linkedConcreteKeys = new Set(
|
||||
linkedPromotionEntries
|
||||
.map((entry) => this._resolveConcretePromotionEntryKey(entry))
|
||||
.filter((key): key is string => key !== undefined)
|
||||
)
|
||||
if (linkedConcreteKeys.size === 0) return fallbackStoredEntries
|
||||
|
||||
const prunedEntries: PromotedWidgetSource[] = []
|
||||
|
||||
for (const entry of fallbackStoredEntries) {
|
||||
if (!this.subgraph.getNodeById(entry.sourceNodeId)) continue
|
||||
|
||||
const concreteKey = this._resolveConcretePromotionEntryKey(entry)
|
||||
if (concreteKey && linkedConcreteKeys.has(concreteKey)) continue
|
||||
|
||||
prunedEntries.push(entry)
|
||||
}
|
||||
|
||||
return prunedEntries
|
||||
}
|
||||
|
||||
private _resolveConcretePromotionEntryKey(
|
||||
entry: PromotedWidgetSource
|
||||
): string | undefined {
|
||||
const result = resolveConcretePromotedWidget(
|
||||
this,
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
)
|
||||
if (result.status !== 'resolved') return undefined
|
||||
|
||||
return this._makePromotionEntryKey(
|
||||
String(result.resolved.node.id),
|
||||
result.resolved.widget.name
|
||||
)
|
||||
}
|
||||
|
||||
private _getConnectedPromotionEntryKeys(): Set<string> {
|
||||
const connectedEntryKeys = new Set<string>()
|
||||
|
||||
@@ -557,7 +375,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
if (!hasWidgetNode(widget)) continue
|
||||
|
||||
connectedEntryKeys.add(
|
||||
this._makePromotionEntryKey(String(widget.node.id), widget.name)
|
||||
makePromotionEntryKey({
|
||||
sourceNodeId: String(widget.node.id),
|
||||
sourceWidgetName: widget.name
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -565,86 +386,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
return connectedEntryKeys
|
||||
}
|
||||
|
||||
private _buildLinkedReconcileEntries(
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): Array<{
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
viewKey: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
slotName: string
|
||||
}> {
|
||||
return linkedEntries.map(
|
||||
({
|
||||
inputKey,
|
||||
inputName,
|
||||
slotName,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
}) => ({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
slotName,
|
||||
disambiguatingSourceNodeId,
|
||||
viewKey: this._makePromotionViewKey(
|
||||
inputKey,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
inputName,
|
||||
disambiguatingSourceNodeId
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private _buildDisplayNameByViewKey(
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): Map<string, string> {
|
||||
return new Map(
|
||||
linkedEntries.map((entry) => [
|
||||
this._makePromotionViewKey(
|
||||
entry.inputKey,
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.inputName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
),
|
||||
entry.inputName
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
private _makePromotionEntryKey(
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
): string {
|
||||
return makePromotionEntryKey({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
})
|
||||
}
|
||||
|
||||
private _makePromotionViewKey(
|
||||
inputKey: string,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
inputName = '',
|
||||
disambiguatingSourceNodeId?: string
|
||||
): string {
|
||||
return disambiguatingSourceNodeId
|
||||
? JSON.stringify([
|
||||
inputKey,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
inputName,
|
||||
disambiguatingSourceNodeId
|
||||
])
|
||||
: JSON.stringify([inputKey, sourceNodeId, sourceWidgetName, inputName])
|
||||
}
|
||||
|
||||
private _serializeEntries(
|
||||
entries: PromotedWidgetSource[]
|
||||
): (string[] | [string, string, string])[] {
|
||||
@@ -1263,7 +1004,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
sourceNodeId,
|
||||
subgraphInput.name
|
||||
),
|
||||
this._makePromotionViewKey(
|
||||
makePromotionViewKey(
|
||||
String(subgraphInput.id),
|
||||
nodeId,
|
||||
widgetName,
|
||||
@@ -1480,7 +1221,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
this._promotedViewManager.removeByViewKey(
|
||||
view.sourceNodeId,
|
||||
view.sourceWidgetName,
|
||||
this._makePromotionViewKey(
|
||||
makePromotionViewKey(
|
||||
String(input._subgraphSlot.id),
|
||||
view.sourceNodeId,
|
||||
view.sourceWidgetName,
|
||||
|
||||
Reference in New Issue
Block a user