firebase_rs_sdk/messaging/
token_store.rs

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}