1// Which listening parties are live now, for the data tab (data.ts): the 2// rules of the site's one PartyDirectory Durable Object (party.ts), as 3// plain code the tests run in Node. 4// 5// A party room cannot be listed (each is its own object, named by a room id 6// only its people know), so every room tells the directory when its people 7// or its song change (party-room.ts's `changed`), and the directory keeps 8// one entry per live room: its song, how many are in it, whether the host 9// is, and since when. Keyed by the room's Durable Object id, a hash of the 10// room's name that cannot be turned back into it, so the directory never 11// holds the join link: a party is counted and described. A room whose host 12// chose to be seen (the lobby's "show me 13// here", website/src/jev/party.mjs) is `listed`: its entry then carries its 14// object id, which /jev/parties/<id> (party.ts) turns into the room to join 15// by asking the room itself, which answers only while it is listed. An 16// unlisted party is counted and described, never joinable from here. 17// An empty room removes its entry; one that stops reporting (a 18// room lost in a crash) is dropped once its last report is STALE_MS old. 19// Live coordination only, like the rooms: nothing here outlives a party. 20 21export type Live = { people: number; host: boolean; song: string | null; listed?: boolean }; 22export type Entry = Live & { since: number; seen: number }; 23 24export const STALE_MS = 6 * 60 * 60 * 1000; 25export const MAX_ENTRIES = 1000; 26const KEY = /^[0-9a-f]{64}$/; 27 28// 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} 35 36// 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} 55 56// 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}