# StateManager _(no submodules)_ ## Purpose `StateManager` is the canonical, observable container for game-session state. It holds a single typed value, exposes read/write APIs (`get`, `update`, `updateMany`, `setState`, `reset`, `restore`), and emits a `StateChangeEvent` on every mutation so downstream consumers — UI binders, sync engines, persistence layers, replay recorders — can react without polling. Snapshots are returned deep-frozen so reckless callers can't reach into the manager's internal state and break its invariants. The Wave B-3 port replaces the upstream's `Logger` and `GameEventEmitter` dependencies with injectable `Clock` and `IdGenerator` from `@suaveplan/types` plus OTel-native instrumentation. Listener faults are counted and absorbed — a misbehaving subscriber can never destabilise the state machine. `restore` accepts an optional validator predicate and throws `GAME_SERVER.INVALID_SNAPSHOT` when the snapshot fails it; clone failures surface as `GAME_SERVER.STATE_RESTORE_FAILED`. ## Features - Single-value typed store with deep-frozen reads. - Six mutation paths (`update`, `updateMany`, `setState`, `reset`, `restore`, plus `subscribe`). - Bounded change history (default 100 entries, configurable). - Synchronous listener notification with isolated error handling. - Injectable `Clock` and `IdGenerator` for deterministic test scenarios. - Three OTel instruments: `mutation_total`, `listener_error_total`, `mutation_duration_ms`. - Snapshot validation via predicate; typed errors on rejection. - Discriminated `StateChangeEvent.kind` for downstream switch logic. ## Usage: Basic ```typescript import { StateManager } from "@suaveplan/game-server/state/state-manager"; interface World { score: number; alive: boolean } const world = new StateManager({ score: 0, alive: true }); const unsub = world.subscribe((event) => { console.log(event.kind, event.currentState.score); }); world.update("score", 1); world.update("score", 2); world.setState({ score: 5, alive: false }); unsub(); world.reset(); // back to initial { score: 0, alive: true } ``` ## Usage: Advanced ```typescript import { StateManager } from "@suaveplan/game-server/state/state-manager"; import { GAME_SERVER_CODES, GameServerError } from "@suaveplan/game-server/errors"; interface Session { round: number; players: string[] } const isSession = (v: unknown): v is Session => typeof v === "object" && v !== null && typeof (v as Session).round === "number" && Array.isArray((v as Session).players); const manager = new StateManager( { round: 0, players: [] }, { maxHistorySize: 500, clock: () => 1700000000000, idGenerator: () => "test" } ); try { manager.restore({ wrong: true }, isSession); } catch (err) { if ( err instanceof GameServerError && err.code === GAME_SERVER_CODES.INVALID_SNAPSHOT ) { // Surface a typed diagnostic; state is unchanged. } } manager.restore({ round: 3, players: ["a", "b"] }, isSession); console.log(manager.getStats()); // { updateCount, listenerCount, ... } ``` ## API Reference ### `class StateManager` #### `constructor(initialState, options?)` `options` — `{ maxHistorySize?, clock?, idGenerator?, meter? }`. #### `getState(): DeepReadonly` #### `get(key): DeepReadonly` #### `has(key): boolean` / `keys(): Array` #### `update(key, value): void` / `updateMany(partial): void` #### `setState(next): void` / `reset(): void` #### `restore(snapshot, validator?): void` Throws `GAME_SERVER.INVALID_SNAPSHOT` (validator) or `GAME_SERVER.STATE_RESTORE_FAILED` (clone). #### `subscribe(listener): () => void` #### `getHistory()` / `getRecentHistory(count)` / `clearHistory()` #### `getStats(): StateManagerStats` / `dispose(): void` ### Types - `StateChangeEvent` — `{ id, timestamp, kind, previousState, currentState }`. - `StateChangeListener`, `StateManagerStats`, `SnapshotValidator`. ## Implementation Notes `getState` deep-clones then deep-freezes the snapshot so callers cannot mutate internal state, and so two consecutive `getState` calls return distinct frozen objects — important for downstream code that uses referential equality to detect change. The clone is recursive and pure (plain objects + arrays + primitives) because the manager is constrained to JSON-serialisable shapes; class instances are treated as opaque values and would survive clone by reference. Game code that wants identity-preserving state should split such objects into a sibling registry rather than embedding them in the state tree. `update` short-circuits via `Object.is` when the value is unchanged, avoiding spurious history entries and listener invocations. The listener loop catches every thrown exception, counts it on `listener_error_total`, and continues — a misbehaving subscriber cannot block other subscribers or corrupt the state machine. `maxHistorySize` is enforced by `slice(-max)` after every push so the array is never more than `max + 1` long; this keeps memory bounded on long-running sessions where a million mutations would otherwise pin the entire history in RAM.