mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 05:19:03 +00:00
refactor(subgraph): address review nits on promotion + migration
- proxyWidgetMigration: switch repairValue/migratePreview to exhaustive
switches with a shared `assertUnreachablePlan(plan: never)` helper so
adding a new Plan kind triggers a TS error at the dispatcher.
- previewExposureStore: drop unused `moveExposure` action (and its
tests); document deferred Object.freeze hardening at getExposures.
- usePromotedPreviews: drop optional chaining on
nodeOutputStore.getNode*ByExecutionId methods (always defined).
- BaseWidget: drop the unused `_suppressPromotedOutline` parameter on
`getOutlineColor` and the `suppressPromotedOutline` field on
`DrawWidgetOptions`; remove all 14 call sites.
- SubgraphNodeWidget.vue: switch to tuple-form `defineEmits<{ ... }>()`.
- LGraph: rewrite the `proxyWidgetMigrationFlush` JSDoc to cite the
real reason for late binding (the migration module's transitive
workbench imports, not PreviewExposureStore); mark both
`proxyWidgetMigrationFlush` and `autoExposePreviewNodes` `@internal`.
- proxyWidgetMigration tests: update assertions to verify behavior
(host inputs, exposures, quarantine entries) now that
`flushProxyWidgetMigration` returns void.
Amp-Thread-ID: https://ampcode.com/threads/T-019e2812-d683-710e-946f-9ddb9018ff5a
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -20,9 +20,7 @@ const {
|
||||
isShown?: boolean
|
||||
class?: ClassValue
|
||||
}>()
|
||||
defineEmits<{
|
||||
(e: 'toggleVisibility'): void
|
||||
}>()
|
||||
defineEmits<{ toggleVisibility: [] }>()
|
||||
|
||||
const icon = computed(() =>
|
||||
isPhysical
|
||||
|
||||
@@ -64,9 +64,9 @@ export function usePromotedPreviews(
|
||||
const reactiveOutputs = nodeOutputStore.nodeOutputs[locatorId]
|
||||
const reactivePreviews = nodeOutputStore.nodePreviewImages[locatorId]
|
||||
const reactiveExecutionOutputs =
|
||||
nodeOutputStore.getNodeOutputByExecutionId?.(leafExecutionId)
|
||||
nodeOutputStore.getNodeOutputByExecutionId(leafExecutionId)
|
||||
const reactiveExecutionPreviews =
|
||||
nodeOutputStore.getNodePreviewImagesByExecutionId?.(leafExecutionId)
|
||||
nodeOutputStore.getNodePreviewImagesByExecutionId(leafExecutionId)
|
||||
const hasAnySource =
|
||||
reactiveOutputs?.images?.length ||
|
||||
reactivePreviews?.length ||
|
||||
@@ -74,7 +74,7 @@ export function usePromotedPreviews(
|
||||
reactiveExecutionPreviews?.length
|
||||
if (!hasAnySource) return undefined
|
||||
return (
|
||||
nodeOutputStore.getNodeImageUrlsByExecutionId?.(
|
||||
nodeOutputStore.getNodeImageUrlsByExecutionId(
|
||||
leafExecutionId,
|
||||
interiorNode
|
||||
) ?? nodeOutputStore.getNodeImageUrls(interiorNode)
|
||||
|
||||
@@ -125,14 +125,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
it('returns an empty result when no proxyWidgets are present', () => {
|
||||
const host = buildHost()
|
||||
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toEqual({
|
||||
repaired: 0,
|
||||
primitiveRepaired: 0,
|
||||
previewMigrated: 0,
|
||||
quarantined: 0
|
||||
})
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -140,14 +134,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
const host = buildHost()
|
||||
host.properties.proxyWidgets = '{not json}'
|
||||
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toEqual({
|
||||
repaired: 0,
|
||||
primitiveRepaired: 0,
|
||||
previewMigrated: 0,
|
||||
quarantined: 0
|
||||
})
|
||||
expect(() => flushProxyWidgetMigration({ hostNode: host })).not.toThrow()
|
||||
expect(host.properties.proxyWidgetErrorQuarantine).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -166,12 +154,11 @@ describe('flushProxyWidgetMigration', () => {
|
||||
})
|
||||
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'seed']]
|
||||
const result = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: host,
|
||||
hostWidgetValues: [99]
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ repaired: 1, quarantined: 0 })
|
||||
expect(handle.getValue()).toBe(99)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
@@ -190,12 +177,11 @@ describe('flushProxyWidgetMigration', () => {
|
||||
subgraph.inputNode.slots[0].connect(inner.inputs[0], inner)
|
||||
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'seed']]
|
||||
const result = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: host,
|
||||
hostWidgetValues: [99]
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ repaired: 1, quarantined: 0 })
|
||||
expect(host.widgets[0].value).toBe(99)
|
||||
const innerWidget = inner.widgets!.find((w) => w.name === 'seed')!
|
||||
expect(innerWidget.value).toBe(0)
|
||||
@@ -216,12 +202,11 @@ describe('flushProxyWidgetMigration', () => {
|
||||
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'seed']]
|
||||
const sparse: unknown[] = []
|
||||
const result = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: host,
|
||||
hostWidgetValues: sparse
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ repaired: 1, quarantined: 0 })
|
||||
expect(handle.getValue()).toBe(7)
|
||||
})
|
||||
|
||||
@@ -246,12 +231,11 @@ describe('flushProxyWidgetMigration', () => {
|
||||
})
|
||||
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'seed']]
|
||||
const result = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: host,
|
||||
hostWidgetValues: [99]
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ repaired: 0, quarantined: 1 })
|
||||
expect(a.getValue()).toBe(1)
|
||||
expect(b.getValue()).toBe(2)
|
||||
expect(readHostQuarantine(host)).toEqual([
|
||||
@@ -272,9 +256,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
|
||||
const inputCountBefore = host.subgraph.inputs.length
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'seed']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ repaired: 1, quarantined: 0 })
|
||||
expect(host.subgraph.inputs).toHaveLength(inputCountBefore + 1)
|
||||
const created = host.subgraph.inputs.at(-1)
|
||||
expect(created?._widget).toBeDefined()
|
||||
@@ -302,9 +285,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
})
|
||||
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'text', '2']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ repaired: 1, quarantined: 0 })
|
||||
const created = host.subgraph.inputs.at(-1)
|
||||
expect(created?._widget).toBeDefined()
|
||||
// The created SubgraphInput connects to inner's "text_1" slot (the
|
||||
@@ -323,9 +305,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
|
||||
const inputCountBefore = host.subgraph.inputs.length
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'seed']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ repaired: 0, quarantined: 1 })
|
||||
expect(host.subgraph.inputs).toHaveLength(inputCountBefore)
|
||||
expect(readHostQuarantine(host)).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -345,13 +326,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
|
||||
const inputCountBefore = host.subgraph.inputs.length
|
||||
host.properties.proxyWidgets = [[String(primitive.id), 'value']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({
|
||||
primitiveRepaired: 1,
|
||||
repaired: 0,
|
||||
quarantined: 0
|
||||
})
|
||||
expect(host.subgraph.inputs).toHaveLength(inputCountBefore + 1)
|
||||
for (const target of targets) {
|
||||
const slot = target.inputs[0]
|
||||
@@ -371,9 +347,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
[String(primitive.id), 'value'],
|
||||
[String(primitive.id), 'value']
|
||||
]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ primitiveRepaired: 1, quarantined: 0 })
|
||||
for (const target of targets) {
|
||||
const slot = target.inputs[0]
|
||||
const link = host.subgraph.links.get(slot.link!)
|
||||
@@ -389,11 +364,10 @@ describe('flushProxyWidgetMigration', () => {
|
||||
})
|
||||
|
||||
host.properties.proxyWidgets = [[String(primitive.id), 'value']]
|
||||
const result = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: host,
|
||||
hostWidgetValues: [123]
|
||||
})
|
||||
expect(result.primitiveRepaired).toBe(1)
|
||||
|
||||
// Host value lands on the host's input mirror (a `PromotedWidgetView`),
|
||||
// not on the shared interior consumer widget. Verifying the host side
|
||||
@@ -410,9 +384,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
})
|
||||
|
||||
host.properties.proxyWidgets = [[String(primitive.id), 'value']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result.primitiveRepaired).toBe(1)
|
||||
// With no host value supplied, the host is seeded per-instance from
|
||||
// the primitive's widget value — never by mutating the shared interior.
|
||||
const hostInput = host.inputs.at(-1)
|
||||
@@ -427,9 +400,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
host.subgraph.add(primitive)
|
||||
|
||||
host.properties.proxyWidgets = [[String(primitive.id), 'value']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ primitiveRepaired: 0, quarantined: 1 })
|
||||
expect(readHostQuarantine(host)).toEqual([
|
||||
expect.objectContaining({
|
||||
originalEntry: [String(primitive.id), 'value'],
|
||||
@@ -447,9 +419,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
|
||||
const inputCountBefore = host.subgraph.inputs.length
|
||||
host.properties.proxyWidgets = [[String(primitive.id), 'value']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ primitiveRepaired: 0, quarantined: 1 })
|
||||
expect(host.subgraph.inputs).toHaveLength(inputCountBefore)
|
||||
expect(readHostQuarantine(host)).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -471,9 +442,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
]
|
||||
|
||||
host.properties.proxyWidgets = [[String(primitive.id), 'value']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ primitiveRepaired: 0, quarantined: 1 })
|
||||
expect(readHostQuarantine(host)).toEqual([
|
||||
expect.objectContaining({
|
||||
originalEntry: [String(primitive.id), 'value'],
|
||||
@@ -511,21 +481,20 @@ describe('flushProxyWidgetMigration', () => {
|
||||
hostA.properties.proxyWidgets = [[String(primitive.id), 'value']]
|
||||
hostB.properties.proxyWidgets = [[String(primitive.id), 'value']]
|
||||
|
||||
const resultA = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: hostA,
|
||||
hostWidgetValues: [11]
|
||||
})
|
||||
const resultB = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: hostB,
|
||||
hostWidgetValues: [22]
|
||||
})
|
||||
|
||||
expect(resultA).toMatchObject({ primitiveRepaired: 1, quarantined: 0 })
|
||||
// Host B's classify recognises the bypass marker on the primitive and
|
||||
// takes the `alreadyLinked` path, so it counts as `repaired` not
|
||||
// `primitiveRepaired`. Either way, no quarantine.
|
||||
expect(resultB).toMatchObject({ quarantined: 0 })
|
||||
expect(resultB.repaired + resultB.primitiveRepaired).toBe(1)
|
||||
// takes the `alreadyLinked` path. Either way, no quarantine, and each
|
||||
// host gets an independent value.
|
||||
expect(hostA.properties.proxyWidgetErrorQuarantine).toBeUndefined()
|
||||
expect(hostB.properties.proxyWidgetErrorQuarantine).toBeUndefined()
|
||||
|
||||
const widgetA = hostA.inputs.at(-1)?._widget
|
||||
const widgetB = hostB.inputs.at(-1)?._widget
|
||||
@@ -544,9 +513,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
host.properties.proxyWidgets = [
|
||||
[String(inner.id), '$$canvas-image-preview']
|
||||
]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ previewMigrated: 1, quarantined: 0 })
|
||||
const exposures = usePreviewExposureStore().getExposures(
|
||||
host.rootGraph.id,
|
||||
String(host.id)
|
||||
@@ -565,9 +533,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
})
|
||||
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'videopreview']]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ previewMigrated: 1, quarantined: 0 })
|
||||
const exposures = usePreviewExposureStore().getExposures(
|
||||
host.rootGraph.id,
|
||||
String(host.id)
|
||||
@@ -599,9 +566,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
host.properties.proxyWidgets = [
|
||||
[String(innerB.id), '$$canvas-image-preview']
|
||||
]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ previewMigrated: 1, quarantined: 0 })
|
||||
const exposures = store.getExposures(host.rootGraph.id, locator)
|
||||
expect(exposures).toHaveLength(2)
|
||||
const newExposure = exposures.find(
|
||||
@@ -626,9 +592,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
host.properties.proxyWidgets = [
|
||||
[String(inner.id), '$$canvas-image-preview']
|
||||
]
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ previewMigrated: 1, quarantined: 0 })
|
||||
expect(store.getExposures(host.rootGraph.id, locator)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -638,9 +603,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
const host = buildHost()
|
||||
host.properties.proxyWidgets = [['9999', 'seed']]
|
||||
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ quarantined: 1 })
|
||||
expect(readHostQuarantine(host)).toEqual([
|
||||
{
|
||||
originalEntry: ['9999', 'seed'],
|
||||
@@ -655,9 +619,8 @@ describe('flushProxyWidgetMigration', () => {
|
||||
const inner = addInnerNode(host, 'Inner')
|
||||
host.properties.proxyWidgets = [[String(inner.id), 'nonexistent']]
|
||||
|
||||
const result = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(result).toMatchObject({ quarantined: 1 })
|
||||
expect(readHostQuarantine(host)).toEqual([
|
||||
expect.objectContaining({
|
||||
originalEntry: [String(inner.id), 'nonexistent'],
|
||||
@@ -740,21 +703,17 @@ describe('flushProxyWidgetMigration', () => {
|
||||
[String(inner.id), '$$canvas-image-preview']
|
||||
]
|
||||
|
||||
const first = flushProxyWidgetMigration({ hostNode: host })
|
||||
expect(first.previewMigrated).toBe(1)
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
const exposuresAfterFirst = usePreviewExposureStore()
|
||||
.getExposures(host.rootGraph.id, String(host.id))
|
||||
.map((e) => ({ ...e }))
|
||||
expect(exposuresAfterFirst).toHaveLength(1)
|
||||
|
||||
const second = flushProxyWidgetMigration({ hostNode: host })
|
||||
flushProxyWidgetMigration({ hostNode: host })
|
||||
|
||||
expect(second).toEqual({
|
||||
repaired: 0,
|
||||
primitiveRepaired: 0,
|
||||
previewMigrated: 0,
|
||||
quarantined: 0
|
||||
})
|
||||
// Re-running the flush is idempotent: no new exposures, no quarantine.
|
||||
expect(host.properties.proxyWidgetErrorQuarantine).toBeUndefined()
|
||||
expect(
|
||||
usePreviewExposureStore().getExposures(
|
||||
host.rootGraph.id,
|
||||
@@ -779,16 +738,11 @@ describe('flushProxyWidgetMigration', () => {
|
||||
[String(inner.id), 'seed'],
|
||||
[String(inner.id), '$$canvas-image-preview']
|
||||
]
|
||||
const result = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: host,
|
||||
hostWidgetValues: [99]
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({
|
||||
repaired: 1,
|
||||
previewMigrated: 1,
|
||||
quarantined: 0
|
||||
})
|
||||
expect(host.subgraph.inputs).toHaveLength(subgraphInputCountBefore + 1)
|
||||
expect(host.subgraph.inputs.find((i) => i.name === 'seed')).toBeDefined()
|
||||
const exposures = usePreviewExposureStore().getExposures(
|
||||
@@ -816,12 +770,11 @@ describe('flushProxyWidgetMigration', () => {
|
||||
]
|
||||
const sparse: unknown[] = []
|
||||
sparse[1] = 'second-value'
|
||||
const result = flushProxyWidgetMigration({
|
||||
flushProxyWidgetMigration({
|
||||
hostNode: host,
|
||||
hostWidgetValues: sparse
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ repaired: 2, quarantined: 0 })
|
||||
expect(host.subgraph.inputs.find((i) => i.name === 'a')).toBeDefined()
|
||||
expect(host.subgraph.inputs.find((i) => i.name === 'b')).toBeDefined()
|
||||
})
|
||||
|
||||
@@ -90,20 +90,6 @@ interface FlushArgs {
|
||||
hostWidgetValues?: readonly unknown[]
|
||||
}
|
||||
|
||||
interface FlushResult {
|
||||
repaired: number
|
||||
primitiveRepaired: number
|
||||
previewMigrated: number
|
||||
quarantined: number
|
||||
}
|
||||
|
||||
const EMPTY_RESULT: FlushResult = Object.freeze({
|
||||
repaired: 0,
|
||||
primitiveRepaired: 0,
|
||||
previewMigrated: 0,
|
||||
quarantined: 0
|
||||
})
|
||||
|
||||
interface PrimitiveBypassTargetRef {
|
||||
targetNodeId: NodeId
|
||||
targetSlot: number
|
||||
@@ -140,11 +126,11 @@ const QUARANTINE_VERSION = 1
|
||||
*/
|
||||
const PROXY_BYPASS_MARKER_PROPERTY = 'proxyBypassedToSubgraphInput'
|
||||
|
||||
export function flushProxyWidgetMigration(args: FlushArgs): FlushResult {
|
||||
export function flushProxyWidgetMigration(args: FlushArgs): void {
|
||||
const { hostNode, hostWidgetValues } = args
|
||||
|
||||
const tuples = parseProxyWidgets(hostNode.properties.proxyWidgets)
|
||||
if (tuples.length === 0) return EMPTY_RESULT
|
||||
if (tuples.length === 0) return
|
||||
|
||||
const cohort: LegacyProxyEntrySource[] = tuples.map(
|
||||
([sourceNodeId, sourceWidgetName, disambiguator]) =>
|
||||
@@ -167,7 +153,6 @@ export function flushProxyWidgetMigration(args: FlushArgs): FlushResult {
|
||||
})
|
||||
|
||||
const previewStore = usePreviewExposureStore()
|
||||
const result: FlushResult = { ...EMPTY_RESULT }
|
||||
const quarantineToAppend: ProxyWidgetErrorQuarantineEntry[] = []
|
||||
const primitiveCohorts = new Map<NodeId, PendingEntry[]>()
|
||||
|
||||
@@ -182,14 +167,12 @@ export function flushProxyWidgetMigration(args: FlushArgs): FlushResult {
|
||||
case 'alreadyLinked':
|
||||
case 'createSubgraphInput': {
|
||||
const r = repairValue(hostNode, entry)
|
||||
if (r.ok) result.repaired += 1
|
||||
else quarantineToAppend.push(quarantineFor(entry, r.reason))
|
||||
if (!r.ok) quarantineToAppend.push(quarantineFor(entry, r.reason))
|
||||
break
|
||||
}
|
||||
case 'previewExposure': {
|
||||
const r = migratePreview(hostNode, entry, previewStore)
|
||||
if (r.ok) result.previewMigrated += 1
|
||||
else quarantineToAppend.push(quarantineFor(entry, r.reason))
|
||||
if (!r.ok) quarantineToAppend.push(quarantineFor(entry, r.reason))
|
||||
break
|
||||
}
|
||||
case 'quarantine':
|
||||
@@ -200,18 +183,15 @@ export function flushProxyWidgetMigration(args: FlushArgs): FlushResult {
|
||||
|
||||
for (const c of primitiveCohorts.values()) {
|
||||
const r = repairPrimitive(hostNode, c)
|
||||
if (r.ok) result.primitiveRepaired += 1
|
||||
else for (const e of c) quarantineToAppend.push(quarantineFor(e, r.reason))
|
||||
if (!r.ok)
|
||||
for (const e of c) quarantineToAppend.push(quarantineFor(e, r.reason))
|
||||
}
|
||||
|
||||
if (quarantineToAppend.length > 0) {
|
||||
appendQuarantine(hostNode, quarantineToAppend)
|
||||
result.quarantined = quarantineToAppend.length
|
||||
}
|
||||
|
||||
delete hostNode.properties.proxyWidgets
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function pickHostValue(
|
||||
@@ -411,17 +391,25 @@ function repairValue(
|
||||
hostNode: SubgraphNode,
|
||||
entry: PendingEntry
|
||||
): RepairValueResult {
|
||||
if (entry.plan.kind === 'alreadyLinked') {
|
||||
return repairAlreadyLinked(hostNode, entry, entry.plan.subgraphInputName)
|
||||
const { plan } = entry
|
||||
switch (plan.kind) {
|
||||
case 'alreadyLinked':
|
||||
return repairAlreadyLinked(hostNode, entry, plan.subgraphInputName)
|
||||
case 'createSubgraphInput':
|
||||
return repairCreateSubgraphInput(hostNode, entry, plan.sourceWidgetName)
|
||||
case 'primitiveBypass':
|
||||
case 'previewExposure':
|
||||
case 'quarantine':
|
||||
throw new Error(`repairValue: unexpected plan kind ${plan.kind}`)
|
||||
default:
|
||||
return assertUnreachablePlan(plan)
|
||||
}
|
||||
if (entry.plan.kind === 'createSubgraphInput') {
|
||||
return repairCreateSubgraphInput(
|
||||
hostNode,
|
||||
entry,
|
||||
entry.plan.sourceWidgetName
|
||||
)
|
||||
}
|
||||
throw new Error(`repairValue: invalid plan kind ${entry.plan.kind}`)
|
||||
}
|
||||
|
||||
function assertUnreachablePlan(plan: never): never {
|
||||
throw new Error(
|
||||
`Unexpected plan kind: ${(plan as { kind: string } | undefined)?.kind}`
|
||||
)
|
||||
}
|
||||
|
||||
function repairAlreadyLinked(
|
||||
@@ -726,9 +714,17 @@ function migratePreview(
|
||||
entry: PendingEntry,
|
||||
store: ReturnType<typeof usePreviewExposureStore>
|
||||
): MigratePreviewResult {
|
||||
const plan = entry.plan
|
||||
if (plan.kind !== 'previewExposure') {
|
||||
throw new Error(`migratePreview: invalid plan kind ${plan.kind}`)
|
||||
const { plan } = entry
|
||||
switch (plan.kind) {
|
||||
case 'previewExposure':
|
||||
break
|
||||
case 'alreadyLinked':
|
||||
case 'createSubgraphInput':
|
||||
case 'primitiveBypass':
|
||||
case 'quarantine':
|
||||
throw new Error(`migratePreview: unexpected plan kind ${plan.kind}`)
|
||||
default:
|
||||
assertUnreachablePlan(plan)
|
||||
}
|
||||
|
||||
const sourceNode = hostNode.subgraph.getNodeById(
|
||||
|
||||
@@ -382,7 +382,6 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
projected.drawWidget(ctx, {
|
||||
width: widgetWidth,
|
||||
showText: !lowQuality,
|
||||
suppressPromotedOutline: true,
|
||||
previewImages: resolved.node.imgs
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -248,7 +248,7 @@ function toPromotionSource(
|
||||
function refreshPromotedWidgetRendering(parents: SubgraphNode[]): void {
|
||||
for (const parent of parents) {
|
||||
parent.computeSize(parent.size)
|
||||
parent.setDirtyCanvas?.(true, true)
|
||||
parent.setDirtyCanvas(true, true)
|
||||
}
|
||||
useCanvasStore().canvas?.setDirty(true, true)
|
||||
}
|
||||
|
||||
@@ -183,9 +183,16 @@ export class LGraph
|
||||
static STATUS_RUNNING = 2
|
||||
|
||||
/**
|
||||
* Late-bound migration hook. Set once during app init from the wiring layer
|
||||
* to avoid a circular dependency through PreviewExposureStore. Left undefined
|
||||
* in tests that exercise `configure()` without the migration pipeline.
|
||||
* Late-bound migration hook. Set once during app init from the wiring layer.
|
||||
* The migration code lives in `@/core/graph/subgraph/migration`, which
|
||||
* transitively imports more workbench surface than the litegraph core is
|
||||
* willing to depend on; binding it here lazily keeps LGraph free of those
|
||||
* imports while still letting `configure()` invoke the flush. Left
|
||||
* undefined in tests that exercise `configure()` without the migration
|
||||
* pipeline.
|
||||
*
|
||||
* @internal Wiring hook for the frontend; not part of the public litegraph
|
||||
* API. Custom nodes and extensions must not read or assign this.
|
||||
*/
|
||||
static proxyWidgetMigrationFlush?: (
|
||||
hostNode: SubgraphNode,
|
||||
@@ -198,6 +205,9 @@ export class LGraph
|
||||
* Used after configure (workflow load and paste) so older clipboard /
|
||||
* workflow data without `properties.previewExposures` still surfaces a
|
||||
* preview on the host SubgraphNode. Idempotent.
|
||||
*
|
||||
* @internal Wiring hook for the frontend; not part of the public litegraph
|
||||
* API. Custom nodes and extensions must not read or assign this.
|
||||
*/
|
||||
static autoExposePreviewNodes?: (hostNode: SubgraphNode) => void
|
||||
|
||||
|
||||
@@ -271,11 +271,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
this._cacheVersion++
|
||||
}
|
||||
|
||||
private _mutateInputs(fn: () => void): void {
|
||||
fn()
|
||||
this.invalidatePromotedViews()
|
||||
}
|
||||
|
||||
private _makePromotionViewKey(
|
||||
inputKey: string,
|
||||
sourceNodeId: string,
|
||||
@@ -349,12 +344,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
)
|
||||
return
|
||||
}
|
||||
let input!: INodeInputSlot & Partial<ISubgraphInput>
|
||||
this._mutateInputs(() => {
|
||||
input = this.addInput(name, type, {
|
||||
_subgraphSlot: subgraphInput
|
||||
})
|
||||
const input = this.addInput(name, type, {
|
||||
_subgraphSlot: subgraphInput
|
||||
})
|
||||
this.invalidatePromotedViews()
|
||||
|
||||
this._addSubgraphInputListeners(subgraphInput, input)
|
||||
},
|
||||
@@ -367,7 +360,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const widget = e.detail.input._widget
|
||||
if (widget) this.ensureWidgetRemoved(widget)
|
||||
|
||||
this._mutateInputs(() => this.removeInput(e.detail.index))
|
||||
this.removeInput(e.detail.index)
|
||||
this.invalidatePromotedViews()
|
||||
this.setDirtyCanvas(true, true)
|
||||
},
|
||||
{ signal }
|
||||
@@ -597,29 +591,28 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
}
|
||||
|
||||
this._mutateInputs(() => {
|
||||
this.inputs.length = 0
|
||||
this.inputs.push(
|
||||
...this.subgraph.inputNode.slots.map((slot) =>
|
||||
Object.assign(
|
||||
new NodeInputSlot(
|
||||
{
|
||||
name: slot.name,
|
||||
localized_name: slot.localized_name,
|
||||
label: slot.label,
|
||||
shape: this.getSlotShape(slot),
|
||||
type: slot.type,
|
||||
link: null
|
||||
},
|
||||
this
|
||||
),
|
||||
this.inputs.length = 0
|
||||
this.inputs.push(
|
||||
...this.subgraph.inputNode.slots.map((slot) =>
|
||||
Object.assign(
|
||||
new NodeInputSlot(
|
||||
{
|
||||
_subgraphSlot: slot
|
||||
}
|
||||
)
|
||||
name: slot.name,
|
||||
localized_name: slot.localized_name,
|
||||
label: slot.label,
|
||||
shape: this.getSlotShape(slot),
|
||||
type: slot.type,
|
||||
link: null
|
||||
},
|
||||
this
|
||||
),
|
||||
{
|
||||
_subgraphSlot: slot
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
this.invalidatePromotedViews()
|
||||
|
||||
this.outputs.length = 0
|
||||
this.outputs.push(
|
||||
@@ -669,10 +662,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
// Prune inputs that don't map to any subgraph slot definition.
|
||||
// This prevents stale/duplicate serialized inputs from persisting (#9977).
|
||||
this._mutateInputs(() => {
|
||||
this.inputs = this.inputs.filter((input) => input._subgraphSlot)
|
||||
})
|
||||
|
||||
this.inputs = this.inputs.filter((input) => input._subgraphSlot)
|
||||
this._promotedViewManager.clear()
|
||||
this.invalidatePromotedViews()
|
||||
|
||||
@@ -693,8 +683,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
this._addSubgraphInputListeners(subgraphInput, input)
|
||||
this._resolveInputWidget(subgraphInput, input)
|
||||
}
|
||||
|
||||
this.invalidatePromotedViews()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,6 @@ export interface DrawWidgetOptions {
|
||||
width: number
|
||||
/** Synonym for "low quality". */
|
||||
showText?: boolean
|
||||
suppressPromotedOutline?: boolean
|
||||
/** Transient image source for preview widgets rendered on behalf of another node (e.g. subgraph promotion). */
|
||||
previewImages?: HTMLImageElement[]
|
||||
}
|
||||
@@ -223,7 +222,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
}
|
||||
}
|
||||
|
||||
getOutlineColor(_suppressPromotedOutline = false) {
|
||||
getOutlineColor() {
|
||||
return this.advanced
|
||||
? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR
|
||||
: LiteGraph.WIDGET_OUTLINE_COLOR
|
||||
@@ -287,13 +286,13 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
*/
|
||||
protected drawWidgetShape(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width, showText, suppressPromotedOutline }: DrawWidgetOptions
|
||||
{ width, showText }: DrawWidgetOptions
|
||||
): void {
|
||||
const { height, y } = this
|
||||
const { margin } = BaseWidget
|
||||
|
||||
ctx.textAlign = 'left'
|
||||
ctx.strokeStyle = this.getOutlineColor(suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.beginPath()
|
||||
|
||||
@@ -314,7 +313,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
*/
|
||||
protected drawVueOnlyWarning(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width, suppressPromotedOutline }: DrawWidgetOptions,
|
||||
{ width }: DrawWidgetOptions,
|
||||
label: string
|
||||
): void {
|
||||
const { y, height } = this
|
||||
@@ -324,7 +323,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ButtonWidget
|
||||
*/
|
||||
override drawWidget(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width, showText = true, suppressPromotedOutline }: DrawWidgetOptions
|
||||
{ width, showText = true }: DrawWidgetOptions
|
||||
) {
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
@@ -41,7 +41,7 @@ export class ButtonWidget
|
||||
|
||||
// Draw button outline if not disabled
|
||||
if (showText && !this.computedDisabled) {
|
||||
ctx.strokeStyle = this.getOutlineColor(suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(margin, y, width - margin * 2, height)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ChartWidget
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
|
||||
@@ -23,7 +23,7 @@ export class FileUploadWidget
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
|
||||
@@ -23,7 +23,7 @@ export class GalleriaWidget
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ImageCompareWidget
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
|
||||
@@ -33,7 +33,7 @@ export class KnobWidget extends BaseWidget<IKnobWidget> implements IKnobWidget {
|
||||
|
||||
drawWidget(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width, showText = true, suppressPromotedOutline }: DrawWidgetOptions
|
||||
{ width, showText = true }: DrawWidgetOptions
|
||||
): void {
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
@@ -145,10 +145,10 @@ export class KnobWidget extends BaseWidget<IKnobWidget> implements IKnobWidget {
|
||||
|
||||
// Draw outline if not disabled
|
||||
if (showText && !this.computedDisabled) {
|
||||
ctx.strokeStyle = this.getOutlineColor(suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
// Draw value
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = this.getOutlineColor(suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.arc(
|
||||
arc_center.x,
|
||||
arc_center.y,
|
||||
|
||||
@@ -23,7 +23,7 @@ export class MarkdownWidget
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
|
||||
@@ -23,7 +23,7 @@ export class MultiSelectWidget
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
|
||||
@@ -23,7 +23,7 @@ export class SelectButtonWidget
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.getOutlineColor(options.suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
|
||||
@@ -20,7 +20,7 @@ export class SliderWidget
|
||||
*/
|
||||
override drawWidget(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width, showText = true, suppressPromotedOutline }: DrawWidgetOptions
|
||||
{ width, showText = true }: DrawWidgetOptions
|
||||
) {
|
||||
// Store original context attributes
|
||||
const { fillStyle, strokeStyle, textAlign } = ctx
|
||||
@@ -43,7 +43,7 @@ export class SliderWidget
|
||||
|
||||
// Draw outline if not disabled
|
||||
if (showText && !this.computedDisabled) {
|
||||
ctx.strokeStyle = this.getOutlineColor(suppressPromotedOutline)
|
||||
ctx.strokeStyle = this.getOutlineColor()
|
||||
ctx.strokeRect(margin, y, width - margin * 2, height)
|
||||
}
|
||||
|
||||
|
||||
@@ -122,45 +122,6 @@ describe(usePreviewExposureStore, () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('moveExposure', () => {
|
||||
beforeEach(() => {
|
||||
store.setExposures(rootGraphA, hostA, [
|
||||
{ name: 'a', sourceNodeId: '1', sourcePreviewName: 'a' },
|
||||
{ name: 'b', sourceNodeId: '2', sourcePreviewName: 'b' },
|
||||
{ name: 'c', sourceNodeId: '3', sourcePreviewName: 'c' }
|
||||
])
|
||||
})
|
||||
|
||||
it('reorders entries from -> to', () => {
|
||||
store.moveExposure(rootGraphA, hostA, 0, 2)
|
||||
|
||||
expect(store.getExposures(rootGraphA, hostA).map((e) => e.name)).toEqual([
|
||||
'b',
|
||||
'c',
|
||||
'a'
|
||||
])
|
||||
})
|
||||
|
||||
it('is a no-op for equal indices', () => {
|
||||
store.moveExposure(rootGraphA, hostA, 1, 1)
|
||||
expect(store.getExposures(rootGraphA, hostA).map((e) => e.name)).toEqual([
|
||||
'a',
|
||||
'b',
|
||||
'c'
|
||||
])
|
||||
})
|
||||
|
||||
it('is a no-op for out-of-bounds indices', () => {
|
||||
store.moveExposure(rootGraphA, hostA, -1, 2)
|
||||
store.moveExposure(rootGraphA, hostA, 0, 5)
|
||||
expect(store.getExposures(rootGraphA, hostA).map((e) => e.name)).toEqual([
|
||||
'a',
|
||||
'b',
|
||||
'c'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearGraph', () => {
|
||||
it('removes all hosts under the rootGraphId without affecting others', () => {
|
||||
store.addExposure(rootGraphA, hostA, {
|
||||
|
||||
@@ -42,6 +42,11 @@ export const usePreviewExposureStore = defineStore('previewExposure', () => {
|
||||
return exposures.value.get(rootGraphId)?.get(hostNodeLocator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the live internal array (typed `readonly` to discourage mutation).
|
||||
* Defensive `Object.freeze` at the four insert sites would close this if
|
||||
* a caller ever casts away the readonly modifier; deferred until needed.
|
||||
*/
|
||||
function getExposures(
|
||||
rootGraphId: UUID,
|
||||
hostNodeLocator: string
|
||||
@@ -93,31 +98,6 @@ export const usePreviewExposureStore = defineStore('previewExposure', () => {
|
||||
setExposures(rootGraphId, hostNodeLocator, next)
|
||||
}
|
||||
|
||||
function moveExposure(
|
||||
rootGraphId: UUID,
|
||||
hostNodeLocator: string,
|
||||
fromIndex: number,
|
||||
toIndex: number
|
||||
): void {
|
||||
const hosts = exposures.value.get(rootGraphId)
|
||||
const current = hosts?.get(hostNodeLocator)
|
||||
if (!hosts || !current?.length) return
|
||||
|
||||
if (
|
||||
fromIndex < 0 ||
|
||||
fromIndex >= current.length ||
|
||||
toIndex < 0 ||
|
||||
toIndex >= current.length ||
|
||||
fromIndex === toIndex
|
||||
)
|
||||
return
|
||||
|
||||
const next = [...current]
|
||||
const [entry] = next.splice(fromIndex, 1)
|
||||
next.splice(toIndex, 0, entry)
|
||||
hosts.set(hostNodeLocator, next)
|
||||
}
|
||||
|
||||
function clearGraph(rootGraphId: UUID): void {
|
||||
exposures.value.delete(rootGraphId)
|
||||
}
|
||||
@@ -148,7 +128,6 @@ export const usePreviewExposureStore = defineStore('previewExposure', () => {
|
||||
setExposures,
|
||||
addExposure,
|
||||
removeExposure,
|
||||
moveExposure,
|
||||
clearGraph,
|
||||
resolveChain
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user