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