leptos_use/
use_web_lock.rs

1use default_struct_builder::DefaultBuilder;
2use std::future::Future;
3use thiserror::Error;
4use wasm_bindgen::JsValue;
5pub use web_sys::LockMode;
6
7/// Rustified [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API).
8///
9/// The **Web Locks API** allows scripts running in one tab or worker to asynchronously acquire a
10/// lock, hold it while work is performed, then release it. While held, no other script executing
11/// in the same origin can acquire the same lock, which allows a web app running in multiple tabs or
12/// workers to coordinate work and the use of resources.
13///
14/// > This function requires `--cfg=web_sys_unstable_apis` to be activated as
15/// > [described in the wasm-bindgen guide](https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html).
16///
17/// ## Demo
18///
19/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_web_lock)
20///
21/// ## Usage
22///
23/// ```
24/// # use leptos::prelude::*;
25/// # use leptos_use::use_web_lock;
26/// #
27/// async fn my_process(_lock: web_sys::Lock) -> i32 {
28///     // do sth
29///     42
30/// }
31///
32/// # #[component]
33/// # fn Demo() -> impl IntoView {
34/// leptos::task::spawn_local(async {
35///     let res = use_web_lock("my_lock", my_process).await;
36///     assert!(matches!(res, Ok(42)));
37/// });
38/// #
39/// # view! { }
40/// # }
41/// ```
42///
43/// ## Server-Side Rendering
44///
45/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
46///
47/// On the server this returns `Err(UseWebLockError::Server)` and the task is not executed.
48// #[doc(cfg(feature = "use_web_lock"))]
49pub async fn use_web_lock<C, F, R>(name: &str, callback: C) -> Result<R, UseWebLockError>
50where
51    C: FnOnce(web_sys::Lock) -> F + 'static,
52    F: Future<Output = R>,
53    R: 'static,
54{
55    use_web_lock_with_options(name, callback, UseWebLockOptions::default()).await
56}
57
58/// Version of [`fn@crate::use_web_lock`] that takes a `UseWebLockOptions`. See [`fn@crate::use_web_lock`] for how to use.
59// #[doc(cfg(feature = "use_web_lock"))]
60pub async fn use_web_lock_with_options<C, F, R>(
61    name: &str,
62    callback: C,
63    options: UseWebLockOptions,
64) -> Result<R, UseWebLockError>
65where
66    C: FnOnce(web_sys::Lock) -> F + 'static,
67    F: Future<Output = R>,
68    R: 'static,
69{
70    #[cfg(feature = "ssr")]
71    {
72        let _ = name;
73        let _ = callback;
74        let _ = options;
75
76        Err(UseWebLockError::Server)
77    }
78
79    #[cfg(not(feature = "ssr"))]
80    {
81        use crate::js_fut;
82        use leptos::prelude::window;
83        use std::sync::{Arc, Mutex};
84        use wasm_bindgen::closure::Closure;
85        use wasm_bindgen::JsCast;
86        use wasm_bindgen_futures::future_to_promise;
87
88        let ret_value = Arc::new(Mutex::new(None));
89
90        let handler = Closure::once(Box::new({
91            let ret_value = Arc::clone(&ret_value);
92
93            move |lock| {
94                future_to_promise(async move {
95                    let ret = callback(lock).await;
96                    ret_value.lock().expect("Lock failed").replace(ret);
97                    Ok(JsValue::null())
98                })
99            }
100        }) as Box<dyn FnOnce(web_sys::Lock) -> _>)
101        .into_js_value();
102
103        let lock_promise = window()
104            .navigator()
105            .locks()
106            .request_with_options_and_callback(
107                name,
108                &options.to_web_sys(),
109                handler.unchecked_ref(),
110            );
111
112        js_fut!(lock_promise)
113            .await
114            .map(move |_| {
115                Arc::into_inner(ret_value)
116                    .expect("Arc has more than one reference still")
117                    .into_inner()
118                    .expect("Lock failed")
119                    .expect("Return value was None")
120            })
121            .map_err(UseWebLockError::Failed)
122    }
123}
124
125#[derive(Error, Debug)]
126pub enum UseWebLockError {
127    #[error("Lock cannot be acquired on the server")]
128    Server,
129
130    #[error("Lock failed")]
131    Failed(JsValue),
132}
133
134/// Options for [`fn@crate::use_web_lock_with_options`].
135// #[doc(cfg(feature = "use_web_lock"))]
136#[allow(dead_code)]
137#[derive(DefaultBuilder)]
138pub struct UseWebLockOptions {
139    /// The default mode is `LockMode::Exclusive`, but `LockMode::Shared` can be specified.
140    /// There can be only one `Exclusive` holder of a lock, but multiple `Shared` requests can be
141    /// granted at the same time. This can be used to implement the
142    /// [readers-writer pattern](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock).
143    mode: LockMode,
144
145    /// If `true`, the lock request will fail if the lock cannot be granted immediately without
146    /// waiting. The callback is invoked with `null`. Defaults to `false`.
147    if_available: bool,
148
149    /// If `true`, then any held locks with the same name will be released, and the request will
150    /// be granted, preempting any queued requests for it. Defaults to `false`.
151    steal: bool,
152    // TODO : add abort signal (this also requires to create a wrapper for AbortSignal similar to UseWindow)
153}
154
155#[cfg(not(feature = "ssr"))]
156impl UseWebLockOptions {
157    fn to_web_sys(&self) -> web_sys::LockOptions {
158        let options = web_sys::LockOptions::new();
159        options.set_mode(self.mode);
160        options.set_if_available(self.if_available);
161        options.set_steal(self.steal);
162
163        options
164    }
165}
166
167impl Default for UseWebLockOptions {
168    fn default() -> Self {
169        Self {
170            mode: LockMode::Exclusive,
171            if_available: false,
172            steal: false,
173        }
174    }
175}