diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js
index 6edc1d277a..8d55e6782d 100644
--- a/packages/svelte/src/internal/server/dev.js
+++ b/packages/svelte/src/internal/server/dev.js
@@ -40,10 +40,10 @@ function print_error(payload, message) {
// eslint-disable-next-line no-console
console.error(message);
- payload.out.push({
- type: 'head',
- content: ``
- });
+ payload.child(
+ (payload) => payload.push(``),
+ 'head'
+ );
}
export function reset_elements() {
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index fdbe07f48c..3785d8996f 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -191,9 +191,11 @@ export async function render_async(component, options = {}) {
* @returns {void}
*/
export function head(payload, fn) {
- payload.out.push({ type: 'head', content: BLOCK_OPEN });
- payload.child(fn, 'head');
- payload.out.push({ type: 'head', content: BLOCK_CLOSE });
+ payload.child((payload) => {
+ payload.push(BLOCK_OPEN);
+ payload.child(fn);
+ payload.push(BLOCK_CLOSE);
+ }, 'head');
}
/**
diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js
index 5261fd6658..df6b6ba12a 100644
--- a/packages/svelte/src/internal/server/payload.js
+++ b/packages/svelte/src/internal/server/payload.js
@@ -1,22 +1,21 @@
-/** @typedef {{ type: 'head' | 'body', content: string }} TNode */
-/** @typedef {{ [key in TNode['type']]: string }} AccumulatedContent */
+/** @typedef {'head' | 'body'} PayloadType */
+/** @typedef {{ [key in PayloadType]: string }} AccumulatedContent */
/** @typedef {{ start: number, end: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise }} Compaction */
-// TODO we test for `instanceof AsyncContentTree` in some tight loops -- we might optimize
-// by giving the tree a symbol property and checking that instead if we can actually notice any impact
-
/**
- * /**
- * A base class for payloads. Payloads are basically a tree of `string | Payload`s, where each
- * `Payload` in the tree represents work that may or may not have completed. A payload can be
- * {@link collect}ed to aggregate the content from itself and all of its children, but this will
- * throw if any of the children are performing asynchronous work. A payload can also be collected
- * asynchronously with {@link collect_async}, which will wait for all children to complete before
- * collecting their contents.
+ * Payloads are basically a tree of `string | Payload`s, where each `Payload` in the tree represents
+ * work that may or may not have completed. A payload can be {@link collect}ed to aggregate the
+ * content from itself and all of its children, but this will throw if any of the children are
+ * performing asynchronous work. A payload can also be collected asynchronously with
+ * {@link collect_async}, which will wait for all children to complete before collecting their
+ * contents.
+ *
+ * The `string` values within a payload are always associated with the {@link type} of that payload. To switch types,
+ * call {@link child} with a different `type` argument.
*/
export class Payload {
/**
- * @type {TNode['type']}
+ * @type {PayloadType}
*/
type;
@@ -25,7 +24,7 @@ export class Payload {
/**
* The contents of the payload.
- * @type {(TNode | Payload)[]}
+ * @type {(string | Payload)[]}
*/
out = [];
@@ -55,7 +54,7 @@ export class Payload {
* @param {TreeState} [global]
* @param {{ select_value: string | undefined }} [local]
* @param {Payload | undefined} [parent]
- * @param {TNode['type']} [type]
+ * @param {PayloadType} [type]
*/
constructor(global = new TreeState(), local = { select_value: undefined }, parent, type) {
this.global = global;
@@ -69,7 +68,7 @@ export class Payload {
* but has its own `out` array and `promise` property. The child payload is automatically
* inserted into the parent payload's `out` array.
* @param {(tree: Payload) => void | Promise} render
- * @param {TNode['type']} [type]
+ * @param {PayloadType} [type]
* @returns {void}
*/
child(render, type) {
@@ -81,13 +80,9 @@ export class Payload {
}
}
- /**
- * This is a convenience function that allows pushing strings, and will automatically use the configured `type` of this
- * payload. It's fine to push content of a different type to the `out` array; it's just more annoying to write.
- * @param {string} content
- */
+ /** @param {string} content */
push(content) {
- this.out.push({ type: this.type, content });
+ this.out.push(content);
}
/**
@@ -100,25 +95,24 @@ export class Payload {
const to_compact = this.out.splice(start, end - start, child);
const promises = Payload.#collect_promises(to_compact, []);
- /** @param {AccumulatedContent | Promise} res */
- const push_result = (res) => {
+ const push_result = () => {
+ const res = fn(Payload.#collect_content(to_compact, this.type));
if (res instanceof Promise) {
- child.promise = res.then((resolved) => {
+ const promise = res.then((resolved) => {
Payload.#push_accumulated_content(child, resolved);
});
+ return promise;
} else {
Payload.#push_accumulated_content(child, res);
}
};
if (promises.length > 0) {
- // we have to wait for the accumulated work associated with all branches to complete,
+ // we have to wait for the accumulated work associated with all pruned branches to complete,
// then we can accumulate their content to compact it.
- child.promise = Promise.all(promises)
- .then(() => fn(Payload.#collect_content(to_compact)))
- .then(push_result);
+ child.promise = Promise.all(promises).then(push_result);
} else {
- push_result(fn(Payload.#collect_content(to_compact)));
+ push_result();
}
}
@@ -136,26 +130,27 @@ export class Payload {
async collect_async() {
// TODO: Should probably use `Promise.allSettled` here just so we can report detailed errors
await Promise.all(Payload.#collect_promises(this.out, this.promise ? [this.promise] : []));
- return Payload.#collect_content(this.out);
+ return Payload.#collect_content(this.out, this.type);
}
/**
- * Collect all of the code from the `out` array and return it as a string.
+ * Collect all of the code from the `out` array and return it as a string. Throws if any of the children are
+ * performing asynchronous work.
* @returns {AccumulatedContent}
*/
collect() {
const promises = Payload.#collect_promises(this.out, this.promise ? [this.promise] : []);
if (promises.length > 0) {
- // TODO is there a good way to report where this is? Probably by using some sort of loc or stack trace in `child` creation
+ // TODO is there a good way to report where this is? Probably by using some sort of loc or stack trace in `child` creation.
throw new Error('Encountered an asynchronous component while rendering synchronously');
}
- return Payload.#collect_content(this.out);
+ return Payload.#collect_content(this.out, this.type);
}
copy() {
const copy = new Payload(this.global, this.local, this.parent, this.type);
- copy.out = this.out.map((item) => (item instanceof Payload ? item.copy() : item));
+ copy.out = this.out.map((item) => (typeof item === 'string' ? item : item.copy()));
copy.promise = this.promise;
return copy;
}
@@ -167,7 +162,7 @@ export class Payload {
this.global.subsume(other.global);
this.local = other.local;
this.out = other.out.map((item) => {
- if (item instanceof Payload) {
+ if (typeof item !== 'string') {
item.subsume(item);
}
return item;
@@ -177,34 +172,34 @@ export class Payload {
}
/**
- * @param {(TNode | Payload)[]} items
+ * @param {(string | Payload)[]} items
* @param {Promise[]} promises
* @returns {Promise[]}
*/
static #collect_promises(items, promises) {
for (const item of items) {
- if (item instanceof Payload) {
- if (item.promise) {
- promises.push(item.promise);
- }
- Payload.#collect_promises(item.out, promises);
+ if (typeof item === 'string') continue;
+ if (item.promise) {
+ promises.push(item.promise);
}
+ Payload.#collect_promises(item.out, promises);
}
return promises;
}
/**
* Collect all of the code from the `out` array and return it as a string.
- * @param {(TNode | Payload)[]} items
+ * @param {(string | Payload)[]} items
+ * @param {PayloadType} current_type
* @param {AccumulatedContent} content
* @returns {AccumulatedContent}
*/
- static #collect_content(items, content = { head: '', body: '' }) {
+ static #collect_content(items, current_type, content = { head: '', body: '' }) {
for (const item of items) {
- if (item instanceof Payload) {
- Payload.#collect_content(item.out, content);
+ if (typeof item === 'string') {
+ content[current_type] += item;
} else {
- content[item.type] += item.content;
+ Payload.#collect_content(item.out, item.type, content);
}
}
return content;
@@ -216,7 +211,10 @@ export class Payload {
*/
static #push_accumulated_content(tree, accumulated_content) {
for (const [type, content] of Object.entries(accumulated_content)) {
- tree.out.push({ type: /** @type {TNode['type']} */ (type), content });
+ if (!content) continue;
+ const child = new Payload(tree.global, tree.local, tree, /** @type {PayloadType} */ (type));
+ child.push(content);
+ tree.out.push(child);
}
}
}
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_config.js b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_config.js
new file mode 100644
index 0000000000..63632e95e2
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({ load_compiled: true });