From 34576a1ccda1ee192e8efbc653cb436111a9c5e8 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 28 Sep 2026 12:29:20 +0200 Subject: fix(hub): say so when a lazily loaded view cannot be fetched A tab opened before a hub deploy got 404 for every module it had not loaded yet, and lazy.js kept its spinner for good. It now shows a notice with a Reload button and logs the failure. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-hub/src/meshbay_hub/static/lazy.js | 40 +++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/lazy.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/lazy.js b/packages/meshbay-hub/src/meshbay_hub/static/lazy.js index 30a564c..fda73c0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/lazy.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/lazy.js @@ -9,17 +9,45 @@ // // A failed fetch (a network drop on a phone) is not remembered: the next time // the component is shown it asks again, rather than failing for the session. +// +// A failed fetch is also *said*, never left as the placeholder's spinner. The +// commonest failure is not the network: the hub serves the module graph under +// `/a//` for the current hash only, so a tab opened before a hub deploy +// asks for every module it has not loaded yet under a prefix that now answers +// 404. Such a tab kept Files and Chat (already loaded) and spun for ever on +// Search, Videos, Music, Photos and the player — with nothing in the console, +// because the rejection was swallowed here. Asking again cannot help that tab; +// reloading the page is the one thing that does, so that is what is offered. +// Not done automatically: a reload that fails the same way would loop, and it +// would throw away whatever the reader had in progress. import { html, useState, useEffect } from './vendor/htm-preact.js'; +import { t } from './i18n.js'; const SPINNER = html`

`; -function lazy(load, name, placeholder = SPINNER) { +function LoadFailed({ onClose }) { + return html` + + `; +} + +// `frame` wraps the failure notice, so a component that is an overlay (the +// video player) can put it where the overlay would have been rather than +// somewhere at the bottom of the page, under nothing anyone is looking at. +function lazy(load, name, placeholder = SPINNER, frame = (content) => content) { let Loaded = null; let pending = null; function Lazy(props) { const [, setReady] = useState(Loaded !== null); + const [failed, setFailed] = useState(false); useEffect(() => { if (Loaded) return undefined; let alive = true; @@ -28,11 +56,15 @@ function lazy(load, name, placeholder = SPINNER) { (m) => { Loaded = m[name]; }, (e) => { pending = null; throw e; }); } - pending.then(() => { if (alive) setReady(true); }, () => {}); + pending.then(() => { if (alive) setReady(true); }, (e) => { + console.error(`[MeshBay] could not load ${name}:`, e); + if (alive) setFailed(true); + }); return () => { alive = false; }; }, []); - if (!Loaded) return placeholder; - return html`<${Loaded} ...${props} />`; + if (Loaded) return html`<${Loaded} ...${props} />`; + if (failed) return frame(html`<${LoadFailed} onClose=${props.onClose} />`); + return placeholder; } Lazy.displayName = `Lazy(${name})`; return Lazy; -- cgit v1.2.3