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