| 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. |
| 18 | import { newClientId } from './presence.mjs' |
| 19 | |
| 20 | const ID = /^[0-9a-f]{16}$/ |
| 21 | |
| 22 | /** The storage key for a deck's client id. */ |
| 23 | export 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 | */ |
| 32 | export 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 | */ |
| 50 | export 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 | |