# `GameInstanceRegistry` _(no submodules)_ ## Purpose `GameInstanceRegistry` is the in-memory primitive that tracks every live game-server instance in a multi-host deployment. It owns the canonical `{id → instance}` map, evicts stale entries whose last heartbeat is older than `instanceTTL`, and selects one healthy instance per discovery request using one of four load-balancing strategies. Transport façades (HTTP/gRPC) that surface the registry over the network live in sibling packages — this module is the pure data structure plus its lifecycle. ## Features - TTL-based eviction driven by an injected `Clock` (no implicit `Date.now`). - Four load-balancing strategies: `least-players`, `random`, `region-based`, `round-robin`. The `random` strategy consults an injected `Rng`. - Filter-based `discover()`: by game type, region, mode, map, slot count, privacy, tags, and version. - Lifecycle events streamed through a single fan-in listener: `registered`, `deregistered`, `health-changed`, `capacity-changed`, `full`, `available`. - Zod-validated `register()` boundary; every public mutation routes to a typed `GameServerError` on lookup miss. - One OTel counter (`game_server.registry.instance_lifecycle_total`) tagged with `op ∈ {register, deregister, expire, health, capacity}`. ## Usage: Basic ```ts import { GameInstanceRegistry } from "@suaveplan/game-server/enhanced/service-discovery/game-instance-registry"; const registry = new GameInstanceRegistry({ clock: Date.now, rng: { nextUint32: () => 0, nextFloat: Math.random }, }); registry.register({ id: "i-001", name: "us-east-shard-1", host: "10.0.0.1", port: 4000, metadata: { gameType: "duel", region: "us-east", playerCount: 0, maxPlayers: 8, }, }); const selected = registry.selectInstance( { gameType: "duel", minAvailableSlots: 2 }, "least-players" ); ``` ## Usage: Advanced ```ts const off = registry.on((event) => { if (event.kind === "full") notifyMatchmaker(event.instance.id); if (event.kind === "health-changed" && event.newStatus === "unhealthy") { drainSessionsFrom(event.instance.id); } }); // Region-weighted selection — pass a latency map for cold start. const r = new GameInstanceRegistry( { clock, rng }, { instanceTTL: 30_000, defaultStrategy: "region-based", regionLatencies: new Map([ ["us-east", 25], ["eu-west", 95], ["ap-southeast", 180], ]), autoDeregisterEmpty: true, } ); setInterval(() => r.sweep(), 5_000); off(); ``` ## API Reference - `new GameInstanceRegistry(deps, config?)` — `deps: { clock, rng }`, `config: { instanceTTL?, defaultStrategy?, autoDeregisterEmpty?, regionLatencies? }`. - `register(registration)` — validate + insert; emits `registered`. - `deregister(id)` — remove; emits `deregistered`. Throws on unknown id. - `heartbeat(id)` — refresh `lastHeartbeat`. - `updateHealth(id, status)` — transition health; emits `health-changed`. - `updateCapacity({ instanceId, playerCount, maxPlayers? })` — emits `capacity-changed`, plus `full` or `available` at the boundary. - `discover(filter?)` — filter healthy instances. - `selectInstance(filter?, strategy?)` — pick one or `null`. - `getInstance(id)` — direct lookup, or `null`. - `getStats()` — aggregate snapshot. - `sweep()` — force the TTL pass (also invoked by `discover`). - `on(listener)` — subscribe to lifecycle events, returns an unsubscribe. - `dispose()` — clear all internal state. ## Implementation Notes - Eviction runs in two phases: at `age > instanceTTL` the instance is marked `unhealthy`; at `age > instanceTTL * 2` it is removed. This preserves the upstream semantics where downstream consumers had one full TTL window to fail their own probes. - `random` prefers the optional `Rng.nextIntInRange` and falls back to `Math.floor(rng.nextFloat() * len)` when only `nextFloat` is provided. - `round-robin` keys on a sorted join of instance IDs so adding / removing instances does not silently reset the rotation. - The lifecycle counter is registered through the global OTel meter so no SDK wiring is needed in tests.