Unsubscribe + UnsubscribeMulti coverage #10

Closed
opened 2026-08-19 06:49:39 +00:00 by yuris · 1 comment
Owner

Unsubscribe with SendDroppedRows flag; UnsubscribeMulti for query-set groups. Cache drop logic exists (cache.drop_query_set), untested against a live server.

  • Unsubscribe default vs SendDroppedRows rows payload
  • Multi-subscription subscribe/unsubscribe flows
  • Fixture + unit coverage
Unsubscribe with SendDroppedRows flag; UnsubscribeMulti for query-set groups. Cache drop logic exists (`cache.drop_query_set`), untested against a live server. - [ ] Unsubscribe default vs SendDroppedRows rows payload - [ ] Multi-subscription subscribe/unsubscribe flows - [ ] Fixture + unit coverage
Author
Owner

Closed in 3528c2b

local qid = db:subscribe_query({ "SELECT * FROM message WHERE room = 1" })
db:unsubscribe(qid)

The legacy Subscribe replaces the entire subscription set and cannot be undone one query at a time, so until now a client could only ever add. SubscribeMulti/UnsubscribeMulti are query-id-keyed and can be dropped.

What the wire actually does

Captured against clockworklabs/spacetime:v2.8.2 and pinned in test/fixtures/session.jsonl (now 17 frames), so a server bump breaks the test rather than a game:

1. Unsubscribe replies carry the rows back as DELETES. The same cache path that fills on subscribe empties on unsubscribe — no special casing.

2. The server computes the real delta. This was the load-bearing question, since upstream's own comment says clients "would need to track which rows exist in which queries". Two identical live subscriptions, dropped in turn:

action deletes
drop set 100 (set 101 still matches) 0
drop set 101 (nothing left matching) 2

So the client needs no query-to-row bookkeeping. Both directions are asserted.

3. An overlapping subscribe returns tables: []. Subscribe twice to the same query and the second reply is empty — those rows were already sent on this connection. Upstream's doc comment claims the opposite ("the client will receive multiple copies of rows that appear in multiple queries"). A UI that clears a list and repopulates it from a subscribe payload would render empty. Read the cache instead.

4. Single and multi forms nest differently. SubscribeMultiApplied carries .update (a DatabaseUpdate with a tables array); SubscribeApplied carries .rows.table_rows, a bare TableUpdate with no array. Decoding one as the other silently applies nothing — normalised in one place, with its own test.

Reconnect interaction

Query sets are replayed under their original query_id, so a handle the caller is still holding keeps addressing the same subscription. A set the server rejected is dropped from the replay list (SubscriptionError carries an optional query_id).

Proof

Unit tests cover the shapes, but the engine is what matters — conformance now runs a third phase against a live server:

SMOKE: PHASE2 OK survived restart; row id=1 restored
SMOKE: query set 1 subscribed
SMOKE: PASS unsubscribed query set 1; cache drained

Not done

SubscribeSingle has no dedicated API — the multi form with a one-element set covers it, and both server replies are handled. Reopen if a single-query send path is actually wanted.

## Closed in 3528c2b ```lua local qid = db:subscribe_query({ "SELECT * FROM message WHERE room = 1" }) db:unsubscribe(qid) ``` The legacy `Subscribe` replaces the entire subscription set and cannot be undone one query at a time, so until now a client could only ever add. `SubscribeMulti`/`UnsubscribeMulti` are query-id-keyed and can be dropped. ## What the wire actually does Captured against `clockworklabs/spacetime:v2.8.2` and pinned in `test/fixtures/session.jsonl` (now 17 frames), so a server bump breaks the test rather than a game: **1. Unsubscribe replies carry the rows back as DELETES.** The same cache path that fills on subscribe empties on unsubscribe — no special casing. **2. The server computes the real delta.** This was the load-bearing question, since upstream's own comment says clients "would need to track which rows exist in which queries". Two identical live subscriptions, dropped in turn: | action | deletes | |---|---| | drop set 100 (set 101 still matches) | **0** | | drop set 101 (nothing left matching) | **2** | So the client needs no query-to-row bookkeeping. Both directions are asserted. **3. An overlapping subscribe returns `tables: []`.** Subscribe twice to the same query and the second reply is empty — those rows were already sent on this connection. Upstream's doc comment claims the opposite ("the client will receive multiple copies of rows that appear in multiple queries"). A UI that clears a list and repopulates it from a subscribe payload would render **empty**. Read the cache instead. **4. Single and multi forms nest differently.** `SubscribeMultiApplied` carries `.update` (a DatabaseUpdate with a `tables` array); `SubscribeApplied` carries `.rows.table_rows`, a bare TableUpdate with no array. Decoding one as the other silently applies nothing — normalised in one place, with its own test. ## Reconnect interaction Query sets are replayed under their **original** `query_id`, so a handle the caller is still holding keeps addressing the same subscription. A set the server rejected is dropped from the replay list (`SubscriptionError` carries an optional `query_id`). ## Proof Unit tests cover the shapes, but the engine is what matters — conformance now runs a third phase against a live server: ``` SMOKE: PHASE2 OK survived restart; row id=1 restored SMOKE: query set 1 subscribed SMOKE: PASS unsubscribed query set 1; cache drained ``` ## Not done `SubscribeSingle` has no dedicated API — the multi form with a one-element set covers it, and both server replies are handled. Reopen if a single-query send path is actually wanted.
yuris closed this issue 2026-08-19 09:40:27 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/spacetimedb-defold#10
No description provided.