Skip to content

Commit bff4c21

Browse files
ryansolidbrenelzcursoragent
committed
fix: handle pending store reads in server projections
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent baa47d2 commit bff4c21

3 files changed

Lines changed: 69 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"solid-js": patch
3+
---
4+
5+
Handle pending store reads in server projections during SSR.

packages/solid/src/server/signals.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,7 +1080,34 @@ export function createProjection<T extends object>(
10801080
const draft = useProxy ? createDeepProxy(state as any, patches) : (state as any as T);
10811081

10821082
const runProjection = () => runWithOwner(owner, () => fn(draft));
1083-
const result = runProjection();
1083+
let result: void | T | Promise<void | T> | AsyncIterable<void | T>;
1084+
try {
1085+
result = runProjection();
1086+
} catch (error) {
1087+
if (!(error instanceof NotReadyError)) throw error;
1088+
1089+
const deferred = createDeferredPromise<T>();
1090+
const [pending, markReady] = createPendingProxy(state, deferred.promise);
1091+
settleServerAsync<void | T, T>(
1092+
Promise.reject(error),
1093+
() => runProjection() as void | T | PromiseLike<void | T>,
1094+
deferred,
1095+
(value: void | T) => {
1096+
if (value !== undefined && value !== state && value !== draft) {
1097+
Object.assign(state, value);
1098+
}
1099+
markReady();
1100+
return state as T;
1101+
},
1102+
(_error: any) => {
1103+
markReady();
1104+
},
1105+
() => disposed
1106+
);
1107+
if (ctx?.async && !getContext(NoHydrateContext) && owner.id)
1108+
ctx.serialize(owner.id, deferred.promise, options?.deferStream);
1109+
return pending;
1110+
}
10841111

10851112
// Async iterable (generator)
10861113
const iteratorFn = (result as any)?.[Symbol.asyncIterator];

packages/solid/test/server/ssr-async.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
createRoot,
55
createMemo,
66
createSignal,
7+
createStore,
78
createProjection,
89
NotReadyError,
910
getOwner,
@@ -567,6 +568,41 @@ describe("Loading SSR Async", () => {
567568
expect([...fragmentResults.values()][0]).toBe("<div>HELLO</div>");
568569
});
569570

571+
test("sync projection callback can wrap a pending async store read", async () => {
572+
const { context, registeredFragments, fragmentResults } = createMockSSRContext();
573+
sharedConfig.context = context;
574+
575+
const d = deferred<{ id: string; name: string }[]>();
576+
577+
createRoot(
578+
() => {
579+
Loading({
580+
fallback: "Loading...",
581+
get children() {
582+
const [users] = createStore(() => d.promise, [] as { id: string; name: string }[]);
583+
const projected = createProjection(
584+
() => users.map(user => ({ ...user, label: user.name.toUpperCase() })),
585+
[] as { id: string; name: string; label: string }[]
586+
);
587+
return ssr(["<div>", "</div>"], () =>
588+
projected.map(user => user.label).join(",")
589+
) as any;
590+
}
591+
});
592+
},
593+
{ id: "t" }
594+
);
595+
596+
expect(registeredFragments.size).toBe(1);
597+
598+
d.resolve([{ id: "1", name: "hello" }]);
599+
await tick();
600+
await tick();
601+
602+
expect(fragmentResults.size).toBe(1);
603+
expect([...fragmentResults.values()][0]).toBe("<div>HELLO</div>");
604+
});
605+
570606
test("async iterator memo can wrap a pending async read before first yield", async () => {
571607
const { context, fragmentResults } = createMockSSRContext();
572608
sharedConfig.context = context;

0 commit comments

Comments
 (0)