this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Collapse Opake time injection into a single microsecond clock

Opake took two parallel time function pointers — now: fn() -> String and
now_micros: fn() -> u64 — even though both wrapped the same underlying
clock on each platform (chrono on CLI, js_sys::Date on WASM). The RFC 3339
string is just a formatted view of the same instant.

Derive the string inside core from the microsecond source via a new
timestamp::rfc3339_from_micros helper (Hinnant's civil-from-days, no
chrono dep in core — platform-free stays platform-free). One injection,
one source of truth, identical formatting between CLI and WASM.

Also fix two callsites in opake-wasm/src/opake_wasm.rs::check_session that
were bypassing the injection and calling now_iso() directly — they now
route through opake.now() like every other consumer.

+164 -41
-1
apps/cli/src/commands/daemon/mod.rs
··· 403 403 Some(did), 404 404 ReqwestTransport::new(), 405 405 OsRng, 406 - session::chrono_now, 407 406 session::chrono_now_micros, 408 407 ) 409 408 .await?;
-5
apps/cli/src/session.rs
··· 17 17 pub storage: FileStorage, 18 18 } 19 19 20 - pub fn chrono_now() -> String { 21 - Utc::now().to_rfc3339() 22 - } 23 - 24 20 pub fn chrono_now_micros() -> u64 { 25 21 Utc::now().timestamp_micros() as u64 26 22 } ··· 37 33 Some(&self.did), 38 34 ReqwestTransport::new(), 39 35 OsRng, 40 - chrono_now, 41 36 chrono_now_micros, 42 37 ) 43 38 .await?;
+2 -3
crates/opake-core/src/account_config_tests.rs
··· 65 65 Some(identity), 66 66 OsRng, 67 67 NoopStorage, 68 - || "2026-01-01T00:00:00Z".to_string(), 69 68 || 1_700_000_000_000_000, 70 69 ) 71 70 } ··· 243 242 "indexer_url must be preserved when absent from updates" 244 243 ); 245 244 assert_eq!( 246 - result.modified_at, "2026-01-01T00:00:00Z", 247 - "modified_at must be refreshed to the mocked now()" 245 + result.modified_at, "2023-11-14T22:13:20.000000Z", 246 + "modified_at must be refreshed to the mocked now() (derived from test_now_micros)" 248 247 ); 249 248 } 250 249
+1
crates/opake-core/src/lib.rs
··· 42 42 pub mod sharing; 43 43 pub mod storage; 44 44 pub mod tid; 45 + pub mod timestamp; 45 46 pub mod workspace; 46 47 47 48 #[cfg(any(test, feature = "test-utils"))]
+4 -4
crates/opake-core/src/manager/manager_tests.rs
··· 82 82 Some(identity), 83 83 OsRng, 84 84 NoopStorage, 85 - || "2026-01-01T00:00:00Z".into(), 86 85 || 1_700_000_000_000_000, 87 86 ); 88 87 ··· 119 118 vec![OTHER_DOC_URI.to_string()], 120 119 "deleted doc URI must be pruned from parent.entries; siblings preserved", 121 120 ); 122 - assert_eq!(updated.modified_at.as_deref(), Some("2026-01-01T00:00:00Z")); 121 + assert_eq!( 122 + updated.modified_at.as_deref(), 123 + Some("2023-11-14T22:13:20.000000Z") 124 + ); 123 125 } 124 126 125 127 /// Workspace delete where the caller owns the parent directory is the same ··· 168 170 Some(identity), 169 171 OsRng, 170 172 NoopStorage, 171 - || "2026-01-01T00:00:00Z".into(), 172 173 || 1_700_000_000_000_000, 173 174 ); 174 175 ··· 244 245 Some(identity), 245 246 OsRng, 246 247 NoopStorage, 247 - || "2026-01-01T00:00:00Z".into(), 248 248 || 1_700_000_000_000_000, 249 249 ); 250 250
+11 -9
crates/opake-core/src/opake.rs
··· 10 10 // - R: CryptoRng + RngCore (OsRng for both, ChaCha8Rng for tests) 11 11 // - S: Storage (FileStorage for CLI, NoopStorage for WASM until IndexedDb lands) 12 12 // 13 - // Time is injected as a function pointer — CLI passes chrono, WASM passes 14 - // js_sys::Date. No captures, no allocation. 13 + // Time is injected as a single `fn() -> u64` returning microseconds since 14 + // the Unix epoch. CLI passes a chrono-backed fn, WASM passes one backed by 15 + // `js_sys::Date`. RFC 3339 strings for record timestamp fields are derived 16 + // from the same source via `timestamp::rfc3339_from_micros` — one clock, one 17 + // injection, no drift between CLI and WASM formatting. 15 18 // 16 19 // Session persistence is automatic: after any XRPC call that triggers a 17 20 // token refresh, Opake persists the new session through Storage. ··· 37 40 pub(crate) identity: Option<Identity>, 38 41 pub(crate) rng: R, 39 42 pub(crate) storage: S, 40 - pub(crate) now_fn: fn() -> String, 43 + /// Injected clock returning microseconds since Unix epoch. RFC 3339 44 + /// timestamps are derived from this via `timestamp::rfc3339_from_micros`, 45 + /// so there is a single source of truth for "what time is it". 41 46 pub(crate) now_micros_fn: fn() -> u64, 42 47 /// Host-set runtime override — highest priority. Populated via 43 48 /// `set_indexer_url` at boot (CLI: `OPAKE_INDEXER_URL` env var; ··· 66 71 identity: Option<Identity>, 67 72 rng: R, 68 73 storage: S, 69 - now: fn() -> String, 70 74 now_micros: fn() -> u64, 71 75 ) -> Self { 72 76 Self { ··· 75 79 identity, 76 80 rng, 77 81 storage, 78 - now_fn: now, 79 82 now_micros_fn: now_micros, 80 83 runtime_indexer_url: None, 81 84 config_indexer_url: None, ··· 101 104 did: Option<&str>, 102 105 transport: T, 103 106 mut rng: R, 104 - now: fn() -> String, 105 107 now_micros: fn() -> u64, 106 108 ) -> Result<Self, Error> { 107 109 let config = storage.load_config().await?; ··· 131 133 }; 132 134 133 135 let client = XrpcClient::with_session(transport, account.pds_url.clone(), session); 134 - let opake = Self::new(client, target_did, identity, rng, storage, now, now_micros); 136 + let opake = Self::new(client, target_did, identity, rng, storage, now_micros); 135 137 // Indexer URL resolution happens lazily in `resolve_indexer_url`: 136 138 // runtime override → PDS config → compile-time default. No seeding 137 139 // needed here — the priority chain has a const fallback. ··· 363 365 }) 364 366 } 365 367 366 - /// Current ISO 8601 timestamp from the platform clock. 368 + /// Current RFC 3339 UTC timestamp (microsecond precision). 367 369 pub fn now(&self) -> String { 368 - (self.now_fn)() 370 + crate::timestamp::rfc3339_from_micros((self.now_micros_fn)()) 369 371 } 370 372 371 373 /// Generate a TID (Timestamp ID) for use as a record rkey.
+3 -8
crates/opake-core/src/opake_tests.rs
··· 9 9 use crate::storage::{Identity, NoopStorage}; 10 10 use crate::test_utils::MockTransport; 11 11 12 - fn test_now() -> String { 13 - "2026-01-01T00:00:00Z".to_string() 14 - } 15 - 16 12 fn test_now_micros() -> u64 { 17 13 1_700_000_000_000_000 18 14 } ··· 27 23 Some(identity), 28 24 OsRng, 29 25 NoopStorage, 30 - test_now, 31 26 test_now_micros, 32 27 ) 33 28 } ··· 39 34 } 40 35 41 36 #[test] 42 - fn now_calls_injected_function() { 37 + fn now_derives_rfc3339_from_injected_micros() { 43 38 let opake = make_test_opake(); 44 - assert_eq!(opake.now(), "2026-01-01T00:00:00Z"); 39 + // `test_now_micros()` returns 1_700_000_000_000_000 µs. 40 + assert_eq!(opake.now(), "2023-11-14T22:13:20.000000Z"); 45 41 } 46 42 47 43 #[test] ··· 156 152 Some(identity), 157 153 OsRng, 158 154 NoopStorage, 159 - test_now, 160 155 test_now_micros, 161 156 ) 162 157 }
+135
crates/opake-core/src/timestamp.rs
··· 1 + // RFC 3339 timestamp formatting from microseconds since Unix epoch. 2 + // 3 + // Core deliberately has no chrono dependency — `Opake` injects the platform 4 + // clock as `fn() -> u64` (microseconds) and derives the RFC 3339 string here. 5 + // One injection instead of two, and the formatting is identical between CLI 6 + // and WASM. 7 + // 8 + // Output shape: `YYYY-MM-DDTHH:MM:SS.ffffffZ` — microsecond precision, UTC, 9 + // Z suffix. Accepted by atproto record schemas alongside any other valid 10 + // ISO 8601 / RFC 3339 form. 11 + 12 + /// Format microseconds since Unix epoch as an RFC 3339 UTC timestamp with 13 + /// microsecond precision. 14 + /// 15 + /// Only handles post-1970 timestamps (the input is unsigned). Returns 16 + /// `YYYY-MM-DDTHH:MM:SS.ffffffZ`. 17 + pub fn rfc3339_from_micros(micros: u64) -> String { 18 + let seconds = micros / 1_000_000; 19 + let sub_micros = (micros % 1_000_000) as u32; 20 + 21 + let days = seconds / 86_400; 22 + let time_of_day = seconds % 86_400; 23 + let hour = time_of_day / 3_600; 24 + let minute = (time_of_day % 3_600) / 60; 25 + let second = time_of_day % 60; 26 + 27 + let (year, month, day) = civil_from_days(days); 28 + 29 + format!( 30 + "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{sub_micros:06}Z" 31 + ) 32 + } 33 + 34 + /// Convert days since 1970-01-01 to a civil (year, month, day). 35 + /// 36 + /// Howard Hinnant's algorithm — see http://howardhinnant.github.io/date_algorithms.html. 37 + /// Post-1970 only (input is `u64`, never pre-shift negative); leap years and 38 + /// month lengths are handled by the mapping. 39 + fn civil_from_days(days: u64) -> (u64, u64, u64) { 40 + let z = days + 719_468; 41 + let era = z / 146_097; 42 + let doe = z - era * 146_097; 43 + let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; 44 + let year = yoe + era * 400; 45 + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); 46 + let mp = (5 * doy + 2) / 153; 47 + let day = doy - (153 * mp + 2) / 5 + 1; 48 + let month = if mp < 10 { mp + 3 } else { mp - 9 }; 49 + let year = if month <= 2 { year + 1 } else { year }; 50 + (year, month, day) 51 + } 52 + 53 + #[cfg(test)] 54 + mod tests { 55 + use super::*; 56 + 57 + #[test] 58 + fn epoch_is_1970_01_01() { 59 + assert_eq!(rfc3339_from_micros(0), "1970-01-01T00:00:00.000000Z"); 60 + } 61 + 62 + #[test] 63 + fn one_microsecond_past_epoch() { 64 + assert_eq!(rfc3339_from_micros(1), "1970-01-01T00:00:00.000001Z"); 65 + } 66 + 67 + #[test] 68 + fn known_unix_timestamp_nov_2023() { 69 + // 1_700_000_000 s = 2023-11-14T22:13:20 UTC 70 + assert_eq!( 71 + rfc3339_from_micros(1_700_000_000_000_000), 72 + "2023-11-14T22:13:20.000000Z" 73 + ); 74 + } 75 + 76 + #[test] 77 + fn leap_day_2024() { 78 + // 2024-02-29T00:00:00 UTC = 1_709_164_800 s 79 + assert_eq!( 80 + rfc3339_from_micros(1_709_164_800_000_000), 81 + "2024-02-29T00:00:00.000000Z" 82 + ); 83 + } 84 + 85 + #[test] 86 + fn non_leap_century_2100() { 87 + // 2100 is NOT a leap year (divisible by 100 but not 400). 88 + // 2100-03-01T00:00:00 UTC = 4_107_542_400 s. 89 + // Check 2100-02-28 rolls to 03-01 without hitting a 02-29. 90 + assert_eq!( 91 + rfc3339_from_micros(4_107_542_400_000_000), 92 + "2100-03-01T00:00:00.000000Z" 93 + ); 94 + } 95 + 96 + #[test] 97 + fn leap_year_2000_was_leap() { 98 + // 2000 IS a leap year (divisible by 400). 99 + // 2000-02-29T12:34:56 UTC = 951_827_696 s. 100 + assert_eq!( 101 + rfc3339_from_micros(951_827_696_000_000), 102 + "2000-02-29T12:34:56.000000Z" 103 + ); 104 + } 105 + 106 + #[test] 107 + fn sub_second_precision_preserved() { 108 + // 1_700_000_000.123456 s 109 + assert_eq!( 110 + rfc3339_from_micros(1_700_000_000_123_456), 111 + "2023-11-14T22:13:20.123456Z" 112 + ); 113 + } 114 + 115 + #[test] 116 + fn sub_second_pads_to_six_digits() { 117 + // Single-digit microseconds should be zero-padded. 118 + assert_eq!( 119 + rfc3339_from_micros(1_700_000_000_000_007), 120 + "2023-11-14T22:13:20.000007Z" 121 + ); 122 + } 123 + 124 + #[test] 125 + fn output_is_sortable_lexicographically() { 126 + // The whole point of ISO 8601 with zero-padded fields: string order 127 + // matches chronological order. 128 + let earlier = rfc3339_from_micros(1_700_000_000_000_000); 129 + let later = rfc3339_from_micros(1_700_000_000_000_001); 130 + assert!(earlier < later); 131 + 132 + let much_later = rfc3339_from_micros(1_800_000_000_000_000); 133 + assert!(later < much_later); 134 + } 135 + }
+4 -7
crates/opake-wasm/src/lib.rs
··· 33 33 console_log::init_with_level(log::Level::Debug).ok(); 34 34 } 35 35 36 - /// ISO 8601 UTC timestamp via JS Date. 37 - #[cfg(target_arch = "wasm32")] 38 - pub(crate) fn now_iso() -> String { 39 - js_sys::Date::new_0().to_iso_string().into() 40 - } 41 - 42 - /// Microseconds since Unix epoch via JS Date.now() (milliseconds → micros). 36 + /// Microseconds since Unix epoch via JS `Date.now()` (milliseconds → micros). 37 + /// 38 + /// Single clock source for the WASM build — RFC 3339 strings are derived 39 + /// from this value inside `opake-core` (`timestamp::rfc3339_from_micros`). 43 40 #[cfg(target_arch = "wasm32")] 44 41 pub(crate) fn now_micros() -> u64 { 45 42 (js_sys::Date::now() * 1000.0) as u64
+4 -3
crates/opake-wasm/src/opake_wasm.rs
··· 641 641 #[wasm_bindgen(js_name = checkSession)] 642 642 pub async fn check_session(&self) -> Result<(), JsError> { 643 643 let mut opake = self.opake().await?; 644 + let now = opake.now(); 644 645 let config = opake.get_account_config().await.map_err(wasm_err)?; 645 - let mut record = config 646 - .unwrap_or_else(|| opake_core::records::AccountConfigRecord::new(&crate::now_iso())); 647 - record.modified_at = crate::now_iso(); 646 + let mut record = 647 + config.unwrap_or_else(|| opake_core::records::AccountConfigRecord::new(&now)); 648 + record.modified_at = now; 648 649 opake.set_account_config(&record).await.map_err(wasm_err)?; 649 650 Ok(()) 650 651 }
-1
crates/opake-wasm/src/wasm_util.rs
··· 117 117 did, 118 118 WasmTransport::new(), 119 119 OsRng, 120 - crate::now_iso, 121 120 crate::now_micros, 122 121 ) 123 122 .await