Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
74 linesCodeBlameRaw
1/**
2 * One tab pings. Every open tab of g1t hears the same notification over the
3 * feed, so each would play its sound and show its desktop toast: three tabs,
4 * three pings. Before pinging, a tab claims the arrival across the browser
5 * (lib/sound-events.ts): the first claim wins and the rest stay quiet. A tab
6 * the person is looking at claims at once and the others wait a beat, so the
7 * one in front is the one that pings when there is one.
8 *
9 * The claim is a Web Lock held while a short-lived mark is written to
10 * localStorage, the storage every tab of the site shares: a tab that gets the
11 * lock after the winner finds the mark. Where neither exists (an old browser,
12 * storage switched off), every tab pings as before.
13 */
14
15/** How long a background tab waits before claiming, so a tab in front wins. */
16export const BACKGROUND_WAIT_MS = 400;
17/** How long a mark is kept: long enough for every tab to have heard. */
18export const MARK_TTL_MS = 10 * 60_000;
19const PREFIX = "g1t:pinged:";
20
21/** Storage as a claim reads and writes it. */
22export type Marks = {
23 get(key: string): string | null;
24 set(key: string, value: string): void;
25 keys(): string[];
26 remove(key: string): void;
27};
28
29/**
30 * With the lock held: whether this tab is the first to claim `key`. Marks
31 * it claimed, and clears marks older than the TTL as it goes.
32 */
33export function takeMark(marks: Marks, key: string, now: number): boolean {
34 for (const name of marks.keys()) {
35 if (!name.startsWith(PREFIX)) continue;
36 const at = Number(marks.get(name));
37 if (!Number.isFinite(at) || now - at > MARK_TTL_MS) marks.remove(name);
38 }
39 const name = PREFIX + key;
40 if (marks.get(name) !== null) return false;
41 marks.set(name, String(now));
42 return true;
43}
44
45function localMarks(): Marks | null {
46 try {
47 const storage = window.localStorage;
48 // Some browsers throw only on the first write.
49 storage.setItem(`${PREFIX}probe`, "0");
50 storage.removeItem(`${PREFIX}probe`);
51 return {
52 get: (key) => storage.getItem(key),
53 set: (key, value) => storage.setItem(key, value),
54 keys: () => Array.from({ length: storage.length }, (_, at) => storage.key(at) ?? ""),
55 remove: (key) => storage.removeItem(key),
56 };
57 } catch {
58 return null;
59 }
60}
61
62/** Whether this tab should ping for `key`: true for exactly one tab of the browser. */
63export async function claimPing(key: string, inFront: boolean): Promise<boolean> {
64 if (typeof window === "undefined") return false;
65 const marks = localMarks();
66 const locks = typeof navigator !== "undefined" ? navigator.locks : undefined;
67 if (!marks || !locks) return true;
68 if (!inFront) await new Promise((resolve) => setTimeout(resolve, BACKGROUND_WAIT_MS));
69 try {
70 return await locks.request(`${PREFIX}${key}`, async () => takeMark(marks, key, Date.now()));
71 } catch {
72 return true;
73 }
74}