stdb: default to the v2 BSATN wire, and ship it as 2.0.0 #94

Merged
yuris merged 2 commits from flip-default into main 2026-08-21 11:18:40 +00:00
Owner

Gate 5 of #89. The flip, the version and the docs in one commit.

stdb/init.lua now resolves opts.protocol or "v2", game.project reads 2.0.0, and both harnesses default to v2 so a hand-run agrees with what ships. connect({ protocol = "v1" }) keeps the JSON wire, unchanged and run against a live server on every conformance trigger — stdb/protocol.lua has an empty diff against main.

A real bug the flip exposed. subscribe_url composed the query string from opts.protocol rather than from the wire connect had actually resolved. That was invisible while the default was v1 and the two always agreed. Under a v2 default a defaulted caller leaves opts.protocol nil, so the socket negotiated BSATN with no compression=None — and the codec then refuses the compressed frame — while light = true slipped past the v2 guard and went out on the wire. It is how the new default assertion and test_slicing first failed. subscribe_url now takes the resolved protocol as a parameter, and both halves are pinned: native_url({}) must equal base .. "?compression=None", and native_url({ light = true }) must raise.

Test sweep was wider than one line. native_url drives connect, so every assertion built on it inherited the default. The whole URL-composition block is the v1 surface — light exists only there, and part of it is explicitly "the pre-1.0 URLs, byte for byte" — so it now names its wire on every call. Those assertions test composition, not whichever protocol happens to be default; leaving them implicit would have silently converted them into default-tracking tests. The default assertions invert, and the protocol = "v1" case inherits the invariant that the default must stay reachable by name — it now protects v1.

Docs: the connect docstring, both SUBPROTOCOL comments, the on_applied and out_of_energy qualifiers in connection.lua, and the README's two-wire paragraph, which now leads with the loud breaks a reader hits first (light raises, calling anything needs reducers/procedures, legacy subscribe raises, tables need generated types) before the quieter behavioural ones. The f32 paragraph survives unchanged in substance — still the one type whose value differs, and v2 is the more faithful side.

Verified locally, all five engine runs: native default names v2, native STDB_PROTOCOL=v1 names v1, sliced (50000 rows over 6 frames at 8.0ms), HTML5 default asserts the v2.bsatn.spacetimedb handshake, HTML5 v1 passes. Unit suite 11/11, stylua clean.

The README's install pin points at v2.0.0.zip, tagged immediately after this merges.

Gate 5 of #89. The flip, the version and the docs in one commit. `stdb/init.lua` now resolves `opts.protocol or "v2"`, `game.project` reads `2.0.0`, and both harnesses default to v2 so a hand-run agrees with what ships. `connect({ protocol = "v1" })` keeps the JSON wire, unchanged and run against a live server on every conformance trigger — `stdb/protocol.lua` has an empty diff against `main`. **A real bug the flip exposed.** `subscribe_url` composed the query string from `opts.protocol` rather than from the wire `connect` had actually resolved. That was invisible while the default was v1 and the two always agreed. Under a v2 default a defaulted caller leaves `opts.protocol` nil, so the socket negotiated BSATN with **no** `compression=None` — and the codec then refuses the compressed frame — while `light = true` slipped past the v2 guard and went out on the wire. It is how the new default assertion and `test_slicing` first failed. `subscribe_url` now takes the resolved protocol as a parameter, and both halves are pinned: `native_url({})` must equal `base .. "?compression=None"`, and `native_url({ light = true })` must raise. **Test sweep was wider than one line.** `native_url` drives `connect`, so every assertion built on it inherited the default. The whole URL-composition block is the v1 surface — `light` exists only there, and part of it is explicitly "the pre-1.0 URLs, byte for byte" — so it now names its wire on every call. Those assertions test composition, not whichever protocol happens to be default; leaving them implicit would have silently converted them into default-tracking tests. The default assertions invert, and the `protocol = "v1"` case inherits the invariant that the default must stay reachable by name — it now protects v1. Docs: the `connect` docstring, both SUBPROTOCOL comments, the `on_applied` and `out_of_energy` qualifiers in `connection.lua`, and the README's two-wire paragraph, which now leads with the loud breaks a reader hits first (`light` raises, calling anything needs `reducers`/`procedures`, legacy `subscribe` raises, tables need generated `types`) before the quieter behavioural ones. The `f32` paragraph survives unchanged in substance — still the one type whose value differs, and v2 is the more faithful side. Verified locally, all five engine runs: native default names v2, native `STDB_PROTOCOL=v1` names v1, sliced (50000 rows over 6 frames at 8.0ms), HTML5 default asserts the `v2.bsatn.spacetimedb` handshake, HTML5 `v1` passes. Unit suite 11/11, stylua clean. The README's install pin points at `v2.0.0.zip`, tagged immediately after this merges.
stdb: default to the v2 BSATN wire, and ship it as 2.0.0
All checks were successful
ci / ci (pull_request) Successful in 1m28s
conformance / conformance (v1) (pull_request) Successful in 2m36s
conformance / conformance (v2) (pull_request) Successful in 2m27s
d914b1cabb
Closes the last gate of #89. `connect()` with no `protocol` now negotiates
`v2.bsatn.spacetimedb`; `connect({ protocol = "v1" })` selects the JSON wire,
which stays frozen, first-class and run against a live server on every
conformance trigger since #91.

## The flip exposed a bug, and it is fixed here

`subscribe_url` composed the query string off `opts.protocol` rather than off
the wire `connect` had already resolved. That read the same on both wires as
long as the default was v1 and a v2 caller had to say so. It does not now: a
defaulted caller leaves `opts.protocol` nil, so the socket negotiated BSATN
while the URL carried no `compression=None` — a brotli frame the codec refuses —
and `light = true` skipped the v2 guard and went out as `light=true` on a wire
with no light mode. `subscribe_url` now takes the resolved protocol. Two
assertions in `test_init.lua` pin both halves; the first of them is what failed
before the fix.

## connection.new keeps its own v1 default

The PUBLIC default is `stdb/init.lua`'s, and `connect` always passes its
resolved value through, so `connection.new`'s fallback is reached only by a
direct construction. Flipping it too would change the wire under every test that
builds a connection without the option, for nothing a consumer can see. Its
comment now says which default is the real one.

## Tests

`test_init.lua`'s URL block is the pre-2.0 surface — parameter composition, the
platform token split, boolean validation — and `light` exists only on v1, so
every call in it now names its wire through a block-local `v1_url`. Left to
inherit the default, those assertions would have tested `?compression=None`
instead of composition, and four of them would have raised. Three helpers
further down name v1 for a harder reason: they replay captured v1 JSON frames,
or send a reducer call BSATN cannot encode without argument types.
`test_slicing` does the same, for its hand-built JSON frames.

What inverts: `native_url({})` is now `?compression=None` over the BSATN
subprotocol, and the byte-for-byte base URL over the JSON one is what
`protocol = "v1"` buys. Three cases are new — `light` refused with no protocol
named, the frame type of a defaulted connect, and the subprotocol the opt-out
negotiates. Suite count is unchanged at 11.

## Elsewhere

The three harness defaults (`conformance.sh`, `html5-conformance.mjs`,
`smoke.script`) follow the shipped default, so a bare local run exercises what
consumers get; CI sets the matrix value explicitly either way. The dispatch
input's default follows too. Docstrings in `init.lua` and `connection.lua` are
comment-only: the protocol paragraph inverts, `light` is marked as raising on
the default, `on_out_of_energy` as never firing on it, and `confirmed_reads`
gains the per-protocol server default it resolves to. `protocol.lua` is
untouched. README leads with the four loud breaks a v1-era callsite hits at
`connect` before the quieter behavioural ones; the f32 paragraph is unchanged in
substance. `game.project` reads 2.0.0.

## Verification

Six engine runs from this worktree, all PASS: native bare (`PHASE6 v2`), native
`STDB_PROTOCOL=v1` (`PHASE6 v1`), native sliced at `STDB_BULK_BUDGET=0.008
STDB_BULK_SWEEP=50000` (`PHASE6 v2 sliced at a 8.0ms budget`, 50000 rows over 6
frames), and both HTML5 tiers — the bare one asserting a `v2.bsatn.spacetimedb`
handshake. Plus 11/11 unit suites and a clean `stylua --check`.
docs: say where each v2 break actually surfaces
All checks were successful
ci / ci (pull_request) Successful in 21s
conformance / conformance (v1) (pull_request) Successful in 2m29s
conformance / conformance (v2) (pull_request) Successful in 2m23s
da95c8e95c
The README promised that all four "find out at connect rather than in
production". Only one does. `light = true` raises inside subscribe_url, which
connect calls synchronously; the reducers/procedures requirement raises from
encode_args at the CALL; legacy `db:subscribe` raises at the subscribe callsite;
and a table with no generated types is not refused at all -- cache.lua skips it
and reports an `undescribed` event, deliberately not a raise, which the same
paragraph's last sentence already said. On a release where the docs are the
deliverable, telling an upgrader their bad callsite surfaces at connect time
when three of four do not is the wrong half of the truth.

Also fixes connection.lua's module header, which still called v2 opt-in and so
contradicted protocol_v2.lua's header and its own comment twenty lines down,
both rewritten in this same change. And notes on the connect docstring that the
generated types are needed for the table-args form only: protocol_v2 guards
both encode paths with `type(args) ~= "string"` and passes a pre-encoded BSATN
string straight through.
yuris merged commit 11f8817dd7 into main 2026-08-21 11:18:40 +00:00
yuris deleted branch flip-default 2026-08-21 11:18:40 +00:00
Sign in to join this conversation.
No description provided.