leptos_use/
use_service_worker.rs1use 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
15pub 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
56pub fn use_service_worker_with_options(
58 options: UseServiceWorkerOptions,
59) -> UseServiceWorkerReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
60 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 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 create_or_update_registration.dispatch(ServiceWorkerScriptUrl(options.script_url.to_string()));
83
84 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 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 Effect::new(move |_| {
109 registration.with(|reg| match reg {
110 Ok(registration) => {
111 registration.set_onupdatefound(Some(fetch_registration.as_ref().unchecked_ref()));
113
114 update_sw.dispatch(registration.clone());
116
117 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#[derive(DefaultBuilder)]
181pub struct UseServiceWorkerOptions {
182 #[builder(into)]
185 script_url: String,
186
187 #[builder(into)]
191 skip_waiting_message: String,
192
193 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
217pub struct UseServiceWorkerReturn<CheckFn, SkipFn>
219where
220 CheckFn: Fn() + Clone + Send + Sync,
221 SkipFn: Fn() + Clone + Send + Sync,
222{
223 pub registration:
225 Signal<Result<SendWrapper<ServiceWorkerRegistration>, ServiceWorkerRegistrationError>>,
226
227 pub installing: Signal<bool>,
229
230 pub waiting: Signal<bool>,
232
233 pub active: Signal<bool>,
235
236 pub check_for_update: CheckFn,
238
239 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
252fn 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
274fn 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
295fn 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}