adjust-boundary-error-message
S. Elliott Johnson 1 week ago
parent e39b4b7546
commit 51392be8ae

@ -40,10 +40,10 @@ function print_error(payload, message) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(message); console.error(message);
payload.out.push({ payload.child(
type: 'head', (payload) => payload.push(`<script>console.error(${JSON.stringify(message)})</script>`),
content: `<script>console.error(${JSON.stringify(message)})</script>` 'head'
}); );
} }
export function reset_elements() { export function reset_elements() {

@ -191,9 +191,11 @@ export async function render_async(component, options = {}) {
* @returns {void} * @returns {void}
*/ */
export function head(payload, fn) { export function head(payload, fn) {
payload.out.push({ type: 'head', content: BLOCK_OPEN }); payload.child((payload) => {
payload.child(fn, 'head'); payload.push(BLOCK_OPEN);
payload.out.push({ type: 'head', content: BLOCK_CLOSE }); payload.child(fn);
payload.push(BLOCK_CLOSE);
}, 'head');
} }
/** /**

@ -1,22 +1,21 @@
/** @typedef {{ type: 'head' | 'body', content: string }} TNode */ /** @typedef {'head' | 'body'} PayloadType */
/** @typedef {{ [key in TNode['type']]: string }} AccumulatedContent */ /** @typedef {{ [key in PayloadType]: string }} AccumulatedContent */
/** @typedef {{ start: number, end: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent> }} Compaction */ /** @typedef {{ start: number, end: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent> }} 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
/** /**
* /** * Payloads are basically a tree of `string | Payload`s, where each `Payload` in the tree represents
* A base class for payloads. Payloads are basically a tree of `string | Payload`s, where each * work that may or may not have completed. A payload can be {@link collect}ed to aggregate the
* `Payload` in the tree represents work that may or may not have completed. A payload can be * content from itself and all of its children, but this will throw if any of the children are
* {@link collect}ed to aggregate the content from itself and all of its children, but this will * performing asynchronous work. A payload can also be collected asynchronously with
* throw if any of the children are performing asynchronous work. A payload can also be collected * {@link collect_async}, which will wait for all children to complete before collecting their
* asynchronously with {@link collect_async}, which will wait for all children to complete before * contents.
* 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 { export class Payload {
/** /**
* @type {TNode['type']} * @type {PayloadType}
*/ */
type; type;
@ -25,7 +24,7 @@ export class Payload {
/** /**
* The contents of the payload. * The contents of the payload.
* @type {(TNode | Payload)[]} * @type {(string | Payload)[]}
*/ */
out = []; out = [];
@ -55,7 +54,7 @@ export class Payload {
* @param {TreeState} [global] * @param {TreeState} [global]
* @param {{ select_value: string | undefined }} [local] * @param {{ select_value: string | undefined }} [local]
* @param {Payload | undefined} [parent] * @param {Payload | undefined} [parent]
* @param {TNode['type']} [type] * @param {PayloadType} [type]
*/ */
constructor(global = new TreeState(), local = { select_value: undefined }, parent, type) { constructor(global = new TreeState(), local = { select_value: undefined }, parent, type) {
this.global = global; this.global = global;
@ -69,7 +68,7 @@ export class Payload {
* but has its own `out` array and `promise` property. The child payload is automatically * but has its own `out` array and `promise` property. The child payload is automatically
* inserted into the parent payload's `out` array. * inserted into the parent payload's `out` array.
* @param {(tree: Payload) => void | Promise<void>} render * @param {(tree: Payload) => void | Promise<void>} render
* @param {TNode['type']} [type] * @param {PayloadType} [type]
* @returns {void} * @returns {void}
*/ */
child(render, type) { child(render, type) {
@ -81,13 +80,9 @@ export class Payload {
} }
} }
/** /** @param {string} content */
* 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
*/
push(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 to_compact = this.out.splice(start, end - start, child);
const promises = Payload.#collect_promises(to_compact, []); const promises = Payload.#collect_promises(to_compact, []);
/** @param {AccumulatedContent | Promise<AccumulatedContent>} res */ const push_result = () => {
const push_result = (res) => { const res = fn(Payload.#collect_content(to_compact, this.type));
if (res instanceof Promise) { if (res instanceof Promise) {
child.promise = res.then((resolved) => { const promise = res.then((resolved) => {
Payload.#push_accumulated_content(child, resolved); Payload.#push_accumulated_content(child, resolved);
}); });
return promise;
} else { } else {
Payload.#push_accumulated_content(child, res); Payload.#push_accumulated_content(child, res);
} }
}; };
if (promises.length > 0) { 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. // then we can accumulate their content to compact it.
child.promise = Promise.all(promises) child.promise = Promise.all(promises).then(push_result);
.then(() => fn(Payload.#collect_content(to_compact)))
.then(push_result);
} else { } else {
push_result(fn(Payload.#collect_content(to_compact))); push_result();
} }
} }
@ -136,26 +130,27 @@ export class Payload {
async collect_async() { async collect_async() {
// TODO: Should probably use `Promise.allSettled` here just so we can report detailed errors // 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] : [])); 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} * @returns {AccumulatedContent}
*/ */
collect() { collect() {
const promises = Payload.#collect_promises(this.out, this.promise ? [this.promise] : []); const promises = Payload.#collect_promises(this.out, this.promise ? [this.promise] : []);
if (promises.length > 0) { 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'); 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() { copy() {
const copy = new Payload(this.global, this.local, this.parent, this.type); 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; copy.promise = this.promise;
return copy; return copy;
} }
@ -167,7 +162,7 @@ export class Payload {
this.global.subsume(other.global); this.global.subsume(other.global);
this.local = other.local; this.local = other.local;
this.out = other.out.map((item) => { this.out = other.out.map((item) => {
if (item instanceof Payload) { if (typeof item !== 'string') {
item.subsume(item); item.subsume(item);
} }
return item; return item;
@ -177,34 +172,34 @@ export class Payload {
} }
/** /**
* @param {(TNode | Payload)[]} items * @param {(string | Payload)[]} items
* @param {Promise<void>[]} promises * @param {Promise<void>[]} promises
* @returns {Promise<void>[]} * @returns {Promise<void>[]}
*/ */
static #collect_promises(items, promises) { static #collect_promises(items, promises) {
for (const item of items) { for (const item of items) {
if (item instanceof Payload) { if (typeof item === 'string') continue;
if (item.promise) { if (item.promise) {
promises.push(item.promise); promises.push(item.promise);
}
Payload.#collect_promises(item.out, promises);
} }
Payload.#collect_promises(item.out, promises);
} }
return promises; return promises;
} }
/** /**
* 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.
* @param {(TNode | Payload)[]} items * @param {(string | Payload)[]} items
* @param {PayloadType} current_type
* @param {AccumulatedContent} content * @param {AccumulatedContent} content
* @returns {AccumulatedContent} * @returns {AccumulatedContent}
*/ */
static #collect_content(items, content = { head: '', body: '' }) { static #collect_content(items, current_type, content = { head: '', body: '' }) {
for (const item of items) { for (const item of items) {
if (item instanceof Payload) { if (typeof item === 'string') {
Payload.#collect_content(item.out, content); content[current_type] += item;
} else { } else {
content[item.type] += item.content; Payload.#collect_content(item.out, item.type, content);
} }
} }
return content; return content;
@ -216,7 +211,10 @@ export class Payload {
*/ */
static #push_accumulated_content(tree, accumulated_content) { static #push_accumulated_content(tree, accumulated_content) {
for (const [type, content] of Object.entries(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);
} }
} }
} }

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({ load_compiled: true });
Loading…
Cancel
Save