From ff63d1263e9aacc126ec67df05d3a67bf5d999f1 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Sat, 6 Jun 2026 21:56:12 -0500 Subject: [PATCH 1/2] fix: track nested deep optimistic store pending state --- packages/solid-signals/src/store/utils.ts | 3 +- .../tests/store/createOptimisticStore.test.ts | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/solid-signals/src/store/utils.ts b/packages/solid-signals/src/store/utils.ts index daa9209b1..027462abc 100644 --- a/packages/solid-signals/src/store/utils.ts +++ b/packages/solid-signals/src/store/utils.ts @@ -9,6 +9,7 @@ import { getPropertyDescriptor, isWrappable, STORE_OVERRIDE, + STORE_LOOKUP, STORE_VALUE, storeLookup, trackSelf, @@ -38,7 +39,7 @@ function snapshotImpl( : target[STORE_VALUE] ); item = target[STORE_VALUE]; - lookup = storeLookup; + lookup = target[STORE_LOOKUP] ?? storeLookup; } else { isArray = Array.isArray(item); map.set(item, item); diff --git a/packages/solid-signals/tests/store/createOptimisticStore.test.ts b/packages/solid-signals/tests/store/createOptimisticStore.test.ts index 5f1ed98aa..ae5809f12 100644 --- a/packages/solid-signals/tests/store/createOptimisticStore.test.ts +++ b/packages/solid-signals/tests/store/createOptimisticStore.test.ts @@ -1720,6 +1720,56 @@ describe("createOptimisticStore", () => { expect(pendingValues.at(-1)).toBe(false); }); + it("isPending tracks deep nested optimistic store reads before async refresh starts", async () => { + let state: Refreshable<{ items: { name: string }[] }>; + let setState: (fn: (s: { items: { name: string }[] }) => void) => void; + let save!: () => Promise; + let resolveAction!: () => void; + const pendingValues: boolean[] = []; + + createRoot(() => { + [state, setState] = createOptimisticStore(async () => ({ items: [{ name: "Initial" }] }), { + items: [{ name: "Initial" }] + }); + + createRenderEffect( + () => isPending(() => deep(state)), + value => { + pendingValues.push(value); + } + ); + + save = action(function* () { + setState(s => { + s.items[0].name = "Modified"; + }); + expect(isPending(() => deep(state))).toBe(true); + yield new Promise(resolve => { + resolveAction = resolve; + }); + }); + }); + + flush(); + await Promise.resolve(); + await Promise.resolve(); + flush(); + expect(pendingValues.at(-1)).toBe(false); + + const actionPromise = save(); + flush(); + + expect(state!.items[0].name).toBe("Modified"); + expect(pendingValues.at(-1)).toBe(true); + + resolveAction(); + await actionPromise; + await Promise.resolve(); + flush(); + + expect(pendingValues.at(-1)).toBe(false); + }); + it("isPending clears for deep optimistic store reads when fresh projection data lands", async () => { let serverCount = 0; const fetches: Array<() => void> = []; From e17633c3a450ce635e582b9b3e2f03ded150d51e Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Sat, 6 Jun 2026 22:19:22 -0700 Subject: [PATCH 2/2] test: cover nested optimistic deep reads Co-authored-by: Cursor --- .../fix-nested-optimistic-store-pending.md | 5 ++ .../tests/store/createOptimisticStore.test.ts | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .changeset/fix-nested-optimistic-store-pending.md diff --git a/.changeset/fix-nested-optimistic-store-pending.md b/.changeset/fix-nested-optimistic-store-pending.md new file mode 100644 index 000000000..3ba79c207 --- /dev/null +++ b/.changeset/fix-nested-optimistic-store-pending.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +Track pending state for nested deep optimistic store reads. diff --git a/packages/solid-signals/tests/store/createOptimisticStore.test.ts b/packages/solid-signals/tests/store/createOptimisticStore.test.ts index ae5809f12..5b97fff28 100644 --- a/packages/solid-signals/tests/store/createOptimisticStore.test.ts +++ b/packages/solid-signals/tests/store/createOptimisticStore.test.ts @@ -1770,6 +1770,56 @@ describe("createOptimisticStore", () => { expect(pendingValues.at(-1)).toBe(false); }); + it("isPending tracks deep optimistic store reads from a nested proxy", async () => { + let state: Refreshable<{ items: { name: string }[] }>; + let setState: (fn: (s: { items: { name: string }[] }) => void) => void; + let save!: () => Promise; + let resolveAction!: () => void; + const pendingValues: boolean[] = []; + + createRoot(() => { + [state, setState] = createOptimisticStore(async () => ({ items: [{ name: "Initial" }] }), { + items: [{ name: "Initial" }] + }); + + createRenderEffect( + () => isPending(() => deep(state.items)), + value => { + pendingValues.push(value); + } + ); + + save = action(function* () { + setState(s => { + s.items[0].name = "Modified"; + }); + expect(isPending(() => deep(state.items))).toBe(true); + yield new Promise(resolve => { + resolveAction = resolve; + }); + }); + }); + + flush(); + await Promise.resolve(); + await Promise.resolve(); + flush(); + expect(pendingValues.at(-1)).toBe(false); + + const actionPromise = save(); + flush(); + + expect(state!.items[0].name).toBe("Modified"); + expect(pendingValues.at(-1)).toBe(true); + + resolveAction(); + await actionPromise; + await Promise.resolve(); + flush(); + + expect(pendingValues.at(-1)).toBe(false); + }); + it("isPending clears for deep optimistic store reads when fresh projection data lands", async () => { let serverCount = 0; const fetches: Array<() => void> = [];