diff --git a/.changeset/short-oranges-kiss.md b/.changeset/short-oranges-kiss.md new file mode 100644 index 000000000000..f0f2807a20f5 --- /dev/null +++ b/.changeset/short-oranges-kiss.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": minor +--- + +feat: inline load fetch `response.body` stream data as base64 in page diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 3be6e7039c09..d70276550469 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -311,6 +311,9 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) } } + /** @type {ReadableStream} */ + let teed_body; + const proxy = new Proxy(response, { get(response, key, _receiver) { /** @@ -342,6 +345,39 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) }); } + if (key === 'body') { + if (response.body === null) { + return null; + } + + if (teed_body) { + return teed_body; + } + + const [a, b] = response.body.tee(); + + void (async () => { + let result = new Uint8Array(); + + for await (const chunk of a) { + const combined = new Uint8Array(result.length + chunk.length); + + combined.set(result, 0); + combined.set(chunk, result.length); + + result = combined; + } + + if (dependency) { + dependency.body = new Uint8Array(result); + } + + void push_fetched(base64_encode(result), true); + })(); + + return (teed_body = b); + } + if (key === 'arrayBuffer') { return async () => { const buffer = await response.arrayBuffer(); diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/+page.js new file mode 100644 index 000000000000..b24aa0d069c9 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/+page.js @@ -0,0 +1,29 @@ +const body_stream_to_buffer = async (body) => { + let buffer = new Uint8Array(); + const reader = body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (value != null) { + const newBuffer = new Uint8Array(buffer.length + value.length); + newBuffer.set(buffer, 0); + newBuffer.set(value, buffer.length); + buffer = newBuffer; + } + if (done) break; + } + return buffer; +}; + +export async function load({ fetch }) { + const res = await fetch('/load/fetch-body-stream-b64/data'); + + const l = await fetch('/load/fetch-body-stream-b64/data', { + body: Uint8Array.from(Array(256).fill(0), (_, i) => i), + method: 'POST' + }); + + return { + data: await body_stream_to_buffer(res.body), + data_long: await body_stream_to_buffer(l.body) + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/+page.svelte new file mode 100644 index 000000000000..bf3c9341bc8b --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/+page.svelte @@ -0,0 +1,30 @@ + + +{JSON.stringify(arr)} + +
+ +{ok} + + {JSON.stringify([...new Uint8Array(data.data_long)])} + diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/data/+server.js b/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/data/+server.js new file mode 100644 index 000000000000..c866fb28c2fd --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-body-stream-b64/data/+server.js @@ -0,0 +1,7 @@ +export const GET = () => { + return new Response(new Uint8Array([1, 2, 3, 4])); +}; + +export const POST = async ({ request }) => { + return new Response(await request.arrayBuffer()); +}; diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 9d5bd283ebac..55db0bf20db6 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -318,6 +318,28 @@ test.describe('Load', () => { } }); + test('fetches using a body stream serialized with b64', async ({ page, javaScriptEnabled }) => { + await page.goto('/load/fetch-body-stream-b64'); + + expect(await page.textContent('.test-content')).toBe('[1,2,3,4]'); + + if (!javaScriptEnabled) { + const payload = '{"status":200,"statusText":"","headers":{},"body":"AQIDBA=="}'; + const post_payload = + '{"status":200,"statusText":"","headers":{},"body":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=="}'; + + const script_content = await page.innerHTML( + 'script[data-sveltekit-fetched][data-b64][data-url="/load/fetch-body-stream-b64/data"]' + ); + const post_script_content = await page.innerHTML( + 'script[data-sveltekit-fetched][data-b64][data-url="/load/fetch-body-stream-b64/data"][data-hash="16h3sp1"]' + ); + + expect(script_content).toBe(payload); + expect(post_script_content).toBe(post_payload); + } + }); + test('json string is returned', async ({ page }) => { await page.goto('/load/relay'); expect(await page.textContent('h1')).toBe('42');