Skip to content

No compile-time or CI parity check binds TS message types to Rust ClientMessage/ServerMessage variants #6947

Description

@matthewevans

Problem

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:

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.
  • Filed as a follow-up to Multiplayer: all interaction submissions rejected — ClientMessage has no Interaction variant (live since v0.42.0) #6941 and deliberately not bundled with it — fixing the live multiplayer outage should not be blocked on building the guard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:frontendReact frontendarea:multiplayerMultiplayer/server/lobby (phase-server, server-core)source:githubReported via GitHubtestAdd tests

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions