firebase_rs_sdk/platform/browser/
indexed_db.rs1#[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 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 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 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 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 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::*;