Files
Rendezvous/docs/integration/live-session-updates.md
KyuubiYoru 06c4ecf8f3
quality-gate / quality (push) Failing after 1m47s
quality-gate / container (push) Has been skipped
feat(browser): stream bounded live session updates (#26)
2026-07-16 23:25:48 +02:00

5.1 KiB

Live session-list updates

Tracking: #26

Live updates are an optional acceleration for an open server browser. The bounded GET /v1/sessions snapshot remains the source of truth, and join authorization still revalidates current capacity, presence, policy, and compatibility. A displayed player count is advisory, never an admission promise.

Snapshot, stream, reset

Every BrowseSessionsResponse includes streamCursor in addition to its normal pagination cursor. Connect to GET /v1/sessions/stream with the same game, environment, protocol, optional region, and excludeFull filter. Send the most recent stream cursor as Last-Event-ID.

SSE event Contract kind UI action
session_upsert sessionUpsert Add or replace the complete public projection by listing ID.
session_remove sessionRemove Remove the listing ID.
reset reset Discard local state, fetch a fresh snapshot, then reconnect with its cursor.
keepalive keepalive Preserve the cursor and connection; do not change UI state.

Each SSE id equals the opaque cursor inside its JSON event. Cursors are signed, short-lived, monotonically ordered, and bound to the complete filter. A missing, expired, corrupted, foreign, future, or replay-gapped cursor produces reset instead of a potentially incomplete view. Do not parse or retain it as a stable identifier.

Updates cover creation after fresh UDP presence, public-field/capacity changes, presence staleness and recovery, lease expiry, deregistration, operator or principal revocation, and visibility/region/protocol changes. Events contain the same bounded public SessionListing as snapshots. They never contain raw peer endpoints, lease tokens, punch capabilities, tickets, publisher subjects, or internal store identifiers.

SDK and polling fallback

BrowseSessionsRequest filter = new()
{
    GameId = new("space-game"),
    EnvironmentId = new("production"),
    ProtocolVersion = 7,
    RegionId = new("eu-central"),
    ExcludeFull = true,
};
RendezvousClientResult<BrowseSessionsResponse> snapshot =
    await browser.BrowseAsync(filter, cancellationToken);

await foreach (RendezvousClientResult<SessionStreamEvent> update in
    browser.StreamAsync(filter, snapshot.Value!.StreamCursor, cancellationToken))
{
    if (!update.IsSuccess)
    {
        // Switch to bounded polling with jittered backoff.
        break;
    }
    // Apply upsert/remove by listing ID. On reset, discard and browse again.
}

Cancellation or enumerator disposal closes the response and releases the server subscription. A normal connection-duration close is a reconnect signal: use the last applied event cursor. Repeated failures, unsupported platform HTTP stacks, and restrictive proxies fall back to snapshots with exponential jittered backoff, a capped interval, and Retry-After. Never open parallel streams to compensate for a slow UI.

TestClient

dotnet run --project src/FinalFactory.Rendezvous.TestClient \
  --configuration Release --no-build -- \
  watch --service https://rendezvous.example.invalid/ \
  --game space-game --environment production --region eu-central --protocol 7 \
  --run-seconds 60 --json

watch.snapshot, watch.session-upsert, watch.session-remove, watch.keepalive, and watch.reconnect are stable diagnostics. Add --exercise-reset --script to corrupt the snapshot cursor deliberately and verify a typed reset plus snapshot refresh. Use --exercise-reconnect --script while producing one update to close the first stream deliberately, reconnect from its prior cursor, and verify that the same ordered event is replayed. Polished list diffing, selection retention, animation, and accessibility remain in each game.

Bounds and slow consumers

The v1 journal retains at most 4,096 public-only changes. It admits at most 256 subscribers total and 64 per tenant, reads at most 128 changes per batch, waits a configurable 50 milliseconds after a live change and coalesces the resulting batch to the final change per listing, sends a keepalive every 15 seconds, and closes a connection after five minutes. A consumer behind the replay window receives reset; it never acquires an unbounded queue.

Normal optional-work concurrency and per-source/tenant rate controls apply for the stream lifetime. Exhaustion returns typed HTTP 429 before streaming. Shutdown cancels streams; reconnect only after readiness returns and expect a reset after a single-active restart because listings and replay are ephemeral.

Reverse proxy

  • Disable response buffering (X-Accel-Buffering: no is also emitted), compression, transformation, and caching for text/event-stream.
  • Preserve Last-Event-ID; set upstream/read timeouts above the 15-second keepalive and around six minutes for the five-minute connection ceiling.
  • Flush events promptly and use HTTP/2 only when streaming semantics survive.
  • Preserve the source-IP trust boundary and abuse controls; do not add a bypass.

Verify the deployed proxy with an idle keepalive, update, reconnect, invalid cursor reset, slow reader, and graceful shutdown. An in-process pass does not prove that a production proxy is non-buffering.