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