firebase_rs_sdk/messaging/
sw_manager.rs

1use 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    /// Thin wrapper around a `ServiceWorkerRegistration` reference.
22    #[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        /// Returns the underlying `ServiceWorkerRegistration` handle.
33        pub fn as_web_sys(&self) -> &web_sys::ServiceWorkerRegistration {
34            &self.inner
35        }
36    }
37
38    /// Coordinates service worker registration for messaging.
39    ///
40    /// Port of the logic in `packages/messaging/src/helpers/registerDefaultSw.ts`.
41    #[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        /// Returns the cached service worker registration, if one was previously stored.
52        pub fn registration(&self) -> Option<ServiceWorkerRegistrationHandle> {
53            self.registration.clone()
54        }
55
56        /// Caches a user-supplied `ServiceWorkerRegistration`.
57        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        /// Registers the default Firebase Messaging service worker and waits until it activates.
67        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(&registration).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                // If setTimeout fails we propagate an error through the promise rejection path.
166                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}