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