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}