import { randomUUID } from "node:crypto"; import { appendFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; // Append-only archive of every posted message. The in-memory queues are // consume-once (read() drains the inbox) and vanish on restart, so this is // the only durable, full-body record of relay traffic. One JSON object per // line; never truncated. const LOG_PATH = join(dirname(fileURLToPath(import.meta.url)), "relay-log.jsonl"); export type Role = "pm" | "dev-a" | "dev-b" | "dev-c" | "dev-d" | "dev-e" | "dev-f"; export type MessageKind = "status" | "question" | "directive" | "free"; export interface RelayMessage { id: string; from: Role; to: Role; kind: MessageKind; body: string; ts: string; } const KNOWN_ROLES = new Set(["pm", "dev-a", "dev-b", "dev-c", "dev-d", "dev-e", "dev-f"]); export function isRole(s: string): s is Role { return KNOWN_ROLES.has(s); } export class RelayQueue { private readonly queues = new Map([ ["pm", []], ["dev-a", []], ["dev-b", []], ["dev-c", []], ["dev-d", []], ["dev-e", []], ["dev-f", []], ]); post(from: Role, to: Role, kind: MessageKind, body: string): RelayMessage { const msg: RelayMessage = { id: randomUUID(), from, to, kind, body, ts: new Date().toISOString(), }; this.queues.get(to)!.push(msg); try { appendFileSync(LOG_PATH, JSON.stringify(msg) + "\n"); } catch { // Logging is best-effort; never let a disk error drop a message. } return msg; } read(forRole: Role): RelayMessage[] { const inbox = this.queues.get(forRole)!; const messages = [...inbox]; inbox.length = 0; return messages; } pending(forRole: Role): { count: number; kinds: MessageKind[] } { const inbox = this.queues.get(forRole)!; return { count: inbox.length, kinds: inbox.map((m) => m.kind), }; } }