jevstrudel.git / worker / src / party-directory.ts

Which listening parties are live now, for the data tab (data.ts): the rules of the site's one PartyDirectory Durable Object (party.ts), as plain code the tests run in Node.

A party room cannot be listed (each is its own object, named by a room id only its people know), so every room tells the directory when its people or its song change (party-room.ts's changed), and the directory keeps one entry per live room: its song, how many are in it, whether the host is, and since when. Keyed by the room's Durable Object id, a hash of the room's name that cannot be turned back into it, so the directory never holds the join link: a party is counted and described. A room whose host chose to be seen (the lobby's "show me here", website/src/jev/party.mjs) is listed: its entry then carries its object id, which /jev/parties/<id> (party.ts) turns into the room to join by asking the room itself, which answers only while it is listed. An unlisted party is counted and described, never joinable from here. An empty room removes its entry; one that stops reporting (a room lost in a crash) is dropped once its last report is STALE_MS old. Live coordination only, like the rooms: nothing here outlives a party.

21export type Live = { people: number; host: boolean; song: string | null; listed?: boolean };
22export type Entry = Live & { since: number; seen: number };
24export const STALE_MS = 6 * 60 * 60 * 1000;
25export const MAX_ENTRIES = 1000;
26const KEY = /^[0-9a-f]{64}$/;

The part of a Durable Object's storage the directory uses.

29export interface Storage {
30  get<T>(key: string): Promise<T | undefined>;
31  put(key: string, value: unknown): Promise<void>;
32  delete(key: string): Promise<boolean>;
33  list<T>(options: { prefix: string; limit?: number }): Promise<Map<string, T>>;
34}

A room's report: its entry kept or updated, or removed when empty.

37export async function note(storage: Storage, room: string, live: Live | null, now: number): Promise<void> {
38  if (!KEY.test(room)) return;
39  const key = `p:${room}`;
40  if (!live || live.people < 1) {
41    await storage.delete(key);
42    return;
43  }
44  const before = await storage.get<Entry>(key);
45  const entry: Entry = {
46    people: Math.min(Math.max(Math.floor(live.people), 0), 1000),
47    host: Boolean(live.host),
48    song: typeof live.song === 'string' ? live.song.slice(0, 129) : null,
49    listed: live.listed === true,
50    since: before?.since ?? now,
51    seen: now,
52  };
53  await storage.put(key, entry);
54}

Every live party, the longest-running first; stale entries are removed.

57export type Listed = { people: number; host: boolean; song: string | null; since: number; listed: boolean; id?: string };
58export async function live(storage: Storage, now: number): Promise<Listed[]> {
59  const entries = await storage.list<Entry>({ prefix: 'p:', limit: MAX_ENTRIES });
60  const out: Listed[] = [];
61  for (const [key, e] of entries) {
62    if (now - e.seen > STALE_MS) {
63      await storage.delete(key);
64      continue;
65    }
66    const listed = e.listed === true;
67    out.push({ people: e.people, host: e.host, song: e.song, since: e.since, listed, ...(listed ? { id: key.slice(2) } : {}) });
68  }
69  return out.sort((a, b) => a.since - b.since);
70}