From b915fa0c3b60a847ae3505fea83681a0709fad9a Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 9 Oct 2026 15:00:30 -0600 Subject: [PATCH] feat: keep client changes to a `Warp` separate from the values being streamed in --- documentation/docs/06-runtime/05-warp.md | 2 + .../svelte/src/internal/client/types.d.ts | 5 + packages/svelte/src/internal/client/warp.js | 245 ++++++++++++++++-- .../svelte/src/internal/client/warp.test.ts | 158 +++++++++++ .../svelte/src/internal/server/renderer.js | 46 ++-- .../svelte/src/internal/server/warp.test.ts | 6 +- packages/svelte/types/index.d.ts | 3 + 7 files changed, 431 insertions(+), 34 deletions(-) create mode 100644 packages/svelte/src/internal/client/warp.test.ts diff --git a/documentation/docs/06-runtime/05-warp.md b/documentation/docs/06-runtime/05-warp.md index 14c4000b24..f23b4b39c7 100644 --- a/documentation/docs/06-runtime/05-warp.md +++ b/documentation/docs/06-runtime/05-warp.md @@ -159,6 +159,8 @@ response.end(''); If you stop iterating over `tail` early, the background work is stopped. Since the streamed ``, - tail: - late === null || !ready.next - ? empty() - : stream_scripts(tail, ready.next, csp_attr, late.close) + tail: streaming + ? stream_scripts( + tail, + /** @type {Promise>} */ (ready.next), + csp_attr, + late.close + ) + : empty() }; } @@ -1226,26 +1236,30 @@ function stream_scripts(tail, next, csp_attr, close) { * @returns {AsyncGenerator} */ async function* generate_scripts(tail, next, csp_attr) { - while (true) { - const result = await next; - if (result.done) return; + let done = false; - let code = result.value; - next = tail.next(); + while (!done) { + let code = ''; + let result = await next; while (true) { + if (result.done) { + // tell the client that this stream has finished + code += 'window.__svelte.s -= 1;'; + done = true; + break; + } + + code += result.value; + next = tail.next(); + const more = await Promise.race([ next, new Promise((fulfil) => setTimeout(() => fulfil(MACROTASK), 0)) ]); if (more === MACROTASK) break; - - const { done, value } = /** @type {IteratorResult} */ (more); - if (done) break; - - code += value; - next = tail.next(); + result = /** @type {IteratorResult} */ (more); } yield `${code}`; diff --git a/packages/svelte/src/internal/server/warp.test.ts b/packages/svelte/src/internal/server/warp.test.ts index f8577dbf38..98ad8aa3dd 100644 --- a/packages/svelte/src/internal/server/warp.test.ts +++ b/packages/svelte/src/internal/server/warp.test.ts @@ -318,7 +318,7 @@ describe('streaming', () => { /** Collects the tail, and returns the revived values once everything has been evaluated */ async function revive_streamed(head: string, tail: AsyncIterable) { - const window: { __svelte?: { w?: Map> } } = {}; + const window: { __svelte?: { w?: Map>; s?: number } } = {}; const run = (html: string) => { for (const [, script] of html.matchAll(/]*)?>([\s\S]*?)<\/script>/g)) { new Function('window', script)(window); @@ -329,10 +329,14 @@ describe('streaming', () => { const chunks: string[] = []; for await (const chunk of tail) { + // the client is told that a stream is in progress until the last chunk + expect(window.__svelte?.s).toBe(1); chunks.push(chunk); run(chunk); } + expect(window.__svelte?.s).toBe(0); + return { values: window.__svelte?.w, chunks }; } diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index fee37e2ec2..f025691b50 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -497,8 +497,11 @@ declare module 'svelte' { clear(): void; forEach(callback: (value: V, key: K, map: Map) => void, this_arg?: any): void; + entries(): IterableIterator<[K, V]>; + keys(): IterableIterator; + values(): IterableIterator; get size(): number; [Symbol.iterator](): IterableIterator<[K, V]>;