Skip to content

Commit d761749

Browse files
solve async tree stuff
1 parent 51392be commit d761749

File tree

3 files changed

+133
-78
lines changed

3 files changed

+133
-78
lines changed

packages/svelte/src/internal/server/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export async function render_async(component, options = {}) {
168168
for (const cleanup of async_on_destroy) cleanup();
169169
async_on_destroy = prev_on_destroy;
170170

171-
let { head, body } = await payload.collect_async();
171+
let { head, body } = await payload;
172172
head += payload.global.head.title.value;
173173

174174
for (const { hash, code } of payload.global.css) {
@@ -625,10 +625,10 @@ export function maybe_selected(payload, value) {
625625
* @returns {void}
626626
*/
627627
export function valueless_option(payload, children) {
628-
var i = payload.out.length;
628+
var i = payload.length;
629629

630630
// prior to children, `payload` has some combination of string/unresolved payload that ends in `<option ...>`
631-
payload.child((payload) => children(payload));
631+
payload.child(children);
632632

633633
// post-children, `payload` has child content, possibly also with some number of hydration comments.
634634
// we can compact this last chunk of content to see if it matches the select value...

packages/svelte/src/internal/server/payload.js

Lines changed: 130 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
/** @typedef {'head' | 'body'} PayloadType */
22
/** @typedef {{ [key in PayloadType]: string }} AccumulatedContent */
33
/** @typedef {{ start: number, end: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent> }} Compaction */
4+
/**
5+
* @template T
6+
* @typedef {T | Promise<T>} MaybePromise<T>
7+
*/
48

59
/**
610
* Payloads are basically a tree of `string | Payload`s, where each `Payload` in the tree represents
711
* work that may or may not have completed. A payload can be {@link collect}ed to aggregate the
812
* content from itself and all of its children, but this will throw if any of the children are
9-
* performing asynchronous work. A payload can also be collected asynchronously with
10-
* {@link collect_async}, which will wait for all children to complete before collecting their
11-
* contents.
13+
* performing asynchronous work. To asynchronously collect a payload, just `await` it.
1214
*
1315
* The `string` values within a payload are always associated with the {@link type} of that payload. To switch types,
1416
* call {@link child} with a different `type` argument.
1517
*/
1618
export class Payload {
1719
/**
20+
* The contents of the payload.
21+
* @type {(string | Payload)[]}
22+
*/
23+
#out = [];
24+
25+
/**
26+
* The type of string content that this payload is accumulating.
1827
* @type {PayloadType}
1928
*/
2029
type;
@@ -23,17 +32,12 @@ export class Payload {
2332
parent;
2433

2534
/**
26-
* The contents of the payload.
27-
* @type {(string | Payload)[]}
28-
*/
29-
out = [];
30-
31-
/**
32-
* A promise that resolves when this payload's blocking asynchronous work is done.
33-
* If this promise is not resolved, it is not safe to collect the payload from `out`.
34-
* @type {Promise<void> | undefined}
35+
* Asynchronous work associated with this payload. `initial` is the promise from the function
36+
* this payload was passed to (if that function was async), and `followup` is any any additional
37+
* work from `compact` calls that needs to complete prior to collecting this payload's content.
38+
* @type {{ initial: Promise<void> | undefined, followup: Promise<void>[] | undefined }}
3539
*/
36-
promise;
40+
promises = { initial: undefined, followup: undefined };
3741

3842
/**
3943
* State which is associated with the content tree as a whole.
@@ -65,72 +69,62 @@ export class Payload {
6569

6670
/**
6771
* Create a child payload. The child payload inherits the state from the parent,
68-
* but has its own `out` array and `promise` property. The child payload is automatically
69-
* inserted into the parent payload's `out` array.
70-
* @param {(tree: Payload) => void | Promise<void>} render
72+
* but has its own content.
73+
* @param {(tree: Payload) => MaybePromise<void>} render
7174
* @param {PayloadType} [type]
7275
* @returns {void}
7376
*/
7477
child(render, type) {
7578
const child = new Payload(this.global, this.local, this, type);
76-
this.out.push(child);
79+
this.#out.push(child);
7780
const result = render(child);
7881
if (result instanceof Promise) {
79-
child.promise = result;
82+
child.promises.initial = result;
8083
}
8184
}
8285

83-
/** @param {string} content */
86+
/**
87+
* @param {(value: { head: string, body: string }) => void} onfulfilled
88+
*/
89+
async then(onfulfilled) {
90+
const content = await Payload.#collect_content([this], this.type);
91+
return onfulfilled(content);
92+
}
93+
94+
/**
95+
* @param {string} content
96+
*/
8497
push(content) {
85-
this.out.push(content);
98+
this.#out.push(content);
8699
}
87100

88101
/**
89102
* Compact everything between `start` and `end` into a single payload, then call `fn` with the result of that payload.
90103
* The compacted payload will be sync if all of the children are sync and {@link fn} is sync, otherwise it will be async.
91-
* @param {{ start: number, end?: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent> }} args
104+
* @param {{ start: number, end?: number, fn: (content: AccumulatedContent) => AccumulatedContent }} args
92105
*/
93-
compact({ start, end = this.out.length, fn }) {
106+
compact({ start, end = this.#out.length, fn }) {
94107
const child = new Payload(this.global, this.local, this);
95-
const to_compact = this.out.splice(start, end - start, child);
96-
const promises = Payload.#collect_promises(to_compact, []);
97-
98-
const push_result = () => {
99-
const res = fn(Payload.#collect_content(to_compact, this.type));
100-
if (res instanceof Promise) {
101-
const promise = res.then((resolved) => {
102-
Payload.#push_accumulated_content(child, resolved);
103-
});
104-
return promise;
105-
} else {
106-
Payload.#push_accumulated_content(child, res);
107-
}
108-
};
109-
110-
if (promises.length > 0) {
111-
// we have to wait for the accumulated work associated with all pruned branches to complete,
112-
// then we can accumulate their content to compact it.
113-
child.promise = Promise.all(promises).then(push_result);
108+
const to_compact = this.#out.splice(start, end - start, child);
109+
const content = Payload.#collect_content(to_compact, this.type);
110+
111+
if (content instanceof Promise) {
112+
const followup = content
113+
.then((content) => fn(content))
114+
.then((transformed_content) =>
115+
Payload.#push_accumulated_content(child, transformed_content)
116+
);
117+
(this.promises.followup ??= []).push(followup);
114118
} else {
115-
push_result();
119+
Payload.#push_accumulated_content(child, fn(content));
116120
}
117121
}
118122

119123
/**
120124
* @returns {number[]}
121125
*/
122126
get_path() {
123-
return this.parent ? [...this.parent.get_path(), this.parent.out.indexOf(this)] : [];
124-
}
125-
126-
/**
127-
* Waits for all child payloads to finish their blocking asynchronous work, then returns the generated content.
128-
* @returns {Promise<AccumulatedContent>}
129-
*/
130-
async collect_async() {
131-
// TODO: Should probably use `Promise.allSettled` here just so we can report detailed errors
132-
await Promise.all(Payload.#collect_promises(this.out, this.promise ? [this.promise] : []));
133-
return Payload.#collect_content(this.out, this.type);
127+
return this.parent ? [...this.parent.get_path(), this.parent.#out.indexOf(this)] : [];
134128
}
135129

136130
/**
@@ -139,19 +133,19 @@ export class Payload {
139133
* @returns {AccumulatedContent}
140134
*/
141135
collect() {
142-
const promises = Payload.#collect_promises(this.out, this.promise ? [this.promise] : []);
143-
if (promises.length > 0) {
136+
const content = Payload.#collect_content(this.#out, this.type);
137+
if (content instanceof Promise) {
144138
// TODO is there a good way to report where this is? Probably by using some sort of loc or stack trace in `child` creation.
145139
throw new Error('Encountered an asynchronous component while rendering synchronously');
146140
}
147141

148-
return Payload.#collect_content(this.out, this.type);
142+
return content;
149143
}
150144

151145
copy() {
152146
const copy = new Payload(this.global, this.local, this.parent, this.type);
153-
copy.out = this.out.map((item) => (typeof item === 'string' ? item : item.copy()));
154-
copy.promise = this.promise;
147+
copy.#out = this.#out.map((item) => (typeof item === 'string' ? item : item.copy()));
148+
copy.promises = this.promises;
155149
return copy;
156150
}
157151

@@ -161,45 +155,94 @@ export class Payload {
161155
subsume(other) {
162156
this.global.subsume(other.global);
163157
this.local = other.local;
164-
this.out = other.out.map((item) => {
158+
this.#out = other.#out.map((item) => {
165159
if (typeof item !== 'string') {
166160
item.subsume(item);
167161
}
168162
return item;
169163
});
170-
this.promise = other.promise;
164+
this.promises = other.promises;
171165
this.type = other.type;
172166
}
173167

168+
get length() {
169+
return this.#out.length;
170+
}
171+
174172
/**
173+
* Collect all of the code from the `out` array and return it as a string, or a promise resolving to a string.
175174
* @param {(string | Payload)[]} items
176-
* @param {Promise<void>[]} promises
177-
* @returns {Promise<void>[]}
175+
* @param {PayloadType} current_type
176+
* @param {AccumulatedContent} content
177+
* @returns {MaybePromise<AccumulatedContent>}
178178
*/
179-
static #collect_promises(items, promises) {
179+
static #collect_content(items, current_type, content = { head: '', body: '' }) {
180+
/** @type {MaybePromise<AccumulatedContent>[]} */
181+
const segments = [];
182+
let has_async = false;
183+
184+
const flush = () => {
185+
if (content.head || content.body) {
186+
segments.push(content);
187+
content = { head: '', body: '' };
188+
}
189+
};
190+
180191
for (const item of items) {
181-
if (typeof item === 'string') continue;
182-
if (item.promise) {
183-
promises.push(item.promise);
192+
if (typeof item === 'string') {
193+
content[current_type] += item;
194+
} else {
195+
flush();
196+
197+
if (item.promises.initial) {
198+
has_async = true;
199+
segments.push(
200+
Payload.#collect_content_async([item], current_type, { head: '', body: '' })
201+
);
202+
} else {
203+
const sub = Payload.#collect_content(item.#out, item.type, { head: '', body: '' });
204+
if (sub instanceof Promise) {
205+
has_async = true;
206+
}
207+
segments.push(sub);
208+
}
184209
}
185-
Payload.#collect_promises(item.out, promises);
186210
}
187-
return promises;
211+
212+
flush();
213+
214+
if (has_async) {
215+
return Promise.all(segments).then((content_array) =>
216+
Payload.#squash_accumulated_content(content_array)
217+
);
218+
}
219+
220+
// No async segments — combine synchronously
221+
return Payload.#squash_accumulated_content(/** @type {AccumulatedContent[]} */ (segments));
188222
}
189223

190224
/**
191225
* Collect all of the code from the `out` array and return it as a string.
192226
* @param {(string | Payload)[]} items
193227
* @param {PayloadType} current_type
194228
* @param {AccumulatedContent} content
195-
* @returns {AccumulatedContent}
229+
* @returns {Promise<AccumulatedContent>}
196230
*/
197-
static #collect_content(items, current_type, content = { head: '', body: '' }) {
231+
static async #collect_content_async(items, current_type, content = { head: '', body: '' }) {
198232
for (const item of items) {
199233
if (typeof item === 'string') {
200234
content[current_type] += item;
201235
} else {
202-
Payload.#collect_content(item.out, item.type, content);
236+
if (item.promises.initial) {
237+
// this represents the async function that's modifying this payload.
238+
// we can't do anything until it's done and we know our `out` array is complete.
239+
await item.promises.initial;
240+
}
241+
for (const followup of item.promises.followup ?? []) {
242+
// this is sequential because `compact` could synchronously queue up additional followup work
243+
await followup;
244+
}
245+
await Payload.#collect_content_async(item.#out, item.type, content);
203246
}
204247
}
205248
return content;
@@ -214,9 +257,24 @@ export class Payload {
214257
if (!content) continue;
215258
const child = new Payload(tree.global, tree.local, tree, /** @type {PayloadType} */ (type));
216259
child.push(content);
217-
tree.out.push(child);
260+
tree.#out.push(child);
218261
}
219262
}
263+
264+
/**
265+
* @param {AccumulatedContent[]} content_array
266+
* @returns {AccumulatedContent}
267+
*/
268+
static #squash_accumulated_content(content_array) {
269+
return content_array.reduce(
270+
(acc, content) => {
271+
acc.head += content.head;
272+
acc.body += content.body;
273+
return acc;
274+
},
275+
{ head: '', body: '' }
276+
);
277+
}
220278
}
221279

222280
export class TreeState {

packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_config.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)