Skip to content

E5-F1: Queue Join and Position Assignment

Delivered by waiting-room

This feature is implemented by the standalone waiting-room service. The spec below describes the consumer contract — what mobile clients and the protected origin (the ticketing backend) require. The actual queue join hits POST /queues/{waiting_room_queue_id}/tickets on waiting-room with a tenant API key; position math is ZRANK + 1 on the Valkey sorted set queue:{queue_id}:waiting. See ../waiting-room/docs/workflow.md for the mobile integration guide.

Epic: E5: Waiting Queue System

Size: M (Medium)

Problem / Outcome

Users join queue and receive position.

Scope

In-Scope:

  • Queue join endpoint via Queue Service
  • Position assignment using Redis sorted set
  • Queue size tracking
  • FIFO ordering
  • WebSocket connection for real-time updates

Out-of-Scope:

  • Priority queue

Queue Service API Contract

Extracted Service

This feature is implemented in the Queue Service (Node.js or Go), not the main Symfony monolith. See Microservices Strategy.

REST Endpoints

Join Queue

POST /api/v1/queue/{match_id}/join
Authorization: Bearer {jwt_token}
Content-Type: application/json

Response (200 OK):

{
  "queue_token": "qt_abc123",
  "position": 4521,
  "total_in_queue": 45000,
  "estimated_wait_minutes": 45,
  "websocket_url": "wss://queue.hns.hr/ws/{queue_token}"
}

Get Current Position

GET /api/v1/queue/{match_id}/position
Authorization: Bearer {jwt_token}

Response (200 OK):

{
  "position": 4200,
  "total_in_queue": 44500,
  "estimated_wait_minutes": 42,
  "status": "waiting"
}

Leave Queue

DELETE /api/v1/queue/{match_id}
Authorization: Bearer {jwt_token}

WebSocket Protocol

Connect to wss://queue.hns.hr/ws/{queue_token} after joining.

Server → Client Messages:

// Position update (every 30 seconds or on significant change)
{
  "type": "position_update",
  "position": 3500,
  "estimated_wait_minutes": 35
}

// Your turn (purchase window granted)
{
  "type": "turn_granted",
  "purchase_window_expires_at": "2026-09-15T14:35:00Z",
  "checkout_url": "https://hns.hr/checkout/{session_token}"
}

// Queue closed (sold out)
{
  "type": "queue_closed",
  "reason": "sold_out"
}

Client → Server Messages:

waiting-room does not use a heartbeat protocol. WebSocket disconnect does not transition the ticket; the hard TTL (ticket_ttl_seconds / session_ttl_seconds) is the contract. See ../waiting-room/ARCHITECTURE.md ("Disconnection model").

Backend integration

The ticketing backend learns about admitted sessions on-demand by calling GET /access on waiting-room with the mobile client's sessionToken (cached 1–5s, fail-closed). There are no queue.turn_granted or queue.expired Redis Pub/Sub channels, and no queue.* subjects on the NATS Event Bus. See Architecture Overview → Origin gating with waiting-room and ADR 0001.

Acceptance Criteria

  • AC1: Given user joins queue, when processed, then position assigned (FIFO)
  • AC2: Queue join returns position, estimated wait time, total queue size
  • AC3: Same user joining from another device gets same position (by user_id)

Data Model Impact

QueueEntry (Redis sorted set):
- key: queue:{match_id}
- member: user_id
- score: timestamp (for FIFO ordering)

QueueMetadata (Redis hash):
- key: queue_meta:{match_id}
- total_size (INTEGER)
- processing_rate (INTEGER per minute)
- created_at (TIMESTAMP)

QueueEntry table (PostgreSQL for persistence):
- id (UUID, PK)
- user_id (UUID, FK)
- match_id (UUID, FK)
- position (INTEGER)
- joined_at (TIMESTAMP)
- status (ENUM: waiting, active, expired, completed)

Permissions/Roles

  • Authenticated user

How to Verify

npm test -- --grep "queue join"

Expected: Position assigned, multi-device sync works.

Dependencies

  • None (foundational for queue)

Implementation Tasks

See E5: Waiting Queue Tasks

Doc References


Last Updated: January 2026