// The page stays current without a reload. /events is a stream of what // changed; each event becomes `live` and `live:` on `document`, and // any element with data-live="" is refetched and swapped when one of // its types arrives — server-rendered, so the page never renders twice. // Without this file the page is the same page, one reload behind. let since = ""; let tries = 0; let source; function connect() { source = new EventSource(since ? `/events?since=${since}` : "/events"); source.onopen = () => { if (tries > 0) refresh(); // Reconnected: whatever was missed, catch up. tries = 0; }; source.onmessage = (message) => { since = message.lastEventId; const { type, data } = JSON.parse(message.data); document.dispatchEvent(new CustomEvent("live", { detail: { type, data } })); document.dispatchEvent(new CustomEvent(`live:${type}`, { detail: data })); }; // The browser retries a dropped connection itself; a refused one it gives // up on, and then we try again, later each time. source.onerror = () => { if (source.readyState !== EventSource.CLOSED) return; source.close(); setTimeout(connect, Math.min(30_000, 1000 * 2 ** tries++)); }; } let refreshing = false; let again = false; async function refresh(types) { const regions = [...document.querySelectorAll("[data-live]")].filter( (el) => types === undefined || el.dataset.live.split(" ").some((t) => types.includes(t)), ); if (regions.length === 0) return; if (refreshing) { again = true; return; } refreshing = true; try { const response = await fetch(location.href, { headers: { accept: "text/html" }, }); const fresh = new DOMParser().parseFromString( await response.text(), "text/html", ); for (const el of regions) { const next = fresh.getElementById(el.id); if (next) el.replaceWith(next); } document.dispatchEvent(new CustomEvent("live:swapped")); } finally { refreshing = false; if (again) { again = false; refresh(); } } } document.addEventListener("live", (event) => refresh([event.detail.type])); connect();