leptos_use/
use_service_worker.rs

1use default_struct_builder::DefaultBuilder;
2use leptos::reactive::actions::Action;
3use leptos::reactive::wrappers::read::Signal;
4use leptos::{
5    logging::{debug_warn, warn},
6    prelude::*,
7};
8use send_wrapper::SendWrapper;
9use std::sync::Arc;
10use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
11use web_sys::ServiceWorkerRegistration;
12
13use crate::{js_fut, sendwrap_fn, use_window};
14
15/// Reactive [ServiceWorker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API).
16///
17/// Please check the [working example](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_service_worker).
18///
19/// ## Usage
20///
21/// ```
22/// # use leptos::prelude::*;
23/// # use leptos_use::{use_service_worker_with_options, UseServiceWorkerOptions, UseServiceWorkerReturn};
24/// #
25/// # #[component]
26/// # fn Demo() -> impl IntoView {
27/// let UseServiceWorkerReturn {
28///         registration,
29///         installing,
30///         waiting,
31///         active,
32///         skip_waiting,
33///         check_for_update,
34/// } = use_service_worker_with_options(UseServiceWorkerOptions::default()
35///     .script_url("service-worker.js")
36///     .skip_waiting_message("skipWaiting"),
37/// );
38///
39/// # view! { }
40/// # }
41/// ```
42///
43/// ## SendWrapped Return
44///
45/// The returned closures `check_for_update` and `skip_waiting` are sendwrapped functions. They can
46/// only be called from the same thread that called `use_service_worker`.
47///
48/// ## Server-Side Rendering
49///
50/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
51///
52/// This function does **not** support SSR. Call it inside a `create_effect`.
53pub fn use_service_worker(
54) -> UseServiceWorkerReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
55    use_service_worker_with_options(UseServiceWorkerOptions::default())
56}
57
58/// Version of [`use_service_worker`] that takes a `UseServiceWorkerOptions`. See [`use_service_worker`] for how to use.
59pub fn use_service_worker_with_options(
60    options: UseServiceWorkerOptions,
61) -> UseServiceWorkerReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
62    // Trigger the user-defined action (page-reload by default)
63    // whenever a new ServiceWorker is installed.
64    if let Some(navigator) = use_window().navigator() {
65        let on_controller_change = options.on_controller_change.clone();
66        let js_closure = Closure::wrap(Box::new(move |_event: JsValue| {
67            #[cfg(debug_assertions)]
68            let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
69
70            on_controller_change();
71        }) as Box<dyn FnMut(JsValue)>)
72        .into_js_value();
73        navigator
74            .service_worker()
75            .set_oncontrollerchange(Some(js_closure.as_ref().unchecked_ref()));
76    }
77
78    // Create async actions.
79    let create_or_update_registration = create_action_create_or_update_registration();
80    let get_registration = create_action_get_registration();
81    let update_sw = create_action_update();
82
83    // Immediately create or update the SW registration.
84    create_or_update_registration.dispatch(ServiceWorkerScriptUrl(options.script_url.to_string()));
85
86    // And parse the result into individual signals.
87    let registration: Signal<
88        Result<SendWrapper<ServiceWorkerRegistration>, ServiceWorkerRegistrationError>,
89    > = Signal::derive(move || {
90        let a = get_registration.value().get();
91        let b = create_or_update_registration.value().get();
92        // We only dispatch create_or_update_registration once.
93        // Whenever we manually re-fetched the registration, the result of that has precedence!
94        match a {
95            Some(res) => res.map_err(ServiceWorkerRegistrationError::Js),
96            None => match b {
97                Some(res) => res.map_err(ServiceWorkerRegistrationError::Js),
98                None => Err(ServiceWorkerRegistrationError::NeverQueried),
99            },
100        }
101    });
102
103    let fetch_registration = Closure::wrap(Box::new(move |_event: JsValue| {
104        get_registration.dispatch(());
105    }) as Box<dyn FnMut(JsValue)>)
106    .into_js_value();
107
108    // Handle a changing registration state.
109    // Notify to developer if SW registration or retrieval fails.
110    Effect::new(move |_| {
111        registration.with(|reg| match reg {
112            Ok(registration) => {
113                // We must be informed when an updated SW is available.
114                registration.set_onupdatefound(Some(fetch_registration.as_ref().unchecked_ref()));
115
116                // Trigger a check to see IF an updated SW is available.
117                update_sw.dispatch(registration.clone());
118
119                // If a SW is installing, we must be notified if its state changes!
120                if let Some(sw) = registration.installing() {
121                    sw.set_onstatechange(Some(fetch_registration.as_ref().unchecked_ref()));
122                }
123            }
124            Err(err) => match err {
125                ServiceWorkerRegistrationError::Js(err) => {
126                    warn!("ServiceWorker registration failed: {err:?}")
127                }
128                ServiceWorkerRegistrationError::NeverQueried => {}
129            },
130        })
131    });
132
133    UseServiceWorkerReturn {
134        registration,
135        installing: Signal::derive(move || {
136            registration.with(|reg| {
137                reg.as_ref()
138                    .map(|reg| reg.installing().is_some())
139                    .unwrap_or_default()
140            })
141        }),
142        waiting: Signal::derive(move || {
143            registration.with(|reg| {
144                reg.as_ref()
145                    .map(|reg| reg.waiting().is_some())
146                    .unwrap_or_default()
147            })
148        }),
149        active: Signal::derive(move || {
150            registration.with(|reg| {
151                reg.as_ref()
152                    .map(|reg| reg.active().is_some())
153                    .unwrap_or_default()
154            })
155        }),
156        check_for_update: sendwrap_fn!(move || {
157            registration.with(|reg| {
158                if let Ok(reg) = reg {
159                    update_sw.dispatch(reg.clone());
160                }
161            })
162        }),
163        skip_waiting: sendwrap_fn!(move || {
164            registration.with_untracked(|reg| if let Ok(reg) = reg {
165                match reg.waiting() {
166                    Some(sw) => {
167                        debug_warn!("Updating to newly installed SW...");
168                        if let Err(err) = sw.post_message(&JsValue::from_str(&options.skip_waiting_message)) {
169                            warn!("Could not send message to active SW: Error: {err:?}");
170                        }
171                    },
172                    None => {
173                        warn!("You tried to update the SW while no new SW was waiting. This is probably a bug.");
174                    },
175                }
176            });
177        }),
178    }
179}
180
181/// Options for [`use_service_worker_with_options`].
182#[derive(DefaultBuilder)]
183pub struct UseServiceWorkerOptions {
184    /// The name of your service-worker file. Must be deployed alongside your app.
185    /// The default name is 'service-worker.js'.
186    #[builder(into)]
187    script_url: String,
188
189    /// The message sent to a waiting ServiceWorker when you call the `skip_waiting` callback.
190    /// The callback is part of the return type of [`use_service_worker`]!
191    /// The default message is 'skipWaiting'.
192    #[builder(into)]
193    skip_waiting_message: String,
194
195    /// What should happen when a new service worker was activated?
196    /// The default implementation reloads the current page.
197    on_controller_change: Arc<dyn Fn()>,
198}
199
200impl Default for UseServiceWorkerOptions {
201    fn default() -> Self {
202        Self {
203            script_url: "service-worker.js".into(),
204            skip_waiting_message: "skipWaiting".into(),
205            on_controller_change: Arc::new(move || {
206                use std::ops::Deref;
207                if let Some(window) = use_window().deref() {
208                    if let Err(err) = window.location().reload() {
209                        warn!(
210                            "Detected a ServiceWorkerController change but the page reload failed! Error: {err:?}"
211                        );
212                    }
213                }
214            }),
215        }
216    }
217}
218
219/// Return type of [`use_service_worker`].
220pub struct UseServiceWorkerReturn<CheckFn, SkipFn>
221where
222    CheckFn: Fn() + Clone + Send + Sync,
223    SkipFn: Fn() + Clone + Send + Sync,
224{
225    /// The current registration state.
226    pub registration:
227        Signal<Result<SendWrapper<ServiceWorkerRegistration>, ServiceWorkerRegistrationError>>,
228
229    /// Whether a SW is currently installing.
230    pub installing: Signal<bool>,
231
232    /// Whether a SW was installed and is now awaiting activation.
233    pub waiting: Signal<bool>,
234
235    /// Whether a SW is active.
236    pub active: Signal<bool>,
237
238    /// Check for a ServiceWorker update.
239    pub check_for_update: CheckFn,
240
241    /// Call this to activate a new ("waiting") SW if one is available.
242    /// Calling this while the [`UseServiceWorkerReturn::waiting`] signal resolves to false has no effect.
243    pub skip_waiting: SkipFn,
244}
245
246struct ServiceWorkerScriptUrl(pub String);
247
248#[derive(Debug, Clone)]
249pub enum ServiceWorkerRegistrationError {
250    Js(SendWrapper<JsValue>),
251    NeverQueried,
252}
253
254/// A leptos action which asynchronously checks for ServiceWorker updates, given an existing ServiceWorkerRegistration.
255fn create_action_update() -> Action<
256    SendWrapper<ServiceWorkerRegistration>,
257    Result<SendWrapper<ServiceWorkerRegistration>, SendWrapper<JsValue>>,
258> {
259    Action::new_unsync(
260        move |registration: &SendWrapper<ServiceWorkerRegistration>| {
261            let registration = registration.clone();
262            async move {
263                match registration.update() {
264                    Ok(promise) => js_fut!(promise)
265                        .await
266                        .and_then(|ok| ok.dyn_into::<ServiceWorkerRegistration>())
267                        .map(SendWrapper::new)
268                        .map_err(SendWrapper::new),
269                    Err(err) => Err(SendWrapper::new(err)),
270                }
271            }
272        },
273    )
274}
275
276/// A leptos action which asynchronously creates or updates and than retrieves the ServiceWorkerRegistration.
277fn create_action_create_or_update_registration() -> Action<
278    ServiceWorkerScriptUrl,
279    Result<SendWrapper<ServiceWorkerRegistration>, SendWrapper<JsValue>>,
280> {
281    Action::new_unsync(move |script_url: &ServiceWorkerScriptUrl| {
282        let script_url = script_url.0.to_owned();
283        async move {
284            if let Some(navigator) = use_window().navigator() {
285                js_fut!(navigator.service_worker().register(script_url.as_str()))
286                    .await
287                    .and_then(|ok| ok.dyn_into::<ServiceWorkerRegistration>())
288                    .map(SendWrapper::new)
289                    .map_err(SendWrapper::new)
290            } else {
291                Err(SendWrapper::new(JsValue::from_str("no navigator")))
292            }
293        }
294    })
295}
296
297/// A leptos action which asynchronously fetches the current ServiceWorkerRegistration.
298fn create_action_get_registration(
299) -> Action<(), Result<SendWrapper<ServiceWorkerRegistration>, SendWrapper<JsValue>>> {
300    Action::new_unsync(move |(): &()| async move {
301        if let Some(navigator) = use_window().navigator() {
302            js_fut!(navigator.service_worker().get_registration())
303                .await
304                .and_then(|ok| ok.dyn_into::<ServiceWorkerRegistration>())
305                .map(SendWrapper::new)
306                .map_err(SendWrapper::new)
307        } else {
308            Err(SendWrapper::new(JsValue::from_str("no navigator")))
309        }
310    })
311}