diff --git a/integration-tests/create-state/state-modifications.test.ts b/integration-tests/create-state/state-modifications.test.ts index 266cd1b..e1dddb7 100644 --- a/integration-tests/create-state/state-modifications.test.ts +++ b/integration-tests/create-state/state-modifications.test.ts @@ -185,5 +185,43 @@ describe("state modifications", () => { expect(emissions).toHaveLength(1); expect(emissions[0]).toEqual([4, 6]); }); + + test("schedules a dirty parent outside the current flush queue", () => { + const choices = new StateCore(["first"]); + const disabled = new StateCore(false); + const canAdd = choices.map((items) => items.length < 10); + const combined = disabled.combine(canAdd); + const subscriber = vi.fn(); + + combined.on(subscriber); + disabled.set(true); + + expect(subscriber).toHaveBeenCalledOnce(); + expect(subscriber).toHaveBeenCalledWith([true, true], emptyValue); + }); + + test("notifies and invalidates descendants when scheduling a deep dirty parent", () => { + const source = new StateCore(1); + const first = source.map((value) => value * 2); + const second = first.map((value) => value + 1); + const combined = source.combine(second); + const descendant = second.map((value) => value * 10); + const secondSubscriber = vi.fn(); + const descendantSubscriber = vi.fn(); + + expect(combined.get()).toEqual([1, 3]); + expect(descendant.get()).toBe(30); + second.on(secondSubscriber); + descendant.on(descendantSubscriber); + + source.set(2); + + expect(secondSubscriber).toHaveBeenCalledOnce(); + expect(secondSubscriber).toHaveBeenCalledWith(5, 3); + expect(descendantSubscriber).toHaveBeenCalledOnce(); + expect(descendantSubscriber).toHaveBeenCalledWith(50, 30); + expect(combined.get()).toEqual([2, 5]); + expect(descendant.get()).toBe(50); + }); }); }); diff --git a/src/create-state/state-core.ts b/src/create-state/state-core.ts index 7bf3168..306a560 100644 --- a/src/create-state/state-core.ts +++ b/src/create-state/state-core.ts @@ -210,7 +210,22 @@ class StateCore { if (!child._dirty) continue; - if (child.hasDirtyParents()) { + // in case a child has dirty parents, we need to recompute them + // first (potentially recursively). If that happens, we need to + // process them first, and then process this child node again. + let shouldRescheduleChild = false; + child._parents.forEach((parentNode) => { + if (parentNode._dirty) { + if (!queued.has(parentNode)) { + queue.push(parentNode); + queued.add(parentNode); + } + + shouldRescheduleChild = true; + } + }); + + if (shouldRescheduleChild) { queue.push(child); queued.add(child); continue; @@ -232,18 +247,6 @@ class StateCore { } } - private hasDirtyParents() { - let hasDirtyParent = false; - - this._parents.forEach((parent) => { - if (parent._dirty) { - hasDirtyParent = true; - } - }); - - return hasDirtyParent; - } - private notifySubscribers(value: CoreValue, prevValue: CoreValue) { if (value === emptyValue) { return;