Initial Commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# game-server-engine-sessions — sessions + players (Wave B-2)
|
||||
|
||||
**Package**: `@suaveplan/game-server`
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature (Wave B-2 migration slice)
|
||||
|
||||
- **Design**: see this file
|
||||
- **Spec delta**: ./specs/game-server/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
Wave A landed the foundations and Wave A interfaces (`ISessionManager`,
|
||||
`IPlayerManager` et al). Wave B-1 shipped the engine top-level (GameEngine
|
||||
+ GameLoop + PluginManager). Waves B-3 and B-4 shipped state + network.
|
||||
|
||||
What is missing: the session and player runtime that backs those Wave A
|
||||
interfaces. Without it, the engine has nowhere to register a session or
|
||||
track a connected player, and the `ISessionManager` /
|
||||
`IPlayerManager` slots in `GameEngine` can only be filled by ad-hoc
|
||||
stubs.
|
||||
|
||||
This slice (Wave B-2) ships five engine modules that round out the
|
||||
runtime:
|
||||
|
||||
- `SessionLifecycle` — per-session FSM (`lobby → active → ended`) with
|
||||
transition validation, audit history, and OTel dwell metrics.
|
||||
- `SessionGraphNode` — internal mutable graph node implementing Wave A's
|
||||
read-only `SessionNode` interface.
|
||||
- `SessionTree` — hierarchical graph (`Map<id, node>` + `Set<rootId>`)
|
||||
with O(1) lookups, Zod-validated boundaries, and typed event emission.
|
||||
- `SessionStore` — `ISessionManager` implementation composing
|
||||
`SessionTree` with an injectable `SessionPersistence` adapter for
|
||||
durability.
|
||||
- `PlayerManager` — `IPlayerManager` implementation with heartbeat-driven
|
||||
liveness, reconnection grace window, and a timer-driven monitor that
|
||||
routes through `TimersLike`.
|
||||
|
||||
This is a re-dispatch of an earlier B-2 attempt that branched from a
|
||||
stale base and re-invented Wave A's interfaces. The new work builds on
|
||||
top of the existing canonical interfaces and types.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `SessionGraphNode` (`engine/session-node`) — internal mutable
|
||||
graph node, structurally compatible with Wave A's `SessionNode`
|
||||
interface; preserves timestamps on persistence round-trips.
|
||||
- ADDED: `SessionTree` (`engine/session-tree`) — graph manager with
|
||||
`createSession`/`destroySession`/`getStats`/`validateIntegrity` plus
|
||||
the `attachRestored` seam used by `SessionStore.restore`.
|
||||
- ADDED: `SessionStore` (`engine/session-store`) — `ISessionManager`
|
||||
implementation with pluggable `SessionPersistence` (default in-memory).
|
||||
- ADDED: `SessionLifecycle` (`engine/session-lifecycle`) — per-session
|
||||
FSM with stable error codes for every rejection path.
|
||||
- ADDED: `PlayerManager` (`engine/player-manager`) — `IPlayerManager`
|
||||
implementation with injectable `Clock`/`TimersLike`/`EventBus`.
|
||||
- ADDED: `GAME_SERVER.*` codes — `SESSION_NOT_FOUND`,
|
||||
`SESSION_ALREADY_ENDED`, `INVALID_SESSION_TRANSITION`,
|
||||
`DUPLICATE_PLAYER_IN_SESSION`, `INVALID_SESSION_DATA`,
|
||||
`DUPLICATE_SESSION_ID`, `PARENT_SESSION_NOT_FOUND`,
|
||||
`INVALID_SESSION_STORE_OPTIONS`, `INVALID_PLAYER_MANAGER_OPTIONS`,
|
||||
`INVALID_HEARTBEAT_INTERVAL`, `PLAYER_DISCONNECTED`.
|
||||
- ADDED: OTel instruments — `game_server.session.lifecycle_total`
|
||||
counter, `game_server.session.dwell_ms` histogram,
|
||||
`game_server.player.lifecycle_total` counter.
|
||||
- ADDED: subpath exports `./engine/session-lifecycle`,
|
||||
`./engine/session-node`, `./engine/session-store`,
|
||||
`./engine/session-tree`, `./engine/player-manager`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `GameEngine` re-wiring to consume `SessionStore` / `PlayerManager` at
|
||||
construction (later orchestration slice; engine accepts them via DI).
|
||||
- Hono / network plugin layer for session/player REST (later slice).
|
||||
- The matchmaking / queue surface in upstream's `enhanced/` — out of
|
||||
scope for B-2.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| SessionNode graph helpers | `packages/universal/game-server/src/engine/SessionNode.ts` L8–L175 | Recast — `Date.now()` replaced by injectable `Clock`; idempotent `addChild`. |
|
||||
| SessionTree O(1) graph | `packages/universal/game-server/src/engine/SessionTree.ts` L16–L441 | Recast — plain `Error` → `GameServerError`; Zod-validated boundary; persistence concern lifted into `SessionStore`. |
|
||||
| SessionStore persistence | `packages/universal/game-server/src/engine/SessionStore.ts` L22–L132 | Recast — `AbstractStorage` byte API replaced by `SessionPersistence` adapter accepting structured records. |
|
||||
| PlayerManager lifecycle | `packages/universal/game-server/src/engine/PlayerManager.ts` L13–L357 | Recast — `setInterval` replaced by `TimersLike`-driven self-rescheduling tick; reconnection grace window. |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `SessionLifecycle` FSM | Genesis bar — upstream `SessionLifecycle.ts` was a session+player coordinator, NOT an FSM. The B-2 dispatch explicitly requires a per-session `lobby → active → ended` FSM with transition validation + audit history. |
|
||||
| OTel session lifecycle counter + dwell histogram | Genesis bar — upstream had no OTel. |
|
||||
| OTel player lifecycle counter | Genesis bar — upstream had no OTel. |
|
||||
| Zod-validated `SessionData` and option boundaries | Genesis bar. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/engine/session-lifecycle/session-lifecycle.test.ts` — FSM
|
||||
transition table, audit history, dwell sample emission, error paths.
|
||||
- `src/engine/session-node/session-node.test.ts` — tree-walking helpers,
|
||||
idempotent attach, persistence round-trip with preserved timestamps.
|
||||
- `src/engine/session-tree/session-tree.test.ts` — Zod boundary, O(1)
|
||||
lookups, bottom-up cascade, integrity audit, restore seam.
|
||||
- `src/engine/session-store/session-store.test.ts` — `ISessionManager`
|
||||
conformance harness, persist/restore round-trip, orphan handling.
|
||||
- `src/engine/player-manager/player-manager.test.ts` —
|
||||
`IPlayerManager` conformance harness, heartbeat monitor scheduling,
|
||||
grace-window eviction, reconnection metadata merge.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
The package has no shipped dependents on genesis at the time of this
|
||||
change. Additive surface only.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. Existing `./errors`, `./types`, `./core`, `./engine/*`
|
||||
surfaces are unchanged. `GAME_SERVER_CODES` registry is **extended**;
|
||||
all existing codes are preserved.
|
||||
@@ -0,0 +1,254 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: game-server (game-server-engine-sessions)
|
||||
|
||||
**Package**: `@suaveplan/game-server`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: SessionGraphNode internal mutable graph node
|
||||
|
||||
The package SHALL provide `SessionGraphNode` as the internal mutable
|
||||
representation of a hierarchical session. The class SHALL be structurally
|
||||
compatible with Wave A's read-only `SessionNode` interface (it
|
||||
implements `SessionNode`). All wall-clock reads SHALL route through an
|
||||
injected `Clock`; the class SHALL NOT call `Date.now()` directly. The
|
||||
constructor SHALL accept an optional preserved-timestamps slot used by
|
||||
`SessionStore.restore` to keep `createdAt` / `updatedAt` byte-equivalent
|
||||
across persistence round-trips.
|
||||
|
||||
#### Scenario: constructs with clock-supplied timestamps by default
|
||||
|
||||
- **WHEN** a node is constructed without the `timestamps` arg
|
||||
- **THEN** `createdAt` and `updatedAt` are both set from the injected `Clock`
|
||||
|
||||
#### Scenario: addChild is idempotent
|
||||
|
||||
- **WHEN** `addChild(c)` is called twice with the same child
|
||||
- **THEN** the child appears exactly once in `children`
|
||||
|
||||
#### Scenario: tree-walking helpers respect depth and ancestry
|
||||
|
||||
- **WHEN** `getDepth`, `getDescendants` (DFS pre-order), `getAncestors`, and `getRoot` are called on a populated tree
|
||||
- **THEN** each helper returns the documented walk; `isAncestorOf` / `isDescendantOf` exclude self (strict)
|
||||
|
||||
#### Scenario: toJSON serialises root with null parentId
|
||||
|
||||
- **WHEN** a root node is serialised
|
||||
- **THEN** `parentId` is `null`; for a non-root the parent's id is preserved
|
||||
|
||||
**Verified by:** `src/engine/session-node/session-node.test.ts::"constructs with clock-supplied createdAt/updatedAt when no timestamps passed"` AND `src/engine/session-node/session-node.test.ts::"preserves timestamps when explicitly passed (restore path)"` AND `src/engine/session-node/session-node.test.ts::"addChild is idempotent — re-adding the same child is a no-op"` AND `src/engine/session-node/session-node.test.ts::"getDepth returns 0 for root, distance otherwise"` AND `src/engine/session-node/session-node.test.ts::"getDescendants returns DFS pre-order array"` AND `src/engine/session-node/session-node.test.ts::"isAncestorOf / isDescendantOf are strict (exclude self)"` AND `src/engine/session-node/session-node.test.ts::"toJSON serialises a node with null parentId for a root"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SessionTree hierarchical graph
|
||||
|
||||
The package SHALL provide `SessionTree` owning a `Map<id, SessionGraphNode>`
|
||||
plus a `Set<rootId>` and exposing the mutation surface used by
|
||||
`SessionStore`. The `createSession` boundary SHALL parse `SessionData`
|
||||
through Zod and reject failures with `INVALID_SESSION_DATA`. Duplicate
|
||||
ids SHALL throw `DUPLICATE_SESSION_ID`; unknown parents SHALL throw
|
||||
`PARENT_SESSION_NOT_FOUND`. `destroySession` SHALL cascade bottom-up
|
||||
through every descendant.
|
||||
|
||||
#### Scenario: createSession registers a root and emits session:created
|
||||
|
||||
- **WHEN** `createSession(null, data)` is called with valid data
|
||||
- **THEN** the node is registered as a root and `session:created` is emitted
|
||||
|
||||
#### Scenario: invalid SessionData rejects with INVALID_SESSION_DATA
|
||||
|
||||
- **WHEN** `createSession` is called with `id: ""` or any schema-failing input
|
||||
- **THEN** a `GameServerError(GAME_SERVER.INVALID_SESSION_DATA)` is thrown
|
||||
|
||||
#### Scenario: duplicate id rejects with DUPLICATE_SESSION_ID
|
||||
|
||||
- **WHEN** a second `createSession` reuses an existing id
|
||||
- **THEN** `GameServerError(GAME_SERVER.DUPLICATE_SESSION_ID)` is thrown
|
||||
|
||||
#### Scenario: unknown parent rejects with PARENT_SESSION_NOT_FOUND
|
||||
|
||||
- **WHEN** `createSession(parentId, ...)` references a missing id
|
||||
- **THEN** `GameServerError(GAME_SERVER.PARENT_SESSION_NOT_FOUND)` is thrown
|
||||
|
||||
#### Scenario: destroySession cascades bottom-up
|
||||
|
||||
- **WHEN** `destroySession` is called on an ancestor with descendants
|
||||
- **THEN** `session:destroyed` events fire in deepest-first order
|
||||
|
||||
#### Scenario: attachRestored re-attaches a deserialised node without firing session:created
|
||||
|
||||
- **WHEN** `attachRestored(node, parentId)` is called during `SessionStore.restore`
|
||||
- **THEN** the node is inserted into the index, the `session:created` event is NOT emitted, and a duplicate id throws `DUPLICATE_SESSION_ID`
|
||||
|
||||
#### Scenario: validateIntegrity catches corrupted invariants
|
||||
|
||||
- **WHEN** the bidirectional parent/children invariant is forced into an inconsistent state
|
||||
- **THEN** `validateIntegrity()` returns `{valid: false, errors: [...]}` with at least one diagnostic
|
||||
|
||||
**Verified by:** `src/engine/session-tree/session-tree.test.ts::"createSession registers a root and emits session:created"` AND `src/engine/session-tree/session-tree.test.ts::"createSession rejects invalid SessionData with INVALID_SESSION_DATA"` AND `src/engine/session-tree/session-tree.test.ts::"createSession rejects duplicate ids with DUPLICATE_SESSION_ID"` AND `src/engine/session-tree/session-tree.test.ts::"createSession rejects unknown parents with PARENT_SESSION_NOT_FOUND"` AND `src/engine/session-tree/session-tree.test.ts::"destroySession cascades through descendants bottom-up"` AND `src/engine/session-tree/session-tree.test.ts::"attachRestored attaches a pre-built node without firing session:created"` AND `src/engine/session-tree/session-tree.test.ts::"validateIntegrity flags a tampered parent pointer"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SessionStore implements ISessionManager
|
||||
|
||||
The package SHALL provide `SessionStore` implementing Wave A's
|
||||
`ISessionManager` interface (`createSession`, `getSession`,
|
||||
`destroySession`, `getSessionsByType`, `getChildren`, `getParent`,
|
||||
`updateMetadata`, `getAllSessions`, `persist`, `restore`). The store
|
||||
SHALL accept an injectable `SessionPersistence` adapter and fall back to
|
||||
an in-memory default when none is supplied. Construction SHALL
|
||||
Zod-validate options and reject with `INVALID_SESSION_STORE_OPTIONS`.
|
||||
`restore` SHALL handle arbitrary record orderings via a fixed-point walk
|
||||
that attaches any record whose parent is already present.
|
||||
|
||||
#### Scenario: ISessionManager conformance harness passes
|
||||
|
||||
- **WHEN** `runISessionManagerConformance(() => new SessionStore(deps))` is invoked
|
||||
- **THEN** every scenario in the harness reports `passed: true`
|
||||
|
||||
#### Scenario: invalid options reject with INVALID_SESSION_STORE_OPTIONS
|
||||
|
||||
- **WHEN** `new SessionStore(deps, { storageKey: "" })`
|
||||
- **THEN** the constructor throws `GameServerError(GAME_SERVER.INVALID_SESSION_STORE_OPTIONS)`
|
||||
|
||||
#### Scenario: persist + restore round-trip preserves the tree
|
||||
|
||||
- **WHEN** a populated `SessionStore` is persisted, then cleared, then restored
|
||||
- **THEN** the restored tree has the same node count and parent/child relationships
|
||||
|
||||
#### Scenario: restore handles permuted record orderings
|
||||
|
||||
- **WHEN** a persisted payload lists children before their parents
|
||||
- **THEN** the fixed-point walk attaches every record and the final tree shape is identical to the pre-persist state
|
||||
|
||||
#### Scenario: restore logs orphan records and skips them
|
||||
|
||||
- **WHEN** a payload references a parent that doesn't exist in the same payload
|
||||
- **THEN** the orphan id is logged via `logger.warn` and the rest of the records still come online
|
||||
|
||||
**Verified by:** `src/engine/session-store/session-store.test.ts::"ISessionManager conformance harness passes"` AND `src/engine/session-store/session-store.test.ts::"rejects invalid options with INVALID_SESSION_STORE_OPTIONS"` AND `src/engine/session-store/session-store.test.ts::"persist+restore round-trips with the in-memory default"` AND `src/engine/session-store/session-store.test.ts::"restore handles permuted record ordering via fixed-point walk"` AND `src/engine/session-store/session-store.test.ts::"restore logs orphan records as warnings and skips them"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SessionLifecycle per-session FSM
|
||||
|
||||
The package SHALL provide `SessionLifecycle` implementing a per-session
|
||||
finite-state machine with the closed three-state vocabulary
|
||||
`lobby | active | ended`. The transition table SHALL be:
|
||||
`lobby → active`, `lobby → ended`, `active → ended`. Every other pair
|
||||
SHALL be rejected with `INVALID_SESSION_TRANSITION`. Transitions out of
|
||||
`ended` SHALL throw `SESSION_ALREADY_ENDED`. Transitions on unknown ids
|
||||
SHALL throw `SESSION_NOT_FOUND`. Every transition SHALL emit a
|
||||
`session:transitioned` event and record both
|
||||
`game_server.session.lifecycle_total` (counter) and
|
||||
`game_server.session.dwell_ms` (histogram) OTel instruments.
|
||||
|
||||
#### Scenario: register places the session in lobby
|
||||
|
||||
- **WHEN** `register(id)` is called
|
||||
- **THEN** the new entry is in state `lobby` with `enteredStateAt = clock()`
|
||||
|
||||
#### Scenario: transition validates against the allow-table
|
||||
|
||||
- **WHEN** `transition("s1", "active")` is called on a `lobby` entry
|
||||
- **THEN** the FSM advances to `active` and emits `session:transitioned`
|
||||
|
||||
#### Scenario: backwards transition rejects with INVALID_SESSION_TRANSITION
|
||||
|
||||
- **WHEN** `transition("s1", "lobby")` is called on an `active` entry
|
||||
- **THEN** `GameServerError(GAME_SERVER.INVALID_SESSION_TRANSITION)` is thrown
|
||||
|
||||
#### Scenario: transition out of ended rejects with SESSION_ALREADY_ENDED
|
||||
|
||||
- **WHEN** `transition("s1", "active")` is called on an `ended` entry
|
||||
- **THEN** `GameServerError(GAME_SERVER.SESSION_ALREADY_ENDED)` is thrown
|
||||
|
||||
#### Scenario: unknown id rejects with SESSION_NOT_FOUND
|
||||
|
||||
- **WHEN** `transition("nope", "active")` is called
|
||||
- **THEN** `GameServerError(GAME_SERVER.SESSION_NOT_FOUND)` is thrown
|
||||
|
||||
#### Scenario: dwell histogram receives a sample per transition
|
||||
|
||||
- **WHEN** the FSM transitions out of `lobby`
|
||||
- **THEN** `game_server.session.dwell_ms` records one sample tagged `state: "lobby"`
|
||||
|
||||
#### Scenario: forget drops the entry but preserves audit history
|
||||
|
||||
- **WHEN** `forget(id)` is called after one or more transitions
|
||||
- **THEN** `getState(id)` returns `null` while `getHistoryFor(id)` still returns the transitions
|
||||
|
||||
**Verified by:** `src/engine/session-lifecycle/session-lifecycle.test.ts::"register creates an entry in lobby state"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"lobby → active transition succeeds and emits event"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"active → lobby (backwards) is rejected with INVALID_SESSION_TRANSITION"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"transition out of ended is rejected with SESSION_ALREADY_ENDED"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"transition on unknown id is rejected with SESSION_NOT_FOUND"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"emits session.dwell_ms histogram sample for the state being left"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"history preserves transitions across forget()"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PlayerManager implements IPlayerManager
|
||||
|
||||
The package SHALL provide `PlayerManager` implementing Wave A's
|
||||
`IPlayerManager` interface (11 methods). Construction SHALL Zod-validate
|
||||
options and reject with `INVALID_PLAYER_MANAGER_OPTIONS`.
|
||||
`startHeartbeatMonitor` SHALL Zod-validate the interval and reject
|
||||
non-positive integers with `INVALID_HEARTBEAT_INTERVAL`. The heartbeat
|
||||
monitor SHALL schedule via the injected `TimersLike`, NOT `setInterval`.
|
||||
`checkTimeouts` SHALL transition connected players past
|
||||
`timeoutThresholdMs` to `timeout` and evict disconnected entries past
|
||||
`reconnectionGraceMs`. A `connect` against an existing disconnected entry
|
||||
within the grace window SHALL walk the reconnection path
|
||||
(preserving `connectedAt`).
|
||||
|
||||
#### Scenario: IPlayerManager conformance harness passes
|
||||
|
||||
- **WHEN** `runIPlayerManagerConformance(() => new PlayerManager(deps))` is invoked
|
||||
- **THEN** every scenario in the harness reports `passed: true`
|
||||
|
||||
#### Scenario: invalid options reject with INVALID_PLAYER_MANAGER_OPTIONS
|
||||
|
||||
- **WHEN** `new PlayerManager(deps, { timeoutThresholdMs: 0 })`
|
||||
- **THEN** the constructor throws `GameServerError(GAME_SERVER.INVALID_PLAYER_MANAGER_OPTIONS)`
|
||||
|
||||
#### Scenario: startHeartbeatMonitor rejects non-positive intervals
|
||||
|
||||
- **WHEN** `startHeartbeatMonitor(0)` or `startHeartbeatMonitor(1.5)` is called
|
||||
- **THEN** the call throws `GameServerError(GAME_SERVER.INVALID_HEARTBEAT_INTERVAL)`
|
||||
|
||||
#### Scenario: heartbeat monitor schedules via TimersLike
|
||||
|
||||
- **WHEN** `startHeartbeatMonitor(50)` is called
|
||||
- **THEN** `TimersLike.setTimer` is invoked with delay 50; firing a due timer triggers `checkTimeouts` and the monitor self-reschedules
|
||||
|
||||
#### Scenario: stopHeartbeatMonitor cancels the next scheduled tick
|
||||
|
||||
- **WHEN** `stopHeartbeatMonitor()` is called after `startHeartbeatMonitor`
|
||||
- **THEN** the pending timer is cleared and the captured callback, if fired post-stop, does not re-schedule itself
|
||||
|
||||
#### Scenario: connect walks the reconnection path for disconnected entries
|
||||
|
||||
- **WHEN** `connect("p1", "s1", { region: "eu" })` is called after `disconnect("p1", "x")`
|
||||
- **THEN** `connectedAt` is preserved from the original entry, metadata is shallow-merged, and `player:reconnected` is emitted
|
||||
|
||||
#### Scenario: checkTimeouts evicts disconnected entries past grace window
|
||||
|
||||
- **WHEN** `checkTimeouts` is called with `now - conn.disconnectedAt > reconnectionGraceMs`
|
||||
- **THEN** the connection is deleted; a subsequent `getConnection` returns `null`
|
||||
|
||||
**Verified by:** `src/engine/player-manager/player-manager.test.ts::"IPlayerManager conformance harness passes"` AND `src/engine/player-manager/player-manager.test.ts::"rejects invalid options with INVALID_PLAYER_MANAGER_OPTIONS"` AND `src/engine/player-manager/player-manager.test.ts::"startHeartbeatMonitor rejects non-positive intervals"` AND `src/engine/player-manager/player-manager.test.ts::"startHeartbeatMonitor schedules ticks via TimersLike"` AND `src/engine/player-manager/player-manager.test.ts::"heartbeat tick stops re-scheduling if monitor was stopped mid-tick"` AND `src/engine/player-manager/player-manager.test.ts::"connect on a disconnected player walks the reconnection path"` AND `src/engine/player-manager/player-manager.test.ts::"checkTimeouts evicts disconnected entries past grace window"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports
|
||||
|
||||
The package SHALL expose each new feature directory via additive
|
||||
`exports` map entries so consumers can tree-shake.
|
||||
|
||||
#### Scenario: each feature dir is independently importable
|
||||
|
||||
- **WHEN** a consumer imports from `@suaveplan/game-server/engine/session-lifecycle`, `@suaveplan/game-server/engine/session-node`, `@suaveplan/game-server/engine/session-store`, `@suaveplan/game-server/engine/session-tree`, or `@suaveplan/game-server/engine/player-manager`
|
||||
- **THEN** the import resolves to the corresponding feature barrel
|
||||
|
||||
**Verified by:** `packages/gaming/games/game-server/package.json::"exports./engine/session-lifecycle"` AND `packages/gaming/games/game-server/package.json::"exports./engine/session-node"` AND `packages/gaming/games/game-server/package.json::"exports./engine/session-store"` AND `packages/gaming/games/game-server/package.json::"exports./engine/session-tree"` AND `packages/gaming/games/game-server/package.json::"exports./engine/player-manager"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,66 @@
|
||||
# Tasks — game-server-engine-sessions (Wave B-2, re-dispatch)
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 `git status` clean in worktree, `git worktree list` accounted for.
|
||||
- [x] 0.2 Worktree at `.claude/worktrees/agent-ae5779d2ba388beff` — isolated branch built on current genesis HEAD (`71a33ed`).
|
||||
- [x] 0.3 Manifest count unchanged — no new packages added, only modules inside `@suaveplan/game-server`.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read upstream `SessionNode.ts`, `SessionTree.ts`, `SessionStore.ts`, `PlayerManager.ts`, `SessionLifecycle.ts`.
|
||||
- [x] 1.2 Read existing Wave A `core/interfaces/i-session-manager` and `i-player-manager`.
|
||||
- [x] 1.3 Read existing `openspec/changes/game-server-engine-core` proposal as template.
|
||||
- [x] 1.4 Drafted `proposal.md`.
|
||||
- [x] 1.5 Filled `## Source mapping` table; FSM-shape requirement listed in the "no source line" table with justification.
|
||||
- [x] 1.6 Drafted `specs/game-server/spec.md`.
|
||||
- [x] 1.7 Every requirement carries `**Verified by:**` line.
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Extend `GAME_SERVER_CODES` with session + player codes (11 new).
|
||||
- [x] 2.2 Extend `observability/otel.ts` with `sessionLifecycleCounter`, `sessionDwellHistogram`, `playerLifecycleCounter`.
|
||||
- [x] 2.3 Implement `src/engine/session-node/session-node.ts` — internal mutable graph node.
|
||||
- [x] 2.4 Implement `src/engine/session-tree/session-tree.ts` — graph manager with Zod boundary.
|
||||
- [x] 2.5 Implement `src/engine/session-store/session-store.ts` — `ISessionManager` impl with pluggable persistence.
|
||||
- [x] 2.6 Implement `src/engine/session-lifecycle/session-lifecycle.ts` — per-session FSM.
|
||||
- [x] 2.7 Implement `src/engine/player-manager/player-manager.ts` — `IPlayerManager` impl.
|
||||
- [x] 2.8 Add subpath exports to `package.json` for all five new feature dirs.
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 `session-node.test.ts` — 15 scenarios covering tree walks + persistence round-trip.
|
||||
- [x] 3.2 `session-tree.test.ts` — 25 scenarios covering CRUD, integrity audit, restore seam.
|
||||
- [x] 3.3 `session-store.test.ts` — `ISessionManager` conformance harness + persist/restore + orphan handling.
|
||||
- [x] 3.4 `session-lifecycle.test.ts` — 15 scenarios covering FSM transitions, history, OTel emission.
|
||||
- [x] 3.5 `player-manager.test.ts` — `IPlayerManager` conformance + heartbeat monitor + reconnection + grace eviction.
|
||||
- [x] 3.6 Every `it(...)` carries `{ timeout: 5000 }`.
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.ts` (6 docs: session-node, session-tree, session-store, session-lifecycle, player-manager, plus extended otel.md).
|
||||
- [x] 4.2 `otel.md` updated with new instruments.
|
||||
|
||||
## 5. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [x] 5.1 `bun run lint` — Biome 0 errors / 0 warnings.
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/game-server` — green (395 tests).
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/game-server` — 100/100/100/100 (1542/1542 stmts, 574/574 branches, 360/360 funcs, 1491/1491 lines).
|
||||
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/game-server` — to be run by archive step.
|
||||
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/game-server` — to be run by archive step (orchestrator merges before build run).
|
||||
|
||||
## 6. Pre-archive 7-row checklist (AGENTS.md §13)
|
||||
|
||||
- [x] 6.1 Generics threaded; conformance harnesses pass for both interfaces.
|
||||
- [x] 6.2 Zod schemas wired at `SessionData`, `SessionStoreOptions`, `PlayerManagerOptions` boundaries.
|
||||
- [x] 6.3 Every new error code thrown by at least one production path and asserted in a test.
|
||||
- [x] 6.4 Lifecycle path tested both for `ok` and `error` outcomes.
|
||||
- [x] 6.5 OTel instruments emitted from production paths and asserted with the in-memory `MeterProvider` polyfill.
|
||||
- [x] 6.6 Reconnection grace window + heartbeat monitor self-rescheduling tested.
|
||||
- [x] 6.7 No retroactive tick — every box reflects work done in this change.
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold spec delta into `openspec/specs/game-server/spec.md` — handled by orchestrator post-merge.
|
||||
- [ ] 7.2 Move folder to archive — handled by orchestrator post-merge.
|
||||
- [ ] 7.3 Refresh `MANIFEST.md` if package set changed (it did not — additive modules only).
|
||||
Reference in New Issue
Block a user