anvilsign in

collin/mahjong

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 */
20export 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}