ping_core/transport.rs
1//! Transport trait — the SDK never opens a socket. Hosts implement this against their backend.
2
3use crate::{
4 conversation::ConversationId, device::DeviceId, identity::UserId, message::MessageEnvelope,
5 sync::SyncCursor, Result,
6};
7use std::fmt::Debug;
8use std::future::Future;
9use std::pin::Pin;
10use std::sync::Arc;
11
12// See `storage.rs` — same reason: `JsFuture` is `!Send` on wasm32. The native bound is kept so
13// UniFFI's tokio multi-thread runtime can schedule the future across worker threads.
14#[cfg(not(target_arch = "wasm32"))]
15pub type TransportFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
16#[cfg(target_arch = "wasm32")]
17pub type TransportFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + 'a>>;
18
19/// Server returns events strictly after the given cursor, sorted by (epoch ASC, server_seq ASC).
20/// May return at-most-`limit` envelopes; caller pages.
21pub trait Transport: Send + Sync + Debug {
22 /// Publish a single envelope. Servers may batch; callers should not depend on it.
23 fn send<'a>(&'a self, envelope: MessageEnvelope) -> TransportFuture<'a, ()>;
24
25 /// Pull events for one conversation since `cursor`. Empty list = caught up.
26 fn fetch_since<'a>(
27 &'a self,
28 conversation_id: ConversationId,
29 cursor: SyncCursor,
30 limit: u32,
31 ) -> TransportFuture<'a, Vec<MessageEnvelope>>;
32
33 /// Subscribe to live events. Returned subscription's drop cancels.
34 fn subscribe<'a>(
35 &'a self,
36 callback: Arc<dyn Fn(MessageEnvelope) + Send + Sync>,
37 ) -> TransportFuture<'a, TransportSubscription>;
38
39 /// Look up published KeyPackages for a user's currently-active devices.
40 fn discover_devices<'a>(
41 &'a self,
42 user_id: UserId,
43 ) -> TransportFuture<'a, Vec<DiscoveredDevice>>;
44
45 /// Notify the auth-layer server that `device_id` has been revoked
46 /// (`POST /v1/devices/{device_id}/revoke`). Called from
47 /// [`crate::MessagingClient::revoke_device`] AFTER each per-conversation
48 /// MLS Commit has been sent — i.e. peers learn about the removal via
49 /// MLS first, then the auth layer cleans up KeyPackages, refresh
50 /// tokens, and any future envelope rejection.
51 ///
52 /// Defaults to `Ok(())` so existing host transports (web, iOS,
53 /// Android) keep building while they wire the new endpoint up. Hosts
54 /// SHOULD override to call the backend; the SDK's MLS-side work is
55 /// sufficient for in-group hygiene, but auth-layer state (KeyPackage
56 /// pool, sessions on `auth.devices`) stays stale until this method
57 /// hits the server.
58 ///
59 /// Idempotent at the server: re-calling on an already-revoked device
60 /// is a no-op (the server returns success on the second call).
61 fn revoke_device_remote<'a>(&'a self, _device_id: DeviceId) -> TransportFuture<'a, ()> {
62 Box::pin(async move { Ok(()) })
63 }
64
65 /// Prime the host transport with the recipient device id(s) the NEXT
66 /// `Welcome` envelope on `conversation_id` should address. The web
67 /// host's MLS POST `/v1/messages` routes Welcomes via a
68 /// `?recipient=<device_id[,...]>` query param the BE needs to fan
69 /// the inbox row to the joiner — without it the BE rejects (400)
70 /// or stores the Welcome with no inbox route. Called by
71 /// [`crate::MessagingClient::admit_device_to_chats`] before each
72 /// per-chat `Welcome` send so hosts don't have to coordinate the
73 /// priming themselves.
74 ///
75 /// Defaults to `Ok(())` so non-web hosts (iOS/Android FFI bindings)
76 /// keep building without implementing this — they can route
77 /// Welcomes by other means (e.g. an envelope-side recipient field).
78 fn set_next_welcome_recipients<'a>(
79 &'a self,
80 _conversation_id: ConversationId,
81 _recipient_device_ids: Vec<DeviceId>,
82 ) -> TransportFuture<'a, ()> {
83 Box::pin(async move { Ok(()) })
84 }
85}
86
87#[derive(Debug, Clone)]
88pub struct DiscoveredDevice {
89 pub device_id: DeviceId,
90 pub key_package: Vec<u8>, // serialized OpenMLS KeyPackage
91}
92
93/// Cancels its subscription on drop.
94pub struct TransportSubscription {
95 pub(crate) cancel: Box<dyn FnOnce() + Send>,
96}
97
98impl std::fmt::Debug for TransportSubscription {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.write_str("TransportSubscription")
101 }
102}
103
104impl TransportSubscription {
105 pub fn new(cancel: impl FnOnce() + Send + 'static) -> Self {
106 Self {
107 cancel: Box::new(cancel),
108 }
109 }
110}
111
112impl Drop for TransportSubscription {
113 fn drop(&mut self) {
114 // Replace with a no-op closure so we can take ownership and call once.
115 let cancel = std::mem::replace(&mut self.cancel, Box::new(|| ()));
116 cancel();
117 }
118}