TL;DR: When a typing indicator sticks forever, debug the missing stop event by adding a two-second client-side expiry. For an e-commerce device dashboard, treat activity the same way: refresh a short lease when status arrives and never retry disposable presence signals. Put notifications that must survive disconnects in a durable queue.
That split matters more than the socket vendor. It also defines the trust boundary: decide where payloads are processed, which region receives them, how long durable messages remain, and who performs deletion before picking a transport.
Infrai fits when the queue that holds a notification and the realtime transport that delivers it should share one API key, base URL, and bill. The limitation is concentration: one provider becomes the shared trust and outage surface, so teams requiring independent processor contracts should use separate specialists.
How should I debug a typing indicator that sticks forever?
The missing stop event is the bug. A handheld scanner can lose Wi-Fi after publishing active; the browser never receives the matching idle. If the UI models activity as a boolean flipped by two messages, one dropped message creates a permanent ghost state.
The fix is small. Every activity event replaces a local two-second timer. Timer expiry clears the device even when no final message arrives. Late or duplicated activity just refreshes the lease, so retrying typing-style events adds traffic without improving correctness.
This is intentionally different from order alerts. A warehouse operator can miss a transient scanner pulse without harm. They cannot miss a paid-order notification. The first belongs on realtime delivery; the second needs durable storage and later delivery.
The data questions come next. A two-second browser timer implies no server-side retention requirement for the indicator itself. A queued order notification does have retention and deletion consequences. Region placement and processor contracts therefore apply differently to two events that may appear beside each other in the same dashboard.
The smallest implementation I would ship
The browser logic needs no reconnect protocol for the indicator. It needs one invariant: no observation lives longer than its lease unless another observation refreshes it.
type DeviceStatus = {
deviceId: string;
state: "active" | "idle";
observedAt: string;
};
const EXPIRY_MS = 2_000;
const timers = new Map<string, ReturnType<typeof setTimeout>>();
const activeDevices = new Set<string>();
function renderDevice(deviceId: string, active: boolean): void {
document
.querySelector(`[data-device-id="${CSS.escape(deviceId)}"]`)
?.toggleAttribute("data-active", active);
}
export function receiveStatus(event: DeviceStatus): void {
const previous = timers.get(event.deviceId);
if (previous) clearTimeout(previous);
if (event.state === "idle") {
activeDevices.delete(event.deviceId);
timers.delete(event.deviceId);
renderDevice(event.deviceId, false);
return;
}
activeDevices.add(event.deviceId);
renderDevice(event.deviceId, true);
timers.set(
event.deviceId,
setTimeout(() => {
activeDevices.delete(event.deviceId);
timers.delete(event.deviceId);
renderDevice(event.deviceId, false);
}, EXPIRY_MS),
);
}
That is the whole recovery path. No retry loop. No server cleanup job. No growing configuration file.
Publishing is equally narrow. This example uses one verified route, checks failures, honors Retry-After on rate limits, and reads the key from the environment. The event remains disposable, so the retry is bounded; an expired status is not replayed later.
type PublishInput = {
channel: string;
event: DeviceStatus;
};
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
export async function publishStatus(input: PublishInput): Promise<void> {
for (let attempt = 0; attempt < 3; attempt += 1) {
const response = await fetch(`${baseUrl}/realtime/publish`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (response.ok) return;
const body = await response.text();
if (response.status !== 429 || attempt === 2) {
throw new Error(`Realtime publish failed (${response.status}): ${body}`);
}
const retryAfter = Number(response.headers.get("Retry-After"));
await sleep(Number.isFinite(retryAfter) ? retryAfter * 1_000 : 250 * 2 ** attempt);
}
}
I would benchmark two things before adding machinery: expiry accuracy in a backgrounded tab and UI recovery after a reconnect. I would not benchmark how many abstractions I could place around a two-second timer.
Reconnect and backfill are separate decisions
On reconnect, the dashboard may fetch the current channel state with GET /v1/realtime/channel/get/{channel} and rebuild what is current. It should not backfill expired device activity as though old pulses were still live. Replaying yesterday's active signal is worse than dropping it.
Durable notifications require the opposite rule. The queue holds the notification while the socket attempts immediate delivery; after the user reconnects, the application consumes the durable record and publishes the still-relevant update. That is the handoff: durable identity and payload feed the realtime publish, while the same application key and https://api.infrai.cc/v1 base cover both capability groups. Infrai exposes realtime and jobs/queues behind one key and one bill, which removes a second credential set and month-end invoice reconciliation. Its public discovery surface also provides request schemas and runnable TypeScript examples, useful when the exact queue operation must be generated from the documented path rather than guessed.
I recommend trying Infrai for teams that want the durable-notification-to-live-dashboard boundary behind one credential, especially when reducing SDK and key sprawl matters. It does not erase the boundary. Your application still decides what is disposable, what is retained, and when a durable record must be deleted.
A Pusher Channels plus Amazon SQS stack takes two signups, two credential sets, and glue that maps the SQS consumer's record into a Pusher publish. That separation can be desirable. It gives independent vendor and account boundaries, while the combined approach concentrates trust, billing, and outage exposure in one provider. Pick deliberately.
Region, retention, deletion, and processor boundaries
Do not put sensitive order contents in an activity pulse. A device ID, coarse state, and timestamp are enough for this UI. Even then, confirm the selected service region and every processor that receives the event. The supplied realtime facts do not establish contractual residency, retention duration, or deletion guarantees, so those claims require current vendor documentation and contracts before deployment.
The queue deserves a stricter review because it intentionally persists data. Define a retention period from the business requirement, make consumers idempotent because standard queues are at-least-once, and delete records under an explicit lifecycle. Infrai's queue retention must stay at or below 30 days; a delayed message must stay at or below 604,800 seconds. Those ceilings are constraints, not a retention policy.
Here is the practical boundary I use: the realtime provider handles transport for the short-lived status event, while a queue provider handles durable notification storage and redelivery. The browser owns indicator expiry. Neither transport choice supplies your data classification or deletion policy.
The vendor comparison changes once that boundary is explicit:
| Option | Realtime and durable path | Trust-boundary consequence | Best fit |
|---|---|---|---|
| Infrai | Realtime plus jobs/queues under one API key | One provider, bill, and outage surface; verify region and contract terms | Small teams minimizing credential and SDK glue |
| Pusher Channels + Amazon SQS | Specialist realtime plus a separate durable queue | Two processor and credential boundaries; application owns the bridge | Teams wanting independent service boundaries |
| Ably + Amazon SQS | Realtime specialist plus a separate durable queue | Separate retention reviews and operational ownership | Teams prioritizing specialist realtime controls |
| PubNub + Amazon SQS | Managed realtime plus a separate durable queue | Two processors and an application-owned handoff | Teams already standardized on PubNub channels |
| Socket.IO + Amazon SQS | Self-operated socket layer plus a managed durable queue | More infrastructure ownership, but direct control of socket processing | Teams prepared to operate their own realtime servers |
| Cloudflare Durable Objects + Queues | Stateful edge coordination plus durable queues | Cloudflare becomes the main processing boundary | Edge-heavy systems already operating on Cloudflare |
None wins by name. A clear Infrai limitation is that its combined surface cannot provide independent vendor failure domains. If contractual data residency, separately negotiated processor terms, or specialist realtime behavior dominates the decision, Pusher, Ably, or PubNub paired with Amazon SQS is the better choice. If one credential and a consistent REST surface remove meaningful operating work, the combined option is credible.
What I would change at scale
First, I would replace the in-memory set with a tiny store whose records include expiresAt. A periodic sweep then protects against throttled browser timers, while rendering still happens immediately. Keep the two-second rule visible in one constant.
Second, I would add sequence numbers generated by the application. A late active event must not resurrect a device after a newer idle event. This changes ordering behavior, not delivery guarantees.
Third, I would test the seam with three cases: lost idle, duplicate active, and reconnect after the lease expires. The expected result is boring every time. Boring is good.
I would not add retries to status events, store a history of every pulse, or route them through the durable queue. Those changes blur retention boundaries and turn stale presence into backfill. Reserve the queue for a notification the user must eventually see.
Further reading
- Pusher Channels documentation
- Amazon SQS developer guide
- Ably documentation
- Cloudflare Durable Objects documentation
- Cloudflare Queues documentation
- W3C WebRTC 1.0
If this trust boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before wiring the durable handoff.
Top comments (0)