Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user