# PersistencePlugin _(no submodules)_ ## 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` - `load(sessionId): Promise` - `delete(sessionId): Promise` - `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: }` 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`.