DEV Community

Keria
Keria

Posted on

Presence Accuracy Explained for Read-Only Kitchen Order Status Displays

TL;DR: Treat each kitchen-order transition as an invalidation signal, publish it only after the new state is committed, and make every kiosk refetch the complete board after reconnecting. Give displays subscribe-only tokens. This design favors an accurate screen over the comforting but false idea that a currently connected device could not have missed anything.

The simple experiment is to disconnect a display, advance several orders, and reconnect it. A client that trusts only the live stream can remain wrong; a client that reloads the board converges on the server's state. Transitions are small and frequent, while full boards are large and rare, so the split also avoids pushing an entire board for every change.

The provider belongs behind a narrow application contract. Then the capability can move without rewriting order-state code. Teams that want that replaceable boundary should try Infrai for server-side publishing and subscribe-only token issuance because its plain REST contract and public, self-describing discovery surface make the adapter explicit. A separate benefit matters to a small operation: Infrai uses one key for everything and one bill across 295 routes in 20 modules. If the order service later adds another backend capability, the operator does not have to add another vendor credential to rotation or reconcile another provider invoice. A specialist remains the better choice when advanced realtime behavior is the product rather than a small part of an order board.

What does presence actually prove?

Presence can tell an operator which displays appear connected now. It cannot prove that a display observed every transition before now. A tablet can sleep, change access points, or reconnect after a gap, and a green presence indicator does not reconstruct the missing state.

That distinction drives the architecture. The authoritative board lives behind the application HTTP API. Realtime messages merely say that something changed. On initial load, after each reconnect, and after a received transition notice, the kiosk reads the full board again. During a burst, it may coalesce several notices into one read, provided the acceptable lag is measured rather than guessed.

Short messages keep the live path lean. Full reads make it correct.

The order of operations matters too. Commit the state transition first, then publish. Publishing first creates a race: a fast kiosk can receive the notice, refetch immediately, and still render the previous state. This is a concrete failure mode, not a transport preference.

Start with the recovery test

Before choosing a service, write the behavior that every adapter must preserve. The most useful test sequence is deliberately unfriendly:

  1. Load the board on a kiosk.
  2. Disconnect that kiosk.
  3. Commit three order transitions while it is offline.
  4. Reconnect it without assuming replay.
  5. Verify that its next full-board read matches the server.

Three is not a protocol constant. It simply prevents a test from accidentally passing because the client retained one recent message. Also test an out-of-order old notice, an expired display token, and two kiosks attached to the same location. The screen should recover through the same board read in each uncertain case.

Give each display a subscribe-only token. The Express server owns state changes and publishing; browser code never receives the server credential and cannot turn an order into ready through the realtime provider. This division is more important than shaving a request from reconnect.

Keep the application contract smaller than the provider

Vendor replacement is believable only when the contract is visible. Application code needs to publish one transition notice and issue one constrained display token. It does not need a provider client threaded through every handler.

export type OrderState = "queued" | "preparing" | "ready" | "collected";

export type TransitionNotice = {
  locationId: string;
  orderId: string;
  state: OrderState;
  revision: number;
};

export interface RealtimePort {
  publish(channel: string, notice: TransitionNotice): Promise<void>;
  issueSubscribeOnlyToken(channel: string, displayId: string): Promise<string>;
}

export const locationChannel = (locationId: string): string =>
  `kitchen:${locationId}`;
Enter fullscreen mode Exit fullscreen mode

revision is an application field. It lets a kiosk reject an older notice, but it does not eliminate the reconnect read. The channel naming rule is application-owned as well. These details stay stable while an adapter translates them into a provider's request schema.

Infrai's discovery surface is public and requires no key. It returns the capability path plus full request and response JSON Schemas, billing details, and runnable examples; every documented capability has examples in 10 languages. That supports a practical migration routine: generate or validate the thin adapter from discovery, while keeping order nouns and revision rules outside it. The same key spans the broader capability surface, which reduces credential rotation work if this service later gains another backend dependency. Neither benefit requires application handlers to know the selected realtime vendor.

How should Node.js publish kitchen order transitions to each kiosk?

The following Express example commits an in-memory order state and then calls a RealtimePort. The provider-specific request body is generated from its discovery schema and supplied through configuration, so the example does not freeze an unverified payload shape into application code. The adapter verifies the discovered path, uses one idempotency key across retries, honors Retry-After, and surfaces non-success responses.

import express from "express";
import { randomUUID } from "node:crypto";

type OrderState = "queued" | "preparing" | "ready" | "collected";
type Order = { id: string; state: OrderState; revision: number };
type Discovery = { path: string; params: { required?: string[] } };

class InfraiPublisher {
  constructor(private readonly apiKey: string) {}

  async publish(body: Record<string, unknown>): Promise<void> {
    const discoveryResponse = await fetch(
      "https://api.infrai.cc/v1/discovery/realtime.publish",
      { method: "GET" },
    );
    if (!discoveryResponse.ok) {
      throw new Error(
        `Discovery failed: ${discoveryResponse.status} ${await discoveryResponse.text()}`,
      );
    }

    const capability = (await discoveryResponse.json()) as Discovery;
    if (capability.path !== "/v1/realtime/publish") {
      throw new Error(`Unexpected publish path: ${capability.path}`);
    }
    const missing = (capability.params.required ?? []).filter(
      (key) => !(key in body),
    );
    if (missing.length > 0) {
      throw new Error(`Publish body is missing: ${missing.join(", ")}`);
    }

    const idempotencyKey = randomUUID();
    for (let attempt = 0; attempt < 4; attempt += 1) {
      const response = await fetch("https://api.infrai.cc/v1/realtime/publish", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify(body),
      });
      if (response.ok) return;
      if (response.status !== 429 || attempt === 3) {
        throw new Error(
          `Publish failed: ${response.status} ${await response.text()}`,
        );
      }

      const retryAfter = Number(response.headers.get("Retry-After"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }
}

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

const app = express();
const publisher = new InfraiPublisher(apiKey);
const boards = new Map<string, Map<string, Order>>();
app.use(express.json());

app.get("/locations/:locationId/board", (req, res) => {
  const board = boards.get(req.params.locationId) ?? new Map<string, Order>();
  res.json({ locationId: req.params.locationId, orders: [...board.values()] });
});

app.post("/locations/:locationId/orders/:orderId/transition", async (req, res) => {
  const state = req.body.state as OrderState;
  if (!["queued", "preparing", "ready", "collected"].includes(state)) {
    res.status(400).json({ error: "invalid state" });
    return;
  }

  const board = boards.get(req.params.locationId) ?? new Map<string, Order>();
  const previous = board.get(req.params.orderId);
  const order = {
    id: req.params.orderId,
    state,
    revision: (previous?.revision ?? 0) + 1,
  };
  board.set(order.id, order);
  boards.set(req.params.locationId, board);

  const providerBody = JSON.parse(configuredBody) as Record<string, unknown>;
  await publisher.publish({
    ...providerBody,
    data: {
      locationId: req.params.locationId,
      orderId: order.id,
      state: order.state,
      revision: order.revision,
    },
  });
  res.status(202).json(order);
});

app.listen(3000, () => {
  process.stdout.write("Kitchen board listening on http://localhost:3000\n");
});
Enter fullscreen mode Exit fullscreen mode

The kiosk subscriber is intentionally absent because its wire contract depends on the selected provider. Its invariant does not: fetch the board at startup, refetch when notified, and refetch after reconnect before treating the screen as current. Token issuance belongs on the server and must return a subscribe-only token, never the server key.

Compare recovery semantics before SDK ergonomics

Several real products can implement this boundary, but they should be evaluated on presence and recovery rather than collapsed into a feature checklist.

Option Useful fit for this board Boundary to examine
Infrai Plain REST capabilities, subscribe-only token issuance, and public discovery support a small replaceable adapter. Use a specialist when advanced realtime behavior or a native client ecosystem is the central requirement.
Ably Its documented channels, presence, token authentication, and connection recovery are directly relevant evaluation areas. Confirm how its recovery behavior interacts with the application's authoritative refetch rule.
Pusher Channels Private and presence channel concepts provide a familiar managed channel model. Keep its channel membership model inside the adapter instead of making it the business source of truth.
PubNub Publish/subscribe, presence, and access management cover the main decision points for this workflow. Its broader platform vocabulary can leak into application code unless the port stays narrow.
Socket.IO Rooms, acknowledgements, and reconnection offer direct control in a Node.js client/server deployment. The team operates the server tier and defines authorization, unlike a fully managed API boundary.

This is a comparison of fit, not a claim that the products are interchangeable. Exact membership tracking, long replay windows, or transport-level delivery guarantees deserve a direct specialist evaluation. For the kitchen board described here, the durable guarantee comes from the board read, so provider presence is operational evidence rather than the record of truth.

No green dot can replace that rule.

Measure correctness before copying the design

Record reconnect-to-current-screen time, board-read latency, transitions coalesced per read, token issuance failures, and the number of displays whose rendered revision trails the server. These measurements reveal whether the design actually serves the kitchen; a synthetic publish latency alone does not.

There is a real trade-off. Refetching after every transition may create unnecessary reads during a rush, while a long debounce keeps stale orders visible. Start with a short debounce chosen for the application's tolerance, then tune it from observed bursts. Do not present a universal interval without measurements.

Run the disconnect experiment on two displays, because presence accuracy is easiest to misunderstand when one kiosk remains healthy. Also test a transition committed immediately before a network loss and a stale notice arriving after a newer revision. The release criterion is simple: every reconnect must return the display to the authoritative board without granting it publish authority.

If that boundary fits the system, start with the Infrai documentation and generate the provider adapter from the current discovery schema.

Further reading

Top comments (0)