1use std::sync::Arc;
5use std::time::Duration;
6
7#[cfg(feature = "nats")]
8use futures::StreamExt;
9use parking_lot::RwLock;
10use serde::{Deserialize, Serialize};
11use time::{format_description::well_known::Rfc3339, OffsetDateTime};
12
13pub mod replication;
14pub mod swarm;
15pub mod world_events;
16
17type HelloHandler = dyn Fn(NodeHello) + Send + Sync;
18
19#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
21pub struct NodeHello {
22 pub node_id: String,
23 pub endpoint: String,
24 pub worlds_served: Vec<String>,
25 pub trust_score: f32,
26 pub started_at: String,
27 pub version: String,
28}
29
30#[derive(Clone, Debug)]
32pub struct PeerInfo {
33 pub hello: NodeHello,
34 pub last_seen: OffsetDateTime,
35 pub ttl: Duration,
36}
37
38#[derive(Default, Clone)]
40pub struct PeerTable {
41 inner: Arc<RwLock<Vec<PeerInfo>>>,
42}
43
44impl PeerTable {
45 pub fn upsert(&self, hello: NodeHello, ttl: Duration) {
46 let mut guard = self.inner.write();
47 let now = OffsetDateTime::now_utc();
48 if let Some(existing) = guard.iter_mut().find(|p| p.hello.node_id == hello.node_id) {
49 existing.hello = hello;
50 existing.last_seen = now;
51 existing.ttl = ttl;
52 } else {
53 guard.push(PeerInfo {
54 hello,
55 last_seen: now,
56 ttl,
57 });
58 }
59 }
60
61 pub fn peers(&self) -> Vec<NodeHello> {
62 self.prune();
63 self.inner.read().iter().map(|p| p.hello.clone()).collect()
64 }
65
66 fn prune(&self) {
67 let now = OffsetDateTime::now_utc();
68 let mut guard = self.inner.write();
69 guard.retain(|p| now - p.last_seen <= p.ttl);
70 }
71}
72
73pub trait NatsClient: Send + Sync {
75 fn publish_hello(&self, hello: &NodeHello) -> anyhow::Result<()>;
76 fn subscribe_hello(&self, handler: Box<HelloHandler>) -> anyhow::Result<()>;
77}
78
79#[derive(Default, Clone)]
81pub struct MockNats {
82 subs: Arc<RwLock<Vec<Arc<HelloHandler>>>>,
83}
84
85impl NatsClient for MockNats {
86 fn publish_hello(&self, hello: &NodeHello) -> anyhow::Result<()> {
87 for sub in self.subs.read().iter() {
88 sub(hello.clone());
89 }
90 Ok(())
91 }
92
93 fn subscribe_hello(&self, handler: Box<HelloHandler>) -> anyhow::Result<()> {
94 self.subs.write().push(handler.into());
95 Ok(())
96 }
97}
98
99#[cfg(feature = "nats")]
101pub struct AsyncNats {
102 client: async_nats::Client,
103 runtime: tokio::runtime::Runtime,
104}
105
106#[cfg(feature = "nats")]
107impl AsyncNats {
108 pub fn connect(url: &str) -> anyhow::Result<Self> {
109 let rt = tokio::runtime::Builder::new_multi_thread()
110 .enable_all()
111 .build()?;
112 let client = rt.block_on(async { async_nats::connect(url).await })?;
113 Ok(Self {
114 client,
115 runtime: rt,
116 })
117 }
118}
119
120#[cfg(feature = "nats")]
121impl NatsClient for AsyncNats {
122 fn publish_hello(&self, hello: &NodeHello) -> anyhow::Result<()> {
123 let payload = serde_json::to_vec(hello)?;
124 self.runtime.block_on(async {
125 self.client
126 .publish("dwbase.node.hello", payload.into())
127 .await
128 })?;
129 Ok(())
130 }
131
132 fn subscribe_hello(&self, handler: Box<HelloHandler>) -> anyhow::Result<()> {
133 let client = self.client.clone();
134 let handler = Arc::new(handler);
135 self.runtime.spawn(async move {
136 if let Ok(mut sub) = client.subscribe("dwbase.node.hello").await {
137 while let Some(msg) = sub.next().await {
138 if let Ok(hello) = serde_json::from_slice::<NodeHello>(&msg.payload) {
139 handler(hello);
140 }
141 }
142 }
143 });
144 Ok(())
145 }
146}
147
148pub fn start_presence_loop(
150 client: Arc<dyn NatsClient>,
151 hello: NodeHello,
152 table: PeerTable,
153 ttl: Duration,
154) -> std::thread::JoinHandle<()> {
155 let send_interval = ttl / 2;
156 table.upsert(hello.clone(), ttl);
158 let c = client.clone();
159 let h = hello.clone();
160 std::thread::spawn(move || loop {
161 let _ = c.publish_hello(&h);
162 std::thread::sleep(send_interval);
163 });
164
165 let handler_table = table.clone();
166 let _ = client.subscribe_hello(Box::new(move |msg| {
167 handler_table.upsert(msg, ttl);
168 }));
169
170 std::thread::spawn(|| {})
172}
173
174pub fn now_rfc3339() -> String {
175 OffsetDateTime::now_utc()
176 .format(&Rfc3339)
177 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".into())
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use std::time::Duration;
184
185 #[test]
186 fn peer_table_expires() {
187 let table = PeerTable::default();
188 table.upsert(
189 NodeHello {
190 node_id: "a".into(),
191 endpoint: "http://a".into(),
192 worlds_served: vec!["w1".into()],
193 trust_score: 1.0,
194 started_at: now_rfc3339(),
195 version: "v1".into(),
196 },
197 Duration::from_millis(10),
198 );
199 assert_eq!(table.peers().len(), 1);
200 std::thread::sleep(Duration::from_millis(30));
201 assert_eq!(table.peers().len(), 0);
202 }
203
204 #[test]
205 fn mock_bus_propagates_presence() {
206 let client = Arc::new(MockNats::default());
207 let table_a = PeerTable::default();
208 let table_b = PeerTable::default();
209 let hello_a = NodeHello {
210 node_id: "a".into(),
211 endpoint: "http://a".into(),
212 worlds_served: vec!["w1".into()],
213 trust_score: 0.9,
214 started_at: now_rfc3339(),
215 version: "v1".into(),
216 };
217 let hello_b = NodeHello {
218 node_id: "b".into(),
219 endpoint: "http://b".into(),
220 worlds_served: vec!["w2".into()],
221 trust_score: 0.8,
222 started_at: now_rfc3339(),
223 version: "v1".into(),
224 };
225 let ttl = Duration::from_millis(100);
226 start_presence_loop(client.clone(), hello_a.clone(), table_a.clone(), ttl);
227 start_presence_loop(client.clone(), hello_b.clone(), table_b.clone(), ttl);
228 std::thread::sleep(Duration::from_millis(100));
229 let peers_a = table_a.peers();
230 let peers_b = table_b.peers();
231 assert!(peers_a.iter().any(|p| p.node_id == "b"));
232 assert!(peers_b.iter().any(|p| p.node_id == "a"));
233 }
234}