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¶
Doc References¶
Last Updated: January 2026