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: nois also emitted), compression, transformation, and caching fortext/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.