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,145 @@
# StateManager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<World>({ 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<Session>(
{ 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<TState extends object>`
#### `constructor(initialState, options?)`
`options` — `{ maxHistorySize?, clock?, idGenerator?, meter? }`.
#### `getState(): DeepReadonly<TState>`
#### `get<K>(key): DeepReadonly<TState[K]>`
#### `has(key): boolean` / `keys(): Array<keyof TState>`
#### `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<TState>` — `{ id, timestamp, kind, previousState, currentState }`.
- `StateChangeListener<TState>`, `StateManagerStats`, `SnapshotValidator<TState>`.
## 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.