1use serde::{Deserialize, Serialize};
2
3#[cfg(all(
4 feature = "wasm-web",
5 target_arch = "wasm32",
6 feature = "experimental-indexed-db"
7))]
8use crate::messaging::error::internal_error;
9use crate::messaging::error::MessagingResult;
10#[cfg(all(
11 feature = "wasm-web",
12 target_arch = "wasm32",
13 feature = "experimental-indexed-db"
14))]
15use crate::platform::browser::indexed_db;
16
17#[cfg_attr(
18 all(
19 feature = "wasm-web",
20 target_arch = "wasm32",
21 not(feature = "experimental-indexed-db")
22 ),
23 allow(dead_code)
24)]
25#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
26pub struct SubscriptionInfo {
27 pub vapid_key: String,
28 pub scope: String,
29 pub endpoint: String,
30 pub auth: String,
31 pub p256dh: String,
32}
33
34#[cfg_attr(
35 not(all(feature = "wasm-web", target_arch = "wasm32")),
36 allow(dead_code)
37)]
38#[cfg_attr(
39 all(
40 feature = "wasm-web",
41 target_arch = "wasm32",
42 not(feature = "experimental-indexed-db")
43 ),
44 allow(dead_code)
45)]
46const AUTH_TOKEN_REFRESH_BUFFER_MS: u64 = 60_000;
47
48#[cfg_attr(
49 all(
50 feature = "wasm-web",
51 target_arch = "wasm32",
52 not(feature = "experimental-indexed-db")
53 ),
54 allow(dead_code)
55)]
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
57pub struct TokenRecord {
58 pub token: String,
59 pub create_time_ms: u64,
60 pub subscription: Option<SubscriptionInfo>,
61 pub installation: InstallationInfo,
62}
63
64#[cfg_attr(
65 all(
66 feature = "wasm-web",
67 target_arch = "wasm32",
68 not(feature = "experimental-indexed-db")
69 ),
70 allow(dead_code)
71)]
72impl TokenRecord {
73 #[cfg_attr(
74 not(all(feature = "wasm-web", target_arch = "wasm32")),
75 allow(dead_code)
76 )]
77 pub fn is_expired(&self, now_ms: u64, ttl_ms: u64) -> bool {
78 now_ms.saturating_sub(self.create_time_ms) >= ttl_ms
79 }
80}
81
82#[cfg_attr(
83 all(
84 feature = "wasm-web",
85 target_arch = "wasm32",
86 not(feature = "experimental-indexed-db")
87 ),
88 allow(dead_code)
89)]
90#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
91pub struct InstallationInfo {
92 pub fid: String,
93 pub refresh_token: String,
94 pub auth_token: String,
95 pub auth_token_expiration_ms: u64,
96}
97
98#[cfg_attr(
99 all(
100 feature = "wasm-web",
101 target_arch = "wasm32",
102 not(feature = "experimental-indexed-db")
103 ),
104 allow(dead_code)
105)]
106impl InstallationInfo {
107 #[cfg_attr(
108 not(all(feature = "wasm-web", target_arch = "wasm32")),
109 allow(dead_code)
110 )]
111 pub fn auth_token_expired(&self, now_ms: u64) -> bool {
112 now_ms + AUTH_TOKEN_REFRESH_BUFFER_MS >= self.auth_token_expiration_ms
113 }
114}
115
116#[cfg_attr(
117 all(
118 feature = "wasm-web",
119 target_arch = "wasm32",
120 not(feature = "experimental-indexed-db")
121 ),
122 allow(dead_code)
123)]
124#[cfg(any(
125 not(all(feature = "wasm-web", target_arch = "wasm32")),
126 all(
127 feature = "wasm-web",
128 target_arch = "wasm32",
129 not(feature = "experimental-indexed-db")
130 )
131))]
132mod memory_store {
133 use std::collections::HashMap;
134 use std::sync::Mutex;
135
136 use once_cell::sync::Lazy;
137
138 use super::{MessagingResult, TokenRecord};
139
140 static STORE: Lazy<Mutex<HashMap<String, TokenRecord>>> =
141 Lazy::new(|| Mutex::new(HashMap::new()));
142
143 pub fn read(app_key: &str) -> MessagingResult<Option<TokenRecord>> {
144 Ok(STORE.lock().unwrap().get(app_key).cloned())
145 }
146
147 pub fn write(app_key: &str, record: &TokenRecord) -> MessagingResult<()> {
148 STORE
149 .lock()
150 .unwrap()
151 .insert(app_key.to_string(), record.clone());
152 Ok(())
153 }
154
155 pub fn remove(app_key: &str) -> MessagingResult<bool> {
156 Ok(STORE.lock().unwrap().remove(app_key).is_some())
157 }
158}
159
160#[cfg(all(
161 feature = "wasm-web",
162 target_arch = "wasm32",
163 feature = "experimental-indexed-db"
164))]
165use std::cell::RefCell;
166#[cfg(all(
167 feature = "wasm-web",
168 target_arch = "wasm32",
169 feature = "experimental-indexed-db"
170))]
171use std::collections::HashMap;
172
173#[cfg(all(
174 feature = "wasm-web",
175 target_arch = "wasm32",
176 feature = "experimental-indexed-db"
177))]
178use wasm_bindgen::closure::Closure;
179#[cfg(all(
180 feature = "wasm-web",
181 target_arch = "wasm32",
182 feature = "experimental-indexed-db"
183))]
184use wasm_bindgen::{JsCast, JsValue};
185#[cfg(all(
186 feature = "wasm-web",
187 target_arch = "wasm32",
188 feature = "experimental-indexed-db"
189))]
190use web_sys::{BroadcastChannel, MessageEvent};
191
192#[cfg(all(
193 feature = "wasm-web",
194 target_arch = "wasm32",
195 feature = "experimental-indexed-db"
196))]
197const DATABASE_NAME: &str = "firebase-messaging-database";
198#[cfg(all(
199 feature = "wasm-web",
200 target_arch = "wasm32",
201 feature = "experimental-indexed-db"
202))]
203const DATABASE_VERSION: u32 = 1;
204#[cfg(all(
205 feature = "wasm-web",
206 target_arch = "wasm32",
207 feature = "experimental-indexed-db"
208))]
209const STORE_NAME: &str = "firebase-messaging-store";
210#[cfg(all(
211 feature = "wasm-web",
212 target_arch = "wasm32",
213 feature = "experimental-indexed-db"
214))]
215const BROADCAST_CHANNEL_NAME: &str = "firebase-messaging-token-updates";
216
217#[cfg(all(
218 feature = "wasm-web",
219 target_arch = "wasm32",
220 feature = "experimental-indexed-db"
221))]
222#[derive(Serialize, Deserialize, Clone, Debug)]
223struct BroadcastMessage {
224 app_key: String,
225 payload: BroadcastPayload,
226}
227
228#[cfg(all(
229 feature = "wasm-web",
230 target_arch = "wasm32",
231 feature = "experimental-indexed-db"
232))]
233#[derive(Serialize, Deserialize, Clone, Debug)]
234enum BroadcastPayload {
235 Set(TokenRecord),
236 Remove,
237}
238
239#[cfg(all(
240 feature = "wasm-web",
241 target_arch = "wasm32",
242 feature = "experimental-indexed-db"
243))]
244thread_local! {
245 static TOKEN_CACHE: RefCell<HashMap<String, Option<TokenRecord>>> = RefCell::new(HashMap::new());
246 static BROADCAST_CHANNEL: RefCell<Option<BroadcastChannel>> = RefCell::new(None);
247 static BROADCAST_HANDLER: RefCell<Option<Closure<dyn FnMut(MessageEvent)>>> = RefCell::new(None);
248}
249
250#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
251pub fn read_token(app_key: &str) -> MessagingResult<Option<TokenRecord>> {
252 memory_store::read(app_key)
253}
254
255#[cfg(all(
256 feature = "wasm-web",
257 target_arch = "wasm32",
258 feature = "experimental-indexed-db"
259))]
260pub async fn read_token(app_key: &str) -> MessagingResult<Option<TokenRecord>> {
261 ensure_broadcast_channel();
262 if let Some(cached) = cache_get(app_key) {
263 return Ok(cached);
264 }
265
266 let db = open_db().await?;
267 let stored = indexed_db::get_string(&db, STORE_NAME, app_key)
268 .await
269 .map_err(|err| internal_error(err.to_string()))?;
270 let record = if let Some(json) = stored {
271 let record: TokenRecord = serde_json::from_str(&json)
272 .map_err(|err| internal_error(format!("Failed to parse stored token: {err}")))?;
273 Some(record)
274 } else {
275 None
276 };
277 cache_set(app_key, record.clone());
278 Ok(record)
279}
280
281#[cfg_attr(
282 all(
283 feature = "wasm-web",
284 target_arch = "wasm32",
285 not(feature = "experimental-indexed-db")
286 ),
287 allow(dead_code)
288)]
289#[cfg(all(
290 feature = "wasm-web",
291 target_arch = "wasm32",
292 not(feature = "experimental-indexed-db")
293))]
294pub async fn read_token(app_key: &str) -> MessagingResult<Option<TokenRecord>> {
295 Ok(memory_store::read(app_key)?)
296}
297
298#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
299pub fn write_token(app_key: &str, record: &TokenRecord) -> MessagingResult<()> {
300 memory_store::write(app_key, record)
301}
302
303#[cfg(all(
304 feature = "wasm-web",
305 target_arch = "wasm32",
306 feature = "experimental-indexed-db"
307))]
308pub async fn write_token(app_key: &str, record: &TokenRecord) -> MessagingResult<()> {
309 ensure_broadcast_channel();
310 let json = serde_json::to_string(record)
311 .map_err(|err| internal_error(format!("Failed to serialize token: {err}")))?;
312 let db = open_db().await?;
313 indexed_db::put_string(&db, STORE_NAME, app_key, &json)
314 .await
315 .map_err(|err| internal_error(err.to_string()))?;
316 cache_set(app_key, Some(record.clone()));
317 broadcast_update(app_key, BroadcastPayload::Set(record.clone()));
318 Ok(())
319}
320
321#[cfg_attr(
322 all(
323 feature = "wasm-web",
324 target_arch = "wasm32",
325 not(feature = "experimental-indexed-db")
326 ),
327 allow(dead_code)
328)]
329#[cfg(all(
330 feature = "wasm-web",
331 target_arch = "wasm32",
332 not(feature = "experimental-indexed-db")
333))]
334pub async fn write_token(app_key: &str, record: &TokenRecord) -> MessagingResult<()> {
335 memory_store::write(app_key, record)?;
336 Ok(())
337}
338
339#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
340pub fn remove_token(app_key: &str) -> MessagingResult<bool> {
341 memory_store::remove(app_key)
342}
343
344#[cfg(all(
345 feature = "wasm-web",
346 target_arch = "wasm32",
347 feature = "experimental-indexed-db"
348))]
349pub async fn remove_token(app_key: &str) -> MessagingResult<bool> {
350 ensure_broadcast_channel();
351 let db = open_db().await?;
352 let existed = indexed_db::get_string(&db, STORE_NAME, app_key)
353 .await
354 .map_err(|err| internal_error(err.to_string()))?
355 .is_some();
356 if existed {
357 indexed_db::delete_key(&db, STORE_NAME, app_key)
358 .await
359 .map_err(|err| internal_error(err.to_string()))?;
360 cache_set(app_key, None);
361 broadcast_update(app_key, BroadcastPayload::Remove);
362 }
363 Ok(existed)
364}
365
366#[cfg_attr(
367 all(
368 feature = "wasm-web",
369 target_arch = "wasm32",
370 not(feature = "experimental-indexed-db")
371 ),
372 allow(dead_code)
373)]
374#[cfg(all(
375 feature = "wasm-web",
376 target_arch = "wasm32",
377 not(feature = "experimental-indexed-db")
378))]
379pub async fn remove_token(app_key: &str) -> MessagingResult<bool> {
380 memory_store::remove(app_key)
381}
382
383#[cfg(all(
384 feature = "wasm-web",
385 target_arch = "wasm32",
386 feature = "experimental-indexed-db"
387))]
388async fn open_db() -> MessagingResult<web_sys::IdbDatabase> {
389 indexed_db::open_database_with_store(DATABASE_NAME, DATABASE_VERSION, STORE_NAME)
390 .await
391 .map_err(|err| internal_error(err.to_string()))
392}
393
394#[cfg(all(
395 feature = "wasm-web",
396 target_arch = "wasm32",
397 feature = "experimental-indexed-db"
398))]
399fn cache_get(app_key: &str) -> Option<Option<TokenRecord>> {
400 TOKEN_CACHE.with(|cache| cache.borrow().get(app_key).cloned())
401}
402
403#[cfg(all(
404 feature = "wasm-web",
405 target_arch = "wasm32",
406 feature = "experimental-indexed-db"
407))]
408fn cache_set(app_key: &str, value: Option<TokenRecord>) {
409 TOKEN_CACHE.with(|cache| {
410 cache.borrow_mut().insert(app_key.to_string(), value);
411 });
412}
413
414#[cfg(all(
415 feature = "wasm-web",
416 target_arch = "wasm32",
417 feature = "experimental-indexed-db"
418))]
419fn ensure_broadcast_channel() {
420 BROADCAST_CHANNEL.with(|channel_cell| {
421 if channel_cell.borrow().is_some() {
422 return;
423 }
424
425 match BroadcastChannel::new(BROADCAST_CHANNEL_NAME) {
426 Ok(channel) => {
427 let handler = Closure::wrap(Box::new(|event: MessageEvent| {
428 if let Some(text) = event.data().as_string() {
429 if let Ok(message) = serde_json::from_str::<BroadcastMessage>(&text) {
430 handle_broadcast_message(message);
431 }
432 }
433 }) as Box<dyn FnMut(_)>);
434 channel.set_onmessage(Some(handler.as_ref().unchecked_ref()));
435 BROADCAST_HANDLER.with(|slot| {
436 slot.replace(Some(handler));
437 });
438 channel_cell.replace(Some(channel));
439 }
440 Err(err) => {
441 log_warning("Failed to initialize BroadcastChannel", Some(&err));
442 }
443 }
444 });
445}
446
447#[cfg(all(
448 feature = "wasm-web",
449 target_arch = "wasm32",
450 feature = "experimental-indexed-db"
451))]
452fn handle_broadcast_message(message: BroadcastMessage) {
453 match message.payload {
454 BroadcastPayload::Set(record) => cache_set(&message.app_key, Some(record)),
455 BroadcastPayload::Remove => cache_set(&message.app_key, None),
456 }
457}
458
459#[cfg(all(
460 feature = "wasm-web",
461 target_arch = "wasm32",
462 feature = "experimental-indexed-db"
463))]
464fn broadcast_update(app_key: &str, payload: BroadcastPayload) {
465 BROADCAST_CHANNEL.with(|cell| {
466 if cell.borrow().is_none() {
467 ensure_broadcast_channel();
468 }
469 });
470
471 BROADCAST_CHANNEL.with(|cell| {
472 if let Some(channel) = cell.borrow().as_ref() {
473 let message = BroadcastMessage {
474 app_key: app_key.to_string(),
475 payload,
476 };
477 if let Ok(serialized) = serde_json::to_string(&message) {
478 if let Err(err) = channel.post_message(&JsValue::from_str(&serialized)) {
479 log_warning("Failed to broadcast messaging token update", Some(&err));
480 }
481 }
482 }
483 });
484}
485
486#[cfg(all(
487 feature = "wasm-web",
488 target_arch = "wasm32",
489 feature = "experimental-indexed-db"
490))]
491fn log_warning(message: &str, err: Option<&JsValue>) {
492 if let Some(err) = err {
493 web_sys::console::warn_2(&JsValue::from_str(message), err);
494 } else {
495 web_sys::console::warn_1(&JsValue::from_str(message));
496 }
497}