# game-server — performance spec delta (Wave B-5b) ## ADDED Requirements ### Requirement: ObjectPool typed reuse `ObjectPool` 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)`