indexed_db/
transaction.rs

1use crate::{
2    utils::{err_from_event, str_slice_to_array},
3    ObjectStore,
4};
5use futures_channel::oneshot;
6use futures_util::future::{self, Either};
7use std::{future::Future, marker::PhantomData};
8use web_sys::{
9    wasm_bindgen::{JsCast, JsValue},
10    IdbDatabase, IdbRequest, IdbTransaction, IdbTransactionMode,
11};
12
13pub(crate) mod unsafe_jar;
14
15/// Wrapper for [`IDBTransaction`](https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction)
16#[derive(Debug)]
17pub struct Transaction<Err> {
18    sys: IdbTransaction,
19    _phantom: PhantomData<Err>,
20}
21
22impl<Err> Transaction<Err> {
23    pub(crate) fn from_sys(sys: IdbTransaction) -> Transaction<Err> {
24        Transaction {
25            sys,
26            _phantom: PhantomData,
27        }
28    }
29
30    pub(crate) fn as_sys(&self) -> &IdbTransaction {
31        &self.sys
32    }
33
34    /// Returns an [`ObjectStore`] that can be used to operate on data in this transaction
35    ///
36    /// Internally, this uses [`IDBTransaction::objectStore`](https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/objectStore).
37    pub fn object_store(&self, name: &str) -> crate::Result<ObjectStore<Err>, Err> {
38        Ok(ObjectStore::from_sys(self.sys.object_store(name).map_err(
39            |err| match error_name!(&err) {
40                Some("NotFoundError") => crate::Error::DoesNotExist,
41                _ => crate::Error::from_js_value(err),
42            },
43        )?))
44    }
45}
46
47/// Helper to build a transaction
48pub struct TransactionBuilder<Err> {
49    db: IdbDatabase,
50    stores: JsValue,
51    mode: IdbTransactionMode,
52    _phantom: PhantomData<Err>,
53    // TODO: add support for transaction durability when web-sys gets it
54}
55
56impl<Err> TransactionBuilder<Err> {
57    pub(crate) fn from_names(db: IdbDatabase, names: &[&str]) -> TransactionBuilder<Err> {
58        TransactionBuilder {
59            db,
60            stores: str_slice_to_array(names).into(),
61            mode: IdbTransactionMode::Readonly,
62            _phantom: PhantomData,
63        }
64    }
65
66    /// Allow writes in this transaction
67    ///
68    /// Without this, the transaction will only be allowed reads, and will error upon trying to
69    /// write objects.
70    pub fn rw(mut self) -> Self {
71        self.mode = IdbTransactionMode::Readwrite;
72        self
73    }
74
75    /// Actually execute the transaction
76    ///
77    /// The `transaction` argument defines what will be run in the transaction. Note that due to
78    /// limitations of the IndexedDb API, the future returned by `transaction` cannot call `.await`
79    /// on any future except the ones provided by the [`Transaction`] itself. This function will
80    /// do its best to detect these cases to abort the transaction and panic, but you should avoid
81    /// doing so anyway. Note also that these errors are not recoverable: even if wasm32 were not
82    /// having `panic=abort`, once there is such a panic no `indexed-db` functions will work any
83    /// longer.
84    ///
85    /// If `transaction` returns an `Ok` value, then the transaction will be committed. If it
86    /// returns an `Err` value, then it will be aborted.
87    ///
88    /// Note that you should avoid sending requests that you do not await. If you do, it is hard
89    /// to say whether the transaction will commit or abort, due to both the IndexedDB and the
90    /// `wasm-bindgen` semantics.
91    ///
92    /// Note that transactions cannot be nested.
93    ///
94    /// Internally, this uses [`IDBDatabase::transaction`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/transaction).
95    // For more details of what will happen if one does not await:
96    // - If the `Closure` from `transaction_request` is not dropped yet, then the error will be
97    //   explicitly ignored, and thus transaction will commit.
98    // - If the `Closure` from `transaction_request` has already been dropped, then the callback
99    //   will panic. Most likely this will lead to the transaction aborting, but this is an
100    //   untested and unsupported code path.
101    pub async fn run<Fun, RetFut, Ret>(self, transaction: Fun) -> crate::Result<Ret, Err>
102    where
103        Fun: 'static + FnOnce(Transaction<Err>) -> RetFut,
104        RetFut: 'static + Future<Output = crate::Result<Ret, Err>>,
105        Ret: 'static,
106        Err: 'static,
107    {
108        let t = self
109            .db
110            .transaction_with_str_sequence_and_mode(&self.stores, self.mode)
111            .map_err(|err| match error_name!(&err) {
112                Some("InvalidStateError") => crate::Error::DatabaseIsClosed,
113                Some("NotFoundError") => crate::Error::DoesNotExist,
114                Some("InvalidAccessError") => crate::Error::InvalidArgument,
115                _ => crate::Error::from_js_value(err),
116            })?;
117        let (tx, rx) = futures_channel::oneshot::channel();
118        let fut = {
119            let t = t.clone();
120            async move {
121                let res = transaction(Transaction::from_sys(t.clone())).await;
122                let return_value = match &res {
123                    Ok(_) => Ok(()),
124                    Err(_) => Err(()),
125                };
126                if let Err(_) = tx.send(res) {
127                    // Transaction was cancelled by being dropped, abort it
128                    let _ = t.abort();
129                }
130                return_value
131            }
132        };
133        unsafe_jar::run(t, fut);
134        let res = rx.await;
135        if unsafe_jar::POLLED_FORBIDDEN_THING.get() {
136            panic!("Transaction blocked without any request under way");
137        }
138        res.expect("Transaction never completed")
139    }
140}
141
142pub(crate) async fn transaction_request(req: IdbRequest) -> Result<JsValue, JsValue> {
143    // TODO: remove these oneshot-channel in favor of a custom-made atomiccell-based channel.
144    // the custom-made channel will not call the waker (because we're handling wakes another way),
145    // which'll allow using a panicking context again.
146    let (success_tx, success_rx) = oneshot::channel();
147    let (error_tx, error_rx) = oneshot::channel();
148
149    // Keep the callbacks alive until execution completed
150    let _callbacks = unsafe_jar::add_request(req, success_tx, error_tx);
151
152    let res = match future::select(success_rx, error_rx).await {
153        Either::Left((res, _)) => Ok(res.unwrap()),
154        Either::Right((res, _)) => Err(res.unwrap()),
155    };
156
157    res.map_err(|evt| err_from_event(evt).into()).map(|evt| {
158        evt.target()
159            .expect("Trying to parse indexed_db::Error from an event that has no target")
160            .dyn_into::<web_sys::IdbRequest>()
161            .expect(
162                "Trying to parse indexed_db::Error from an event that is not from an IDBRequest",
163            )
164            .result()
165            .expect("Failed retrieving the result of successful IDBRequest")
166    })
167}