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,121 @@
# StateSync
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`StateSync` is the orchestration layer that turns `StateManager` mutations
into wire packets and applies received packets back into the manager. It
brokers between a single `StateManager<TState>` and a pluggable
`StateSyncTransport` — WebSocket, WebRTC data channel, in-process pipe —
deciding per call whether to send a delta, a full snapshot, or honour an
inbound request. The state-sync algorithm is otherwise transport-agnostic.
The Wave B-3 port replaces the upstream's hard dependency on
`NetworkAdapter`, `GameEventEmitter`, and `Logger` with a single
`StateSyncTransport` interface and OTel-native instrumentation. Push
cadence is left to callers — there is no internal `setInterval` —
because game loops, dirty-flag policies, and bandwidth limiters belong
to the application layer, not the sync engine.
## Features
- Three-message protocol (`delta`, `full`, `request`) over an abstract transport.
- Automatic fallback to full snapshot when the delta exceeds `maxDeltaSize`.
- Initial-sync handshake: when no previous state has been synced, sends a full snapshot.
- Inbound `request` triggers an outbound full snapshot.
- Per-kind OTel counters + bytes histogram.
- Typed errors: transport faults surface as `STATE_SYNC_TRANSPORT_FAILED`.
- Statistics snapshot and reset.
- Disposal removes the transport listener.
## Usage: Basic
```typescript
import { StateManager } from "@suaveplan/game-server/state/state-manager";
import { StateSync } from "@suaveplan/game-server/state/state-sync";
interface World { tick: number }
const manager = new StateManager<World>({ tick: 0 });
const sync = new StateSync(manager, transport);
// On each game tick:
manager.update("tick", manager.get("tick") + 1);
await sync.sync();
```
## Usage: Advanced
```typescript
import type { StateSyncTransport, StateSyncMessage } from "@suaveplan/game-server/state/state-sync";
// In-process pipe for tests / single-process play
class InProcessTransport implements StateSyncTransport {
private readonly handlers = new Set<(m: StateSyncMessage) => void>();
public peer?: InProcessTransport;
isConnected() { return this.peer !== undefined; }
onMessage(h: (m: StateSyncMessage) => void) {
this.handlers.add(h);
return () => { this.handlers.delete(h); };
}
async send(m: StateSyncMessage) {
for (const h of this.peer?.handlers ?? []) h(m);
}
}
const t1 = new InProcessTransport();
const t2 = new InProcessTransport();
t1.peer = t2; t2.peer = t1;
const sync = new StateSync(manager, t1, {
config: { maxDeltaSize: 4096 },
});
await sync.requestFullSync(); // ask peer for full state
```
## API Reference
### `class StateSync<TState extends object>`
#### `constructor(manager, transport, options?)`
`options` — `{ compressor?, clock?, meter?, config?: { maxDeltaSize? } }`.
#### `sync(): Promise<void>`
Sends a delta or, on first call or when the delta exceeds `maxDeltaSize`,
a full snapshot.
#### `syncFull(): Promise<void>` / `requestFullSync(): Promise<void>`
#### `getStats(): SyncStats` / `resetStats(): void`
#### `dispose(): void`
### Types
- `StateSyncTransport` — `{ isConnected, send, onMessage }`.
- `StateSyncMessage` — `delta | full | request` discriminated union.
- `SyncStats` — counters for deltas/fulls/requests sent + received, bytes, ratio, last timestamp.
- `StateSyncConfig` — `{ maxDeltaSize? }`.
## Implementation Notes
The `snapshotCurrent` step explicitly deep-clones via `JSON.parse(JSON.stringify(...))`
so the `lastSyncedState` does not retain references to the manager's
deep-frozen snapshots — any subsequent attempt to compute a delta would
otherwise compare two snapshots whose primitive leaves are identical
but whose container references differ, producing spurious replace ops.
JSON-clone is acceptable because the sync protocol already constrains
the state to JSON-serialisable shapes.
Transport errors are wrapped in `GameServerError(STATE_SYNC_TRANSPORT_FAILED)`
with the failing message kind on `context` so structured logs and
observability dashboards can pinpoint the misbehaving leg of the
connection. Inbound message handling does not catch transport-side
errors — those are the transport's problem; the `StateSync` contract is
that `onMessage` only delivers well-formed `StateSyncMessage` values.