Skip to main content

fsqlite_types/
sync_primitives.rs

1//! Platform-agnostic sync primitives for FrankenSQLite.
2//!
3//! On native targets, these re-export `parking_lot` types for performance.
4//! On `wasm32`, they provide wrappers around `std::sync` primitives, which
5//! work correctly in the single-threaded wasm environment without requiring
6//! unsafe Send/Sync impls.
7//!
8//! Downstream crates should import from here instead of `parking_lot`
9//! directly to enable WASM compilation without `#[cfg]` at every call site.
10
11// ---------------------------------------------------------------------------
12// Native (parking_lot) — faster than std::sync on multi-core
13// ---------------------------------------------------------------------------
14
15#[cfg(not(target_arch = "wasm32"))]
16pub use parking_lot::{
17    Condvar, Mutex, MutexGuard, Once, RwLock, RwLockReadGuard, RwLockWriteGuard,
18};
19
20// ---------------------------------------------------------------------------
21// WASM — thin wrappers around std::sync to match parking_lot's API
22// ---------------------------------------------------------------------------
23
24#[cfg(target_arch = "wasm32")]
25mod wasm_sync {
26    use std::ops::{Deref, DerefMut};
27    use std::sync::{
28        Mutex as StdMutex, MutexGuard as StdMutexGuard, Once as StdOnce, PoisonError,
29        RwLock as StdRwLock, RwLockReadGuard as StdRwReadGuard,
30        RwLockWriteGuard as StdRwWriteGuard,
31    };
32
33    // -- Mutex ----------------------------------------------------------------
34
35    pub struct Mutex<T: ?Sized>(StdMutex<T>);
36
37    impl<T> Mutex<T> {
38        pub const fn new(val: T) -> Self {
39            Self(StdMutex::new(val))
40        }
41
42        pub fn into_inner(self) -> T {
43            self.0.into_inner().unwrap_or_else(PoisonError::into_inner)
44        }
45    }
46
47    impl<T: ?Sized> Mutex<T> {
48        pub fn lock(&self) -> MutexGuard<'_, T> {
49            MutexGuard(self.0.lock().unwrap_or_else(PoisonError::into_inner))
50        }
51
52        pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
53            self.0.try_lock().ok().map(MutexGuard)
54        }
55
56        pub fn is_locked(&self) -> bool {
57            self.0.try_lock().is_err()
58        }
59
60        pub fn get_mut(&mut self) -> &mut T {
61            self.0.get_mut().unwrap_or_else(PoisonError::into_inner)
62        }
63    }
64
65    impl<T: Default> Default for Mutex<T> {
66        fn default() -> Self {
67            Self::new(T::default())
68        }
69    }
70
71    impl<T: std::fmt::Debug + ?Sized> std::fmt::Debug for Mutex<T> {
72        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73            match self.try_lock() {
74                Some(guard) => f.debug_struct("Mutex").field("data", &&*guard).finish(),
75                None => f.debug_struct("Mutex").field("data", &"<locked>").finish(),
76            }
77        }
78    }
79
80    pub struct MutexGuard<'a, T: ?Sized>(StdMutexGuard<'a, T>);
81
82    impl<T: ?Sized> Deref for MutexGuard<'_, T> {
83        type Target = T;
84        fn deref(&self) -> &T {
85            &self.0
86        }
87    }
88
89    impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
90        fn deref_mut(&mut self) -> &mut T {
91            &mut self.0
92        }
93    }
94
95    // -- RwLock ---------------------------------------------------------------
96
97    pub struct RwLock<T: ?Sized>(StdRwLock<T>);
98
99    impl<T> RwLock<T> {
100        pub const fn new(val: T) -> Self {
101            Self(StdRwLock::new(val))
102        }
103
104        pub fn into_inner(self) -> T {
105            self.0.into_inner().unwrap_or_else(PoisonError::into_inner)
106        }
107    }
108
109    impl<T: ?Sized> RwLock<T> {
110        pub fn read(&self) -> RwLockReadGuard<'_, T> {
111            RwLockReadGuard(self.0.read().unwrap_or_else(PoisonError::into_inner))
112        }
113
114        pub fn try_read(&self) -> Option<RwLockReadGuard<'_, T>> {
115            self.0.try_read().ok().map(RwLockReadGuard)
116        }
117
118        pub fn write(&self) -> RwLockWriteGuard<'_, T> {
119            RwLockWriteGuard(self.0.write().unwrap_or_else(PoisonError::into_inner))
120        }
121
122        pub fn try_write(&self) -> Option<RwLockWriteGuard<'_, T>> {
123            self.0.try_write().ok().map(RwLockWriteGuard)
124        }
125
126        pub fn get_mut(&mut self) -> &mut T {
127            self.0.get_mut().unwrap_or_else(PoisonError::into_inner)
128        }
129    }
130
131    impl<T: Default> Default for RwLock<T> {
132        fn default() -> Self {
133            Self::new(T::default())
134        }
135    }
136
137    impl<T: std::fmt::Debug + ?Sized> std::fmt::Debug for RwLock<T> {
138        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139            match self.try_read() {
140                Some(guard) => f.debug_struct("RwLock").field("data", &&*guard).finish(),
141                None => f.debug_struct("RwLock").field("data", &"<locked>").finish(),
142            }
143        }
144    }
145
146    pub struct RwLockReadGuard<'a, T: ?Sized>(StdRwReadGuard<'a, T>);
147
148    impl<T: ?Sized> Deref for RwLockReadGuard<'_, T> {
149        type Target = T;
150        fn deref(&self) -> &T {
151            &self.0
152        }
153    }
154
155    pub struct RwLockWriteGuard<'a, T: ?Sized>(StdRwWriteGuard<'a, T>);
156
157    impl<T: ?Sized> Deref for RwLockWriteGuard<'_, T> {
158        type Target = T;
159        fn deref(&self) -> &T {
160            &self.0
161        }
162    }
163
164    impl<T: ?Sized> DerefMut for RwLockWriteGuard<'_, T> {
165        fn deref_mut(&mut self) -> &mut T {
166            &mut self.0
167        }
168    }
169
170    // -- Once -----------------------------------------------------------------
171
172    pub struct Once(StdOnce);
173
174    impl Once {
175        pub const fn new() -> Self {
176            Self(StdOnce::new())
177        }
178
179        pub fn call_once(&self, f: impl FnOnce()) {
180            self.0.call_once(f);
181        }
182
183        pub fn is_completed(&self) -> bool {
184            self.0.is_completed()
185        }
186    }
187
188    impl Default for Once {
189        fn default() -> Self {
190            Self::new()
191        }
192    }
193
194    // -- Condvar --------------------------------------------------------------
195
196    pub struct Condvar(std::sync::Condvar);
197
198    impl Condvar {
199        pub const fn new() -> Self {
200            Self(std::sync::Condvar::new())
201        }
202
203        pub fn notify_one(&self) {
204            self.0.notify_one();
205        }
206
207        pub fn notify_all(&self) {
208            self.0.notify_all();
209        }
210
211        /// Wait on the condvar. On wasm32, the underlying std::sync::Condvar
212        /// works but will never actually block (single-threaded).
213        /// Note: parking_lot's Condvar::wait takes `&mut MutexGuard`, so the
214        /// wasm shim mirrors that signature even though it is effectively a
215        /// no-op in the single-threaded runtime.
216        pub fn wait<T>(&self, guard: &mut MutexGuard<'_, T>) {
217            let _ = (&self.0, guard);
218        }
219    }
220
221    impl Default for Condvar {
222        fn default() -> Self {
223            Self::new()
224        }
225    }
226
227    impl std::fmt::Debug for Condvar {
228        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229            f.write_str("Condvar")
230        }
231    }
232}
233
234#[cfg(target_arch = "wasm32")]
235pub use wasm_sync::{Condvar, Mutex, MutexGuard, Once, RwLock, RwLockReadGuard, RwLockWriteGuard};
236
237// ---------------------------------------------------------------------------
238// Time polyfill — Instant / Duration
239// ---------------------------------------------------------------------------
240
241#[cfg(not(target_arch = "wasm32"))]
242pub use std::time::{Duration, Instant};
243
244#[cfg(target_arch = "wasm32")]
245pub use std::time::Duration;
246
247#[cfg(target_arch = "wasm32")]
248use std::sync::atomic::{AtomicU64, Ordering};
249
250/// Monotonic instant polyfill for wasm32.
251///
252/// On `wasm32-unknown-unknown` there is no reliable host-independent
253/// monotonic clock without a JavaScript runtime. We therefore expose a small
254/// deterministic pseudo-clock that advances one millisecond on each `now()`
255/// call. This preserves ordering/arithmetic semantics used by timeout and
256/// observability code without introducing JS host dependencies.
257#[cfg(target_arch = "wasm32")]
258#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
259pub struct Instant(Duration);
260
261#[cfg(target_arch = "wasm32")]
262static WASM_INSTANT_TICKS_MS: AtomicU64 = AtomicU64::new(0);
263
264#[cfg(target_arch = "wasm32")]
265impl Instant {
266    pub fn now() -> Self {
267        Self(Duration::from_millis(
268            WASM_INSTANT_TICKS_MS.fetch_add(1, Ordering::Relaxed),
269        ))
270    }
271
272    pub fn elapsed(&self) -> Duration {
273        Self::now().saturating_duration_since(*self)
274    }
275
276    pub fn duration_since(self, earlier: Self) -> Duration {
277        self.0.checked_sub(earlier.0).unwrap_or(Duration::ZERO)
278    }
279
280    pub fn saturating_duration_since(self, earlier: Self) -> Duration {
281        self.duration_since(earlier)
282    }
283
284    pub fn checked_duration_since(self, earlier: Self) -> Option<Duration> {
285        self.0.checked_sub(earlier.0)
286    }
287
288    pub fn checked_add(self, duration: Duration) -> Option<Self> {
289        self.0.checked_add(duration).map(Self)
290    }
291
292    pub fn checked_sub(self, duration: Duration) -> Option<Self> {
293        self.0.checked_sub(duration).map(Self)
294    }
295}
296
297#[cfg(target_arch = "wasm32")]
298impl std::ops::Add<Duration> for Instant {
299    type Output = Self;
300
301    fn add(self, rhs: Duration) -> Self::Output {
302        Self(self.0 + rhs)
303    }
304}
305
306#[cfg(target_arch = "wasm32")]
307impl std::ops::AddAssign<Duration> for Instant {
308    fn add_assign(&mut self, rhs: Duration) {
309        self.0 += rhs;
310    }
311}
312
313#[cfg(target_arch = "wasm32")]
314impl std::ops::Sub<Duration> for Instant {
315    type Output = Self;
316
317    fn sub(self, rhs: Duration) -> Self::Output {
318        Self(self.0.checked_sub(rhs).unwrap_or(Duration::ZERO))
319    }
320}
321
322#[cfg(target_arch = "wasm32")]
323impl std::ops::SubAssign<Duration> for Instant {
324    fn sub_assign(&mut self, rhs: Duration) {
325        self.0 = self.0.checked_sub(rhs).unwrap_or(Duration::ZERO);
326    }
327}
328
329#[cfg(target_arch = "wasm32")]
330impl std::ops::Sub<Instant> for Instant {
331    type Output = Duration;
332
333    fn sub(self, rhs: Instant) -> Self::Output {
334        self.duration_since(rhs)
335    }
336}
337
338// ---------------------------------------------------------------------------
339// Thread ID polyfill
340// ---------------------------------------------------------------------------
341
342#[cfg(not(target_arch = "wasm32"))]
343pub fn current_thread_id() -> u64 {
344    // ThreadId doesn't expose a numeric value on stable Rust, so we
345    // use the Debug format to extract a deterministic integer. This is
346    // only used for diagnostics/tracing, never for correctness.
347    let id = std::thread::current().id();
348    let s = format!("{id:?}");
349    s.trim_start_matches("ThreadId(")
350        .trim_end_matches(')')
351        .parse::<u64>()
352        .unwrap_or(0)
353}
354
355#[cfg(target_arch = "wasm32")]
356pub fn current_thread_id() -> u64 {
357    0 // Single-threaded on wasm32, always "thread 0".
358}
359
360// ---------------------------------------------------------------------------
361// Tests
362// ---------------------------------------------------------------------------
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn mutex_lock_unlock() {
370        let m = Mutex::new(42);
371        {
372            let mut g = m.lock();
373            assert_eq!(*g, 42);
374            *g = 99;
375        }
376        assert_eq!(*m.lock(), 99);
377    }
378
379    #[test]
380    fn mutex_try_lock_when_unlocked() {
381        let m = Mutex::new(1);
382        assert!(m.try_lock().is_some());
383    }
384
385    #[test]
386    fn rwlock_read_write() {
387        let rw = RwLock::new(String::from("hello"));
388        {
389            let r = rw.read();
390            assert_eq!(&*r, "hello");
391        }
392        {
393            let mut w = rw.write();
394            w.push_str(" world");
395        }
396        assert_eq!(&*rw.read(), "hello world");
397    }
398
399    #[test]
400    fn once_calls_once() {
401        let once = Once::new();
402        let mut count = 0;
403        once.call_once(|| count += 1);
404        once.call_once(|| count += 1);
405        assert_eq!(count, 1);
406    }
407
408    #[test]
409    fn thread_id_returns_value() {
410        let id = current_thread_id();
411        // On native: non-zero thread ID; on wasm: 0
412        let _ = id;
413    }
414
415    #[test]
416    fn mutex_default() {
417        let m: Mutex<i32> = Mutex::default();
418        assert_eq!(*m.lock(), 0);
419    }
420
421    #[test]
422    fn mutex_into_inner() {
423        let m = Mutex::new(vec![1, 2, 3]);
424        let v = m.into_inner();
425        assert_eq!(v, vec![1, 2, 3]);
426    }
427
428    #[test]
429    fn condvar_notify_noop() {
430        let cv = Condvar::new();
431        cv.notify_one();
432        cv.notify_all();
433    }
434
435    #[test]
436    fn rwlock_into_inner() {
437        let rw = RwLock::new(42);
438        assert_eq!(rw.into_inner(), 42);
439    }
440
441    #[cfg(target_arch = "wasm32")]
442    #[test]
443    fn once_is_completed() {
444        let once = Once::new();
445        assert!(!once.is_completed());
446        once.call_once(|| {});
447        assert!(once.is_completed());
448    }
449}