indexed_db/
factory.rs

1use crate::{transaction::unsafe_jar, utils::generic_request, Database, Transaction};
2use futures_channel::oneshot;
3use futures_util::{
4    future::{self, Either},
5    pin_mut, FutureExt,
6};
7use std::{future::Future, marker::PhantomData};
8use web_sys::{
9    js_sys::{self, Function},
10    wasm_bindgen::{closure::Closure, JsCast, JsValue},
11    IdbDatabase, IdbFactory, IdbOpenDbRequest, IdbVersionChangeEvent, WorkerGlobalScope,
12};
13
14/// Wrapper for [`IDBFactory`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory)
15///
16/// Note that it is quite likely that type inference will fail on the `Err` generic argument here.
17/// This argument is the type of user-defined errors that will be passed through transactions and
18/// callbacks.
19/// You should set it to whatever error type your program uses around the `indexed-db`-using code.
20#[derive(Debug)]
21pub struct Factory<Err> {
22    sys: IdbFactory,
23    _phantom: PhantomData<Err>,
24}
25
26impl<Err: 'static> Factory<Err> {
27    /// Retrieve the global `Factory` from the browser
28    ///
29    /// This internally uses [`indexedDB`](https://developer.mozilla.org/en-US/docs/Web/API/indexedDB).
30    pub fn get() -> crate::Result<Factory<Err>, Err> {
31        let indexed_db = if let Some(window) = web_sys::window() {
32            window.indexed_db()
33        } else if let Ok(worker_scope) = js_sys::global().dyn_into::<WorkerGlobalScope>() {
34            worker_scope.indexed_db()
35        } else {
36            return Err(crate::Error::NotInBrowser);
37        };
38
39        let sys = indexed_db
40            .map_err(|_| crate::Error::IndexedDbDisabled)?
41            .ok_or(crate::Error::IndexedDbDisabled)?;
42
43        Ok(Factory {
44            sys,
45            _phantom: PhantomData,
46        })
47    }
48
49    /// Compare two keys for ordering
50    ///
51    /// Returns an error if one of the two values would not be a valid IndexedDb key.
52    ///
53    /// This internally uses [`IDBFactory::cmp`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/cmp).
54    pub fn cmp(&self, lhs: &JsValue, rhs: &JsValue) -> crate::Result<std::cmp::Ordering, Err> {
55        use std::cmp::Ordering::*;
56        self.sys
57            .cmp(lhs, rhs)
58            .map(|v| match v {
59                -1 => Less,
60                0 => Equal,
61                1 => Greater,
62                v => panic!("Unexpected result of IDBFactory::cmp: {v}"),
63            })
64            .map_err(|e| match error_name!(&e) {
65                Some("DataError") => crate::Error::InvalidKey,
66                _ => crate::Error::from_js_value(e),
67            })
68    }
69
70    // TODO: add `databases` once web-sys has it
71
72    /// Delete a database
73    ///
74    /// Returns an error if something failed during the deletion. Note that trying to delete
75    /// a database that does not exist will result in a successful result.
76    ///
77    /// This internally uses [`IDBFactory::deleteDatabase`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/deleteDatabase)
78    pub async fn delete_database(&self, name: &str) -> crate::Result<(), Err> {
79        generic_request(
80            self.sys
81                .delete_database(name)
82                .map_err(crate::Error::from_js_value)?
83                .into(),
84        )
85        .await
86        .map(|_| ())
87        .map_err(crate::Error::from_js_event)
88    }
89
90    /// Open a database
91    ///
92    /// Returns an error if something failed while opening or upgrading the database.
93    /// Blocks until it can actually open the database.
94    ///
95    /// Note that `version` must be at least `1`. `on_upgrade_needed` will be called when `version` is higher
96    /// than the previous database version, or upon database creation.
97    ///
98    /// This internally uses [`IDBFactory::open`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/open)
99    /// as well as the methods from [`IDBOpenDBRequest`](https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest)
100    pub async fn open<Fun, RetFut>(
101        &self,
102        name: &str,
103        version: u32,
104        on_upgrade_needed: Fun,
105    ) -> crate::Result<Database<Err>, Err>
106    where
107        Fun: 'static + FnOnce(VersionChangeEvent<Err>) -> RetFut,
108        RetFut: 'static + Future<Output = crate::Result<(), Err>>,
109    {
110        if version == 0 {
111            return Err(crate::Error::VersionMustNotBeZero);
112        }
113
114        let open_req = self
115            .sys
116            .open_with_u32(name, version)
117            .map_err(crate::Error::from_js_value)?;
118
119        let (upgrade_tx, upgrade_rx) = oneshot::channel();
120        let on_upgrade_needed = Closure::once(|evt: IdbVersionChangeEvent| {
121            let evt = VersionChangeEvent::from_sys(evt);
122            let transaction = evt.transaction().as_sys().clone();
123            let fut = {
124                let transaction = transaction.clone();
125                async move {
126                    let res = on_upgrade_needed(evt).await;
127                    let return_value = match &res {
128                        Ok(_) => Ok(()),
129                        Err(_) => Err(()),
130                    };
131                    if let Err(_) = upgrade_tx.send(res) {
132                        // Opening request was cancelled by dropping, abort the transaction
133                        let _ = transaction.abort();
134                    }
135                    return_value
136                }
137            };
138            unsafe_jar::run(transaction, fut);
139        });
140        open_req.set_onupgradeneeded(Some(
141            on_upgrade_needed.as_ref().dyn_ref::<Function>().unwrap(),
142        ));
143
144        let completion_fut = generic_request(open_req.clone().into());
145        pin_mut!(completion_fut);
146
147        let res = future::select(upgrade_rx, completion_fut).await;
148        if unsafe_jar::POLLED_FORBIDDEN_THING.get() {
149            panic!("Transaction blocked without any request under way");
150        }
151
152        match res {
153            Either::Right((completion, _)) => {
154                completion.map_err(crate::Error::from_js_event)?;
155            }
156            Either::Left((upgrade_res, completion_fut)) => {
157                let upgrade_res = upgrade_res.expect("Closure dropped before its end of scope");
158                upgrade_res?;
159                completion_fut.await.map_err(crate::Error::from_js_event)?;
160            }
161        }
162
163        let db = open_req
164            .result()
165            .map_err(crate::Error::from_js_value)?
166            .dyn_into::<IdbDatabase>()
167            .expect("Result of successful IDBOpenDBRequest is not an IDBDatabase");
168
169        Ok(Database::from_sys(db))
170    }
171
172    /// Open a database at the latest version
173    ///
174    /// Returns an error if something failed while opening.
175    /// Blocks until it can actually open the database.
176    ///
177    /// This internally uses [`IDBFactory::open`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/open)
178    /// as well as the methods from [`IDBOpenDBRequest`](https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest)
179    pub async fn open_latest_version(&self, name: &str) -> crate::Result<Database<Err>, Err> {
180        let open_req = self.sys.open(name).map_err(crate::Error::from_js_value)?;
181
182        let completion_fut = generic_request(open_req.clone().into())
183            .map(|res| res.map_err(crate::Error::from_js_event));
184        pin_mut!(completion_fut);
185
186        completion_fut.await?;
187
188        let db = open_req
189            .result()
190            .map_err(crate::Error::from_js_value)?
191            .dyn_into::<IdbDatabase>()
192            .expect("Result of successful IDBOpenDBRequest is not an IDBDatabase");
193
194        Ok(Database::from_sys(db))
195    }
196}
197
198/// Wrapper for [`IDBVersionChangeEvent`](https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeEvent)
199#[derive(Debug)]
200pub struct VersionChangeEvent<Err> {
201    sys: IdbVersionChangeEvent,
202    db: Database<Err>,
203    transaction: Transaction<Err>,
204}
205
206impl<Err> VersionChangeEvent<Err> {
207    fn from_sys(sys: IdbVersionChangeEvent) -> VersionChangeEvent<Err> {
208        let db_req = sys
209            .target()
210            .expect("IDBVersionChangeEvent had no target")
211            .dyn_into::<IdbOpenDbRequest>()
212            .expect("IDBVersionChangeEvent target was not an IDBOpenDBRequest");
213        let db_sys = db_req
214            .result()
215            .expect("IDBOpenDBRequest had no result in its on_upgrade_needed handler")
216            .dyn_into::<IdbDatabase>()
217            .expect("IDBOpenDBRequest result was not an IDBDatabase");
218        let transaction_sys = db_req
219            .transaction()
220            .expect("IDBOpenDBRequest had no associated transaction");
221        let db = Database::from_sys(db_sys);
222        let transaction = Transaction::from_sys(transaction_sys);
223        VersionChangeEvent {
224            sys,
225            db,
226            transaction,
227        }
228    }
229
230    /// The version before the database upgrade, clamped to `u32::MAX`
231    ///
232    /// Internally, this uses [`IDBVersionChangeEvent::oldVersion`](https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeEvent/oldVersion)
233    pub fn old_version(&self) -> u32 {
234        self.sys.old_version() as u32
235    }
236
237    /// The version after the database upgrade, clamped to `u32::MAX`
238    ///
239    /// Internally, this uses [`IDBVersionChangeEvent::newVersion`](https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeEvent/newVersion)
240    pub fn new_version(&self) -> u32 {
241        self.sys
242            .new_version()
243            .expect("IDBVersionChangeEvent did not provide a new version") as u32
244    }
245
246    /// The database under creation
247    pub fn database(&self) -> &Database<Err> {
248        &self.db
249    }
250
251    /// The `versionchange` transaction that triggered this event
252    ///
253    /// This transaction can be used to submit further requests.
254    pub fn transaction(&self) -> &Transaction<Err> {
255        &self.transaction
256    }
257}