A disconnect event should start a 30-second host lease, not trigger an immediate promotion. Let the same player reclaim the fintech review room with a fresh scoped token during that window; after expiry, elect the next eligible participant with one compare-and-swap operation, then backfill every client from an ordered event log. This rule separates a noisy transport signal from the durable authority decision.
Short answer: Express can receive the disconnect and reconnect commands, but a shared transactional store must own the lease, membership generation, and event sequence. Treat retries as normal. A request ID makes each mutation idempotent, while the generation prevents an old socket from reclaiming a newer room incarnation.
How should Node.js handle a player disconnect and reassign the lobby host?
A WebRTC connection exposes connection-state changes, including disconnected, failed, and closed. Those states are useful signals, but they do not prove that a person has abandoned a room. A laptop can change networks, a browser can suspend, or signaling can briefly disappear while the media path changes. The W3C specification defines the state machine; it does not define application-level host succession.
That distinction matters in a fintech video review. The host may be allowed to admit a second analyst or end a recorded session. Promoting someone on the first close notification can leave the returning host and the promoted host holding conflicting views of authority. The token must not settle that dispute: it should carry the authority already committed by the room state.
The operational invariant is compact: one live room generation has at most one active host lease. Everything else follows from it.
Use a lease, a generation, and an append-only event
Store membership independently of transient connections. A member record identifies the player and eligibility; a connection record may come and go. The room record carries generation, host_id, lease_until, and event_seq. The server advances event_seq whenever it commits an authority change.
For a new room, issue a scoped token only after the membership and generation are committed. The token should identify the room, member, role, generation, and expiry. Keep it short-lived, verify its signature and audience at the boundary, and reject a token whose generation no longer matches the room. Those are design requirements, not claims about the media protocol.
The handoff transaction has four checks:
- The room generation still equals the caller's generation.
- The observed host and lease still match the values read by the worker.
- The lease has expired according to the store's time source.
- The candidate remains connected and eligible under the room's policy.
Then the transaction updates the host, increments the event sequence, and appends one host.changed event. A unique request ID returns the prior result when Express or a queue worker retries the operation. No duplicate promotion.
Here is the service boundary I use for that critical section. The in-memory implementation is deliberately omitted because a process-local mutex cannot coordinate two Express replicas.
package lobby
import (
"context"
"errors"
"time"
)
var (
ErrLeaseActive = errors.New("host lease is still active")
ErrStaleGeneration = errors.New("room generation changed")
)
type HandoffCommand struct {
RoomID string
ExpectedGeneration uint64
ExpectedHostID string
RequestID string
Now time.Time
}
type HostChanged struct {
RoomID string
HostID string
Generation uint64
Sequence uint64
}
type RoomStore interface {
PromoteAfterLease(context.Context, HandoffCommand) (HostChanged, error)
}
type Service struct { Rooms RoomStore }
func (s Service) Reassign(ctx context.Context, cmd HandoffCommand) (HostChanged, error) {
if cmd.RoomID == "" || cmd.RequestID == "" || cmd.ExpectedHostID == "" {
return HostChanged{}, errors.New("missing handoff identity")
}
return s.Rooms.PromoteAfterLease(ctx, cmd)
}
The database operation behind PromoteAfterLease must make the room update, idempotency record, and outbox event atomic. SQL row locking or a conditional update can provide that property. A queue consumer may publish the outbox record more than once, so consumers also deduplicate by room generation and sequence.
This design has a real trade-off: it adds a durable store, an outbox, and clock-based lease handling to a problem that looks local inside one Node.js process. It does not fit a disposable lobby where duplicate authority has no consequence and all participants share one server. In that smaller case, a single-process state machine is easier to operate. Once replicas, scoped credentials, or privileged host actions enter the room, the simpler design stops providing a defensible authority boundary.
How reconnect and backfill fit together
Reconnect is a claim against the current generation, not permission to overwrite it. The client presents its member identity, room generation, last applied sequence, and a one-use reconnect nonce. The server verifies the claim, attaches the new connection, and returns a freshly scoped token. If the original lease is still active, the returning member remains host. If a handoff already committed, the member returns as a non-host participant.
Backfill closes the gap. The server returns all authorized room events after the client's last sequence, followed by the current snapshot version. The client applies only the next sequence and requests another backfill when it sees a gap. It must never infer host status from message arrival order.
A practical response can stay small:
type ResumeRequest struct {
RoomID string `json:"room_id"`
MemberID string `json:"member_id"`
Generation uint64 `json:"generation"`
AfterSequence uint64 `json:"after_sequence"`
Nonce string `json:"nonce"`
}
type ResumeResponse struct {
ScopedToken string `json:"scoped_token"`
CurrentHost string `json:"current_host"`
Generation uint64 `json:"generation"`
ThroughSequence uint64 `json:"through_sequence"`
Events []HostChanged `json:"events"`
}
Do not put broad account permissions in that token. The video-room credential needs only the room and actions required for this session. Logging the token is also out; log its key identifier, subject, room, generation, and expiry instead.
Failure signals and the runbook
Page on violated outcomes, not individual disconnects. Useful service-level signals include rooms reporting more than one current host, handoff latency beyond the lease plus processing allowance, backfill gaps that persist after retry, rejected stale-generation reconnects, and outbox age. Split metrics by deployment version and store region so a rollback decision has evidence behind it.
The sharp edge is concurrent evidence: the old connection disappears, the reconnect lands on another replica, and the lease-expiry worker fires at nearly the same time. I initially want to serialize those HTTP handlers. That is the wrong boundary. The correction is to serialize the room mutation in the shared store and make both paths conditional on the same generation and lease values.
Keep the alert payload useful: room identifier, generation, expected and actual host, request ID, last sequence, and lease deadline. Exclude tokens and customer financial data. An operator should be able to answer one question quickly: did authority diverge, or is a client merely behind?
Verification before rollout and rollback after it
Test the state machine with controlled time. Start with a host disconnect, reconnect at 29 seconds, and verify there is no promotion. Then reconnect after expiry while a promotion races; exactly one transaction should win, and every successful resume should converge on its result. Repeat the same request ID, deliver the outbox event twice, skip an event during client delivery, and restart the worker between commit and publish.
Also test generation rollover. A delayed reconnect from a destroyed room must not enter a newly created room that reused the visible lobby code. This case is easy to miss because the identifiers look familiar while the authority epoch is different.
Roll out behind a server-side policy switch by a small cohort of rooms, but preserve a single writer for each room throughout the change. Compare duplicate-host invariant violations, reconnect success, handoff latency, and backfill completion with the prior policy. Rollback should stop new lease-based decisions while continuing to read and publish already committed events; deleting those events would strand clients at sequence gaps.
The final check is boring by design: expire the lease, commit one successor, mint credentials from that committed state, and prove every reconnect can replay to the same sequence. Boring survives pages.
References
- W3C, WebRTC 1.0: Real-Time Communication Between Browsers: https://www.w3.org/TR/webrtc/
Top comments (0)