elliott/hydratable
Elliott Johnson 19 hours ago
parent 2ec34a23b2
commit 90dd32b6e2

@ -6,6 +6,7 @@ import * as e from './errors.js';
import * as devalue from 'devalue'; import * as devalue from 'devalue';
import { get_stack } from './dev.js'; import { get_stack } from './dev.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { deferred } from '../shared/utils.js';
/** /**
* @template T * @template T
@ -23,8 +24,10 @@ export function hydratable(key, fn) {
let entry = hydratable.lookup.get(key); let entry = hydratable.lookup.get(key);
if (entry !== undefined) { if (entry !== undefined) {
if (DEV) { if (DEV && entry.dev) {
compare(key, entry, encode(key, fn(), [])); const comparison = compare(key, entry, encode(key, fn(), []));
comparison.catch(() => {});
hydratable.comparisons.push(comparison);
} }
return /** @type {T} */ (entry.value); return /** @type {T} */ (entry.value);
@ -49,12 +52,25 @@ function encode(key, value, values, unresolved) {
const entry = { value, index: -1 }; const entry = { value, index: -1 };
if (DEV) { 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) => { let serialized = devalue.uneval(entry.value, (value, uneval) => {
if (value instanceof Promise) { 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); unresolved?.set(serialize_promise, key);
serialize_promise.finally(() => unresolved?.delete(serialize_promise)); 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 // of a given hydratable are identical with a simple string comparison
const result = DEV ? `d("${index}")` : `d(${index})`; const result = DEV ? `d("${index}")` : `d(${index})`;
if (DEV) { if (DEV && entry.dev) {
(entry.promises ??= []).push( const { dev } = entry;
dev.serialize_work.push(
serialize_promise.then((s) => { serialize_promise.then((s) => {
serialized = serialized.replace(result, 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; return entry;
} }
@ -90,21 +108,22 @@ function encode(key, value, values, unresolved) {
* @param {HydratableLookupEntry} b * @param {HydratableLookupEntry} b
*/ */
async function compare(key, a, 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; await p;
} }
for (const p of b.promises ?? []) { for (const p of b.dev?.serialize_work ?? []) {
await p; await p;
} }
if (a.serialized !== b.serialized) { if (a.dev?.serialized !== b.dev?.serialized) {
// TODO right now this causes an unhandled rejection — it
// needs to happen somewhere else
e.hydratable_clobbering( e.hydratable_clobbering(
key, key,
a.stack ?? '<missing stack trace>', a.dev?.stack ?? '<missing stack trace>',
b.stack ?? '<missing stack trace>' b.dev?.stack ?? '<missing stack trace>'
); );
} }
} }

@ -58,6 +58,7 @@ export async function with_render_context(fn) {
hydratable: { hydratable: {
lookup: new Map(), lookup: new Map(),
values: [], values: [],
comparisons: [],
unresolved_promises: new Map() unresolved_promises: new Map()
} }
}; };

@ -583,11 +583,16 @@ export class Renderer {
// serialize it, so we're blocking the response on useless content. // serialize it, so we're blocking the response on useless content.
w.unresolved_hydratable( w.unresolved_hydratable(
key, key,
DEV ? ctx.lookup.get(key)?.stack ?? 'unavailable' : 'unavailable in production builds', ctx.lookup.get(key)?.dev?.stack ?? '<missing stack trace>',
await promise await promise
); );
} }
for (const comparison of ctx.comparisons) {
// these reject if there's a mismatch
await comparison;
}
return await Renderer.#hydratable_block(ctx, []); return await Renderer.#hydratable_block(ctx, []);
} }
@ -666,8 +671,11 @@ export class Renderer {
<script> <script>
{ {
const r = (v) => Promise.resolve(v); const r = (v) => Promise.resolve(v);
const v = [${values.map((v) => `() => (${v})`).join(',')}]; const v = [${values.join(',')}];
function d(i) { return v[i]() }; function d(i) {
const value = v[i];
return typeof value === 'function' ? value() : value;
};
const sv = window.__svelte ??= {};${Renderer.#used_hydratables(ctx.lookup)}${Renderer.#unused_hydratables(unused_keys)} const sv = window.__svelte ??= {};${Renderer.#used_hydratables(ctx.lookup)}${Renderer.#unused_hydratables(unused_keys)}
} }
</script>`; </script>`;

@ -18,17 +18,17 @@ export interface SSRContext {
export interface HydratableLookupEntry { export interface HydratableLookupEntry {
value: unknown; value: unknown;
index: number; index: number;
/** dev-only */ dev?: {
promises?: Array<Promise<void>>; serialize_work: Array<Promise<void>>;
/** dev-only */ serialized: string | undefined;
serialized?: string; stack: string | undefined;
/** dev-only */ };
stack?: string;
} }
export interface HydratableContext { export interface HydratableContext {
lookup: Map<string, HydratableLookupEntry>; lookup: Map<string, HydratableLookupEntry>;
values: MaybePromise<string>[]; values: MaybePromise<string>[];
comparisons: Promise<void>[];
unresolved_promises: Map<Promise<string>, string>; unresolved_promises: Map<Promise<string>, string>;
} }

Loading…
Cancel
Save