Skip to content

Commit 1c416bc

Browse files
fix: serialize server load data before passing to universal load alternative (#14298)
* add universal load to /streaming/server test * revert #14268 * server data serializer * Satisfy linter * rethrow serialization error after load promises are resolved * format * add changeset * get rid of the global placeholder * fix dumb mistake * fix errors * return directly again * return iterator even if all promises are resolved already * prevent memory leak * format * use `DEV` from `esm-env` * revert reordering * revert reordering * revert reordering * revert reordering * remove type, everything is a type in a .d.ts file * revert reordering * serialize -> add_node * unused * simplify create_async_iterator * avoid the need for set_nonce dance --------- Co-authored-by: Rich Harris <[email protected]>
1 parent f3a0dda commit 1c416bc

File tree

13 files changed

+324
-274
lines changed

13 files changed

+324
-274
lines changed

.changeset/blue-deer-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: serialize server `load` data before passing to universal `load`, to handle mutations and promises

packages/kit/src/runtime/server/data/index.js

Lines changed: 5 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import { text } from '@sveltejs/kit';
22
import { HttpError, SvelteKitError, Redirect } from '@sveltejs/kit/internal';
33
import { normalize_error } from '../../../utils/error.js';
44
import { once } from '../../../utils/functions.js';
5+
import { server_data_serializer_json } from '../page/data_serializer.js';
56
import { load_server_data } from '../page/load_data.js';
6-
import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js';
7+
import { handle_error_and_jsonify } from '../utils.js';
78
import { normalize_path } from '../../../utils/url.js';
8-
import * as devalue from 'devalue';
9-
import { create_async_iterator } from '../../../utils/streaming.js';
109
import { text_encoder } from '../../utils.js';
1110

1211
/**
@@ -120,7 +119,9 @@ export async function render_data(
120119
)
121120
);
122121

123-
const { data, chunks } = get_data_json(event, event_state, options, nodes);
122+
const data_serializer = server_data_serializer_json(event, event_state, options);
123+
for (let i = 0; i < nodes.length; i++) data_serializer.add_node(i, nodes[i]);
124+
const { data, chunks } = data_serializer.get_data();
124125

125126
if (!chunks) {
126127
// use a normal JSON response where possible, so we get `content-length`
@@ -185,92 +186,3 @@ export function redirect_json_response(redirect) {
185186
})
186187
);
187188
}
188-
189-
/**
190-
* If the serialized data contains promises, `chunks` will be an
191-
* async iterable containing their resolutions
192-
* @param {import('@sveltejs/kit').RequestEvent} event
193-
* @param {import('types').RequestState} event_state
194-
* @param {import('types').SSROptions} options
195-
* @param {Array<import('types').ServerDataSkippedNode | import('types').ServerDataNode | import('types').ServerErrorNode | null | undefined>} nodes
196-
* @returns {{ data: string, chunks: AsyncIterable<string> | null }}
197-
*/
198-
export function get_data_json(event, event_state, options, nodes) {
199-
let promise_id = 1;
200-
let count = 0;
201-
202-
const { iterator, push, done } = create_async_iterator();
203-
204-
const reducers = {
205-
...Object.fromEntries(
206-
Object.entries(options.hooks.transport).map(([key, value]) => [key, value.encode])
207-
),
208-
/** @param {any} thing */
209-
Promise: (thing) => {
210-
if (typeof thing?.then === 'function') {
211-
const id = promise_id++;
212-
count += 1;
213-
214-
/** @type {'data' | 'error'} */
215-
let key = 'data';
216-
217-
thing
218-
.catch(
219-
/** @param {any} e */ async (e) => {
220-
key = 'error';
221-
return handle_error_and_jsonify(event, event_state, options, /** @type {any} */ (e));
222-
}
223-
)
224-
.then(
225-
/** @param {any} value */
226-
async (value) => {
227-
let str;
228-
try {
229-
str = devalue.stringify(value, reducers);
230-
} catch {
231-
const error = await handle_error_and_jsonify(
232-
event,
233-
event_state,
234-
options,
235-
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
236-
);
237-
238-
key = 'error';
239-
str = devalue.stringify(error, reducers);
240-
}
241-
242-
count -= 1;
243-
244-
push(`{"type":"chunk","id":${id},"${key}":${str}}\n`);
245-
if (count === 0) done();
246-
}
247-
);
248-
249-
return id;
250-
}
251-
}
252-
};
253-
254-
try {
255-
const strings = nodes.map((node) => {
256-
if (!node) return 'null';
257-
258-
if (node.type === 'error' || node.type === 'skip') {
259-
return JSON.stringify(node);
260-
}
261-
262-
return `{"type":"data","data":${devalue.stringify(node.data, reducers)},"uses":${JSON.stringify(
263-
serialize_uses(node)
264-
)}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`;
265-
});
266-
267-
return {
268-
data: `{"type":"data","nodes":[${strings.join(',')}]}\n`,
269-
chunks: count > 0 ? iterator : null
270-
};
271-
} catch (e) {
272-
// @ts-expect-error
273-
e.path = 'data' + e.path;
274-
throw new Error(clarify_devalue_error(event, /** @type {any} */ (e)));
275-
}
276-
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import * as devalue from 'devalue';
2+
import { create_async_iterator } from '../../../utils/streaming.js';
3+
import {
4+
clarify_devalue_error,
5+
get_global_name,
6+
handle_error_and_jsonify,
7+
serialize_uses
8+
} from '../utils.js';
9+
10+
/**
11+
* If the serialized data contains promises, `chunks` will be an
12+
* async iterable containing their resolutions
13+
* @param {import('@sveltejs/kit').RequestEvent} event
14+
* @param {import('types').RequestState} event_state
15+
* @param {import('types').SSROptions} options
16+
* @returns {import('./types.js').ServerDataSerializer}
17+
*/
18+
export function server_data_serializer(event, event_state, options) {
19+
let promise_id = 1;
20+
21+
const iterator = create_async_iterator();
22+
const global = get_global_name(options);
23+
24+
/** @param {any} thing */
25+
function replacer(thing) {
26+
if (typeof thing?.then === 'function') {
27+
const id = promise_id++;
28+
29+
const promise = thing
30+
.then(/** @param {any} data */ (data) => ({ data }))
31+
.catch(
32+
/** @param {any} error */ async (error) => ({
33+
error: await handle_error_and_jsonify(event, event_state, options, error)
34+
})
35+
)
36+
.then(
37+
/**
38+
* @param {{data: any; error: any}} result
39+
*/
40+
async ({ data, error }) => {
41+
let str;
42+
try {
43+
str = devalue.uneval(error ? [, error] : [data], replacer);
44+
} catch {
45+
error = await handle_error_and_jsonify(
46+
event,
47+
event_state,
48+
options,
49+
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
50+
);
51+
data = undefined;
52+
str = devalue.uneval([, error], replacer);
53+
}
54+
55+
return `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})`;
56+
}
57+
);
58+
59+
iterator.add(promise);
60+
61+
return `${global}.defer(${id})`;
62+
} else {
63+
for (const key in options.hooks.transport) {
64+
const encoded = options.hooks.transport[key].encode(thing);
65+
if (encoded) {
66+
return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`;
67+
}
68+
}
69+
}
70+
}
71+
72+
const strings = /** @type {string[]} */ ([]);
73+
74+
return {
75+
add_node(i, node) {
76+
try {
77+
if (!node) {
78+
strings[i] = 'null';
79+
return;
80+
}
81+
82+
/** @type {any} */
83+
const payload = { type: 'data', data: node.data, uses: serialize_uses(node) };
84+
if (node.slash) payload.slash = node.slash;
85+
86+
strings[i] = devalue.uneval(payload, replacer);
87+
} catch (e) {
88+
// @ts-expect-error
89+
e.path = e.path.slice(1);
90+
throw new Error(clarify_devalue_error(event, /** @type {any} */ (e)));
91+
}
92+
},
93+
94+
get_data(csp) {
95+
const open = `<script${csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''}>`;
96+
const close = `</script>\n`;
97+
98+
return {
99+
data: `[${strings.join(',')}]`,
100+
chunks: promise_id > 1 ? iterator.iterate((str) => open + str + close) : null
101+
};
102+
}
103+
};
104+
}
105+
106+
/**
107+
* If the serialized data contains promises, `chunks` will be an
108+
* async iterable containing their resolutions
109+
* @param {import('@sveltejs/kit').RequestEvent} event
110+
* @param {import('types').RequestState} event_state
111+
* @param {import('types').SSROptions} options
112+
* @returns {import('./types.js').ServerDataSerializerJson}
113+
*/
114+
export function server_data_serializer_json(event, event_state, options) {
115+
let promise_id = 1;
116+
117+
const iterator = create_async_iterator();
118+
119+
const reducers = {
120+
...Object.fromEntries(
121+
Object.entries(options.hooks.transport).map(([key, value]) => [key, value.encode])
122+
),
123+
/** @param {any} thing */
124+
Promise: (thing) => {
125+
if (typeof thing?.then !== 'function') {
126+
return;
127+
}
128+
129+
const id = promise_id++;
130+
131+
/** @type {'data' | 'error'} */
132+
let key = 'data';
133+
134+
const promise = thing
135+
.catch(
136+
/** @param {any} e */ async (e) => {
137+
key = 'error';
138+
return handle_error_and_jsonify(event, event_state, options, /** @type {any} */ (e));
139+
}
140+
)
141+
.then(
142+
/** @param {any} value */
143+
async (value) => {
144+
let str;
145+
try {
146+
str = devalue.stringify(value, reducers);
147+
} catch {
148+
const error = await handle_error_and_jsonify(
149+
event,
150+
event_state,
151+
options,
152+
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
153+
);
154+
155+
key = 'error';
156+
str = devalue.stringify(error, reducers);
157+
}
158+
159+
return `{"type":"chunk","id":${id},"${key}":${str}}\n`;
160+
}
161+
);
162+
163+
iterator.add(promise);
164+
165+
return id;
166+
}
167+
};
168+
169+
const strings = /** @type {string[]} */ ([]);
170+
171+
return {
172+
add_node(i, node) {
173+
try {
174+
if (!node) {
175+
strings[i] = 'null';
176+
return;
177+
}
178+
179+
if (node.type === 'error' || node.type === 'skip') {
180+
strings[i] = JSON.stringify(node);
181+
return;
182+
}
183+
184+
strings[i] =
185+
`{"type":"data","data":${devalue.stringify(node.data, reducers)},"uses":${JSON.stringify(
186+
serialize_uses(node)
187+
)}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`;
188+
} catch (e) {
189+
// @ts-expect-error
190+
e.path = 'data' + e.path;
191+
throw new Error(clarify_devalue_error(event, /** @type {any} */ (e)));
192+
}
193+
},
194+
195+
get_data() {
196+
return {
197+
data: `{"type":"data","nodes":[${strings.join(',')}]}\n`,
198+
chunks: promise_id > 1 ? iterator.iterate() : null
199+
};
200+
}
201+
};
202+
}

0 commit comments

Comments
 (0)