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 diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 37fc409ab42d..83e16c3a9d12 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -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'; /** @@ -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` @@ -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} 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))); - } -} 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..2f0197ae648b --- /dev/null +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -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 = ``; + const close = `\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 + }; + } + }; +} diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 7d24641f062d..9e05ab3a4334 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -10,10 +10,10 @@ import { 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'; @@ -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) { @@ -172,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, @@ -187,6 +194,11 @@ export async function render_page( return data; } }); + + data_serializer.add_node(i, server_data); + data_serializer_json?.add_node(i, server_data); + + return server_data; } catch (e) { load_error = /** @type {Error} */ (e); throw load_error; @@ -287,7 +299,8 @@ export async function render_page( data: null, server_data: null }), - fetched + fetched, + data_serializer: server_data_serializer(event, event_state, options) }); } } @@ -303,14 +316,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 +347,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/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index a7b63235305a..520d6ff5bb85 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 { 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 - }); - }); + }) + ); } }); diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 928be29255d3..2ad4908508d0 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -8,15 +8,14 @@ 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 { text_encoder } from '../../utils.js'; +import { get_global_name } from '../utils.js'; // TODO rename this function/module @@ -40,6 +39,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 +54,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') { @@ -295,16 +296,8 @@ export async function render_response({ } } - const global = 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 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 @@ -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..0767e177d124 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -4,6 +4,7 @@ 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 { server_data_serializer } from './data_serializer.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.add_node(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..bfe975fb37a9 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, + ServerDataSkippedNode, + ServerErrorNode +} from 'types'; +import { 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 = { + add_node(i: number, node: ServerDataNode | null): void; + get_data(csp: Csp): { data: string; chunks: AsyncIterable | null }; +}; + +export type ServerDataSerializerJson = { + add_node( + 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 bcb0defd0846..2e97adaaf441 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -35,6 +35,7 @@ import { strip_data_suffix, strip_resolution_suffix } from '../pathname.js'; +import { server_data_serializer } from './page/data_serializer.js'; import { get_remote_id, handle_remote_call } from './remote.js'; import { record_span } from '../telemetry/record_span.js'; import { otel } from '../telemetry/otel.js'; @@ -542,7 +543,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) }); } diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 1c66bd25196c..f7c33124a57b 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 DEV ? '__sveltekit_dev' : `__sveltekit_${options.version_hash}`; +} + /** * Return as a response that renders the error.html * diff --git a/packages/kit/src/utils/streaming.js b/packages/kit/src/utils/streaming.js index cfb0d6a04f93..449507ac6695 100644 --- a/packages/kit/src/utils/streaming.js +++ b/packages/kit/src/utils/streaming.js @@ -16,36 +16,50 @@ function defer() { /** * Create an async iterator and a function to push values into it + * @template T * @returns {{ - * iterator: AsyncIterable; - * push: (value: any) => void; - * done: () => void; + * iterate: (transform?: (input: T) => T) => AsyncIterable; + * add: (promise: Promise) => void; * }} */ export function create_async_iterator() { + let count = 0; + 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; + } + }; + } + }; }, - 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..2a94ad178bef 100644 --- a/packages/kit/src/utils/streaming.spec.js +++ b/packages/kit/src/utils/streaming.spec.js @@ -2,16 +2,15 @@ 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 { iterate, 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 iterate((n) => n * 10)) { actual.push(value); } - expect(actual).toEqual([1, 2]); + expect(actual).toEqual([10, 20]); }); 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 }; +}