| 1 | /** |
| 2 | * Keep the room alive while this tab is hidden. |
| 3 | * |
| 4 | * The antics SDK runs its loops — the state flush above all — off |
| 5 | * requestAnimationFrame, which a browser suspends the moment a tab stops being |
| 6 | * visible. For most games that is right: a hidden player is not playing. The |
| 7 | * party table is different — the common screen is the room's host, and a host |
| 8 | * that stops flushing takes the whole room with it: a phone's seat claim is |
| 9 | * processed but never announced, so the phone sits on "pick a seat" forever |
| 10 | * while the answer waits in a queue for the laptop to be looked at. |
| 11 | * |
| 12 | * The SDK provides for exactly this: anything on `globalThis.__anticsClock__` |
| 13 | * replaces its own timers, resolved when each loop starts. So the ticks come |
| 14 | * from a Web Worker instead — worker timers are not throttled in background |
| 15 | * tabs — and the host keeps hosting whether or not its tab is front. |
| 16 | * |
| 17 | * Installed before joinRoom, once. The game's own render loops are untouched: |
| 18 | * they use the real requestAnimationFrame and are happy to sleep with the tab. |
| 19 | */ |
| 20 | export function installBackgroundClock() { |
| 21 | const g = globalThis as unknown as Record<string, unknown>; |
| 22 | if (g.__anticsClock__) return; |
| 23 | |
| 24 | let worker: Worker | null = null; |
| 25 | try { |
| 26 | const src = URL.createObjectURL( |
| 27 | new Blob(['setInterval(() => postMessage(0), 66);'], { type: 'text/javascript' }), |
| 28 | ); |
| 29 | worker = new Worker(src); |
| 30 | URL.revokeObjectURL(src); |
| 31 | } catch { |
| 32 | // A sandbox that refuses workers keeps the SDK's own timers — no worse |
| 33 | // than before. |
| 34 | return; |
| 35 | } |
| 36 | |
| 37 | const callbacks = new Map<number, (t: number) => void>(); |
| 38 | let nextId = 1; |
| 39 | worker.onmessage = () => { |
| 40 | if (callbacks.size === 0) return; |
| 41 | const now = performance.now(); |
| 42 | const due = [...callbacks.values()]; |
| 43 | callbacks.clear(); |
| 44 | for (const cb of due) cb(now); |
| 45 | }; |
| 46 | |
| 47 | g.__anticsClock__ = { |
| 48 | now: () => Date.now(), |
| 49 | requestAnimationFrame(cb: (t: number) => void): number { |
| 50 | const id = nextId++; |
| 51 | callbacks.set(id, cb); |
| 52 | return id; |
| 53 | }, |
| 54 | cancelAnimationFrame(id: number) { |
| 55 | callbacks.delete(id); |
| 56 | }, |
| 57 | }; |
| 58 | } |