Skip to content

xmtp_proto: XIP-83 d14n binding — QueryApi.Subscribe (bidi mutable subscription)#338

Merged
tylerhawkes merged 1 commit into
mainfrom
tyler/xip-83-d14n-subscribe-proto
Jun 18, 2026
Merged

xmtp_proto: XIP-83 d14n binding — QueryApi.Subscribe (bidi mutable subscription)#338
tylerhawkes merged 1 commit into
mainfrom
tyler/xip-83-d14n-subscribe-proto

Conversation

@tylerhawkes

@tylerhawkes tylerhawkes commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Adds the decentralized-backend binding of XIP-83 — companion to the v3 MlsApi.Subscribe binding (#337, merged). A new bidirectional Subscribe(stream SubscribeRequest) returns (stream SubscribeResponse) RPC on QueryApi, additive alongside the existing server-streaming SubscribeTopics.

The control protocol is identical to the v3 binding — Mutate (add/remove topics in place, history_only, mutate_id), Started, CatchupComplete, TopicsLive, Ping/Pong — adapted to the decentralized data model:

  • Per-originator vector cursor: each Subscription resumes from a xmtp.xmtpv4.envelopes.Cursor (the same type SubscribeTopics uses), not v3's scalar id_cursor.
  • Envelope delivery: responses carry OriginatorEnvelopes; the client demultiplexes by topic.
  • Kind-prefixed binary topics (XIP-49 §3.3.2) — already the decentralized backend's native representation.

Bidirectional streaming requires HTTP/2; browser/connect-web clients stay on SubscribeTopics.

Spec: xmtp/XIPs#139 (the "Decentralized (d14n) binding" section).

🤖 Generated with Claude Code

Note

Add bidirectional QueryApi.Subscribe RPC with mutable topic subscriptions and ping/pong liveness

  • Adds SubscribeRequest and SubscribeResponse messages to message_api.proto defining the client→node and node→client framing for the new bidi subscription protocol (XIP-83 d14n binding).
  • SubscribeRequest.V1.Mutate supports atomic topic adds/removes, a history_only catch-up flag, and a mutate_id for correlating CatchupComplete responses.
  • SubscribeResponse.V1 carries batched Envelopes, stream init metadata (Started), topic live-boundary markers (TopicsLive), per-mutation completion signals (CatchupComplete), and Ping/Pong liveness frames.
  • Registers the new Subscribe bidi-streaming RPC on the QueryApi service in query_api.proto, alongside the existing server-streaming SubscribeTopics.

Macroscope summarized 6ccd578.

…bscription; vector-cursor topics; OriginatorEnvelope delivery; mutate_id waves; Started/CatchupComplete/TopicsLive; ping/pong; history_only)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tylerhawkes tylerhawkes requested a review from a team as a code owner June 18, 2026 21:45
@macroscopeapp

macroscopeapp Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

Purely additive protobuf schema changes defining a new bidirectional subscription API. No existing definitions modified, and the author is the designated code owner for both files.

You can customize Macroscope's approvability policy. Learn more.

@tylerhawkes tylerhawkes merged commit f04e01a into main Jun 18, 2026
6 of 7 checks passed
@tylerhawkes tylerhawkes deleted the tyler/xip-83-d14n-subscribe-proto branch June 18, 2026 22:31
tylerhawkes added a commit to xmtp/xmtpd that referenced this pull request Jun 26, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant