115 lines
5.1 KiB
Markdown
115 lines
5.1 KiB
Markdown
# 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<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
|
|
|
|
```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.
|