Web: use the browser clock everywhere on wasm (insert weekday, PDF dates, layout timing, UI timers) — follow-up to #214 (#230)

#214 gave now_iso the browser's clock on wasm; the other clock helpers were
still stubs there: Insert Date's weekday was always Thursday, the PDF
creation date was left out, and the layout and UI millisecond clocks
returned 0, so browser AutoSave's timer never fired, the caret didn't blink
and status messages never cleared.

The engine's clock is now one now_unix() (std on native, js_sys::Date in the
browser, as #214 does) that now_iso and the weekday share; the pdf, layout
and ui-egui clocks read js_sys::Date the same way on wasm. Refs #188.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dmitrii Khlapov
2026-10-10 12:20:56 -04:00
committed by GitHub
co-authored by Dmitrii Khlapov Claude Opus 5.5
parent 4c4dc14a54
commit 302528d67e
10 changed files with 62 additions and 31 deletions
Generated
+3
View File
@@ -4994,6 +4994,7 @@ dependencies = [
name = "wordcraft-layout"
version = "0.4.0"
dependencies = [
"js-sys",
"log",
"serde",
"serde_json",
@@ -5026,6 +5027,7 @@ dependencies = [
"hayro-interpret",
"hayro-syntax",
"image",
"js-sys",
"krilla",
"kurbo",
"log",
@@ -5065,6 +5067,7 @@ dependencies = [
"egui",
"egui_kittest",
"image",
"js-sys",
"log",
"serde",
"serde_json",
+2 -14
View File
@@ -449,20 +449,8 @@ pub fn format_date(fmt: &str) -> String {
const MONTHS: [&str; 12] =
["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const DAYS: [&str; 7] = ["Thursday", "Friday", "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday"];
let days_since = {
// Days since epoch for weekday.
let secs = {
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
#[cfg(target_arch = "wasm32")]
{
0u64
}
};
(secs / 86_400) as usize
};
// Days since the epoch, for the weekday.
let days_since = (super::now_unix() / 86_400) as usize;
let month = MONTHS.get(m.saturating_sub(1)).copied().unwrap_or("January");
let day = DAYS.get(days_since % 7).copied().unwrap_or("Monday");
let mut out = String::new();
+10 -6
View File
@@ -254,28 +254,32 @@ pub fn split_para(s: &mut Session, at: &Pos) -> Result<Pos, CmdError> {
}
/// ISO-8601 timestamp (UTC, seconds) for `secs` since the Unix epoch.
fn iso_from_unix_secs(secs: u64) -> String {
pub fn iso_from_unix_secs(secs: u64) -> String {
let (y, m, d) = civil_from_days((secs / 86_400) as i64);
let t = secs % 86_400;
format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", t / 3600, t / 60 % 60, t % 60)
}
/// ISO-8601 timestamp (UTC, seconds).
pub fn now_iso() -> String {
/// Seconds since the Unix epoch (UTC): the system clock, or the browser's on the web.
pub fn now_unix() -> u64 {
#[cfg(not(target_arch = "wasm32"))]
{
let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
iso_from_unix_secs(secs)
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
// `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock.
#[cfg(target_arch = "wasm32")]
{
let ms = js_sys::Date::now();
// Clamp a NaN or pre-epoch clock to 0 rather than fail.
iso_from_unix_secs(if ms.is_finite() && ms > 0.0 { (ms / 1000.0) as u64 } else { 0 })
if ms.is_finite() && ms > 0.0 { (ms / 1000.0) as u64 } else { 0 }
}
}
/// ISO-8601 timestamp (UTC, seconds).
pub fn now_iso() -> String {
iso_from_unix_secs(now_unix())
}
/// Days since 1970-01-01 → (year, month, day) (Howard Hinnant's algorithm).
pub fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
+14
View File
@@ -1638,3 +1638,17 @@ fn styles_apply_rejected_on_protected_document() {
assert_eq!(para_style(&s, 0), None);
assert_eq!(s.undo_labels(), undo);
}
/// Comments, revisions and dates read the real clock (#188: the web stamped 2026-01-01).
#[test]
fn timestamps_come_from_the_clock() {
let before = cmd::now_unix();
assert!(before > 1_760_000_000, "{before}");
let mut s = s();
run(&mut s, "text.insert", json!({"text": "Dated"}));
run(&mut s, "select.text", json!({"text": "Dated"}));
run(&mut s, "review.newComment", json!({"text": "When?"}));
let date = run(&mut s, "review.comments", json!({}))[0]["date"].as_str().unwrap_or_default().to_string();
// Fixed-width ISO strings sort by time.
assert!(date >= cmd::iso_from_unix_secs(before) && date <= cmd::iso_from_unix_secs(cmd::now_unix()), "{date}");
}
+4
View File
@@ -19,3 +19,7 @@ unicode-segmentation = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
# The browser clock (`SystemTime::now()`/`Instant::now()` panic on wasm32).
js-sys = "0.3"
+4 -2
View File
@@ -1617,16 +1617,18 @@ pub fn is_floating(o: &InlineObject) -> bool {
}
}
/// Milliseconds since an arbitrary epoch (monotonic on native; 0 on wasm).
/// Milliseconds since an arbitrary epoch (monotonic on native; the browser's clock on the web).
pub fn now_ms() -> f64 {
#[cfg(not(target_arch = "wasm32"))]
{
static T0: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
T0.get_or_init(std::time::Instant::now).elapsed().as_secs_f64() * 1000.0
}
// `Instant::now()` panics on wasm32-unknown-unknown, so ask the browser's clock.
#[cfg(target_arch = "wasm32")]
{
0.0
let ms = js_sys::Date::now();
if ms.is_finite() { ms } else { 0.0 }
}
}
+4
View File
@@ -19,6 +19,10 @@ image = { workspace = true }
thiserror = { workspace = true }
log = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
# The browser clock (`SystemTime::now()` panics on wasm32).
js-sys = "0.3"
[dev-dependencies]
hayro-syntax = "0.7.2"
hayro-interpret = { version = "0.7.0", default-features = false, features = ["embed-fonts"] }
+13 -8
View File
@@ -259,15 +259,20 @@ fn parse_iso(s: &str) -> Option<Civil> {
Some((y, mo as u8, d as u8, h as u8, mi as u8, sec as u8))
}
#[cfg(not(target_arch = "wasm32"))]
/// The system clock, or the browser's on the web.
fn now_unix() -> Option<i64> {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs() as i64)
}
#[cfg(target_arch = "wasm32")]
fn now_unix() -> Option<i64> {
None
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
i64::try_from(secs).ok()
}
// `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock.
#[cfg(target_arch = "wasm32")]
{
let ms = js_sys::Date::now();
(ms.is_finite() && ms > 0.0).then(|| (ms / 1000.0) as i64)
}
}
/// Unix seconds (UTC) → civil time (proleptic Gregorian).
+4
View File
@@ -23,6 +23,10 @@ log = { workspace = true }
# The system's preferred interface languages (#8). Native: OS APIs; wasm: none (English).
sys-locale = "0.3"
[target.'cfg(target_arch = "wasm32")'.dependencies]
# The browser clock (`SystemTime::now()`/`Instant::now()` panic on wasm32).
js-sys = "0.3"
[dev-dependencies]
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
image = { workspace = true }
+4 -1
View File
@@ -890,15 +890,18 @@ fn discards_document(id: &str, params: &Value) -> bool {
}
}
/// Wall-clock milliseconds since the Unix epoch: the system clock, or the browser's on the web.
pub fn now_ms() -> f64 {
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0)
}
// `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock.
#[cfg(target_arch = "wasm32")]
{
0.0
let ms = js_sys::Date::now();
if ms.is_finite() && ms > 0.0 { ms } else { 0.0 }
}
}