stdb: drop the v1 JSON wire, and ship it as 4.0.0 #110

Merged
yuris merged 1 commit from remove-v1-wire into main 2026-08-22 07:47:07 +00:00
Owner

Closes the last thing making this SDK structurally different from every official one: it spoke two wires.

Why

SpacetimeDB 2.x serves the v1 message schema on v1.json.spacetimedb and the v2 schema only on v2.bsatn.spacetimedb (crates/client-api/src/routes/subscribe.rs maps TEXT_PROTOCOL → WsVersion::V1). Upstream already decided what to do about that:

Client Subprotocol Source
Rust SDK v2, hardcoded sdks/rust/src/websocket.rs:250,371
C# SDK v2, hardcoded sdks/csharp/src/SpacetimeDBClient.cs:226
Unreal SDK v2, hardcoded sdks/unreal/.../Websocket.cpp:52
spacetime subscribe v3 crates/cli/src/subcommands/subscribe.rs:254

None of them keeps v1 selectable — no negotiation, no fallback, and the v2 types are welded into their signatures. Grepping sdks/csharp/src and sdks/unreal/src for any v1 identifier returns nothing.

This SDK was the only client carrying both, and it paid for it with a second implementation of every send path, a parallel handler table, and a 1215-line JSON codec shipped to every consumer inside include_dirs = stdb.

stdb/ loses 1710 lines (−20%).

Removed — all of it already unreachable on the default wire

  • db:subscribe, the legacy whole-set form. Not a judgement call: the v2 ClientMessage has no legacy Subscribe — its Subscribe is the query-set form — so there was no frame to send and the call has raised since 2.0.0. subscribe_query, subscription_builder and unsubscribe are untouched. c.subscriptions and the subscription_replaced event (which reported #43's hold-count drift) go with it.
  • opts.protocol, opts.light, Builder:with_protocol, SUBPROTOCOL_V2. light was v1-only and already raised on v2.
  • opts.json on connect/connection.new — it only ever fed the JSON codec. auth.exchange and schema.fetch still take one, being HTTP. Its process-global codec guard goes too: that hazard existed only because stdb/protocol.lua held one codec per process.
  • Call:on_out_of_energy. ReducerOutcome has no OutOfEnergy variant, so nothing could set that state any more — it registered a callback that could never fire, which reads as working. Exhaustion arrives as InternalError text and reaches on_error.
  • cache.lua's untyped decode path. row_decoder is always installed now, so undecodable rejects a relation with no types before any decoder runs; the fallback was dead code.
  • one_off_key — v1 correlated on an echoed hex message_id; v2 correlates on request_id.

Kept, and why

stdb/protocol.lua → dev/test/crosscheck_v1.lua, decode half only. Not part of the SDK; nothing under stdb/ requires it, and nothing ships it.

It survives because BSATN is opaque. 03 00 00 00 is a valid u32, half a u64, or a length prefix, and only the decoder's belief about the type decides — so a wrong belief returns a plausible number instead of raising, and in Lua nothing catches that. test_protocol_v2.lua decodes frame 29 of the captured session both ways and asserts the two Lua values are equal. The decoders share no code and were written from different upstream sources, so agreement is evidence rather than tautology.

Verified by mutation, not assertion:

Mutation to protocol_v2.lua Caught by
u64 reader → u32 test_protocol_v2, test_slicing, test_tables, test_connection_handles
identity byte order reversed test_protocol_v2, test_connection_handles

The second failure is named "identity rendered big-endian, matching identity_to_hex" — identity_to_hex is crosscheck_v1's own function. That is the cross-check doing the job it was kept for.

The v1 client encoder was deleted with the wire: an encoder has no second implementation to disagree with, so it was only ever testing itself.

New seam

connection.dispatch(c, msg), split out of handle_frame, which is now decode + dispatch. Decoding bytes and acting on a message are two jobs, and everything below that line is bookkeeping that does not care how the bytes arrived. There is no server-side BSATN encoder in this repo, so without the split the connection tests could not exist at all — they would have to hand-assemble every server frame.

Tests

2119 → 2078 assertions, ~25 of them new (connection_id_to_hex after its move and signature narrowing, the gzip URL case from #107, procedure-return decode failure). Every deletion covered behaviour that no longer exists.

test_connection.lua was jammed against Lua 5.1's hard 200-local-per-function ceiling — its own comments were already working around it with do blocks and nested functions — so it splits into test_connection.lua, test_connection_handles.lua, and a shared conn_fixtures.lua.

Cache and fixture tests install crosscheck_v1 as cache.row_decoder, keeping their readable JSON rows rather than re-encoding 47 fixtures to BSATN. Those files test the cache, not a codec.

Version

4.0.0. Public surface is gone, which matches the precedent of 2.0.0 — that bumped major for a default change alone. No migration guide: the install URL is a version-pinned zip so nobody on 3.x is disturbed, and #105 already set that precedent.

Not verified locally

The live conformance harness needs a real SpacetimeDB server and the Defold engine, so CI is what exercises it here. Syntax is clean (luac -p, bash -n, node --check).

PHASE8's claim was rewritten rather than dropped: it used to prove the v1 light-mode cross-client asymmetry, and now proves the v2-native equivalent — a broadcast TransactionUpdate carries no request_id at all, structurally distinct from the caller's own ReducerResult::Ok. Sound against connection.lua, but only a live run proves it.

Wiki

Handled separately — the wiki is its own git repo, so it cannot ride in this PR. V1-Opt-Out goes away entirely; Usage-Guide, API-Reference, Protocol-Notes, Roadmap, Quickstart, Installation, Development and Home all carry v1 references that follow this change.

Closes the last thing making this SDK structurally different from every official one: it spoke two wires. ## Why SpacetimeDB 2.x serves the v1 message schema on `v1.json.spacetimedb` and the v2 schema only on `v2.bsatn.spacetimedb` (`crates/client-api/src/routes/subscribe.rs` maps `TEXT_PROTOCOL` → `WsVersion::V1`). Upstream already decided what to do about that: | Client | Subprotocol | Source | |---|---|---| | Rust SDK | v2, hardcoded | `sdks/rust/src/websocket.rs:250,371` | | C# SDK | v2, hardcoded | `sdks/csharp/src/SpacetimeDBClient.cs:226` | | Unreal SDK | v2, hardcoded | `sdks/unreal/.../Websocket.cpp:52` | | `spacetime subscribe` | v3 | `crates/cli/src/subcommands/subscribe.rs:254` | None of them keeps v1 selectable — no negotiation, no fallback, and the v2 types are welded into their signatures. Grepping `sdks/csharp/src` and `sdks/unreal/src` for any v1 identifier returns nothing. This SDK was the only client carrying both, and it paid for it with a second implementation of every send path, a parallel handler table, and a 1215-line JSON codec shipped to every consumer inside `include_dirs = stdb`. `stdb/` loses **1710 lines (−20%)**. ## Removed — all of it already unreachable on the default wire - **`db:subscribe`**, the legacy whole-set form. Not a judgement call: the v2 `ClientMessage` has no legacy `Subscribe` — its `Subscribe` *is* the query-set form — so there was no frame to send and the call has raised since 2.0.0. `subscribe_query`, `subscription_builder` and `unsubscribe` are untouched. `c.subscriptions` and the `subscription_replaced` event (which reported #43's hold-count drift) go with it. - **`opts.protocol`, `opts.light`, `Builder:with_protocol`, `SUBPROTOCOL_V2`.** `light` was v1-only and already raised on v2. - **`opts.json`** on `connect`/`connection.new` — it only ever fed the JSON codec. `auth.exchange` and `schema.fetch` still take one, being HTTP. Its process-global codec guard goes too: that hazard existed *only* because `stdb/protocol.lua` held one codec per process. - **`Call:on_out_of_energy`.** `ReducerOutcome` has no `OutOfEnergy` variant, so nothing could set that state any more — it registered a callback that could never fire, which reads as working. Exhaustion arrives as `InternalError` text and reaches `on_error`. - **`cache.lua`'s untyped decode path.** `row_decoder` is always installed now, so `undecodable` rejects a relation with no `types` before any decoder runs; the fallback was dead code. - **`one_off_key`** — v1 correlated on an echoed hex `message_id`; v2 correlates on `request_id`. ## Kept, and why `stdb/protocol.lua` → `dev/test/crosscheck_v1.lua`, **decode half only**. Not part of the SDK; nothing under `stdb/` requires it, and nothing ships it. It survives because BSATN is opaque. `03 00 00 00` is a valid u32, half a u64, or a length prefix, and only the decoder's belief about the type decides — so a wrong belief returns a *plausible* number instead of raising, and in Lua nothing catches that. `test_protocol_v2.lua` decodes frame 29 of the captured session both ways and asserts the two Lua values are equal. The decoders share no code and were written from different upstream sources, so agreement is evidence rather than tautology. Verified by mutation, not assertion: | Mutation to `protocol_v2.lua` | Caught by | |---|---| | `u64` reader → `u32` | `test_protocol_v2`, `test_slicing`, `test_tables`, `test_connection_handles` | | identity byte order reversed | `test_protocol_v2`, `test_connection_handles` | The second failure is named *"identity rendered big-endian, matching identity_to_hex"* — `identity_to_hex` is `crosscheck_v1`'s own function. That is the cross-check doing the job it was kept for. The v1 client **encoder** was deleted with the wire: an encoder has no second implementation to disagree with, so it was only ever testing itself. ## New seam `connection.dispatch(c, msg)`, split out of `handle_frame`, which is now decode + dispatch. Decoding bytes and acting on a message are two jobs, and everything below that line is bookkeeping that does not care how the bytes arrived. There is no server-side BSATN encoder in this repo, so without the split the connection tests could not exist at all — they would have to hand-assemble every server frame. ## Tests **2119 → 2078 assertions**, ~25 of them new (`connection_id_to_hex` after its move and signature narrowing, the gzip URL case from #107, procedure-return decode failure). Every deletion covered behaviour that no longer exists. `test_connection.lua` was jammed against Lua 5.1's hard 200-local-per-function ceiling — its own comments were already working around it with `do` blocks and nested functions — so it splits into `test_connection.lua`, `test_connection_handles.lua`, and a shared `conn_fixtures.lua`. Cache and fixture tests install `crosscheck_v1` as `cache.row_decoder`, keeping their readable JSON rows rather than re-encoding 47 fixtures to BSATN. Those files test the cache, not a codec. ## Version `4.0.0`. Public surface is gone, which matches the precedent of 2.0.0 — that bumped major for a *default change* alone. No migration guide: the install URL is a version-pinned zip so nobody on 3.x is disturbed, and #105 already set that precedent. ## Not verified locally The live conformance harness needs a real SpacetimeDB server and the Defold engine, so CI is what exercises it here. Syntax is clean (`luac -p`, `bash -n`, `node --check`). PHASE8's claim was rewritten rather than dropped: it used to prove the v1 light-mode cross-client asymmetry, and now proves the v2-native equivalent — a broadcast `TransactionUpdate` carries no `request_id` at all, structurally distinct from the caller's own `ReducerResult::Ok`. Sound against `connection.lua`, but only a live run proves it. ## Wiki Handled separately — the wiki is its own git repo, so it cannot ride in this PR. `V1-Opt-Out` goes away entirely; `Usage-Guide`, `API-Reference`, `Protocol-Notes`, `Roadmap`, `Quickstart`, `Installation`, `Development` and `Home` all carry v1 references that follow this change.
stdb: drop the v1 JSON wire, and ship it as 4.0.0
Some checks failed
ci / ci (pull_request) Failing after 13s
conformance / conformance (pull_request) Successful in 2m27s
bf30c06dba
SpacetimeDB 2.x serves the v1 message schema on `v1.json.spacetimedb` and
the v2 schema only on `v2.bsatn.spacetimedb`. Every official SDK deleted
v1 outright rather than keeping it selectable: the Rust, C# and Unreal
SDKs hardcode `v2.bsatn.spacetimedb` with no negotiation and no fallback,
and the v2 types are welded into their signatures. This SDK was the only
client still carrying both, and carrying it cost a second implementation
of every send path, a parallel handler table, and a JSON codec shipped to
every consumer inside `include_dirs = stdb`.

So the wire is v2 and only v2. stdb/ loses 1710 lines.

REMOVED, ALL OF IT UNREACHABLE ON THE DEFAULT WIRE ALREADY:

  * `db:subscribe` - the legacy whole-set form. Not a judgement call: the
    v2 ClientMessage has no legacy Subscribe, its Subscribe IS the
    query-set form, so there was no frame to send and the call has raised
    since 2.0.0. `subscribe_query`, `subscription_builder` and
    `unsubscribe` are untouched. With it go `c.subscriptions` and the
    `subscription_replaced` event that reported #43's hold-count drift.
  * `opts.protocol`, `opts.light`, `Builder:with_protocol`,
    `SUBPROTOCOL_V2`. `light` was v1-only and already raised on v2.
  * `opts.json` on `connect`/`connection.new`. It only ever fed the JSON
    codec; `auth.exchange` and `schema.fetch` still take one, being HTTP.
    Its process-global codec guard goes too - that hazard existed only
    because stdb/protocol.lua held one codec per process.
  * `Call:on_out_of_energy`. ReducerOutcome has no OutOfEnergy variant,
    so nothing could set that state any more: it registered a callback
    that could never fire, which reads as working. Exhaustion arrives as
    InternalError text and reaches `on_error`.
  * cache.lua's untyped decode path. `row_decoder` is always installed
    now, so `undecodable` rejects a relation with no `types` before any
    decoder runs and the fallback was dead.
  * `one_off_key` - v1 correlated on an echoed hex message_id; v2
    correlates on request_id.

SPLIT, NOT DELETED: stdb/protocol.lua -> dev/test/crosscheck_v1.lua,
decode half only. It is not part of the SDK and nothing under stdb/
requires it. It stays because BSATN is opaque - `03 00 00 00` is a valid
u32, half a u64, or a length prefix, and only the decoder's belief
decides, so a wrong belief returns a plausible number instead of raising.
test_protocol_v2.lua decodes frame 29 of the captured session both ways
and asserts the two Lua values are equal; the decoders share no code and
were written from different upstream sources, so agreement is evidence.
Mutating the BSATN identity byte order or its u64 reader fails that
differential - verified both ways. The v1 client ENCODER was deleted with
the wire: an encoder has no second implementation to disagree with.

NEW SEAM: `connection.dispatch(c, msg)`, split out of `handle_frame`,
which is now decode + dispatch. Decoding bytes and acting on a message
are two jobs, and everything below that line is bookkeeping that does not
care how the bytes arrived. There is no server-side BSATN encoder in this
repo, so without the split the connection tests could not exist at all.

TESTS: 2119 -> 2078 assertions, ~25 of them new. Every deletion covered
behaviour that no longer exists. test_connection.lua was jammed against
Lua 5.1's 200-local ceiling - its own comments were already working
around it - so it splits into test_connection.lua,
test_connection_handles.lua and a shared conn_fixtures.lua. Cache and
fixture tests install crosscheck_v1 as `cache.row_decoder`, keeping
readable JSON rows: those files test the cache, not a codec.

4.0.0 rather than 4.x because public surface is gone, matching the
precedent of 2.0.0, which bumped major for a default change alone. No
migration guide: the install URL is a version-pinned zip, so nobody on
3.x is disturbed, and #105 already set that precedent.

Not verified here: the live conformance harness needs a real server and
the Defold engine. PHASE8's claim was rewritten from the v1 light-mode
cross-client asymmetry to its v2-native equivalent - a broadcast
TransactionUpdate carries no request_id at all, unlike ReducerResult::Ok.
yuris force-pushed remove-v1-wire from bf30c06dba
Some checks failed
ci / ci (pull_request) Failing after 13s
conformance / conformance (pull_request) Successful in 2m27s
to 866cbe16e7
All checks were successful
ci / ci (pull_request) Successful in 21s
conformance / conformance (pull_request) Successful in 2m36s
2026-08-22 07:19:03 +00:00
Compare
yuris merged commit a0e7284834 into main 2026-08-22 07:47:07 +00:00
yuris deleted branch remove-v1-wire 2026-08-22 07:47:08 +00:00
Sign in to join this conversation.
No description provided.