diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 404cba9..fd3f87d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: Test on: - workflow_call: { } + workflow_call: {} push: branches: - master @@ -104,7 +104,7 @@ jobs: include: - toolchain: nightly-2025-05-05 os: ubuntu-latest - - toolchain: 1.75.0 + - toolchain: 1.85.0 os: ubuntu-latest - toolchain: stable os: ubuntu-latest @@ -120,7 +120,7 @@ jobs: fail-fast: false matrix: flags: - - '' + - "" - --features cursors - --features dates - --features indices diff --git a/Cargo.toml b/Cargo.toml index 33f2ed4..a71db38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ name = "indexed_db_futures" version = "0.6.4" authors = ["Arturas Molcanovas "] edition = "2021" -rust-version = "1.75.0" +rust-version = "1.85.0" license = "MIT" description = "Future bindings for IndexedDB via web_sys" repository = "https://github.com/Alorel/rust-indexed-db" diff --git a/internal_macros/src/generic_bounds.rs b/internal_macros/src/generic_bounds.rs index cf2d0e3..a2e646d 100644 --- a/internal_macros/src/generic_bounds.rs +++ b/internal_macros/src/generic_bounds.rs @@ -1,7 +1,5 @@ use crate::commons::FnTarget; use crate::TokenStream1; -use macroific::prelude::*; -use proc_macro2::Ident; use quote::ToTokens; use syn::{parse_quote, Error, WherePredicate}; @@ -22,7 +20,7 @@ macro_rules! make_opts { /// /// | Option | Type | /// |--------|-----------| - $($(#[doc = concat!(" | `", stringify!($extra_opt), "` | `", stringify!($extra_ty), "` |")])+)+ + $($(#[doc = concat!(" | `", stringify!($extra_opt), "` | `", stringify!($extra_ty), "` |")])+)* #[derive(::macroific::attr_parse::AttributeOptions)] pub(super) struct $struct_name { $($($opt: ::syn::punctuated::Punctuated,)+)+ @@ -53,10 +51,8 @@ make_opts!(Opts => { db_name|index_name|store_name|key_path => ::core::convert::AsRef, db_version => crate::factory::DBVersion, blocked_cb => ::core::ops::FnOnce(crate::database::VersionChangeEvent) -> crate::Result<()> + 'static, - upgrade_cb => ::core::ops::FnOnce(crate::database::VersionChangeEvent, crate::database::Database) -> crate::Result<()> + 'static, - [custom] => { - upgrade_async_cb => UpgradeAsyncCb, - }, + upgrade_cb => ::core::ops::FnOnce(crate::database::VersionChangeEvent, &crate::transaction::Transaction<'_>) -> crate::Result<()> + 'static, + upgrade_async_cb => ::core::ops::AsyncFnOnce(crate::database::VersionChangeEvent, &crate::transaction::Transaction<'_>) -> crate::Result<()> + 'static, }); #[inline] @@ -73,31 +69,6 @@ pub(super) fn exec(spec: TokenStream1, target: TokenStream1) -> TokenStream1 { } } -#[derive(ParseOption)] -struct UpgradeAsyncCb { - #[attr_opts(default = false)] - fun: Ident, - - #[attr_opts(default = false)] - fut: Ident, -} - -impl UpgradeAsyncCb { - fn extend_target(self, target: &mut FnTarget) { - let Self { fun, fut } = self; - let wheres = [ - parse_quote!(#fun: ::core::ops::FnOnce(crate::database::VersionChangeEvent, crate::database::Database) -> #fut + 'static), - parse_quote!(#fut: ::core::future::Future> + 'static), - ]; - - target - .generics_mut() - .make_where_clause() - .predicates - .extend::<[WherePredicate; 2]>(wheres); - } -} - fn on_err(mut target: TokenStream1, e: Error) -> TokenStream1 { let e: TokenStream1 = e.into_compile_error().into(); target.extend(e); diff --git a/src/error/unexpected_data.rs b/src/error/unexpected_data.rs index 49da5e3..16fb24b 100644 --- a/src/error/unexpected_data.rs +++ b/src/error/unexpected_data.rs @@ -22,6 +22,10 @@ pub enum UnexpectedDataError { #[error("`Future` polled unexpectedly.")] PollState, + /// Expected a Transaction to exist, but it was not found. + #[error("Expected the Transaction to exist, but it was not found.")] + TransactionNotFound, + /// Expected a Transaction to be aborted, but it was committed. #[error("Expected the Transaction to be aborted, but it was committed.")] TransactionCommitted, diff --git a/src/factory/req_builder.rs b/src/factory/req_builder.rs index 57f5343..eedf7fc 100644 --- a/src/factory/req_builder.rs +++ b/src/factory/req_builder.rs @@ -102,9 +102,9 @@ impl OpenDbRequestBuilder { /// Set the [upgradeneeded](https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/upgradeneeded_event) /// event handler that returns a `Future`. - #[generic_bounds(upgrade_async_cb(fun(U2), fut(U2Fut)))] + #[generic_bounds(upgrade_async_cb(U2))] #[cfg(feature = "async-upgrade")] - pub fn with_on_upgrade_needed_fut( + pub fn with_on_upgrade_needed_fut( self, on_upgrade_needed: U2, ) -> OpenDbRequestBuilder { diff --git a/src/future/open_db/listener.rs b/src/future/open_db/listener.rs index a5ea5cb..63567ce 100644 --- a/src/future/open_db/listener.rs +++ b/src/future/open_db/listener.rs @@ -1,5 +1,6 @@ use crate::database::{Database, VersionChangeEvent}; use crate::error::{Error, UnexpectedDataError}; +use crate::transaction::{OnTransactionDrop, Transaction}; use internal_macros::generic_bounds; use std::fmt::{Debug, Display, Formatter}; use std::mem; @@ -56,9 +57,16 @@ impl OpenDbListener { #[cfg(feature = "async-upgrade")] async_notify: Self::fake_rx(), listener: Closure::once(move |evt: web_sys::IdbVersionChangeEvent| { - let res = Database::from_event(&evt) - .and_then(move |db| callback(VersionChangeEvent::new(evt), db)); - + let res = Database::from_event(&evt).and_then(|db| { + Transaction::from_raw_version_change_event(&db, &evt).and_then(|mut tx| { + callback(VersionChangeEvent::new(evt), &tx).inspect(|_| { + // If the callback succeeded, we want to ensure that + // the transaction is committed when dropped and not + // aborted. + tx.on_drop(OnTransactionDrop::Commit); + }) + }) + }); Self::handle_result(LBL_UPGRADE, &status, res) }), } @@ -154,8 +162,8 @@ const _: () = { tokio::sync::mpsc::unbounded_channel().1 } - #[generic_bounds(upgrade_async_cb(fun(Fn), fut(Fut)))] - pub(crate) fn new_upgrade_fut(callback: Fn) -> Self { + #[generic_bounds(upgrade_async_cb(Fn))] + pub(crate) fn new_upgrade_fut(callback: Fn) -> Self { let status = Status::new(); let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); Self { @@ -168,11 +176,21 @@ const _: () = { }; Self::set_status(&status, Status::Pending, LBL_UPGRADE)?; - let fut = callback(VersionChangeEvent::new(evt), db); - wasm_bindgen_futures::spawn_local(async move { - let result = match fut.await { - Ok(()) => Status::Ok, + let db = db; + let result = match Transaction::from_raw_version_change_event(&db, &evt) { + Ok(mut transaction) => { + match callback(VersionChangeEvent::new(evt), &transaction).await { + Ok(_) => { + // If the callback succeeded, we want to ensure that + // the transaction is committed when dropped and not + // aborted. + transaction.on_drop(OnTransactionDrop::Commit); + Status::Ok + } + Err(e) => Status::Err(e), + } + } Err(e) => Status::Err(e), }; let _ = Self::set_status(&status, result, LBL_UPGRADE); diff --git a/src/transaction.rs b/src/transaction.rs index 3ce7ae8..5def5c1 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,7 +1,7 @@ //! An [`IDBTransaction`](https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction) implementation. use crate::database::Database; -use crate::error::Error; +use crate::error::{Error, SimpleValueError, UnexpectedDataError}; use crate::internal_utils::{StructName, SystemRepr}; pub use base::TransactionRef; use listeners::TxListeners; @@ -10,6 +10,7 @@ pub use options::{TransactionDurability, TransactionOptions}; use std::fmt::{Debug, Formatter}; use std::ops::Deref; pub(crate) use tx_sys::TransactionSys; +use wasm_bindgen::JsCast; pub use web_sys::IdbTransactionMode as TransactionMode; mod base; @@ -35,6 +36,27 @@ pub struct Transaction<'a> { listeners: TxListeners<'a>, done: bool, + on_drop: OnTransactionDrop, +} + +/// An enum representing the possible behavior which a [`Transaction`] may exhibit +/// when it is dropped. +/// +/// Note that unlike JavaScript's [`IDBTransaction`][1], this crate's [`Transaction`] +/// defaults to aborting - i.e., [`OnTransactionDrop::Abort`] - instead of +/// committing - i.e., [`OnTransactionDrop::Commit`] - the transaction! +/// +/// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction +#[derive(Debug, Copy, Clone)] +pub enum OnTransactionDrop { + /// Abort the [`Transaction`] when it is dropped. This is the default + /// behavior of [`Transaction`]. + Abort, + /// Commit the [`Transaction`] when it is dropped. This is the default + /// behavior of an [`IDBTransaction`][1] in JavaScript. + /// + /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction + Commit, } /// A [transaction's](Transaction) result. @@ -65,9 +87,35 @@ impl<'a> Transaction<'a> { Self { listeners: TxListeners::new(db, inner), done: false, + on_drop: OnTransactionDrop::Abort, } } + /// Create a [`Transaction`] from an [`web_sys::IdbVersionChangeEvent`]. + /// + /// This is useful for extracting the transaction being used to upgrade + /// the database. + pub(crate) fn from_raw_version_change_event( + db: &'a Database, + event: &web_sys::IdbVersionChangeEvent, + ) -> crate::Result { + let inner = match event.target() { + Some(target) => match target.dyn_ref::() { + Some(req) => req + .transaction() + .ok_or(Error::from(UnexpectedDataError::TransactionNotFound)), + None => Err(SimpleValueError::DynCast(target.unchecked_into()).into()), + }, + None => Err(UnexpectedDataError::NoEventTarget.into()), + }?; + Ok(Self::new(db, inner)) + } + + /// Set the behavior for when the [`Transaction`] is dropped + pub fn on_drop(&mut self, on_drop: OnTransactionDrop) { + self.on_drop = on_drop; + } + /// Rolls back all the changes to objects in the database associated with this transaction. /// /// # Browser compatibility note @@ -115,7 +163,10 @@ impl Drop for Transaction<'_> { self.listeners.free_listeners(); if !self.done { - let _ = self.as_sys().abort(); + let _ = match self.on_drop { + OnTransactionDrop::Abort => self.as_sys().abort(), + OnTransactionDrop::Commit => self.as_sys().do_commit(), + }; } } } @@ -126,6 +177,7 @@ impl Debug for Transaction<'_> { .field("transaction", self.as_sys()) .field("db", self.db()) .field("done", &self.done) + .field("on_drop", &self.on_drop) .finish() } } diff --git a/tests/tests/database/delete_obj_store.rs b/tests/tests/database/delete_obj_store.rs index 8660ba1..7eb2a79 100644 --- a/tests/tests/database/delete_obj_store.rs +++ b/tests/tests/database/delete_obj_store.rs @@ -12,7 +12,8 @@ pub async fn invalid_state_error() { #[wasm_bindgen_test] pub async fn not_found_error() { let err = Database::open(random_str()) - .with_on_upgrade_needed(move |_, db| { + .with_on_upgrade_needed(move |_, tx| { + let db = tx.db(); db.delete_object_store(&db.name())?; Ok(()) }) @@ -28,7 +29,8 @@ pub async fn happy_path() { let n1_clone = n1.clone(); let db = Database::open(&n1) - .with_on_upgrade_needed(move |_, db| { + .with_on_upgrade_needed(move |_, tx| { + let db = tx.db(); db.create_object_store(&n1_clone).build()?; db.create_object_store(&random_str()) .build()? diff --git a/tests/tests/database/obj_store_create.rs b/tests/tests/database/obj_store_create.rs index 7667bfe..f090ecf 100644 --- a/tests/tests/database/obj_store_create.rs +++ b/tests/tests/database/obj_store_create.rs @@ -12,7 +12,8 @@ pub async fn happy_path() { #[wasm_bindgen_test] pub async fn constraint_error() { let err = Database::open(random_str()) - .with_on_upgrade_needed(move |_, db| { + .with_on_upgrade_needed(move |_, tx| { + let db = tx.db(); let name = random_str(); db.create_object_store(&name).build()?; db.create_object_store(&name).build()?; @@ -27,7 +28,8 @@ pub async fn constraint_error() { #[wasm_bindgen_test] pub async fn invalid_access_error() { let err = Database::open(random_str()) - .with_on_upgrade_needed(move |_, db| { + .with_on_upgrade_needed(move |_, tx| { + let db = tx.db(); db.create_object_store(&db.name()) .with_auto_increment(true) .with_key_path("".into()) diff --git a/tests/tests/example_reproductions.rs b/tests/tests/example_reproductions.rs index 1d45b9f..9488fff 100644 --- a/tests/tests/example_reproductions.rs +++ b/tests/tests/example_reproductions.rs @@ -28,7 +28,8 @@ pub async fn multi_threaded_executor() { } let db = Database::open("my_db_multi_threaded_executor") - .with_on_upgrade_needed(|_, db| { + .with_on_upgrade_needed(|_, tx| { + let db = tx.db(); db.create_object_store("my_store") .with_auto_increment(true) .build()?; @@ -50,47 +51,46 @@ pub async fn opening_a_database_and_making_some_schema_changes() { let _ = Database::open("opening_a_database_and_making_some_schema_changes") .with_version(2u8) .with_on_blocked(|_| Ok(())) - .with_on_upgrade_needed_fut(|event, db| { + .with_on_upgrade_needed_fut(async |event, tx| { + let db = tx.db(); // Convert versions from floats to integers to allow using them in match expressions let old_version = event.old_version() as u64; let new_version = event.new_version().map(|v| v as u64); - async move { - match (old_version, new_version) { - (0, Some(1)) => { - db.create_object_store("my_store") - .with_auto_increment(true) - .build()?; + match (old_version, new_version) { + (0, Some(1)) => { + db.create_object_store("my_store") + .with_auto_increment(true) + .build()?; + } + (prev, Some(2)) => { + if prev == 1 { + db.delete_object_store("my_store")?; } - (prev, Some(2)) => { - if prev == 1 { - db.delete_object_store("my_store")?; - } - // Create an object store and await its transaction before inserting data. - db.create_object_store("my_other_store") - .with_auto_increment(true) - .build()? - .transaction() - .on_done()? - .await - .into_result()?; - - //- Start a new transaction & add some data - let tx = db - .transaction("my_other_store") - .with_mode(TransactionMode::Readwrite) - .build()?; - let store = tx.object_store("my_other_store")?; - store.add("foo").await?; - store.add("bar").await?; - tx.commit().await?; - } - _ => {} + // Create an object store and await its transaction before inserting data. + db.create_object_store("my_other_store") + .with_auto_increment(true) + .build()? + .transaction() + .on_done()? + .await + .into_result()?; + + //- Start a new transaction & add some data + let tx = db + .transaction("my_other_store") + .with_mode(TransactionMode::Readwrite) + .build()?; + let store = tx.object_store("my_other_store")?; + store.add("foo").await?; + store.add("bar").await?; + tx.commit().await?; } - - Ok(()) + _ => {} } + + Ok(()) }) .await .expect("Error opening DB"); @@ -111,7 +111,8 @@ pub async fn rw_serde() { } let db = Database::open("example_rw_serde") - .with_on_upgrade_needed(|_, db| { + .with_on_upgrade_needed(|_, tx| { + let db = tx.db(); db.create_object_store("users") .with_key_path("id".into()) .build()?; @@ -159,7 +160,8 @@ pub async fn readme_example() { async fn main() -> indexed_db_futures::OpenDbResult<()> { let db = Database::open("my_db_readme_example") .with_version(2u8) - .with_on_upgrade_needed(|event, db| { + .with_on_upgrade_needed(|event, tx| { + let db = tx.db(); // Convert versions from floats to integers to allow using them in match expressions let old_version = event.old_version() as u64; let new_version = event.new_version().map(|v| v as u64); @@ -262,7 +264,8 @@ pub async fn iterating_a_cursor() { let db = Database::open("example_iterating_a_cursor") .with_version(2u8) - .with_on_upgrade_needed(|_, db| { + .with_on_upgrade_needed(|_, tx| { + let db = tx.db(); db.create_object_store("my_store").build()?; Ok(()) }) @@ -329,7 +332,8 @@ pub async fn iterating_index_as_a_stream() { } let db = Database::open("example_iterating_index_as_a_stream") - .with_on_upgrade_needed(|_, db| { + .with_on_upgrade_needed(|_, tx| { + let db = tx.db(); let store = db .create_object_store("my_store") .with_key_path("id".into()) diff --git a/tests/tests/index/create.rs b/tests/tests/index/create.rs index 4690a48..6818f1c 100644 --- a/tests/tests/index/create.rs +++ b/tests/tests/index/create.rs @@ -4,7 +4,8 @@ use idb_fut::database::Database; #[wasm_bindgen_test] pub async fn constraint_error() { let err = Database::open(random_str()) - .with_on_upgrade_needed(move |_, db| { + .with_on_upgrade_needed(move |_, tx| { + let db = tx.db(); let name = db.name(); let store = db .create_object_store(&name) diff --git a/tests/tests/object_store/add_put.rs b/tests/tests/object_store/add_put.rs index 1c26e43..7e43947 100644 --- a/tests/tests/object_store/add_put.rs +++ b/tests/tests/object_store/add_put.rs @@ -11,7 +11,8 @@ macro_rules! common_tests { #[wasm_bindgen_test] pub async fn data_error_inline_key() { - let db = random_db_with_init(move |_, db| { + let db = random_db_with_init(move |_, tx| { + let db = tx.db(); db.create_object_store(&db.name()) .with_key_path("foo".into()) .build()?; @@ -64,7 +65,8 @@ macro_rules! common_tests { #[cfg(feature = "serde")] #[wasm_bindgen_test] pub async fn serde_object_nesting() { - let db = random_db_with_init(move |_, db| { + let db = random_db_with_init(move |_, tx| { + let db = tx.db(); db.create_object_store(&db.name()) .with_key_path("foo".into()) .build()?; diff --git a/tests/tests/object_store/query_source/key_path.rs b/tests/tests/object_store/query_source/key_path.rs index 9522485..12a699d 100644 --- a/tests/tests/object_store/query_source/key_path.rs +++ b/tests/tests/object_store/query_source/key_path.rs @@ -5,7 +5,8 @@ use idb_fut::KeyPath; #[wasm_bindgen_test] pub async fn auto_incremented() { let db = Database::open(random_str()) - .with_on_upgrade_needed(move |_, db| { + .with_on_upgrade_needed(move |_, tx| { + let db = tx.db(); db.create_object_store(&db.name()) .with_auto_increment(true) .build()?; @@ -21,7 +22,8 @@ pub async fn auto_incremented() { #[wasm_bindgen_test] pub async fn none() { let db = Database::open(random_str()) - .with_on_upgrade_needed(move |_, db| { + .with_on_upgrade_needed(move |_, tx| { + let db = tx.db(); db.create_object_store(&db.name()).build()?; Ok(()) }) @@ -35,7 +37,8 @@ pub async fn none() { #[wasm_bindgen_test] pub async fn explicit() { let db = Database::open(random_str()) - .with_on_upgrade_needed(move |_, db| { + .with_on_upgrade_needed(move |_, tx| { + let db = tx.db(); db.create_object_store(&db.name()) .with_key_path(Key::KEY_PATH) .build()?; diff --git a/tests/tests/transaction/mod.rs b/tests/tests/transaction/mod.rs index 54afa41..6e1868e 100644 --- a/tests/tests/transaction/mod.rs +++ b/tests/tests/transaction/mod.rs @@ -7,7 +7,8 @@ pub mod on_done; #[wasm_bindgen_test] pub async fn multi_store() { - let db = random_db_with_init(move |_, db| { + let db = random_db_with_init(move |_, tx| { + let db = tx.db(); db.create_object_store("s1").build()?; db.create_object_store("s2").build()?; Ok(()) diff --git a/tests/tests/transaction/on_done.rs b/tests/tests/transaction/on_done.rs index febb2e8..b6aefc0 100644 --- a/tests/tests/transaction/on_done.rs +++ b/tests/tests/transaction/on_done.rs @@ -40,7 +40,8 @@ pub mod async_upgrade { let err = Database::open(random_str()) .with_version(2u8) - .with_on_upgrade_needed_fut(move |_, db| async move { + .with_on_upgrade_needed_fut(async |_, tx| { + let db = tx.db(); // Create an object store and await its transaction db.create_object_store(STORE_NAME) .with_auto_increment(true) @@ -74,7 +75,8 @@ pub mod async_upgrade { .with_version(2u8) .with_on_upgrade_needed_fut({ let events = Arc::clone(&events); - move |_, db| async move { + async move |_, tx| { + let db = tx.db(); events.lock().unwrap().push(Event::CallbackStart); // Create an object store and await its transaction diff --git a/tests/tests/utils/init.rs b/tests/tests/utils/init.rs index 4601248..849c9ec 100644 --- a/tests/tests/utils/init.rs +++ b/tests/tests/utils/init.rs @@ -1,9 +1,10 @@ use crate::prelude::*; use idb_fut::database::{Database, VersionChangeEvent}; +use indexed_db_futures::transaction::Transaction; pub async fn random_db_with_init(on_upgrade_needed: F) -> Database where - F: Fn(VersionChangeEvent, Database) -> idb_fut::Result<()> + 'static, + F: Fn(VersionChangeEvent, &Transaction<'_>) -> idb_fut::Result<()> + 'static, { Database::open(random_str()) .with_on_upgrade_needed(on_upgrade_needed) @@ -13,7 +14,8 @@ where /// Crate a DB with and an object store with a matching name and default params. pub async fn random_db_with_store() -> Database { - random_db_with_init(move |_, db| { + random_db_with_init(move |_, tx| { + let db = tx.db(); db.create_object_store(&db.name()).build()?; Ok(()) }) @@ -22,7 +24,8 @@ pub async fn random_db_with_store() -> Database { /// Create a random DB and a store with a matching name that expect [`KeyVal`] inputs. pub async fn random_db_keyval() -> Database { - random_db_with_init(move |_, db| { + random_db_with_init(move |_, tx| { + let db = tx.db(); db.create_object_store(&db.name()) .with_auto_increment(false) .with_key_path(Key::KEY_PATH) @@ -35,7 +38,8 @@ pub async fn random_db_keyval() -> Database { /// [`random_db_keyval`] + an index with default params. #[cfg(feature = "indices")] pub async fn random_db_idx_keyval() -> Database { - random_db_with_init(move |_, db| { + random_db_with_init(move |_, tx| { + let db = tx.db(); let name = db.name(); let store = db .create_object_store(&name)