You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Nothing binds the TypeScript client's WebSocket message-type string literals to the Rust ClientMessage / ServerMessage variants. A client can send a type the server has never heard of, and the only signal is a runtime serde rejection surfaced to the player as a toast.
#6941 is the concrete instance: PR #6778 added this.send({ type: "Interaction", … }) to client/src/adapter/ws-adapter.ts:764 and client/src/adapter/server-draft-adapter.ts:218 while touching no file under crates/server-core/. ClientMessage (crates/server-core/src/protocol.rs:134) has no Interaction variant. Every WebSocket multiplayer interaction submission has failed since v0.42.0 (2026-07-31), and the defect shipped through two releases and ~2 days of live play before a user reported it.
scripts/check-protocol-version.mjs did not catch this — it version-checks the protocol, it does not enumerate the variant set.
Why this is a class, not a one-off
The failure mode is available to every message type in both directions:
Client → server: a type string with no matching Rust variant. Fails closed but late, at runtime, in production, as an opaque toast.
There are 26 ClientMessage variants and a larger ServerMessage set. Both are #[serde(tag = "type", content = "data")], so the wire contract is fully mechanical and therefore fully checkable.
Suggested direction
Two shapes, either acceptable; the first is stronger.
A — Generate the constants. Emit the variant-name set from the Rust enums into a TS module (build script or a small cargo bin, checked in as a generated artifact like client/src/wasm/engine_wasm.d.ts already is). Client adapters import the constant instead of writing a string literal, so an unknown message type becomes a TypeScript compile error in check-frontend.
B — Parity test in CI. A test that extracts variant names from protocol.rs and the type: literals reachable from the adapters, and asserts set equality (with an explicit, reviewed allowlist for intentionally client-only or server-only messages). Cheaper to build, but string-scraping TS is brittle and it catches the mismatch later than A does.
Either way the guard must cover both directions — client-sent types and server-sent types — since the server→client direction is the one that can fail silently.
Notes
crates/server-core/src/client_message_wire_guard.rs already establishes the right precedent on the Rust side: its module doc states "this layer guarantees a single exhaustive match so new variants must declare wire policy", and it has two exhaustive matches so a new variant cannot compile without declaring its policy. This issue is asking for the equivalent guarantee across the language boundary, which is currently unguarded.
Problem
Nothing binds the TypeScript client's WebSocket message-type string literals to the Rust
ClientMessage/ServerMessagevariants. A client can send atypethe server has never heard of, and the only signal is a runtime serde rejection surfaced to the player as a toast.#6941 is the concrete instance: PR #6778 added
this.send({ type: "Interaction", … })toclient/src/adapter/ws-adapter.ts:764andclient/src/adapter/server-draft-adapter.ts:218while touching no file undercrates/server-core/.ClientMessage(crates/server-core/src/protocol.rs:134) has noInteractionvariant. Every WebSocket multiplayer interaction submission has failed since v0.42.0 (2026-07-31), and the defect shipped through two releases and ~2 days of live play before a user reported it.scripts/check-protocol-version.mjsdid not catch this — it version-checks the protocol, it does not enumerate the variant set.Why this is a class, not a one-off
The failure mode is available to every message type in both directions:
typestring with no matching Rust variant. Fails closed but late, at runtime, in production, as an opaque toast.casefor. Depending on the adapter's default arm, this can fail silently — strictly worse than Multiplayer: all interaction submissions rejected — ClientMessage has no Interaction variant (live since v0.42.0) #6941, which at least produced a visible error.There are 26
ClientMessagevariants and a largerServerMessageset. Both are#[serde(tag = "type", content = "data")], so the wire contract is fully mechanical and therefore fully checkable.Suggested direction
Two shapes, either acceptable; the first is stronger.
A — Generate the constants. Emit the variant-name set from the Rust enums into a TS module (build script or a small
cargobin, checked in as a generated artifact likeclient/src/wasm/engine_wasm.d.tsalready is). Client adapters import the constant instead of writing a string literal, so an unknown message type becomes a TypeScript compile error incheck-frontend.B — Parity test in CI. A test that extracts variant names from
protocol.rsand thetype:literals reachable from the adapters, and asserts set equality (with an explicit, reviewed allowlist for intentionally client-only or server-only messages). Cheaper to build, but string-scraping TS is brittle and it catches the mismatch later than A does.Either way the guard must cover both directions — client-sent types and server-sent types — since the server→client direction is the one that can fail silently.
Notes
crates/server-core/src/client_message_wire_guard.rsalready establishes the right precedent on the Rust side: its module doc states "this layer guarantees a single exhaustive match so new variants must declare wire policy", and it has two exhaustive matches so a new variant cannot compile without declaring its policy. This issue is asking for the equivalent guarantee across the language boundary, which is currently unguarded.