# 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 ```csharp BrowseSessionsRequest filter = new() { GameId = new("space-game"), EnvironmentId = new("production"), ProtocolVersion = 7, RegionId = new("eu-central"), ExcludeFull = true, }; RendezvousClientResult snapshot = await browser.BrowseAsync(filter, cancellationToken); await foreach (RendezvousClientResult 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 ```bash 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.