From 36a9b6467c76bc77decebbd6bf9570f649da1cc2 Mon Sep 17 00:00:00 2001 From: DrJKL Date: Mon, 27 Apr 2026 17:15:15 -0700 Subject: [PATCH] refactor(world): delete bridge, revert BaseWidget/useUpstreamValue Phase 2b of temp/plans/world-consolidation.md (completes Strategy A bridge elimination begun in Phase 2a). Now that useWidgetValueStore is a World facade (Phase 2a), the bridge is redundant. Deletions: - src/world/widgetWorldBridge.ts + widgetWorldBridge.test.ts deleted. Their register/getNodeWidgets coverage rolled into widgetValueStore facade tests in Phase 2a; widgetParent tests already moved to src/stores/widgetComponents.test.ts in Phase 1. Reverts (back to pre-slice-1 baseline): - BaseWidget.setNodeId no longer double-writes via registerWidgetInWorld; the store IS the World writer now. Drops three @/world/* imports. - useUpstreamValue reads via useWidgetValueStore().getNodeWidgets(). Drops three @/world/* imports. Test updates: - useUpstreamValue.test.ts setup uses useWidgetValueStore().registerWidget instead of registerWidgetInWorld(getWorld(), ...). Hoists setActivePinia(createTestingPinia) + resetWorldInstance into beforeEach. - widgetComponents.test.ts setup inlines a 4-line widget registration to replace the deleted bridge import. - entityIds.ts: GraphId type un-exported (no external consumer; YAGNI; re-export when slice 2 needs it). End state: - src/world/ is pure substrate (5 source files: brand, entityIds, componentKey, world, worldInstance). No bridge, no components dir. - BaseWidget.ts byte-identical with pre-slice-1 form at the setNodeId seam. - useWidgetValueStore is the sole owner of widget value state; Vue tracking flows naturally through reactive(Map) inside the World. Verification: - pnpm typecheck, format:check, knip clean. - 52 tests pass across src/world, src/stores/widgetValueStore, src/stores/widgetComponents, src/composables/useUpstreamValue, src/lib/litegraph/src/widgets/BaseWidget. - rg "@/world" src/lib/litegraph/src/widgets/BaseWidget.ts returns 0. - rg "@/world" src/composables/useUpstreamValue.ts returns 0. - rg "registerWidgetInWorld|getNodeWidgetsThroughWorld|widgetWorldBridge" src/ returns 0. Combined Phase 2 non-test diff -53 LOC (under 150 LOC budget). BaseWidget._state shared reactive identity contract preserved end-to-end. Amp-Thread-ID: https://ampcode.com/threads/T-019dd146-a3ad-734d-9825-0ab356454dd5 Co-authored-by: Amp --- src/composables/useUpstreamValue.test.ts | 27 ++++--- src/composables/useUpstreamValue.ts | 13 +-- src/lib/litegraph/src/widgets/BaseWidget.ts | 8 -- src/stores/widgetComponents.test.ts | 19 +++-- src/world/entityIds.ts | 2 +- src/world/widgetWorldBridge.test.ts | 88 --------------------- src/world/widgetWorldBridge.ts | 62 --------------- 7 files changed, 34 insertions(+), 185 deletions(-) delete mode 100644 src/world/widgetWorldBridge.test.ts delete mode 100644 src/world/widgetWorldBridge.ts diff --git a/src/composables/useUpstreamValue.test.ts b/src/composables/useUpstreamValue.test.ts index c1ff9b17ba..8d886ad3df 100644 --- a/src/composables/useUpstreamValue.test.ts +++ b/src/composables/useUpstreamValue.test.ts @@ -1,11 +1,12 @@ -import { describe, expect, it, vi } from 'vitest' -import { reactive } from 'vue' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' +import type { UUID } from '@/lib/litegraph/src/utils/uuid' +import { useWidgetValueStore } from '@/stores/widgetValueStore' import type { WidgetState } from '@/stores/widgetValueStore' -import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' -import { asGraphId } from '@/world/entityIds' -import { registerWidgetInWorld } from '@/world/widgetWorldBridge' -import { getWorld, resetWorldInstance } from '@/world/worldInstance' +import { resetWorldInstance } from '@/world/worldInstance' import { boundsExtractor, @@ -133,18 +134,21 @@ describe('boundsExtractor', () => { }) }) -describe('useUpstreamValue (World-backed read path)', () => { - it('reads upstream node widgets via the World, not the Pinia store', () => { +describe('useUpstreamValue (store-backed read path)', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) resetWorldInstance() - const graphId = asGraphId('00000000-0000-0000-0000-000000000001') - const state = reactive({ + }) + + it('reads upstream node widgets via the widget value store', () => { + const graphId = '00000000-0000-0000-0000-000000000001' as UUID + const state = useWidgetValueStore().registerWidget(graphId, { nodeId: 'upstream-1' as NodeId, name: 'value', type: 'number', value: 7, options: {} }) - registerWidgetInWorld(getWorld(), graphId, state) const upstreamValue = useUpstreamValue( () => ({ nodeId: 'upstream-1', outputName: 'value' }), @@ -157,7 +161,6 @@ describe('useUpstreamValue (World-backed read path)', () => { }) it('returns undefined when no upstream linkage is provided', () => { - resetWorldInstance() const upstreamValue = useUpstreamValue( () => undefined, singleValueExtractor((v): v is number => typeof v === 'number') diff --git a/src/composables/useUpstreamValue.ts b/src/composables/useUpstreamValue.ts index da3e93159e..bdb51f2f00 100644 --- a/src/composables/useUpstreamValue.ts +++ b/src/composables/useUpstreamValue.ts @@ -1,12 +1,10 @@ import { computed } from 'vue' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' -import type { WidgetState } from '@/stores/widgetValueStore' import type { Bounds } from '@/renderer/core/layout/types' +import type { WidgetState } from '@/stores/widgetValueStore' +import { useWidgetValueStore } from '@/stores/widgetValueStore' import type { LinkedUpstreamInfo } from '@/types/simplifiedWidget' -import { asGraphId } from '@/world/entityIds' -import { getNodeWidgetsThroughWorld } from '@/world/widgetWorldBridge' -import { getWorld } from '@/world/worldInstance' type ValueExtractor = ( widgets: WidgetState[], @@ -18,17 +16,14 @@ export function useUpstreamValue( extractValue: ValueExtractor ) { const canvasStore = useCanvasStore() + const widgetValueStore = useWidgetValueStore() return computed(() => { const upstream = getLinkedUpstream() if (!upstream) return undefined const graphId = canvasStore.canvas?.graph?.rootGraph.id if (!graphId) return undefined - const widgets = getNodeWidgetsThroughWorld( - getWorld(), - asGraphId(graphId), - upstream.nodeId - ) + const widgets = widgetValueStore.getNodeWidgets(graphId, upstream.nodeId) return extractValue(widgets, upstream.outputName) }) } diff --git a/src/lib/litegraph/src/widgets/BaseWidget.ts b/src/lib/litegraph/src/widgets/BaseWidget.ts index c59f73f7a0..b87d4aad18 100644 --- a/src/lib/litegraph/src/widgets/BaseWidget.ts +++ b/src/lib/litegraph/src/widgets/BaseWidget.ts @@ -20,9 +20,6 @@ import type { import { usePromotionStore } from '@/stores/promotionStore' import type { WidgetState } from '@/stores/widgetValueStore' import { useWidgetValueStore } from '@/stores/widgetValueStore' -import { asGraphId } from '@/world/entityIds' -import { registerWidgetInWorld } from '@/world/widgetWorldBridge' -import { getWorld } from '@/world/worldInstance' export interface DrawWidgetOptions { /** The width of the node where this widget will be displayed. */ @@ -152,11 +149,6 @@ export abstract class BaseWidget value: this.value, nodeId }) - registerWidgetInWorld( - getWorld(), - asGraphId(graphId), - this._state as WidgetState - ) } constructor(widget: TWidget & { node: LGraphNode }) diff --git a/src/stores/widgetComponents.test.ts b/src/stores/widgetComponents.test.ts index ed6445fb60..d651125cc0 100644 --- a/src/stores/widgetComponents.test.ts +++ b/src/stores/widgetComponents.test.ts @@ -2,10 +2,14 @@ import { describe, expect, it } from 'vitest' import type { WidgetState } from '@/stores/widgetValueStore' import { asGraphId, nodeEntityId, widgetEntityId } from '@/world/entityIds' -import { registerWidgetInWorld } from '@/world/widgetWorldBridge' import { createWorld } from '@/world/world' -import { widgetParent } from './widgetComponents' +import type { WidgetValue } from './widgetComponents' +import { + WidgetContainerComponent, + WidgetValueComponent, + widgetParent +} from './widgetComponents' const graphId = asGraphId('00000000-0000-0000-0000-000000000001') @@ -16,9 +20,14 @@ function makeState(nodeId: string, name: string, value: unknown): WidgetState { describe('widgetParent', () => { it('returns the owning node entity for a widget', () => { const world = createWorld() - registerWidgetInWorld(world, graphId, makeState('node-1', 'seed', 1)) - const widgetId = widgetEntityId(graphId, 'node-1', 'seed') - expect(widgetParent(world, widgetId)).toBe(nodeEntityId(graphId, 'node-1')) + const state = makeState('node-1', 'seed', 1) + const widgetId = widgetEntityId(graphId, state.nodeId, state.name) + const ownerId = nodeEntityId(graphId, state.nodeId) + world.setComponent(widgetId, WidgetValueComponent, state as WidgetValue) + world.setComponent(ownerId, WidgetContainerComponent, { + widgetIds: [widgetId] + }) + expect(widgetParent(world, widgetId)).toBe(ownerId) }) it('returns undefined when no container references the widget', () => { diff --git a/src/world/entityIds.ts b/src/world/entityIds.ts index 659940f290..ee87e40b2e 100644 --- a/src/world/entityIds.ts +++ b/src/world/entityIds.ts @@ -13,7 +13,7 @@ import type { UUID } from '@/lib/litegraph/src/utils/uuid' import type { Brand } from './brand' -export type GraphId = Brand +type GraphId = Brand export type NodeEntityId = Brand export type WidgetEntityId = Brand diff --git a/src/world/widgetWorldBridge.test.ts b/src/world/widgetWorldBridge.test.ts deleted file mode 100644 index 5898a5c2e8..0000000000 --- a/src/world/widgetWorldBridge.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { - WidgetContainerComponent, - WidgetValueComponent -} from '@/stores/widgetComponents' -import type { WidgetState } from '@/stores/widgetValueStore' - -import { asGraphId, nodeEntityId, widgetEntityId } from './entityIds' -import { - getNodeWidgetsThroughWorld, - registerWidgetInWorld -} from './widgetWorldBridge' -import { createWorld } from './world' - -function makeState(nodeId: string, name: string, value: unknown): WidgetState { - return { nodeId, name, type: 'number', value, options: {} } -} - -const graphId = asGraphId('00000000-0000-0000-0000-000000000001') - -describe('registerWidgetInWorld', () => { - it('writes WidgetValue and updates WidgetContainer on the node', () => { - const world = createWorld() - const state = makeState('node-1', 'seed', 100) - - registerWidgetInWorld(world, graphId, state) - - const widgetId = widgetEntityId(graphId, 'node-1', 'seed') - const nodeId = nodeEntityId(graphId, 'node-1') - expect(world.getComponent(widgetId, WidgetValueComponent)?.value).toBe(100) - expect( - world.getComponent(nodeId, WidgetContainerComponent)?.widgetIds - ).toEqual([widgetId]) - }) - - it('shares object identity with the registered state (reactive bridge)', () => { - const world = createWorld() - const state = makeState('node-1', 'seed', 100) - registerWidgetInWorld(world, graphId, state) - - state.value = 200 - const widgetId = widgetEntityId(graphId, 'node-1', 'seed') - expect(world.getComponent(widgetId, WidgetValueComponent)?.value).toBe(200) - }) - - it('appends additional widgets to the same node container', () => { - const world = createWorld() - registerWidgetInWorld(world, graphId, makeState('node-1', 'seed', 1)) - registerWidgetInWorld(world, graphId, makeState('node-1', 'cfg', 7)) - - const nodeId = nodeEntityId(graphId, 'node-1') - const ids = world.getComponent(nodeId, WidgetContainerComponent)?.widgetIds - expect(ids).toEqual([ - widgetEntityId(graphId, 'node-1', 'seed'), - widgetEntityId(graphId, 'node-1', 'cfg') - ]) - }) - - it('does not duplicate widgetIds when the same widget re-registers', () => { - const world = createWorld() - const state = makeState('node-1', 'seed', 1) - registerWidgetInWorld(world, graphId, state) - registerWidgetInWorld(world, graphId, state) - - const nodeId = nodeEntityId(graphId, 'node-1') - expect( - world.getComponent(nodeId, WidgetContainerComponent)?.widgetIds - ).toHaveLength(1) - }) -}) - -describe('getNodeWidgetsThroughWorld', () => { - it('returns all widget states attached to a node', () => { - const world = createWorld() - registerWidgetInWorld(world, graphId, makeState('node-1', 'seed', 1)) - registerWidgetInWorld(world, graphId, makeState('node-1', 'cfg', 7)) - registerWidgetInWorld(world, graphId, makeState('node-2', 'seed', 99)) - - const widgets = getNodeWidgetsThroughWorld(world, graphId, 'node-1') - expect(widgets.map((w) => w.name).sort()).toEqual(['cfg', 'seed']) - }) - - it('returns an empty array for unknown nodes', () => { - const world = createWorld() - expect(getNodeWidgetsThroughWorld(world, graphId, 'missing')).toEqual([]) - }) -}) diff --git a/src/world/widgetWorldBridge.ts b/src/world/widgetWorldBridge.ts deleted file mode 100644 index 242422ca6e..0000000000 --- a/src/world/widgetWorldBridge.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { NodeId } from '@/lib/litegraph/src/LGraphNode' -import type { WidgetValue } from '@/stores/widgetComponents' -import { - WidgetContainerComponent, - WidgetValueComponent -} from '@/stores/widgetComponents' -import type { WidgetState } from '@/stores/widgetValueStore' - -import type { GraphId } from './entityIds' -import { nodeEntityId, widgetEntityId } from './entityIds' -import type { World } from './world' - -/** - * Slice 1 bridge: writes widget entities into the World whenever - * `WidgetValueStore.registerWidget` runs. The `state` argument is the - * SAME reactive object the store holds — sharing identity preserves Vue - * tracking across both read paths. - */ -export function registerWidgetInWorld( - world: World, - graphId: GraphId, - state: WidgetState -): void { - const widgetId = widgetEntityId(graphId, state.nodeId, state.name) - // `state` IS the reactive object owned by `WidgetValueStore`; sharing the - // reference is intentional — Vue tracking flows through both read paths - // during the slice-1 bridge window. The wider WidgetState shape collapses - // to `WidgetValue` at the component-key boundary. - world.setComponent(widgetId, WidgetValueComponent, state as WidgetValue) - - const nodeId = nodeEntityId(graphId, state.nodeId) - const container = world.getComponent(nodeId, WidgetContainerComponent) - if (!container) { - world.setComponent(nodeId, WidgetContainerComponent, { - widgetIds: [widgetId] - }) - return - } - if (!container.widgetIds.includes(widgetId)) { - container.widgetIds.push(widgetId) - } -} - -/** - * Look up all widget value states attached to a node, going through the - * World rather than the Pinia store. Used by `useUpstreamValue`. - */ -export function getNodeWidgetsThroughWorld( - world: World, - graphId: GraphId, - nodeId: NodeId -): WidgetState[] { - const owner = nodeEntityId(graphId, nodeId) - const container = world.getComponent(owner, WidgetContainerComponent) - if (!container) return [] - const widgets: WidgetState[] = [] - for (const widgetId of container.widgetIds) { - const value = world.getComponent(widgetId, WidgetValueComponent) - if (value) widgets.push(value as unknown as WidgetState) - } - return widgets -}