wacore 0.7.0

Core WhatsApp protocol implementation without runtime dependencies
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
406
407
408
409
410
411
412
413
414
415
416
417
//! Pluggable time providers.
//!
//! Two clocks live here, kept distinct on purpose:
//!
//! - **Wall clock** ([`TimeProvider`], [`now_millis`], [`now_utc`]) — answers
//!   "what time is it?". Can jump backwards (NTP sync, manual clock changes,
//!   leap-second smearing). Resolution: milliseconds is sufficient for
//!   timestamps in stanzas, app-state mutations, and log lines.
//! - **Monotonic clock** ([`MonotonicProvider`], [`Instant`]) — answers
//!   "how much time passed?". Never moves backwards; immune to NTP
//!   adjustments. Resolution: nanoseconds where the platform supports it
//!   (`std::time::Instant` on native; sub-millisecond from `performance.now`
//!   in browsers, true ns in Node/WASI when those targets supply a custom
//!   provider).
//!
//! Conflating the two — using a wall clock to measure elapsed time — silently
//! corrupts timeouts and latency metrics whenever the system clock is adjusted
//! mid-measurement. That is why `std::time` separates `SystemTime` from
//! `Instant`, and we mirror the split here.

use std::sync::OnceLock;

/// Test-only clock-read accounting, so a budget over the hot path can be
/// asserted instead of estimated.
///
/// Counting sits at the abstraction boundary, not inside a provider:
/// [`set_time_provider`] is a `OnceLock` that the process's first `now_millis()`
/// fills with the default, so a suite sharing one process cannot install an
/// instrumented provider per test. Counting here also covers the defaults.
#[cfg(feature = "test-util")]
pub mod clock_reads {
    use core::cell::Cell;

    std::thread_local! {
        static WALL: Cell<u64> = const { Cell::new(0) };
        static MONOTONIC: Cell<u64> = const { Cell::new(0) };
    }

    /// Reads counted on one thread.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub struct Reads {
        /// Reads of the wall clock ([`super::now_millis`] and everything built
        /// on it).
        pub wall: u64,
        /// Reads of the monotonic clock ([`super::Instant::now`],
        /// [`super::Instant::elapsed`]).
        pub monotonic: u64,
    }

    impl Reads {
        pub fn total(&self) -> u64 {
            self.wall + self.monotonic
        }
    }

    #[inline]
    pub(super) fn bump_wall() {
        WALL.with(|c| c.set(c.get().saturating_add(1)));
    }

    #[inline]
    pub(super) fn bump_monotonic() {
        MONOTONIC.with(|c| c.set(c.get().saturating_add(1)));
    }

    /// Counters for the calling thread.
    ///
    /// Per thread, not process-wide, so a measurement stays exact while the
    /// rest of the suite runs in parallel. Work handed to a blocking pool (the
    /// storage backend) is counted on that thread and stays invisible here.
    pub fn snapshot() -> Reads {
        Reads {
            wall: WALL.with(Cell::get),
            monotonic: MONOTONIC.with(Cell::get),
        }
    }

    /// Reads made on this thread since `base`.
    pub fn since(base: Reads) -> Reads {
        let now = snapshot();
        Reads {
            wall: now.wall.saturating_sub(base.wall),
            monotonic: now.monotonic.saturating_sub(base.monotonic),
        }
    }
}

/// Wall-clock provider. Returns the current Unix time. May move backwards
/// across calls when the system clock is adjusted.
pub trait TimeProvider: Send + Sync + 'static {
    /// Current time as milliseconds since Unix epoch.
    fn now_millis(&self) -> i64;
}

/// Default wall-clock provider using `chrono` (native targets only).
///
/// cfg-gated off `wasm32` for the same reason the monotonic clock is: there is
/// no backend for `chrono::Utc::now()` on `wasm32-unknown-unknown` (we don't pull
/// `wasmbind`), so it falls through to `SystemTime::now()`, which panics. The
/// wasm default is [`UnsetWasmTimeProvider`].
#[cfg(not(target_arch = "wasm32"))]
struct ChronoTimeProvider;

#[cfg(not(target_arch = "wasm32"))]
impl TimeProvider for ChronoTimeProvider {
    // The single legitimate call to `chrono::Utc::now()`: this IS the default
    // provider backing `wacore::time::now_utc()`. Everywhere else must go
    // through the abstraction — see clippy.toml.
    #[allow(clippy::disallowed_methods)]
    fn now_millis(&self) -> i64 {
        chrono::Utc::now().timestamp_millis()
    }
}

/// WASM default when no wall-clock provider is registered. Unlike the monotonic
/// clock, the wall clock has no internal source on `wasm32` (and we won't panic
/// like `chrono::Utc::now()` would), so this returns epoch (0) and warns once.
/// Embedders MUST call [`set_time_provider`] with a real provider (e.g. backed
/// by `Date.now()`) before the first timestamp.
#[cfg(target_arch = "wasm32")]
struct UnsetWasmTimeProvider;

#[cfg(target_arch = "wasm32")]
impl TimeProvider for UnsetWasmTimeProvider {
    fn now_millis(&self) -> i64 {
        use std::sync::atomic::{AtomicBool, Ordering};
        static WARNED: AtomicBool = AtomicBool::new(false);
        if !WARNED.swap(true, Ordering::Relaxed) {
            log::warn!(
                "wacore::time: no wall-clock provider set on wasm32; returning epoch. \
                 Call set_time_provider() before the first timestamp."
            );
        }
        0
    }
}

static TIME_PROVIDER: OnceLock<Box<dyn TimeProvider>> = OnceLock::new();

/// Set a custom wall-clock provider. Must be called before any time function
/// is used. Returns `Err` if a provider has already been set.
pub fn set_time_provider(provider: impl TimeProvider) -> Result<(), &'static str> {
    TIME_PROVIDER
        .set(Box::new(provider))
        .map_err(|_| "time provider already set")
}

/// Current time in milliseconds since Unix epoch.
#[cfg(not(target_arch = "wasm32"))]
#[inline]
pub fn now_millis() -> i64 {
    #[cfg(feature = "test-util")]
    clock_reads::bump_wall();
    TIME_PROVIDER
        .get_or_init(default_time_provider)
        .now_millis()
}

/// On wasm32 the epoch fallback is used transiently and never stored in the
/// `OnceLock`, so a later `set_time_provider()` always wins even if an early
/// timestamp already ran during initialization.
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn now_millis() -> i64 {
    #[cfg(feature = "test-util")]
    clock_reads::bump_wall();
    match TIME_PROVIDER.get() {
        Some(provider) => provider.now_millis(),
        None => UnsetWasmTimeProvider.now_millis(),
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn default_time_provider() -> Box<dyn TimeProvider> {
    Box::new(ChronoTimeProvider)
}

/// Current time in seconds since Unix epoch.
#[inline]
pub fn now_secs() -> i64 {
    now_millis() / 1000
}

/// Current time in seconds since Unix epoch, saturated at 0.
///
/// Most stanza encodings carry timestamps as unsigned u64. A naive
/// `now_secs() as u64` silently wraps when the clock is pre-1970 (e.g. an
/// uninitialized system clock during early boot) and corrupts the stanza.
/// This helper clamps to 0 instead.
#[inline]
pub fn now_secs_u64() -> u64 {
    now_secs().max(0) as u64
}

/// Current time as `chrono::DateTime<Utc>`.
#[inline]
pub fn now_utc() -> chrono::DateTime<chrono::Utc> {
    chrono::DateTime::from_timestamp_millis(now_millis())
        .expect("time provider returned out-of-range millisecond timestamp")
}

/// Convert a Unix timestamp (seconds) to `DateTime<Utc>`.
/// Returns `None` for out-of-range values.
#[inline]
pub fn from_secs(ts: i64) -> Option<chrono::DateTime<chrono::Utc>> {
    chrono::DateTime::from_timestamp(ts, 0)
}

/// Convert a Unix timestamp (seconds) to `DateTime<Utc>`,
/// falling back to `now_utc()` for out-of-range values.
#[inline]
pub fn from_secs_or_now(ts: i64) -> chrono::DateTime<chrono::Utc> {
    from_secs(ts).unwrap_or_else(now_utc)
}

/// Convert a Unix timestamp (milliseconds) to `DateTime<Utc>`.
/// Returns `None` for out-of-range values.
#[inline]
pub fn from_millis(ts: i64) -> Option<chrono::DateTime<chrono::Utc>> {
    chrono::DateTime::from_timestamp_millis(ts)
}

/// Convert a Unix timestamp (milliseconds) to `DateTime<Utc>`,
/// falling back to `now_utc()` for out-of-range values.
#[inline]
pub fn from_millis_or_now(ts: i64) -> chrono::DateTime<chrono::Utc> {
    from_millis(ts).unwrap_or_else(now_utc)
}

// ---------------------------------------------------------------------------
// Monotonic clock
// ---------------------------------------------------------------------------

/// Monotonic-clock provider. Returns nanoseconds since an arbitrary fixed
/// reference; the only guarantee is that successive calls never return a
/// smaller value than a previous one.
pub trait MonotonicProvider: Send + Sync + 'static {
    /// Nanoseconds since the provider's reference point. The reference is
    /// implementation-defined (may be process start, system boot, etc.) —
    /// only differences are meaningful.
    fn now_nanos(&self) -> u64;
}

/// Native default: backed by `std::time::Instant`, which is monotonic and
/// has nanosecond resolution on every supported native platform.
#[cfg(not(target_arch = "wasm32"))]
struct StdMonotonicProvider {
    epoch: std::time::Instant,
}

#[cfg(not(target_arch = "wasm32"))]
impl StdMonotonicProvider {
    // The single legitimate call to `std::time::Instant::now()`: this IS the
    // default provider backing `wacore::time::Instant`. Everywhere else must
    // go through this abstraction — see clippy.toml.
    #[allow(clippy::disallowed_methods)]
    fn new() -> Self {
        Self {
            epoch: std::time::Instant::now(),
        }
    }
}

#[cfg(not(target_arch = "wasm32"))]
impl MonotonicProvider for StdMonotonicProvider {
    fn now_nanos(&self) -> u64 {
        // Saturate at u64::MAX (584 years) — well beyond any realistic
        // process lifetime.
        self.epoch.elapsed().as_nanos().min(u64::MAX as u128) as u64
    }
}

/// WASM fallback when no platform provider is registered. Derives nanos
/// from the wall clock and clamps to non-decreasing so the trait contract
/// holds across NTP backjumps (the value freezes until the wall clock
/// catches up). Resolution is ms × 1_000_000; embedders should register
/// a real provider via [`set_monotonic_provider`] for sub-ms precision.
#[cfg(target_arch = "wasm32")]
struct WallDerivedMonotonicProvider {
    last: portable_atomic::AtomicU64,
}

#[cfg(target_arch = "wasm32")]
impl WallDerivedMonotonicProvider {
    const fn new() -> Self {
        Self {
            last: portable_atomic::AtomicU64::new(0),
        }
    }
}

#[cfg(target_arch = "wasm32")]
impl MonotonicProvider for WallDerivedMonotonicProvider {
    fn now_nanos(&self) -> u64 {
        use std::sync::atomic::Ordering;
        let raw = (now_millis().max(0) as u64).saturating_mul(1_000_000);
        let mut last = self.last.load(Ordering::Relaxed);
        loop {
            let next = raw.max(last);
            match self
                .last
                .compare_exchange_weak(last, next, Ordering::Relaxed, Ordering::Relaxed)
            {
                Ok(_) => return next,
                Err(observed) => last = observed,
            }
        }
    }
}

static MONOTONIC_PROVIDER: OnceLock<Box<dyn MonotonicProvider>> = OnceLock::new();

/// Set a custom monotonic-clock provider. Must be called before any
/// [`Instant`] is captured. Returns `Err` if a provider has already been set.
pub fn set_monotonic_provider(provider: impl MonotonicProvider) -> Result<(), &'static str> {
    MONOTONIC_PROVIDER
        .set(Box::new(provider))
        .map_err(|_| "monotonic provider already set")
}

#[inline]
fn now_nanos() -> u64 {
    #[cfg(feature = "test-util")]
    clock_reads::bump_monotonic();
    MONOTONIC_PROVIDER
        .get_or_init(default_monotonic_provider)
        .now_nanos()
}

#[cfg(not(target_arch = "wasm32"))]
fn default_monotonic_provider() -> Box<dyn MonotonicProvider> {
    Box::new(StdMonotonicProvider::new())
}

#[cfg(target_arch = "wasm32")]
fn default_monotonic_provider() -> Box<dyn MonotonicProvider> {
    Box::new(WallDerivedMonotonicProvider::new())
}

/// Portable monotonic instant. On native targets this wraps `std::time::Instant`
/// (via the default [`MonotonicProvider`]) and exposes nanosecond resolution.
/// On `wasm32` targets the embedder should register a sub-millisecond provider
/// via [`set_monotonic_provider`]; otherwise the fallback derives from the
/// wall clock and quantizes to milliseconds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Instant(u64);

impl Instant {
    /// The origin of the configured monotonic clock.
    ///
    /// Useful as a storage sentinel when a component has time-based behavior
    /// disabled and therefore must not read the platform clock.
    pub const ZERO: Self = Self(0);

    /// Capture the current monotonic instant.
    #[inline]
    pub fn now() -> Self {
        Self(now_nanos())
    }

    /// Duration elapsed since this instant was captured. Returns
    /// [`Duration::ZERO`](std::time::Duration::ZERO) if the clock somehow reported a smaller value
    /// than at capture time — which a well-behaved monotonic provider
    /// must never do, but we saturate defensively.
    #[inline]
    pub fn elapsed(&self) -> std::time::Duration {
        let now = now_nanos();
        std::time::Duration::from_nanos(now.saturating_sub(self.0))
    }

    /// Duration from `earlier` to `self`. Returns [`Duration::ZERO`](std::time::Duration::ZERO) if
    /// `earlier` is after `self`.
    #[inline]
    pub fn saturating_duration_since(&self, earlier: Instant) -> std::time::Duration {
        std::time::Duration::from_nanos(self.0.saturating_sub(earlier.0))
    }
}

impl std::ops::Add<std::time::Duration> for Instant {
    type Output = Instant;
    fn add(self, rhs: std::time::Duration) -> Self {
        let rhs_nanos: u64 = rhs.as_nanos().min(u64::MAX as u128) as u64;
        Self(self.0.saturating_add(rhs_nanos))
    }
}

impl std::ops::Sub<std::time::Duration> for Instant {
    type Output = Instant;
    fn sub(self, rhs: std::time::Duration) -> Self {
        let rhs_nanos: u64 = rhs.as_nanos().min(u64::MAX as u128) as u64;
        Self(self.0.saturating_sub(rhs_nanos))
    }
}

impl std::ops::Sub<Instant> for Instant {
    type Output = std::time::Duration;
    fn sub(self, rhs: Instant) -> std::time::Duration {
        self.saturating_duration_since(rhs)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // The native default must yield a real wall-clock time, not the wasm fallback's
    // epoch. Guards the cfg-split refactor that keeps wasm32 from panicking.
    #[cfg(not(target_arch = "wasm32"))]
    #[test]
    fn native_default_time_provider_returns_real_time() {
        let ms = default_time_provider().now_millis();
        assert!(
            ms > 1_600_000_000_000,
            "expected a post-2020 timestamp, got {ms}"
        );
    }
}