Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/sync/
peer_types.rs

1//! Peer management types for the sync module.
2//!
3//! This module defines the data structures used to track remote peers,
4//! their sync relationships, and simple address information for transports.
5
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10use crate::auth::crypto::PublicKey;
11
12/// A peer's unique identifier, wrapping their [`PublicKey`].
13///
14/// This is syntactic sugar for a `PublicKey` used in the peer context.
15/// The serialized format is `ed25519:{base64_encoded_key}` (via `PublicKey`'s serde).
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(transparent)]
18pub struct PeerId(PublicKey);
19
20impl PeerId {
21    /// Create a new PeerId from a [`PublicKey`].
22    pub fn new(pk: PublicKey) -> Self {
23        Self(pk)
24    }
25
26    /// Get the underlying public key.
27    pub fn public_key(&self) -> &PublicKey {
28        &self.0
29    }
30}
31
32impl fmt::Display for PeerId {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38impl From<PublicKey> for PeerId {
39    fn from(pk: PublicKey) -> Self {
40        Self(pk)
41    }
42}
43
44impl From<&PublicKey> for PeerId {
45    fn from(pk: &PublicKey) -> Self {
46        Self(pk.clone())
47    }
48}
49
50/// Connection state for a peer.
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
52pub enum ConnectionState {
53    /// Not connected to the peer
54    Disconnected,
55    /// Currently attempting to connect
56    Connecting,
57    /// Successfully connected
58    Connected,
59    /// Connection failed with error message
60    Failed(String),
61}
62
63/// Simple address type containing transport type and address string
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
65pub struct Address {
66    /// Transport type identifier ("http", "iroh", etc.)
67    pub transport_type: String,
68    /// The actual address string
69    pub address: String,
70}
71
72impl Address {
73    /// Create a new Address
74    pub fn new(transport_type: impl Into<String>, address: impl Into<String>) -> Self {
75        Self {
76            transport_type: transport_type.into(),
77            address: address.into(),
78        }
79    }
80
81    // Helpers for the internally implemented Transports.
82
83    /// Create an HTTP address
84    pub fn http(address: impl Into<String>) -> Self {
85        Self::new("http", address)
86    }
87
88    /// Create an Iroh address from a node ID string
89    pub fn iroh(node_id: impl Into<String>) -> Self {
90        Self::new("iroh", node_id)
91    }
92}
93
94/// Information about a remote peer in the sync network.
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96pub struct PeerInfo {
97    /// The peer's unique identifier
98    pub id: PeerId,
99    /// Optional human-readable display name for the peer
100    pub display_name: Option<String>,
101    /// ISO timestamp when this peer was first seen
102    pub first_seen: String,
103    /// ISO timestamp when this peer was last seen/active
104    pub last_seen: String,
105    /// Current status of the peer
106    pub status: PeerStatus,
107    /// Connection addresses for this peer
108    pub addresses: Vec<Address>,
109    /// Current connection state
110    pub connection_state: ConnectionState,
111    /// ISO timestamp of last successful sync
112    pub last_successful_sync: Option<String>,
113    /// Number of connection attempts
114    pub connection_attempts: u32,
115    /// Last connection error if any
116    pub last_error: Option<String>,
117}
118
119/// Status of a remote peer in the sync network.
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
121pub enum PeerStatus {
122    /// Peer is active and available for sync
123    #[default]
124    Active,
125    /// Peer is inactive (not currently reachable)
126    Inactive,
127    /// Peer is blocked and should not be synced with
128    Blocked,
129}
130
131impl PeerInfo {
132    /// Create a new PeerInfo with the given timestamp.
133    ///
134    /// Use `instance.now_rfc3339()` or `transaction.now_rfc3339()` to get the timestamp.
135    pub fn new_at(id: impl Into<PeerId>, display_name: Option<&str>, timestamp: String) -> Self {
136        Self {
137            id: id.into(),
138            display_name: display_name.map(|s| s.to_string()),
139            first_seen: timestamp.clone(),
140            last_seen: timestamp,
141            status: PeerStatus::Active,
142            addresses: Vec::new(),
143            connection_state: ConnectionState::Disconnected,
144            last_successful_sync: None,
145            connection_attempts: 0,
146            last_error: None,
147        }
148    }
149
150    /// Update the last_seen timestamp.
151    ///
152    /// Use `instance.now_rfc3339()` or `transaction.now_rfc3339()` to get the timestamp.
153    pub fn touch_at(&mut self, timestamp: String) {
154        self.last_seen = timestamp;
155    }
156
157    /// Add an address if not already present
158    pub fn add_address(&mut self, address: Address) {
159        if !self.addresses.contains(&address) {
160            self.addresses.push(address);
161        }
162    }
163
164    /// Remove a specific address
165    pub fn remove_address(&mut self, address: &Address) -> bool {
166        let initial_len = self.addresses.len();
167        self.addresses.retain(|a| a != address);
168        self.addresses.len() != initial_len
169    }
170
171    /// Get addresses for a specific transport type
172    pub fn get_addresses(&self, transport_type: impl AsRef<str>) -> Vec<&Address> {
173        self.addresses
174            .iter()
175            .filter(|a| a.transport_type == transport_type.as_ref())
176            .collect()
177    }
178
179    /// Get all addresses
180    pub fn get_all_addresses(&self) -> &Vec<Address> {
181        &self.addresses
182    }
183
184    /// Check if peer has any addresses for a transport type
185    pub fn has_transport(&self, transport_type: impl AsRef<str>) -> bool {
186        self.addresses
187            .iter()
188            .any(|a| a.transport_type == transport_type.as_ref())
189    }
190}