From 90dd32b6e264ec619182a8cdd8b1e9d7c0df733c Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 20 Nov 2025 16:26:25 -0700 Subject: [PATCH] thunkify --- .../svelte/src/internal/server/hydratable.js | 49 +++++++++++++------ .../src/internal/server/render-context.js | 1 + .../svelte/src/internal/server/renderer.js | 14 ++++-- .../svelte/src/internal/server/types.d.ts | 12 ++--- 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/svelte/src/internal/server/hydratable.js b/packages/svelte/src/internal/server/hydratable.js index 8bdb6ca15d..d8979ad7f5 100644 --- a/packages/svelte/src/internal/server/hydratable.js +++ b/packages/svelte/src/internal/server/hydratable.js @@ -6,6 +6,7 @@ import * as e from './errors.js'; import * as devalue from 'devalue'; import { get_stack } from './dev.js'; import { DEV } from 'esm-env'; +import { deferred } from '../shared/utils.js'; /** * @template T @@ -23,8 +24,10 @@ export function hydratable(key, fn) { let entry = hydratable.lookup.get(key); if (entry !== undefined) { - if (DEV) { - compare(key, entry, encode(key, fn(), [])); + if (DEV && entry.dev) { + const comparison = compare(key, entry, encode(key, fn(), [])); + comparison.catch(() => {}); + hydratable.comparisons.push(comparison); } return /** @type {T} */ (entry.value); @@ -49,12 +52,25 @@ function encode(key, value, values, unresolved) { const entry = { value, index: -1 }; if (DEV) { - entry.stack = get_stack(`hydratable"`)?.stack; + entry.dev = { + serialized: undefined, + serialize_work: [], + stack: get_stack('hydratable')?.stack + }; } + let needs_thunk = false; let serialized = devalue.uneval(entry.value, (value, uneval) => { if (value instanceof Promise) { - const serialize_promise = value.then((v) => `r(${uneval(v)})`); + needs_thunk = true; + /** @param {string} val */ + const scoped_uneval = (val) => { + const raw = `r(${uneval(val)})`; + const result = needs_thunk ? `()=>(${raw})` : raw; + needs_thunk = false; + return result; + }; + const serialize_promise = value.then(scoped_uneval); unresolved?.set(serialize_promise, key); serialize_promise.finally(() => unresolved?.delete(serialize_promise)); @@ -66,11 +82,12 @@ function encode(key, value, values, unresolved) { // of a given hydratable are identical with a simple string comparison const result = DEV ? `d("${index}")` : `d(${index})`; - if (DEV) { - (entry.promises ??= []).push( + if (DEV && entry.dev) { + const { dev } = entry; + dev.serialize_work.push( serialize_promise.then((s) => { serialized = serialized.replace(result, s); - entry.serialized = serialized; + dev.serialized = serialized; }) ); } @@ -79,7 +96,8 @@ function encode(key, value, values, unresolved) { } }); - entry.index = values.push(serialized) - 1; + entry.index = values.push(needs_thunk ? `()=>(${serialized})` : serialized) - 1; + needs_thunk = false; return entry; } @@ -90,21 +108,22 @@ function encode(key, value, values, unresolved) { * @param {HydratableLookupEntry} b */ async function compare(key, a, b) { - for (const p of a.promises ?? []) { + // note: these need to be loops (as opposed to Promise.all) because + // additional promises can get pushed to them while we're awaiting + // an earlier one + for (const p of a.dev?.serialize_work ?? []) { await p; } - for (const p of b.promises ?? []) { + for (const p of b.dev?.serialize_work ?? []) { await p; } - if (a.serialized !== b.serialized) { - // TODO right now this causes an unhandled rejection — it - // needs to happen somewhere else + if (a.dev?.serialized !== b.dev?.serialized) { e.hydratable_clobbering( key, - a.stack ?? '', - b.stack ?? '' + a.dev?.stack ?? '', + b.dev?.stack ?? '' ); } } diff --git a/packages/svelte/src/internal/server/render-context.js b/packages/svelte/src/internal/server/render-context.js index 5aa25d0724..3c71ae2754 100644 --- a/packages/svelte/src/internal/server/render-context.js +++ b/packages/svelte/src/internal/server/render-context.js @@ -58,6 +58,7 @@ export async function with_render_context(fn) { hydratable: { lookup: new Map(), values: [], + comparisons: [], unresolved_promises: new Map() } }; diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 601494aa47..94b2426a81 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -583,11 +583,16 @@ export class Renderer { // serialize it, so we're blocking the response on useless content. w.unresolved_hydratable( key, - DEV ? ctx.lookup.get(key)?.stack ?? 'unavailable' : 'unavailable in production builds', + ctx.lookup.get(key)?.dev?.stack ?? '', await promise ); } + for (const comparison of ctx.comparisons) { + // these reject if there's a mismatch + await comparison; + } + return await Renderer.#hydratable_block(ctx, []); } @@ -666,8 +671,11 @@ export class Renderer { `; diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index b3cf80d026..6728d4752e 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -18,17 +18,17 @@ export interface SSRContext { export interface HydratableLookupEntry { value: unknown; index: number; - /** dev-only */ - promises?: Array>; - /** dev-only */ - serialized?: string; - /** dev-only */ - stack?: string; + dev?: { + serialize_work: Array>; + serialized: string | undefined; + stack: string | undefined; + }; } export interface HydratableContext { lookup: Map; values: MaybePromise[]; + comparisons: Promise[]; unresolved_promises: Map, string>; }