api: settle the v1.1.0 surface before it freezes #68
No reviewers
Labels
No labels
area:defold
area:docs
area:infra
area:protocol
area:testing
kind:bug
kind:chore
phase:breadth
phase:core
phase:hardening
phase:slice
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
yuris/spacetimedb-defold!68
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/api-surface-v11"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 5 of the v1.1.0 milestone (#59): the API-surface review of the new
surface only. v1.0.0's freeze review found eight renames worth making, and this
is the same question for everything added this milestone — the last moment any of
it is free. Zero external consumers, so every change here is a clean cutover: no
aliases, no shims, no deprecated paths.
Four changes, each from the review, each demonstrated rather than argued from
taste.
1.
auth.exchange'stokenoption becomescredentialopts.tokenwas the provider credential, buttokenmeans "a SpacetimeDBtoken" in every other public place:
connect{token=},db.token,stdb.with_token(url, token), the?token=query parameter, the persisted{identity, token}record, andauth.exchange's owncb(err, token).The clinching evidence is that the implementation already refused the name — it
read
local credential = required(opts, "token")because the result needed theword
token. One function body needed two words for two things and only thepublic key stayed ambiguous. Both meanings appeared within twelve lines of the
module's own example, and the headline example is not a token at all: a Steam
session ticket is a ticket.
The HTTP body field stays
token— that is the wire shape and belongs to thecaller's backend. The README now states the mapping explicitly, and a test
asserts it directly, plus that no
credentialfield leaks onto the wire.Error string is now
stdb.auth: credential is required.2. The
http_seamcallback dropserrWas
cb(err, status, body), witherrset only for a transport failure andevery HTTP status — 4xx and 5xx included — arriving with
err = nil. Two factskilled it:
if err then return failed("... transport failure ..."). The first parameter of a public contractcarried no information to its only reader.
are
(err, ...)—erris nil on success").http_seamis a documented,injectable seam, so third parties will write one, and the natural
implementation sets
errfor a 401.Observed cost of that natural mistake, before this change: four requests for a
credential rejected on the first, reported as a transport failure — silently
defeating the 4xx-no-retry rule the README calls the part worth knowing.
Now
post(url, headers, body, cb)calls back(status, body), withstatus == 0meaning nothing answered. That is Defold's own signal, already verified against
engine source, so there is one way to say "no response" instead of two. The seam
became a pure pass-through and the two duplicated transport-failure branches in
auth.luamerged into one. Error strings are unchanged.3. The auth cache nests under its own key
The record was a flat
{ [provider] = token }withprovidercaller-chosen,sharing a namespace with
connect's{ identity, token }— only the slot namekept them apart. Both directions were reproduced:
provider = "token"on ashared slot returned
connect's identity token as the exchange result with zerorequests, and
connectpersisting an identity wiped every cached provider.The SDK already made this argument in the opposite direction: table accessors are
namespaced under
db.tablesprecisely because "table names belong to whoeverwrote the server module", and that construction has no reserved list. A flat
provider map had two reserved words and refused nothing.
Now
{ auth = { [provider] = token } }. Merge semantics preserved on both axes.The implementer deviated from the review's suggested
cached.auth and cached.auth[provider]and type-checked both levels instead — correctly: baretruthiness introduces a raise class the flat version could not produce, since
{ auth = 99 }raises on indexing, and{ auth = "junk" }withprovider = "format"reachesstring.formatthrough the string metatable, whichis the exact leak the existing string check exists to stop, one level down.
4.
connection.spread_forbecomesconnection.backoff_spreadRight on meaning, wrong on family:
spread_forwas the only_for-suffixed namein the public surface, among
backoff_delay,settle_pending,resubscribe_all,set_send,identity_to_hex,decode_typed.backoff_spreadsits next to theonly function it is ever passed to — in the README table, in autocomplete, and
alphabetically.
backoff_delay's signature is untouched and itsspreadparameter keeps itsname. The review also confirmed the trailing-optional-multiplier shape is right
and that taking the identity directly would be worse, since it keeps
backoff_delaya pure numeric function testable by exact value.5. Completing #3:
connectnow merges rather than overwritingNesting fixed two of the three collisions. The third survived, and the
implementer flagged it rather than letting it pass: the
IdentityTokenhandlerwrote
save({identity=..., token=...})wholesale, so every connect droppedthe
authkey regardless of nesting. Auth preserving connect's record is onlyhalf of a mutual arrangement.
The handler now loads, sets the two fields, and saves.
connect's own behaviouris unchanged — it only ever reads
persisted.tokenback — and a shared slot is asupported configuration rather than a misuse, since
sys_storage(name)alwaystook a caller-chosen name.
6. Two stale archive figures, from the archive gate
.gitattributes:4still said "54 entries before, 11 after"; re-measured, thoseare 56 and 12, so the repo stated the pair twice with conflicting values while
the README pointed at
.gitattributesas corroboration. And the README creditedthose counts to "the zip a consumer fetches", which actually enumerates 13 —
the forge prepends a
spacetimedb-defold/directory entry. Both reworded toattribute the counts to
git archive.Verification
The four v1.0.0 exact-value backoff assertions remain byte-identical.
Mutation-checked: reverting the wholesale save fails the suite; breaking the
cache nesting and the
status == 0branch each fail it too.One process note worth recording
Adding the new test locals tipped
test_connection.luaover Lua 5.1's ceiling of200 locals per function. The runner printed that crash before its per-file
oklines, so reading the tail of the output showed a false green — I nearly took it.
The block is now scoped in
do ... endwith a comment, since the file will hitthis again, and the lesson is that this runner's summary must be grepped rather
than tailed.
Four renames from the API-surface review, plus two stale doc figures. All of this is unreleased, so there are no consumers and no shims: each old name is gone, not aliased. `auth.exchange`'s `token` option becomes `credential`. `token` means a SpacetimeDB token everywhere else here - `connect{token=}`, `db.token`, `with_token`, the `?token=` parameter, the persisted `{identity, token}` record, and `exchange`'s own `cb(err, token)` result - and both meanings appeared within a dozen lines of the module's worked example. The implementation already refused the name internally (`local credential = required(opts, "token")`). The request BODY field stays `token`: that shape belongs to the caller's backend, not to this SDK, and the README now says so explicitly since option and field differ. The `http_seam` callback drops `err` and calls back `(status, body)`, where status 0 means nothing answered. The old `err` was discarded by its only consumer, and it inverted the `(err, ...)` rule the README states for the whole library - so a third party writing this documented, injectable seam naturally set `err` for a 401, which turned a credential rejected on the first ask into four requests reported as a transport failure. Status 0 is Defold's own signal, already the seam's exact test, so there is now one way to say it instead of two that can disagree. The seam is a pass-through and auth's two transport-failure branches merge into one. The auth cache nests under its own `auth` key. Flat, `{ [provider] = token }` collided with `connect`'s `{ identity, token }` record - same shape of file, same `sys_storage(name)` seam, only the slot name between them - and it bit both ways: `provider = "token"` returned connect's identity token as an exchange result having made zero requests, and a connect save wiped every cached provider. `provider` is caller-chosen, so no reserved-name list can fix that; the SDK already namespaced table accessors under `db.tables` on exactly this argument. Both levels are type-checked rather than merely truthy, because a tampered `{ auth = 99 }` would raise on the index. `connection.spread_for` becomes `connection.backoff_spread` - correct on meaning, wrong on family, being the only `_for` name beside `backoff_delay`, `settle_pending`, `resubscribe_all`, `set_send`. It now sits next to the only function it is ever passed to. `backoff_delay`'s `spread` parameter is positional and unchanged. Finally, two figures re-measured rather than trusted: `.gitattributes` claimed 54 entries before and 11 after where `git archive` gives 56 and 12, and the README attributed those counts to the fetched zip, which enumerates one more because the forge prepends a `spacetimedb-defold/` directory entry. Not fixed, and worth a follow-up: nesting stops auth from clobbering connect, but connect still rewrites its slot wholesale at connection.lua:725, so an identity saved into a shared slot still drops the cache. That is frozen v1.0.0 behaviour and out of scope here; the README now tells callers to give the exchange its own slot name, as the documented example already does.Completes the third direction of the auth-cache nesting. The API review found the flat {[provider]=token} record shared a key namespace with connect's {identity, token}, and nesting under `auth` fixed two of the three collisions: provider="token" no longer reads back connect's identity token, and auth's own save now preserves a connect record. The third stayed broken, as the previous commit's report correctly flagged: the IdentityToken handler wrote `save({identity=..., token=...})` wholesale, so every connect dropped the `auth` key. Nesting cannot fix that from auth's side - auth preserving ours is only half a mutual arrangement. So the handler now merges: load, set identity and token, save. A storage slot is not exclusively connect's, and it never was - `sys_storage(name)` always took a caller-chosen slot name, so a consumer pointing both at one slot is a supported configuration rather than a misuse. Behaviour for connect itself is unchanged, since it only ever reads `persisted.token` back. Pinned by a test asserting both directions, including the case of a slot that holds ONLY an auth cache. Mutation-checked: reverting to the wholesale save fails the suite. One incidental find while adding it: the new locals tipped test_connection.lua over Lua 5.1's ceiling of 200 locals per function, which surfaced as "main function has more than 200 local variables" - and, worth recording, the runner reported that crash BEFORE the per-file ok lines, so reading the tail of the output showed a false green. The block is scoped in `do ... end` with a comment saying why, since the file will hit this again.90cb455bc2328c8b500a