Skip to content

feat: XIP-83 bidirectional Subscribe RPC on MlsApi#337

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

feat: XIP-83 bidirectional Subscribe RPC on MlsApi#337
tylerhawkes merged 1 commit into
mainfrom
tyler/xip-83-subscribe-proto

Conversation

@tylerhawkes

@tylerhawkes tylerhawkes commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Adds the XIP-83 bidirectional, mutable subscription stream to the v3 MLS API: one long-lived Subscribe(stream SubscribeRequest) returns (stream SubscribeResponse) RPC.

  • Mutate-in-place subscriptions over kind-prefixed binary topics (the XIP-49 §3.3.2 representation shared with the d14n envelopes — thanks @insipx): Mutate{ adds: [{topic, id_cursor}], removes: [topics] }. One shape extends to future streamable kinds (key packages, identity updates) with no proto changes; v3 keeps its single uint64 cursor (0 = from the beginning; joiners SHOULD seed from the welcome's encrypted WelcomeMetadata.message_cursor). No reconnect on membership change.
  • Correlated catch-up waves: each adding Mutate carries a client-chosen mutate_id, echoed on that wave's CatchupComplete, so completions stay attributable when waves overlap.
  • Dedicated lifecycle arms Started{keepalive_interval_ms, capabilities} / CatchupComplete{mutate_id} instead of a status enum with side fields — frame metadata is unambiguous by construction (no proto3 presence games).
  • Live-boundary markers: TopicsLive{topics} after each topic's history.
  • history_only Mutates: bounded catch-up with no live registration — combined with client half-close this is the stream-native sync flow (server drains the wave, then closes).
  • WebSocket-style liveness: top-level Ping/Pong; version pinned per stream (V1 requests ⇒ V1 responses only).

Spec: xmtp/XIPs#139. Server implementation: xmtp/xmtp-node-go#562. Client: xmtp/libxmtp#3769–#3772.

🤖 Generated with Claude Code

Note

Add bidirectional Subscribe RPC to MlsApi for mutable topic subscriptions

  • Adds a new bidirectional streaming RPC MlsApi.Subscribe in mls.proto that accepts a stream of SubscribeRequest and returns a stream of SubscribeResponse.
  • SubscribeRequest supports atomic topic mutations (add/remove with history_only mode and mutate_id correlation), plus Ping/Pong liveness messages.
  • SubscribeResponse carries batched group and welcome messages, a Started handshake with keepalive interval, TopicsLive transition events, CatchupComplete acknowledgments, and Ping/Pong liveness.
  • The stream is designed as a single long-lived, mutable subscription that can multiplex group-message and welcome topics.
  • The RPC has no HTTP/grpc-gateway mapping and is gRPC-only.

Macroscope summarized 3988a8d.

@macroscopeapp

macroscopeapp Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a significant new bidirectional streaming RPC with complex protocol semantics (XIP-83). Additionally, there's an unresolved design comment questioning the cursor structure for V4 migration compatibility that should be addressed before merging.

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

Comment thread proto/mls/api/v1/mls.proto Outdated
Comment thread proto/mls/api/v1/mls.proto Outdated
@tylerhawkes tylerhawkes force-pushed the tyler/xip-83-subscribe-proto branch 2 times, most recently from 1a738fe to 902b69d Compare June 12, 2026 21:57
…ed topics; mutate_id-correlated waves; Started/CatchupComplete; ping/pong; TopicsLive; history_only)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
// WelcomeMetadata.message_cursor so a new membership does not refetch
// pre-join history it cannot decrypt; for a new installation's welcome
// topic, 0 is how pending welcomes are collected.
uint64 id_cursor = 2;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not want this to be a multi-cursor to make it easier to migrate to V4?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's something that each client can handle since we have a v3/d14n separation already.

@tylerhawkes tylerhawkes force-pushed the tyler/xip-83-subscribe-proto branch from 902b69d to 3988a8d Compare June 15, 2026 19:47
@tylerhawkes tylerhawkes merged commit ac2aa57 into main Jun 15, 2026
7 checks passed
@tylerhawkes tylerhawkes deleted the tyler/xip-83-subscribe-proto branch June 15, 2026 20:01
tylerhawkes added a commit to xmtp/libxmtp that referenced this pull request Jun 16, 2026
…#3769)

**Stack 1/4** of the XIP-83 bidi client lane: #3769#3770#3771#3772.

Regenerated `xmtp.mls.api.v1` from xmtp/proto#337: the bidirectional
`Subscribe` RPC with versioned `SubscribeRequest`/`SubscribeResponse`,
id-based `Mutate` (cursors, `history_only`), `Ping`/`Pong`,
`TopicsLive`, `CATCHUP_COMPLETE`, and STARTED `capabilities`. Purely
additive (+1,896 generated lines); `proto_version` pinned to that
branch's sha — draft until the proto PR merges.
tylerhawkes added a commit to xmtp/xmtp-node-go that referenced this pull request Jun 16, 2026
… ping/pong liveness) (#562)

Server side of XIP-83: one bidirectional `Subscribe` stream on the v3
MLS API that replaces repeated server-streaming subscriptions.

**Depends on xmtp/proto#337** (regenerated `pkg/proto/mls/api/v1` comes
from that branch) — draft until it merges.

## What's in here

- **Single-writer handler** (`pkg/mls/api/v1/subscribe.go`): the select
loop is the sole owner of all stream state and the sole caller of
`stream.Send` — no mutexes; catch-up fetchers and the frame reader are
pure producers over channels.
- **Mutate-in-place**: id-based add/remove with per-topic cursors;
`(*Subscription).Add/Remove` are O(1) under the dispatch lock
(`pkg/subscriptions`).
- **Batched catch-up**: chunked (256) bounded-pool (4) `unnest(...)
CROSS JOIN LATERAL` queries with per-group pagination
(`pkg/mls/store/readStore.go`); 2MB frame splitting; 64MB pending-buffer
cap.
- **Ordering guarantees**: live-gate before dispatcher Add + per-topic
high-water mark ⇒ history-before-live, no duplicates, no gaps.
- **Live-boundary signals**: `TopicsLive` after each topic's history
(including the drained pending buffer); one `CATCHUP_COMPLETE` per
adding Mutate (wave), after the wave's last marker.
- **Bounded catch-up**: `history_only` Mutates never register with the
dispatcher; client half-close drains in-flight waves then the server
closes `OK` (no pings post-half-close).
- **Liveness**: idle-triggered `Ping` (≤30s, resets on traffic), reap on
missed `Pong` (`DeadlineExceeded`), answers client pings.

## Testing

8 handler tests against real Postgres via a hand-written
`MlsApi_SubscribeServer` fake: catch-up-then-live no-dupes,
mutate-remove, ping/pong keepalive, reap-on-missed-pong, multi-identity
multiplexing, TopicsLive boundary ordering, history-only non-delivery,
half-close drain. All race-clean across 3 consecutive `-race` runs;
`golangci-lint` 0 issues.
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.

3 participants