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