Skip to main content

blockworx_opfs/
locks.rs

1//! The origin's lock manager, as a container's single-writer lock.
2//!
3//! A `lock` entry naming a process is how a container is held on a
4//! filesystem, and it cannot be how one is held in a browser: nothing there
5//! can be asked whether the tab that wrote an entry is still open, so an
6//! entry a crashed tab left behind would hold the document for good. The
7//! Web Locks API answers exactly that question and is the browser's own
8//! answer to it — a lock lives as long as the context holding it, and the
9//! browser takes it back when the tab goes.
10//!
11//! **How a lock is held.** `navigator.locks.request(name, …, callback)`
12//! grants the lock for as long as the promise the callback returns is
13//! unsettled, so [`Held`] is a promise nobody has resolved yet and
14//! [`Held::release`] is the call that resolves it. Releasing is therefore
15//! one synchronous call, which is what the container needs of it — a lock
16//! is given up as the handle holding it is dropped. On a tab close nothing
17//! is called at all and the browser releases the lock itself, which is the
18//! case a lock entry could not survive.
19//!
20//! `ifAvailable` is what makes a second claim an answer rather than a wait:
21//! a document already open in another view is refused here and read-only
22//! there, which is [one document per tab](../../docs/dioxus-web-shell-playbook.md).
23//!
24//! The bindings are reached through `js_sys::Reflect` rather than
25//! `web_sys::LockManager`, which is behind `--cfg=web_sys_unstable_apis` —
26//! a flag that would apply to every crate in the workspace's wasm build for
27//! the sake of three calls.
28
29use std::cell::RefCell;
30use std::rc::Rc;
31
32use js_sys::{Function, Object, Promise, Reflect};
33use wasm_bindgen::prelude::Closure;
34use wasm_bindgen::{JsCast as _, JsValue};
35
36use crate::fault::{faulted, settled};
37
38/// A lock held for as long as this value lives.
39pub struct Held {
40    /// The resolver of the promise the lock manager is waiting on.
41    settle: Function,
42    /// The callback the manager called, kept alive because the promise it
43    /// returned is still in the manager's hands.
44    _callback: Closure<dyn FnMut(JsValue) -> JsValue>,
45}
46
47impl Held {
48    /// Let the lock go.
49    pub fn release(&self) {
50        let _ = self.settle.call0(&JsValue::UNDEFINED);
51    }
52}
53
54impl Drop for Held {
55    fn drop(&mut self) {
56        self.release();
57    }
58}
59
60/// Ask for `name` without waiting for whoever has it: `None` is another
61/// view of the same document holding it.
62///
63/// # Errors
64/// A page with no `navigator`, a browser with no lock manager, or the
65/// manager's own refusal.
66pub async fn request(name: &str) -> std::io::Result<Option<Held>> {
67    let manager = manager()?;
68    let asking = Reflect::get(&manager, &JsValue::from_str("request"))
69        .map_err(faulted)?
70        .dyn_into::<Function>()
71        .map_err(|_| unheld("this browser's lock manager cannot be asked for a lock"))?;
72
73    let settle: Rc<RefCell<Option<Function>>> = Rc::default();
74    let announce: Rc<RefCell<Option<Function>>> = Rc::default();
75    let granted = {
76        let announce = Rc::clone(&announce);
77        Promise::new(&mut move |resolve, _| {
78            *announce.borrow_mut() = Some(resolve);
79        })
80    };
81
82    let callback = {
83        let settle = Rc::clone(&settle);
84        Closure::wrap(Box::new(move |lock: JsValue| -> JsValue {
85            let taken = !lock.is_null() && !lock.is_undefined();
86            if let Some(announce) = announce.borrow().as_ref() {
87                let _ = announce.call1(&JsValue::UNDEFINED, &JsValue::from_bool(taken));
88            }
89            if !taken {
90                return JsValue::UNDEFINED;
91            }
92            let settle = Rc::clone(&settle);
93            Promise::new(&mut move |resolve, _| {
94                *settle.borrow_mut() = Some(resolve);
95            })
96            .into()
97        }) as Box<dyn FnMut(JsValue) -> JsValue>)
98    };
99
100    let options = Object::new();
101    Reflect::set(
102        &options,
103        &JsValue::from_str("ifAvailable"),
104        &JsValue::from_bool(true),
105    )
106    .map_err(faulted)?;
107    // The promise this answers settles when the lock is released, which is
108    // not something anything here waits for; what the claim waits for is
109    // the callback, which `granted` is resolved from inside.
110    asking
111        .call3(
112            &manager,
113            &JsValue::from_str(name),
114            &options,
115            callback.as_ref().unchecked_ref(),
116        )
117        .map_err(faulted)?;
118
119    if !settled(granted).await?.is_truthy() {
120        return Ok(None);
121    }
122    let settle = settle
123        .borrow()
124        .clone()
125        .ok_or_else(|| unheld("the lock manager granted a lock with nothing to release it by"))?;
126    Ok(Some(Held {
127        settle,
128        _callback: callback,
129    }))
130}
131
132fn manager() -> std::io::Result<JsValue> {
133    let navigator = web_sys::window()
134        .ok_or_else(|| unheld("a lock is asked for from a page, and there is none"))?
135        .navigator();
136    let manager = Reflect::get(&navigator, &JsValue::from_str("locks")).map_err(faulted)?;
137    if manager.is_undefined() || manager.is_null() {
138        return Err(unheld("this browser has no lock manager"));
139    }
140    Ok(manager)
141}
142
143fn unheld(why: &str) -> std::io::Error {
144    std::io::Error::other(why.to_owned())
145}