firebase_rs_sdk/platform/browser/
indexed_db.rs

1//! Lightweight IndexedDB helpers shared across browser-facing modules.
2
3#[cfg(all(
4    feature = "wasm-web",
5    target_arch = "wasm32",
6    feature = "experimental-indexed-db"
7))]
8mod wasm {
9    use wasm_bindgen::closure::Closure;
10    use wasm_bindgen::JsCast;
11    use wasm_bindgen::JsValue;
12    use wasm_bindgen_futures::JsFuture;
13    use web_sys::{
14        DomStringList, Event, IdbDatabase, IdbOpenDbRequest, IdbRequest, IdbTransactionMode,
15        IdbVersionChangeEvent,
16    };
17
18    #[derive(Debug)]
19    pub enum IndexedDbError {
20        Unsupported(&'static str),
21        Operation(String),
22    }
23
24    impl std::fmt::Display for IndexedDbError {
25        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26            match self {
27                IndexedDbError::Unsupported(msg) => write!(f, "IndexedDB unsupported: {msg}"),
28                IndexedDbError::Operation(msg) => write!(f, "IndexedDB error: {msg}"),
29            }
30        }
31    }
32
33    impl std::error::Error for IndexedDbError {}
34
35    pub type IndexedDbResult<T> = Result<T, IndexedDbError>;
36
37    const UNSUPPORTED: &str = "IndexedDB APIs are not available in this environment";
38
39    /// Opens (or creates) an IndexedDB database, ensuring that the provided object store exists.
40    pub async fn open_database_with_store(
41        name: &str,
42        version: u32,
43        store: &str,
44    ) -> IndexedDbResult<IdbDatabase> {
45        let window = web_sys::window().ok_or(IndexedDbError::Unsupported(UNSUPPORTED))?;
46        let factory = window
47            .indexed_db()
48            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?
49            .ok_or(IndexedDbError::Unsupported(UNSUPPORTED))?;
50        let request = factory
51            .open_with_u32(name, version)
52            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
53
54        let store_name = store.to_owned();
55        let upgrade_handler = Closure::wrap(Box::new(move |event: IdbVersionChangeEvent| {
56            if let Some(target) = event.target() {
57                if let Ok(open_request) = target.dyn_into::<IdbOpenDbRequest>() {
58                    if let Ok(result) = open_request.result() {
59                        if let Ok(db) = result.dyn_into::<IdbDatabase>() {
60                            ensure_store_exists(&db, &store_name);
61                        }
62                    }
63                }
64            }
65        }) as Box<dyn FnMut(_)>);
66        request.set_onupgradeneeded(Some(upgrade_handler.as_ref().unchecked_ref()));
67        upgrade_handler.forget();
68
69        let db_js = JsFuture::from(request_to_future(clone_as_idb_request(&request)))
70            .await
71            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
72        let db: IdbDatabase = db_js
73            .dyn_into()
74            .map_err(|_| IndexedDbError::Operation("Failed to acquire database handle".into()))?;
75        Ok(db)
76    }
77
78    /// Reads a UTF-8 string value from the specified store and key.
79    pub async fn get_string(
80        db: &IdbDatabase,
81        store: &str,
82        key: &str,
83    ) -> IndexedDbResult<Option<String>> {
84        let tx = db
85            .transaction_with_str_and_mode(store, IdbTransactionMode::Readonly)
86            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
87        let object_store = tx
88            .object_store(store)
89            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
90        let request = object_store
91            .get(&JsValue::from_str(key))
92            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
93        let result = JsFuture::from(request_to_future(request))
94            .await
95            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
96        if result.is_undefined() || result.is_null() {
97            Ok(None)
98        } else if let Some(value) = result.as_string() {
99            Ok(Some(value))
100        } else {
101            Err(IndexedDbError::Operation(
102                "Stored value is not a string".into(),
103            ))
104        }
105    }
106
107    /// Writes a UTF-8 string value into the specified store/key.
108    pub async fn put_string(
109        db: &IdbDatabase,
110        store: &str,
111        key: &str,
112        value: &str,
113    ) -> IndexedDbResult<()> {
114        let tx = db
115            .transaction_with_str_and_mode(store, IdbTransactionMode::Readwrite)
116            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
117        let object_store = tx
118            .object_store(store)
119            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
120        let request = object_store
121            .put_with_key(&JsValue::from_str(value), &JsValue::from_str(key))
122            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
123        JsFuture::from(request_to_future(request))
124            .await
125            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
126        Ok(())
127    }
128
129    /// Deletes the value stored under the given key.
130    pub async fn delete_key(db: &IdbDatabase, store: &str, key: &str) -> IndexedDbResult<()> {
131        let tx = db
132            .transaction_with_str_and_mode(store, IdbTransactionMode::Readwrite)
133            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
134        let object_store = tx
135            .object_store(store)
136            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
137        let request = object_store
138            .delete(&JsValue::from_str(key))
139            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
140        JsFuture::from(request_to_future(request))
141            .await
142            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
143        Ok(())
144    }
145
146    /// Deletes the entire database. Useful for tests.
147    pub async fn delete_database(name: &str) -> IndexedDbResult<()> {
148        let window = web_sys::window().ok_or(IndexedDbError::Unsupported(UNSUPPORTED))?;
149        let factory = window
150            .indexed_db()
151            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?
152            .ok_or(IndexedDbError::Unsupported(UNSUPPORTED))?;
153        let request = factory
154            .delete_database(name)
155            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
156        JsFuture::from(request_to_future(clone_as_idb_request(&request)))
157            .await
158            .map_err(|err| IndexedDbError::Operation(js_value_to_string(&err)))?;
159        Ok(())
160    }
161
162    fn ensure_store_exists(db: &IdbDatabase, store: &str) {
163        let existing = db.object_store_names();
164        if !dom_string_list_contains(&existing, store) {
165            let _ = db.create_object_store(store);
166        }
167    }
168
169    fn dom_string_list_contains(list: &DomStringList, target: &str) -> bool {
170        for idx in 0..list.length() {
171            if let Some(value) = list.item(idx) {
172                if value == target {
173                    return true;
174                }
175            }
176        }
177        false
178    }
179
180    fn request_to_future(request: IdbRequest) -> js_sys::Promise {
181        let success_request = request.clone();
182        let error_request = request.clone();
183        js_sys::Promise::new(&mut move |resolve, reject| {
184            let resolve_fn = resolve.clone();
185            let reject_for_success = reject.clone();
186            let success_request_clone = success_request.clone();
187            let success =
188                Closure::once(
189                    Box::new(move |_event: Event| match success_request_clone.result() {
190                        Ok(result) => {
191                            let _ = resolve_fn.call1(&JsValue::UNDEFINED, &result);
192                        }
193                        Err(err) => {
194                            let _ = reject_for_success.call1(&JsValue::UNDEFINED, &err);
195                        }
196                    }) as Box<dyn FnMut(_)>,
197                );
198            request.set_onsuccess(Some(success.as_ref().unchecked_ref()));
199            success.forget();
200
201            let reject_fn = reject.clone();
202            let error_request_clone = error_request.clone();
203            let error =
204                Closure::once(
205                    Box::new(move |_event: Event| match error_request_clone.error() {
206                        Ok(Some(err)) => {
207                            let _ = reject_fn.call1(&JsValue::UNDEFINED, &err);
208                        }
209                        Ok(None) => {
210                            let _ = reject_fn.call1(&JsValue::UNDEFINED, &JsValue::NULL);
211                        }
212                        Err(js_err) => {
213                            let _ = reject_fn.call1(&JsValue::UNDEFINED, &js_err);
214                        }
215                    }) as Box<dyn FnMut(_)>,
216                );
217            request.set_onerror(Some(error.as_ref().unchecked_ref()));
218            error.forget();
219        })
220    }
221
222    fn clone_as_idb_request(request: &IdbOpenDbRequest) -> IdbRequest {
223        request.clone().unchecked_into::<IdbRequest>()
224    }
225
226    fn js_value_to_string(value: &JsValue) -> String {
227        if let Some(exception) = value.dyn_ref::<web_sys::DomException>() {
228            format!("{}: {}", exception.name(), exception.message())
229        } else if let Some(text) = value.as_string() {
230            text
231        } else {
232            format!("{:?}", value)
233        }
234    }
235
236    pub use IndexedDbError as Error;
237}
238
239#[cfg(all(
240    feature = "wasm-web",
241    target_arch = "wasm32",
242    feature = "experimental-indexed-db"
243))]
244pub use wasm::*;
245
246#[cfg(not(all(
247    feature = "wasm-web",
248    target_arch = "wasm32",
249    feature = "experimental-indexed-db"
250)))]
251mod stub {
252
253    #[derive(Debug)]
254    pub enum IndexedDbError {
255        Unsupported,
256    }
257
258    impl std::fmt::Display for IndexedDbError {
259        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260            write!(f, "IndexedDB not supported on this target")
261        }
262    }
263
264    impl std::error::Error for IndexedDbError {}
265
266    pub type IndexedDbResult<T> = std::result::Result<T, IndexedDbError>;
267
268    pub type IdbDatabase = ();
269
270    pub async fn open_database_with_store(
271        _name: &str,
272        _version: u32,
273        _store: &str,
274    ) -> IndexedDbResult<IdbDatabase> {
275        Err(IndexedDbError::Unsupported)
276    }
277
278    pub async fn get_string(
279        _db: &IdbDatabase,
280        _store: &str,
281        _key: &str,
282    ) -> IndexedDbResult<Option<String>> {
283        Err(IndexedDbError::Unsupported)
284    }
285
286    pub async fn put_string(
287        _db: &IdbDatabase,
288        _store: &str,
289        _key: &str,
290        _value: &str,
291    ) -> IndexedDbResult<()> {
292        Err(IndexedDbError::Unsupported)
293    }
294
295    pub async fn delete_key(_db: &IdbDatabase, _store: &str, _key: &str) -> IndexedDbResult<()> {
296        Err(IndexedDbError::Unsupported)
297    }
298
299    pub async fn delete_database(_name: &str) -> IndexedDbResult<()> {
300        Err(IndexedDbError::Unsupported)
301    }
302
303    pub use IndexedDbError as Error;
304}
305
306#[cfg(not(all(
307    feature = "wasm-web",
308    target_arch = "wasm32",
309    feature = "experimental-indexed-db"
310)))]
311pub use stub::*;