From b4b2061392bdbdde4b61c06b5cc37d8199ac2b78 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Tue, 2 Jun 2026 20:12:24 -0500 Subject: [PATCH] fix: handle pending store reads in server projections --- packages/solid/src/server/signals.ts | 29 +++++++++++++++- packages/solid/test/server/ssr-async.spec.ts | 36 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/solid/src/server/signals.ts b/packages/solid/src/server/signals.ts index 772976a5e..8738cb3c9 100644 --- a/packages/solid/src/server/signals.ts +++ b/packages/solid/src/server/signals.ts @@ -1080,7 +1080,34 @@ export function createProjection( const draft = useProxy ? createDeepProxy(state as any, patches) : (state as any as T); const runProjection = () => runWithOwner(owner, () => fn(draft)); - const result = runProjection(); + let result: void | T | Promise | AsyncIterable; + try { + result = runProjection(); + } catch (error) { + if (!(error instanceof NotReadyError)) throw error; + + const deferred = createDeferredPromise(); + const [pending, markReady] = createPendingProxy(state, deferred.promise); + settleServerAsync( + Promise.reject(error), + () => runProjection() as void | T | PromiseLike, + deferred, + (value: void | T) => { + if (value !== undefined && value !== state && value !== draft) { + Object.assign(state, value); + } + markReady(); + return state as T; + }, + (error: any) => { + markReady(); + }, + () => disposed + ); + if (ctx?.async && !getContext(NoHydrateContext) && owner.id) + ctx.serialize(owner.id, deferred.promise, options?.deferStream); + return pending; + } // Async iterable (generator) const iteratorFn = (result as any)?.[Symbol.asyncIterator]; diff --git a/packages/solid/test/server/ssr-async.spec.ts b/packages/solid/test/server/ssr-async.spec.ts index 4ab7da52f..d5f8e8239 100644 --- a/packages/solid/test/server/ssr-async.spec.ts +++ b/packages/solid/test/server/ssr-async.spec.ts @@ -4,6 +4,7 @@ import { createRoot, createMemo, createSignal, + createStore, createProjection, NotReadyError, getOwner, @@ -567,6 +568,41 @@ describe("Loading SSR Async", () => { expect([...fragmentResults.values()][0]).toBe("
HELLO
"); }); + test("sync projection callback can wrap a pending async store read", async () => { + const { context, registeredFragments, fragmentResults } = createMockSSRContext(); + sharedConfig.context = context; + + const d = deferred<{ id: string; name: string }[]>(); + + createRoot( + () => { + Loading({ + fallback: "Loading...", + get children() { + const [users] = createStore(() => d.promise, [] as { id: string; name: string }[]); + const projected = createProjection( + () => users.map(user => ({ ...user, label: user.name.toUpperCase() })), + [] as { id: string; name: string; label: string }[] + ); + return ssr(["
", "
"], () => + projected.map(user => user.label).join(",") + ) as any; + } + }); + }, + { id: "t" } + ); + + expect(registeredFragments.size).toBe(1); + + d.resolve([{ id: "1", name: "hello" }]); + await tick(); + await tick(); + + expect(fragmentResults.size).toBe(1); + expect([...fragmentResults.values()][0]).toBe("
HELLO
"); + }); + test("async iterator memo can wrap a pending async read before first yield", async () => { const { context, fragmentResults } = createMockSSRContext(); sharedConfig.context = context;