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(
54) -> UseServiceWorkerReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
55 use_service_worker_with_options(UseServiceWorkerOptions::default())
56}
57
58pub fn use_service_worker_with_options(
60 options: UseServiceWorkerOptions,
61) -> UseServiceWorkerReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
62 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 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 create_or_update_registration.dispatch(ServiceWorkerScriptUrl(options.script_url.to_string()));
85
86 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 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 Effect::new(move |_| {
111 registration.with(|reg| match reg {
112 Ok(registration) => {
113 registration.set_onupdatefound(Some(fetch_registration.as_ref().unchecked_ref()));
115
116 update_sw.dispatch(registration.clone());
118
119 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#[derive(DefaultBuilder)]
183pub struct UseServiceWorkerOptions {
184 #[builder(into)]
187 script_url: String,
188
189 #[builder(into)]
193 skip_waiting_message: String,
194
195 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
219pub struct UseServiceWorkerReturn<CheckFn, SkipFn>
221where
222 CheckFn: Fn() + Clone + Send + Sync,
223 SkipFn: Fn() + Clone + Send + Sync,
224{
225 pub registration:
227 Signal<Result<SendWrapper<ServiceWorkerRegistration>, ServiceWorkerRegistrationError>>,
228
229 pub installing: Signal<bool>,
231
232 pub waiting: Signal<bool>,
234
235 pub active: Signal<bool>,
237
238 pub check_for_update: CheckFn,
240
241 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
254fn 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
276fn 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
297fn 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}