3.7 KiB
GamePersistence
(no submodules)
Purpose
GamePersistence persists game state to a pluggable key-value backend via
the StorageLike structural contract from @suaveplan/types. State, named
snapshots, and a bounded audit-trail history are serialised together as a
UTF-8 JSON document under a deterministic key (game-server:save:<gameId>:<id>),
allowing any conformant AbstractStorage adapter — in-memory, SurrealDB,
SQLite, S3 — to back the engine without code changes.
The manager exposes async save, load, delete, createSnapshot, and
restoreSnapshot operations that throw stable GAME_SERVER.* errors,
plus an OTel save-duration histogram and counter pair so production
throughput is observable without bespoke instrumentation.
Features
- Pluggable
StorageLikebackend (in-memory, durable, network). - Named snapshots with rotating
maxSnapshotscap. - Bounded audit-trail history with
maxHistoryEntriescap. - Injectable
ClockandIdGeneratorfor deterministic tests. - Stable error codes:
PERSISTENCE_SAVE_FAILED,PERSISTENCE_LOAD_FAILED,PERSISTENCE_NOT_FOUND,INVALID_SAVE_RECORD. - OTel counters + duration histogram.
- Tracks
currentSaveIdfor snapshot context.
Usage: Basic
import { GamePersistence } from "@suaveplan/game-server/enhanced/persistence/game-persistence";
const gp = new GamePersistence<MyState>(
{ gameId: "chess" },
{ storage: createSurrealStorage() }
);
const result = await gp.save(state, { name: "quicksave" });
const loaded = await gp.load(result.id!);
Usage: Advanced
import { GamePersistence } from "@suaveplan/game-server/enhanced/persistence/game-persistence";
const gp = new GamePersistence<MyState>(
{ gameId: "chess", maxSnapshots: 50, maxHistoryEntries: 200 },
{
storage,
clock: () => now,
idGenerator: () => `save-${++counter}`,
meter,
}
);
const save = await gp.save(state, { name: "checkpoint", tags: ["wave-4"] });
const snap = await gp.createSnapshot(state, "before-boss", "auto");
const restored = await gp.restoreSnapshot(save.id!, snap.id);
const history = await gp.getHistory(save.id!);
const stats = gp.getStats();
API Reference
new GamePersistence(config, deps)—deps.storage: StorageLike.save(state, input): Promise<SaveResult>load(id): Promise<LoadResult>delete(id): Promise<boolean>exists(id): Promise<boolean>createSnapshot(state, name, reason?): Promise<Snapshot>restoreSnapshot(saveId, snapshotId): Promise<LoadResult>getHistory(saveId): Promise<readonly HistoryEntry[]>getStats(): PersistenceStatsgetCurrentSaveId(): string | null
Implementation Notes
State is encoded as UTF-8 JSON via TextEncoder/TextDecoder so the
on-disk shape is debuggable with any text viewer; binary-only stores will
treat the payload as opaque bytes. For game states with large embedded
binaries (images, audio), callers should chunk those out to a separate
asset store and reference them by id in the persisted state to avoid
inflating the JSON payload.
Storage keys are namespaced by both gameId and the per-save id so a
single shared backend can host saves for many concurrent game instances
without prefix collisions. The currentSaveId cursor is in-memory only —
restarting the process requires an explicit load() before
createSnapshot() will succeed.
JSON decode failures are mapped to PERSISTENCE_LOAD_FAILED rather than
silently returning null, so corrupted storage surfaces as an
inspectable error chain instead of an empty save list. Save failures
preserve their underlying cause through GameServerError.cause, making
storage-side diagnostics directly available to the caller.