DEV Community

EthanBrooks1647
EthanBrooks1647

Posted on

Reliable Realtime Recovery for Collaborative Whiteboard Updates — An Architecture Record

Short answer: for realtime error recovery, design reliable collaborative whiteboard updates as ordered, replayable events instead of a fresh subscription. The client should resume from its last acknowledged sequence, receive a snapshot plus a delta window when that point is too old, and apply each operation idempotently.

This decision is about preserving user intent when a laptop sleeps, a classroom Wi-Fi network changes, or a mobile browser drops a tab in the middle of a stroke. Typing indicators and read receipts have a different tolerance: they can expire, while a shape move or erase cannot. That distinction belongs in the protocol, not in a best-effort UI patch.

How should a collaborative whiteboard recover realtime updates?

Start by writing down invariants. A board has a monotonically increasing server sequence, each client operation has a stable operation ID, and the server acknowledges an operation only after it is durable in the board log. A reconnect request carries the last sequence the client applied and the IDs it has submitted but not yet seen acknowledged. The server then chooses one of two paths: replay deltas after that sequence, or send a fresh snapshot followed by deltas from the snapshot boundary.

The recovery boundary is explicit. If the requested sequence has been compacted, the server must not pretend that an empty replay means success. It sends a snapshot with its sequence, and the client replaces local materialized state before applying later events. This keeps a seven-hour offline client from silently drawing on an obsolete base.

That is the contract.

Here is the decision record I use for a board that supports undo, cursors, and presence:

Option Reconnect behavior Failure boundary Appropriate use
Replay-only log Replay from the client sequence Fails when history is compacted Short disconnects with a generous log window
Snapshot plus delta Install a known state, then replay Snapshot transfer can be large Long disconnects and compacted history
Last-write-wins document Fetch current document Loses concurrent intent Simple notes where conflicts are acceptable
Operation-based merge Reapply unique operations Requires conflict rules and tombstones Boards with concurrent edits and offline work

The table is deliberately unglamorous. Reliability is choosing the failure you can explain to a user.

The critical path: acknowledge, reconnect, backfill

An event envelope needs enough metadata to detect gaps and duplicates. The payload can be a JSON patch, a vector operation, or a domain command; the recovery contract stays the same. In this example, the server is an abstract interface so the code does not smuggle in a vendor-specific route.

from dataclasses import dataclass


@dataclass(frozen=True)
class Event:
    seq: int
    op_id: str
    kind: str
    payload: dict


def recover(client, board_id, last_seq, pending_ids):
    """Return an ordered stream beginning at the client's applied state."""
    reply = client.resume(
        board_id=board_id,
        after_seq=last_seq,
        pending_op_ids=sorted(pending_ids),
    )

    if reply.snapshot is not None:
        client.replace_state(reply.snapshot.state)
        client.last_seq = reply.snapshot.seq

    for event in reply.events:
        if event.seq <= client.last_seq:
            continue  # duplicate delivery after a reconnect
        if event.seq != client.last_seq + 1:
            raise RuntimeError("sequence gap; request a new snapshot")
        client.apply_once(event.op_id, event.kind, event.payload)
        client.last_seq = event.seq

    client.persist_cursor(client.last_seq)
    return client.last_seq
Enter fullscreen mode Exit fullscreen mode

apply_once must be backed by an operation-ID ledger or an equivalent transactional check. A cursor update written separately from the shape mutation can acknowledge work that was never applied after a crash. I once diagnosed a whiteboard that looked correct until refresh: the browser persisted the sequence first, then lost power during the shape write. The visible symptom was a missing rectangle, not a socket error, so metrics initially missed it. We had to compare the operation ledger, browser cursor, and server sequence for the same board, then replay the missing command against a copy before touching production state. That extra forensic path is expensive, but it turns a vague “someone lost a drawing” report into a bounded repair.

Keep transient presence outside the durable board log. A typing indicator should have a lease and a timestamp; a read receipt should be monotonic per reader and document version. Replaying either forever creates stale UI. Shape operations, comments, and permissions need durable ordering; presence needs expiry.

What failure modes should the protocol make observable?

Reconnect is a state machine, not a single retry. Record transitions such as connected, auth_failed, backing_off, snapshot_required, replaying, and ready. Emit the board ID, client sequence, server sequence, replay count, snapshot bytes, and operation IDs as structured fields with privacy-safe hashing. A rate-limit response should increase backoff; an authorization response should stop retries and surface a clear action to the user.

Bound the work. A delta window can be capped by event count and bytes, and snapshot downloads need cancellation when the user leaves the board. Exponential backoff with jitter prevents a classroom full of browsers from reconnecting in lockstep. Your mileage may vary on the exact cap; measure the 95th-percentile board size and the slowest supported network before selecting it.

Test the seams deliberately: drop packets after an acknowledgment, duplicate every third event, reorder two events, compact history during backfill, and kill the client between applying an operation and persisting its cursor. A property test should assert that applying the same operation IDs twice yields the same materialized board. A canary should compare the server's final sequence with the client cursor after recovery.

The rejected option and its valid use case

The rejected design is “just fetch the latest document on reconnect.” It is easy to ship, but it overwrites offline intent and cannot explain why a peer's erase disappeared after another peer moved the same object. The catch is that snapshot-plus-delta needs storage, retention, and migration work; it is not suitable when the product deliberately permits one writer and discards intermediate history. Stick with a latest-document fetch for a single-author sketchpad where the server is the only writer and that loss is acceptable.

For a collaborative whiteboard, choose the simplest protocol that preserves your stated invariants. If offline edits are out of scope, disable local mutation while disconnected and use snapshot plus delta. If offline editing is required, invest in operation IDs, conflict semantics, and tombstones before optimizing transport. WebRTC can carry peer media and data, but its reliability modes do not replace application-level ordering, authentication, or durable backfill; those remain responsibilities of the board service.

References

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev •

The explicit snapshot boundary is important because an empty replay is otherwise ambiguous. I would add a crash-consistency test that kills the client between applying an operation and persisting its cursor, then repeats it in the opposite order. The invariant should be that recovery either reapplies an idempotent operation or requests a snapshot, never advances past a mutation that was not materialized.