Initial Commit
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# Wave B-5b: `@suaveplan/game-server` performance slice
|
||||
|
||||
**Package**: `@suaveplan/game-server`
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature-add (additive to existing 0.1.0 package)
|
||||
|
||||
## Why
|
||||
|
||||
Wave A landed the `@suaveplan/game-server` core foundation. Five
|
||||
parallel waves now extend the package along orthogonal axes; B-5 owns
|
||||
the **matchmaking + performance** axis. This change covers the
|
||||
performance half; a sibling change (`game-server-matchmaking`) covers
|
||||
queue / matcher / spectator / engine.
|
||||
|
||||
The upstream reference
|
||||
(`/mnt/s/web/suaveplan/packages/universal/game-server/src/performance/`)
|
||||
ships three modules — `ObjectPool`, `SessionPool`, `NetworkTierAdapter`
|
||||
— but couples them to upstream-only types (`Logger`,
|
||||
`GameEventEmitter`, `SessionNodeImpl`) and lacks typed-error /
|
||||
Zod-validated boundaries. This slice ports the algorithms, replaces the
|
||||
upstream couplings with `@suaveplan/types` (`Clock`) plus typed
|
||||
observer callbacks, and adds OTel-native instrumentation across all
|
||||
three modules.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `src/performance/object-pool/` — generic typed object pool
|
||||
with factory + reset + optional dispose; configurable
|
||||
`initialSize`/`maxSize`/`preAllocate`; `acquire` / `release` /
|
||||
`withObject` / `clear` / `dispose` / `getStats`; two OTel counters;
|
||||
typed-error boundary (`POOL_EXHAUSTED`, `POOL_DISPOSED`,
|
||||
`RELEASE_FOREIGN_OBJECT`, `INVALID_POOL_CONFIG`).
|
||||
- ADDED: `src/performance/session-pool/` — session-specific wrapper
|
||||
over `ObjectPool` that pools plain-data `PooledSession` records
|
||||
(`id`, `type`, `metadata`, `children[]`); injectable `Clock`;
|
||||
`acquireSession`/`releaseSession`/`withSession`/`getStats`.
|
||||
- ADDED: `src/performance/network-tier-adapter/` — bridge between the
|
||||
game-server's matchmaking layer and the `@suaveplan/network` tier
|
||||
model; `registerNodeCapabilities` / `getOptimalTier` /
|
||||
`getLoadBalancingRecommendations` / `validateTierCapacity` /
|
||||
`getStats`; one OTel counter; typed-error boundary
|
||||
(`INVALID_TIER_CONFIG`, `UNKNOWN_TIER`); optional
|
||||
`onCapabilityUpdated` listener.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `openspec/specs/game-server/spec.md` (game-server
|
||||
capability).
|
||||
- New error codes: `POOL_EXHAUSTED`, `POOL_DISPOSED`,
|
||||
`RELEASE_FOREIGN_OBJECT`, `INVALID_POOL_CONFIG`,
|
||||
`INVALID_TIER_CONFIG`, `UNKNOWN_TIER`.
|
||||
- New subpath exports: `./performance/{object-pool, session-pool,
|
||||
network-tier-adapter}`.
|
||||
- No `core/` / `state/` / `engine/` / `network/` modifications.
|
||||
@@ -0,0 +1,99 @@
|
||||
# game-server — performance spec delta (Wave B-5b)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: ObjectPool typed reuse
|
||||
|
||||
`ObjectPool<T>` SHALL recycle typed objects between a configurable
|
||||
`factory`, `reset`, and optional `dispose` to dampen GC pressure, and
|
||||
SHALL enforce a configurable `maxSize` cap by raising typed errors
|
||||
when callers misuse the pool.
|
||||
|
||||
#### Scenario: pre-allocation populates the pool
|
||||
|
||||
- **GIVEN** an `ObjectPool` configured with `initialSize: 2` and
|
||||
`preAllocate: true`
|
||||
- **WHEN** the pool is constructed
|
||||
- **THEN** `getStats().available` reports `2`
|
||||
|
||||
#### Scenario: acquire returns a recycled object
|
||||
|
||||
- **GIVEN** a pool with two pre-allocated objects
|
||||
- **WHEN** the caller invokes `acquire()`
|
||||
- **THEN** the returned object came from the pool
|
||||
- **AND** `getStats().totalReused` reports `1`
|
||||
|
||||
#### Scenario: cap rejection
|
||||
|
||||
- **GIVEN** a pool with `maxSize: 1` and one object outstanding
|
||||
- **WHEN** the caller invokes `acquire()` again
|
||||
- **THEN** the call throws
|
||||
`GameServerError(GAME_SERVER.POOL_EXHAUSTED)`
|
||||
|
||||
#### Scenario: foreign-object release rejection
|
||||
|
||||
- **WHEN** the caller releases an object not handed out by this pool
|
||||
- **THEN** `release` throws
|
||||
`GameServerError(GAME_SERVER.RELEASE_FOREIGN_OBJECT)`
|
||||
|
||||
#### Scenario: dispose lockout
|
||||
|
||||
- **GIVEN** a disposed `ObjectPool`
|
||||
- **WHEN** the caller invokes `acquire()`, `release(obj)`, or `clear()`
|
||||
- **THEN** each call throws
|
||||
`GameServerError(GAME_SERVER.POOL_DISPOSED)`
|
||||
|
||||
### Requirement: SessionPool plain-data session recycling
|
||||
|
||||
`SessionPool` SHALL pool plain-data `PooledSession` records and
|
||||
expose a `acquireSession(data)` API that stamps the borrowed record
|
||||
with the caller-supplied identity and metadata before returning it.
|
||||
|
||||
#### Scenario: acquireSession stamps the record
|
||||
|
||||
- **WHEN** the caller invokes
|
||||
`acquireSession({ id: "match-42", type: "game", metadata: {...} })`
|
||||
- **THEN** the returned `PooledSession` carries `id === "match-42"`
|
||||
- **AND** its `children` array is empty
|
||||
- **AND** its `metadata.createdAt` mirrors the caller's input
|
||||
|
||||
#### Scenario: releaseSession recycles the record
|
||||
|
||||
- **GIVEN** a pool with `initialSize: 1, maxSize: 5`
|
||||
- **WHEN** the caller acquires, mutates, and releases a session
|
||||
- **AND** the caller immediately re-acquires
|
||||
- **THEN** the same underlying object is returned with a freshly
|
||||
empty `children` array
|
||||
|
||||
### Requirement: NetworkTierAdapter capability-driven tier selection
|
||||
|
||||
`NetworkTierAdapter` SHALL accept a non-empty, validated tier list at
|
||||
construction time, track per-node capability assessments, and
|
||||
recommend an optimal tier per node based on its composite score.
|
||||
|
||||
#### Scenario: empty tier list rejection
|
||||
|
||||
- **WHEN** the adapter is constructed with `tiers: []`
|
||||
- **THEN** the constructor throws
|
||||
`GameServerError(GAME_SERVER.INVALID_TIER_CONFIG)`
|
||||
|
||||
#### Scenario: score-based optimal tier
|
||||
|
||||
- **GIVEN** an adapter with `host`, `sub-host`, `player` tiers
|
||||
- **WHEN** `getOptimalTier({score: 92, ...})` is invoked
|
||||
- **THEN** the returned tier carries `nodeType === "host"`
|
||||
- **AND** `getOptimalTier({score: 60, ...})` returns `sub-host`
|
||||
- **AND** `getOptimalTier({score: 20, ...})` returns `player`
|
||||
|
||||
#### Scenario: load-balancing recommendation sort order
|
||||
|
||||
- **GIVEN** three registered nodes with scores 90, 60, 20
|
||||
- **WHEN** `getLoadBalancingRecommendations()` is invoked
|
||||
- **THEN** the result is sorted by capability score descending
|
||||
- **AND** the first entry's `recommendedLoad` is `floor(90 / 10) === 9`
|
||||
|
||||
#### Scenario: unknown-tier rejection
|
||||
|
||||
- **WHEN** `getTierForNodeType("nope")` is invoked
|
||||
- **THEN** the call throws
|
||||
`GameServerError(GAME_SERVER.UNKNOWN_TIER)`
|
||||
74
openspec/archive/2026-05-24-game-server-performance/tasks.md
Normal file
74
openspec/archive/2026-05-24-game-server-performance/tasks.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Tasks: Wave B-5b game-server performance
|
||||
|
||||
## 0. Pre-flight
|
||||
|
||||
- [x] `pwd && git worktree list && git status` — confirm isolated worktree.
|
||||
- [x] Confirm worktree HEAD is descended from genesis tip.
|
||||
- [x] Read `CLAUDE.md`, `openspec/AGENTS.md`, existing `src/core`, `src/errors`, `src/types`.
|
||||
- [x] Read upstream reference:
|
||||
`/mnt/s/web/suaveplan/packages/universal/game-server/src/performance/{ObjectPool,SessionPool,NetworkTierAdapter}.ts`.
|
||||
|
||||
## 1. Error registry
|
||||
|
||||
- [x] Append six performance codes to `GAME_SERVER_CODES`
|
||||
(`POOL_EXHAUSTED`, `POOL_DISPOSED`, `RELEASE_FOREIGN_OBJECT`,
|
||||
`INVALID_POOL_CONFIG`, `INVALID_TIER_CONFIG`, `UNKNOWN_TIER`).
|
||||
- [x] Extend `game-server-error.test.ts` with assertions on the new codes.
|
||||
- [x] Update `game-server-error.md` to enumerate the new codes.
|
||||
|
||||
## 2. `object-pool`
|
||||
|
||||
- [x] `object-pool.types.ts` — `ObjectPoolConfig`, `PoolStats`, Zod
|
||||
numeric config schema.
|
||||
- [x] `object-pool.ts` — generic typed pool with
|
||||
`acquire`/`release`/`withObject`/`getStats`/`clear`/`dispose`, two
|
||||
OTel counters, typed-error boundary, pre-allocation gate.
|
||||
- [x] `object-pool.test.ts` — 100% coverage incl. cap rejection +
|
||||
foreign-release rejection + dispose lockout.
|
||||
- [x] `object-pool.md` — six sections, ≥200 words.
|
||||
- [x] `index.ts` barrel.
|
||||
|
||||
## 3. `session-pool`
|
||||
|
||||
- [x] `session-pool.types.ts` — `PooledSession`, `PooledSessionData`,
|
||||
`SessionPoolConfig`.
|
||||
- [x] `session-pool.ts` — typed wrapper over `ObjectPool` with
|
||||
`acquireSession`/`releaseSession`/`withSession`, injectable `Clock`,
|
||||
default session-type configurable.
|
||||
- [x] `session-pool.test.ts` — 100% coverage incl. reset path +
|
||||
injected clock + auto-release on throw.
|
||||
- [x] `session-pool.md` — six sections, ≥200 words.
|
||||
- [x] `index.ts` barrel.
|
||||
|
||||
## 4. `network-tier-adapter`
|
||||
|
||||
- [x] `network-tier-adapter.types.ts` — `NetworkTier`,
|
||||
`NodeCapabilities`, `LoadBalancingRecommendation`,
|
||||
`NetworkTierAdapterStats`, Zod schemas, listener typedef.
|
||||
- [x] `network-tier-adapter.ts` —
|
||||
`registerNodeCapabilities`/`getTierForNodeType`/`getTierByIndex`/
|
||||
`getOptimalTier`/`getLoadBalancingRecommendations`/
|
||||
`validateTierCapacity`/`getMaxChildren`/`getStats`/`clear`, one OTel
|
||||
counter, typed-error boundary, optional `onCapabilityUpdated`
|
||||
listener.
|
||||
- [x] `network-tier-adapter.test.ts` — 100% coverage incl. invalid
|
||||
tier list + duplicate tier index + score-bucket fallbacks.
|
||||
- [x] `network-tier-adapter.md` — six sections, ≥200 words.
|
||||
- [x] `index.ts` barrel.
|
||||
|
||||
## 5. Wiring
|
||||
|
||||
- [x] Re-export every public surface from `src/index.ts`.
|
||||
- [x] Add subpath exports to `package.json` for each module.
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- [x] `bunx turbo lint typecheck test --filter=@suaveplan/game-server` green.
|
||||
- [x] `bunx turbo test:coverage --filter=@suaveplan/game-server` =
|
||||
100/100/100/100.
|
||||
- [x] `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/gaming/games/game-server/src/`
|
||||
→ zero output.
|
||||
- [x] `grep -rn "biome-ignore|TODO|FIXME|XXX"
|
||||
packages/gaming/games/game-server/src/performance` → zero output.
|
||||
- [x] All source files ≤300 lines; all test files ≤500 lines.
|
||||
- [x] `bun run docs:check` reports 100% coverage.
|
||||
Reference in New Issue
Block a user