From 541c459adcfb14eefc908eb90056028e8b56f110 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 00:14:56 +0200 Subject: [PATCH 01/25] add universal load to /streaming/server test --- .../kit/test/apps/basics/src/routes/streaming/server/+page.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/kit/test/apps/basics/src/routes/streaming/server/+page.js diff --git a/packages/kit/test/apps/basics/src/routes/streaming/server/+page.js b/packages/kit/test/apps/basics/src/routes/streaming/server/+page.js new file mode 100644 index 000000000000..a5673a92a7b2 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/streaming/server/+page.js @@ -0,0 +1,3 @@ +export function load({ data }) { + return { ...data }; +} From a175295c3771150799ccdd1ba4771e4d45d98862 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 00:17:01 +0200 Subject: [PATCH 02/25] revert #14268 --- .../kit/src/runtime/server/page/load_data.js | 42 ++++--------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 8d06de88038e..9eacf46b7b22 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -1,12 +1,11 @@ +import { merge_tracing, with_request_store } from '@sveltejs/kit/internal/server'; import { DEV } from 'esm-env'; -import * as devalue from 'devalue'; import { disable_search, make_trackable } from '../../../utils/url.js'; import { validate_depends, validate_load_response } from '../../shared.js'; -import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server'; import { record_span } from '../../telemetry/record_span.js'; -import { clarify_devalue_error, get_node_type } from '../utils.js'; import { base64_encode, text_decoder } from '../../utils.js'; import { NULL_BODY_STATUS } from '../constants.js'; +import { get_node_type } from '../utils.js'; /** * Calls the user's server `load` function. @@ -234,38 +233,11 @@ export async function load_data({ }, fn: async (current) => { const traced_event = merge_tracing(event, current); - return await with_request_store({ event: traced_event, state: event_state }, () => { - /** @type {Record | null} */ - let data = null; - - return load.call(null, { + return await with_request_store({ event: traced_event, state: event_state }, () => + load.call(null, { url: event.url, params: event.params, - get data() { - if (data === null && server_data_node?.data != null) { - /** @type {Record any>} */ - const reducers = {}; - - /** @type {Record any>} */ - const revivers = {}; - - for (const key in event_state.transport) { - reducers[key] = event_state.transport[key].encode; - revivers[key] = event_state.transport[key].decode; - } - - // run it through devalue so that the developer can't accidentally mutate it - try { - data = devalue.parse(devalue.stringify(server_data_node.data, reducers), revivers); - } catch (e) { - // @ts-expect-error - e.path = e.path.slice(1); - throw new Error(clarify_devalue_error(event, /** @type {any} */ (e))); - } - } - - return data; - }, + data: server_data_node?.data ?? null, route: event.route, fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts), setHeaders: event.setHeaders, @@ -273,8 +245,8 @@ export async function load_data({ parent, untrack: (fn) => fn(), tracing: traced_event.tracing - }); - }); + }) + ); } }); From 663f72811751e7bc76c4a68656d71f17d970b9ab Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 00:21:54 +0200 Subject: [PATCH 03/25] server data serializer --- packages/kit/src/runtime/server/data/index.js | 15 +- .../runtime/server/page/data_serializer.js | 207 ++++++++++++++++++ packages/kit/src/runtime/server/page/index.js | 43 ++-- .../kit/src/runtime/server/page/render.js | 127 ++--------- .../runtime/server/page/respond_with_error.js | 12 +- .../kit/src/runtime/server/page/types.d.ts | 22 +- packages/kit/src/runtime/server/respond.js | 54 ++--- 7 files changed, 314 insertions(+), 166 deletions(-) create mode 100644 packages/kit/src/runtime/server/page/data_serializer.js diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 37fc409ab42d..b5aa34e3f5da 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -1,13 +1,14 @@ import { text } from '@sveltejs/kit'; -import { HttpError, SvelteKitError, Redirect } from '@sveltejs/kit/internal'; +import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal'; +import * as devalue from 'devalue'; import { normalize_error } from '../../../utils/error.js'; import { once } from '../../../utils/functions.js'; -import { load_server_data } from '../page/load_data.js'; -import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js'; -import { normalize_path } from '../../../utils/url.js'; -import * as devalue from 'devalue'; import { create_async_iterator } from '../../../utils/streaming.js'; +import { normalize_path } from '../../../utils/url.js'; import { text_encoder } from '../../utils.js'; +import { server_data_serializer_json } from '../page/data_serializer.js'; +import { load_server_data } from '../page/load_data.js'; +import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js'; /** * @param {import('@sveltejs/kit').RequestEvent} event @@ -120,7 +121,9 @@ export async function render_data( ) ); - const { data, chunks } = get_data_json(event, event_state, options, nodes); + const data_serializer = server_data_serializer_json(event, event_state, options); + for (let i = 0; i < nodes.length; i++) data_serializer.serialize(i, nodes[i]); + const { data, chunks } = data_serializer.get_data(); if (!chunks) { // use a normal JSON response where possible, so we get `content-length` diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js new file mode 100644 index 000000000000..663726587402 --- /dev/null +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -0,0 +1,207 @@ +import * as devalue from 'devalue'; +import { create_async_iterator } from '../../../utils/streaming.js'; +import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js'; + +/** + * If the serialized data contains promises, `chunks` will be an + * async iterable containing their resolutions + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {import('types').RequestState} event_state + * @param {import('types').SSROptions} options + * @returns {import('./types.js').ServerDataSerializer} + */ +export function server_data_serializer(event, event_state, options) { + let promise_id = 1; + let count = 0; + + const { iterator, push, done } = create_async_iterator(); + const global_placeholder = `__GLOBAL__${Math.random().toString(36).slice(2)}__`; + + /** @type {(info: { global: string; nonce: string; }) => void} */ + let set_info; + const info = /** @type {Promise<{ global: string; nonce: string }>} */ new Promise( + (r) => (set_info = r) + ); + + /** @param {any} thing */ + function replacer(thing) { + if (typeof thing?.then === 'function') { + const id = promise_id++; + count += 1; + + thing + .then(/** @param {any} data */ (data) => ({ data })) + .catch( + /** @param {any} error */ async (error) => ({ + error: await handle_error_and_jsonify(event, event_state, options, error) + }) + ) + .then( + /** + * @param {{data: any; error: any}} result + */ + async ({ data, error }) => { + count -= 1; + + let str; + try { + str = devalue.uneval(error ? [, error] : [data], replacer); + } catch { + error = await handle_error_and_jsonify( + event, + event_state, + options, + new Error(`Failed to serialize promise while rendering ${event.route.id}`) + ); + data = undefined; + str = devalue.uneval([, error], replacer); + } + + const { nonce, global } = await info; + push( + `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})\n` + ); + if (count === 0) done(); + } + ); + + return `${global_placeholder}.defer(${id})`; + } else { + for (const key in options.hooks.transport) { + const encoded = options.hooks.transport[key].encode(thing); + if (encoded) { + return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`; + } + } + } + } + + const strings = /** @type {string[]} */ ([]); + + return { + serialize(i, node) { + try { + if (!node) { + strings[i] = 'null'; + return; + } + + /** @type {any} */ + const payload = { type: 'data', data: node.data, uses: serialize_uses(node) }; + if (node.slash) payload.slash = node.slash; + + strings[i] = devalue.uneval(payload, replacer); + } catch (e) { + // @ts-expect-error + e.path = e.path.slice(1); + throw new Error(clarify_devalue_error(event, /** @type {any} */ (e))); + } + }, + + get_data(csp, global) { + set_info({ global, nonce: csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : '' }); + return { + data: `[${strings.join(',').replaceAll(global_placeholder, global)}]`, + chunks: count > 0 ? iterator : null + }; + } + }; +} + +/** + * If the serialized data contains promises, `chunks` will be an + * async iterable containing their resolutions + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {import('types').RequestState} event_state + * @param {import('types').SSROptions} options + * @returns {import('./types.js').ServerDataSerializerJson} + */ +export function server_data_serializer_json(event, event_state, options) { + let promise_id = 1; + let count = 0; + + const { iterator, push, done } = create_async_iterator(); + + const reducers = { + ...Object.fromEntries( + Object.entries(options.hooks.transport).map(([key, value]) => [key, value.encode]) + ), + /** @param {any} thing */ + Promise: (thing) => { + if (typeof thing?.then === 'function') { + const id = promise_id++; + count += 1; + + /** @type {'data' | 'error'} */ + let key = 'data'; + + thing + .catch( + /** @param {any} e */ async (e) => { + key = 'error'; + return handle_error_and_jsonify(event, event_state, options, /** @type {any} */ (e)); + } + ) + .then( + /** @param {any} value */ + async (value) => { + let str; + try { + str = devalue.stringify(value, reducers); + } catch { + const error = await handle_error_and_jsonify( + event, + event_state, + options, + new Error(`Failed to serialize promise while rendering ${event.route.id}`) + ); + + key = 'error'; + str = devalue.stringify(error, reducers); + } + + count -= 1; + + push(`{"type":"chunk","id":${id},"${key}":${str}}\n`); + if (count === 0) done(); + } + ); + + return id; + } + } + }; + + const strings = /** @type {string[]} */ ([]); + + return { + serialize(i, node) { + try { + if (!node) { + strings[i] = 'null'; + return; + } + + if (node.type === 'error' || node.type === 'skip') { + strings[i] = JSON.stringify(node); + return; + } + + return `{"type":"data","data":${devalue.stringify(node.data, reducers)},"uses":${JSON.stringify( + serialize_uses(node) + )}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`; + } catch (e) { + // @ts-expect-error + e.path = 'data' + e.path; + throw new Error(clarify_devalue_error(event, /** @type {any} */ (e))); + } + }, + + get_data() { + return { + data: `{"type":"data","nodes":[${strings.join(',')}]}\n`, + chunks: count > 0 ? iterator : null + }; + } + }; +} diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 7d24641f062d..202b94c808ea 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -1,22 +1,22 @@ import { text } from '@sveltejs/kit'; import { Redirect } from '@sveltejs/kit/internal'; +import { DEV } from 'esm-env'; import { compact } from '../../../utils/array.js'; import { get_status, normalize_error } from '../../../utils/error.js'; +import { PageNodes } from '../../../utils/page_nodes.js'; import { add_data_suffix } from '../../pathname.js'; -import { redirect_response, static_error_page, handle_error_and_jsonify } from '../utils.js'; +import { get_remote_action, handle_remote_form_post } from '../remote.js'; +import { handle_error_and_jsonify, redirect_response, static_error_page } from '../utils.js'; import { handle_action_json_request, handle_action_request, is_action_json_request, is_action_request } from './actions.js'; +import { server_data_serializer, server_data_serializer_json } from './data_serializer.js'; import { load_data, load_server_data } from './load_data.js'; import { render_response } from './render.js'; import { respond_with_error } from './respond_with_error.js'; -import { get_data_json } from '../data/index.js'; -import { DEV } from 'esm-env'; -import { get_remote_action, handle_remote_form_post } from '../remote.js'; -import { PageNodes } from '../../../utils/page_nodes.js'; /** * The maximum request depth permitted before assuming we're stuck in an infinite loop @@ -147,7 +147,8 @@ export async function render_page( options, manifest, state, - resolve_opts + resolve_opts, + data_serializer: server_data_serializer(event, event_state, options) }); } @@ -157,6 +158,12 @@ export async function render_page( /** @type {Error | null} */ let load_error = null; + const data_serializer = server_data_serializer(event, event_state, options); + const data_serializer_json = + state.prerendering && should_prerender_data + ? server_data_serializer_json(event, event_state, options) + : null; + /** @type {Array>} */ const server_promises = nodes.data.map((node, i) => { if (load_error) { @@ -164,7 +171,7 @@ export async function render_page( throw load_error; } - return Promise.resolve().then(async () => { + const promise = Promise.resolve().then(async () => { try { if (node === leaf_node && action_result?.type === 'error') { // we wait until here to throw the error so that we can use @@ -192,6 +199,13 @@ export async function render_page( throw load_error; } }); + + promise.then((server_data) => { + data_serializer.serialize(i, server_data); + data_serializer_json?.serialize(i, server_data); + }); + + return promise; }); /** @type {Array | null>>} */ @@ -287,7 +301,8 @@ export async function render_page( data: null, server_data: null }), - fetched + fetched, + data_serializer: server_data_serializer(event, event_state, options) }); } } @@ -303,14 +318,9 @@ export async function render_page( } } - if (state.prerendering && should_prerender_data) { + if (state.prerendering && data_serializer_json) { // ndjson format - let { data, chunks } = get_data_json( - event, - event_state, - options, - branch.map((node) => node?.server_data) - ); + let { data, chunks } = data_serializer_json.get_data(); if (chunks) { for await (const chunk of chunks) { @@ -339,7 +349,8 @@ export async function render_page( error: null, branch: ssr === false ? [] : compact(branch), action_result, - fetched + fetched, + data_serializer }); } catch (e) { // if we end up here, it means the data loaded successfully diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 141fbac33570..5b7816697256 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -1,22 +1,20 @@ -import * as devalue from 'devalue'; -import { readable, writable } from 'svelte/store'; -import { DEV } from 'esm-env'; import { text } from '@sveltejs/kit'; +import { with_request_store } from '@sveltejs/kit/internal/server'; import * as paths from '__sveltekit/paths'; +import * as devalue from 'devalue'; +import { DEV } from 'esm-env'; +import { readable, writable } from 'svelte/store'; +import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import { hash } from '../../../utils/hash.js'; -import { serialize_data } from './serialize_data.js'; import { s } from '../../../utils/misc.js'; -import { Csp } from './csp.js'; -import { uneval_action_response } from './actions.js'; -import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js'; -import { public_env } from '../../shared-server.js'; -import { create_async_iterator } from '../../../utils/streaming.js'; -import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import { SCHEME } from '../../../utils/url.js'; -import { create_server_routing_response, generate_route_object } from './server_routing.js'; import { add_resolution_suffix } from '../../pathname.js'; -import { with_request_store } from '@sveltejs/kit/internal/server'; +import { public_env } from '../../shared-server.js'; import { text_encoder } from '../../utils.js'; +import { uneval_action_response } from './actions.js'; +import { Csp } from './csp.js'; +import { serialize_data } from './serialize_data.js'; +import { create_server_routing_response, generate_route_object } from './server_routing.js'; // TODO rename this function/module @@ -40,6 +38,7 @@ const updated = { * event_state: import('types').RequestState; * resolve_opts: import('types').RequiredResolveOptions; * action_result?: import('@sveltejs/kit').ActionResult; + * data_serializer: import('./types.js').ServerDataSerializer * }} opts */ export async function render_response({ @@ -54,7 +53,8 @@ export async function render_response({ event, event_state, resolve_opts, - action_result + action_result, + data_serializer }) { if (state.prerendering) { if (options.csp.mode === 'nonce') { @@ -297,14 +297,7 @@ export async function render_response({ const global = __SVELTEKIT_DEV__ ? '__sveltekit_dev' : `__sveltekit_${options.version_hash}`; - const { data, chunks } = get_data( - event, - event_state, - options, - branch.map((b) => b.server_data), - csp, - global - ); + const { data, chunks } = data_serializer.get_data(csp, global); if (page_config.ssr && page_config.csr) { body += `\n\t\t\t${fetched @@ -644,95 +637,3 @@ export async function render_response({ } ); } - -/** - * If the serialized data contains promises, `chunks` will be an - * async iterable containing their resolutions - * @param {import('@sveltejs/kit').RequestEvent} event - * @param {import('types').RequestState} event_state - * @param {import('types').SSROptions} options - * @param {Array} nodes - * @param {import('./csp.js').Csp} csp - * @param {string} global - * @returns {{ data: string, chunks: AsyncIterable | null }} - */ -function get_data(event, event_state, options, nodes, csp, global) { - let promise_id = 1; - let count = 0; - - const { iterator, push, done } = create_async_iterator(); - - /** @param {any} thing */ - function replacer(thing) { - if (typeof thing?.then === 'function') { - const id = promise_id++; - count += 1; - - thing - .then(/** @param {any} data */ (data) => ({ data })) - .catch( - /** @param {any} error */ async (error) => ({ - error: await handle_error_and_jsonify(event, event_state, options, error) - }) - ) - .then( - /** - * @param {{data: any; error: any}} result - */ - async ({ data, error }) => { - count -= 1; - - let str; - try { - str = devalue.uneval(error ? [, error] : [data], replacer); - } catch { - error = await handle_error_and_jsonify( - event, - event_state, - options, - new Error(`Failed to serialize promise while rendering ${event.route.id}`) - ); - data = undefined; - str = devalue.uneval([, error], replacer); - } - - const nonce = csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''; - push( - `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})\n` - ); - if (count === 0) done(); - } - ); - - return `${global}.defer(${id})`; - } else { - for (const key in options.hooks.transport) { - const encoded = options.hooks.transport[key].encode(thing); - if (encoded) { - return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`; - } - } - } - } - - try { - const strings = nodes.map((node) => { - if (!node) return 'null'; - - /** @type {any} */ - const payload = { type: 'data', data: node.data, uses: serialize_uses(node) }; - if (node.slash) payload.slash = node.slash; - - return devalue.uneval(payload, replacer); - }); - - return { - data: `[${strings.join(',')}]`, - chunks: count > 0 ? iterator : null - }; - } catch (e) { - // @ts-expect-error - e.path = e.path.slice(1); - throw new Error(clarify_devalue_error(event, /** @type {any} */ (e))); - } -} diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index b1a6ae9ae606..9c276f7eafc8 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -1,9 +1,10 @@ import { Redirect } from '@sveltejs/kit/internal'; -import { render_response } from './render.js'; -import { load_data, load_server_data } from './load_data.js'; -import { handle_error_and_jsonify, static_error_page, redirect_response } from '../utils.js'; import { get_status } from '../../../utils/error.js'; import { PageNodes } from '../../../utils/page_nodes.js'; +import { handle_error_and_jsonify, redirect_response, static_error_page } from '../utils.js'; +import { server_data_serializer } from './data_serializer.js'; +import { load_data, load_server_data } from './load_data.js'; +import { render_response } from './render.js'; /** * @typedef {import('./types.js').Loaded} Loaded @@ -45,6 +46,7 @@ export async function respond_with_error({ const nodes = new PageNodes([default_layout]); const ssr = nodes.ssr(); const csr = nodes.csr(); + const data_serializer = server_data_serializer(event, event_state, options); if (ssr) { state.error = true; @@ -59,6 +61,7 @@ export async function respond_with_error({ }); const server_data = await server_data_promise; + data_serializer.serialize(0, server_data); const data = await load_data({ event, @@ -101,7 +104,8 @@ export async function respond_with_error({ fetched, event, event_state, - resolve_opts + resolve_opts, + data_serializer }); } catch (e) { // Edge case: If route is a 404 and the user redirects to somewhere from the root layout, diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index 300e05fefbe9..032333479d1f 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -1,5 +1,12 @@ import { CookieSerializeOptions } from 'cookie'; -import { SSRNode, CspDirectives, ServerDataNode } from 'types'; +import { + CspDirectives, + ServerDataNode, + SSRNode, + type ServerDataSkippedNode, + type ServerErrorNode +} from 'types'; +import type { Csp } from './csp.js'; export interface Fetched { url: string; @@ -34,3 +41,16 @@ export interface Cookie { value: string; options: CookieSerializeOptions & { path: string }; } + +export type ServerDataSerializer = { + serialize(i: number, node: ServerDataNode | null): void; + get_data(csp: Csp, global: string): { data: string; chunks: AsyncIterable | null }; +}; + +export type ServerDataSerializerJson = { + serialize( + i: number, + node: ServerDataSkippedNode | ServerDataNode | ServerErrorNode | null | undefined + ): void; + get_data(): { data: string; chunks: AsyncIterable | null }; +}; diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index fe2ed1c6f86a..7590de924acc 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -1,32 +1,14 @@ /** @import { RequestState } from 'types' */ -import { DEV } from 'esm-env'; import { json, text } from '@sveltejs/kit'; import { Redirect, SvelteKitError } from '@sveltejs/kit/internal'; import { merge_tracing, with_request_store } from '@sveltejs/kit/internal/server'; -import { base, app_dir } from '__sveltekit/paths'; -import { is_endpoint_request, render_endpoint } from './endpoint.js'; -import { render_page } from './page/index.js'; -import { render_response } from './page/render.js'; -import { respond_with_error } from './page/respond_with_error.js'; +import { app_dir, base } from '__sveltekit/paths'; +import { DEV } from 'esm-env'; +import { validate_server_exports } from '../../utils/exports.js'; import { is_form_content_type } from '../../utils/http.js'; -import { - handle_fatal_error, - has_prerendered_path, - method_not_allowed, - redirect_response -} from './utils.js'; -import { decode_pathname, decode_params, disable_search, normalize_path } from '../../utils/url.js'; -import { exec } from '../../utils/routing.js'; -import { redirect_json_response, render_data } from './data/index.js'; -import { add_cookies_to_headers, get_cookies } from './cookie.js'; -import { create_fetch } from './fetch.js'; import { PageNodes } from '../../utils/page_nodes.js'; -import { validate_server_exports } from '../../utils/exports.js'; -import { action_json_redirect, is_action_json_request } from './page/actions.js'; -import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js'; -import { get_public_env } from './env_module.js'; -import { resolve_route } from './page/server_routing.js'; -import { validateHeaders } from './validate-headers.js'; +import { exec } from '../../utils/routing.js'; +import { decode_params, decode_pathname, disable_search, normalize_path } from '../../utils/url.js'; import { add_data_suffix, add_resolution_suffix, @@ -35,9 +17,28 @@ import { strip_data_suffix, strip_resolution_suffix } from '../pathname.js'; -import { get_remote_id, handle_remote_call } from './remote.js'; -import { record_span } from '../telemetry/record_span.js'; +import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js'; import { otel } from '../telemetry/otel.js'; +import { record_span } from '../telemetry/record_span.js'; +import { add_cookies_to_headers, get_cookies } from './cookie.js'; +import { redirect_json_response, render_data } from './data/index.js'; +import { is_endpoint_request, render_endpoint } from './endpoint.js'; +import { get_public_env } from './env_module.js'; +import { create_fetch } from './fetch.js'; +import { action_json_redirect, is_action_json_request } from './page/actions.js'; +import { server_data_serializer } from './page/data_serializer.js'; +import { render_page } from './page/index.js'; +import { render_response } from './page/render.js'; +import { respond_with_error } from './page/respond_with_error.js'; +import { resolve_route } from './page/server_routing.js'; +import { get_remote_id, handle_remote_call } from './remote.js'; +import { + handle_fatal_error, + has_prerendered_path, + method_not_allowed, + redirect_response +} from './utils.js'; +import { validateHeaders } from './validate-headers.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ /* global __SVELTEKIT_DEV__ */ @@ -541,7 +542,8 @@ export async function internal_respond(request, options, manifest, state) { error: null, branch: [], fetched: [], - resolve_opts + resolve_opts, + data_serializer: server_data_serializer(event, event_state, options) }); } From 49eb7b14cfc66ae288658fc717387b6dc8f5daa1 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 00:35:38 +0200 Subject: [PATCH 04/25] Satisfy linter --- packages/kit/src/runtime/server/page/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 202b94c808ea..2456636ccdb6 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -200,7 +200,7 @@ export async function render_page( } }); - promise.then((server_data) => { + void promise.then((server_data) => { data_serializer.serialize(i, server_data); data_serializer_json?.serialize(i, server_data); }); From 7ac181c565a2833b2c4892fa36502620a5ab6be1 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 15:26:41 +0200 Subject: [PATCH 05/25] rethrow serialization error after load promises are resolved --- packages/kit/src/runtime/server/page/index.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 2456636ccdb6..76d41ff26ca3 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -164,6 +164,9 @@ export async function render_page( ? server_data_serializer_json(event, event_state, options) : null; + /** @type {Error | null} */ + let serialization_error; + /** @type {Array>} */ const server_promises = nodes.data.map((node, i) => { if (load_error) { @@ -201,8 +204,13 @@ export async function render_page( }); void promise.then((server_data) => { - data_serializer.serialize(i, server_data); - data_serializer_json?.serialize(i, server_data); + if (serialization_error) return; + try { + data_serializer.serialize(i, server_data); + data_serializer_json?.serialize(i, server_data); + } catch (e) { + serialization_error = /** @type {Error} */ (e); + } }); return promise; @@ -239,7 +247,7 @@ export async function render_page( // if we don't do this, rejections will be unhandled for (const p of server_promises) p.catch(() => {}); - for (const p of load_promises) p.catch(() => {}); + for (const p of load_promises) p.catch(() => { }); for (let i = 0; i < nodes.data.length; i += 1) { const node = nodes.data[i]; @@ -249,6 +257,9 @@ export async function render_page( const server_data = await server_promises[i]; const data = await load_promises[i]; + // @ts-ignore ts(2454) it is not used before being assigned + if (serialization_error) throw serialization_error; + branch.push({ node, server_data, data }); } catch (e) { const err = normalize_error(e); From 7be9a61c190b208bba8162b6a9c75fe7739f53c4 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 15:30:30 +0200 Subject: [PATCH 06/25] format --- packages/kit/src/runtime/server/page/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 76d41ff26ca3..c89b1b910971 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -247,7 +247,7 @@ export async function render_page( // if we don't do this, rejections will be unhandled for (const p of server_promises) p.catch(() => {}); - for (const p of load_promises) p.catch(() => { }); + for (const p of load_promises) p.catch(() => {}); for (let i = 0; i < nodes.data.length; i += 1) { const node = nodes.data[i]; From b3f227ccd275edc9906291a096c4d5436558436a Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 15:51:24 +0200 Subject: [PATCH 07/25] add changeset --- .changeset/blue-deer-relax.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/blue-deer-relax.md diff --git a/.changeset/blue-deer-relax.md b/.changeset/blue-deer-relax.md new file mode 100644 index 000000000000..9b7c9e6bcf6a --- /dev/null +++ b/.changeset/blue-deer-relax.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: serialize server `load` data before passing to universal `load`, to handle mutations and promises From 4975e7a1cd310030cae94fbced2382ba5e253947 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 20:18:52 +0200 Subject: [PATCH 08/25] get rid of the global placeholder --- .../runtime/server/page/data_serializer.js | 28 ++++++++++--------- .../kit/src/runtime/server/page/render.js | 6 ++-- .../kit/src/runtime/server/page/types.d.ts | 2 +- packages/kit/src/runtime/server/utils.js | 7 +++++ 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js index 663726587402..703927d535ed 100644 --- a/packages/kit/src/runtime/server/page/data_serializer.js +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -1,6 +1,11 @@ import * as devalue from 'devalue'; import { create_async_iterator } from '../../../utils/streaming.js'; -import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js'; +import { + clarify_devalue_error, + get_global_name, + handle_error_and_jsonify, + serialize_uses +} from '../utils.js'; /** * If the serialized data contains promises, `chunks` will be an @@ -15,13 +20,11 @@ export function server_data_serializer(event, event_state, options) { let count = 0; const { iterator, push, done } = create_async_iterator(); - const global_placeholder = `__GLOBAL__${Math.random().toString(36).slice(2)}__`; + const global = get_global_name(options); - /** @type {(info: { global: string; nonce: string; }) => void} */ - let set_info; - const info = /** @type {Promise<{ global: string; nonce: string }>} */ new Promise( - (r) => (set_info = r) - ); + /** @type {(nonce: string) => void} */ + let set_nonce; + const nonce = /** @type {Promise} */ new Promise((r) => (set_nonce = r)); /** @param {any} thing */ function replacer(thing) { @@ -57,15 +60,14 @@ export function server_data_serializer(event, event_state, options) { str = devalue.uneval([, error], replacer); } - const { nonce, global } = await info; push( - `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})\n` + `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})\n` ); if (count === 0) done(); } ); - return `${global_placeholder}.defer(${id})`; + return `${global}.defer(${id})`; } else { for (const key in options.hooks.transport) { const encoded = options.hooks.transport[key].encode(thing); @@ -98,10 +100,10 @@ export function server_data_serializer(event, event_state, options) { } }, - get_data(csp, global) { - set_info({ global, nonce: csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : '' }); + get_data(csp) { + set_nonce(csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''); return { - data: `[${strings.join(',').replaceAll(global_placeholder, global)}]`, + data: `[${strings.join(',')}]`, chunks: count > 0 ? iterator : null }; } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 5b7816697256..6c36b7bd18e1 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -11,6 +11,7 @@ import { SCHEME } from '../../../utils/url.js'; import { add_resolution_suffix } from '../../pathname.js'; import { public_env } from '../../shared-server.js'; import { text_encoder } from '../../utils.js'; +import { get_global_name } from '../utils.js'; import { uneval_action_response } from './actions.js'; import { Csp } from './csp.js'; import { serialize_data } from './serialize_data.js'; @@ -295,9 +296,8 @@ export async function render_response({ } } - const global = __SVELTEKIT_DEV__ ? '__sveltekit_dev' : `__sveltekit_${options.version_hash}`; - - const { data, chunks } = data_serializer.get_data(csp, global); + const global = get_global_name(options); + const { data, chunks } = data_serializer.get_data(csp); if (page_config.ssr && page_config.csr) { body += `\n\t\t\t${fetched diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index 032333479d1f..25fedee052bd 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -44,7 +44,7 @@ export interface Cookie { export type ServerDataSerializer = { serialize(i: number, node: ServerDataNode | null): void; - get_data(csp: Csp, global: string): { data: string; chunks: AsyncIterable | null }; + get_data(csp: Csp): { data: string; chunks: AsyncIterable | null }; }; export type ServerDataSerializerJson = { diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 46fe454c06d7..caf812c50789 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -44,6 +44,13 @@ export function allowed_methods(mod) { return allowed; } +/** + * @param {import('types').SSROptions} options + */ +export function get_global_name(options) { + return __SVELTEKIT_DEV__ ? '__sveltekit_dev' : `__sveltekit_${options.version_hash}`; +} + /** * Return as a response that renders the error.html * From 2596c1995be524345651349ccb68790c55e28208 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 20:42:09 +0200 Subject: [PATCH 09/25] fix dumb mistake --- packages/kit/src/runtime/server/page/data_serializer.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js index 703927d535ed..704eeca62ba0 100644 --- a/packages/kit/src/runtime/server/page/data_serializer.js +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -189,9 +189,10 @@ export function server_data_serializer_json(event, event_state, options) { return; } - return `{"type":"data","data":${devalue.stringify(node.data, reducers)},"uses":${JSON.stringify( - serialize_uses(node) - )}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`; + strings[i] = + `{"type":"data","data":${devalue.stringify(node.data, reducers)},"uses":${JSON.stringify( + serialize_uses(node) + )}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`; } catch (e) { // @ts-expect-error e.path = 'data' + e.path; From bf84d665f99622e164b91a105acf8603dbca52be Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 21:29:04 +0200 Subject: [PATCH 10/25] fix errors --- packages/kit/src/runtime/server/page/index.js | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index c89b1b910971..498588024eaf 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -164,9 +164,6 @@ export async function render_page( ? server_data_serializer_json(event, event_state, options) : null; - /** @type {Error | null} */ - let serialization_error; - /** @type {Array>} */ const server_promises = nodes.data.map((node, i) => { if (load_error) { @@ -182,7 +179,7 @@ export async function render_page( throw action_result.error; } - return await load_server_data({ + const server_data = await load_server_data({ event, event_state, state, @@ -197,19 +194,14 @@ export async function render_page( return data; } }); - } catch (e) { - load_error = /** @type {Error} */ (e); - throw load_error; - } - }); - void promise.then((server_data) => { - if (serialization_error) return; - try { data_serializer.serialize(i, server_data); data_serializer_json?.serialize(i, server_data); + + return server_data; } catch (e) { - serialization_error = /** @type {Error} */ (e); + load_error = /** @type {Error} */ (e); + throw load_error; } }); @@ -257,9 +249,6 @@ export async function render_page( const server_data = await server_promises[i]; const data = await load_promises[i]; - // @ts-ignore ts(2454) it is not used before being assigned - if (serialization_error) throw serialization_error; - branch.push({ node, server_data, data }); } catch (e) { const err = normalize_error(e); From 96b203e8fa8e5a254765541a6d3c846ef8b71076 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 21:33:29 +0200 Subject: [PATCH 11/25] return directly again --- packages/kit/src/runtime/server/page/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 498588024eaf..dcbb8ff4004a 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -171,7 +171,7 @@ export async function render_page( throw load_error; } - const promise = Promise.resolve().then(async () => { + return Promise.resolve().then(async () => { try { if (node === leaf_node && action_result?.type === 'error') { // we wait until here to throw the error so that we can use @@ -204,8 +204,6 @@ export async function render_page( throw load_error; } }); - - return promise; }); /** @type {Array | null>>} */ From caa49044cb17478f0f81cfe205ac68b7555a0ad1 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 21:46:21 +0200 Subject: [PATCH 12/25] return iterator even if all promises are resolved already --- packages/kit/src/runtime/server/page/data_serializer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js index 704eeca62ba0..2f9fe62ca0b7 100644 --- a/packages/kit/src/runtime/server/page/data_serializer.js +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -104,7 +104,7 @@ export function server_data_serializer(event, event_state, options) { set_nonce(csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''); return { data: `[${strings.join(',')}]`, - chunks: count > 0 ? iterator : null + chunks: promise_id > 1 ? iterator : null }; } }; @@ -203,7 +203,7 @@ export function server_data_serializer_json(event, event_state, options) { get_data() { return { data: `{"type":"data","nodes":[${strings.join(',')}]}\n`, - chunks: count > 0 ? iterator : null + chunks: promise_id > 1 ? iterator : null }; } }; From f90dc427fdcd0df0547f4c0544490160b0558004 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 22:40:39 +0200 Subject: [PATCH 13/25] prevent memory leak --- packages/kit/src/runtime/server/page/data_serializer.js | 4 ++++ packages/kit/src/runtime/server/page/index.js | 3 +++ packages/kit/src/runtime/server/page/types.d.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js index 2f9fe62ca0b7..9b3a594e8d40 100644 --- a/packages/kit/src/runtime/server/page/data_serializer.js +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -106,6 +106,10 @@ export function server_data_serializer(event, event_state, options) { data: `[${strings.join(',')}]`, chunks: promise_id > 1 ? iterator : null }; + }, + + discard() { + set_nonce(''); } }; } diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index dcbb8ff4004a..c74dd1770332 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -281,6 +281,9 @@ export async function render_page( const layouts = compact(branch.slice(0, j + 1)); const nodes = new PageNodes(layouts.map((layout) => layout.node)); + // prevent memory leak by resolving the `nonce` promise of the unused data_serializer + data_serializer.discard() + return await render_response({ event, event_state, diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index 25fedee052bd..e75dfef3c21c 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -45,6 +45,7 @@ export interface Cookie { export type ServerDataSerializer = { serialize(i: number, node: ServerDataNode | null): void; get_data(csp: Csp): { data: string; chunks: AsyncIterable | null }; + discard(): void; }; export type ServerDataSerializerJson = { From f3d038474a6a8efd4811b4c9e7a603867c947208 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 23 Aug 2025 23:25:05 +0200 Subject: [PATCH 14/25] format --- packages/kit/src/runtime/server/page/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index c74dd1770332..27cd3823d002 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -282,7 +282,7 @@ export async function render_page( const nodes = new PageNodes(layouts.map((layout) => layout.node)); // prevent memory leak by resolving the `nonce` promise of the unused data_serializer - data_serializer.discard() + data_serializer.discard(); return await render_response({ event, From fe10ccb309ca42f98bb4e62f317aa58b225c21c7 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 26 Aug 2025 18:37:30 +0200 Subject: [PATCH 15/25] use `DEV` from `esm-env` --- packages/kit/src/runtime/server/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 30a17cea507b..f7c33124a57b 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -48,7 +48,7 @@ export function allowed_methods(mod) { * @param {import('types').SSROptions} options */ export function get_global_name(options) { - return __SVELTEKIT_DEV__ ? '__sveltekit_dev' : `__sveltekit_${options.version_hash}`; + return DEV ? '__sveltekit_dev' : `__sveltekit_${options.version_hash}`; } /** From 05402a5291b5da04fee40b9c718a8c66d1781b9c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 14:53:46 -0400 Subject: [PATCH 16/25] revert reordering --- packages/kit/src/runtime/server/page/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 27cd3823d002..75c2f08e9e0e 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -1,12 +1,9 @@ import { text } from '@sveltejs/kit'; import { Redirect } from '@sveltejs/kit/internal'; -import { DEV } from 'esm-env'; import { compact } from '../../../utils/array.js'; import { get_status, normalize_error } from '../../../utils/error.js'; -import { PageNodes } from '../../../utils/page_nodes.js'; import { add_data_suffix } from '../../pathname.js'; -import { get_remote_action, handle_remote_form_post } from '../remote.js'; -import { handle_error_and_jsonify, redirect_response, static_error_page } from '../utils.js'; +import { redirect_response, static_error_page, handle_error_and_jsonify } from '../utils.js'; import { handle_action_json_request, handle_action_request, @@ -17,6 +14,9 @@ import { server_data_serializer, server_data_serializer_json } from './data_seri import { load_data, load_server_data } from './load_data.js'; import { render_response } from './render.js'; import { respond_with_error } from './respond_with_error.js'; +import { DEV } from 'esm-env'; +import { get_remote_action, handle_remote_form_post } from '../remote.js'; +import { PageNodes } from '../../../utils/page_nodes.js'; /** * The maximum request depth permitted before assuming we're stuck in an infinite loop From 76282d414e66158d31001b85c874fe180e457bcb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 15:00:13 -0400 Subject: [PATCH 17/25] revert reordering --- .../kit/src/runtime/server/page/load_data.js | 2 +- .../kit/src/runtime/server/page/render.js | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 0079cbf44d2b..520d6ff5bb85 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -1,7 +1,7 @@ -import { merge_tracing, with_request_store } from '@sveltejs/kit/internal/server'; import { DEV } from 'esm-env'; import { disable_search, make_trackable } from '../../../utils/url.js'; import { validate_depends, validate_load_response } from '../../shared.js'; +import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server'; import { record_span } from '../../telemetry/record_span.js'; import { base64_encode, text_decoder } from '../../utils.js'; import { NULL_BODY_STATUS } from '../constants.js'; diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 2702be1ccd31..2ad4908508d0 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -1,21 +1,21 @@ -import { text } from '@sveltejs/kit'; -import { with_request_store } from '@sveltejs/kit/internal/server'; -import * as paths from '__sveltekit/paths'; import * as devalue from 'devalue'; -import { DEV } from 'esm-env'; import { readable, writable } from 'svelte/store'; -import { SVELTE_KIT_ASSETS } from '../../../constants.js'; +import { DEV } from 'esm-env'; +import { text } from '@sveltejs/kit'; +import * as paths from '__sveltekit/paths'; import { hash } from '../../../utils/hash.js'; +import { serialize_data } from './serialize_data.js'; import { s } from '../../../utils/misc.js'; +import { Csp } from './csp.js'; +import { uneval_action_response } from './actions.js'; +import { public_env } from '../../shared-server.js'; +import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import { SCHEME } from '../../../utils/url.js'; +import { create_server_routing_response, generate_route_object } from './server_routing.js'; import { add_resolution_suffix } from '../../pathname.js'; -import { public_env } from '../../shared-server.js'; +import { with_request_store } from '@sveltejs/kit/internal/server'; import { text_encoder } from '../../utils.js'; import { get_global_name } from '../utils.js'; -import { uneval_action_response } from './actions.js'; -import { Csp } from './csp.js'; -import { serialize_data } from './serialize_data.js'; -import { create_server_routing_response, generate_route_object } from './server_routing.js'; // TODO rename this function/module From 4d3994c3285b9a24b1d9e5c84114f3ff2eeb5f6b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 15:01:48 -0400 Subject: [PATCH 18/25] revert reordering --- packages/kit/src/runtime/server/page/respond_with_error.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index 9c276f7eafc8..b1f185745519 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -1,10 +1,10 @@ import { Redirect } from '@sveltejs/kit/internal'; +import { render_response } from './render.js'; +import { load_data, load_server_data } from './load_data.js'; +import { handle_error_and_jsonify, redirect_response, static_error_page } from '../utils.js'; import { get_status } from '../../../utils/error.js'; import { PageNodes } from '../../../utils/page_nodes.js'; -import { handle_error_and_jsonify, redirect_response, static_error_page } from '../utils.js'; import { server_data_serializer } from './data_serializer.js'; -import { load_data, load_server_data } from './load_data.js'; -import { render_response } from './render.js'; /** * @typedef {import('./types.js').Loaded} Loaded From 1ad3db4b3eb90fb84ddd738ac7847f04476fb03f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 15:16:30 -0400 Subject: [PATCH 19/25] revert reordering --- packages/kit/src/runtime/server/page/respond_with_error.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index b1f185745519..58b881f32bf5 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -1,7 +1,7 @@ import { Redirect } from '@sveltejs/kit/internal'; import { render_response } from './render.js'; import { load_data, load_server_data } from './load_data.js'; -import { handle_error_and_jsonify, redirect_response, static_error_page } from '../utils.js'; +import { handle_error_and_jsonify, static_error_page, redirect_response } from '../utils.js'; import { get_status } from '../../../utils/error.js'; import { PageNodes } from '../../../utils/page_nodes.js'; import { server_data_serializer } from './data_serializer.js'; From a1e91ed635540652a3540755aa18c9c95382cff5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 15:18:03 -0400 Subject: [PATCH 20/25] remove type, everything is a type in a .d.ts file --- packages/kit/src/runtime/server/page/types.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index e75dfef3c21c..89de1d756922 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -3,10 +3,10 @@ import { CspDirectives, ServerDataNode, SSRNode, - type ServerDataSkippedNode, - type ServerErrorNode + ServerDataSkippedNode, + ServerErrorNode } from 'types'; -import type { Csp } from './csp.js'; +import { Csp } from './csp.js'; export interface Fetched { url: string; From b86768619c3a1529d0c609931707a467abbe17a4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 15:22:57 -0400 Subject: [PATCH 21/25] revert reordering --- packages/kit/src/runtime/server/data/index.js | 10 ++-- packages/kit/src/runtime/server/respond.js | 50 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index b5aa34e3f5da..a16d72134c1f 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -1,14 +1,14 @@ import { text } from '@sveltejs/kit'; -import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal'; -import * as devalue from 'devalue'; +import { HttpError, SvelteKitError, Redirect } from '@sveltejs/kit/internal'; import { normalize_error } from '../../../utils/error.js'; import { once } from '../../../utils/functions.js'; -import { create_async_iterator } from '../../../utils/streaming.js'; -import { normalize_path } from '../../../utils/url.js'; -import { text_encoder } from '../../utils.js'; import { server_data_serializer_json } from '../page/data_serializer.js'; import { load_server_data } from '../page/load_data.js'; import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js'; +import { normalize_path } from '../../../utils/url.js'; +import * as devalue from 'devalue'; +import { create_async_iterator } from '../../../utils/streaming.js'; +import { text_encoder } from '../../utils.js'; /** * @param {import('@sveltejs/kit').RequestEvent} event diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index cf1512af5515..2e97adaaf441 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -1,14 +1,32 @@ /** @import { RequestState } from 'types' */ +import { DEV } from 'esm-env'; import { json, text } from '@sveltejs/kit'; import { Redirect, SvelteKitError } from '@sveltejs/kit/internal'; import { merge_tracing, with_request_store } from '@sveltejs/kit/internal/server'; -import { app_dir, base } from '__sveltekit/paths'; -import { DEV } from 'esm-env'; -import { validate_server_exports } from '../../utils/exports.js'; +import { base, app_dir } from '__sveltekit/paths'; +import { is_endpoint_request, render_endpoint } from './endpoint.js'; +import { render_page } from './page/index.js'; +import { render_response } from './page/render.js'; +import { respond_with_error } from './page/respond_with_error.js'; import { is_form_content_type } from '../../utils/http.js'; -import { PageNodes } from '../../utils/page_nodes.js'; +import { + handle_fatal_error, + has_prerendered_path, + method_not_allowed, + redirect_response +} from './utils.js'; +import { decode_pathname, decode_params, disable_search, normalize_path } from '../../utils/url.js'; import { exec } from '../../utils/routing.js'; -import { decode_params, decode_pathname, disable_search, normalize_path } from '../../utils/url.js'; +import { redirect_json_response, render_data } from './data/index.js'; +import { add_cookies_to_headers, get_cookies } from './cookie.js'; +import { create_fetch } from './fetch.js'; +import { PageNodes } from '../../utils/page_nodes.js'; +import { validate_server_exports } from '../../utils/exports.js'; +import { action_json_redirect, is_action_json_request } from './page/actions.js'; +import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js'; +import { get_public_env } from './env_module.js'; +import { resolve_route } from './page/server_routing.js'; +import { validateHeaders } from './validate-headers.js'; import { add_data_suffix, add_resolution_suffix, @@ -17,28 +35,10 @@ import { strip_data_suffix, strip_resolution_suffix } from '../pathname.js'; -import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js'; -import { otel } from '../telemetry/otel.js'; -import { record_span } from '../telemetry/record_span.js'; -import { add_cookies_to_headers, get_cookies } from './cookie.js'; -import { redirect_json_response, render_data } from './data/index.js'; -import { is_endpoint_request, render_endpoint } from './endpoint.js'; -import { get_public_env } from './env_module.js'; -import { create_fetch } from './fetch.js'; -import { action_json_redirect, is_action_json_request } from './page/actions.js'; import { server_data_serializer } from './page/data_serializer.js'; -import { render_page } from './page/index.js'; -import { render_response } from './page/render.js'; -import { respond_with_error } from './page/respond_with_error.js'; -import { resolve_route } from './page/server_routing.js'; import { get_remote_id, handle_remote_call } from './remote.js'; -import { - handle_fatal_error, - has_prerendered_path, - method_not_allowed, - redirect_response -} from './utils.js'; -import { validateHeaders } from './validate-headers.js'; +import { record_span } from '../telemetry/record_span.js'; +import { otel } from '../telemetry/otel.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ From 03c9a4a688eaeb80ba9ec221f9f45f9f9daece63 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 15:56:40 -0400 Subject: [PATCH 22/25] serialize -> add_node --- packages/kit/src/runtime/server/data/index.js | 2 +- packages/kit/src/runtime/server/page/data_serializer.js | 4 ++-- packages/kit/src/runtime/server/page/index.js | 4 ++-- packages/kit/src/runtime/server/page/respond_with_error.js | 2 +- packages/kit/src/runtime/server/page/types.d.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index a16d72134c1f..a57eddb8bdae 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -122,7 +122,7 @@ export async function render_data( ); const data_serializer = server_data_serializer_json(event, event_state, options); - for (let i = 0; i < nodes.length; i++) data_serializer.serialize(i, nodes[i]); + for (let i = 0; i < nodes.length; i++) data_serializer.add_node(i, nodes[i]); const { data, chunks } = data_serializer.get_data(); if (!chunks) { diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js index 9b3a594e8d40..3d376f377df7 100644 --- a/packages/kit/src/runtime/server/page/data_serializer.js +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -81,7 +81,7 @@ export function server_data_serializer(event, event_state, options) { const strings = /** @type {string[]} */ ([]); return { - serialize(i, node) { + add_node(i, node) { try { if (!node) { strings[i] = 'null'; @@ -181,7 +181,7 @@ export function server_data_serializer_json(event, event_state, options) { const strings = /** @type {string[]} */ ([]); return { - serialize(i, node) { + add_node(i, node) { try { if (!node) { strings[i] = 'null'; diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 75c2f08e9e0e..8c33dd09c479 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -195,8 +195,8 @@ export async function render_page( } }); - data_serializer.serialize(i, server_data); - data_serializer_json?.serialize(i, server_data); + data_serializer.add_node(i, server_data); + data_serializer_json?.add_node(i, server_data); return server_data; } catch (e) { diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index 58b881f32bf5..0767e177d124 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -61,7 +61,7 @@ export async function respond_with_error({ }); const server_data = await server_data_promise; - data_serializer.serialize(0, server_data); + data_serializer.add_node(0, server_data); const data = await load_data({ event, diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index 89de1d756922..f9e08cd2c64c 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -43,13 +43,13 @@ export interface Cookie { } export type ServerDataSerializer = { - serialize(i: number, node: ServerDataNode | null): void; + add_node(i: number, node: ServerDataNode | null): void; get_data(csp: Csp): { data: string; chunks: AsyncIterable | null }; discard(): void; }; export type ServerDataSerializerJson = { - serialize( + add_node( i: number, node: ServerDataSkippedNode | ServerDataNode | ServerErrorNode | null | undefined ): void; From 019c5734250420ddfe6e827c393fb16ed8d63c86 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 16:25:40 -0400 Subject: [PATCH 23/25] unused --- packages/kit/src/runtime/server/data/index.js | 93 +------------------ 1 file changed, 1 insertion(+), 92 deletions(-) diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index a57eddb8bdae..83e16c3a9d12 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -4,10 +4,8 @@ import { normalize_error } from '../../../utils/error.js'; import { once } from '../../../utils/functions.js'; import { server_data_serializer_json } from '../page/data_serializer.js'; import { load_server_data } from '../page/load_data.js'; -import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js'; +import { handle_error_and_jsonify } from '../utils.js'; import { normalize_path } from '../../../utils/url.js'; -import * as devalue from 'devalue'; -import { create_async_iterator } from '../../../utils/streaming.js'; import { text_encoder } from '../../utils.js'; /** @@ -188,92 +186,3 @@ export function redirect_json_response(redirect) { }) ); } - -/** - * If the serialized data contains promises, `chunks` will be an - * async iterable containing their resolutions - * @param {import('@sveltejs/kit').RequestEvent} event - * @param {import('types').RequestState} event_state - * @param {import('types').SSROptions} options - * @param {Array} nodes - * @returns {{ data: string, chunks: AsyncIterable | null }} - */ -export function get_data_json(event, event_state, options, nodes) { - let promise_id = 1; - let count = 0; - - const { iterator, push, done } = create_async_iterator(); - - const reducers = { - ...Object.fromEntries( - Object.entries(options.hooks.transport).map(([key, value]) => [key, value.encode]) - ), - /** @param {any} thing */ - Promise: (thing) => { - if (typeof thing?.then === 'function') { - const id = promise_id++; - count += 1; - - /** @type {'data' | 'error'} */ - let key = 'data'; - - thing - .catch( - /** @param {any} e */ async (e) => { - key = 'error'; - return handle_error_and_jsonify(event, event_state, options, /** @type {any} */ (e)); - } - ) - .then( - /** @param {any} value */ - async (value) => { - let str; - try { - str = devalue.stringify(value, reducers); - } catch { - const error = await handle_error_and_jsonify( - event, - event_state, - options, - new Error(`Failed to serialize promise while rendering ${event.route.id}`) - ); - - key = 'error'; - str = devalue.stringify(error, reducers); - } - - count -= 1; - - push(`{"type":"chunk","id":${id},"${key}":${str}}\n`); - if (count === 0) done(); - } - ); - - return id; - } - } - }; - - try { - const strings = nodes.map((node) => { - if (!node) return 'null'; - - if (node.type === 'error' || node.type === 'skip') { - return JSON.stringify(node); - } - - return `{"type":"data","data":${devalue.stringify(node.data, reducers)},"uses":${JSON.stringify( - serialize_uses(node) - )}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`; - }); - - return { - data: `{"type":"data","nodes":[${strings.join(',')}]}\n`, - chunks: count > 0 ? iterator : null - }; - } catch (e) { - // @ts-expect-error - e.path = 'data' + e.path; - throw new Error(clarify_devalue_error(event, /** @type {any} */ (e))); - } -} From f0601595fc0fd880a337e6170f79995359e74df6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 16:39:32 -0400 Subject: [PATCH 24/25] simplify create_async_iterator --- .../runtime/server/page/data_serializer.js | 90 +++++++++---------- packages/kit/src/utils/streaming.js | 26 +++--- packages/kit/src/utils/streaming.spec.js | 9 +- 3 files changed, 62 insertions(+), 63 deletions(-) diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js index 3d376f377df7..fa863aef6850 100644 --- a/packages/kit/src/runtime/server/page/data_serializer.js +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -17,9 +17,8 @@ import { */ export function server_data_serializer(event, event_state, options) { let promise_id = 1; - let count = 0; - const { iterator, push, done } = create_async_iterator(); + const { iterator, add } = create_async_iterator(); const global = get_global_name(options); /** @type {(nonce: string) => void} */ @@ -30,9 +29,8 @@ export function server_data_serializer(event, event_state, options) { function replacer(thing) { if (typeof thing?.then === 'function') { const id = promise_id++; - count += 1; - thing + const promise = thing .then(/** @param {any} data */ (data) => ({ data })) .catch( /** @param {any} error */ async (error) => ({ @@ -44,8 +42,6 @@ export function server_data_serializer(event, event_state, options) { * @param {{data: any; error: any}} result */ async ({ data, error }) => { - count -= 1; - let str; try { str = devalue.uneval(error ? [, error] : [data], replacer); @@ -60,13 +56,12 @@ export function server_data_serializer(event, event_state, options) { str = devalue.uneval([, error], replacer); } - push( - `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})\n` - ); - if (count === 0) done(); + return `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})\n`; } ); + add(promise); + return `${global}.defer(${id})`; } else { for (const key in options.hooks.transport) { @@ -124,9 +119,8 @@ export function server_data_serializer(event, event_state, options) { */ export function server_data_serializer_json(event, event_state, options) { let promise_id = 1; - let count = 0; - const { iterator, push, done } = create_async_iterator(); + const { iterator, add } = create_async_iterator(); const reducers = { ...Object.fromEntries( @@ -134,47 +128,47 @@ export function server_data_serializer_json(event, event_state, options) { ), /** @param {any} thing */ Promise: (thing) => { - if (typeof thing?.then === 'function') { - const id = promise_id++; - count += 1; + if (typeof thing?.then !== 'function') { + return; + } + + const id = promise_id++; + + /** @type {'data' | 'error'} */ + let key = 'data'; - /** @type {'data' | 'error'} */ - let key = 'data'; + const promise = thing + .catch( + /** @param {any} e */ async (e) => { + key = 'error'; + return handle_error_and_jsonify(event, event_state, options, /** @type {any} */ (e)); + } + ) + .then( + /** @param {any} value */ + async (value) => { + let str; + try { + str = devalue.stringify(value, reducers); + } catch { + const error = await handle_error_and_jsonify( + event, + event_state, + options, + new Error(`Failed to serialize promise while rendering ${event.route.id}`) + ); - thing - .catch( - /** @param {any} e */ async (e) => { key = 'error'; - return handle_error_and_jsonify(event, event_state, options, /** @type {any} */ (e)); + str = devalue.stringify(error, reducers); } - ) - .then( - /** @param {any} value */ - async (value) => { - let str; - try { - str = devalue.stringify(value, reducers); - } catch { - const error = await handle_error_and_jsonify( - event, - event_state, - options, - new Error(`Failed to serialize promise while rendering ${event.route.id}`) - ); - - key = 'error'; - str = devalue.stringify(error, reducers); - } - - count -= 1; - - push(`{"type":"chunk","id":${id},"${key}":${str}}\n`); - if (count === 0) done(); - } - ); - return id; - } + return `{"type":"chunk","id":${id},"${key}":${str}}\n`; + } + ); + + add(promise); + + return id; } }; diff --git a/packages/kit/src/utils/streaming.js b/packages/kit/src/utils/streaming.js index cfb0d6a04f93..209afb10aba5 100644 --- a/packages/kit/src/utils/streaming.js +++ b/packages/kit/src/utils/streaming.js @@ -18,11 +18,12 @@ function defer() { * Create an async iterator and a function to push values into it * @returns {{ * iterator: AsyncIterable; - * push: (value: any) => void; - * done: () => void; + * add: (promise: Promise) => void; * }} */ export function create_async_iterator() { + let count = 0; + const deferred = [defer()]; return { @@ -37,15 +38,20 @@ export function create_async_iterator() { }; } }, - push: (value) => { - deferred[deferred.length - 1].fulfil({ - value, - done: false + add: (promise) => { + count += 1; + + void promise.then((value) => { + deferred[deferred.length - 1].fulfil({ + value, + done: false + }); + deferred.push(defer()); + + if (--count === 0) { + deferred[deferred.length - 1].fulfil({ done: true }); + } }); - deferred.push(defer()); - }, - done: () => { - deferred[deferred.length - 1].fulfil({ done: true }); } }; } diff --git a/packages/kit/src/utils/streaming.spec.js b/packages/kit/src/utils/streaming.spec.js index 320fbe3e2769..b9b999df4e95 100644 --- a/packages/kit/src/utils/streaming.spec.js +++ b/packages/kit/src/utils/streaming.spec.js @@ -2,14 +2,13 @@ import { expect, test } from 'vitest'; import { create_async_iterator } from './streaming.js'; test('works with fast consecutive promise resolutions', async () => { - const iterator = create_async_iterator(); + const { iterator, add } = create_async_iterator(); - void Promise.resolve(1).then((n) => iterator.push(n)); - void Promise.resolve(2).then((n) => iterator.push(n)); - void Promise.resolve().then(() => iterator.done()); + add(Promise.resolve(1)); + add(Promise.resolve(2)); const actual = []; - for await (const value of iterator.iterator) { + for await (const value of iterator) { actual.push(value); } From 530de8820dba8c79afd0041d2ec47138e7e8cced Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Aug 2025 17:02:00 -0400 Subject: [PATCH 25/25] avoid the need for set_nonce dance --- .../runtime/server/page/data_serializer.js | 26 ++++++--------- packages/kit/src/runtime/server/page/index.js | 3 -- .../kit/src/runtime/server/page/types.d.ts | 1 - packages/kit/src/utils/streaming.js | 32 ++++++++++++------- packages/kit/src/utils/streaming.spec.js | 6 ++-- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js index fa863aef6850..2f0197ae648b 100644 --- a/packages/kit/src/runtime/server/page/data_serializer.js +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -18,13 +18,9 @@ import { export function server_data_serializer(event, event_state, options) { let promise_id = 1; - const { iterator, add } = create_async_iterator(); + const iterator = create_async_iterator(); const global = get_global_name(options); - /** @type {(nonce: string) => void} */ - let set_nonce; - const nonce = /** @type {Promise} */ new Promise((r) => (set_nonce = r)); - /** @param {any} thing */ function replacer(thing) { if (typeof thing?.then === 'function') { @@ -56,11 +52,11 @@ export function server_data_serializer(event, event_state, options) { str = devalue.uneval([, error], replacer); } - return `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})\n`; + return `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})`; } ); - add(promise); + iterator.add(promise); return `${global}.defer(${id})`; } else { @@ -96,15 +92,13 @@ export function server_data_serializer(event, event_state, options) { }, get_data(csp) { - set_nonce(csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''); + const open = ``; + const close = `\n`; + return { data: `[${strings.join(',')}]`, - chunks: promise_id > 1 ? iterator : null + chunks: promise_id > 1 ? iterator.iterate((str) => open + str + close) : null }; - }, - - discard() { - set_nonce(''); } }; } @@ -120,7 +114,7 @@ export function server_data_serializer(event, event_state, options) { export function server_data_serializer_json(event, event_state, options) { let promise_id = 1; - const { iterator, add } = create_async_iterator(); + const iterator = create_async_iterator(); const reducers = { ...Object.fromEntries( @@ -166,7 +160,7 @@ export function server_data_serializer_json(event, event_state, options) { } ); - add(promise); + iterator.add(promise); return id; } @@ -201,7 +195,7 @@ export function server_data_serializer_json(event, event_state, options) { get_data() { return { data: `{"type":"data","nodes":[${strings.join(',')}]}\n`, - chunks: promise_id > 1 ? iterator : null + chunks: promise_id > 1 ? iterator.iterate() : null }; } }; diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 8c33dd09c479..9e05ab3a4334 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -281,9 +281,6 @@ export async function render_page( const layouts = compact(branch.slice(0, j + 1)); const nodes = new PageNodes(layouts.map((layout) => layout.node)); - // prevent memory leak by resolving the `nonce` promise of the unused data_serializer - data_serializer.discard(); - return await render_response({ event, event_state, diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index f9e08cd2c64c..bfe975fb37a9 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -45,7 +45,6 @@ export interface Cookie { export type ServerDataSerializer = { add_node(i: number, node: ServerDataNode | null): void; get_data(csp: Csp): { data: string; chunks: AsyncIterable | null }; - discard(): void; }; export type ServerDataSerializerJson = { diff --git a/packages/kit/src/utils/streaming.js b/packages/kit/src/utils/streaming.js index 209afb10aba5..449507ac6695 100644 --- a/packages/kit/src/utils/streaming.js +++ b/packages/kit/src/utils/streaming.js @@ -16,9 +16,10 @@ function defer() { /** * Create an async iterator and a function to push values into it + * @template T * @returns {{ - * iterator: AsyncIterable; - * add: (promise: Promise) => void; + * iterate: (transform?: (input: T) => T) => AsyncIterable; + * add: (promise: Promise) => void; * }} */ export function create_async_iterator() { @@ -27,16 +28,23 @@ export function create_async_iterator() { const deferred = [defer()]; return { - iterator: { - [Symbol.asyncIterator]() { - return { - next: async () => { - const next = await deferred[0].promise; - if (!next.done) deferred.shift(); - return next; - } - }; - } + iterate: (transform = (x) => x) => { + return { + [Symbol.asyncIterator]() { + return { + next: async () => { + const next = await deferred[0].promise; + + if (!next.done) { + deferred.shift(); + return { value: transform(next.value), done: false }; + } + + return next; + } + }; + } + }; }, add: (promise) => { count += 1; diff --git a/packages/kit/src/utils/streaming.spec.js b/packages/kit/src/utils/streaming.spec.js index b9b999df4e95..2a94ad178bef 100644 --- a/packages/kit/src/utils/streaming.spec.js +++ b/packages/kit/src/utils/streaming.spec.js @@ -2,15 +2,15 @@ import { expect, test } from 'vitest'; import { create_async_iterator } from './streaming.js'; test('works with fast consecutive promise resolutions', async () => { - const { iterator, add } = create_async_iterator(); + const { iterate, add } = create_async_iterator(); add(Promise.resolve(1)); add(Promise.resolve(2)); const actual = []; - for await (const value of iterator) { + for await (const value of iterate((n) => n * 10)) { actual.push(value); } - expect(actual).toEqual([1, 2]); + expect(actual).toEqual([10, 20]); });