oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// client-id.mjs: a tab keeps its presence id across a reload, two tabs
2// do not share one, and a duplicated tab (which copies its original's
3// sessionStorage) gives the copied id up.
4import test from 'node:test'
5import assert from 'node:assert/strict'
6import { tabClientId, claimClientId, clientIdKey } from './client-id.mjs'
7import { createPresence } from './presence.mjs'
8
9function memoryStorage () {
10 const m = new Map()
11 return {
12 getItem: k => (m.has(k) ? m.get(k) : null),
13 setItem: (k, v) => { m.set(k, String(v)) },
14 copy () { const c = memoryStorage(); for (const [k, v] of m) c.setItem(k, v); return c }
15 }
16}
17
18let n = 0
19const make = () => (++n).toString(16).padStart(16, '0')
20
21test('a reload reuses the id this tab stored for the deck', () => {
22 const tab = memoryStorage()
23 const first = tabClientId({ storage: tab, deck: 'd1', make })
24 const reloaded = tabClientId({ storage: tab, deck: 'd1', make })
25 assert.equal(reloaded, first)
26 assert.equal(tab.getItem(clientIdKey('d1')), first)
27})
28
29test('two tabs, and two decks in one tab, get different ids', () => {
30 const a = tabClientId({ storage: memoryStorage(), deck: 'd1', make })
31 const b = tabClientId({ storage: memoryStorage(), deck: 'd1', make })
32 assert.notEqual(a, b)
33 const tab = memoryStorage()
34 assert.notEqual(tabClientId({ storage: tab, deck: 'd1', make }), tabClientId({ storage: tab, deck: 'd2', make }))
35})
36
37test('a stored value that is not an id, or storage that throws, gives a fresh id', () => {
38 const tab = memoryStorage()
39 tab.setItem(clientIdKey('d1'), '<script>')
40 const id = tabClientId({ storage: tab, deck: 'd1', make })
41 assert.match(id, /^[0-9a-f]{16}$/)
42 const throwing = { getItem () { throw new Error('blocked') }, setItem () { throw new Error('blocked') } }
43 assert.match(tabClientId({ storage: throwing, deck: 'd1', make }), /^[0-9a-f]{16}$/)
44 assert.match(tabClientId({ storage: null, deck: 'd1', make }), /^[0-9a-f]{16}$/)
45})
46
47// A pair of in-memory BroadcastChannels: a message posted on one is
48// delivered to the other, never to itself (as the real one does).
49function channelPair () {
50 const a = { onmessage: null, postMessage: m => queueMicrotask(() => b.onmessage && b.onmessage({ data: m })), close () {} }
51 const b = { onmessage: null, postMessage: m => queueMicrotask(() => a.onmessage && a.onmessage({ data: m })), close () {} }
52 return [a, b]
53}
54
55test('a duplicated tab gives up the copied id; the original keeps it', async () => {
56 const original = memoryStorage()
57 const id = tabClientId({ storage: original, deck: 'd1', make })
58 const duplicate = original.copy()
59 let dupId = tabClientId({ storage: duplicate, deck: 'd1', make })
60 assert.equal(dupId, id, 'the duplicate starts with the copy — the case being guarded')
61
62 const [chA, chB] = channelPair()
63 let originalTaken = false
64 claimClientId({ channel: chA, getId: () => id, onTaken: () => { originalTaken = true } })
65 const guard = claimClientId({
66 channel: chB,
67 getId: () => dupId,
68 onTaken: () => {
69 duplicate.setItem(clientIdKey('d1'), '')
70 dupId = tabClientId({ storage: duplicate, deck: 'd1', make })
71 }
72 })
73 guard.claim()
74 await new Promise(r => setTimeout(r, 0))
75 assert.notEqual(dupId, id)
76 assert.equal(originalTaken, false)
77 assert.equal(original.getItem(clientIdKey('d1')), id)
78 assert.equal(duplicate.getItem(clientIdKey('d1')), dupId)
79})
80
81test('a reload finds no rival and keeps its id', async () => {
82 const tab = memoryStorage()
83 const id = tabClientId({ storage: tab, deck: 'd1', make })
84 const [ch] = channelPair() // nobody on the other end: the old page is gone
85 let taken = false
86 claimClientId({ channel: ch, getId: () => id, onTaken: () => { taken = true } }).claim()
87 await new Promise(r => setTimeout(r, 0))
88 assert.equal(taken, false)
89 assert.equal(tabClientId({ storage: tab, deck: 'd1', make }), id)
90})
91
92test('presence switches id: beats carry the new one and the old one is somebody else', () => {
93 const posts = []
94 const p = createPresence({
95 clientId: 'aaaaaaaaaaaaaaaa',
96 post: b => { posts.push(b); return Promise.resolve() },
97 getState: () => ({ slide: 's', block: '' }),
98 setTimeoutFn: () => 1,
99 clearTimeoutFn: () => {}
100 })
101 p.start()
102 p.setClientId('bbbbbbbbbbbbbbbb')
103 assert.equal(p.clientId, 'bbbbbbbbbbbbbbbb')
104 assert.equal(posts.at(-1).client, 'bbbbbbbbbbbbbbbb')
105 const d = p.applySet([{ client: 'aaaaaaaaaaaaaaaa', name: 'x' }, { client: 'bbbbbbbbbbbbbbbb', name: 'me' }])
106 assert.deepEqual(d.people.map(x => x.client), ['aaaaaaaaaaaaaaaa'])
107})
108