2026-10-16 00:18:33 +03:00
|
|
|
import { FinishedGameSummary, GameRecord } from "@mancala/core";
|
|
|
|
|
import { GameHistoryRepository } from "../../src/history/GameHistoryRepository";
|
|
|
|
|
import { LiveGameStore } from "../../src/live/LiveGameStore";
|
2026-10-16 00:21:09 +03:00
|
|
|
import { Cache } from "../../src/cache/Cache";
|
2026-10-16 00:18:33 +03:00
|
|
|
|
|
|
|
|
export class InMemoryLiveGameStore implements LiveGameStore {
|
|
|
|
|
readonly games = new Map<string, unknown>();
|
|
|
|
|
readonly pending = new Map<string, GameRecord>();
|
|
|
|
|
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()];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 00:21:09 +03:00
|
|
|
// Lists leave out the moves, like the Postgres repository.
|
|
|
|
|
const toSummary = ({ moves, totalSpectators, ...summary }: GameRecord): FinishedGameSummary => summary;
|
|
|
|
|
|
2026-10-16 00:18:33 +03:00
|
|
|
export class InMemoryGameHistoryRepository implements GameHistoryRepository {
|
|
|
|
|
readonly records = new Map<string, GameRecord>();
|
|
|
|
|
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<FinishedGameSummary[]> {
|
|
|
|
|
return [...this.records.values()]
|
|
|
|
|
.filter((r) => r.player1.userId === userId || r.player2.userId === userId)
|
2026-10-16 00:21:09 +03:00
|
|
|
.sort((a, b) => b.endedAt - a.endedAt).slice(0, limit).map(toSummary);
|
2026-10-16 00:18:33 +03:00
|
|
|
}
|
|
|
|
|
async getPopular(limit: number): Promise<FinishedGameSummary[]> {
|
|
|
|
|
return [...this.records.values()]
|
2026-10-16 00:21:09 +03:00
|
|
|
.sort((a, b) => b.peakSpectators - a.peakSpectators || b.endedAt - a.endedAt).slice(0, limit).map(toSummary);
|
2026-10-16 00:18:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lets fire-and-forget promises (saving to Valkey or the database) settle.
|
|
|
|
|
export const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
|
2026-10-16 00:21:09 +03:00
|
|
|
|
|
|
|
|
export class InMemoryCache implements Cache {
|
|
|
|
|
readonly values = new Map<string, unknown>();
|
|
|
|
|
async getJson<T>(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));
|
|
|
|
|
}
|
|
|
|
|
}
|