use default_struct_builder::DefaultBuilder; use leptos::*; use std::rc::Rc; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; use web_sys::ServiceWorkerRegistration; use crate::{js_fut, use_window}; /// Reactive [ServiceWorker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). /// /// Please check the [working example](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_service_worker). /// /// ## Usage /// /// ``` /// # use leptos::*; /// # use leptos_use::{use_service_worker_with_options, UseServiceWorkerOptions, UseServiceWorkerReturn}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { /// let UseServiceWorkerReturn { /// registration, /// installing, /// waiting, /// active, /// skip_waiting, /// check_for_update, /// } = use_service_worker_with_options(UseServiceWorkerOptions::default() /// .script_url("service-worker.js") /// .skip_waiting_message("skipWaiting"), /// ); /// /// # view! { } /// # } /// ``` /// /// ## Server-Side Rendering /// /// This function does **not** support SSR. Call it inside a `create_effect`. pub fn use_service_worker() -> UseServiceWorkerReturn { use_service_worker_with_options(UseServiceWorkerOptions::default()) } /// Version of [`use_service_worker`] that takes a `UseServiceWorkerOptions`. See [`use_service_worker`] for how to use. pub fn use_service_worker_with_options( options: UseServiceWorkerOptions, ) -> UseServiceWorkerReturn { // Trigger the user-defined action (page-reload by default) // whenever a new ServiceWorker is installed. if let Some(navigator) = use_window().navigator() { let on_controller_change = options.on_controller_change.clone(); let js_closure = Closure::wrap(Box::new(move |_event: JsValue| { #[cfg(debug_assertions)] let prev = SpecialNonReactiveZone::enter(); on_controller_change(); #[cfg(debug_assertions)] SpecialNonReactiveZone::exit(prev); }) as Box) .into_js_value(); navigator .service_worker() .set_oncontrollerchange(Some(js_closure.as_ref().unchecked_ref())); } // Create async actions. let create_or_update_registration = create_action_create_or_update_registration(); let get_registration = create_action_get_registration(); let update_sw = create_action_update(); // Immediately create or update the SW registration. create_or_update_registration.dispatch(ServiceWorkerScriptUrl(options.script_url.to_string())); // And parse the result into individual signals. let registration: Signal> = Signal::derive(move || { let a = get_registration.value().get(); let b = create_or_update_registration.value().get(); // We only dispatch create_or_update_registration once. // Whenever we manually re-fetched the registration, the result of that has precedence! match a { Some(res) => res.map_err(ServiceWorkerRegistrationError::Js), None => match b { Some(res) => res.map_err(ServiceWorkerRegistrationError::Js), None => Err(ServiceWorkerRegistrationError::NeverQueried), }, } }); let fetch_registration = Closure::wrap(Box::new(move |_event: JsValue| { get_registration.dispatch(()); }) as Box) .into_js_value(); // Handle a changing registration state. // Notify to developer if SW registration or retrieval fails. create_effect(move |_| { registration.with(|reg| match reg { Ok(registration) => { // We must be informed when an updated SW is available. registration.set_onupdatefound(Some(fetch_registration.as_ref().unchecked_ref())); // Trigger a check to see IF an updated SW is available. update_sw.dispatch(registration.clone()); // If a SW is installing, we must be notified if its state changes! if let Some(sw) = registration.installing() { sw.set_onstatechange(Some(fetch_registration.as_ref().unchecked_ref())); } } Err(err) => match err { ServiceWorkerRegistrationError::Js(err) => { logging::warn!("ServiceWorker registration failed: {err:?}") } ServiceWorkerRegistrationError::NeverQueried => {} }, }) }); UseServiceWorkerReturn { registration, installing: Signal::derive(move || { registration.with(|reg| { reg.as_ref() .map(|reg| reg.installing().is_some()) .unwrap_or_default() }) }), waiting: Signal::derive(move || { registration.with(|reg| { reg.as_ref() .map(|reg| reg.waiting().is_some()) .unwrap_or_default() }) }), active: Signal::derive(move || { registration.with(|reg| { reg.as_ref() .map(|reg| reg.active().is_some()) .unwrap_or_default() }) }), check_for_update: move || { registration.with(|reg| { if let Ok(reg) = reg { update_sw.dispatch(reg.clone()) } }) }, skip_waiting: move || { registration.with_untracked(|reg| if let Ok(reg) = reg { match reg.waiting() { Some(sw) => { logging::debug_warn!("Updating to newly installed SW..."); if let Err(err) = sw.post_message(&JsValue::from_str(&options.skip_waiting_message)) { logging::warn!("Could not send message to active SW: Error: {err:?}"); } }, None => { logging::warn!("You tried to update the SW while no new SW was waiting. This is probably a bug."); }, } }); }, } } /// Options for [`use_service_worker_with_options`]. #[derive(DefaultBuilder)] pub struct UseServiceWorkerOptions { /// The name of your service-worker file. Must be deployed alongside your app. /// The default name is 'service-worker.js'. #[builder(into)] script_url: String, /// The message sent to a waiting ServiceWorker when you call the `skip_waiting` callback. /// The callback is part of the return type of [`use_service_worker`]! /// The default message is 'skipWaiting'. #[builder(into)] skip_waiting_message: String, /// What should happen when a new service worker was activated? /// The default implementation reloads the current page. on_controller_change: Rc, } impl Default for UseServiceWorkerOptions { fn default() -> Self { Self { script_url: "service-worker.js".into(), skip_waiting_message: "skipWaiting".into(), on_controller_change: Rc::new(move || { use std::ops::Deref; if let Some(window) = use_window().deref() { if let Err(err) = window.location().reload() { logging::warn!( "Detected a ServiceWorkerController change but the page reload failed! Error: {err:?}" ); } } }), } } } /// Return type of [`use_service_worker`]. pub struct UseServiceWorkerReturn where CheckFn: Fn() + Clone, SkipFn: Fn() + Clone, { /// The current registration state. pub registration: Signal>, /// Whether a SW is currently installing. pub installing: Signal, /// Whether a SW was installed and is now awaiting activation. pub waiting: Signal, /// Whether a SW is active. pub active: Signal, /// Check for a ServiceWorker update. pub check_for_update: CheckFn, /// Call this to activate a new ("waiting") SW if one is available. /// Calling this while the [`UseServiceWorkerReturn::waiting`] signal resolves to false has no effect. pub skip_waiting: SkipFn, } struct ServiceWorkerScriptUrl(pub String); #[derive(Debug, Clone)] pub enum ServiceWorkerRegistrationError { Js(JsValue), NeverQueried, } /// A leptos action which asynchronously checks for ServiceWorker updates, given an existing ServiceWorkerRegistration. fn create_action_update( ) -> Action> { create_action(move |registration: &ServiceWorkerRegistration| { let registration = registration.clone(); async move { match registration.update() { Ok(promise) => js_fut!(promise) .await .and_then(|ok| ok.dyn_into::()), Err(err) => Err(err), } } }) } /// A leptos action which asynchronously creates or updates and than retrieves the ServiceWorkerRegistration. fn create_action_create_or_update_registration( ) -> Action> { create_action(move |script_url: &ServiceWorkerScriptUrl| { let script_url = script_url.0.to_owned(); async move { if let Some(navigator) = use_window().navigator() { js_fut!(navigator.service_worker().register(script_url.as_str())) .await .and_then(|ok| ok.dyn_into::()) } else { Err(JsValue::from_str("no navigator")) } } }) } /// A leptos action which asynchronously fetches the current ServiceWorkerRegistration. fn create_action_get_registration() -> Action<(), Result> { create_action(move |(): &()| async move { if let Some(navigator) = use_window().navigator() { js_fut!(navigator.service_worker().get_registration()) .await .and_then(|ok| ok.dyn_into::()) } else { Err(JsValue::from_str("no navigator")) } }) }