fix(subgraph): address review blockers on promotion + clone hygiene

- proxyWidgetMigration: when a disambiguator is supplied but doesn't
  match, only fall back to non-promoted widgets with the same name so
  we don't silently bind to a sibling PromotedWidgetView.
- previewExposureChain: chainFromLastStep was using the already-advanced
  currentRootGraphId for the leaf, producing an inconsistent
  (rootGraphId, sourceNodeId) pair on cycle / no-exposure / max-depth
  exits. Use the last pushed step's rootGraphId instead.
- promotedWidgetView.applyValueControlToHost: control-after-generate
  was reassigning this.value, which cascades into the shared interior
  widget via the value setter. Switch to hydrateHostValue so the
  per-host overlay is updated without touching shared state.
- promotionUtils: capture promoteValueWidgetViaSubgraphInput results at
  both call sites and emit a warning Sentry breadcrumb on failure
  instead of silently dropping {ok:false, reason}.
- LGraphNode.clone: in UUID mode, configure() was overwriting the
  borrowed source id with the freshly generated UUID before subclass
  hydration could run. Generate the UUID up front and reapply it after
  configure() so hydration sees the borrowed id and the cloned node
  still gets a fresh identity.

Amp-Thread-ID: https://ampcode.com/threads/T-019e1e84-3270-730a-8c95-49a383ffdf20
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
DrJKL
2026-05-12 16:49:33 -07:00
parent ce47bc3771
commit 4c0df82aee
5 changed files with 36 additions and 8 deletions

View File

@@ -50,6 +50,13 @@ function findSourceWidget(
w.sourceWidgetName === sourceWidgetName
)
if (byDisambiguator) return byDisambiguator
// Disambiguator was provided but missed: only fall back to non-promoted
// widgets with the same name. Returning a sibling PromotedWidgetView
// bound to a different interior node would silently re-introduce the
// cross-binding bug the disambiguator exists to prevent.
return widgets.find(
(w) => !isPromotedWidgetView(w) && w.name === sourceWidgetName
)
}
return widgets.find((w) => w.name === sourceWidgetName)

View File

@@ -45,13 +45,13 @@ export function resolvePreviewExposureChain(
const chainFromLastStep = (): ResolvedPreviewChain | undefined => {
if (steps.length === 0) return undefined
const last = steps[steps.length - 1].exposure
const lastStep = steps[steps.length - 1]
return {
steps,
leaf: {
rootGraphId: currentRootGraphId,
sourceNodeId: last.sourceNodeId,
sourcePreviewName: last.sourcePreviewName
rootGraphId: lastStep.rootGraphId,
sourceNodeId: lastStep.exposure.sourceNodeId,
sourcePreviewName: lastStep.exposure.sourcePreviewName
}
}
}

View File

@@ -449,7 +449,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
next = Math.floor(Math.random() * range) * step2 + safeMin
}
next = Math.min(Math.max(next, min), max)
this.value = next
this.hydrateHostValue(next)
}
private resolveAtHost():

View File

@@ -356,7 +356,18 @@ export function promoteWidget(
continue
}
if ('getSlotFromWidget' in node) {
promoteValueWidgetViaSubgraphInput(parent, node as LGraphNode, widget)
const result = promoteValueWidgetViaSubgraphInput(
parent,
node as LGraphNode,
widget
)
if (!result.ok) {
Sentry.addBreadcrumb({
category: 'subgraph',
level: 'warning',
message: `Failed to promote widget "${source.sourceWidgetName}" on node ${node.id}: ${result.reason}`
})
}
}
}
refreshPromotedWidgetRendering(parents)
@@ -571,7 +582,14 @@ export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
.filter(isRecommendedWidget)
.filter(([, widget]) => !isPreviewPseudoWidget(widget))
for (const [n, w] of filteredWidgets) {
promoteValueWidgetViaSubgraphInput(subgraphNode, n, w)
const result = promoteValueWidgetViaSubgraphInput(subgraphNode, n, w)
if (!result.ok) {
Sentry.addBreadcrumb({
category: 'subgraph',
level: 'warning',
message: `Failed to promote widget "${getWidgetName(w)}" on node ${n.id}: ${result.reason}`
})
}
}
subgraphNode.computeSize(subgraphNode.size)
}

View File

@@ -1023,13 +1023,16 @@ export class LGraphNode
// @ts-expect-error Exceptional case: id is removed so that the graph can assign a new one on add.
data.id = undefined
if (LiteGraph.use_uuids) data.id = LiteGraph.uuidv4()
// In UUID mode, configure() would overwrite the borrowed id with data.id;
// generate the fresh UUID up front and reapply it after hydration.
const clonedUuid = LiteGraph.use_uuids ? LiteGraph.uuidv4() : undefined
// Subclasses' configure() (e.g. SubgraphNode) keys per-instance state
// by id; borrow the source's id so that hydration targets the right slot
// before graph.add reassigns.
node.id = this.id
node.configure(data)
if (clonedUuid !== undefined) node.id = clonedUuid
return node
}