firebase_rs_sdk/messaging/
sw_manager.rs1use crate::messaging::error::{unsupported_browser, MessagingResult};
2
3#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
4mod wasm {
5 use std::rc::Rc;
6
7 use wasm_bindgen::closure::Closure;
8 use wasm_bindgen::JsCast;
9 use wasm_bindgen::JsValue;
10 use wasm_bindgen_futures::JsFuture;
11
12 use crate::messaging::constants::{
13 DEFAULT_REGISTRATION_TIMEOUT_MS, DEFAULT_SW_PATH, DEFAULT_SW_SCOPE,
14 REGISTRATION_POLL_INTERVAL_MS,
15 };
16 use crate::messaging::error::{
17 available_in_window, failed_default_registration, internal_error, unsupported_browser,
18 MessagingResult,
19 };
20
21 #[derive(Clone)]
23 pub struct ServiceWorkerRegistrationHandle {
24 inner: web_sys::ServiceWorkerRegistration,
25 }
26
27 impl ServiceWorkerRegistrationHandle {
28 fn new(inner: web_sys::ServiceWorkerRegistration) -> Self {
29 Self { inner }
30 }
31
32 pub fn as_web_sys(&self) -> &web_sys::ServiceWorkerRegistration {
34 &self.inner
35 }
36 }
37
38 #[derive(Default)]
42 pub struct ServiceWorkerManager {
43 registration: Option<ServiceWorkerRegistrationHandle>,
44 }
45
46 impl ServiceWorkerManager {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn registration(&self) -> Option<ServiceWorkerRegistrationHandle> {
53 self.registration.clone()
54 }
55
56 pub fn use_registration(
58 &mut self,
59 registration: web_sys::ServiceWorkerRegistration,
60 ) -> ServiceWorkerRegistrationHandle {
61 let handle = ServiceWorkerRegistrationHandle::new(registration);
62 self.registration = Some(handle.clone());
63 handle
64 }
65
66 pub async fn register_default(
68 &mut self,
69 ) -> MessagingResult<ServiceWorkerRegistrationHandle> {
70 if let Some(handle) = &self.registration {
71 return Ok(handle.clone());
72 }
73
74 let window = web_sys::window().ok_or_else(|| {
75 available_in_window("Service worker registration requires a Window context")
76 })?;
77 let navigator = window.navigator();
78 let container = navigator.service_worker().ok_or_else(|| {
79 unsupported_browser(
80 "Service workers are not available in this browser environment.",
81 )
82 })?;
83
84 let mut options = web_sys::RegistrationOptions::new();
85 options.scope(DEFAULT_SW_SCOPE);
86
87 let promise = container
88 .register_with_str_and_options(DEFAULT_SW_PATH, &options)
89 .map_err(|err| internal_error(format_js_error("serviceWorker.register", err)))?;
90 let registration_js = JsFuture::from(promise).await.map_err(|err| {
91 failed_default_registration(format_js_error("serviceWorker.register", err))
92 })?;
93 let registration: web_sys::ServiceWorkerRegistration =
94 registration_js.dyn_into().map_err(|_| {
95 failed_default_registration(
96 "Unexpected return value from serviceWorker.register",
97 )
98 })?;
99
100 if let Ok(update_promise) = registration.update() {
101 let _ = JsFuture::from(update_promise).await;
102 }
103
104 wait_for_registration_active(®istration).await?;
105
106 let handle = ServiceWorkerRegistrationHandle::new(registration);
107 self.registration = Some(handle.clone());
108 Ok(handle)
109 }
110 }
111
112 async fn wait_for_registration_active(
113 registration: &web_sys::ServiceWorkerRegistration,
114 ) -> MessagingResult<()> {
115 if registration.active().is_some() {
116 return Ok(());
117 }
118
119 let mut elapsed = 0;
120 while elapsed < DEFAULT_REGISTRATION_TIMEOUT_MS {
121 if registration.active().is_some() {
122 return Ok(());
123 }
124
125 if registration.installing().is_none() && registration.waiting().is_none() {
126 return Err(failed_default_registration(
127 "No incoming service worker found during registration.",
128 ));
129 }
130
131 sleep_ms(REGISTRATION_POLL_INTERVAL_MS).await?;
132 elapsed += REGISTRATION_POLL_INTERVAL_MS;
133 }
134
135 Err(failed_default_registration(format!(
136 "Service worker not registered after {} ms",
137 DEFAULT_REGISTRATION_TIMEOUT_MS
138 )))
139 }
140
141 async fn sleep_ms(ms: i32) -> MessagingResult<()> {
142 let window = web_sys::window().ok_or_else(|| {
143 available_in_window("Timers require a Window context for service worker polling")
144 })?;
145 let window = Rc::new(window);
146
147 let promise = js_sys::Promise::new(&mut |resolve, reject| {
148 let resolve_fn = resolve.unchecked_into::<js_sys::Function>();
149 let reject_fn = reject.unchecked_into::<js_sys::Function>();
150 let window = Rc::clone(&window);
151
152 let closure = Closure::once(move || {
153 let _ = resolve_fn.call0(&JsValue::UNDEFINED);
154 });
155
156 if window
157 .set_timeout_with_callback_and_timeout_and_arguments_0(
158 closure.as_ref().unchecked_ref(),
159 ms,
160 )
161 .is_ok()
162 {
163 closure.forget();
164 } else {
165 let error = js_sys::Error::new("Failed to schedule timeout");
167 let _ = reject_fn.call1(&JsValue::UNDEFINED, &error);
168 }
169 });
170
171 JsFuture::from(promise)
172 .await
173 .map(|_| ())
174 .map_err(|err| internal_error(format_js_error("setTimeout", err)))
175 }
176
177 fn format_js_error(operation: &str, err: JsValue) -> String {
178 let detail = err.as_string().unwrap_or_else(|| format!("{:?}", err));
179 format!("{operation} failed: {detail}")
180 }
181
182 pub use ServiceWorkerManager as Manager;
183 pub use ServiceWorkerRegistrationHandle as Handle;
184}
185
186#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
187pub use wasm::{Handle as ServiceWorkerRegistrationHandle, Manager as ServiceWorkerManager};
188
189#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
190#[derive(Default)]
191pub struct ServiceWorkerManager;
192
193#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
194impl ServiceWorkerManager {
195 pub fn new() -> Self {
196 Self
197 }
198
199 pub fn registration(&self) -> Option<ServiceWorkerRegistrationHandle> {
200 None
201 }
202
203 pub async fn register_default(&mut self) -> MessagingResult<ServiceWorkerRegistrationHandle> {
204 Err(unsupported_browser(
205 "Service worker registration is only available when the `wasm-web` feature is enabled.",
206 ))
207 }
208}
209
210#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
211#[derive(Clone, Debug)]
212pub struct ServiceWorkerRegistrationHandle;
213
214#[cfg(all(test, not(all(feature = "wasm-web", target_arch = "wasm32"))))]
215mod tests {
216 use super::*;
217 use std::future::Future;
218 use std::pin::Pin;
219 use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
220
221 #[test]
222 fn native_manager_reports_unsupported() {
223 let mut manager = ServiceWorkerManager::new();
224 assert!(manager.registration().is_none());
225 let err = block_on_ready(manager.register_default()).unwrap_err();
226 assert_eq!(err.code_str(), "messaging/unsupported-browser");
227 }
228
229 fn block_on_ready<F: Future>(future: F) -> F::Output {
230 let waker = noop_waker();
231 let mut cx = Context::from_waker(&waker);
232 let mut future = future;
233 let mut pinned = unsafe { Pin::new_unchecked(&mut future) };
234 match Future::poll(pinned.as_mut(), &mut cx) {
235 Poll::Ready(value) => value,
236 Poll::Pending => panic!("future unexpectedly pending"),
237 }
238 }
239
240 fn noop_waker() -> Waker {
241 unsafe { Waker::from_raw(noop_raw_waker()) }
242 }
243
244 fn noop_raw_waker() -> RawWaker {
245 RawWaker::new(std::ptr::null(), &NOOP_RAW_WAKER_VTABLE)
246 }
247
248 unsafe fn noop_raw_waker_clone(_: *const ()) -> RawWaker {
249 noop_raw_waker()
250 }
251
252 unsafe fn noop_raw_waker_wake(_: *const ()) {}
253
254 unsafe fn noop_raw_waker_wake_by_ref(_: *const ()) {}
255
256 unsafe fn noop_raw_waker_drop(_: *const ()) {}
257
258 static NOOP_RAW_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
259 noop_raw_waker_clone,
260 noop_raw_waker_wake,
261 noop_raw_waker_wake_by_ref,
262 noop_raw_waker_drop,
263 );
264}