1// `/live.js`: one `/live` socket for every tab of this browser, as a 2// SharedWorker. The browser runs one per script address, so every tab of the 3// same build shares it, and it lives as long as any of them is open; a tab the 4// browser freezes in the background does not take the socket with it, as it 5// would if a tab held it. The archive therefore counts browsers, not tabs. 6// 7// What the archive sends is passed to every tab as it came. A tab that joins 8// is first given the latest of what a page needs from the socket and cannot 9// have rendered itself: the build, how many are online, and where they are. 10// Toasts and feed updates are news; a page that loads has the feeds already. 11 12const build = new URLSearchParams(location.search).get("build") ?? ""; 13const tabs = new Set(); 14// The latest message for each piece of state, by what it is. 15const state = new Map(); 16let socket = null; 17let wait = 1000; 18 19function tell(text) { 20 for (const tab of tabs) tab.postMessage(text); 21} 22 23// Kept so the newest is replayed last. 24function keep(what, text) { 25 state.delete(what); 26 state.set(what, text); 27} 28 29function connect() { 30 const scheme = location.protocol === "https:" ? "wss" : "ws"; 31 socket = new WebSocket(`${scheme}://${location.host}/live?build=${encodeURIComponent(build)}`); 32 socket.addEventListener("open", () => { wait = 1000; }); 33 socket.addEventListener("message", (event) => { 34 let live; 35 try { live = JSON.parse(event.data); } catch { return; } 36 if ("build" in live) keep("build", event.data); 37 if ("online" in live) keep("online", event.data); 38 if (typeof live.patch === "string" && live.patch.startsWith('<ul class="places"')) keep("places", event.data); 39 tell(event.data); 40 }); 41 socket.addEventListener("close", () => { 42 state.delete("online"); 43 state.delete("places"); 44 tell(JSON.stringify({ closed: true })); 45 setTimeout(connect, wait); 46 wait = Math.min(wait * 2, 60000); 47 }); 48} 49 50onconnect = (event) => { 51 const tab = event.ports[0]; 52 tabs.add(tab); 53 // A tab says goodbye when it is closed or put away; one that comes back 54 // from the back-forward cache connects again. 55 tab.addEventListener("message", (message) => { if (message.data === "bye") tabs.delete(tab); }); 56 tab.start(); 57 for (const text of state.values()) tab.postMessage(text); 58 if (!socket) connect(); 59};