firebase_rs_sdk/messaging/
sw_manager.rs

1#[cfg(all(
2    feature = "wasm-web",
3    target_arch = "wasm32",
4    feature = "experimental-indexed-db"
5))]
6mod wasm {
7    use js_sys::Reflect;
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, unsupported_browser, MessagingResult,
18    };
19    use crate::platform::runtime;
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 navigator_js = JsValue::from(navigator.clone());
79            let container_value = Reflect::get(&navigator_js, &JsValue::from_str("serviceWorker"))
80                .map_err(|_| {
81                    unsupported_browser(
82                        "Service workers are not available in this browser environment.",
83                    )
84                })?;
85            if container_value.is_undefined() || container_value.is_null() {
86                return Err(unsupported_browser(
87                    "Service workers are not available in this browser environment.",
88                ));
89            }
90            let container: web_sys::ServiceWorkerContainer =
91                container_value.dyn_into().map_err(|_| {
92                    unsupported_browser(
93                        "Service workers are not available in this browser environment.",
94                    )
95                })?;
96
97            let options = web_sys::RegistrationOptions::new();
98            options.set_scope(DEFAULT_SW_SCOPE);
99
100            let promise = container.register_with_options(DEFAULT_SW_PATH, &options);
101            let registration_js = JsFuture::from(promise).await.map_err(|err| {
102                failed_default_registration(format_js_error("serviceWorker.register", err))
103            })?;
104            let registration: web_sys::ServiceWorkerRegistration =
105                registration_js.dyn_into().map_err(|_| {
106                    failed_default_registration(
107                        "Unexpected return value from serviceWorker.register",
108                    )
109                })?;
110
111            if let Ok(update_promise) = registration.update() {
112                let _ = JsFuture::from(update_promise).await;
113            }
114
115            wait_for_registration_active(&registration).await?;
116
117            let handle = ServiceWorkerRegistrationHandle::new(registration);
118            self.registration = Some(handle.clone());
119            Ok(handle)
120        }
121    }
122
123    async fn wait_for_registration_active(
124        registration: &web_sys::ServiceWorkerRegistration,
125    ) -> MessagingResult<()> {
126        if registration.active().is_some() {
127            return Ok(());
128        }
129
130        let mut elapsed = 0;
131        while elapsed < DEFAULT_REGISTRATION_TIMEOUT_MS {
132            if registration.active().is_some() {
133                return Ok(());
134            }
135
136            if registration.installing().is_none() && registration.waiting().is_none() {
137                return Err(failed_default_registration(
138                    "No incoming service worker found during registration.",
139                ));
140            }
141
142            sleep_ms(REGISTRATION_POLL_INTERVAL_MS).await?;
143            elapsed += REGISTRATION_POLL_INTERVAL_MS;
144        }
145
146        Err(failed_default_registration(format!(
147            "Service worker not registered after {} ms",
148            DEFAULT_REGISTRATION_TIMEOUT_MS
149        )))
150    }
151
152    async fn sleep_ms(ms: i32) -> MessagingResult<()> {
153        if ms <= 0 {
154            return Ok(());
155        }
156        let duration = std::time::Duration::from_millis(ms as u64);
157        runtime::sleep(duration).await;
158        Ok(())
159    }
160
161    fn format_js_error(operation: &str, err: JsValue) -> String {
162        let detail = err.as_string().unwrap_or_else(|| format!("{:?}", err));
163        format!("{operation} failed: {detail}")
164    }
165
166    pub use ServiceWorkerManager as Manager;
167    pub use ServiceWorkerRegistrationHandle as Handle;
168}
169
170#[cfg(all(
171    feature = "wasm-web",
172    target_arch = "wasm32",
173    feature = "experimental-indexed-db"
174))]
175pub use wasm::{Handle as ServiceWorkerRegistrationHandle, Manager as ServiceWorkerManager};
176
177#[cfg(any(
178    not(all(feature = "wasm-web", target_arch = "wasm32")),
179    all(
180        feature = "wasm-web",
181        target_arch = "wasm32",
182        not(feature = "experimental-indexed-db")
183    )
184))]
185#[derive(Default)]
186pub struct ServiceWorkerManager;
187
188#[cfg(any(
189    not(all(feature = "wasm-web", target_arch = "wasm32")),
190    all(
191        feature = "wasm-web",
192        target_arch = "wasm32",
193        not(feature = "experimental-indexed-db")
194    )
195))]
196impl ServiceWorkerManager {
197    pub fn new() -> Self {
198        Self
199    }
200
201    pub fn registration(&self) -> Option<ServiceWorkerRegistrationHandle> {
202        None
203    }
204
205    pub async fn register_default(
206        &mut self,
207    ) -> crate::messaging::error::MessagingResult<ServiceWorkerRegistrationHandle> {
208        Err(crate::messaging::error::unsupported_browser(
209            "Service worker registration is only available when the `wasm-web` feature is enabled.",
210        ))
211    }
212}
213
214#[cfg(any(
215    not(all(feature = "wasm-web", target_arch = "wasm32")),
216    all(
217        feature = "wasm-web",
218        target_arch = "wasm32",
219        not(feature = "experimental-indexed-db")
220    )
221))]
222#[derive(Clone, Debug)]
223pub struct ServiceWorkerRegistrationHandle;
224
225#[cfg(all(
226    test,
227    any(
228        not(all(feature = "wasm-web", target_arch = "wasm32")),
229        all(
230            feature = "wasm-web",
231            target_arch = "wasm32",
232            not(feature = "experimental-indexed-db")
233        )
234    )
235))]
236mod tests {
237    use super::*;
238
239    #[tokio::test(flavor = "current_thread")]
240    async fn native_manager_reports_unsupported() {
241        let mut manager = ServiceWorkerManager::new();
242        assert!(manager.registration().is_none());
243        let err = manager.register_default().await.unwrap_err();
244        assert_eq!(err.code_str(), "messaging/unsupported-browser");
245    }
246}