DEV Community

SuttonHawkins6723
SuttonHawkins6723

Posted on

Presence Truth After a Browser Drop: Cursor Recovery for Shared Team Sidebars

The operational constraint changes the design: a team presence sidebar must not turn a brief browser disconnect into a false statement about who can take the next customer handoff. Short answer: keep the server authoritative, reconcile from a snapshot after every reconnect, and apply only events newer than a server cursor. Scope the reconnect token to one team and the exact client actions; a live socket is not proof that its view is current.

The incident pattern behind stale sidebars

I have been paged for missed jobs and duplicate deliveries in production, so I assume a browser tab will reconnect at the worst possible moment. In an e-commerce support queue, an agent can close a laptop during a handoff, wake it on another network, and open a video room with a token that was minted for the wrong team. The presence panel and the room then disagree about who is available. That is one incident path, not two separate UI bugs.

The failure pattern is predictable. The client keeps a local map, receives events with no ordering boundary, and marks a person online when any delayed packet says so. Imagine event 104 saying Alex is online, followed by event 105 recording offline while the tab is disconnected. When the tab returns after a 47-second Wi-Fi gap, a delayed copy of 104 can arrive from the old stream and paint Alex online again. The map looks plausible while being wrong. Plausible is dangerous during a handoff.

The invariant I want is narrower: the server owns membership, presence timestamps, and room authorization; the client owns rendering and a short-lived cache. A reconnect invalidates that cache. It does not extend it.

That distinction is the whole design.

What should a browser reconnect do for reliable presence updates?

Use a monotonic connection epoch and a server-issued cursor. On connect, authenticate a token that names the team, user, and allowed actions. Fetch a snapshot for that team, then subscribe with the snapshot cursor. Events older than that cursor are ignored. Events from a prior epoch are ignored too.

The exact transport can vary. WebSocket is a common choice for bidirectional updates; Server-Sent Events is reasonable when the browser only consumes server changes. WebRTC data channels solve a different problem and bring peer-connection lifecycle concerns. None of these transports supplies application-level truth about presence or decides whether a client may join a video room.

Here is the state-machine shape in Go service code. The browser implementation can use the same transitions even though its runtime is JavaScript.

package presence

import "sync"

type Event struct {
    Team   string
    User   string
    Status string
    Seq    uint64
}

type Store struct {
    mu    sync.Mutex
    epoch uint64
    seq   uint64
    users map[string]string
}

func (s *Store) Reconcile(snapshot map[string]string, cursor uint64) uint64 {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.epoch++
    s.users = snapshot
    s.seq = cursor
    return s.epoch
}

func (s *Store) Apply(event Event, epoch uint64) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    if epoch != s.epoch || event.Seq <= s.seq {
        return false
    }
    s.users[event.User] = event.Status
    s.seq = event.Seq
    return true
}
Enter fullscreen mode Exit fullscreen mode

The rejection path is the useful path. A duplicate event is harmless, and a late event cannot roll the map backward. The server should make Seq monotonic per team; if it cannot retain a requested cursor, it should return a fresh snapshot marker and force reconciliation. I am not sure every hosted transport exposes the same cursor semantics, so verify that contract before choosing one.

How do token scope and client trust shape a video-room reconnect?

Presence data is often treated as harmless UI state until it reveals a private team roster or an agent's working hours. A token copied from browser storage should therefore be useful only for the current team and the minimum operations needed by the sidebar. For the support video room, mint a separate short-lived token that names the room, participant, and permitted actions. Do not put an account-wide secret in a reconnect URL.

The server should validate, on every connection and room operation:

  • subject and team membership;
  • expiry and an audience that identifies the realtime service;
  • allowed actions, such as presence:read or room:join, rather than a broad admin role;
  • origin and rate limits appropriate for a browser client.

Keep tokens short-lived. On expiry, obtain a replacement through the normal authenticated session, then reconnect and reconcile. Do not silently widen scope to make a failed reconnect pass. The sidebar may show “reconnecting” while the token is renewed; that uncertainty is more honest than displaying a stale green dot beside a room button.

Replay tests that catch the expensive failures

Test transitions, not just a green connection. Turn off the network while an event is in flight. Sleep a laptop across token expiry. Deliver event 12 after event 13. Open two tabs for the same user, and close one while the other remains active. Start a room join with a token for team A, then attempt the same join against team B. The expected result is deterministic: one server presence record, idempotent updates, and a sidebar that shows “stale” or “reconnecting” instead of inventing certainty.

Instrument the state machine with connection epoch, last accepted sequence, snapshot age, reconnect count, and time spent outside the fresh state. Sample user and team identifiers rather than logging tokens or full roster payloads. Alert on a rising stale duration, not on every individual disconnect; mobile networks make brief disconnects normal.

Backoff needs a ceiling and jitter. A practical starting point is exponential delays from 250 ms to 30 seconds, capped and reset only after a successful snapshot plus subscription. The numbers are policy, not a protocol guarantee; tune them against rate limits and the cost of stale presence.

When is snapshot-plus-cursor the wrong operational choice?

This approach is a poor fit when the sidebar is merely decorative and can be minutes out of date; polling may be easier to operate. It is also insufficient for authoritative access control, financial status, or a workflow that requires a transactional lock. Use the system that owns those decisions and render its result here.

The catch is operational ownership. You need a durable sequence source, a bounded snapshot path, and dashboards for stale clients. If the team cannot run those pieces, choose a simpler polling contract and make the freshness limit visible. A transport with more features does not remove that work.

I would ship the state machine only after replay tests prove that reconnects converge to the server snapshot and that a stolen token cannot read another team or join its room. The sidebar is a view. The server remains the record.

References

Top comments (0)