- Valkey client that fails commands at once while Valkey is down instead of queueing them, and logs connection errors once - ValkeyLiveGameStore: live game snapshots and finished games waiting to be saved, loaded with SCAN on startup - ValkeyCache: JSON values with a TTL; errors count as cache misses - Integration tests against a real Valkey, including an unreachable one
62 lines
2.2 KiB
TypeScript
62 lines
2.2 KiB
TypeScript
import { GameRecord } from "@mancala/core";
|
|
import { Valkey } from "iovalkey";
|
|
import { scanKeys } from "./valkey";
|
|
|
|
const LIVE_PREFIX = "mancala:live:";
|
|
const PENDING_SAVE_PREFIX = "mancala:pending-save:";
|
|
|
|
// Durable copy of state that otherwise only lives in memory: live games, so they survive a restart, and finished
|
|
// games that could not be saved to the database yet. Methods reject when Valkey is unreachable.
|
|
export interface LiveGameStore {
|
|
saveGame(gameId: string, snapshot: unknown): Promise<void>;
|
|
removeGame(gameId: string): Promise<void>;
|
|
loadGames(): Promise<unknown[]>;
|
|
savePendingRecord(record: GameRecord): Promise<void>;
|
|
removePendingRecord(gameId: string): Promise<void>;
|
|
loadPendingRecords(): Promise<GameRecord[]>;
|
|
}
|
|
|
|
export class ValkeyLiveGameStore implements LiveGameStore {
|
|
constructor(private readonly client: Valkey) { }
|
|
|
|
async saveGame(gameId: string, snapshot: unknown): Promise<void> {
|
|
await this.client.set(LIVE_PREFIX + gameId, JSON.stringify(snapshot));
|
|
}
|
|
|
|
async removeGame(gameId: string): Promise<void> {
|
|
await this.client.del(LIVE_PREFIX + gameId);
|
|
}
|
|
|
|
loadGames(): Promise<unknown[]> {
|
|
return this.loadAll(LIVE_PREFIX);
|
|
}
|
|
|
|
async savePendingRecord(record: GameRecord): Promise<void> {
|
|
await this.client.set(PENDING_SAVE_PREFIX + record.id, JSON.stringify(record));
|
|
}
|
|
|
|
async removePendingRecord(gameId: string): Promise<void> {
|
|
await this.client.del(PENDING_SAVE_PREFIX + gameId);
|
|
}
|
|
|
|
loadPendingRecords(): Promise<GameRecord[]> {
|
|
return this.loadAll(PENDING_SAVE_PREFIX) as Promise<GameRecord[]>;
|
|
}
|
|
|
|
private async loadAll(prefix: string): Promise<unknown[]> {
|
|
const keys = await scanKeys(this.client, prefix + "*");
|
|
if (keys.length === 0) return [];
|
|
const values = await this.client.mget(keys);
|
|
const parsed: unknown[] = [];
|
|
values.forEach((value, i) => {
|
|
if (value === null) return;
|
|
try {
|
|
parsed.push(JSON.parse(value));
|
|
} catch {
|
|
console.error(`(Valkey) Ignoring unreadable value in ${keys[i]}`);
|
|
}
|
|
});
|
|
return parsed;
|
|
}
|
|
}
|