first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
# PersistencePlugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PersistencePlugin` tracks per-session game state via a `GameEventEmitter`
and persists it to an injectable `StorageLike` backend. It subscribes to
`session:created`, `game:started`, `game:state:changed`, `player:joined`,
`player:left`, and `game:ended` so engine state is captured without
intrusive instrumentation.
The plugin saves on `game:ended` automatically (toggleable) and exposes
explicit `save`, `load`, and `delete` operations so callers can drive
persistence from outside the event stream too — for example, during an
emergency shutdown hook.
## Features
- Subscribes to the game emitter for transparent state capture.
- Accumulates `gameState` from `game:state:changed` events.
- Tracks per-session player membership.
- Save record is Zod-validated on load — corrupt files throw
`GAME_SERVER.PERSISTENCE_LOAD_FAILED`.
- Saves on `game:ended` (configurable via `saveOnGameEnd`).
- Customisable key prefix (`prefix`, default `"game:save:"`).
- Lifecycle-safe: unsubscribes and clears tracked sessions on uninstall.
## Usage: Basic
```ts
import { GameEventEmitter } from "@suaveplan/game-server";
import { PersistencePlugin } from "@suaveplan/game-server/plugins/persistence-plugin";
const emitter = new GameEventEmitter();
const storage = createMyKvAdapter();
const plugin = new PersistencePlugin({ emitter, storage });
await plugin.onInstall(ctx);
```
## Usage: Advanced
```ts
import { PersistencePlugin } from "@suaveplan/game-server/plugins/persistence-plugin";
const plugin = new PersistencePlugin({
emitter,
storage,
prefix: "tournament:",
saveOnGameEnd: false,
clock: () => 1_700_000_000_000,
});
await plugin.onInstall(ctx);
emitter.emit("game:started", { sessionId: "s1" });
emitter.emit("game:state:changed", {
sessionId: "s1",
newState: { score: 42 },
});
const saved = await plugin.save("s1");
const loaded = await plugin.load("s1");
await plugin.delete("s1");
```
## API Reference
- `class PersistencePlugin extends BasePlugin`
- `constructor(options: PersistencePluginOptions)`
- `save(sessionId): Promise<SavedGameState>`
- `load(sessionId): Promise<SavedGameState | null>`
- `delete(sessionId): Promise<boolean>`
- `peek(sessionId): SavedGameState | undefined`
- `mergeState(sessionId, fragment): void`
- `size(): number`
## Implementation Notes
- `game:state:changed` carries `newState: unknown`. The plugin merges
object payloads into the tracked map and wraps non-object payloads as
`{ value: <payload> }` so the saved record always has a stable shape.
- `load` performs three checks: storage `get`, JSON parse, and Zod
schema validation. Any failure surfaces as
`PERSISTENCE_LOAD_FAILED` with the original error preserved as the
cause; `null` is reserved for "nothing stored at this key".
- Errors in the auto-save path (`saveOnGameEnd`) are caught and
swallowed after a logger.error so a storage outage cannot stall the
emitter. Manual `save` calls still surface the error to the caller.
- `mergeState` is a public hook for callers that want to capture state
from a non-event source (e.g. an external scheduler). It writes
through to the in-memory row only; persistence still requires `save`.