From b0550d62ca6982005d878884cd2584c220b36929 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Tue, 13 Oct 2026 21:52:46 +0200 Subject: [PATCH] chore: some loader improvements --- src/common/loader.js | 41 ++++++++++++++++++++++++++++++++++++- src/l/index.js | 9 +++++++- src/styles/loader-fonts.css | 8 ++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/styles/loader-fonts.css diff --git a/src/common/loader.js b/src/common/loader.js index daa4991f..bd027795 100644 --- a/src/common/loader.js +++ b/src/common/loader.js @@ -157,7 +157,18 @@ export function createLoader(config) { }).then(() => { if (loadable.id === loadedId) return; loadedId = loadable.id ?? null; - config.render(loadable); + // A throwing/rejecting `render` would otherwise escape as an unhandled + // rejection, leaving the spinner bouncing with no message. + return Promise.resolve() + .then(() => config.render(loadable)) + .catch((err) => { + if (swControllerChanging) return; + renderError( + container, + `Failed to render ${config.label.toLowerCase()}`, + { context: err, throw: true }, + ); + }); }); }); } @@ -222,6 +233,11 @@ export async function ensureHTML(item) { blocks: tile.blocks, }, }); + } else if (item.uri?.endsWith(".tile") || item.resources) { + // A tile that resolved no root (missing `/` resource, CID, or block bytes) + // would otherwise fall through to loading the `.tile` binary as text and + // inject garbage. + throw new Error("Tile has no root resource"); } else if (item.uri) { loaded = /** @type {T} */ ({ ...item, html: await loadURI(item.uri) }); } @@ -461,12 +477,35 @@ export function linkTileResources(container, resources, blocks) { } } +/** + * Loads the site font styles when an error is first rendered. The loader page + * doesn't include them upfront, so the woff2 is only fetched when the error + * message actually renders with the family (@font-face fonts are downloaded + * lazily, on first use). + */ +function loadErrorStyles() { + // `styles/base.css` already registers the font faces. + if ( + document.querySelector( + `link[href="styles/loader-fonts.css"], link[href="styles/base.css"]`, + ) + ) { + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "styles/loader-fonts.css"; + document.head.append(link); +} + /** * @param {HTMLElement} container * @param {string} error * @param {{ context?: Error; throw?: boolean }} [options] */ export function renderError(container, error, options) { + loadErrorStyles(); document.querySelector("#diffuse-loader")?.classList.add("loaded"); container.classList.add("has-loaded"); container.innerHTML = ` diff --git a/src/l/index.js b/src/l/index.js index b3ddf195..88f1b166 100644 --- a/src/l/index.js +++ b/src/l/index.js @@ -21,7 +21,14 @@ if (!window.isSecureContext) { } // Output element -const output = await foundation.orchestrator.output(); +// Without the orchestrator there's nothing to load, so surface the failure +// instead of leaving the spinner bouncing forever on an unhandled rejection. +const output = await foundation.orchestrator.output().catch((err) => { + renderError(container, "Failed to initialize the output orchestrator", { + context: err, + }); + throw err; +}); // Preludes const facets = await Output.data(output.facets); diff --git a/src/styles/loader-fonts.css b/src/styles/loader-fonts.css new file mode 100644 index 00000000..98dcf8b7 --- /dev/null +++ b/src/styles/loader-fonts.css @@ -0,0 +1,8 @@ +/* + * Fonts for pages that don't load `base.css` (e.g. the `/l/` loader), injected + * on demand when a loader error is rendered. Declared at this depth (not in + * `diffuse/`) because imports are inlined without rebasing URLs, so + * `font-faces.css`'s `../fonts/` only resolves from this directory. + */ +@import "./diffuse/font-faces.css" layer(diffuse.font-faces); +@import "./diffuse/fonts.css" layer(diffuse.fonts); -- 2.51.2