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