Skip to main content

nodedb_lite/
runtime.rs

1//! Platform-specific async runtime abstractions.
2//!
3//! NodeDB-Lite compiles for native (Tokio) and WASM (`wasm-bindgen-futures`).
4//! This module provides a thin abstraction over the differences so engine
5//! code doesn't need `#[cfg]` everywhere.
6//!
7//! **Native (iOS/Android/Desktop):** Tokio — `spawn`, `spawn_blocking`, `sleep`.
8//! **WASM (Browser):** `wasm-bindgen-futures` — `spawn_local`, no blocking threads.
9
10use std::future::Future;
11use std::time::Duration;
12
13/// Spawn a future on the runtime.
14///
15/// - Native: `tokio::spawn` (runs on Tokio thread pool, requires `Send`).
16/// - WASM: `wasm_bindgen_futures::spawn_local` (runs on the microtask queue).
17#[cfg(not(target_arch = "wasm32"))]
18pub fn spawn<F>(future: F)
19where
20    F: Future<Output = ()> + Send + 'static,
21{
22    tokio::spawn(future);
23}
24
25#[cfg(target_arch = "wasm32")]
26pub fn spawn<F>(future: F)
27where
28    F: Future<Output = ()> + 'static,
29{
30    // wasm_bindgen_futures::spawn_local(future);
31    // For now, this is a compile-gate placeholder. The actual
32    // wasm-bindgen-futures dependency is added when WASM support
33    // is fully wired.
34    let _ = future;
35}
36
37/// Run a blocking closure off the async runtime.
38///
39/// - Native: `tokio::task::spawn_blocking` — moves closure to the blocking pool.
40/// - WASM: Runs synchronously (WASM has no blocking pool; callers must
41///   ensure the closure is fast or use the async StorageEngine path).
42#[cfg(not(target_arch = "wasm32"))]
43pub async fn spawn_blocking<F, T>(f: F) -> Result<T, crate::error::LiteError>
44where
45    F: FnOnce() -> T + Send + 'static,
46    T: Send + 'static,
47{
48    tokio::task::spawn_blocking(f)
49        .await
50        .map_err(|e| crate::error::LiteError::JoinError {
51            detail: e.to_string(),
52        })
53}
54
55#[cfg(target_arch = "wasm32")]
56pub async fn spawn_blocking<F, T>(f: F) -> Result<T, crate::error::LiteError>
57where
58    F: FnOnce() -> T,
59{
60    // No blocking pool on WASM — run synchronously.
61    // This is acceptable because:
62    // 1. SQLite WASM operations are fast (in-memory or OPFS sync access)
63    // 2. HNSW/CSR operations are CPU-bound but sub-millisecond for edge datasets
64    Ok(f())
65}
66
67/// Sleep for a duration.
68///
69/// - Native: `tokio::time::sleep`.
70/// - WASM: placeholder (will use `gloo_timers` or JS `setTimeout` via wasm-bindgen).
71#[cfg(not(target_arch = "wasm32"))]
72pub async fn sleep(duration: Duration) {
73    tokio::time::sleep(duration).await;
74}
75
76#[cfg(target_arch = "wasm32")]
77pub async fn sleep(duration: Duration) {
78    // Placeholder for WASM sleep. In production, this would use:
79    // gloo_timers::future::sleep(duration).await
80    let _ = duration;
81}
82
83/// Create a recurring interval timer.
84///
85/// Returns a stream-like async function that yields at each tick.
86/// Used by the sync client for periodic keepalive and vector clock exchange.
87///
88/// - Native: `tokio::time::interval`.
89/// - WASM: placeholder (will use `gloo_timers::future::IntervalStream`).
90#[cfg(not(target_arch = "wasm32"))]
91pub fn interval(period: Duration) -> tokio::time::Interval {
92    tokio::time::interval(period)
93}
94
95/// Get the current timestamp in milliseconds since Unix epoch.
96///
97/// Platform-independent — works on native and WASM.
98pub fn now_millis() -> u64 {
99    #[cfg(not(target_arch = "wasm32"))]
100    {
101        std::time::SystemTime::now()
102            .duration_since(std::time::UNIX_EPOCH)
103            .unwrap_or_default()
104            .as_millis() as u64
105    }
106    #[cfg(target_arch = "wasm32")]
107    {
108        // js_sys::Date::now() returns milliseconds as f64.
109        // For now, return 0 — wired when wasm-bindgen is added.
110        0
111    }
112}
113
114/// Get the current timestamp in seconds since Unix epoch.
115pub fn now_secs() -> u64 {
116    now_millis() / 1000
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[tokio::test]
124    async fn spawn_blocking_works() {
125        let result = spawn_blocking(|| 42).await.unwrap();
126        assert_eq!(result, 42);
127    }
128
129    #[tokio::test]
130    async fn spawn_blocking_string() {
131        let result = spawn_blocking(|| "hello".to_string()).await.unwrap();
132        assert_eq!(result, "hello");
133    }
134
135    #[tokio::test]
136    async fn sleep_returns() {
137        // Just verify it doesn't hang.
138        sleep(Duration::from_millis(1)).await;
139    }
140
141    #[test]
142    fn now_millis_nonzero() {
143        let ts = now_millis();
144        assert!(ts > 0, "timestamp should be nonzero on native");
145    }
146
147    #[test]
148    fn now_secs_reasonable() {
149        let ts = now_secs();
150        // Should be after 2024-01-01 (1704067200).
151        assert!(ts > 1_704_067_200, "timestamp {ts} seems too old");
152    }
153
154    #[tokio::test]
155    async fn interval_creation() {
156        let _iv = interval(Duration::from_secs(1));
157        // Just verify it compiles and doesn't panic.
158    }
159
160    #[tokio::test]
161    async fn spawn_fires() {
162        let (tx, rx) = tokio::sync::oneshot::channel();
163        spawn(async move {
164            let _ = tx.send(42);
165        });
166        let val = rx.await.unwrap();
167        assert_eq!(val, 42);
168    }
169}