Research: prior art for the six garden-path stage-1 forks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-27 06:08:40 +01:00
commit baffd774f0

View file

@ -0,0 +1,127 @@
# Stage 1 forks: prior art for six open questions (2026-10-27)
Campaign `cultmesh-garden-path`. Fills the gaps named by six questions: `browser-transport`, `tick-model`, `tick-state-commit`, `room-durability`, `player-identity`, `browser-client-store`. Facts with evidence pointers; no recommendation. Already kept and not repeated: `prior-art-2026-10-27.md`, `browser-sim-runtime-2026-10-27.md`, `witness-authoritative-networking/prior-art-2026-10-26.md`.
Tags: VERIFIED = read on the primary page in this pass. SECONDARY = third-party, forum, or search-engine summary. UNVERIFIED = looked for, not found, or not readable.
Method note: WebFetch summarises pages with a small model, so quoted numbers are as that tool returned them. Valve's wiki (403), Unreal docs (403), GDC Vault (maintenance redirect) and archive.org were unreachable; those claims are SECONDARY at best.
---
## 1. `browser-transport`
### 1.1 WebTransport browser support (today is 2026-10-27)
| Browser | WebTransport | `datagrams` | Notes |
|---|---|---|---|
| Chrome desktop | 97 | 97 | VERIFIED (MDN browser-compat-data `api/WebTransport.json`) |
| Edge | 98 | follows Chromium | VERIFIED (caniuse.com/webtransport) |
| Firefox desktop | 114 | 114 | VERIFIED (BCD) |
| Safari macOS | 26.4 | 26.4 | VERIFIED (BCD; caniuse; MDN says "Baseline 2026, Newly available", since March 2026) |
| Safari iOS | 26.4 | 26.4 | VERIFIED (BCD, caniuse) |
| Chrome Android | 156 | not separately listed | VERIFIED (caniuse only; BCD lists no Android split) |
| Firefox Android | 158 | not separately listed | VERIFIED (caniuse) |
| Samsung Internet | 18.0 | | VERIFIED (caniuse) |
- caniuse reports about 91.6 % global usage. VERIFIED (caniuse.com/webtransport).
- Sub-features that matter for a game (BCD, VERIFIED): `serverCertificateHashes` Chrome 100 / Firefox 125 / Safari 26.4. `congestionControl` Chrome none / Firefox 114 / Safari 26.4. `sendOrder` Chrome none / Firefox 119 / Safari 26.4. `reliability` Chrome 157 / Firefox 114 / Safari 26.4. `supportsReliableOnly` Chrome 157 / Firefox 158 / Safari 26.4. `createUnidirectionalStream` Firefox "partial".
- Safari 26.4's WebKit feature post does not mention WebTransport in the page read. VERIFIED (absence on webkit.org/blog/17333). Safari 26.4 shipping is confirmed by BCD and caniuse, so treat the blog omission as an incomplete read, not a contradiction.
- One third-party page claims Safari's wire implementation sits in Apple's Network.framework (so the floor is OS 26.4, not only Safari), and that WKWebView support is unconfirmed; tested on an iOS 27 beta. SECONDARY, provisional (echo.semantic-ui.com/compat via search).
- Runtime feature test: `typeof WebTransport === "undefined"`; `WebTransport.supportsReliableOnly`; instance `reliability` says whether a given session has unreliable datagrams. VERIFIED (MDN WebTransport page).
- Self-signed certificates: Kestrel docs say browsers do not allow self-signed certificates on HTTP/3, including the dev certificate. VERIFIED (learn.microsoft.com Kestrel HTTP/3 page). `serverCertificateHashes` (above) is the browser-side escape, with a short certificate validity limit (the Kestrel experimental doc's test certificate uses 14 days; the W3C rule itself was not read, UNVERIFIED).
### 1.2 Server side on .NET
- HTTP/3 in Kestrel: fully supported since .NET 7; not enabled by default (`HttpProtocols.Http1AndHttp2AndHttp3` plus HTTPS); depends on MsQuic; falls back silently if MsQuic is missing. The same text applies to the .NET 8, 9, 10 monikers; a .NET 11 moniker exists. VERIFIED (learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/http3, page updated 2026-10-08). The page does not mention WebTransport at all.
- System.Net.Quic APIs are stable from .NET 9. SECONDARY (search summary of learn.microsoft.com QUIC overview). Windows ships msquic.dll with the runtime; Linux needs `libmsquic` installed. SECONDARY (same).
- Kestrel WebTransport: experimental since .NET 7 RC1 (2022-09). Needs `EnablePreviewFeatures` plus the runtime option `Microsoft.AspNetCore.Server.Kestrel.Experimental.WebTransportAndH3Datagrams`. Implements "most of the WebTransport draft-02 specification, except for datagrams"; datagrams planned later; server cannot initiate sessions. VERIFIED (github.com/dotnet/aspnetcore docs/WebTransport.md). Browsers ship the later WebTransport drafts (draft number the browsers speak was not looked up, UNVERIFIED), so draft-02 interop with current Chrome/Firefox/Safari is not shown by any source read.
- Tracking issue dotnet/aspnetcore#42788 is open, milestone Backlog (moved from ".NET 8 Planning" 2024-02-14), no assignee; datagrams are a separate issue #42784. VERIFIED (issue page). dotnet/runtime#43641 "Add support for WebTransport" is open, milestone Future, `api-suggestion`. VERIFIED (issue page). No .NET 10 or 11 announcement of WebTransport graduating was found. UNVERIFIED that none exists.
- System.Net.Quic datagram surface: a third-party package note says System.Net.Quic does not surface msquic datagrams. SECONDARY (nuget package page via search). No dotnet/runtime issue for it was found. UNVERIFIED.
- Maintained libraries: the only C# WebTransport package found is `Demiurgos.WebTransport.Client` 0.2.1 (2026-06-09), client only, Windows x64 preview, native MsQuic backend, has datagram send/receive; Linux, macOS, WebGL, mobile "not yet". VERIFIED (nuget.org page). No C# WebTransport server library usable with browsers was found. UNVERIFIED that none exists.
- Non-.NET reference point: Colyseus ships WebTransport as `@colyseus/h3-transport` on `@fails-components/webtransport` (Node), marked experimental, "hasn't been battle tested", "not widely available yet". VERIFIED (docs.colyseus.io/server/transport/webtransport).
### 1.3 What shipped browser games use
- Colyseus: WebTransport is its only transport with an unreliable channel (QUIC datagrams); `.unreliable()` schema fields and `room.input({mode:"unreliable"})` (redundant recent inputs) use it; on WebSocket those fields stop updating after the initial sync, and unreliable input falls back to the reliable channel (redundancy wasted). Client WebTransport is JS/TS SDK only. VERIFIED (same Colyseus page).
- Rune (mobile/web multiplayer game platform) engineering post compares WebSockets and WebRTC data channels, says TCP head-of-line blocking needs UDP, "in the current web browser, that means WebRTC", and that WebTransport "isn't ready yet". Its own choice is not stated; no loss measurements. VERIFIED (developers.rune.ai/blog/webrtc-vs-websockets-for-multiplayer-games). The post is undated in the fetch.
- Safari 26.0 added WebSocket over HTTP/2 and HTTP/3. VERIFIED (webkit.org features-in-safari-26-4 page, mentioned in its HTTP section).
- No source found documenting which transport named .io games (agar.io, slither.io, krunker) use, nor any measured WebSocket stall under loss for them. UNVERIFIED. General guides recommend WebSocket for server-authoritative games needing wide support, WebRTC data channels for unreliable traffic (signalling and TURN cost), WebTransport as narrower-support newcomer. SECONDARY (search summaries of ably.com, openreplay.com, rune).
---
## 2. `tick-model`
Question's own framing: every framework already read uses one fixed simulation tick. Below: what varies per system, entity, or client.
- Unreal: `NetUpdateFrequency` is a per-actor maximum replication rate in Hz; docs give typical values 10 for important player-controlled actors, 5 for slower AI, 2 for background actors. `MinNetUpdateFrequency` is the floor when adaptive update is on (`net.UseAdaptiveNetUpdateFrequency=1`, off by default in the 4.27 docs; a forum thread says it became default-on at 4.16 then regressed latency for some). Ramp-down happens when no replicated property changed. SECONDARY (search summary of docs.unrealengine.com 4.27 Actor Properties and dev.epicgames.com; the pages themselves returned 403 or off-topic). The server simulation still ticks at one rate; only the replication cadence is per actor. Server tick rate (`NetServerMaxTickRate`) relation: UNVERIFIED.
- Netcode for Entities (Unity): snapshots are sent at `NetworkTickRate`, which "can differ from `SimulationTickRate`"; when lower, connections are split into round-robin subsets sent on successive ticks. `MaxSendRate` is per ghost prefab, "only denotes the maximum possible replication frequency, and cannot be enforced in all cases", capped by the tick rate (100 Hz cap on a 60 Hz default stays 60; 45 Hz on 30 Hz rounds down to 30). Importance (per chunk, grows with `ticksSinceLastSent`) picks what fits in bandwidth; `GhostImportance.BatchScaleImportanceFunction` can scale per chunk and per connection. VERIFIED (docs.unity3d.com/Packages/com.unity.netcode@1.8/manual/ghost-snapshots, fetched; plus search summaries of the 1.4 to 1.9 API pages). Fixed: one simulation tick. Varies: per-prefab and per-connection send cadence.
- Photon Fusion 2: tick rate and send rate were locked to 32 and 16 in Fusion 2.0; from 2.1 tick rate is selectable 8 to 256 (Shared mode capped at 32); send rate is an integer interval 1 to 8 ("1 sends every tick, 2 every second tick") and only divisors of the tick rate are offered; server and client send rates are separate settings. VERIFIED (doc.photonengine.com/fusion/current/manual/network-project-config). Replication features: scheduling (default) raises a culled object's priority for the next outgoing tick; optional Area of Interest. VERIFIED (same page). One simulation tick; send is a divisor of it. This is the "fixed-with-divisors" shape for sending, applied to snapshots rather than to systems.
- Valve Source: server simulates at the tick interval (commonly 66 or 100 Hz in community servers; default 66 for older titles is not verified); client sends usercmds at `cl_cmdrate` (a wiki line says usually 30) and requests snapshots at `cl_updaterate` (default 20); the server "will never send more updates than simulated ticks" nor above the client request or `sv_maxupdaterate`. SECONDARY (search summaries of developer.valvesoftware.com Source Multiplayer Networking; page returned 403; defaults differ between community sources). Fixed: server tick. Varies: per-client command and update rates.
- Overwatch GDC 2017 (Timothy Ford): the talk page confirms topic only. A secondary article says the simulation runs on fixed 16 ms command frames, 7 ms in tournament mode; a forum summary says the client runs ahead of the server by about half RTT plus one command frame. SECONDARY (edgegap.com blog; gamedev.net thread; GDC Vault page abstract only). Talk slides not read. UNVERIFIED for any per-system rate.
- Nakama and Colyseus (already kept): one developer-set tick.
- Per-system independent timelines with prediction and reconciliation: none found in this pass. UNVERIFIED that none exists. In every case read above, what is allowed to vary is the send/replication cadence per object, prefab, or client, derived from one simulation clock.
---
## 3. `tick-state-commit`
- SpacetimeDB commit path: each database is a single-threaded actor; database lock held only for the transaction at the head of the pipeline; table data held in memory on the leader. VERIFIED (spacetimedb.com/blog/how-does-spacetime-scale). It "never exposes any data to users that has not been persisted to disk, unless you specifically enable it", holding responses in a buffer until persisted ("a form of early lock release") and pipelining writes to disk. VERIFIED (spacetimedb.com/blog/benchmarking). Per-transaction fsync vs group commit: not stated in either page. UNVERIFIED.
- SpacetimeDB measured throughput (vendor, single node, Intel 14900K): 303,920 ± 4,712 TPS (TypeScript harness, power-law contention) and 265,541 ± 940 (Rust harness); 279,025 uncontended; latency p50 about 7.4 to 8.1 ms, p99 about 11.7 to 12.9 ms. Transactions are balance transfers sent as individual requests with 40 in flight per client; 5 min runs with 30 s warmup. VERIFIED (spacetimedb.com/blog/benchmarking). The vendor says an earlier March 2026 claim of over 100,000 TPS was misleading because of database bugs. SECONDARY (search summary of the same blog's corrections). Competing backends in the same post: Node plus SQLite about 3,100 to 3,200 TPS; Bun plus Postgres 10,730 (uncontended) and 2,773 (contended). VERIFIED (same). These are end-to-end request throughput, not per-tick batch commit cost.
- Embedded document store or ECS per-tick commit cost: no measurement found for per-tick commits in an embedded document store or an ECS persistence layer. UNVERIFIED. Nearest numbers: a SQLite forum post on a WAL2 branch, NVMe, 6 inserts per transaction: 26,425 TPS with synchronous=OFF, about 13,000 with NORMAL, 9,082 with FULL. SECONDARY (sqlite.org forum via search; non-stock build). SQLite docs: WAL with synchronous=NORMAL is power-safe but a committed transaction can be lost on power failure. SECONDARY (search summary of sqlite.org).
- How game servers persist (never or periodic, none per tick in the sources read):
- Minecraft Bukkit-family servers autosave every 6000 ticks (5 minutes); a hosting guide warns shorter intervals strain the server. SECONDARY (shockbyte.com guide; the same guide says vanilla has no automatic save, conflicting with another page).
- Factorio: optional non-blocking autosave is Linux-only and works by `fork()`, the child writes the save while the parent keeps simulating. SECONDARY (forums.factorio.com threads).
- Colyseus and Nakama: state is in memory; persistence is developer code at dispose or terminate (see section 4).
- Photon Quantum: all mutable state lives in one Frame per tick (already kept); persistence not described.
---
## 4. `room-durability`
- Colyseus production: rooms are in-memory; `onDispose()` "suggested to persist data such as player data after a match"; graceful shutdown runs `onBeforeShutdown` (default disconnects all clients, disposes rooms), clients get `CloseCode.SERVER_SHUTDOWN`; no production room restore described. VERIFIED (docs.colyseus.io/room/lifecycle). The only restore hooks, `onCacheRoom`/`onRestoreRoom`, apply only in `devMode`, which caches room state and clients' sessionId seat reservations locally and recreates rooms after a restart. The docs say "Do not use devMode in a production environment" and that it is not optimised for many rooms. VERIFIED (docs.colyseus.io/server/devmode). Crash without planned restart: not covered. UNVERIFIED. Forum answers: rooms are "meant to be ephemeral"; `LocalPresence` persists nothing; cache state in `onDispose` and reload in `onCreate`; a two-node RedisPresence user lost all state when one node crashed. SECONDARY (discuss.colyseus.io posts, 2019-2020, search summary).
- Colyseus reconnection after a drop: server holds the seat for `allowReconnection(client, seconds)` (docs suggest about 30 s for fast games, 5 min for turn-based); client persists `room.reconnectionToken` (docs example uses `sessionStorage`) and calls `client.reconnect(token)`; the token only works while the server still holds the seat; page refresh returns a new Room object with a full state. VERIFIED (docs.colyseus.io/room/reconnection). What a process restart does to held seats outside devMode: not stated. UNVERIFIED.
- Nakama authoritative matches: Heroic Labs staff (2019-08-09): "When the server shuts down there is no choice but to stop running matches"; pattern is to save state to a system-owned storage object in the terminate hook within the shutdown grace period and rebuild it in match init; 2022 follow-up says size the grace period for the match count and launch Nakama via `exec` so signals arrive; moving matches to another instance needs a roster of instances and an RPC. VERIFIED (forum.heroiclabs.com/t/preserve-match-states-after-server-restart/133). A hard crash skips the terminate window (inference from that thread; the thread does not discuss crashes explicitly). Per-VM crash isolation differs between the Lua runtime (VM per match) and Go (shared). SECONDARY (forum.heroiclabs.com thread via search).
- Photon Realtime rejoin: needs a stable UserId and `PlayerTtl` at room creation; `EmptyRoomTTL` should be at least `PlayerTTL` if room state is not saved; reconnect with a fresh anonymous UserId after app restart fails with error 32748. SECONDARY (forum.photonengine.com threads via search). A TTL is held by the Photon server process; whether it survives a Photon server restart: UNVERIFIED.
- SpacetimeDB persistence: write-ahead log; table data in memory, restored from log (already kept in `prior-art-2026-10-27.md`).
- .io games (agar.io-likes): no source found on restart or crash behaviour. UNVERIFIED. The open-source Agar.io server clones (Ogar, Cigar) were located by search but not read.
- Pattern across Colyseus, Nakama and Photon: session-scoped game state is not durable by default; the vendor-documented route is application code that saves at a shutdown hook, plus a reconnect window measured in seconds to minutes held in server memory.
---
## 5. `player-identity`
- Colyseus: each connection gets a `sessionId`; a refreshed `reconnectionToken` is the resume credential, stored by the app in `sessionStorage`; valid only while the seat is held. VERIFIED (docs.colyseus.io/room/reconnection). No account or key-pair concept in these pages; identity beyond that is the developer's, with a separate Auth module not read. UNVERIFIED for the Auth module.
- Nakama device auth: client-generated device id, 10 to 128 bytes alphanumeric with dashes; docs say to generate a random UUID on first launch and keep it in app-private storage, not an OS hardware id; unknown id creates an account by default (`create=false` disables); an account can link several device ids (link of an id owned by another account returns 409; unlinking the last returns 403); docs warn device ids "can be unreliable because device identifiers can sometimes change" and suggest Google or Game Center for games. VERIFIED (heroiclabs.com/docs/nakama/concepts/authentication, first 100k characters read). The id is a bearer secret with no proof of possession beyond knowing it (inference from the page; not stated as such).
- Photon Realtime `UserId`: server assigns one if the client sends none; rejoin requires the same UserId; a client-chosen UserId without Custom Authentication is unverified, so anyone can claim another player's id; Custom Authentication lets an external provider return the UserId, overriding the client's. SECONDARY (search summaries of doc.photonengine.com Realtime v5 userids pages and forum threads; the current docs URL I tried was 404).
- PlayFab `LoginWithCustomID`: the id is chosen by the client; Microsoft's best-practices page limits its published-game use to pairing with a pre-existing backend and recommends moving to another login before launch; "encrypted logins" hardening exists but must be on from the beginning; a community thread asks how to stop clients spamming random ids to create accounts, unanswered in the result. SECONDARY (search summaries of learn.microsoft.com login-basics-best-practices, devdocs.xbox.com encrypted-logins, community.playfab.com). The direct custom-id page returned 404.
- SpacetimeDB: `ctx.sender` is the caller identity hook (already kept); how identities are minted and resumed was not read. UNVERIFIED.
- Key pair in the browser:
- WebCrypto `CryptoKey` has `extractable`; `false` blocks `exportKey`/`wrapKey`. VERIFIED (MDN CryptoKey). CryptoKey is Baseline widely available (since September 2016), secure contexts only, available in workers. VERIFIED (MDN).
- Non-extractable keys are structured-cloneable so IndexedDB can hold them without raw bytes reaching script. SECONDARY (MDN page did not state it; search summaries of W3C WebCrypto list and library docs). Historic bugs: Safari 10 `DataCloneError` for CryptoKey in IndexedDB (fixed, WebKit bug 162554); Safari failed on `CryptoKeyPair` objects while Chrome and Firefox worked (WebKit bug 164868, marked duplicate); Firefox bug 1048931 had private-key cloning limits for ECDH in the 55 era. SECONDARY, 2016-2019, current state UNVERIFIED. ECDSA P-256 sign/verify availability per browser: UNVERIFIED in this pass (MDN SubtleCrypto page not read).
- Not protected against script running on the same origin: such script can call `sign()` with the stored key. This is a property of the API (extractable=false blocks export only), stated in MDN's definition; the XSS consequence is inference.
- Eviction: MDN storage-quotas page: best-effort storage may be evicted under pressure (LRU); `navigator.storage.persist()` upgrades to persistent, which Firefox grants by prompt and Safari and Chromium decide from user interaction history without a prompt; private modes may use different quotas and usually delete data at session end. VERIFIED (MDN Storage quotas and eviction criteria).
- Safari ITP: script-writable storage including IndexedDB, LocalStorage, SessionStorage and service-worker registrations is deleted after 7 days of browser use without user interaction (click, tap, keyboard) with the site; home-screen web apps are exempt. VERIFIED (webkit.org/tracking-prevention; MDN gives the same rule with "when cross-site tracking prevention is on"). Server-set cookies are exempt (MDN). So a browser-held key on Safari is lost after seven days of non-interaction.
- WebAuthn or passkeys as the alternative key store: not researched in this pass. UNVERIFIED.
---
## 6. `browser-client-store`
- Typed-array views over wasm memory: `Float32Array::view` and similar are zero-copy but "only valid so long as the backing buffer isn't resized"; any allocation in the module (`Box::new`, malloc) can grow memory, which detaches the old `ArrayBuffer` and leaves existing views empty (also when the grow delta is zero); the safe patterns are to create the view immediately before use and not allocate while holding it, or copy into a JS-owned array. VERIFIED (wasm-bindgen discussion #3802 and js-sys docs, via fetch and search). Resizable `ArrayBuffer` (TC39 Stage 4) is said to address this; whether wasm memory uses it today: UNVERIFIED.
- Unity WebGL: the emscripten heap is exposed to JavaScript as typed-array views (`HEAP8`, `HEAPU8`, up to 64-bit floats) usable from `.jslib` plugins; textures are reached through emscripten's `GL.textures` mapping; the build runs in its own scope on the page. SECONDARY (Unity manual page for WebGL browser scripting, several versions, via search summary). Unity renders itself inside wasm into its own canvas; the page-side renderer is not the Unity renderer. A JS renderer would read the heap through those views; no source describing that pattern was found. UNVERIFIED.
- Godot web export: `JavaScriptBridge` wraps JS objects as references; `eval` returns numbers, booleans, strings and binary types (`ArrayBuffer`, `TypedArray`, `DataView`) to GDScript; floats may lose precision; no performance guidance given. VERIFIED (docs.godotengine.org javascript_bridge page). Engine renders itself; the page does not say how. UNVERIFIED.
- Bevy on the web: renders inside wasm through WebGL2 by default or WebGPU opt-in at build time; simulation and renderer are the same wasm module. SECONDARY (bevy.org/news/bevy-webgpu and example pages via search). No JS-side renderer reading Bevy memory was found. UNVERIFIED.
- Photon Quantum (Unity): the simulation exposes `Predicted`, `PredictedPrevious`, `PreviousUpdatePredicted` and `Verified` Frames through `Game.Frames`; `PredictedPrevious` is documented as the second latest predicted copy "meant for visual interpolation between that copy and the Predicted frame"; view components read these through `PredictedFrame`/`PredictedPreviousFrame`/`VerifiedFrame`, updated from Unity's late-update by `QuantumEntityViewUpdater`; the entity view's transform source is switched by `InterpolationMode` between predicted frames and snapshot interpolation (`AUTO` picks by whether the entity is prediction-culled). VERIFIED (doc-api.photonengine.com Quantum 3 `QuantumGame.FramesContainer`, `QuantumEntityView`, `QuantumViewComponent` pages, via fetch and search). So the view reads retained whole-state Frame copies by reference in the same process, not a per-frame serialised copy; whether the Frames are read-only or thread-isolated: UNVERIFIED. Photon blog says Quantum WebGL samples exist and a Quantum game won a web game jam. SECONDARY (blog.photonengine.com/photon-multiplayer-webgl-for-game-jams, via search).
- Wasm to JS boundary guidance: a struct-of-arrays layout with one typed-array view per attribute lets a renderer read positions without copying; copying a JS typed array into Rust via wasm-bindgen slices costs a copy. SECONDARY (abratabia.com, scour.ing/Medium, reintech.io via search; blog-grade, no measurements read).
- Rapier JS (wasm physics) position read-out (view vs copy): not found. UNVERIFIED.
- Local, already kept (not re-verified): .NET `Span<byte|int|float|double>` crosses to JS as a `MemoryView` without copy; synchronous `JSImport` about 1 µs (browser-sim-runtime A4).
---
## Gaps left
- Chrome 97 Datagram interop with Kestrel's draft-02 WebTransport: no source either way.
- Any measured WebSocket under-loss behaviour for .io games; any .io restart behaviour.
- Overwatch talk content, Valve and Unreal primary pages (access blocked).
- ECDSA P-256 WebCrypto per-browser matrix; WebAuthn as key store.
- A concrete example of a JS renderer reading wasm game state from shared memory in a shipped game.