Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
# GamePersistence
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 `StorageLike` backend (in-memory, durable, network).
- Named snapshots with rotating `maxSnapshots` cap.
- Bounded audit-trail history with `maxHistoryEntries` cap.
- Injectable `Clock` and `IdGenerator` for deterministic tests.
- Stable error codes: `PERSISTENCE_SAVE_FAILED`, `PERSISTENCE_LOAD_FAILED`,
`PERSISTENCE_NOT_FOUND`, `INVALID_SAVE_RECORD`.
- OTel counters + duration histogram.
- Tracks `currentSaveId` for snapshot context.
## Usage: Basic
```ts
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
```ts
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(): PersistenceStats`
- `getCurrentSaveId(): 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.