From 5fc85ee03404925ed522fc04867b43ed5538dc4f Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:45:45 +0200 Subject: [PATCH] Fix #226: a refused Web Locks request no longer leaves the web app loading --- apps/lightcraft-web/src/safety.rs | 26 +++++++++++++++++++++----- docs/web.md | 4 ++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/lightcraft-web/src/safety.rs b/apps/lightcraft-web/src/safety.rs index c0dac59c2..b3aa504d1 100644 --- a/apps/lightcraft-web/src/safety.rs +++ b/apps/lightcraft-web/src/safety.rs @@ -93,25 +93,41 @@ pub fn install_panic_hook() { const TAB_LOCK: &str = "lightcraft-library"; /// Hold the library lock for as long as this page lives (Web Locks API). `Some(true)`: ours; -/// `Some(false)`: another tab of this site has it; `None`: the browser can't tell (no Web Locks). +/// `Some(false)`: another tab of this site has it; `None`: the browser can't tell (no Web Locks, or +/// it refused the request). pub async fn acquire_tab_lock() -> Option { let nav = Reflect::get(&js_sys::global(), &"navigator".into()).ok()?; let locks = Reflect::get(&nav, &"locks".into()).ok().filter(|l| !l.is_undefined() && !l.is_null())?; let request: js_sys::Function = Reflect::get(&locks, &"request".into()).ok()?.dyn_into().ok()?; let opts = js_sys::Object::new(); Reflect::set(&opts, &"ifAvailable".into(), &true.into()).ok()?; - let granted = js_sys::Promise::new(&mut |resolve, _reject| { + let granted = js_sys::Promise::new(&mut |resolve, reject| { let cb = Closure::once_into_js(move |lock: JsValue| -> JsValue { let ours = !lock.is_null() && !lock.is_undefined(); let _ = resolve.call1(&JsValue::NULL, &ours.into()); // keep the lock until the page goes away: a promise that never settles if ours { js_sys::Promise::new(&mut |_, _| {}).into() } else { JsValue::NULL } }); - if let Err(e) = request.call3(&locks, &TAB_LOCK.into(), &opts, &cb) { - log::warn!("Web Locks request failed: {e:?}"); + // a refused request never calls `cb`: settle with its error instead of waiting forever + match request.call3(&locks, &TAB_LOCK.into(), &opts, &cb) { + Ok(pending) => { + let catch = Reflect::get(&pending, &"catch".into()).ok().and_then(|c| c.dyn_into::().ok()); + if let Some(catch) = catch { + let _ = catch.call1(&pending, &reject); + } + } + Err(e) => { + let _ = reject.call1(&JsValue::NULL, &e); + } } }); - JsFuture::from(granted).await.ok().map(|v| v.is_truthy()) + match JsFuture::from(granted).await { + Ok(v) => Some(v.is_truthy()), + Err(e) => { + log::warn!("Web Locks request failed, starting without the one-tab guard: {e:?}"); + None + } + } } /// Offer a blob as a download named `name`. diff --git a/docs/web.md b/docs/web.md index 6d3283c8f..47a4087c0 100644 --- a/docs/web.md +++ b/docs/web.md @@ -122,8 +122,8 @@ are the same in every version, so serve them with `Cache-Control: no-cache` (not a reload); a notice says why. - One tab at a time: the page holds a Web Lock (`navigator.locks`, `lightcraft-library`); a second tab or window shows "LightCraft is already open in another tab" instead of loading - its own copy (two copies would overwrite each other's saves). Browsers without Web Locks - aren't protected. + its own copy (two copies would overwrite each other's saves). Browsers without Web Locks, or + that refuse the lock request, aren't protected: the app starts without the guard. - If the stored library can't be opened, a notice says the session is temporary and nothing is saved; the stored library is left as it was. - A panic (wasm is built with `panic = "abort"`) replaces the page with a message saying the