Skip to content

Commit 1b5e292

Browse files
fix(widgets): reconcile widget store entry when widget type changes (#13774)
## Summary On subgraph convert, a widget could render with the wrong type (a multiline text widget appeared as an int widget until a page reload) because the widget value store returned a stale entry keyed by `graphId:nodeId:name`. ## Changes - **What**: `registerWidget` returned the existing store entry unconditionally (idempotent to preserve edited values across re-render). But when a subgraph convert reuses the same `graphId:nodeId:name` key for a *different* widget, the stale entry won — wrong type, wrong value, only corrected by a reload that rehydrates the store from serialized data. Now the idempotent return applies only when `existing.type === init.type`; on a type mismatch the id belongs to a different widget, so the store overwrites with the live type/value. Same-type re-registration still preserves edited values (unchanged behavior). - **Breaking**: none. ## Review Focus - @DrJKL @AustinMroz — this changes the idempotency semantic in the #12617 widget store (#13073 collision class). The discriminator is `type`; please sanity-check that no legitimate re-registration changes `type` for the same logical widget. Reload already produces the correct result, which is the evidence the in-memory path diverges here. - Stacked on #13773 (base = `fix/widget-store-custom-node-widgets`). Rebase to `main` once that lands. QA source: staging 1.47 — "multiline text widget converts to int inside/outside nested subgraph; refresh fixes it", working on 1.45.21. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ba810e1 commit 1b5e292

2 files changed

Lines changed: 18 additions & 1 deletion

File tree

src/stores/widgetValueStore.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,21 @@ describe('useWidgetValueStore', () => {
112112
expect(second.value).toBe(99)
113113
})
114114

115+
it('replaces a stale entry when the widget type changes', () => {
116+
const store = useWidgetValueStore()
117+
const first = store.registerWidget(seedA, state('number', 5))!
118+
first.value = 42
119+
120+
// After a subgraph convert the graphId:nodeId:name key can be reused by a
121+
// different widget. A type mismatch means it is not the same widget, so
122+
// the live type/value must win rather than the stale entry (BUG: a text
123+
// widget rendered as int until reload).
124+
const reconciled = store.registerWidget(seedA, state('string', 'hello'))!
125+
expect(reconciled.type).toBe('string')
126+
expect(reconciled.value).toBe('hello')
127+
expect(store.getWidget(seedA)?.type).toBe('string')
128+
})
129+
115130
it('registers a widget with all properties', () => {
116131
const store = useWidgetValueStore()
117132
const registered = store.registerWidget(

src/stores/widgetValueStore.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ export const useWidgetValueStore = defineStore('widgetValue', () => {
3737
}
3838

3939
const existing = getWidget(widgetId)
40-
if (existing) return existing as WidgetState<TValue>
40+
if (existing && existing.type === init.type) {
41+
return existing as WidgetState<TValue>
42+
}
4143

4244
const { graphId, nodeId, name } = parseWidgetId(widgetId)
4345
const state: WidgetState<TValue> = {

0 commit comments

Comments
 (0)