openrtc 1.0.4

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
use anyhow::Result;
use async_trait::async_trait;
use futures::stream::BoxStream;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeviceCapabilities {
    pub can_host: bool,
    pub can_sync: bool,
    pub read_only: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Device {
    pub app_tag: Option<String>,
    pub device_id: String,
    pub user_id: Option<String>,
    pub device_name: String,
    pub platform_type: Option<String>,
    pub capabilities: Option<DeviceCapabilities>,
    pub session_id: Option<String>,
    pub node_id: Option<String>,
    pub tag: Option<String>,
    pub kind: Option<String>,
    pub metadata: Option<String>,
    pub online: bool,
    pub ticket: Option<String>,
    pub last_seen_at: Option<serde_json::Value>,
    pub expires_at: Option<serde_json::Value>,
    pub created_at: Option<serde_json::Value>,
    pub updated_at: Option<serde_json::Value>,
    /// Device IDs that this peer has explicitly excluded from auto-connect.
    /// Set when the peer manually disconnects from a device; cleared on manual reconnect.
    /// All other peers check this before dialing in.
    #[serde(default)]
    pub excluded_peers: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct E2eeInfo {
    pub session_key_id: String,
    pub cipher_text: String,
    pub iv: String,
    pub salt: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalingSession {
    pub connection_id: String,
    pub initiator: String,
    pub target: String,
    pub initiator_device_id: String,
    pub target_device_id: String,
    pub connection_type: Option<String>,
    pub offer: Option<serde_json::Value>,
    pub offer_e2ee: Option<E2eeInfo>,
    pub answer: Option<serde_json::Value>,
    pub answer_e2ee: Option<E2eeInfo>,
    #[serde(default)]
    pub ice_candidates: Vec<serde_json::Value>,
    pub initiator_node_id: Option<String>,
    pub target_node_id: Option<String>,
    pub initiator_endpoint_addr: Option<String>,
    pub target_endpoint_addr: Option<String>,
    pub intent: Option<String>,
    pub app_tag: Option<String>,
    pub created_at: Option<i64>,
    pub expires_at: Option<i64>,
    pub state: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SessionEvent {
    Added {
        session: SignalingSession,
    },
    Modified {
        session: SignalingSession,
    },
    Removed {
        #[serde(rename = "sessionId")]
        session_id: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum DeviceEvent {
    Added {
        device: Device,
    },
    Modified {
        device: Device,
    },
    Removed {
        #[serde(rename = "deviceId")]
        device_id: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalingEnvelope {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub app_tag: Option<String>,
    pub sender_id: String,
    pub target_id: String,
    pub payload: String,
    pub state: Option<String>,
    pub reply_payload: Option<String>,
    pub timestamp: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender_user_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_user_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<i64>,
}

#[cfg(target_arch = "wasm32")]
pub trait SendSyncBound {}
#[cfg(target_arch = "wasm32")]
impl<T> SendSyncBound for T {}

#[cfg(not(target_arch = "wasm32"))]
pub trait SendSyncBound: Send + Sync {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Send + Sync> SendSyncBound for T {}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SignalingBackend: SendSyncBound {
    async fn update_presence(
        &self,
        user_id: &str,
        local_node_id: &str,
        ticket_str: &str,
        is_online: bool,
        name: &str,
        ttl_ms: u64,
        metadata: Option<&str>,
    ) -> Result<()>;

    async fn set_offline(&self, user_id: &str, local_node_id: &str) -> Result<()>;

    /// Publish hot liveness/ticket state for a device.
    ///
    /// Firestore `update_presence` is the durable roster record. Implementations
    /// with a cheaper live-presence plane, such as RTDB, override this so native
    /// clients do not need Firestore heartbeat writes.
    async fn update_live_presence(
        &self,
        _user_id: &str,
        _local_node_id: &str,
        _ticket_str: &str,
        _name: &str,
        _metadata: Option<&str>,
    ) -> Result<()> {
        #[cfg(target_arch = "wasm32")]
        return Ok(());

        #[cfg(not(target_arch = "wasm32"))]
        anyhow::bail!("native signaling backend does not implement RTDB live presence")
    }

    /// Mark the live-presence plane offline without requiring a Firestore
    /// heartbeat write.
    async fn set_live_presence_offline(&self, _user_id: &str, _local_node_id: &str) -> Result<()> {
        #[cfg(target_arch = "wasm32")]
        return Ok(());

        #[cfg(not(target_arch = "wasm32"))]
        anyhow::bail!("native signaling backend does not implement RTDB offline presence")
    }

    async fn update_device(
        &self,
        user_id: &str,
        device_id: &str,
        device_name: Option<&str>,
        capabilities: Option<DeviceCapabilities>,
        metadata: Option<&str>,
    ) -> Result<()>;

    async fn delete_device(&self, user_id: &str, device_id: &str) -> Result<()>;

    /// Update the `excludedPeers` list on the local device's presence document.
    /// Called on disconnect (add `remote_device_id`) and reconnect (remove it).
    async fn set_excluded_peers(
        &self,
        user_id: &str,
        local_node_id: &str,
        excluded_peers: &[String],
    ) -> Result<()>;

    async fn search_devices(
        &self,
        user_id: &str,
        exclude_node_id: Option<&str>,
    ) -> Result<Vec<Device>>;

    async fn list_devices(
        &self,
        user_id: &str,
        exclude_node_id: Option<&str>,
    ) -> Result<Vec<Device>>;

    async fn send_message(
        &self,
        sender_id: &str,
        target_id: &str,
        payload: &str,
        state: Option<&str>,
        reply_payload: Option<&str>,
    ) -> Result<String>;

    async fn subscribe_devices(
        &self,
        user_id: &str,
    ) -> Result<BoxStream<'static, Result<Vec<DeviceEvent>>>>;

    async fn create_session(&self, session: SignalingSession) -> Result<()>;

    async fn update_session(&self, session_id: &str, update_data: serde_json::Value) -> Result<()>;

    async fn subscribe_sessions(
        &self,
        local_device_id: &str,
    ) -> Result<BoxStream<'static, Result<Vec<SessionEvent>>>>;
}