syncweb_core/bridge/
service.rs1use 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 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 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 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 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 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 pub async fn leave_gossip(&self, topic_str: &str) {
170 self.gossip_topics.write().await.remove(topic_str);
171 }
172
173 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 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 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 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 #[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}