Skip to main content

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}