// A component whose code is fetched the first time it is shown. // // Every static `import` reachable from app.js is downloaded before anything // renders, whether or not the visitor ever opens it: the Videos app, the video // player and the search page used to cost a member who only chats. Wrapping a // component here moves its module out of that first download and into the // moment it is needed; once loaded it is the component itself, rendered with // the same props, and every later mount is immediate. // // 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 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; if (!pending) { pending = load().then( (m) => { Loaded = m[name]; }, (e) => { pending = null; throw e; }); } 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 html`<${Loaded} ...${props} />`; if (failed) return frame(html`<${LoadFailed} onClose=${props.onClose} />`); return placeholder; } Lazy.displayName = `Lazy(${name})`; return Lazy; } export { lazy };