Commit ac17e82
authored
feat(api): XIP-83 d14n binding — bidirectional Subscribe on QueryApi (#2020)
## XIP-83 d14n binding — bidirectional `QueryApi.Subscribe`
Implements the decentralized (d14n) binding of [XIP-83 mutable
subscription streams](xmtp/XIPs#139) on xmtpd. A
single long-lived bidirectional stream lets a client **mutate its topic
set in place** (add/remove subscriptions) and keep the connection alive
with ping/pong — instead of tearing down and reopening a stream every
time group membership changes.
This is the xmtpd counterpart to the node-go v3 `MlsApi.Subscribe`
binding. Same control protocol (`Mutate` / `Started` / `CatchupComplete`
/ `TopicsLive` / `Ping` / `Pong`); the d14n binding delivers
`OriginatorEnvelope`s and uses per-originator vector cursors.
### Design: gated async catch-up
- **Single-writer + single-sender actor** per subscription. All
topic-set mutation happens on the writer goroutine; all stream sends
happen on the sender goroutine (drained via a bounded queue).
- **Per-topic gate + pending buffer.** A newly-added topic is caught up
by an async fetcher goroutine while already-live topics keep flowing — a
slow catch-up never head-of-line-blocks live delivery.
- **No missing messages across the switch.** A topic is registered
*live* **before** its catch-up snapshot is taken, and delivery is
deduped by a monotonic per-originator cursor (`advanceTopicCursors`).
Anything that lands during catch-up is either in the snapshot or arrives
live; the dedup drops the overlap.
- **Sparse vs. filled cursors.** The writer keeps only the originator
cursors the client provided (growing them as new originators are seen);
the fetcher holds a filled copy purely for the query. Keeps memory
bounded at the active-topic ceiling.
- **1M active-topic ceiling** (`maxActiveSubscribeTopics`) — ~16–32MB
for a consumer that large, deliberately favored over forcing clients
into multiple connections (which would invite rate-limiting and wreck
some topologies).
- **Liveness.** Either peer may `Ping`; the receiver must `Pong` the
nonce. No pong within the deadline → the node reaps the vanished peer
(e.g. a mobile client the OS suspended behind a proxy that still ACKs
the transport).
### Commits
1. `feat(api): add mutableSubscription handle to subscribeWorker` —
in-place topic add/remove on the existing subscribe worker, guarded by
`topicsMu`.
2. `feat(api): XIP-83 bidirectional Subscribe handler on QueryApi +
xmtpv4 protos` — the handler (`subscribe.go`) + regenerated xmtpv4
protos.
3. `test(api): Subscribe handler tests + configurable keepalive interval
for tests` — 5 race-tested scenarios + a test-only knob to shorten the
keepalive cadence.
### Tests
`go test -race ./pkg/api/message` — all green:
- `TestSubscribe_CatchUpThenLive`
- `TestSubscribe_MutateRemoveStopsDelivery`
- `TestSubscribe_HalfCloseHistoryOnlyDrains`
- `TestSubscribe_HistoryOnlyOnLiveRejected`
- `TestSubscribe_NoPongIsReaped`
### Dependency & merge ordering
Built on [xmtp/proto#338](xmtp/proto#338)
(`QueryApi.Subscribe`). The committed `.pb.go` here is generated from
that branch via `dev/gen/protos`; once #338 merges, regenerating from
proto `main` produces byte-identical output.
🤖 Generated with [Claude Code](https://claude.com/claude-code)1 parent 4b14f2a commit ac17e82
14 files changed
Lines changed: 3496 additions & 277 deletions
File tree
- pkg
- api/message
- proto
- openapi
- mls/api/v1
- xmtpv4/message_api
- xmtpv4/message_api
- message_apiconnect
- testutils/api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
15 | | - | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
16 | 20 | | |
17 | 21 | | |
18 | 22 | | |
| |||
101 | 105 | | |
102 | 106 | | |
103 | 107 | | |
104 | | - | |
| 108 | + | |
105 | 109 | | |
106 | 110 | | |
107 | 111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
0 commit comments