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