1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
// 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/<hash>/` 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`<p class="page-message"><span class="spinner"></span></p>`;
function LoadFailed({ onClose }) {
return html`
<div class="page-message lazy-failed" role="alert">
<p>${t('lazy.load_failed')}</p>
<button class="admin-btn" onClick=${() => location.reload()}>${t('lazy.reload')}</button>
${onClose && html`
${' '}<button class="admin-btn" onClick=${onClose}>${t('admin.btn_close')}</button>
`}
</div>
`;
}
// `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 };
|