firebase_rs_sdk/platform/browser/
indexed_db.rs

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