diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 830357f090..293d447e19 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -176,7 +176,7 @@ export async function render_async(component, options = {}) { for (const cleanup of async_on_destroy) cleanup(); async_on_destroy = prev_on_destroy; - let { head, body } = await payload; + let { head, body } = await payload.collect_async(); head += payload.global.head.title.value; body = BLOCK_OPEN + body + BLOCK_CLOSE; // this inserts a fake boundary so hydration matches diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js index 358f02c5c9..25d339b853 100644 --- a/packages/svelte/src/internal/server/payload.js +++ b/packages/svelte/src/internal/server/payload.js @@ -95,14 +95,6 @@ export class Payload { } } - /** - * @param {(value: { head: string, body: string }) => void} onfulfilled - */ - async then(onfulfilled) { - const content = await Payload.#collect_content([this], this.type); - return onfulfilled(content); - } - /** * @param {string | (() => Promise)} content */ @@ -126,25 +118,11 @@ export class Payload { compact({ start, end = this.#out.length, fn }) { const child = new Payload(this.global, this.local, this); const to_compact = this.#out.splice(start, end - start, child); - const content = Payload.#collect_content(to_compact, this.type); - - if (content instanceof Promise) { - const followup = content - .then((content) => fn(content)) - .then((transformed_content) => - Payload.#push_accumulated_content(child, transformed_content) - ); - this.promises.followup.push(followup); + + if (this.global.mode === 'sync') { + Payload.#compact(fn, child, to_compact, this.type); } else { - const transformed_content = fn(content); - if (transformed_content instanceof Promise) { - const followup = transformed_content.then((content) => - Payload.#push_accumulated_content(child, content) - ); - this.promises.followup.push(followup); - } else { - Payload.#push_accumulated_content(child, transformed_content); - } + this.promises.followup.push(Payload.#compact_async(fn, child, to_compact, this.type)); } } @@ -161,16 +139,15 @@ export class Payload { * @returns {AccumulatedContent} */ collect() { - const content = Payload.#collect_content(this.#out, this.type); - if (content instanceof Promise) { - // TODO improve message - // guess you could also end up here if you called `collect` in an async context but... just don't bro - throw new Error( - 'invariant: should never reach this, as child throws when it encounters async work in a synchronous context' - ); - } + return Payload.#collect_content([this], this.type); + } - return content; + /** + * Collect all of the code from the `out` array and return it as a string. + * @returns {Promise} + */ + collect_async() { + return Payload.#collect_content_async([this], this.type); } copy() { @@ -205,63 +182,52 @@ export class Payload { return this.#out.length; } + /** + * @param {(content: AccumulatedContent) => AccumulatedContent | Promise} fn + * @param {Payload} child + * @param {PayloadItem[]} to_compact + * @param {PayloadType} type + */ + static #compact(fn, child, to_compact, type) { + const content = Payload.#collect_content(to_compact, type); + const transformed_content = fn(content); + if (transformed_content instanceof Promise) { + throw new Error('invariant: should never reach this'); + } else { + Payload.#push_accumulated_content(child, transformed_content); + } + } + + /** + * @param {(content: AccumulatedContent) => AccumulatedContent | Promise} fn + * @param {Payload} child + * @param {PayloadItem[]} to_compact + * @param {PayloadType} type + */ + static async #compact_async(fn, child, to_compact, type) { + const content = await Payload.#collect_content_async(to_compact, type); + const transformed_content = await fn(content); + Payload.#push_accumulated_content(child, transformed_content); + } + /** * Collect all of the code from the `out` array and return it as a string, or a promise resolving to a string. * @param {PayloadItem[]} items * @param {PayloadType} current_type * @param {AccumulatedContent} content - * @returns {MaybePromise} + * @returns {AccumulatedContent} */ static #collect_content(items, current_type, content = { head: '', body: '' }) { - /** @type {MaybePromise[]} */ - const segments = []; - let has_async = false; - - const flush = () => { - if (content.head || content.body) { - segments.push(content); - content = { head: '', body: '' }; - } - }; - for (const item of items) { if (typeof item === 'string') { content[current_type] += item; + } else if (item instanceof Payload) { + Payload.#collect_content(item.#out, item.type, content); } else { - flush(); - - if (item instanceof Promise) { - has_async = true; - segments.push( - item.then((resolved) => { - const content = { head: '', body: '' }; - content[current_type] = resolved; - return content; - }) - ); - } else if (item.promises.initial || item.promises.followup.length) { - has_async = true; - segments.push(Payload.#collect_content_async([item], current_type)); - } else { - const sub = Payload.#collect_content(item.#out, item.type); - if (sub instanceof Promise) { - has_async = true; - } - segments.push(sub); - } + throw new Error('invariant: should never reach this'); } } - - flush(); - - if (has_async) { - return Promise.all(segments).then((content_array) => - Payload.#squash_accumulated_content(content_array) - ); - } - - // No async segments — combine synchronously - return Payload.#squash_accumulated_content(/** @type {AccumulatedContent[]} */ (segments)); + return content; } /** @@ -304,21 +270,6 @@ export class Payload { tree.#out.push(child); } } - - /** - * @param {AccumulatedContent[]} content_array - * @returns {AccumulatedContent} - */ - static #squash_accumulated_content(content_array) { - return content_array.reduce( - (acc, content) => { - acc.head += content.head; - acc.body += content.body; - return acc; - }, - { head: '', body: '' } - ); - } } export class TreeState { diff --git a/packages/svelte/src/internal/server/payload.test.ts b/packages/svelte/src/internal/server/payload.test.ts index af8e0f52c2..8e2c87e040 100644 --- a/packages/svelte/src/internal/server/payload.test.ts +++ b/packages/svelte/src/internal/server/payload.test.ts @@ -73,21 +73,7 @@ test('creating an async child in a sync context throws', () => { ).toThrow('Encountered an asynchronous component while rendering synchronously'); }); -test('awaiting payload resolves async children', async () => { - const payload = new Payload(new TreeState('async')); - payload.push('a'); - payload.child(async ($$payload) => { - await Promise.resolve(); - $$payload.push('x'); - }); - payload.push('y'); - - const { body, head } = await payload; - assert.equal(head, ''); - assert.equal(body, 'axy'); -}); - -test('then() allows awaiting payload to get aggregated content', async () => { +test('collect_async allows awaiting payload to get aggregated content', async () => { const payload = new Payload(new TreeState('async')); payload.push('1'); payload.child(async ($$payload) => { @@ -96,7 +82,7 @@ test('then() allows awaiting payload to get aggregated content', async () => { }); payload.push('3'); - const result = await payload; + const result = await payload.collect_async(); assert.deepEqual(result, { head: '', body: '123' }); }); @@ -132,7 +118,7 @@ test('compact schedules followup when compaction input is async', async () => { fn: (content) => ({ body: content.body.toLowerCase(), head: '' }) }); - const { body, head } = await payload; + const { body, head } = await payload.collect_async(); assert.equal(head, ''); assert.equal(body, 'axb'); }); @@ -261,7 +247,7 @@ test('push accepts async functions in async context', async () => { }); payload.push('c'); - const { head, body } = await payload; + const { head, body } = await payload.collect_async(); assert.equal(head, ''); assert.equal(body, 'abc'); }); @@ -284,7 +270,7 @@ test('push handles async functions with different timing', async () => { // Regular string payload.push('sync'); - const { head, body } = await payload; + const { head, body } = await payload.collect_async(); assert.equal(head, ''); assert.equal(body, 'fastslowsync'); }); @@ -296,7 +282,7 @@ test('push async functions work with head content type', async () => { return 'Async Title'; }); - const { head, body } = await payload; + const { head, body } = await payload.collect_async(); assert.equal(body, ''); assert.equal(head, 'Async Title'); }); @@ -316,7 +302,7 @@ test('push async functions can be mixed with child payloads', async () => { payload.push('-end'); - const { head, body } = await payload; + const { head, body } = await payload.collect_async(); assert.equal(head, ''); assert.equal(body, 'start-async-child--end'); }); @@ -335,7 +321,7 @@ test('push async functions work with compact operations', async () => { fn: (content) => ({ head: '', body: content.body.toUpperCase() }) }); - const { head, body } = await payload; + const { head, body } = await payload.collect_async(); assert.equal(head, ''); assert.equal(body, 'ABC'); });