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