Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
541c459
add universal load to /streaming/server test
PatrickG Aug 22, 2025
a175295
revert #14268
PatrickG Aug 22, 2025
663f728
server data serializer
PatrickG Aug 22, 2025
49eb7b1
Satisfy linter
PatrickG Aug 22, 2025
7ac181c
rethrow serialization error after load promises are resolved
PatrickG Aug 23, 2025
7be9a61
format
PatrickG Aug 23, 2025
b3f227c
add changeset
PatrickG Aug 23, 2025
4975e7a
get rid of the global placeholder
PatrickG Aug 23, 2025
2596c19
fix dumb mistake
PatrickG Aug 23, 2025
bf84d66
fix errors
PatrickG Aug 23, 2025
96b203e
return directly again
PatrickG Aug 23, 2025
caa4904
return iterator even if all promises are resolved already
PatrickG Aug 23, 2025
f90dc42
prevent memory leak
PatrickG Aug 23, 2025
f3d0384
format
PatrickG Aug 23, 2025
246fccb
Merge branch 'main' into issue-14291
PatrickG Aug 26, 2025
fe10ccb
use `DEV` from `esm-env`
PatrickG Aug 26, 2025
afa2afe
Merge branch 'main' into pr/14298
Rich-Harris Aug 28, 2025
05402a5
revert reordering
Rich-Harris Aug 28, 2025
76282d4
revert reordering
Rich-Harris Aug 28, 2025
4d3994c
revert reordering
Rich-Harris Aug 28, 2025
1ad3db4
revert reordering
Rich-Harris Aug 28, 2025
a1e91ed
remove type, everything is a type in a .d.ts file
Rich-Harris Aug 28, 2025
b867686
revert reordering
Rich-Harris Aug 28, 2025
03c9a4a
serialize -> add_node
Rich-Harris Aug 28, 2025
019c573
unused
Rich-Harris Aug 28, 2025
f060159
simplify create_async_iterator
Rich-Harris Aug 28, 2025
530de88
avoid the need for set_nonce dance
Rich-Harris Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-deer-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: serialize server `load` data before passing to universal `load`, to handle mutations and promises
98 changes: 5 additions & 93 deletions packages/kit/src/runtime/server/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { text } from '@sveltejs/kit';
import { HttpError, SvelteKitError, Redirect } from '@sveltejs/kit/internal';
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';

/**
Expand Down Expand Up @@ -120,7 +119,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.add_node(i, nodes[i]);
const { data, chunks } = data_serializer.get_data();

if (!chunks) {
// use a normal JSON response where possible, so we get `content-length`
Expand Down Expand Up @@ -185,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<import('types').ServerDataSkippedNode | import('types').ServerDataNode | import('types').ServerErrorNode | null | undefined>} nodes
* @returns {{ data: string, chunks: AsyncIterable<string> | 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)));
}
}
202 changes: 202 additions & 0 deletions packages/kit/src/runtime/server/page/data_serializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import * as devalue from 'devalue';
import { create_async_iterator } from '../../../utils/streaming.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
* 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;

const iterator = create_async_iterator();
const global = get_global_name(options);

/** @param {any} thing */
function replacer(thing) {
if (typeof thing?.then === 'function') {
const id = promise_id++;

const promise = 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 }) => {
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);
}

return `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})`;
}
);

iterator.add(promise);

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)})`;
}
}
}
}

const strings = /** @type {string[]} */ ([]);

return {
add_node(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) {
const open = `<script${csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''}>`;
const close = `</script>\n`;

return {
data: `[${strings.join(',')}]`,
chunks: promise_id > 1 ? iterator.iterate((str) => open + str + close) : 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;

const iterator = 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') {
return;
}

const id = promise_id++;

/** @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}`)
);

key = 'error';
str = devalue.stringify(error, reducers);
}

return `{"type":"chunk","id":${id},"${key}":${str}}\n`;
}
);

iterator.add(promise);

return id;
}
};

const strings = /** @type {string[]} */ ([]);

return {
add_node(i, node) {
try {
if (!node) {
strings[i] = 'null';
return;
}

if (node.type === 'error' || node.type === 'skip') {
strings[i] = JSON.stringify(node);
return;
}

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;
throw new Error(clarify_devalue_error(event, /** @type {any} */ (e)));
}
},

get_data() {
return {
data: `{"type":"data","nodes":[${strings.join(',')}]}\n`,
chunks: promise_id > 1 ? iterator.iterate() : null
};
}
};
}
Loading
Loading