Skip to main content

syncweb_core/bridge/
service.rs

1use std::{
2    collections::{HashMap, HashSet},
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use iroh_docs::NamespaceId;
8use iroh_gossip::api::GossipSender;
9use tokio::sync::{RwLock, broadcast};
10
11use crate::{
12    error::{Result, SyncwebError},
13    node::{gossip_service::GossipService, iroh_node::IrohNode},
14};
15
16const DOC_MAP_FILENAME: &str = "doc_map.json";
17
18#[derive(Clone, Debug)]
19pub struct ConnectedPeer {
20    pub node_id: String,
21    pub first_seen_secs: u64,
22    pub last_seen_secs: u64,
23}
24
25#[derive(Clone)]
26pub struct BridgeService {
27    node: Arc<IrohNode>,
28    doc_map: Arc<RwLock<HashMap<String, NamespaceId>>>,
29    gossip_topics: Arc<RwLock<HashMap<String, GossipSender>>>,
30    pub connected_peers: Arc<RwLock<HashMap<String, ConnectedPeer>>>,
31    blocked_peers: Arc<RwLock<HashSet<String>>>,
32    shutdown: broadcast::Sender<()>,
33    data_dir: PathBuf,
34}
35
36impl BridgeService {
37    /// Create a new bridge service.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the blocklist or doc map cannot be loaded.
42    pub fn new(node: Arc<IrohNode>, data_dir: PathBuf, shutdown: broadcast::Sender<()>) -> Result<Self> {
43        let blocklist_path = data_dir.join("blocklist.txt");
44        let blocked_peers = load_blocklist(&blocklist_path).unwrap_or_default();
45        let doc_map = load_doc_map(&data_dir.join(DOC_MAP_FILENAME)).unwrap_or_default();
46
47        Ok(Self {
48            node,
49            doc_map: Arc::new(RwLock::new(doc_map)),
50            gossip_topics: Arc::new(RwLock::new(HashMap::new())),
51            connected_peers: Arc::new(RwLock::new(HashMap::new())),
52            blocked_peers: Arc::new(RwLock::new(blocked_peers)),
53            shutdown,
54            data_dir,
55        })
56    }
57
58    #[must_use]
59    pub const fn node(&self) -> &Arc<IrohNode> {
60        &self.node
61    }
62
63    #[must_use]
64    pub fn shutdown_receiver(&self) -> broadcast::Receiver<()> {
65        self.shutdown.subscribe()
66    }
67
68    #[must_use]
69    pub fn is_blocked(&self, node_id: &str) -> bool {
70        self.blocked_peers.blocking_read().contains(node_id)
71    }
72
73    /// Get or create a doc namespace for a `collection_id`.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error if the namespace cannot be created or opened.
78    pub async fn get_or_create_collection(&self, collection_id: &str) -> Result<NamespaceId> {
79        {
80            let map = self.doc_map.read().await;
81            if let Some(ns) = map.get(collection_id).copied() {
82                return Ok(ns);
83            }
84        }
85
86        let mut map = self.doc_map.write().await;
87        if let Some(ns) = map.get(collection_id).copied() {
88            return Ok(ns);
89        }
90
91        let doc = self.node.docs_engine().create_namespace().await?;
92        let namespace = self.node.docs_engine().namespace_id(&doc);
93        self.node.docs_engine().start_sync(&doc, Vec::new()).await?;
94        map.insert(collection_id.to_owned(), namespace);
95        drop(map);
96        self.persist_doc_map().await;
97        Ok(namespace)
98    }
99
100    /// Get an existing collection namespace.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if the namespace cannot be opened.
105    pub async fn get_collection(&self, collection_id: &str) -> Result<NamespaceId> {
106        let map = self.doc_map.read().await;
107        map.get(collection_id).copied().ok_or_else(|| {
108            SyncwebError::operation(
109                "collection not found",
110                format!("no namespace for collection_id: {collection_id}"),
111            )
112        })
113    }
114
115    /// Insert a doc namespace for a `collection_id` (used by `import_collection`).
116    pub async fn insert_collection(&self, collection_id: &str, namespace: NamespaceId) {
117        {
118            let mut map = self.doc_map.write().await;
119            map.insert(collection_id.to_owned(), namespace);
120        }
121        self.persist_doc_map().await;
122    }
123
124    async fn persist_doc_map(&self) {
125        let path = self.data_dir.join(DOC_MAP_FILENAME);
126        let content = self.serialize_doc_map().await;
127        if let Some(data) = content {
128            let _ = tokio::fs::write(&path, data).await;
129        }
130    }
131
132    async fn serialize_doc_map(&self) -> Option<String> {
133        let serializable: HashMap<String, NamespaceId> = {
134            let guard = self.doc_map.read().await;
135            guard.iter().map(|(k, v)| (k.clone(), *v)).collect()
136        };
137        serde_json::to_string_pretty(&serializable).ok()
138    }
139
140    /// Subscribe to a gossip topic.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if subscription fails.
145    pub async fn subscribe_gossip(&self, topic_str: &str, with_discovery: bool) -> Result<GossipSender> {
146        if let Some(sender) = self.gossip_topics.read().await.get(topic_str) {
147            return Ok(sender.clone());
148        }
149
150        let topic_id = topic_id_from_str(topic_str);
151        let topic = if with_discovery {
152            self.node
153                .gossip_service()
154                .subscribe_and_join(topic_id, Vec::new())
155                .await?
156        } else {
157            self.node.gossip_service().subscribe(topic_id, Vec::new()).await?
158        };
159        let (sender, _receiver) = GossipService::split(topic);
160
161        self.gossip_topics
162            .write()
163            .await
164            .insert(topic_str.to_owned(), sender.clone());
165        Ok(sender)
166    }
167
168    /// Leave a gossip topic.
169    pub async fn leave_gossip(&self, topic_str: &str) {
170        self.gossip_topics.write().await.remove(topic_str);
171    }
172
173    /// Send a gossip message to a topic.
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if the topic is not subscribed or publishing fails.
178    pub async fn send_gossip(&self, topic_str: &str, message: &[u8]) -> Result<()> {
179        let sender = self
180            .gossip_topics
181            .read()
182            .await
183            .get(topic_str)
184            .ok_or_else(|| {
185                SyncwebError::operation("gossip topic not subscribed", format!("not subscribed to: {topic_str}"))
186            })?
187            .clone();
188        self.node.gossip_service().publish(&sender, message).await
189    }
190
191    /// Add a connected peer.
192    pub fn add_peer(&self, node_id: String, first_seen: u64, last_seen: u64) {
193        let peer = ConnectedPeer {
194            node_id: node_id.clone(),
195            first_seen_secs: first_seen,
196            last_seen_secs: last_seen,
197        };
198        self.connected_peers.blocking_write().insert(node_id, peer);
199    }
200
201    /// Block a peer.
202    ///
203    /// # Errors
204    ///
205    /// Returns an error if the blocklist file cannot be written.
206    pub fn block_peer(&self, node_id: &str) -> Result<()> {
207        self.blocked_peers.blocking_write().insert(node_id.to_owned());
208        self.save_blocklist()
209    }
210
211    /// Unblock a peer.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if the blocklist file cannot be written.
216    pub fn unblock_peer(&self, node_id: &str) -> Result<()> {
217        self.blocked_peers.blocking_write().remove(node_id);
218        self.save_blocklist()
219    }
220
221    /// Get blocked peers.
222    #[must_use]
223    pub fn get_blocked_peers(&self) -> Vec<String> {
224        let peers = self.blocked_peers.blocking_read();
225        peers.iter().cloned().collect()
226    }
227
228    fn save_blocklist(&self) -> Result<()> {
229        let path = self.data_dir.join("blocklist.txt");
230        let peers = self.get_blocked_peers();
231        let content = peers.join("\n");
232        std::fs::write(&path, content).map_err(|error| SyncwebError::operation("failed to write blocklist", error))?;
233        Ok(())
234    }
235}
236
237fn topic_id_from_str(topic_str: &str) -> iroh_gossip::TopicId {
238    let hash = *blake3::hash(topic_str.as_bytes()).as_bytes();
239    let mut arr = [0_u8; 32];
240    arr.copy_from_slice(&hash[..32]);
241    iroh_gossip::TopicId::from_bytes(arr)
242}
243
244fn load_blocklist(path: &Path) -> Result<HashSet<String>> {
245    let content =
246        std::fs::read_to_string(path).map_err(|error| SyncwebError::operation("failed to read blocklist", error))?;
247    Ok(content.lines().map(String::from).collect())
248}
249
250fn load_doc_map(path: &Path) -> Result<HashMap<String, NamespaceId>> {
251    let content =
252        std::fs::read_to_string(path).map_err(|error| SyncwebError::operation("failed to read doc map", error))?;
253    serde_json::from_str(&content).map_err(|error| SyncwebError::operation("failed to parse doc map", error))
254}