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