-
v4.0.0
Stablereleased this
2026-08-22 10:32:04 +00:00 | 5 commits to main since this releaseOne wire, and a surface that is harder to get wrong.
Requires SpacetimeDB 2.x, Defold 1.12.4 or newer, and extension-websocket 4.2.4. The quickstart
moved out of the README and into the wiki, where it walks an unpublished module through to a
connected game that replicates player positions between clients - every command in it executed
against a live 2.8.2 server.Breaking changes
The v1 JSON wire is removed
SpacetimeDB 2.x serves the v1 message schema on
v1.json.spacetimedband the v2 schema only on
v2.bsatn.spacetimedb, and no official client keeps v1 selectable: the Rust, C# and Unreal SDKs
all hardcode v2. 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 insideinclude_dirs = stdb. The v1 wire is deleted, not deprecated -stdb/loses
1710 lines, a fifth of itself.A caller who pinned
protocol = "v1"has five things to change:- Delete
opts.protocol,opts.light,Builder:with_protocolandSUBPROTOCOL_V2. There is one
subprotocol,v2.bsatn.spacetimedb, andstdb.SUBPROTOCOLholds it.lightwas v1-only and
already raised on v2. - Delete
opts.jsononconnectandconnection.new- it only ever fed the JSON codec, and its
process-global codec guard goes with it, a hazard that existed only becausestdb/protocol.lua
held one codec per process.stdb.auth.exchangeandstdb.schema.fetchstill take ajson,
being HTTP. - Pass a generated
schema. Nothing on the v2 BSATN wire names a column, so a generated module is
not an upgrade here but the requirement: a table with notypesis skipped and reported rather
than decoded, and the reducer argument types are what let a call encode itself. - Replace
db:subscribe, the legacy whole-set form, withsubscribe_queryor
subscription_builder. The v2ClientMessagehas no legacySubscribe- itsSubscribeis the
query-set form - sodb:subscribehad no frame to send and has raised on the default wire since
2.0.0.c.subscriptionsand thesubscription_replacedevent go with it;unsubscribeis
untouched. - Drop
Call:on_out_of_energy.ReducerOutcomehas noOutOfEnergyvariant, so it registered a
callback that could never fire, which reads as working. Exhaustion arrives asInternalError
text and reacheson_error.
one_off_keyis gone too: v1 correlated a one-off query on an echoed hexmessage_id, v2
correlates onrequest_id.extension-websocket 4.2.4 is required
Move the dependency in your own
game.projectfrom 4.2.3 to 4.2.4. That release fixes
defold/extension-websocket#67, the bug
where theprotocoloption never reached the HTML5 handshake, which was the only reasonpatches/
existed here.patches/is deleted, along with the three manual steps a browser build used to
need - an HTML5 build now takes the plain remote dependency and nothing else. A consumer who was
running the patched local copy must remove it: twowebsocketextensions in one build fail to
link.Verified rather than assumed.
dev/tools/html5-conformance.shbuilds against the unpatched
dependency in a real Chromium and passes, and the handshake assertion is what proves the upstream
fix landed:[handshake] -> subprotocol=v2.bsatn.spacetimedb [handshake] <- 101 Switching Protocolsdb:pingreports(err, seconds)It used to report
cb(seconds)on success andcb(nil, err)on failure, inverted against every
other async call in the SDK.-- before db:ping(function(seconds, err) end) -- after db:ping(function(err, seconds) end)db.tables.<name>raises on a name the schema does not declaredb.tables.mesage:on_insert(fn)used to fail one line later with "attempt to index a nil value",
naming neither the typo nor the alternatives. It now names both, the same courtesydb.reducers
already did. A connection given noschematherefore raises on every table name, and says which
option is missing. A caller who useddb.tables[name] == nilto test whether a table exists needs
the schema or apcallinstead.A row with bytes left over raises
decode_typed_bsatnchecks for leftover bytes and errors instead of decoding short. Regenerate the
generated module whenever the server's schema changes and this never fires.decode_rowsruns in a
pass that writes nothing, so a raise cannot leave half-applied cache state.schema = genreplaces three correlated optionstypespace,reducersandproceduresare deleted as separate arguments.schema = gentakes
the whole generated module andconnectreads all four sections off it. The four separate options
still work and win if both forms are given.-- before schema = stdb.schema.new(gen.tables), typespace = gen.typespace, reducers = gen.reducers, procedures = gen.procedures, -- after schema = gen,The point is not the line count: the four sections must all come from the same generation, and
dropping justreducersused to decode tables fine and fail only at the first reducer call,
wire-side, which is the one failure that does not point at the option that caused it.jsonis no longer required either. It defaults to the engine's own global instdb.schema.fetch
andstdb.auth.exchange, and is validated when passed; passing it is only needed off engine, in
tests.-- before stdb.schema.fetch({ base_url = url, database = name, json = json }, cb) -- after stdb.schema.fetch({ base_url = url, database = name }, cb)Features
stdb.builder(opts)seeds the raw optionsThe fluent builder no longer stops at what its setters cover.
stdb.builder(opts)seeds
on_event,frame_budget,reconnect,storage,confirmed_readsand anything elseconnect
accepts, andbuild()copies the seed verbatim, so a builder user is never forced back to raw
connectfor one option and never loses the three lifecycle callbacks by going there. The seed is
copied, sowith_tokenandwith_schemawrite into the builder's own table rather than the
caller's.stdb.queryis re-exported from the facadeThe typed query builder is reachable as
stdb.query, like every sibling module on the facade.
subscribe_queryalready accepted what it builds, so the only caller who knew it existed was one
who ranrequire("stdb.query")by hand.Bug fixes
db:pingraised the round trip time as an error - the inverted callback meant
stdb.await(db.ping, db)threw the latency. Both existing tests passed under the bug because
they only assertederr == nil.- A schema behind the server dropped a column silently and forever - nothing checked for bytes
left over after decoding a row, so a column appended on the server vanished from every row, and
one inserted in the middle misaligned the reader: two same-width scalars produced a plausible
wrong number rather than an error. stdb.awaitparked a coroutine forever when the socket dropped mid reducer call -
call_reducersettles itsCallhandle (ANSWERED_ELSEWHERE) instead of firing the callback,
andawaitdiscarded the handle. The same happened to an awaited subscribe across a reconnect
replay. It wires the handle too now.stdb.await_eventunhooked a second waiter when the first resolved - restoringdb.on_event
on resolve dropped a later waiter's wrapper out of the chain, so two coroutines waiting on
different events left one of them stranded. Only reproduces once a second coroutine exists.stdb.await_eventhung when the event had already fired - it waited only for a future
firing, so "wait until connected, then resubscribe" parked forever after a reconnect had already
landed, which is the natural use and the exact case that broke.- The editor command generated a schema from the wrong database -
databaseunder
[spacetimedb]ingame.projectis required now, and named in the error when it is absent. It
used to default to this repository's own dev fixture, so a consumer who never declared the
category got a schema from someone else's module, successfully and silently, and that stale
module then misdecoded every row.base_urlstill defaults tohttp://127.0.0.1:3000. stdb.urlaccepted a base URL with no scheme - a missingws://orwss://used to fail
inside the engine's websocket, naming neither this function nor the missing scheme. It is
rejected at the callsite.- The documented
pcallshape aroundawaitcould not work - Lua 5.1 cannot yield across
pcalland the HTML5 target is plain Lua 5.1, so an awaiting coroutine cannot be wrapped in a
single outerpcall; catch failures around the awaited step instead. The old documentation
claimed otherwise.
What's Changed
- deps: take extension-websocket 4.2.4, and drop the HTML5 patch in #112
- stdb: fix five silent failures, and delete three required arguments in #111
- stdb: drop the v1 JSON wire, and ship it as 4.0.0 in #110
- flake: drop forgejo-mcp and the omp-forgejo wrapper in #109
Full Changelog: v3.1.0...v4.0.0
Use as a Defold library
Add to
game.projectdependencies:https://git.nokopia.com/yuris/spacetimedb-defold/archive/v4.0.0.zip https://github.com/defold/extension-websocket/archive/refs/tags/4.2.4.zipextension-websocket is required: Defold does not resolve dependencies
transitively, so without itconnectfails at runtime.Private repo? Append
?token=<access-token>to the URL, the same
shape Defold documents for GitLab. Basic auth in the URL
(https://user:token@...) is not a substitute: the archive endpoint
ignores URL credentials - a real account with a wrong password still
gets200on a public repo - while?token=is validated and a bad
one gets a plain401.Downloads
-
Source code (ZIP)
2 downloads
-
Source code (TAR.GZ)
1 download
- Delete