oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1// fixtures/client-id.mjs — this tab's presence client id, kept across
2// a reload.
3//
4// A fresh id per page load made a reload look like somebody arriving:
5// the server holds the old id for its 45 s TTL (live/hub.go), the new
6// page has another, and toMap (presence.mjs) only leaves out the id it
7// has — so the tab drew its own previous self as a colleague on the
8// strip and over its selected block (polish audit, slides-52).
9//
10// sessionStorage is per tab and survives a reload, so the id goes
11// there, one key per deck. Two tabs still get two ids — with one
12// exception: a DUPLICATED tab starts with a copy of its original's
13// sessionStorage, id included. claimClientId catches that over a
14// BroadcastChannel: a tab announces the id it means to use, and a live
15// tab already holding it answers; the newcomer then takes a fresh one.
16// A reloaded page has no such rival — the page it replaced is gone —
17// so nothing answers and the id is kept.
18import { newClientId } from './presence.mjs'
19
20const ID = /^[0-9a-f]{16}$/
21
22/** The storage key for a deck's client id. */
23export function clientIdKey (deck) {
24 return `slides.presence.client.${deck}`
25}
26
27/**
28 * The id this tab uses for deck: the one stored for it this session,
29 * or a new one, stored. Storage that throws (site data blocked) or is
30 * absent means a fresh id every load, which is the old behaviour.
31 */
32export function tabClientId ({ storage, deck, make = newClientId }) {
33 const key = clientIdKey(deck)
34 try {
35 const stored = storage && storage.getItem(key)
36 if (typeof stored === 'string' && ID.test(stored)) return stored
37 } catch { /* no storage: a fresh id */ }
38 const id = make()
39 try { if (storage) storage.setItem(key, id) } catch { /* per-load then */ }
40 return id
41}
42
43/**
44 * Guards the id against a duplicated tab. channel is a BroadcastChannel
45 * (or anything with postMessage and onmessage); getId() is this tab's
46 * id right now; onTaken() is called when another live tab already has
47 * it, and should switch to a fresh one. Answers other tabs' claims for
48 * as long as the page lives. Returns {claim, close}.
49 */
50export function claimClientId ({ channel, getId, onTaken }) {
51 if (!channel) return { claim () {}, close () {} }
52 channel.onmessage = ev => {
53 const m = ev && ev.data
54 if (!m || typeof m.id !== 'string' || m.id !== getId()) return
55 if (m.type === 'claim') channel.postMessage({ type: 'taken', id: m.id })
56 else if (m.type === 'taken') onTaken()
57 }
58 return {
59 claim () { channel.postMessage({ type: 'claim', id: getId() }) },
60 close () { channel.onmessage = null; try { channel.close() } catch { /* already */ } }
61 }
62}
63