lazily 0.29.0

Lazy reactive signals with dependency tracking and cache invalidation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! Concrete **WebSocket** [`DataChannel`] backend (#akp3 / #lzwstransport),
//! behind the `websocket` feature.
//!
//! A WebSocket is already ordered and reliable, so — unlike WebRTC — it
//! satisfies the [`DataChannel`] frame contract *directly*: no SDP/ICE handshake
//! and no sans-IO pump are needed (contrast `str0m_backend`). The only
//! impedance mismatch is async-vs-sync: `tokio-tungstenite` is async, but
//! [`DataChannel`] is synchronous and non-blocking so it can drop in beside the
//! in-process and WebRTC transports. [`WsDataChannel`] bridges the two with a
//! background tokio task and a pair of queues:
//!
//! - **outbound:** [`DataChannel::send_frame`] pushes onto a bounded mpsc
//!   (`WS_OUTBOUND_CAPACITY`, a sync non-blocking enqueue that surfaces
//!   [`WsError::Backpressure`] when full); the driver task drains it and writes
//!   each frame as one binary WebSocket message.
//! - **inbound:** the driver task reads WebSocket messages and pushes their
//!   payloads onto a shared queue that [`DataChannel::try_recv_frame`] pops.
//!
//! Each frame is one whole serialized `IpcMessage`, exactly as the
//! `WebRtcSink`/`WebRtcSource` bridge (permission filtering + codec) expects, so
//! that bridge runs unchanged over a real WebSocket. [`WsDataChannel::from_stream`]
//! accepts any already-upgraded `WebSocketStream`, so the same backend serves a
//! real network socket and the deterministic in-process loopback used in tests.

use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};

use futures_util::{SinkExt, StreamExt};
use parking_lot::Mutex;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::tungstenite::Message;

use crate::webrtc_transport::DataChannel;

/// Maximum outbound frames buffered in the driver channel at once. Above this
/// threshold [`WsDataChannel::send_frame`] returns
/// [`WsError::Backpressure`](Self::Backpressure) so a producer faster than the
/// WebSocket can drain is surfaced flow control instead of growing the queue
/// without bound (#lzwsunbounded).
const WS_OUTBOUND_CAPACITY: usize = 1024;

/// Maximum inbound frames buffered for the consumer at once. Above this
/// threshold the oldest frame is dropped (and counted in
/// `dropped_inbound_frames`) so a consumer slower than the socket reader cannot
/// grow receive memory without bound — the same backpressure class already
/// applied to the str0m inbox (#lzstr0mnetinbox) and the WS outbound
/// (#lzwsunbounded) (#lzwsinboundunbounded).
const WS_INBOUND_CAPACITY: usize = 1024;

/// Error from a [`WsDataChannel`].
#[derive(Debug)]
pub enum WsError {
    /// The channel was closed (driver task ended or peer hung up).
    Closed,
    /// The outbound buffer is full (`WS_OUTBOUND_CAPACITY`) — the producer is
    /// generating frames faster than the WebSocket can drain them. Back off
    /// (sleep / await) and retry `send_frame` (#lzwsunbounded).
    Backpressure,
}

impl std::fmt::Display for WsError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Closed => write!(f, "websocket data channel closed"),
            Self::Backpressure => {
                write!(
                    f,
                    "websocket outbound buffer full (apply flow control and retry)"
                )
            }
        }
    }
}

impl std::error::Error for WsError {}

/// Aborts the driver task when the last [`WsDataChannel`] handle is dropped, so a
/// dropped channel does not leak a task pumping a dead socket.
struct AbortOnDrop(JoinHandle<()>);

impl Drop for AbortOnDrop {
    fn drop(&mut self) {
        self.0.abort();
    }
}

/// A [`DataChannel`] backed by a WebSocket connection.
///
/// Construct from any upgraded [`WebSocketStream`] with
/// [`WsDataChannel::from_stream`]; the constructor spawns the driver task, so it
/// must be called from within a tokio runtime. Cloning shares one underlying
/// connection (the queues and driver are reference-counted).
#[derive(Clone)]
pub struct WsDataChannel {
    outbound: mpsc::Sender<Vec<u8>>,
    inbound: Arc<Mutex<VecDeque<Vec<u8>>>>,
    /// Inbound frames dropped because `inbound` reached
    /// `WS_INBOUND_CAPACITY` while the consumer lagged. Read via
    /// [`WsDataChannel::dropped_inbound_frames`] (#lzwsinboundunbounded).
    dropped_inbound: Arc<AtomicUsize>,
    open: Arc<AtomicBool>,
    _driver: Arc<AbortOnDrop>,
}

impl WsDataChannel {
    /// Wrap an already-upgraded WebSocket as a [`DataChannel`].
    ///
    /// Spawns the background driver that moves frames between the queues and the
    /// socket; must be called inside a tokio runtime.
    pub fn from_stream<S>(ws: WebSocketStream<S>) -> Self
    where
        S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
    {
        let (tx, rx) = mpsc::channel(WS_OUTBOUND_CAPACITY);
        let inbound = Arc::new(Mutex::new(VecDeque::new()));
        let dropped_inbound = Arc::new(AtomicUsize::new(0));
        let open = Arc::new(AtomicBool::new(true));
        let driver = tokio::spawn(drive(
            ws,
            rx,
            inbound.clone(),
            dropped_inbound.clone(),
            open.clone(),
        ));
        Self {
            outbound: tx,
            inbound,
            dropped_inbound,
            open,
            _driver: Arc::new(AbortOnDrop(driver)),
        }
    }

    /// Number of inbound frames dropped because the receive buffer was
    /// saturated (`WS_INBOUND_CAPACITY`). Non-zero indicates the consumer is
    /// not calling [`DataChannel::try_recv_frame`] fast enough and frames are
    /// being shed to bound memory; the application should drain faster or
    /// resync (#lzwsinboundunbounded).
    pub fn dropped_inbound_frames(&self) -> usize {
        self.dropped_inbound.load(Ordering::Relaxed)
    }
}

/// Background task: forward queued outbound frames onto the socket and queue
/// inbound socket messages, until either side closes.
async fn drive<S>(
    ws: WebSocketStream<S>,
    mut outbound_rx: mpsc::Receiver<Vec<u8>>,
    inbound: Arc<Mutex<VecDeque<Vec<u8>>>>,
    dropped_inbound: Arc<AtomicUsize>,
    open: Arc<AtomicBool>,
) where
    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
    let (mut write, mut read) = ws.split();
    loop {
        tokio::select! {
            outgoing = outbound_rx.recv() => match outgoing {
                // One whole serialized IpcMessage per binary frame.
                Some(frame) => {
                    if write.send(Message::Binary(frame)).await.is_err() {
                        break;
                    }
                }
                // All senders (every WsDataChannel clone) dropped: close cleanly.
                None => {
                    let _ = write.close().await;
                    break;
                }
            },
            incoming = read.next() => match incoming {
                Some(Ok(Message::Binary(payload))) => {
                    push_inbound(&inbound, &dropped_inbound, payload);
                }
                // Tolerate text frames carrying the same JSON payload.
                Some(Ok(Message::Text(text))) => {
                    push_inbound(&inbound, &dropped_inbound, text.into_bytes());
                }
                // Control frames (ping/pong) are handled by tungstenite; ignore here.
                Some(Ok(_)) => {}
                // Close frame, stream end, or transport error: stop.
                Some(Err(_)) | None => break,
            },
        }
    }
    open.store(false, Ordering::SeqCst);
}

/// Push one inbound frame onto the receive queue, capping it at
/// `WS_INBOUND_CAPACITY`. When the cap is saturated the oldest frame is dropped
/// and counted in `dropped_inbound` so a slow consumer cannot grow receive
/// memory without bound (#lzwsinboundunbounded).
fn push_inbound(inbound: &Mutex<VecDeque<Vec<u8>>>, dropped_inbound: &AtomicUsize, frame: Vec<u8>) {
    let mut queue = inbound.lock();
    if queue.len() >= WS_INBOUND_CAPACITY {
        queue.pop_front();
        dropped_inbound.fetch_add(1, Ordering::Relaxed);
    }
    queue.push_back(frame);
}

impl DataChannel for WsDataChannel {
    type Error = WsError;

    fn send_frame(&self, frame: Vec<u8>) -> Result<(), Self::Error> {
        if !self.is_open() {
            return Err(WsError::Closed);
        }
        // Non-blocking bounded enqueue; the driver task performs the actual
        // async write. try_send surfaces backpressure when the buffer is full
        // instead of growing without bound (#lzwsunbounded).
        self.outbound.try_send(frame).map_err(|e| match e {
            mpsc::error::TrySendError::Full(_) => WsError::Backpressure,
            mpsc::error::TrySendError::Closed(_) => WsError::Closed,
        })
    }

    fn try_recv_frame(&self) -> Result<Option<Vec<u8>>, Self::Error> {
        // Drain already-received frames even after the socket closed, so a
        // final message is never dropped on the floor.
        Ok(self.inbound.lock().pop_front())
    }

    fn is_open(&self) -> bool {
        self.open.load(Ordering::SeqCst)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::webrtc_transport::{WebRtcSink, WebRtcSource};
    use crate::{
        Delta, DeltaOp, IpcMessage, IpcSink, IpcSource, NodeId, NodeSnapshot, OpKind, PeerId,
        PeerPermissions, Snapshot,
    };
    use std::time::Duration;

    /// Build a connected pair of `WsDataChannel`s over an in-process duplex
    /// stream with a *real* WebSocket handshake — deterministic, no network.
    async fn loopback() -> (WsDataChannel, WsDataChannel) {
        let (client_io, server_io) = tokio::io::duplex(64 * 1024);
        let server = tokio::spawn(async move {
            tokio_tungstenite::accept_async(server_io)
                .await
                .expect("server accept")
        });
        let (client_ws, _resp) = tokio_tungstenite::client_async("ws://localhost/", client_io)
            .await
            .expect("client connect");
        let server_ws = server.await.expect("server join");
        (
            WsDataChannel::from_stream(client_ws),
            WsDataChannel::from_stream(server_ws),
        )
    }

    /// Pump the runtime until `source` yields a message or the bound is hit.
    async fn recv_bounded<C: DataChannel>(source: &mut WebRtcSource<C>) -> Option<IpcMessage>
    where
        C::Error: std::fmt::Debug,
    {
        for _ in 0..500 {
            if let Some(msg) = source.recv().expect("recv") {
                return Some(msg);
            }
            tokio::time::sleep(Duration::from_millis(1)).await;
        }
        None
    }

    #[tokio::test]
    async fn ws_send_frame_surfaces_backpressure_when_buffer_fills() {
        let (client, _server) = loopback().await;
        // The default #[tokio::test] runtime is single-threaded, so a tight
        // sync loop never yields to the driver task: the bounded outbound
        // channel fills without being drained.
        let mut hit_backpressure = false;
        for _ in 0..(WS_OUTBOUND_CAPACITY * 2) {
            match client.send_frame(vec![0u8; 4]) {
                Ok(()) => {}
                Err(WsError::Backpressure) => {
                    hit_backpressure = true;
                    break;
                }
                Err(WsError::Closed) => break,
            }
        }
        assert!(
            hit_backpressure,
            "send_frame must surface Backpressure once the bounded outbound buffer fills (instead of growing without bound)"
        );
    }

    #[test]
    fn inbound_caps_at_capacity_and_counts_drops() {
        // Direct unit test of the receive-side backpressure: a slow consumer
        // (never calling try_recv_frame) must not grow inbound memory without
        // bound; overflow is shed oldest-first and counted (#lzwsinboundunbounded).
        let inbound = Arc::new(Mutex::new(VecDeque::new()));
        let dropped = Arc::new(AtomicUsize::new(0));

        for i in 0..(WS_INBOUND_CAPACITY + 50) {
            push_inbound(&inbound, &dropped, vec![i as u8]);
        }

        {
            let queue = inbound.lock();
            assert_eq!(
                queue.len(),
                WS_INBOUND_CAPACITY,
                "inbound must be capped at WS_INBOUND_CAPACITY"
            );
            assert_eq!(
                queue.front().map(|v| v[0]),
                Some(50u8),
                "the oldest 50 overflowed frames must have been shed"
            );
        }
        assert_eq!(
            dropped.load(Ordering::Relaxed),
            50,
            "overflowing frames must be counted as dropped"
        );

        // Pushing past the cap keeps it pinned and the drop counter monotonically
        // increasing.
        push_inbound(&inbound, &dropped, vec![255u8]);
        let queue = inbound.lock();
        assert_eq!(queue.len(), WS_INBOUND_CAPACITY);
        assert_eq!(queue.back().map(|v| v[0]), Some(255u8));
        drop(queue);
        assert_eq!(dropped.load(Ordering::Relaxed), 51);
    }

    #[tokio::test]
    async fn ws_loopback_carries_permission_filtered_snapshot() {
        let (client, server) = loopback().await;

        let peer = PeerId(1);
        let mut perms = PeerPermissions::new();
        perms.allow_many(peer, OpKind::Read, [NodeId(1), NodeId(2)]);

        let mut sink = WebRtcSink::new(client, perms, peer);
        let mut source = WebRtcSource::new(server);

        // Node 3 is NOT in the peer's read allowlist — it must be omitted.
        let snapshot = Snapshot::new(
            1,
            vec![
                NodeSnapshot::payload(NodeId(1), "t", vec![1, 2, 3]),
                NodeSnapshot::payload(NodeId(2), "t", vec![4, 5, 6]),
                NodeSnapshot::payload(NodeId(3), "t", vec![7, 8, 9]),
            ],
            vec![],
            vec![NodeId(1), NodeId(2), NodeId(3)],
        );
        sink.send(&IpcMessage::Snapshot(snapshot)).unwrap();

        match recv_bounded(&mut source)
            .await
            .expect("snapshot to arrive over the websocket")
        {
            IpcMessage::Snapshot(s) => {
                let ids: Vec<u64> = s.nodes.iter().map(|n| n.node.0).collect();
                assert_eq!(ids, vec![1, 2], "unreadable node 3 must be omitted");
            }
            other => panic!("expected snapshot, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn ws_loopback_preserves_delta_order() {
        let (client, server) = loopback().await;

        let peer = PeerId(1);
        let mut perms = PeerPermissions::new();
        perms.allow_many(peer, OpKind::Read, [NodeId(1)]);

        let mut sink = WebRtcSink::new(client, perms, peer);
        let mut source = WebRtcSource::new(server);

        for epoch in 1..=3u64 {
            let delta = Delta::new(
                epoch - 1,
                epoch,
                vec![DeltaOp::cell_set(NodeId(1), vec![epoch as u8])],
            );
            sink.send(&IpcMessage::Delta(delta)).unwrap();
        }

        let mut epochs = Vec::new();
        for _ in 0..3 {
            match recv_bounded(&mut source).await.expect("delta to arrive") {
                IpcMessage::Delta(d) => epochs.push(d.epoch),
                other => panic!("expected delta, got {other:?}"),
            }
        }
        assert_eq!(epochs, vec![1, 2, 3], "deltas must arrive in send order");
    }
}