Skip to content

Support multiple matches in parallel beyond the singleton lobby DO #259

Description

@0xferit

Problem

The current Worker routes every WebSocket upgrade and Durable Object status probe through a single Durable Object instance via GAME_ROOM.idFromName('lobby') in src/worker/httpHandler.ts and src/worker/accountRepo.ts.

src/worker.ts already keeps an activeMatches map, so more than one match can exist in memory inside the same GameRoom. But queueing, forming, WebSocket fanout, reconnect handling, timers, and checkpoint restore still all run inside one singleton Durable Object.

That means “multiple matches” currently means multiple match records managed by one serialized coordinator, not independent matches executing in parallel.

This creates a few concrete problems:

  • one Durable Object instance becomes the hotspot for all queue and match traffic
  • one object restart/eviction affects every active match instead of one room
  • /status and reconnect flows assume a single room, which blocks clean routing to match-specific execution
  • queue formation is centered around a single formingMatch, which makes scaling queue admission awkward once demand exceeds one table

Goal

Support multiple public matches running in parallel as independent room executions while preserving the canonical game rules in docs/game-design.md and the existing auth/economy semantics.

Non-goals

  • changing match rules, prompt selection, commit-reveal, or settlement behavior
  • redesigning wallet auth or session semantics
  • introducing private tables or invite flows unless they are needed as part of routing

Proposed Plan

1. Split matchmaking from match execution

Keep a lobby/coordinator Durable Object for waiting-room concerns, but move each started match into its own match-scoped Durable Object keyed by matchId (or equivalent room identifier).

Expected outcome:

  • the lobby handles queue admission and assignment
  • each active match owns its own timers, WebSockets, reconnects, and lifecycle

2. Add player-to-room routing

Persist enough routing state to answer: “is this player idle, queued in lobby, or assigned to active room X?”

Scope:

  • route /ws and /status without hardcoding 'lobby' for active matches
  • allow profile/update guards and forfeit flows to resolve the correct room
  • clean up routing state on match completion, forfeit, or abandonment

Expected outcome:

  • reconnect and status flows resolve to the correct room instead of assuming one singleton

3. Replace the single-forming-match assumption

Rework queue admission so demand above one table can promote players into more than one forming/starting match when enough players are available.

Scope:

  • remove the architectural assumption that there is only one formingMatch
  • preserve start now and AI backfill behavior per forming group
  • keep queue-state semantics coherent for clients

Expected outcome:

  • the system can admit enough queued players into multiple independent match starts instead of serializing all formation behind one slot

4. Rework checkpoint/persistence boundaries

Checkpoint restore currently assumes one Durable Object can restore all unfinished matches from local storage.

Scope:

  • make checkpoint state match-room scoped instead of singleton-room scoped
  • keep D1 matches and match_players as the historical record
  • define the source of truth for active routing state so evictions/restarts recover cleanly

Expected outcome:

  • a room restart restores only its own match state
  • unfinished matches are not coupled to one shared in-memory coordinator

5. Expand test coverage for concurrent rooms

Add worker tests that prove independent room execution instead of only multi-match state inside one object.

Scope:

  • two matches can start and progress concurrently
  • reconnect attaches a player to the correct active room
  • queue/game events do not leak across rooms
  • lobby status remains accurate while multiple rooms are active

Acceptance Criteria

  • two or more matches can be active at the same time in separate Durable Object executions
  • /ws and /status no longer hardcode 'lobby' for active-match routing
  • a player reconnects directly to their own active match without relying on one singleton room to own every WebSocket
  • queue formation can promote enough players for multiple matches when demand exceeds one table
  • checkpoint restore is scoped to the relevant room/match instead of one global room object
  • npm run lint passes
  • npm run typecheck passes
  • npm run typecheck:worker passes
  • npm test passes
  • npm run test:worker passes

Primary Hotspots

  • src/worker/httpHandler.ts
  • src/worker/accountRepo.ts
  • src/worker.ts
  • src/worker/persistence.ts
  • worker queue/match lifecycle tests

Notes

GameRoom.activeMatches already supports multiple match records in memory. This issue is specifically about removing the singleton lobby/room bottleneck so those matches can be routed and executed in parallel as independent room instances.

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions