// In-memory adapters: used by the tests and by the replays on the page. import { HIRING_COLUMNS, DECISION_COLUMNS, type Clock, type Grid, type HiringSource, type Journal, type JournalEvent, type MailSender, type OutgoingLetter, type PassLock } from "./types"; export const HIRING_HEADER = Object.values(HIRING_COLUMNS); export const DECISION_HEADER = Object.values(DECISION_COLUMNS); export class MemorySheet implements HiringSource { hiring: Grid; decisions: Grid; unreadable = false; constructor(rows: string[][] = [], decisions: string[][] = []) { this.hiring = [[...HIRING_HEADER], ...rows.map((r) => [...r])]; this.decisions = [[...DECISION_HEADER], ...decisions.map((r) => [...r])]; } readHiring(): Grid { if (this.unreadable) throw new Error("access denied"); return this.hiring.map((r) => [...r]); } readDecisions(): Grid { return this.decisions.map((r) => [...r]); } writeArrivalId(rowNumber: number, expected: { therapist: string; welcomeEmail: string }, id: string): boolean { const h = this.hiring[0]; const col = (name: string) => h.findIndex((x) => x.trim().toLowerCase() === name.toLowerCase()); const r = this.hiring[rowNumber - 1]; if (!r) return false; if (r[col(HIRING_COLUMNS.therapist)]?.trim() !== expected.therapist) return false; if ((r[col(HIRING_COLUMNS.welcomeEmail)] ?? "").trim() !== expected.welcomeEmail) return false; if ((r[col(HIRING_COLUMNS.arrivalId)] ?? "").trim()) return false; r[col(HIRING_COLUMNS.arrivalId)] = id; return true; } /** Helpers for scenarios: address a cell by header name. */ set(rowNumber: number, column: string, value: string) { const c = this.hiring[0].findIndex((x) => x === column); this.hiring[rowNumber - 1][c] = value; } get(rowNumber: number, column: string): string { const c = this.hiring[0].findIndex((x) => x === column); return this.hiring[rowNumber - 1][c]; } addRow(values: Partial>): number { const ordered = this.hiring[0].map((name) => { const k = Object.entries(HIRING_COLUMNS).find(([, v]) => v === name)?.[0] as keyof typeof HIRING_COLUMNS | undefined; return k ? (values[k] ?? "") : ""; }); this.hiring.push(ordered); return this.hiring.length; } copyRow(rowNumber: number): number { this.hiring.push([...this.hiring[rowNumber - 1]]); return this.hiring.length; } sortBy(column: string) { const c = this.hiring[0].findIndex((x) => x === column); const [head, ...rest] = this.hiring; rest.sort((a, b) => a[c].localeCompare(b[c])); this.hiring = [head, ...rest]; } renameColumn(from: string, to: string) { const c = this.hiring[0].findIndex((x) => x === from); this.hiring[0][c] = to; } addDecision(arrivalId: string, decision: string, by: string) { this.decisions.push([arrivalId, decision, by]); } } export class MemoryMail implements MailSender { outbox: (OutgoingLetter & { at?: string })[] = []; quota = 100; failWith: string | null = null; afterSend: (() => void) | null = null; constructor(private clock?: Clock) {} send(letter: OutgoingLetter): void { if (this.failWith) throw new Error(this.failWith); this.outbox.push({ ...letter, at: this.clock?.now().toISOString() }); this.quota--; this.afterSend?.(); } remainingQuota(): number { return this.quota; } } export class MemoryJournal implements Journal { events: JournalEvent[] = []; onRead: (() => void) | null = null; /** Makes every append throw, for "the execution died after the send". */ broken = false; read(): JournalEvent[] { const hook = this.onRead; this.onRead = null; const copy = this.events.map((e) => ({ ...e })); hook?.(); return copy; } append(e: JournalEvent): void { if (this.broken) throw new Error("Execution stopped: the journal write never happened."); this.events.push({ ...e }); } } export class ManualClock implements Clock { constructor(private t: Date) {} now() { return new Date(this.t); } set(iso: string) { this.t = new Date(iso); } advance(minutes: number) { this.t = new Date(this.t.getTime() + minutes * 60000); } } export class MemoryLock implements PassLock { held = false; tryAcquire() { if (this.held) return false; this.held = true; return true; } release() { this.held = false; } }