beam/node.rs
1//! Graph node — the core API for reading and writing data in the BEAM graph.
2//!
3//! [`Node`] is the primary user-facing type. It represents a node in the
4//! distributed graph database and provides methods for:
5//!
6//! - **Reading**: [`Node::get`] (traverse to child), [`Node::on`] (subscribe
7//! to value updates), [`Node::once`] (read once), [`Node::map`] (subscribe
8//! to all children)
9//! - **Writing**: [`Node::put`] (set a value), [`Node::batch_put`] (atomic
10//! multi-write)
11//! - **Networking**: [`Node::connect_peer`] (WebSocket), [`Node::connect_webrtc_peer`]
12//! (WebRTC, feature-gated)
13//! - **Lifecycle**: [`Node::stop`]
14//!
15//! # Architecture
16//!
17//! A `Node` is cheaply cloneable (uses `Arc` internally). Each clone shares
18//! the same underlying state. Child nodes are created lazily via `get()` and
19//! are backed by their own actor + broadcast channels.
20//!
21//! The root node owns a [`Router`] actor that manages storage and network
22//! adapters, message deduplication, and peer routing. All `put` and `get`
23//! operations flow through the router.
24//!
25//! # Example
26//!
27//! ```ignore
28//! use beam::{Node, Value};
29//!
30//! let mut db = Node::new();
31//! let mut sub = db.get("greeting").on();
32//! db.get("greeting").put("Hello World!".into());
33//! // sub.recv().await == Some(Value::Text("Hello World!"))
34//! ```
35
36use crate::ack::{AckPolicy, QUORUM_MET_SENTINEL, ReplicationStatus};
37use crate::actor::{Actor, ActorContext, Addr};
38use crate::adapters::MemoryStorage;
39use crate::message::{BatchPut, Flush, Get, Message, Put};
40use crate::metrics::Metrics;
41use crate::router::Router;
42use crate::types::{Children, NodeData, Value};
43use crate::utils::random_string;
44use async_trait::async_trait;
45use futures_util::StreamExt;
46use log::{debug, info, warn};
47use parking_lot::RwLock;
48use std::collections::BTreeMap;
49use std::collections::HashMap;
50use std::sync::Arc;
51use std::time::{Duration, SystemTime};
52use tokio::sync::watch;
53use tokio::sync::{broadcast, oneshot};
54use tokio_tungstenite::connect_async;
55
56/// Configuration for a [`Node`] and its associated adapters.
57///
58/// Controls public space access, broadcast channel sizing,
59/// and WebRTC ICE server configuration.
60#[derive(Clone)]
61pub struct Config {
62 /// Whether to accept writes to public space (non-user-owned nodes).
63 ///
64 /// When `true` (default), the node accepts puts to any node ID. When
65 /// `false`, only content-addressed (signed) data and user-owned nodes
66 /// are accepted — matching Gun.js `opt.enforce` semantics.
67 pub allow_public_space: bool,
68 /// Public key to prioritize for this node (format: `x.y`).
69 ///
70 /// When set, the node will preferentially cache data owned by this
71 /// public key. Used for user-authenticated nodes.
72 pub my_pub: Option<String>,
73 /// Buffer size for broadcast channels used by `on()` and `map()`.
74 ///
75 /// Defaults to 4096. Increase for high-throughput scenarios; decrease
76 /// to save memory per active subscription.
77 pub broadcast_buffer_size: usize,
78 /// STUN/TURN servers for WebRTC ICE negotiation.
79 ///
80 /// Defaults to Google's public STUN server. Only used when the
81 /// `webrtc` feature is enabled.
82 pub ice_servers: Vec<String>,
83}
84
85impl Default for Config {
86 fn default() -> Self {
87 Config {
88 allow_public_space: true,
89 my_pub: None,
90 broadcast_buffer_size: 4096,
91 ice_servers: vec!["stun:stun.l.google.com:19302".to_string()],
92 }
93 }
94}
95
96/// A graph node — the primary API for reading and writing data in BEAM.
97///
98/// A `Node` represents a position in the distributed graph. The root node
99/// (created via [`Node::new`] or [`Node::new_with_config`]) owns the router
100/// and all adapter actors. Child nodes (created via [`Node::get`]) share
101/// the router and communicate through broadcast channels.
102///
103/// # Cloning
104///
105/// `Node` is cheaply cloneable. Clones share the same underlying state
106/// via `Arc`. This is important for async patterns where you need to
107/// send a node into multiple futures.
108///
109/// # Concurrency
110///
111/// Each node runs as a tokio actor, processing [`Message::Put`] messages
112/// in its `handle()` method. Reads are served via `broadcast` channels —
113/// `on()` returns a `Receiver<Value>` and `map()` returns a
114/// `Receiver<(String, Value)>`.
115/// Type alias for the pending put acknowledgment sender.
116///
117/// Each entry is a oneshot sender that resolves when storage adapters
118/// acknowledge a put operation. The key is the put's `id`.
119type PendingPuts = Arc<RwLock<HashMap<String, oneshot::Sender<Result<ReplicationStatus, String>>>>>;
120
121#[derive(Clone)]
122pub struct Node {
123 uid: Arc<RwLock<String>>,
124 path: Vec<String>,
125 children: Arc<RwLock<BTreeMap<String, Node>>>,
126 parent: Arc<RwLock<Option<(String, Node)>>>,
127 broadcast_buffer_size: usize,
128 on_sender: broadcast::Sender<Value>,
129 map_sender: broadcast::Sender<(String, Value)>,
130 actor_context: Box<ActorContext>,
131 /// Shutdown signal sender — broadcasts `true` to all child tasks for graceful shutdown.
132 shutdown_tx: watch::Sender<bool>,
133 addr: Arc<RwLock<Option<Addr>>>,
134 router: Arc<RwLock<Option<Addr>>>,
135 pending_flushes: Arc<RwLock<HashMap<String, oneshot::Sender<()>>>>,
136 /// Pending `put` acknowledgements keyed by `Put.id`.
137 ///
138 /// When `Node::put` (or `Node::batch_put`) is called, a oneshot sender
139 /// is registered here keyed by the put's `id`. Storage adapters ack
140 /// the put by sending a `Put` message with `in_response_to: Some(id)`
141 /// back to this node's `addr`. `Node::handle_put` intercepts the ack
142 /// and completes the oneshot, resolving the awaited future.
143 ///
144 /// The sender's payload carries `Result<(), String>` so commit failures
145 /// propagate to the caller instead of being silently swallowed.
146 pending_puts: PendingPuts,
147 allow_public_space: bool,
148 ice_servers: Vec<String>,
149 /// Shared lock-free observability counters.
150 ///
151 /// Cloned via `Arc` to the [`Router`] at construction time so the
152 /// Router's internal `try_send_or_log` calls and any external
153 /// observer (e.g. e2e tests, telemetry exporter) share the same
154 /// underlying atomic counters. See [`crate::metrics::Metrics`].
155 metrics: Arc<Metrics>,
156}
157
158#[async_trait]
159impl Actor for Node {
160 async fn handle(&mut self, msg: Message, _context: &ActorContext) {
161 if let Message::Put(put) = msg {
162 self.handle_put(put)
163 }
164 }
165}
166
167impl Node {
168 /// Creates a new root-level node with default configuration and in-memory storage.
169 ///
170 /// This is the simplest way to get started with BEAM. The node will use
171 /// [`MemoryStorage`] and have no network adapters connected.
172 pub fn new() -> Self {
173 let storage = MemoryStorage::new();
174 Self::new_with_config(Config::default(), vec![Box::new(storage)], Vec::new())
175 }
176
177 /// Returns the unique identifier of this node (the path joined by `/`).
178 ///
179 /// The root node has an empty `uid`. Child nodes have uids like
180 /// `"parent_key/child_key"`.
181 pub fn id(&self) -> String {
182 self.uid.read().clone()
183 }
184
185 /// Returns the peer ID of this node's actor context.
186 ///
187 /// The peer ID is a random string generated at node creation time.
188 /// It identifies this node instance in the P2P mesh.
189 pub fn peer_id(&self) -> String {
190 self.actor_context.peer_id.read().clone()
191 }
192
193 /// Creates a new root-level node with custom configuration, storage, and network adapters.
194 ///
195 /// # Arguments
196 ///
197 /// * `config` - Node configuration (see [`Config`])
198 /// * `storage_adapters` - Storage actors (e.g. [`MemoryStorage`], `RedbStorage`)
199 /// * `network_adapters` - Network actors (e.g. `OutgoingWebsocketManager`, `WsServer`)
200 pub fn new_with_config(
201 config: Config,
202 storage_adapters: Vec<Box<dyn Actor>>,
203 network_adapters: Vec<Box<dyn Actor>>,
204 ) -> Self {
205 // Shared observability handle — cloned to Router so both observe
206 // the same atomic counters. External observers reach this via
207 // `Node::metrics()`.
208 let metrics = Arc::new(Metrics::new());
209
210 let (shutdown_tx, shutdown_rx) = watch::channel(false);
211 let mut actor_context = ActorContext::new(random_string(16));
212 actor_context.shutdown_rx = shutdown_rx;
213 let mut node = Self {
214 path: vec![],
215 uid: Arc::new(RwLock::new("".to_string())),
216 children: Arc::new(RwLock::new(BTreeMap::new())),
217 parent: Arc::new(RwLock::new(None)),
218 broadcast_buffer_size: config.broadcast_buffer_size,
219 on_sender: broadcast::channel::<Value>(config.broadcast_buffer_size).0,
220 map_sender: broadcast::channel::<(String, Value)>(config.broadcast_buffer_size).0,
221 addr: Arc::new(RwLock::new(None)),
222 router: Arc::new(RwLock::new(None)),
223 pending_flushes: Arc::new(RwLock::new(HashMap::new())),
224 pending_puts: Arc::new(RwLock::new(HashMap::new())),
225 allow_public_space: config.allow_public_space,
226 ice_servers: config.ice_servers.clone(),
227 actor_context: Box::new(actor_context),
228 shutdown_tx,
229 metrics: metrics.clone(),
230 };
231
232 node.actor_context.node = Some(node.clone());
233 let addr = node.actor_context.start_actor(Box::new(node.clone()));
234 *node.addr.write() = Some(addr);
235
236 let router = Box::new(Router::new(storage_adapters, network_adapters, metrics));
237 let router_addr = node.actor_context.start_router(router);
238 node.actor_context.router = router_addr.clone();
239 *node.router.write() = Some(router_addr);
240
241 node
242 }
243
244 /// Returns a clone of the shared `Arc<Metrics>` handle.
245 ///
246 /// The returned Arc points to the same atomic counters as the
247 /// Router's internal field. External observers (tests, telemetry
248 /// exporters) read counters via [`crate::metrics::Metrics::snapshot`]
249 /// or record events via [`crate::metrics::Metrics::record_dropped_send`].
250 ///
251 /// Cloning the Arc is cheap (refcount bump); the atomic counters
252 /// are shared across all clones.
253 pub fn metrics(&self) -> Arc<Metrics> {
254 self.metrics.clone()
255 }
256
257 /// Handles incoming [`Put`] messages by dispatching values to subscribers.
258 ///
259 /// If the put is a flush acknowledgement (has `in_response_to` matching a
260 /// pending flush), the flush's oneshot sender is triggered instead.
261 ///
262 /// For replay puts (have `in_response_to`), a `__beam_replay_complete__`
263 /// marker is sent on the map channel after all child values are dispatched.
264 fn handle_put(&mut self, put: Put) {
265 // Intercept acks BEFORE processing the message as data. Two ack
266 // channels share the `in_response_to` field:
267 //
268 // 1. **Flush acks** — see [`Node::flush_storage`]. Payload is unit `()`;
269 // any `Put` with `in_response_to` matching a `pending_flushes` key
270 // resolves that barrier. Flush acks carry `_flushed` (no payload
271 // discrimination needed — the presence of a match IS the success).
272 //
273 // 2. **Put/BatchPut acks** — sent directly from storage adapters
274 // after commit. Payload is `Result<(), String>` decoded via
275 // [`Self::decode_put_ack_payload`]. The storage adapter chooses
276 // between `_ack` (success) and `_err:<msg>` (failure).
277 //
278 // We check `pending_flushes` first because Flush was the original
279 // ack pattern and remains the simplest. Put acks are a strictly
280 // additive extension.
281 if let Some(response_id) = &put.in_response_to {
282 // Flush acks — any payload, presence is success
283 if let Some(sender) = self.pending_flushes.write().remove(response_id) {
284 let _ = sender.send(());
285 return;
286 }
287 // Put/BatchPut acks — try quorum sentinel first, fall back to _ack/_err
288 if let Some(sender) = self.pending_puts.write().remove(response_id) {
289 let result = if let Some(quorum_result) = Node::decode_quorum_payload(&put) {
290 // Router fired __quorum_met__ — either peer-ack quorum
291 // satisfied (Ok) or cleanup reaper timed us out (Err).
292 quorum_result
293 } else {
294 // Local storage _ack/_err reply — wrap as minimal status
295 Self::decode_put_ack_payload(&put).map(|()| ReplicationStatus {
296 put_id: response_id.clone(),
297 acked_by: 1,
298 quorum_met: true,
299 elapsed: Duration::ZERO,
300 })
301 };
302 let _ = sender.send(result);
303 return;
304 }
305 }
306 let is_replay = put.in_response_to.is_some();
307 for (node_id, node_data) in put.updated_nodes {
308 if node_id == *self.uid.read() {
309 for (child, child_data) in node_data {
310 // Skip internal control keys
311 if child.starts_with("__beam_") {
312 continue;
313 }
314 if let Some(child) = self.children.read().get(&child) {
315 let _ = child.on_sender.send(child_data.value.clone());
316 }
317 let _ = self
318 .map_sender
319 .send((child.to_string(), child_data.value.clone()));
320 }
321 if is_replay {
322 let _ = self
323 .map_sender
324 .send(("__beam_replay_complete__".to_string(), Value::Null));
325 }
326 }
327 }
328 }
329
330 /// Flushes pending writes to persistent storage and waits for acknowledgement.
331 ///
332 /// Sends a [`Flush`] message to all storage adapters via the router, then
333 /// waits for the first adapter to acknowledge. If no acknowledgement
334 /// arrives within the timeout, returns an error.
335 ///
336 /// # Arguments
337 ///
338 /// * `timeout` - Maximum time to wait. Defaults to 30 seconds if `None`.
339 ///
340 /// # Errors
341 ///
342 /// - `"router not initialized"` — node has no router (shouldn't happen in normal use)
343 /// - `"failed to send flush to router"` — router channel is closed
344 /// - `"flush ack channel closed"` — oneshot sender was dropped
345 /// - `"flush timed out"` — no acknowledgement within timeout
346 pub async fn flush_storage(&self, timeout: Option<Duration>) -> Result<(), String> {
347 let router_addr = match &*self.router.read() {
348 Some(addr) => addr.clone(),
349 None => return Err("router not initialized".to_string()),
350 };
351 let flush = Flush::new(self.addr.read().clone().unwrap(), None);
352 let id = flush.id.clone();
353 let (tx, rx) = oneshot::channel();
354 self.pending_flushes.write().insert(id.clone(), tx);
355
356 if let Err(_e) = router_addr.send(Message::Flush(flush)) {
357 self.pending_flushes.write().remove(&id);
358 return Err("failed to send flush to router".to_string());
359 }
360
361 let dur = timeout.unwrap_or(Duration::from_secs(30));
362 match tokio::time::timeout(dur, rx).await {
363 Ok(Ok(())) => Ok(()),
364 Ok(Err(_)) => Err("flush ack channel closed".to_string()),
365 Err(_) => {
366 self.pending_flushes.write().remove(&id);
367 Err("flush timed out".to_string())
368 }
369 }
370 }
371
372 fn new_child(&self, key: String) -> Node {
373 assert!(!key.is_empty(), "Key length must be greater than zero");
374 let mut path = self.path.clone();
375 path.push(key.clone());
376 let new_child_uid = path.join("/");
377 debug!("new_child_uid {}", new_child_uid);
378 let node = Self {
379 path,
380 children: Arc::new(RwLock::new(BTreeMap::new())),
381 parent: Arc::new(RwLock::new(Some((self.uid.read().clone(), self.clone())))),
382 broadcast_buffer_size: self.broadcast_buffer_size,
383 on_sender: broadcast::channel::<Value>(self.broadcast_buffer_size).0,
384 map_sender: broadcast::channel::<(String, Value)>(self.broadcast_buffer_size).0,
385 uid: Arc::new(RwLock::new(new_child_uid)),
386 router: self.router.clone(),
387 pending_flushes: Arc::new(RwLock::new(HashMap::new())),
388 pending_puts: Arc::new(RwLock::new(HashMap::new())),
389 addr: Arc::new(RwLock::new(None)),
390 actor_context: self.actor_context.clone(),
391 allow_public_space: self.allow_public_space,
392 ice_servers: self.ice_servers.clone(),
393 // Children share the parent's metrics Arc so all nodes in a
394 // tree aggregate drops into the same counters.
395 metrics: self.metrics.clone(),
396 shutdown_tx: self.shutdown_tx.clone(),
397 };
398 let addr = self.actor_context.start_actor(Box::new(node.clone()));
399 *node.addr.write() = Some(addr);
400 let mut guard = self.children.write();
401 guard.insert(key, node.clone());
402 node
403 }
404
405 /// Subscribes to this node's value updates.
406 ///
407 /// Returns a [`broadcast::Receiver`] that will receive [`Value`] updates
408 /// whenever the node's value changes. The current value (if any) is
409 /// requested from storage via a `Get` message — it arrives asynchronously.
410 pub fn on(&mut self) -> broadcast::Receiver<Value> {
411 let key = if self.path.len() > 1 {
412 self.path.last().cloned()
413 } else {
414 None
415 };
416 let addr;
417 let node_id;
418 if let Some((parent_id, parent)) = &*self.parent.read() {
419 node_id = parent_id.clone();
420 addr = parent.addr.read().clone().unwrap();
421 } else {
422 node_id = self.uid.read().to_string();
423 addr = self.addr.read().clone().unwrap();
424 }
425 let get = Get::new(node_id, key, addr);
426 // subscribe before send so we don't miss the response
427 let subscriber = self.on_sender.subscribe();
428 if let Some(router) = self.router.read().clone() {
429 let _ = router.send(Message::Get(get));
430 }
431 subscriber
432 }
433
434 /// Reads the node's value once, or `None` if not found within the timeout.
435 ///
436 /// This is a convenience wrapper around [`Node::on`] with a timeout.
437 /// The default timeout is 66ms (matching Gun.js's `opt.wait`).
438 ///
439 /// # Arguments
440 ///
441 /// * `wait` - Optional timeout. Defaults to 66ms.
442 pub async fn once(&mut self, wait: Option<Duration>) -> Option<Value> {
443 let val = tokio::time::timeout(wait.unwrap_or(Duration::from_millis(66)), self.on().recv())
444 .await
445 .ok()?
446 .expect("recv error??");
447 Some(val)
448 }
449
450 /// Connects to a remote peer via WebSocket with automatic reconnection.
451 ///
452 /// Retries with exponential backoff starting at 1 second, maxing at 60
453 /// seconds. The spawned `WsConn` actor auto-handshakes via [`Message::Hi`]
454 /// on `pre_start`, and the [`Router`] registers the peer on `Hi` receipt.
455 ///
456 /// # Arguments
457 ///
458 /// * `url` - WebSocket URL (e.g. `"wss://relay.example.com/ws"`)
459 ///
460 /// # Panics
461 ///
462 /// Panics if the URL is invalid (should not happen with well-formed URLs).
463 pub fn connect_peer(&self, url: &str) {
464 let ctx = self.actor_context.clone();
465 let ctx_for_actor = ctx.clone();
466 let url = url.to_string();
467 let allow_public_space = self.allow_public_space;
468 ctx.child_task(async move {
469 let ctx = ctx_for_actor;
470 let mut backoff = Duration::from_secs(1);
471 let max_backoff = Duration::from_secs(60);
472 loop {
473 match connect_async(&url).await {
474 Ok((socket, _)) => {
475 let (sender, receiver) = socket.split();
476 let conn =
477 crate::adapters::WsConn::new(sender, receiver, allow_public_space);
478 let addr = ctx.start_actor(Box::new(conn));
479 info!("BEAM connected to peer {} (addr: {})", url, addr);
480 backoff = Duration::from_secs(1);
481 // Stay alive; WsConn runs until disconnect.
482 // TODO: detect disconnect for faster reconnect loop.
483 tokio::time::sleep(Duration::from_secs(3600)).await;
484 }
485 Err(e) => {
486 warn!(
487 "BEAM connect to {} failed: {}. retry in {:?}",
488 url, e, backoff
489 );
490 tokio::time::sleep(backoff).await;
491 backoff = (backoff * 2).min(max_backoff);
492 }
493 }
494 }
495 });
496 }
497
498 /// Connects to a remote peer via WebRTC data channel.
499 ///
500 /// Signaling bootstraps over the existing WebSocket mesh via
501 /// [`Message::RtcSignal`]. Once the data channel opens, the peer is
502 /// registered in `Router::known_peers` just like a WebSocket peer, and
503 /// Gun protocol messages flow over the P2P link.
504 ///
505 /// Requires the `webrtc` feature. Without it, this method is a no-op.
506 #[cfg(feature = "webrtc")]
507 pub fn connect_webrtc_peer(
508 &self,
509 peer_id: &str,
510 target_peer_id: &str,
511 role: crate::adapters::WebRtcRole,
512 ) {
513 let peer_id = peer_id.to_string();
514 let target_peer_id = target_peer_id.to_string();
515 let ice_servers = self.ice_servers.clone();
516 let allow_public_space = self.allow_public_space;
517 let ctx = self.actor_context.clone();
518 let ctx_for_actor = ctx.clone();
519 ctx.child_task(async move {
520 let peer = crate::adapters::WebRtcPeer::new(
521 peer_id,
522 target_peer_id,
523 role,
524 allow_public_space,
525 ice_servers,
526 );
527 let addr = ctx_for_actor.start_actor(Box::new(peer));
528 info!("BEAM WebRtcPeer started (addr: {})", addr);
529 });
530 }
531
532 /// Returns a child node corresponding to the given key, creating it if necessary.
533 ///
534 /// This is the primary graph traversal method. Calling `node.get("key")`
535 /// returns a child node. If the child already exists, the existing
536 /// instance is returned; otherwise a new child is created lazily.
537 ///
538 /// # Arguments
539 ///
540 /// * `key` - The child key. Must not be empty (empty returns `self`).
541 pub fn get(&mut self, key: &str) -> Node {
542 if key.is_empty() {
543 return self.clone();
544 }
545 debug!("get key {}", key);
546 // Explicit scope to drop read guard BEFORE entering else branch.
547 // The temporary from `self.children.read()` would otherwise live
548 // until the end of the `if let` statement, including the else block,
549 // causing a deadlock when new_child() tries to write().
550 let existing = {
551 let guard = self.children.read();
552 guard.get(key).cloned()
553 };
554 match existing {
555 Some(child) => child,
556 None => self.new_child(key.to_string()),
557 }
558 }
559
560 /// Subscribes to all children of this node.
561 ///
562 /// Returns a [`broadcast::Receiver`] that emits `(child_key, value)` tuples
563 /// for each child. The current children (if any) are requested from storage
564 /// via a `Get` message.
565 pub fn map(&self) -> broadcast::Receiver<(String, Value)> {
566 let node_id = self.uid.read().to_string();
567 let addr = self.addr.read().clone().unwrap();
568 let get = Get::new(node_id, None, addr);
569 // subscribe before send so we don't miss the response
570 let subscriber = self.map_sender.subscribe();
571 if let Some(router) = self.router.read().clone() {
572 let _ = router.send(Message::Get(get));
573 }
574 subscriber
575 }
576
577 /// Walks up the parent chain, building the `updated_nodes` map for a Put.
578 ///
579 /// For each ancestor, this inserts the child's value as a child of the
580 /// parent node in `updated_nodes`, and links the parent as a `Value::Link`.
581 /// This builds the Gun.js wire format's nested node structure.
582 fn add_parent_nodes(
583 &mut self,
584 updated_nodes: &mut BTreeMap<String, Children>,
585 value: Value,
586 updated_at: f64,
587 ) {
588 let parent = &*self.parent.read();
589 if let Some((parent_id, parent)) = parent {
590 let mut parent = parent.clone();
591 let mut children = Children::default();
592 children.insert(
593 self.path.last().unwrap().clone(),
594 NodeData {
595 value: value.clone(),
596 updated_at,
597 },
598 );
599 updated_nodes.insert(parent_id.to_string(), children);
600 parent.add_parent_nodes(updated_nodes, Value::Link(parent.id()), updated_at);
601 }
602 }
603
604 /// Writes a value to this node and waits for storage acknowledgement.
605 ///
606 /// The value is immediately sent to local `on()` subscribers (so any
607 /// in-process listeners see it before the ack returns), then a [`Put`]
608 /// message is sent to the router for storage and network relay. The
609 /// timestamp is the current Unix epoch in milliseconds.
610 ///
611 /// Returns `Ok(())` once a storage adapter has committed the put durably
612 /// and acked back. Returns `Err(String)` if the adapter reports a commit
613 /// failure, the router was not initialized, the router channel rejected
614 /// the message, or the ack did not arrive within the timeout window
615 /// (default 30 seconds).
616 ///
617 /// # Why Async?
618 ///
619 /// Without the ack, `put` returns synchronously after queuing the message
620 /// to the storage actor — but the storage actor may not have processed
621 /// the message yet. A subsequent `get` or `once` would read stale state.
622 /// The async ack closes that race: this future resolves only after the
623 /// storage actor has committed the put. See [module docs](self) for the
624 /// full race-condition history.
625 ///
626 /// # Ack Pattern
627 ///
628 /// Mirrors [`Node::flush_storage`](Self::flush_storage):
629 /// 1. Register a oneshot keyed by the put's `id` in `pending_puts`
630 /// 2. Send `Message::Put` to the router
631 /// 3. Storage adapter commits, then sends
632 /// `Put { in_response_to: Some(id), updated_nodes: { "_ack": { "_ack"|"_err": ... } } }`
633 /// back to this node's `addr` directly (NOT through the router)
634 /// 4. `Node::handle_put` drains `pending_puts` and resolves the oneshot
635 ///
636 /// # Arguments
637 ///
638 /// * `value` - The value to set (see [`Value`] for supported types)
639 ///
640 /// Writes a value and waits for it to be replicated to N peers per
641 /// the given [`AckPolicy`].
642 ///
643 /// Resolves with a [`ReplicationStatus`] when the policy threshold is
644 /// satisfied, or `Err(String)` on timeout, router failure, or unrecoverable
645 /// storage error.
646 ///
647 /// # Wire-level flow
648 ///
649 /// 1. Build a [`Put`] and register a oneshot in `pending_puts`
650 /// 2. Send `Message::RegisterQuorum { put_id, requester, policy }` to Router
651 /// (this creates a tracked [`crate::router::QuorumEntry`])
652 /// 3. Send `Message::Put(put)` to Router for relay to peers
653 /// 4. Peers eventually reply with `Put { @: put_id, .. }`
654 /// 5. Router's `handle_put` ack branch counts each peer ack in the QuorumEntry
655 /// 6. When `acked_by >= policy.quorum`, Router sends a sentinel
656 /// `Put { @: put_id, updated_nodes: { "__quorum_met__": ack_count } }`
657 /// back to this Node
658 /// 7. This Node's `handle_put` drain decodes the sentinel via
659 /// [`Node::decode_quorum_payload`] and resolves the oneshot with the
660 /// [`ReplicationStatus`]
661 ///
662 /// # Examples
663 ///
664 /// ```ignore
665 /// use beam::{Node, Value, AckPolicy};
666 ///
667 /// let node = Node::new();
668 /// let policy = AckPolicy::for_peer_count(3); // majority of 3 peers
669 /// let status = node.put_quorum(Value::Text("hello".into()), policy).await?;
670 /// assert!(status.quorum_met);
671 /// assert!(status.acked_by >= 2);
672 /// ```
673 ///
674 /// # Arguments
675 ///
676 /// * `value` - The value to set (see [`Value`] for supported types)
677 /// * `policy` - The [`AckPolicy`] describing how many peer acks are required
678 /// and how long to wait
679 ///
680 /// # Errors
681 ///
682 /// Returns `Err(String)` if:
683 /// - The router is not initialized
684 /// - The Router fails to receive the `RegisterQuorum` or `Put` message
685 /// - The policy timeout elapses before quorum is met
686 /// - The ack channel closes (Router dropped us)
687 pub async fn put_quorum(
688 &mut self,
689 value: Value,
690 policy: AckPolicy,
691 ) -> Result<ReplicationStatus, String> {
692 let updated_at: f64 = SystemTime::now()
693 .duration_since(SystemTime::UNIX_EPOCH)
694 .unwrap()
695 .as_millis() as f64;
696 debug!("put_quorum (required: {} peers)", policy.quorum);
697 self.on_sender.send(value.clone()).ok();
698 let mut updated_nodes = BTreeMap::new();
699 self.add_parent_nodes(&mut updated_nodes, value, updated_at);
700 let my_addr = self.addr.read().clone().unwrap();
701 let put = Put::new(updated_nodes, None, my_addr.clone());
702 let put_id = put.id.clone();
703 let (tx, rx) = oneshot::channel();
704 self.pending_puts.write().insert(put_id.clone(), tx);
705
706 let router_addr = match &*self.router.read() {
707 Some(addr) => addr.clone(),
708 None => {
709 self.pending_puts.write().remove(&put_id);
710 return Err("router not initialized".to_string());
711 }
712 };
713
714 // Register the quorum BEFORE sending the put — Router must know about
715 // the put_id before peer acks start arriving, or the first ack will
716 // be missed (no QuorumEntry to increment).
717 if router_addr
718 .send(Message::RegisterQuorum {
719 put_id: put_id.clone(),
720 requester: my_addr,
721 policy,
722 })
723 .is_err()
724 {
725 self.pending_puts.write().remove(&put_id);
726 return Err("failed to send RegisterQuorum to router".to_string());
727 }
728
729 if router_addr.send(Message::Put(put)).is_err() {
730 self.pending_puts.write().remove(&put_id);
731 return Err("failed to send put to router".to_string());
732 }
733
734 match tokio::time::timeout(policy.timeout, rx).await {
735 Ok(Ok(status)) => status,
736 Ok(Err(_)) => Err("put_quorum ack channel closed".to_string()),
737 Err(_) => {
738 self.pending_puts.write().remove(&put_id);
739 Err(format!(
740 "put_quorum timed out after {:?} (required {} peers)",
741 policy.timeout, policy.quorum
742 ))
743 }
744 }
745 }
746
747 pub async fn put(&mut self, value: Value) -> Result<(), String> {
748 let updated_at: f64 = SystemTime::now()
749 .duration_since(SystemTime::UNIX_EPOCH)
750 .unwrap()
751 .as_millis() as f64;
752 self.on_sender.send(value.clone()).ok();
753 debug!("put {}", value.to_string());
754 let mut updated_nodes = BTreeMap::new();
755 // Store the value at self.uid under the "_" convention (Gun.js
756 // soul-value encoding) so that map() on self returns the value
757 // as a synthetic child. This is what Gun.js semantics expect for
758 // a node's own value vs its children. Previously put only wrote
759 // under parent_id via add_parent_nodes, so map() on the leaf
760 // never saw the put value.
761 let self_uid = self.uid.read().clone();
762 let mut self_children = Children::default();
763 self_children.insert(
764 "_".to_string(),
765 NodeData {
766 value: value.clone(),
767 updated_at,
768 },
769 );
770 updated_nodes.insert(self_uid, self_children);
771 // Continue with parent chain propagation using the raw value so
772 // parents' child entries remain the actual value (this is what
773 // node.get("key").put(...) → node.get("key").once(...) expects).
774 self.add_parent_nodes(&mut updated_nodes, value, updated_at);
775 let my_addr = self.addr.read().clone().unwrap();
776 let put = Put::new(updated_nodes, None, my_addr);
777 let put_id = put.id.clone();
778 let (tx, rx) = oneshot::channel();
779 self.pending_puts.write().insert(put_id.clone(), tx);
780
781 let router_addr = match &*self.router.read() {
782 Some(addr) => addr.clone(),
783 None => {
784 self.pending_puts.write().remove(&put_id);
785 return Err("router not initialized".to_string());
786 }
787 };
788
789 if router_addr.send(Message::Put(put)).is_err() {
790 self.pending_puts.write().remove(&put_id);
791 return Err("failed to send put to router".to_string());
792 }
793
794 let dur = Duration::from_secs(30); // TODO: accept timeout as parameter when API stabilizes
795 match tokio::time::timeout(dur, rx).await {
796 Ok(Ok(_status)) => Ok(()), // discard ReplicationStatus for local put
797 Ok(Err(_)) => Err("put ack channel closed".to_string()),
798 Err(_) => {
799 self.pending_puts.write().remove(&put_id);
800 Err("put timed out".to_string())
801 }
802 }
803 }
804
805 /// Writes multiple values in a single storage transaction and waits for ack.
806 ///
807 /// Each operation is a `(path, value)` pair where `path` is a vector of
808 /// keys from the caller's [`Node`] down to the leaf. The caller should
809 /// invoke this on the **root** [`Node`].
810 ///
811 /// Returns `Ok(())` once the storage adapter has committed the batch
812 /// durably and acked back. Returns `Err(String)` on commit failure,
813 /// router error, or ack timeout (default 30s).
814 ///
815 /// # Atomicity
816 ///
817 /// Unlike multiple sequential [`put`](Self::put) calls, all operations
818 /// in a batch either succeed together or fail together. Storage adapters
819 /// that support transactions (e.g. [`crate::adapters::RedbStorage`])
820 /// wrap the batch in a single transaction.
821 ///
822 /// # Ack Pattern
823 ///
824 /// The whole batch shares a single ack keyed by `BatchPut.id`. The
825 /// originating node registers one oneshot, the storage adapter sends
826 /// one ack after the entire transaction commits or aborts.
827 ///
828 /// # Arguments
829 ///
830 /// * `ops` - Vector of `(path, value)` pairs
831 pub async fn batch_put(&mut self, ops: Vec<(Vec<String>, Value)>) -> Result<(), String> {
832 let updated_at: f64 = SystemTime::now()
833 .duration_since(SystemTime::UNIX_EPOCH)
834 .unwrap()
835 .as_millis() as f64;
836
837 let mut puts = Vec::with_capacity(ops.len());
838 for (path, value) in ops {
839 // Traverse from self to leaf, lazily creating children.
840 let mut leaf = self.clone();
841 for key in &path {
842 leaf = leaf.get(key);
843 }
844
845 // Notify local on() subscribers at the leaf (mirrors Node::put).
846 let _ = leaf.on_sender.send(value.clone());
847
848 let mut updated_nodes = BTreeMap::new();
849 leaf.add_parent_nodes(&mut updated_nodes, value, updated_at);
850
851 let my_addr = self.addr.read().clone().unwrap();
852 let put = Put::new(updated_nodes, None, my_addr);
853 puts.push(put);
854 }
855
856 let my_addr = self.addr.read().clone().unwrap();
857 let batch = BatchPut::new(puts, my_addr);
858 let batch_id = batch.id.clone();
859 let (tx, rx) = oneshot::channel();
860 // Register under batch id; storage adapter will ack via `in_response_to: Some(batch.id)`.
861 self.pending_puts.write().insert(batch_id.clone(), tx);
862
863 let router_addr = match &*self.router.read() {
864 Some(addr) => addr.clone(),
865 None => {
866 self.pending_puts.write().remove(&batch_id);
867 return Err("router not initialized".to_string());
868 }
869 };
870
871 if router_addr.send(Message::BatchPut(batch)).is_err() {
872 self.pending_puts.write().remove(&batch_id);
873 return Err("failed to send batch_put to router".to_string());
874 }
875
876 let dur = Duration::from_secs(30); // TODO: accept timeout as parameter when API stabilizes
877 match tokio::time::timeout(dur, rx).await {
878 Ok(Ok(_status)) => Ok(()), // discard ReplicationStatus for batch_put
879 Ok(Err(_)) => Err("batch_put ack channel closed".to_string()),
880 Err(_) => {
881 self.pending_puts.write().remove(&batch_id);
882 Err("batch_put timed out".to_string())
883 }
884 }
885 }
886
887 /// Stops the node and all its child actors and adapters.
888 ///
889 /// This calls [`ActorContext::stop`] from the node's actor context, which
890 /// aborts all child tasks and sends stop signals to all child actors.
891 pub fn stop(&mut self) {
892 info!("Node stopping");
893 self.actor_context.stop();
894 }
895
896 /// Gracefully shuts down the node, ensuring data integrity.
897 ///
898 /// This is the preferred shutdown path. The sequence is:
899 ///
900 /// 1. **Flush storage** — calls [`Node::flush_storage`] to ensure all
901 /// pending writes in the actor mailboxes are processed and committed
902 /// by the storage adapters. The router processes messages in order,
903 /// so any puts ahead of the flush are committed before the flush ack
904 /// returns.
905 ///
906 /// 2. **Signal shutdown** — broadcasts `true` on the shutdown watch
907 /// channel. Long-running child tasks (accept loops, signal processors)
908 /// that `select!` on `shutdown_rx` break their loops and stop
909 /// accepting new connections or work.
910 ///
911 /// 3. **Drain** — waits briefly for in-flight messages to complete and
912 /// network connections to close. The drain duration is bounded by
913 /// the remaining time budget after the flush.
914 ///
915 /// 4. **Force stop** — calls [`Node::stop`] to abort any remaining
916 /// tasks and send stop signals to all child actors. This is the
917 /// same as a hard shutdown, but by this point all critical work
918 /// should already be done.
919 ///
920 /// # Arguments
921 ///
922 /// * `timeout` — maximum total time for the graceful shutdown sequence.
923 /// If the flush and drain do not complete within this duration, the
924 /// method proceeds to force stop and returns an error.
925 ///
926 /// # Returns
927 ///
928 /// - `Ok(())` — graceful shutdown completed within the timeout.
929 /// - `Err(String)` — timed out; force stop was used. The error message
930 /// describes which phase timed out.
931 ///
932 /// # Example
933 ///
934 /// ```no_run
935 /// use std::time::Duration;
936 /// use beam::Node;
937 ///
938 /// # #[tokio::main]
939 /// # async fn main() {
940 /// let mut node = Node::new();
941 /// // ... use node ...
942 /// if let Err(e) = node.shutdown(Duration::from_secs(30)).await {
943 /// eprintln!("graceful shutdown timed out: {}, force-stopped", e);
944 /// }
945 /// # }
946 /// ```
947 pub async fn shutdown(&mut self, timeout: Duration) -> Result<(), String> {
948 info!("Node graceful shutdown initiated (timeout: {:?})", timeout);
949
950 // Phase 1: Flush storage — ensure pending writes reach disk.
951 // The flush message goes through the router, which processes
952 // messages in FIFO order. Any puts ahead of the flush in the
953 // mailbox are committed before the flush ack returns.
954 let flush_result = tokio::time::timeout(timeout, self.flush_storage(Some(timeout))).await;
955
956 match flush_result {
957 Ok(Ok(())) => info!("Storage flush completed during shutdown"),
958 Ok(Err(e)) => {
959 warn!("Storage flush error during shutdown: {} — continuing", e);
960 }
961 Err(_) => {
962 warn!("Storage flush timed out during shutdown — forcing stop");
963 self.stop();
964 return Err("flush timed out".to_string());
965 }
966 }
967
968 // Phase 2: Signal shutdown to all long-running child tasks.
969 // This causes accept loops, retry loops, and signal processors
970 // to break and stop accepting new work.
971 if self.shutdown_tx.send(true).is_err() {
972 warn!("Shutdown signal already sent — all receivers may be dropped");
973 }
974 info!("Shutdown signal broadcast to child tasks");
975
976 // Phase 3: Drain — give in-flight messages and connection close
977 // handshakes time to complete. We use a fraction of the remaining
978 // timeout budget (or a default if flush consumed little).
979 let drain_timeout = Duration::from_secs(5);
980 debug!("Draining for {:?} before force stop", drain_timeout);
981 tokio::time::sleep(drain_timeout).await;
982
983 // Phase 4: Force stop — abort remaining tasks, send stop signals.
984 // By this point all critical work (flush, signal) is done. This
985 // cleans up any stragglers.
986 self.stop();
987 info!("Node graceful shutdown complete");
988 Ok(())
989 }
990
991 /// Decodes a put-ack payload sent by a storage adapter after commit.
992 ///
993 /// Storage adapters ack a put by sending `Message::Put(ack)` with
994 /// `in_response_to: Some(original_put.id)` and `updated_nodes` containing
995 /// a single entry under the `_ack` node id. The child of that entry
996 /// indicates the result:
997 ///
998 /// - `_ack` (Value::Text("ok")) → commit succeeded
999 /// - `_err` (Value::Text("<msg>")) → commit failed with `<msg>`
1000 ///
1001 /// # Sentinel Convention
1002 ///
1003 /// This uses the **same** sentinel convention as the Flush ack
1004 /// (see `Node::flush_storage` and the storage adapter implementations),
1005 /// so all ack-routing logic is uniform across Put, BatchPut, and Flush.
1006 /// A future change to add richer ack payloads (e.g. commit timestamp,
1007 /// byte count) only needs to update this single decoder.
1008 ///
1009 /// # Fallback Behavior
1010 ///
1011 /// If the ack is present (matched `in_response_to` from `pending_puts`)
1012 /// but contains neither sentinel, the ack is treated as success. The
1013 /// ack's mere presence proves the put reached the storage actor and
1014 /// the adapter decided to acknowledge it — absence of a failure marker
1015 /// is positive evidence.
1016 ///
1017 /// Failure to produce an ack at all (timeout, channel drop) is handled
1018 /// by the awaiting caller via `tokio::time::timeout`.
1019 fn decode_put_ack_payload(put: &Put) -> Result<(), String> {
1020 for (_node_id, children) in put.updated_nodes.iter().rev() {
1021 if let Some(node_data) = children.get("_err") {
1022 if let Value::Text(msg) = &node_data.value {
1023 return Err(msg.clone());
1024 }
1025 return Err("storage put commit failed (non-text _err payload)".to_string());
1026 }
1027 if children.contains_key("_ack") {
1028 return Ok(());
1029 }
1030 }
1031 // Ack present but no sentinel — treat as success.
1032 Ok(())
1033 }
1034
1035 /// Sibling decoder for the Router's `__quorum_met__` sentinel reply.
1036 ///
1037 /// Inspects the Put envelope for the sentinel as a top-level key in
1038 /// `updated_nodes` and, if found, returns a [`ReplicationStatus`] carrying
1039 /// the ack count from the sentinel payload. Returns `None` for any other
1040 /// reply shape — the caller falls back to [`Self::decode_put_ack_payload`].
1041 ///
1042 /// The Router emits this sentinel when the configured [`AckPolicy`]
1043 /// threshold is met (see [`crate::router::Router::handle_register_quorum`]).
1044 /// The reply envelope mirrors a storage `_ack` (same `in_response_to`
1045 /// convention) but uses `__quorum_met__` as the `updated_nodes` key so
1046 /// the decoders can disambiguate without coordination.
1047 ///
1048 /// # Wire format (emitted by Router via `Put::new_from_kv`)
1049 ///
1050 /// ```text
1051 /// updated_nodes = {
1052 /// "__quorum_met__" => {
1053 /// "_" => NodeData { value: Number(ack_count), updated_at: 0.0 }
1054 /// }
1055 /// }
1056 /// ```
1057 ///
1058 /// The `ack_count` is the number of peer acks observed before the
1059 /// Decodes a peer-received Put carrying the `__quorum_met__` sentinel.
1060 ///
1061 /// Returns:
1062 /// - `None` — no quorum sentinel present (caller falls through to the
1063 /// local-storage `_ack`/`_err` decoder).
1064 /// - `Some(Ok(status))` — sentinel carried `Value::Number(N)`, quorum met
1065 /// with N peer acks.
1066 /// - `Some(Err(msg))` — sentinel carried `Value::Bit(true)`, the Router
1067 /// cleanup reaper evicted this entry as timed out.
1068 ///
1069 /// The `elapsed` field is filled with the time since this decoder was
1070 /// called. For accurate elapsed measurement, callers should wrap the
1071 /// entire drain with an `Instant`.
1072 fn decode_quorum_payload(put: &Put) -> Option<Result<ReplicationStatus, String>> {
1073 let started_at = std::time::Instant::now();
1074 let children = put.updated_nodes.get(QUORUM_MET_SENTINEL)?;
1075 let node_data = children.get("_")?;
1076 match &node_data.value {
1077 Value::Number(n) => {
1078 let ack_count = *n as usize;
1079 Some(Ok(ReplicationStatus {
1080 put_id: put.id.clone(),
1081 acked_by: ack_count,
1082 quorum_met: true,
1083 elapsed: started_at.elapsed(),
1084 }))
1085 }
1086 Value::Bit(true) => Some(Err(format!("quorum timed out for put_id={}", put.id))),
1087 // Bit(false), String, Null, etc. — malformed. Fall through.
1088 _ => None,
1089 }
1090 }
1091}
1092
1093/// Options for put operations (SEA certificate support).
1094///
1095/// Currently a placeholder — the `cert` field is reserved for future
1096/// certificate-based write authorization.
1097#[derive(Clone, Debug, Default)]
1098pub struct PutOptions {
1099 /// Optional certificate for delegated writes.
1100 ///
1101 /// When set, the put will be checked against the certificate's
1102 /// policy (path restrictions, expiry, authorized certificants).
1103 /// Currently a no-op; reserved for future enforcement.
1104 pub cert: Option<serde_json::Value>,
1105}
1106
1107impl Node {
1108 /// Sets a value with options (currently cert is no-op; reserved for future enforcement).
1109 ///
1110 /// See [`Node::put`] for the basic version. The `options` parameter
1111 /// allows passing a [`PutOptions`] with a certificate for delegated
1112 /// writes, though certificate enforcement is not yet implemented.
1113 pub async fn put_with_options(
1114 &mut self,
1115 value: Value,
1116 _options: PutOptions,
1117 ) -> Result<(), String> {
1118 self.put(value).await
1119 }
1120}
1121
1122impl Default for Node {
1123 fn default() -> Self {
1124 Self::new()
1125 }
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130 use super::*;
1131
1132 #[tokio::test]
1133 async fn test_node_new() {
1134 let node = Node::new();
1135 assert!(node.id().is_empty(), "root node uid should be empty");
1136 assert!(!node.peer_id().is_empty(), "peer_id should be non-empty");
1137 }
1138
1139 #[tokio::test]
1140 async fn test_node_default() {
1141 let node = Node::default();
1142 assert!(node.id().is_empty());
1143 }
1144
1145 #[tokio::test]
1146 async fn test_node_get_creates_child() {
1147 let mut node = Node::new();
1148 let child = node.get("child_key");
1149 assert_eq!(child.id(), "child_key");
1150 }
1151
1152 #[tokio::test]
1153 async fn test_node_get_empty_key_returns_self() {
1154 let mut node = Node::new();
1155 let child = node.get("");
1156 assert_eq!(child.id(), node.id());
1157 }
1158
1159 #[tokio::test]
1160 async fn test_node_get_nested() {
1161 let mut node = Node::new();
1162 let deep = node.get("a").get("b").get("c");
1163 assert_eq!(deep.id(), "a/b/c");
1164 }
1165
1166 #[tokio::test]
1167 async fn test_node_get_returns_existing() {
1168 let mut node = Node::new();
1169 let child1 = node.get("key");
1170 let child2 = node.get("key");
1171 assert_eq!(child1.id(), child2.id());
1172 }
1173
1174 #[tokio::test]
1175 async fn test_node_put_and_on() {
1176 let mut node = Node::new();
1177 let mut sub = node.get("greeting").on();
1178 node.get("greeting").put("hello".into()).await.expect("put");
1179 let val = tokio::time::timeout(Duration::from_secs(2), sub.recv())
1180 .await
1181 .expect("timeout")
1182 .expect("recv error");
1183 assert_eq!(val, Value::Text("hello".to_string()));
1184 }
1185
1186 #[tokio::test]
1187 async fn test_node_once() {
1188 let mut node = Node::new();
1189 node.get("key").put("value".into()).await.expect("put");
1190 let val = node.get("key").once(Some(Duration::from_secs(2))).await;
1191 assert_eq!(val, Some(Value::Text("value".to_string())));
1192 }
1193
1194 #[tokio::test]
1195 async fn test_node_batch_put() {
1196 let mut node = Node::new();
1197 let mut sub = node.get("a").on();
1198 node.batch_put(vec![(vec!["a".to_string()], Value::Text("x".into()))])
1199 .await
1200 .expect("batch_put");
1201 let val = tokio::time::timeout(Duration::from_secs(2), sub.recv())
1202 .await
1203 .expect("timeout")
1204 .expect("recv error");
1205 assert_eq!(val, Value::Text("x".to_string()));
1206 }
1207
1208 #[test]
1209 fn test_config_default() {
1210 let config = Config::default();
1211 assert!(config.allow_public_space);
1212 assert_eq!(config.broadcast_buffer_size, 4096);
1213 assert!(!config.ice_servers.is_empty());
1214 }
1215
1216 #[test]
1217 fn test_config_custom() {
1218 let config = Config {
1219 allow_public_space: false,
1220 my_pub: Some("test.pub".to_string()),
1221 broadcast_buffer_size: 1024,
1222 ice_servers: vec![],
1223 };
1224 assert!(!config.allow_public_space);
1225 assert_eq!(config.broadcast_buffer_size, 1024);
1226 assert!(config.ice_servers.is_empty());
1227 }
1228
1229 #[test]
1230 fn test_put_options_default() {
1231 let opts = PutOptions::default();
1232 assert!(opts.cert.is_none());
1233 }
1234
1235 // ========================================================================
1236 // Async Ack Pattern Tests
1237 // ========================================================================
1238 //
1239 // These tests exercise the async `put`/`batch_put` ack pattern. They are
1240 // distinct from the sync-style tests above in that they actually `.await`
1241 // the put and verify the commit completed before the future resolves.
1242 //
1243 // The race they defend against: before the ack pattern, `put` returned
1244 // synchronously after queueing to the storage actor — a subsequent `get`
1245 // could read stale state. These tests would flake on the old code; on
1246 // the new code they should be deterministic.
1247 // ========================================================================
1248
1249 /// Helper: build a minimal ack `Put` message that mimics what a storage
1250 /// adapter sends back after commit. Used by unit tests that exercise
1251 /// `handle_put` / `decode_put_ack_payload` without the full storage stack.
1252 ///
1253 /// The from-address is a no-op since these unit tests inject the ack
1254 /// directly through `handle(...)` — no routing involved.
1255 fn make_ack_put(put_id: &str, sentinel: &str) -> Put {
1256 let mut children = BTreeMap::new();
1257 children.insert(
1258 sentinel.to_string(),
1259 NodeData {
1260 value: Value::Text(if sentinel == "_err" {
1261 "test error".to_string()
1262 } else {
1263 "ok".to_string()
1264 }),
1265 updated_at: 0.0,
1266 },
1267 );
1268 let mut nodes = BTreeMap::new();
1269 nodes.insert("_ack".to_string(), children);
1270 let mut put = Put::new(nodes, Some(put_id.to_string()), Addr::noop());
1271 // Compute checksum so callers can serialize.
1272 put.to_string();
1273 put
1274 }
1275
1276 #[tokio::test]
1277 async fn test_decode_put_ack_payload_success() {
1278 let ack = make_ack_put("put-1", "_ack");
1279 let result = Node::decode_put_ack_payload(&ack);
1280 assert!(result.is_ok(), "expected Ok, got {:?}", result);
1281 }
1282
1283 #[tokio::test]
1284 async fn test_decode_put_ack_payload_error_carries_message() {
1285 let ack = make_ack_put("put-2", "_err");
1286 let result = Node::decode_put_ack_payload(&ack);
1287 assert!(result.is_err(), "expected Err");
1288 let err = result.unwrap_err();
1289 assert!(
1290 err.contains("test error"),
1291 "expected error message in result, got: {}",
1292 err
1293 );
1294 }
1295
1296 #[tokio::test]
1297 async fn test_decode_put_ack_payload_no_sentinel_treated_as_success() {
1298 // Empty ack payload (no _ack/_err) is treated as success — the ack's
1299 // presence is the signal. This matches the documented fallback.
1300 let mut children = BTreeMap::new();
1301 children.insert(
1302 "_ack".to_string(),
1303 NodeData {
1304 value: Value::Null,
1305 updated_at: 0.0,
1306 },
1307 );
1308 let mut nodes = BTreeMap::new();
1309 nodes.insert("_ack".to_string(), children);
1310 let put = Put::new(nodes, Some("put-3".to_string()), Addr::noop());
1311 let result = Node::decode_put_ack_payload(&put);
1312 assert!(result.is_ok(), "no-sentinel ack should be success");
1313 }
1314
1315 #[tokio::test]
1316 async fn test_decode_put_ack_payload_flushed_sentinel_also_succeeds() {
1317 // `_flushed` is the Flush ack sentinel. handle_put intercepts flush
1318 // acks FIRST (before falling through to the put-ack decoder), so the
1319 // decoder will only see `_flushed` if it sneaks through — which
1320 // shouldn't happen, but the decoder should still treat it as success
1321 // because it isn't `_err`.
1322 let ack = make_ack_put("flush-1", "_flushed");
1323 let result = Node::decode_put_ack_payload(&ack);
1324 assert!(
1325 result.is_ok(),
1326 "_flushed should decode as success (no _err present)"
1327 );
1328 }
1329
1330 #[tokio::test]
1331 async fn test_pending_puts_drain_on_ack() {
1332 // Register a oneshot for a fake put_id, then send a matching ack
1333 // through the actor handle. The pending_puts map should drain.
1334 let mut node = Node::new();
1335 let (tx, rx) = tokio::sync::oneshot::channel();
1336 let put_id = "test-pending-1".to_string();
1337 node.pending_puts.write().insert(put_id.clone(), tx);
1338
1339 // Build ack message
1340 let ack = make_ack_put(&put_id, "_ack");
1341
1342 // Inject via the actor handle
1343 let ctx = ActorContext::new("test-peer".to_string());
1344 node.handle(Message::Put(ack), &ctx).await;
1345
1346 // The future should resolve with Ok(())
1347 let result = tokio::time::timeout(Duration::from_secs(1), rx)
1348 .await
1349 .expect("ack did not arrive within 1s")
1350 .expect("ack channel closed unexpectedly");
1351 assert!(result.is_ok(), "expected Ok, got {:?}", result);
1352 assert!(
1353 !node.pending_puts.read().contains_key(&put_id),
1354 "pending_puts should be drained after ack"
1355 );
1356 }
1357
1358 #[tokio::test]
1359 async fn test_pending_puts_drain_on_error() {
1360 // Same as above but with _err payload — the oneshot should resolve
1361 // with the error message.
1362 let mut node = Node::new();
1363 let (tx, rx) = tokio::sync::oneshot::channel();
1364 let put_id = "test-pending-err".to_string();
1365 node.pending_puts.write().insert(put_id.clone(), tx);
1366
1367 let ack = make_ack_put(&put_id, "_err");
1368
1369 let ctx = ActorContext::new("test-peer".to_string());
1370 node.handle(Message::Put(ack), &ctx).await;
1371
1372 let result = tokio::time::timeout(Duration::from_secs(1), rx)
1373 .await
1374 .expect("error ack did not arrive within 1s")
1375 .expect("ack channel closed unexpectedly");
1376 assert!(result.is_err(), "expected Err, got {:?}", result);
1377 assert!(result.unwrap_err().contains("test error"));
1378 assert!(!node.pending_puts.read().contains_key(&put_id));
1379 }
1380
1381 #[tokio::test]
1382 async fn test_pending_puts_no_match_passes_through() {
1383 // If an ack arrives with an id that doesn't match any pending_put,
1384 // it should be processed as a normal Put (not drain anything).
1385 let mut node = Node::new();
1386 let (tx, _rx) = tokio::sync::oneshot::channel();
1387 let put_id = "registered-id".to_string();
1388 node.pending_puts.write().insert(put_id.clone(), tx);
1389
1390 // Different ack id
1391 let ack = make_ack_put("different-id", "_ack");
1392
1393 let ctx = ActorContext::new("test-peer".to_string());
1394 node.handle(Message::Put(ack), &ctx).await;
1395
1396 // The original pending_put should still be registered (not drained)
1397 assert!(
1398 node.pending_puts.read().contains_key(&put_id),
1399 "unrelated ack should NOT drain unrelated pending_put"
1400 );
1401 }
1402
1403 #[tokio::test]
1404 async fn test_put_returns_after_storage_ack() {
1405 // The KEY test for the race fix. `put` must not return until storage
1406 // has acked. We verify this by chaining a `get` IMMEDIATELY after
1407 // `put` resolves — if the ack pattern works, get sees the new value.
1408 //
1409 // Pre-fix behavior: get returned stale state (or None) because the
1410 // storage actor hadn't processed the Put message yet.
1411 let mut node = Node::new();
1412 node.get("race_key")
1413 .put("race_value".into())
1414 .await
1415 .expect("put should succeed");
1416 let val = node
1417 .get("race_key")
1418 .once(Some(Duration::from_secs(2)))
1419 .await;
1420 assert_eq!(
1421 val,
1422 Some(Value::Text("race_value".to_string())),
1423 "put → get should observe the new value (race fix verification)"
1424 );
1425 }
1426
1427 #[tokio::test]
1428 async fn test_batch_put_returns_after_storage_ack() {
1429 // Batch counterpart of test_put_returns_after_storage_ack.
1430 let mut node = Node::new();
1431 node.batch_put(vec![
1432 (vec!["batch_a".to_string()], Value::Text("1".into())),
1433 (vec!["batch_b".to_string()], Value::Text("2".into())),
1434 (vec!["batch_c".to_string()], Value::Text("3".into())),
1435 ])
1436 .await
1437 .expect("batch_put should succeed");
1438
1439 // All three children should be visible immediately after batch_put resolves.
1440 let a = node.get("batch_a").once(Some(Duration::from_secs(2))).await;
1441 let b = node.get("batch_b").once(Some(Duration::from_secs(2))).await;
1442 let c = node.get("batch_c").once(Some(Duration::from_secs(2))).await;
1443 assert_eq!(a, Some(Value::Text("1".to_string())));
1444 assert_eq!(b, Some(Value::Text("2".to_string())));
1445 assert_eq!(c, Some(Value::Text("3".to_string())));
1446 }
1447
1448 #[tokio::test]
1449 async fn test_put_sequential_no_ack_loss() {
1450 // Issue 5 puts in rapid succession. Each must resolve with Ok and
1451 // each subsequent get must see its respective value — no ack lost.
1452 let mut node = Node::new();
1453 for i in 0..5 {
1454 let key = format!("seq_{}", i);
1455 let val = format!("val_{}", i);
1456 node.get(&key)
1457 .put(val.clone().into())
1458 .await
1459 .expect("put should succeed");
1460 let got = node.get(&key).once(Some(Duration::from_secs(2))).await;
1461 assert_eq!(
1462 got,
1463 Some(Value::Text(val.clone())),
1464 "put {} → get should see {:?}, got {:?}",
1465 i,
1466 val,
1467 got
1468 );
1469 }
1470 }
1471
1472 #[tokio::test]
1473 async fn test_pending_puts_cleared_on_router_send_failure() {
1474 // If the router isn't initialized, put should return an error AND
1475 // remove its pending_puts entry (so we don't leak oneshot channels).
1476 // We test this by constructing a node whose router addr is None.
1477 //
1478 // (Hard to construct from outside since Node::new() wires up a router,
1479 // so we exercise the error path indirectly: invalid router state.)
1480 let mut node = Node::new();
1481 // First put should succeed (router wired up by Node::new()).
1482 node.get("normal_key")
1483 .put("normal".into())
1484 .await
1485 .expect("first put ok");
1486 // Now corrupt the router addr to force the error path.
1487 *node.router.write() = None;
1488 let result = node.get("broken_key").put("x".into()).await;
1489 assert!(result.is_err(), "expected Err when router is None");
1490 // Pending puts should be empty — the failed put should have cleaned up.
1491 assert!(
1492 node.pending_puts.read().is_empty(),
1493 "pending_puts should be empty after router-send failure"
1494 );
1495 }
1496 // ========================================================================
1497 // Phase 5: Quorum Drain Tests (Network Fanout Ack)
1498 // ========================================================================
1499 //
1500 // Tests exercise:
1501 // - decode_quorum_payload return-shape: Some(Ok) | Some(Err) | None
1502 // (success sentinel, timeout sentinel, fall-through to _ack decoder)
1503 // - AckPolicy builder math (any / for_peer_count / all / builders)
1504 // - ReplicationStatus invariant (Ok arm has quorum_met = true)
1505
1506 /// Build a Put carrying the `__quorum_met__` sentinel for decoder tests.
1507 fn make_quorum_put(sentinel_value: Value) -> Put {
1508 let mut children: Children = std::collections::BTreeMap::new();
1509 children.insert(
1510 "_".to_string(),
1511 NodeData {
1512 value: sentinel_value,
1513 updated_at: 0.0,
1514 },
1515 );
1516 let mut put = Put::new_from_kv(QUORUM_MET_SENTINEL.to_string(), children, Addr::noop());
1517 put.id = "test_put_id".to_string();
1518 put.in_response_to = Some("test_put_id".to_string());
1519 put
1520 }
1521
1522 #[test]
1523 fn decode_quorum_payload_success_with_number() {
1524 let put = make_quorum_put(Value::Number(3.0));
1525 let result = Node::decode_quorum_payload(&put);
1526 assert!(result.is_some(), "should decode sentinel-bearing Put");
1527 let inner = result.unwrap();
1528 assert!(inner.is_ok(), "Number payload should decode as Ok");
1529 let status = inner.unwrap();
1530 assert_eq!(status.put_id, "test_put_id");
1531 assert_eq!(status.acked_by, 3);
1532 assert!(status.quorum_met);
1533 }
1534
1535 #[test]
1536 fn decode_quorum_payload_timeout_with_bit_true() {
1537 let put = make_quorum_put(Value::Bit(true));
1538 let result = Node::decode_quorum_payload(&put);
1539 assert!(result.is_some(), "Bit(true) is a recognized sentinel");
1540 let inner = result.unwrap();
1541 assert!(inner.is_err(), "Bit(true) must decode as Err(timeout)");
1542 let err_msg = inner.unwrap_err();
1543 assert!(
1544 err_msg.contains("quorum timed out"),
1545 "error message should mention timeout, got: {err_msg}"
1546 );
1547 assert!(
1548 err_msg.contains("test_put_id"),
1549 "error message should include put_id, got: {err_msg}"
1550 );
1551 }
1552
1553 #[test]
1554 fn decode_quorum_payload_no_sentinel_falls_through() {
1555 let mut children: Children = std::collections::BTreeMap::new();
1556 children.insert(
1557 "_".to_string(),
1558 NodeData {
1559 value: Value::Number(1.0),
1560 updated_at: 0.0,
1561 },
1562 );
1563 let put = Put::new_from_kv("not_quorum".to_string(), children, Addr::noop());
1564 let result = Node::decode_quorum_payload(&put);
1565 assert!(
1566 result.is_none(),
1567 "no sentinel key → None (fall through to _ack decoder)"
1568 );
1569 }
1570
1571 #[test]
1572 fn decode_quorum_payload_malformed_value_falls_through() {
1573 for bad_value in [
1574 Value::Bit(false),
1575 Value::Null,
1576 Value::Text("not a count".to_string()),
1577 ] {
1578 let put = make_quorum_put(bad_value.clone());
1579 let result = Node::decode_quorum_payload(&put);
1580 assert!(
1581 result.is_none(),
1582 "malformed value {bad_value:?} should return None (fall through)"
1583 );
1584 }
1585 }
1586
1587 #[test]
1588 fn decode_quorum_payload_missing_underscore_key() {
1589 let mut children: Children = std::collections::BTreeMap::new();
1590 children.insert(
1591 "wrong_key".to_string(),
1592 NodeData {
1593 value: Value::Number(1.0),
1594 updated_at: 0.0,
1595 },
1596 );
1597 let put = Put::new_from_kv(QUORUM_MET_SENTINEL.to_string(), children, Addr::noop());
1598 let result = Node::decode_quorum_payload(&put);
1599 assert!(result.is_none(), "missing _ key → None");
1600 }
1601
1602 #[test]
1603 fn ack_policy_any_has_quorum_one_and_nine_second_timeout() {
1604 let p = AckPolicy::any();
1605 assert_eq!(p.quorum, 1, "AckPolicy::any → quorum=1");
1606 assert_eq!(
1607 p.timeout,
1608 Duration::from_secs(9),
1609 "AckPolicy::any → 9s timeout (Gun.js lack default)"
1610 );
1611 }
1612
1613 #[test]
1614 fn ack_policy_for_peer_count_majority() {
1615 assert_eq!(AckPolicy::for_peer_count(0).quorum, 1, "0 peers → 1");
1616 assert_eq!(AckPolicy::for_peer_count(1).quorum, 1);
1617 assert_eq!(AckPolicy::for_peer_count(2).quorum, 1); // ⌈2/2⌉ = 1
1618 assert_eq!(AckPolicy::for_peer_count(3).quorum, 2); // ⌈3/2⌉ = 2
1619 assert_eq!(AckPolicy::for_peer_count(4).quorum, 2);
1620 assert_eq!(AckPolicy::for_peer_count(5).quorum, 3); // ⌈5/2⌉ = 3
1621 assert_eq!(AckPolicy::for_peer_count(7).quorum, 4); // ⌈7/2⌉ = 4
1622 }
1623
1624 #[test]
1625 fn ack_policy_all_is_max_usize() {
1626 let p = AckPolicy::all();
1627 assert_eq!(p.quorum, usize::MAX, "AckPolicy::all → quorum=MAX");
1628 assert_eq!(p.timeout, Duration::from_secs(9));
1629 }
1630
1631 #[test]
1632 fn ack_policy_with_timeout_overrides() {
1633 let p = AckPolicy::any().with_timeout(Duration::from_secs(30));
1634 assert_eq!(p.timeout, Duration::from_secs(30));
1635 assert_eq!(p.quorum, 1, "with_timeout preserves quorum");
1636 }
1637
1638 #[test]
1639 fn ack_policy_with_quorum_overrides() {
1640 let p = AckPolicy::any().with_quorum(5);
1641 assert_eq!(p.quorum, 5);
1642 assert_eq!(
1643 p.timeout,
1644 Duration::from_secs(9),
1645 "with_quorum preserves timeout"
1646 );
1647 }
1648
1649 #[test]
1650 fn ack_policy_default_is_any() {
1651 let p = AckPolicy::default();
1652 assert_eq!(p.quorum, 1);
1653 assert_eq!(p.timeout, Duration::from_secs(9));
1654 }
1655
1656 #[test]
1657 fn replication_status_quorum_met_true_on_ok_arm() {
1658 let status = ReplicationStatus {
1659 put_id: "p1".to_string(),
1660 acked_by: 3,
1661 quorum_met: true, // invariant: Ok arm must have this
1662 elapsed: Duration::from_millis(42),
1663 };
1664 assert_eq!(status.put_id, "p1");
1665 assert_eq!(status.acked_by, 3);
1666 assert!(status.quorum_met);
1667 assert_eq!(status.elapsed, Duration::from_millis(42));
1668 }
1669
1670 #[test]
1671 fn drain_dispatch_quorum_ok_vs_err_vs_fallthrough() {
1672 // The drain block (Node::handle_put) dispatches three cases based on
1673 // decode_quorum_payload's return shape:
1674 // Some(Ok(_)) → send Ok(status)
1675 // Some(Err(_)) → send Err(timeout_msg)
1676 // None → fall through to local-storage _ack decoder
1677 let success_put = make_quorum_put(Value::Number(2.0));
1678 match Node::decode_quorum_payload(&success_put) {
1679 Some(Ok(_)) => {}
1680 other => panic!("expected Some(Ok) for Number payload, got {other:?}"),
1681 }
1682 let timeout_put = make_quorum_put(Value::Bit(true));
1683 match Node::decode_quorum_payload(&timeout_put) {
1684 Some(Err(e)) => assert!(e.contains("timed out")),
1685 other => panic!("expected Some(Err) for Bit(true) payload, got {other:?}"),
1686 }
1687 let non_sentinel_put = {
1688 let mut children = std::collections::BTreeMap::new();
1689 children.insert(
1690 "_".to_string(),
1691 NodeData {
1692 value: Value::Number(1.0),
1693 updated_at: 0.0,
1694 },
1695 );
1696 Put::new_from_kv("storage_ack".to_string(), children, Addr::noop())
1697 };
1698 match Node::decode_quorum_payload(&non_sentinel_put) {
1699 None => {}
1700 other => panic!("expected None for non-sentinel Put, got {other:?}"),
1701 }
1702 }
1703
1704 // ========================================================================
1705 // End Phase 5 tests
1706 // ========================================================================
1707}