ifc_lite_wasm/progress_hook.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! JS side of `ifc_lite_geometry::progress`: lets a geometry worker post a
6//! heartbeat while ONE synchronous batch call is still working, so the host can
7//! tell a slow element from a hung call without a wall-clock guess.
8
9use std::cell::{Cell, RefCell};
10
11use js_sys::Function;
12use wasm_bindgen::prelude::*;
13
14/// Minimum spacing between two callback invocations. The host's recovery
15/// budget is tens of seconds; one heartbeat a second is ample and keeps the
16/// callback (a `postMessage`) off every hot path.
17const MIN_INTERVAL_MS: f64 = 1_000.0;
18
19// Thread-local on purpose: a JS `Function` is bound to the thread (worker)
20// that created it, so only geometry work on the thread that installed the
21// callback reports. In the unpublished `threads` (pkg-threaded) build, rayon
22// pool threads see no callback and stay silent; their parent call still ticks
23// on its own thread between parallel sections.
24thread_local! {
25 static CALLBACK: RefCell<Option<Function>> = const { RefCell::new(None) };
26 static LAST_CALL_MS: Cell<f64> = const { Cell::new(f64::NEG_INFINITY) };
27}
28
29fn on_geometry_progress() {
30 let now = js_sys::Date::now();
31 if now - LAST_CALL_MS.with(Cell::get) < MIN_INTERVAL_MS {
32 return;
33 }
34 LAST_CALL_MS.with(|last| last.set(now));
35 CALLBACK.with(|callback| {
36 if let Some(callback) = callback.borrow().as_ref() {
37 // A heartbeat must never fail the geometry it reports on. A throwing
38 // callback is logged and the work continues.
39 if let Err(error) = callback.call0(&JsValue::NULL) {
40 web_sys::console::warn_2(&JsValue::from_str("[ifc-lite] geometry progress callback threw:"), &error);
41 }
42 }
43 });
44}
45
46/// Install (or, with `undefined`, remove) the callback invoked at most once a
47/// second while geometry work is in progress inside a WASM call. It runs
48/// synchronously inside that call, so it must only do something cheap such as
49/// `postMessage`, and must not call back into this module.
50#[wasm_bindgen(js_name = setGeometryProgressCallback)]
51pub fn set_geometry_progress_callback(callback: Option<Function>) {
52 ifc_lite_geometry::progress::set_hook(on_geometry_progress);
53 LAST_CALL_MS.with(|last| last.set(f64::NEG_INFINITY));
54 CALLBACK.with(|slot| *slot.borrow_mut() = callback);
55}