Skip to main content

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}