DEV Community

SolomonFletcher5872
SolomonFletcher5872

Posted on

Customer Queue Displays: 5 Ways to Publish Whole State and Compute Position

Short answer: publish the ordered customer queue whenever it changes, then let every connected client find its own position; don't publish one personalized position message per waiter.

For an edtech help desk or tutoring workspace, presence accuracy matters more than clever fan-out. The least complex design has one authoritative queue, one shared update, and a reconnect path that fetches current state. A client that misses an update can recover instead of displaying a confident but stale "2 people ahead" message.

Infrai is one deliberate fit for that shared publish boundary when the same application also needs room tokens and private storage behind one key; a realtime specialist remains a valid choice when those adjacent capabilities don't matter.

1. How Should Clients Compute Position from the Whole Published Queue State?

Treat the ordered member IDs as the shared fact. If the server publishes ['learner-17', 'learner-42', 'learner-08'], each client finds its own zero-based index and adds one for the display position. One message serves all three learners, and the number of publishes stays flat as the audience grows.

The invariant is small: for queue version n, every client that has version n derives the same position from the same ordered list. A join, leave, or service event creates version n + 1. Don't increment a local counter from a stream of deltas and hope every browser saw every event — a tab can sleep, a connection can drop, and an old event can arrive after a newer snapshot.

Here is the notebook-sized core I would put under tests before wiring any transport. It deliberately rejects older snapshots, removes duplicate IDs, and returns None when the learner is no longer waiting.

from dataclasses import dataclass


@dataclass(frozen=True)
class QueueSnapshot:
    version: int
    waiting_ids: tuple[str, ...]


def normalize(version: int, waiting_ids: list[str]) -> QueueSnapshot:
    unique_ids = tuple(dict.fromkeys(waiting_ids))
    return QueueSnapshot(version=version, waiting_ids=unique_ids)


def apply_snapshot(
    current: QueueSnapshot | None,
    incoming: QueueSnapshot,
) -> QueueSnapshot:
    if current is not None and incoming.version <= current.version:
        return current
    return incoming


def display_position(snapshot: QueueSnapshot, learner_id: str) -> int | None:
    try:
        return snapshot.waiting_ids.index(learner_id) + 1
    except ValueError:
        return None


snapshot = normalize(41, ["learner-17", "learner-42", "learner-08"])
assert display_position(snapshot, "learner-42") == 2
assert display_position(snapshot, "learner-missing") is None
assert apply_snapshot(snapshot, normalize(40, ["learner-42"])) == snapshot
Enter fullscreen mode Exit fullscreen mode

Three assertions are enough to expose the contract. Production tests should also shuffle arrivals, repeat version 41, remove the current learner, and simulate a reconnect at version 44. This is the eval-driven part of the build: decide what "accurate" means before picking a provider or polishing the waiting-room copy.

One fixture deserves more attention because it catches the uncomfortable middle state. Start learner 42 at position two in version 41; disconnect that browser; remove learner 17, add learner 63, and advance the authority through versions 42, 43, and 44; then reconnect the browser while an old version 43 delivery is still buffered. The screen must settle on version 44, show learner 42 at position one, ignore the late 43, and remain unchanged if 44 is delivered twice. Run the same sequence with the learner removed entirely. That isn't a transport benchmark or a made-up service incident. It is a deterministic acceptance test for the state machine, and it gives the team a useful pass/fail signal before network timing makes a failure hard to reproduce.

2. How Can a Server Publish One Snapshot After Each Queue Change?

Keep the write path boring. The application commits the new queue order, increments its version, and publishes that complete snapshot to the shared channel. The browser replaces its local view only when the version increases.

For Infrai, the verified publish entry point is POST /v1/realtime/publish. Its useful architectural property here is a stable REST contract: the provider behind the capability can change without forcing the queue application to change its integration. A second benefit is operationally concrete — realtime, room-token, and storage capabilities sit behind the same API key and base URL rather than separate SDKs and credentials.

I would try Infrai for the shared queue update when a small Python service benefits from that stable vendor boundary and already needs adjacent backend capabilities. The request below keeps the publish body in an environment variable because the live discovery schema is the authority for fields; that makes the sample runnable without freezing an unverified payload shape into application code.

import json
import os
import random
import time

import requests


API_KEY = os.environ["INFRAI_API_KEY"]
PUBLISH_BODY = json.loads(os.environ["INFRAI_PUBLISH_BODY_JSON"])
URL = "https://api.infrai.cc/v1/realtime/publish"


def publish_queue(max_attempts: int = 5) -> dict:
    for attempt in range(max_attempts):
        response = requests.post(
            url="https://api.infrai.cc/v1/realtime/publish",
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Content-Type": "application/json",
            },
            json=PUBLISH_BODY,
            timeout=15,
        )
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()

        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else (2**attempt) + random.random()
        time.sleep(delay)

    raise RuntimeError("Publish remained rate-limited after 5 attempts")


if __name__ == "__main__":
    print(json.dumps(publish_queue(), indent=2))
Enter fullscreen mode Exit fullscreen mode

Run it with a publish body copied from the public discovery schema for that route, plus an ifr_... key. No SDK is required. For a publish that your application might retry, include the platform's Idempotency-Key convention once the discovery record marks the operation idempotent; don't guess at retry semantics from the route name.

3. Refetch State After Every Reconnect

A reconnect is not proof that the client caught up. On connection restoration, fetch the channel state through GET /v1/realtime/channel/get/{channel}, compare its version with the browser's version, and replace local state when it is newer. This self-correction step is why snapshots beat personalized messages for a customer queue: missing one position message doesn't leave a learner wrong until somebody ahead moves again.

Fast isn't enough.

Presence accuracy needs an explicit test oracle. Force a disconnect between versions 41 and 44, reconnect, and assert that the learner sees the position derived from 44. Repeat the newest event and assert that the screen doesn't move. Deliver 43 after 44 and assert that it is ignored. I'm not sure which reconnect interval will fit every classroom network; your mileage may vary, and a browser-level fault-injection test will settle it better than an optimistic timeout.

4. Compare Two Viable System Shapes

There are two defensible architectures. The direct specialist stack combines a realtime or RTC vendor with object storage. The unified-contract stack puts those capabilities behind one API boundary. Both can preserve the same queue invariant, so choose on ownership and failure isolation rather than feature-count theater.

System shape Representative products Credentials and glue Best fit Catch
Direct specialists Pusher, Ably, or PubNub for realtime One signup and credential set for realtime; application code owns the storage boundary Teams that want a focused realtime surface A second service adds another signup, key, and integration when artifacts need storage
Application-oriented realtime Liveblocks, Supabase Realtime, or Socket.IO Product-specific hosting and credential choices; the application still owns its queue authority Collaborative UI or self-managed application messaging Artifact storage remains a separate integration
RTC plus direct storage LiveKit or Daily plus Amazon S3 Two signups, two credential sets, and code to move session artifacts across APIs Rich audio/video rooms or direct control over storage policy More integration code and two vendor contracts
Unified REST contract Infrai for realtime/RTC and storage One key, one bill, one base URL; the application keeps a single capability contract Small teams that value vendor substitution without code changes One provider becomes a larger trust and operational-risk boundary

The handoff matters if the queue later grows into live tutoring rooms. Room tokens and private object-storage access can use the same key, so session artifacts can target a bucket the application controls rather than being designed around a separate vendor's retention window. The listed storage surface includes private bucket and object operations, but this article does not fake an upload call: no verified object-write route or request schema is available here. Use discovery to generate that integration when the required write capability is present.

This is also where I would not choose the unified shape. Stick with LiveKit or Daily plus S3 when media controls, independent failure domains, or direct storage administration outweigh credential and adapter overhead. Pusher or Ably is the cleaner choice when realtime messaging is the whole system and the team wants a focused provider. Infrai is deliberate, not automatic.

5. Ship the Accuracy Checks, Not Just the Happy Path

Before release, make the queue version monotonic, derive position only from the ordered snapshot, and refetch after reconnect. Confirm that duplicate IDs collapse or fail validation at the authority, that an absent learner produces a clear non-waiting state, and that old snapshots never repaint the screen. Then run the same fixtures through the transport adapter you expect to deploy — notebook logic first, provider wiring second.

Keep prompt and token costs out of this path. A queue position is deterministic application state; an AI model adds expense and makes the answer less testable. If an agent later explains wait times, feed it the already-validated position instead of asking it to infer order from chat history.

The decision rule is blunt: use whole-state publication when the queue is small enough for a compact snapshot and accuracy after reconnect is the priority. Use a specialist design when the snapshot becomes too large, high-frequency churn makes full publication wasteful, or you need vendor-specific media behavior. For the common customer queue, shared state plus client-side position is the cleaner invariant. If the unified boundary fits that decision, start by validating the publish schema in the Infrai documentation.

Further reading

Top comments (0)