leptos_use/
use_web_lock.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use default_struct_builder::DefaultBuilder;
use std::future::Future;
use thiserror::Error;
use wasm_bindgen::JsValue;
pub use web_sys::LockMode;

/// Rustified [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API).   
///
/// The **Web Locks API** allows scripts running in one tab or worker to asynchronously acquire a
/// lock, hold it while work is performed, then release it. While held, no other script executing
/// in the same origin can acquire the same lock, which allows a web app running in multiple tabs or
/// workers to coordinate work and the use of resources.
///
/// > This function requires `--cfg=web_sys_unstable_apis` to be activated as
/// > [described in the wasm-bindgen guide](https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html).
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_web_lock)
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::use_web_lock;
/// #
/// async fn my_process(_lock: web_sys::Lock) -> i32 {
///     // do sth
///     42
/// }
///
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// spawn_local(async {
///     let res = use_web_lock("my_lock", my_process).await;
///     assert!(matches!(res, Ok(42)));
/// });
/// #
/// # view! { }
/// # }
/// ```
///
/// ## Server-Side Rendering
///
/// On the server this returns `Err(UseWebLockError::Server)` and the task is not executed.
// #[doc(cfg(feature = "use_web_lock"))]
pub async fn use_web_lock<C, F, R>(name: &str, callback: C) -> Result<R, UseWebLockError>
where
    C: FnOnce(web_sys::Lock) -> F + 'static,
    F: Future<Output = R>,
    R: 'static,
{
    use_web_lock_with_options(name, callback, UseWebLockOptions::default()).await
}

/// Version of [`fn@crate::use_web_lock`] that takes a `UseWebLockOptions`. See [`fn@crate::use_web_lock`] for how to use.
// #[doc(cfg(feature = "use_web_lock"))]
pub async fn use_web_lock_with_options<C, F, R>(
    name: &str,
    callback: C,
    options: UseWebLockOptions,
) -> Result<R, UseWebLockError>
where
    C: FnOnce(web_sys::Lock) -> F + 'static,
    F: Future<Output = R>,
    R: 'static,
{
    #[cfg(feature = "ssr")]
    {
        let _ = name;
        let _ = callback;
        let _ = options;

        Err(UseWebLockError::Server)
    }

    #[cfg(not(feature = "ssr"))]
    {
        use crate::js_fut;
        use leptos::window;
        use std::sync::{Arc, Mutex};
        use wasm_bindgen::closure::Closure;
        use wasm_bindgen::JsCast;
        use wasm_bindgen_futures::future_to_promise;

        let ret_value = Arc::new(Mutex::new(None));

        let handler = Closure::once(Box::new({
            let ret_value = Arc::clone(&ret_value);

            move |lock| {
                future_to_promise(async move {
                    let ret = callback(lock).await;
                    ret_value.lock().expect("Lock failed").replace(ret);
                    Ok(JsValue::null())
                })
            }
        }) as Box<dyn FnOnce(web_sys::Lock) -> _>)
        .into_js_value();

        let lock_promise = window()
            .navigator()
            .locks()
            .request_with_options_and_callback(
                name,
                &options.to_web_sys(),
                handler.unchecked_ref(),
            );

        js_fut!(lock_promise)
            .await
            .map(move |_| {
                Arc::into_inner(ret_value)
                    .expect("Arc has more than one reference still")
                    .into_inner()
                    .expect("Lock failed")
                    .expect("Return value was None")
            })
            .map_err(UseWebLockError::Failed)
    }
}

#[derive(Error, Debug)]
pub enum UseWebLockError {
    #[error("Lock cannot be acquired on the server")]
    Server,

    #[error("Lock failed")]
    Failed(JsValue),
}

/// Options for [`fn@crate::use_web_lock_with_options`].
// #[doc(cfg(feature = "use_web_lock"))]
#[allow(dead_code)]
#[derive(DefaultBuilder)]
pub struct UseWebLockOptions {
    /// The default mode is `LockMode::Exclusive`, but `LockMode::Shared` can be specified.
    /// There can be only one `Exclusive` holder of a lock, but multiple `Shared` requests can be
    /// granted at the same time. This can be used to implement the
    /// [readers-writer pattern](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock).
    mode: LockMode,

    /// If `true`, the lock request will fail if the lock cannot be granted immediately without
    /// waiting. The callback is invoked with `null`. Defaults to `false`.
    if_available: bool,

    /// If `true`, then any held locks with the same name will be released, and the request will
    /// be granted, preempting any queued requests for it. Defaults to `false`.
    steal: bool,
    // TODO : add abort signal (this also requires to create a wrapper for AbortSignal similar to UseWindow)
}

#[cfg(not(feature = "ssr"))]
impl UseWebLockOptions {
    fn to_web_sys(&self) -> web_sys::LockOptions {
        let options = web_sys::LockOptions::new();
        options.set_mode(self.mode);
        options.set_if_available(self.if_available);
        options.set_steal(self.steal);

        options
    }
}

impl Default for UseWebLockOptions {
    fn default() -> Self {
        Self {
            mode: LockMode::Exclusive,
            if_available: false,
            steal: false,
        }
    }
}