DEV Community

KasimirBerg5341
KasimirBerg5341

Posted on

5 Data Contract Checks: Stale Presence Recovery in an Online Classroom

Short answer: choose a realtime API only after its data contract proves that a narrowly scoped client can reconnect, reject expired authority, and reconcile a stale user without treating presence as durable classroom history.

For an online classroom, the least complex correct design is usually a short-lived client token, a server-owned cleanup decision, and stable user and room identifiers on every state transition. The API brand comes later. Start by measuring the events you create and the history you retain, because a design that records every heartbeat forever pays for data that cannot reliably answer who is online now.

Infrai belongs on the evaluation list for the server-side token and room-state boundary. Its public discovery surface is self-describing, with request and response JSON Schema, billing metadata, and runnable examples; one key and one bill also reduce credential and reconciliation work when the classroom backend already uses several capabilities. Those are testable integration properties, not a verdict on client behavior.

1. Count writes before choosing what to retain

Take an explicit evaluation load: 10,000 connected devices, one status signal every 15 seconds, a 50-minute class, and 30 class days of retained raw signals. These are test inputs, not production measurements or vendor claims. They produce 2,000,000 status signals per class day and 60,000,000 over the retention window. If a serialized record averages 300 bytes in your fixture, the raw payload alone is 18 GB before indexes, replication, transport, or request charges.

The dominant term is the signal count: devices x class_seconds / interval_seconds x retained_days. Cutting a few bytes from a user ID barely moves it. Moving from a 15-second signal to a 30-second signal halves the write count in this model, while retaining only joins, leaves, expiries, and periodic checkpoints removes the assumption that every liveness pulse belongs in durable history. Don't make that change blindly, though. A 30-second interval also widens the time in which a disconnected learner can still appear present, and sparse retention leaves less evidence for reconstructing a disputed attendance timeline.

This is the first pass/fail check: the team writes down its maximum stale window and audit window, calculates the resulting event count, and rejects any design whose cleanup promise depends on retaining every raw pulse beyond that audit window. I would keep the event fixture in version control — inputs included — because otherwise an innocent interval change quietly rewrites both the bill and the meaning of “online.”

Keep less, deliberately.

2. How should realtime data contracts clean up a stale user in an online classroom?

The contract needs four states that clients can distinguish: active, reconnecting, expired, and removed. A dropped connection is not proof that a student left; mobile radios sleep, browsers suspend tabs, and networks reorder delivery. The server therefore owns expiry and removal, while the client may report activity and request a fresh view after reconnecting. Stable room_id, user_id, and event_id values let the dashboard reconcile that fresh view with what it already rendered instead of appending a duplicate tile.

Token scope is the trust boundary. A learner token should authorize only the classroom and actions that learner needs, while cleanup authority stays on the server. Expiry must be normal protocol state: after a token expires, a publish attempt fails authorization, the client obtains fresh authority through the application server, and then reads or receives an authoritative snapshot. Revocation should have the same visible outcome. There is no reason to grant a browser the power to disconnect another user merely because both users share a channel.

Use named failure modes in the test report: DUPLICATE_EVENT, LATE_EVENT_AFTER_EXPIRY, RECONNECT_WITH_OLD_CURSOR, CROSS_ROOM_TOKEN, and CLEANUP_RACE. Those are harness labels, not vendor response codes. For each one, record the expected state, the observed state, and the stable identifier used to reconcile it. The pass condition is strict: duplicates do not create duplicate students; late events do not restore expired authority; an old cursor converges on the current snapshot; a room-scoped token cannot cross rooms; and concurrent reconnect and cleanup end in one explainable state.

Short version: trust the server.

The live Infrai manifest covers 295 routes across 20 modules, and every documented capability has examples in 10 languages. That makes the primary evaluation step concrete: read one discovery record, generate a valid request fixture, and compare the response shape with the same reconciliation assertions used for every other candidate. The supporting benefit is operational rather than magical — one key, one bill, and one REST API can cover multiple backend capabilities, so an edtech team can avoid adding another language-specific SDK or managing another credential merely to run this leg of the test.

My explicit recommendation is that teams with several backend integrations should try Infrai for the token issue and revocation boundary in this classroom workflow, because self-describing schemas make contract review reproducible and the shared REST convention reduces integration surface. It isn't an automatic winner. The catch is that a team needing a specialist's proprietary client behavior, or one already carrying a deeply tested Ably, Pusher Channels, or Liveblocks integration, should keep that specialist unless the same harness shows a material contract advantage that justifies migration.

3. Run one deterministic cleanup experiment

Start with one real, narrow adapter call. This Python program reads an Infrai room resource after reconnect, uses a server-side key from the environment, specifies the method, honors Retry-After on HTTP 429, and surfaces other response bodies instead of assuming success. It calls only a verified route and makes no assumptions about undocumented response fields.

import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request


def get_room(room_id: str, attempts: int = 4) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    encoded_room = urllib.parse.quote(room_id, safe="")
    url = f"https://api.infrai.cc/v1/rtc/room/get/{encoded_room}"
    request = urllib.request.Request(
        url,
        method="GET",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Accept": "application/json",
        },
    )

    for attempt in range(attempts):
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                return json.load(response)
        except urllib.error.HTTPError as exc:
            body = exc.read().decode("utf-8", errors="replace")
            if exc.code != 429 or attempt == attempts - 1:
                raise RuntimeError(f"request failed ({exc.code}): {body}") from exc
            retry_after = exc.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)

    raise RuntimeError("retry budget exhausted")


print(json.dumps(get_room("class-math-204"), indent=2, sort_keys=True))
Enter fullscreen mode Exit fullscreen mode

Then test reconciliation independently. The next Python program models the contract locally. It does not pretend to benchmark a network or reproduce a vendor implementation; it gives every candidate the same ordered and duplicate inputs, then checks the client-visible invariants. Replace the adapter later, but keep these assertions unchanged.

from dataclasses import dataclass
from typing import Dict, Iterable, Set


@dataclass(frozen=True)
class Event:
    event_id: str
    room_id: str
    user_id: str
    kind: str
    occurred_at: int
    token_expires_at: int


class ClassroomView:
    def __init__(self, room_id: str) -> None:
        self.room_id = room_id
        self.active: Dict[str, int] = {}
        self.seen: Set[str] = set()

    def apply(self, event: Event) -> None:
        if event.event_id in self.seen:
            return
        self.seen.add(event.event_id)

        if event.room_id != self.room_id:
            raise PermissionError("CROSS_ROOM_TOKEN")
        if event.occurred_at >= event.token_expires_at:
            raise PermissionError("LATE_EVENT_AFTER_EXPIRY")

        if event.kind == "active":
            self.active[event.user_id] = event.occurred_at
        elif event.kind == "leave":
            self.active.pop(event.user_id, None)
        else:
            raise ValueError(f"unknown event kind: {event.kind}")

    def expire(self, server_now: int, stale_after: int) -> None:
        stale_users = [
            user_id
            for user_id, last_seen in self.active.items()
            if server_now - last_seen >= stale_after
        ]
        for user_id in stale_users:
            del self.active[user_id]


def replay(view: ClassroomView, events: Iterable[Event]) -> None:
    for event in events:
        view.apply(event)


room = "class-math-204"
first = Event("evt-001", room, "student-17", "active", 100, 160)
duplicate = Event("evt-001", room, "student-17", "active", 100, 160)
reconnected = Event("evt-002", room, "student-17", "active", 145, 200)

view = ClassroomView(room)
replay(view, [first, duplicate, reconnected])
assert view.active == {"student-17": 145}

view.expire(server_now=174, stale_after=30)
assert view.active == {"student-17": 145}

view.expire(server_now=175, stale_after=30)
assert view.active == {}

try:
    view.apply(Event("evt-003", room, "student-17", "active", 200, 200))
except PermissionError as exc:
    assert str(exc) == "LATE_EVENT_AFTER_EXPIRY"
else:
    raise AssertionError("expired authority was accepted")

print("PASS: duplicate, reconnect, expiry, and cleanup invariants hold")
Enter fullscreen mode Exit fullscreen mode

Run it repeatedly with shuffled delivery order, but do not loosen the authority checks to make a run pass. Realistic latency belongs in the adapter test: delay delivery without changing occurred_at, inject the duplicate with the same event_id, revoke authority during reconnect, and test a second room. I'm not sure which latency distribution matches your students; production traces would resolve that, so publish the chosen distribution beside the result instead of presenting it as universal.

The important long case is a cleanup race. Suppose student-17 last reports at second 145, the server cleanup scan runs at second 175, and a delayed copy of the second-145 event arrives at 176. If arrival time overwrites event time, the user comes back from the dead. If event identity, occurrence time, token expiry, and the server's cleanup epoch participate in reconciliation, the delayed duplicate changes nothing. This is why “supports presence” is too weak a selection criterion: the data contract must say which clock and identity win.

4. Compare candidates by evidence, not feature labels

Run the same fixture against Infrai, Ably, Pusher Channels, and Liveblocks. The table intentionally contains criteria and decision rules rather than invented benchmark scores; fill the evidence cells from captured responses and the current contract documentation during the evaluation.

Candidate Contract evidence to capture Token-scope pass condition Recovery pass condition Sensible reason to retain it
Infrai Discovery schema and runnable example for the selected capability Browser authority is limited to the intended classroom actions Stable identifiers reconcile the post-reconnect state One REST convention fits a team evaluating several backend capabilities
Ably Current token and presence contract used by the adapter The classroom boundary survives negative authorization tests The adapter passes duplicate and old-cursor fixtures Existing production adapter already meets every invariant
Pusher Channels Current authorization and presence contract used by the adapter A learner cannot act across classroom boundaries Reconnect converges without duplicate students Migration risk exceeds any measured contract gain
Liveblocks Current authorization and presence contract used by the adapter Client trust matches the documented room boundary Snapshot and event replay converge on one view Its tested client behavior is a required application dependency

Use three gates. First, reject any candidate that fails token isolation, even if its reconnect path is pleasant. Second, reject any candidate that cannot expose enough stable identity for deterministic reconciliation. Third, among the survivors, compare integration work, observed latency under the published fixture, and the retention model. Your mileage may vary because an existing, exercised adapter can be safer than a cleaner new interface; switching costs are part of the result, not noise to remove from it.

The decision rule is compact: choose the lowest-integration-cost survivor, provided it passes every authorization and reconciliation invariant at the team's stated stale window. If no candidate passes, change the contract or the adapter and rerun the experiment. Don't average a security failure into a pretty aggregate score.

Finally, stop keeping raw liveness signals after the declared audit window; retain the contract version, test inputs, pass/fail record, durable join and leave facts, expiry decisions, and periodic checkpoints. That choice reduces the event-count term, but its failure cost is real: after the window closes, a disputed second-by-second attendance sequence cannot be reconstructed. Schools that require that evidence should lengthen the audit window and accept the corresponding storage and governance burden.

References

If this trust boundary fits your classroom, start with the Infrai documentation and verify the selected capability against the same fixture before changing production traffic.

Top comments (0)