import { FinishedGameSummary, GameRecord } from "@mancala/core"; import { GameHistoryRepository } from "../../src/history/GameHistoryRepository"; import { LiveGameStore } from "../../src/live/LiveGameStore"; import { Cache } from "../../src/cache/Cache"; export class InMemoryLiveGameStore implements LiveGameStore { readonly games = new Map(); readonly pending = new Map(); failing = false; private check() { if (this.failing) return Promise.reject(new Error("Valkey is down")); return Promise.resolve(); } async saveGame(gameId: string, snapshot: unknown) { await this.check(); this.games.set(gameId, JSON.parse(JSON.stringify(snapshot))); } async removeGame(gameId: string) { await this.check(); this.games.delete(gameId); } async loadGames() { await this.check(); return [...this.games.values()]; } async savePendingRecord(record: GameRecord) { await this.check(); this.pending.set(record.id, record); } async removePendingRecord(gameId: string) { await this.check(); this.pending.delete(gameId); } async loadPendingRecords() { await this.check(); return [...this.pending.values()]; } } // Lists leave out the moves, like the Postgres repository. const toSummary = ({ moves, totalSpectators, ...summary }: GameRecord): FinishedGameSummary => summary; export class InMemoryGameHistoryRepository implements GameHistoryRepository { readonly records = new Map(); failing = false; async save(record: GameRecord) { if (this.failing) throw new Error("database is down"); if (!this.records.has(record.id)) this.records.set(record.id, record); } async get(id: string) { if (this.failing) throw new Error("database is down"); return this.records.get(id); } async getRecentForUser(userId: string, limit: number): Promise { return [...this.records.values()] .filter((r) => r.player1.userId === userId || r.player2.userId === userId) .sort((a, b) => b.endedAt - a.endedAt).slice(0, limit).map(toSummary); } async getPopular(limit: number): Promise { return [...this.records.values()] .sort((a, b) => b.peakSpectators - a.peakSpectators || b.endedAt - a.endedAt).slice(0, limit).map(toSummary); } } // Lets fire-and-forget promises (saving to Valkey or the database) settle. export const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); export class InMemoryCache implements Cache { readonly values = new Map(); async getJson(key: string) { return this.values.get(key) as T | undefined; } async setJson(key: string, value: unknown) { this.values.set(key, JSON.parse(JSON.stringify(value))); } async delete(...keys: string[]) { keys.forEach((key) => this.values.delete(key)); } }