@@ -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
65199message SubscribeAllEnvelopesRequest {}
66200
0 commit comments