// fixtures/client-id.mjs — this tab's presence client id, kept across // a reload. // // A fresh id per page load made a reload look like somebody arriving: // the server holds the old id for its 45 s TTL (live/hub.go), the new // page has another, and toMap (presence.mjs) only leaves out the id it // has — so the tab drew its own previous self as a colleague on the // strip and over its selected block (polish audit, slides-52). // // sessionStorage is per tab and survives a reload, so the id goes // there, one key per deck. Two tabs still get two ids — with one // exception: a DUPLICATED tab starts with a copy of its original's // sessionStorage, id included. claimClientId catches that over a // BroadcastChannel: a tab announces the id it means to use, and a live // tab already holding it answers; the newcomer then takes a fresh one. // A reloaded page has no such rival — the page it replaced is gone — // so nothing answers and the id is kept. import { newClientId } from './presence.mjs' const ID = /^[0-9a-f]{16}$/ /** The storage key for a deck's client id. */ export function clientIdKey (deck) { return `slides.presence.client.${deck}` } /** * The id this tab uses for deck: the one stored for it this session, * or a new one, stored. Storage that throws (site data blocked) or is * absent means a fresh id every load, which is the old behaviour. */ export function tabClientId ({ storage, deck, make = newClientId }) { const key = clientIdKey(deck) try { const stored = storage && storage.getItem(key) if (typeof stored === 'string' && ID.test(stored)) return stored } catch { /* no storage: a fresh id */ } const id = make() try { if (storage) storage.setItem(key, id) } catch { /* per-load then */ } return id } /** * Guards the id against a duplicated tab. channel is a BroadcastChannel * (or anything with postMessage and onmessage); getId() is this tab's * id right now; onTaken() is called when another live tab already has * it, and should switch to a fresh one. Answers other tabs' claims for * as long as the page lives. Returns {claim, close}. */ export function claimClientId ({ channel, getId, onTaken }) { if (!channel) return { claim () {}, close () {} } channel.onmessage = ev => { const m = ev && ev.data if (!m || typeof m.id !== 'string' || m.id !== getId()) return if (m.type === 'claim') channel.postMessage({ type: 'taken', id: m.id }) else if (m.type === 'taken') onTaken() } return { claim () { channel.postMessage({ type: 'claim', id: getId() }) }, close () { channel.onmessage = null; try { channel.close() } catch { /* already */ } } } }