Skip to content

Commit f04e01a

Browse files
authored
xmtp_proto: XIP-83 d14n binding — QueryApi.Subscribe (bidi mutable subscription; vector-cursor topics; OriginatorEnvelope delivery; mutate_id waves; Started/CatchupComplete/TopicsLive; ping/pong; history_only) (#338)
1 parent ac2aa57 commit f04e01a

2 files changed

Lines changed: 141 additions & 0 deletions

File tree

proto/xmtpv4/message_api/message_api.proto

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,140 @@ message SubscribeEnvelopesResponse {
6161
repeated xmtp.xmtpv4.envelopes.OriginatorEnvelope envelopes = 1;
6262
}
6363

64+
// ---- XIP-83 bidirectional mutable subscription (d14n binding) ----
65+
//
66+
// QueryApi.Subscribe is the bidirectional evolution of SubscribeTopics: one
67+
// long-lived stream the client mutates in place (no reconnect on group
68+
// join/leave) with a WebSocket-style ping/pong so silent stream death is
69+
// detected on both ends. It mirrors the v3 MlsApi.Subscribe control protocol
70+
// (mls/api/v1/mls.proto), adapted to the decentralized data model: each
71+
// subscription resumes from a per-originator vector Cursor, and delivery is the
72+
// unified OriginatorEnvelope stream rather than typed group/welcome messages
73+
// (the client demuxes by each envelope's target topic). SubscribeTopics remains
74+
// the server-streaming, immutable ancestor for clients that cannot do bidi
75+
// (grpc-web / connect-web). Request and response are wrapped in `oneof version`
76+
// and pinned per stream: V1 requests receive only V1 responses.
77+
78+
// Client -> node. Sent one or more times over the life of the stream.
79+
message SubscribeRequest {
80+
oneof version {
81+
V1 v1 = 1;
82+
}
83+
84+
message V1 {
85+
// Each frame is exactly one of: a mutation, a Ping, or a Pong.
86+
oneof request {
87+
Mutate mutate = 1;
88+
Ping ping = 2; // liveness challenge (e.g. probe the link after resuming)
89+
Pong pong = 3; // answer to a node Ping
90+
}
91+
92+
// Add and/or remove subscriptions in place (applied atomically per frame).
93+
// Topics use the kind-prefixed binary representation (XIP-49 §3.3.2): the
94+
// first byte is the topic kind, the remainder is the identifier. A topic
95+
// whose kind the node does not serve fails the stream with INVALID_ARGUMENT.
96+
message Mutate {
97+
repeated Subscription adds = 1; // begin delivering these topics
98+
repeated bytes removes = 2; // topics to stop delivering
99+
100+
// Catch this Mutate's adds up to the live edge — history, TopicsLive
101+
// markers, and the wave's CatchupComplete — but do NOT register them for
102+
// live delivery. The markers then mean "you have everything as of now".
103+
// Combined with half-closing the request stream, this is the bounded
104+
// catch-up ("sync") mode: the node finishes the wave then closes the
105+
// stream itself. Removals in the Mutate are unaffected.
106+
bool history_only = 3;
107+
108+
// Client-chosen correlation id, echoed on this wave's CatchupComplete so
109+
// completions are attributable when waves overlap. SHOULD be unique per
110+
// stream; 0 = no correlation requested (still echoed as 0).
111+
uint64 mutate_id = 4;
112+
113+
// A topic to subscribe, with the vector cursor to resume from.
114+
message Subscription {
115+
bytes topic = 1;
116+
// Resume point: deliver envelopes beyond this per-originator vector
117+
// cursor. Omitted/empty = from the beginning. Originators absent from
118+
// the cursor map are treated as sequence 0 (the node fills them in),
119+
// mirroring SubscribeTopics.TopicFilter.last_seen.
120+
xmtp.xmtpv4.envelopes.Cursor last_seen = 2;
121+
}
122+
}
123+
}
124+
}
125+
126+
// Node -> client.
127+
message SubscribeResponse {
128+
oneof version {
129+
V1 v1 = 1;
130+
}
131+
132+
message V1 {
133+
oneof response {
134+
Envelopes envelopes = 1;
135+
Started started = 2; // sent once, immediately on open, before any catch-up
136+
Ping ping = 3; // idle liveness challenge; receiver MUST answer with Pong
137+
Pong pong = 4; // answer to a client Ping
138+
TopicsLive topics_live = 5; // these topics just crossed from catch-up to live
139+
CatchupComplete catchup_complete = 6; // a Mutate's adds are fully delivered
140+
}
141+
142+
// A batch of envelopes across the active subscriptions; the client demuxes
143+
// by each envelope's target topic.
144+
message Envelopes {
145+
repeated xmtp.xmtpv4.envelopes.OriginatorEnvelope envelopes = 1;
146+
}
147+
148+
// The first frame on every stream.
149+
message Started {
150+
// The node's ping cadence (ms): the basis for the client's staleness
151+
// threshold and the node's reap deadline.
152+
uint32 keepalive_interval_ms = 1;
153+
// Optional protocol features the node supports on this stream. The node
154+
// silently ignores request types it does not understand, so a client MUST
155+
// NOT send an optional request type whose capability the node did not
156+
// advertise (it would hang waiting on a response that never comes).
157+
repeated Capability capabilities = 2;
158+
}
159+
160+
// Sent once per Mutate that adds subscriptions (a catch-up "wave"), after
161+
// the wave's last TopicsLive: everything the Mutate asked for is delivered.
162+
message CatchupComplete {
163+
uint64 mutate_id = 1; // echoes the Mutate that started this wave (0 if none given)
164+
}
165+
166+
// Emitted when topics finish catch-up, AFTER the last history frame for
167+
// them — including any live envelopes that queued behind the catch-up,
168+
// which were equally historical from the client's perspective — so every
169+
// later frame for a listed topic is live tail. Informational only: delivery
170+
// correctness (no duplicates, no gaps) never depends on it. Re-adding a
171+
// topic re-runs catch-up and re-emits it; receivers treat it idempotently.
172+
message TopicsLive {
173+
repeated bytes topics = 1; // kind-prefixed topics now tailing live
174+
}
175+
176+
// Optional per-stream protocol features (none defined yet; future revisions
177+
// add values, e.g. fetch-over-stream lookups answered with the same read
178+
// view that feeds the stream, or new streamable topic kinds).
179+
enum Capability {
180+
CAPABILITY_UNSPECIFIED = 0;
181+
}
182+
}
183+
}
184+
185+
// Liveness challenge/response for Subscribe, shared across versions. Either peer
186+
// MAY send a Ping; the receiver MUST reply with a Pong echoing the nonce. The
187+
// sender closes the stream if no Pong arrives within its deadline — how a node
188+
// reaps a vanished peer (e.g. a mobile client the OS suspended behind a proxy
189+
// that still ACKs the transport).
190+
message Ping {
191+
uint64 nonce = 1;
192+
}
193+
194+
message Pong {
195+
uint64 nonce = 1; // echoes the nonce of the Ping it answers
196+
}
197+
64198
// Batch subscribe to all envelopes
65199
message SubscribeAllEnvelopesRequest {}
66200

proto/xmtpv4/message_api/query_api.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ service QueryApi {
1414
rpc SubscribeTopics(SubscribeTopicsRequest)
1515
returns (stream SubscribeTopicsResponse) {}
1616

17+
// XIP-83 bidirectional mutable subscription: a single long-lived stream the
18+
// client mutates in place (add/remove topics) with ping/pong liveness, in
19+
// contrast to SubscribeTopics' fixed, immutable, server-streaming filter set.
20+
// Bidi streaming requires HTTP/2 (not grpc-web / connect-web); browser
21+
// clients stay on SubscribeTopics.
22+
rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeResponse) {}
23+
1724
rpc GetInboxIds(GetInboxIdsRequest) returns (GetInboxIdsResponse) {}
1825

1926
rpc GetNewestEnvelope(GetNewestEnvelopeRequest)

0 commit comments

Comments
 (0)