DEV Community

VelvetDusk629047
VelvetDusk629047

Posted on

Testing Realtime Room Capacity Alerts for a Shared Kanban Board (and Why Timing Fails)

Use a realtime room API with an explicit capacity event and recovery path; do not make timing the thing that proves your shared kanban board works. The decision hinges on token scope and client trust: the server owns membership and limits, while clients render alerts from authenticated events.

Short answer: model capacity as a server-side state transition, expose it as an observable business event, and test with a virtual clock plus injected latency, duplicates, reconnects, and authorization failures.

Start with the trust boundary

Picture two lanes. The server lane creates a room, checks who may join, counts active participants, and emits capacity.warning or capacity.full. The browser lane subscribes, draws the banner, and records what it received. A browser may request a join, but it must not declare itself the 21st editor.

For a media team's board, “room” can mean the board's collaboration session. Keep the token narrow: identify the board and a role such as editor or viewer; avoid a token that grants access to every board. The server maps that token to a room before accepting a subscription. This makes a capacity alert an authorization decision, not a decorative toast.

Authentication, subscription state, and business events deserve separate telemetry. I use three counters: auth_denied_total, subscription_state_changes_total, and capacity_alerts_total{level}. A structured log carries room_id, actor_id, event_id, and client_version. Metrics tell me that alerts changed; logs tell me which event was involved. A trace links the join request to the emitted alert without pretending that a successful socket handshake means the workflow succeeded.

One sentence matters here.

The client should show “capacity status unknown” during a reconnect, not “room available.” That small distinction prevents an editor from dragging a card while the server has already rejected the session.

How should realtime room capacity alerts be tested on a shared kanban board?

Start from a state machine, not from sleeps. For a room limit of 20, the useful transitions are 19 -> 20 (warning), 20 -> 21 (reject), and 20 -> 19 (clear). Feed those transitions through a deterministic clock. Deliver each event with a sequence number; the reducer ignores an older sequence and safely applies the same sequence twice.

Here is a minimal TypeScript harness for creating the test room. It uses the documented RTC route, an environment-provided key, an idempotency key, explicit status checks, and bounded retry for rate limits. The test can stub fetch to add 40–300 ms latency, reorder responses, or duplicate a notification without changing production code.

const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function createRoom(roomId: string, capacity: number) {
  const idempotencyKey = `kanban-test-${roomId}`;
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/rtc/room/create`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({ room: roomId, capacity }),
    });

    if (response.ok) return response.json();
    if (response.status !== 429) {
      throw new Error(`room create failed (${response.status}): ${await response.text()}`);
    }

    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
  throw new Error("room create remained rate limited after retries");
}

await createRoom("board-release-42", 20);
Enter fullscreen mode Exit fullscreen mode

The body fields in this harness are test-owned inputs; your contract test should validate the exact schema exposed by the service discovery document before using them in CI. The important testing properties are independent of field names: one logical create, an explicit method, a checked response, and no credential forwarded to a browser-facing URL.

For the event path, replace real time with a Clock interface. Advance it to just before the threshold, publish an editor join, then advance one tick. Assert the alert payload and the metric together. Next, deliver the same payload twice. The board should render one banner and retain one event_id. Finally, expire the token while the client is offline; on reconnect, expect a fresh authorization result and a full snapshot, not a replay guessed from local storage. I like to write this as one replayable timeline: at t=0 the room reports 19 editors; at t=1 the twentieth editor is authorized and the warning event is queued; at t=1.2 the browser receives it; at t=1.3 the same event arrives again; at t=2 the token expires; at t=3 the network returns, but a twenty-first join has already been refused. The assertions then read in plain English: one warning banner, one event identity, no unauthorized card mutation, and a temporary unknown state during the gap. Change the schedule to 250 ms jitter or reverse the two deliveries and the expected result stays the same. That is the point: timing becomes input data, never an implicit pass condition.

What should be observable when delivery is messy?

Treat partial failure as a normal branch in the test matrix. A subscription can be accepted while the first business event is delayed. A reconnect can succeed while the room is now full. A client can receive capacity.warning after capacity.full because the network reordered packets. None of these should produce a false “room open” state.

I log the transition reason (join, leave, expiry, admin_kick) and keep it separate from transport reason (connected, retrying, closed). This lets an alert page answer two different questions: “Are we at capacity?” and “Can this browser currently hear the answer?” They are related, not interchangeable.

The flaky test smell is a naked setTimeout. A 100 ms wait may pass on a laptop and fail under CI contention. Assert on a state transition or a sequence number instead. If a real integration test needs waiting, poll the test clock or await an event with a deadline; never sleep and hope.

How do the practical options compare?

The transport choice changes how much policy you must own. Ably provides managed pub/sub primitives and presence. Pusher is straightforward for channel events, with authorization handled by your application. Socket.IO gives a familiar event API, but you operate the servers and reconnect behavior. Infrai's realtime surface is another plain REST option for room lifecycle calls: one HTTP API, no SDK installation, and the same key can cover other backend capabilities. That consistency is useful when the board already has storage or job APIs behind the same service boundary.

Option Room and presence model Operational trade-off Fit for this board
Ably Managed channels and presence Less infrastructure to run; vendor-specific protocol concepts Strong when global fan-out matters
Pusher Channels with application authorization Simple integration; policy remains in your server Good for modest collaboration rooms
Socket.IO Events, rooms, and adapters Maximum control; you own scaling and reconnect tests Good when you already run Node infrastructure
Infrai RTC Explicit room lifecycle endpoints Plain HTTP and one credential surface; event semantics still belong in your design Good when a single backend API boundary is a priority

The catch is scope. Choose Socket.IO when you need deep control over transport adapters or self-hosting. Choose Ably when managed global presence is more important than keeping protocol details in your stack. A narrow board with strict tenant isolation may prefer a dedicated service whose authorization model your team already audits. Infrai is not a universal replacement for those choices; its advantage here is the simple HTTP boundary, not a promise that every realtime policy is automatic.

Exactly.

A release gate that catches timing bugs

Make the CI suite run the same scenario at three latency profiles: zero, jittered, and delayed. Add duplicate delivery, out-of-order sequence numbers, token expiry, and a viewer attempting an editor action. The pass condition is behavioral: no unauthorized card mutation, one alert per event identity, and a truthful “unknown” state while disconnected.

I am not sure your production traffic will resemble these exact delays, and your mileage may vary by browser and region. That is why the test records the injected schedule alongside each assertion. When a failure appears, you get a timeline to replay instead of a mysterious red build.

Before shipping, inspect the room lifecycle against the documented routes: create only once, read the room state when a snapshot is needed, and delete test rooms explicitly. Keep the route contract in a small adapter so a future provider comparison changes one module, not every board component.

References

Top comments (0)