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