diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 62ee22d6fc..3064ee88d7 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -68,7 +68,7 @@ export let on_destroy = []; */ export function render(component, options = {}) { try { - const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : ''); + const payload = new Payload({ id_prefix: options.idPrefix ? options.idPrefix + '-' : '' }); const prev_on_destroy = on_destroy; on_destroy = []; @@ -107,7 +107,7 @@ export function render(component, options = {}) { head += ``; } - const body = payload.out.join(''); + const body = payload.collect(); return { head, @@ -569,6 +569,9 @@ export function valueless_option(payload, children) { if (body.replace(//g, '') === payload.select_value) { // replace '>' with ' selected>' (closing tag will be added later) var last_item = payload.out[i - 1]; + if (typeof last_item !== 'string') { + throw new Error('TODO something very bad has happened, this should be very impossible'); + } payload.out[i - 1] = last_item.slice(0, -1) + ' selected>'; // Remove the old items after position i and add the body as a single item payload.out.splice(i, payload.out.length - i, body); diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js index 195488e061..315f8af985 100644 --- a/packages/svelte/src/internal/server/payload.js +++ b/packages/svelte/src/internal/server/payload.js @@ -1,3 +1,5 @@ +import { deferred } from '../shared/utils'; + export class HeadPayload { /** @type {Set<{ hash: string; code: string }>} */ css = new Set(); @@ -16,17 +18,109 @@ export class HeadPayload { export class Payload { /** @type {Set<{ hash: string; code: string }>} */ - css = new Set(); - /** @type {string[]} */ + css; + /** @type {(string | ChildPayload)[]} */ out = []; - uid = () => ''; + /** @type {() => string} */ + uid; + /** @type {string | undefined} */ select_value = undefined; + /** @type {HeadPayload} */ + head; + /** @type {'sync' | 'async'} */ + mode; + /** @type {Promise[]} */ + tail = []; + + /** + * @param {{ id_prefix?: string, mode?: 'sync' | 'async', head?: HeadPayload, uid?: () => string, out?: (string | ChildPayload)[], css?: Set<{ hash: string; code: string }>, select_value?: any }} args + */ + constructor({ + id_prefix = '', + mode = 'sync', + head = new HeadPayload(), + uid = props_id_generator(id_prefix), + css = new Set() + } = {}) { + this.uid = uid; + this.head = head; + this.mode = mode; + this.css = css; + } + + /** + * Create a child scope. `front` represents the initial, synchronous code, and `back` represents all code from the first `await` onwards. + * Typically a child will be created for each component. + * @param {{ front: (args: { payload: Payload }) => void, back: (args: { payload: Payload }) => Promise }} args + * @returns {void} + */ + child({ front, back }) { + const child = new ChildPayload(this); + front({ payload: child }); + // TODO: boundary stuff? Or does this go inside the `back` function? + back({ payload: child }).then(() => child.deferred.resolve()); + } + + /** + * Waits for all child payloads to finish their blocking asynchronous work, then returns the generated HTML. + * @returns {Promise} + */ + async collect_async() { + // TODO throw in `sync` mode + /** @type {Promise[]} */ + const promises = []; + + /** + * @param {(string | ChildPayload)[]} items + */ + function collect_promises(items) { + for (const item of items) { + if (item instanceof ChildPayload) { + promises.push(item.deferred.promise); + collect_promises(item.out); + } + } + } + + collect_promises(this.out); + await Promise.all(promises); + return this.collect(); + } + + /** + * Collect all of the code from the `out` array and return it as a string. If in `async` mode, wait on + * `finished` prior to collecting. + * @returns {string} + */ + collect() { + // TODO throw in `async` mode + let html = ''; + for (const item of this.out) { + if (typeof item === 'string') { + html += item; + } else { + html += item.collect(); + } + } + return html; + } +} - head = new HeadPayload(); +class ChildPayload extends Payload { + deferred = /** @type {ReturnType>} */ (deferred()); - constructor(id_prefix = '') { - this.uid = props_id_generator(id_prefix); - this.head.uid = this.uid; + /** + * @param {Payload} parent + */ + constructor(parent) { + super({ + mode: parent.mode, + head: parent.head, + uid: parent.uid, + css: parent.css + }); + this.root = parent; + parent.out.push(this); } }