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