lmjtfy.git / apps / lmjtfy / src / live.js
live.jsannotatedlive.jssource59 lines · 2.3 KB · raw

/live.js: one /live socket for every tab of this browser, as a SharedWorker. The browser runs one per script address, so every tab of the same build shares it, and it lives as long as any of them is open; a tab the browser freezes in the background does not take the socket with it, as it would if a tab held it. The archive therefore counts browsers, not tabs.

What the archive sends is passed to every tab as it came. A tab that joins is first given the latest of what a page needs from the socket and cannot have rendered itself: the build, how many are online, and where they are. Toasts and feed updates are news; a page that loads has the feeds already.

12const build = new URLSearchParams(location.search).get("build") ?? "";
13const tabs = new Set();

The latest message for each piece of state, by what it is.

15const state = new Map();
16let socket = null;
17let wait = 1000;
19function tell(text) {
20  for (const tab of tabs) tab.postMessage(text);
21}

Kept so the newest is replayed last.

24function keep(what, text) {
25  state.delete(what);
26  state.set(what, text);
27}
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};