├── .envrc ├── .gitignore ├── .cargo └── config.toml ├── tests ├── browser.rs ├── worker.rs ├── browser_panic.rs ├── worker_panic.rs ├── common_panic │ └── mod.rs └── common │ └── mod.rs ├── nix ├── default.nix ├── sources.json └── sources.nix ├── Justfile ├── shell.nix ├── src ├── lib.rs ├── error.rs ├── transaction │ ├── unsafe_jar.rs │ └── runner.rs ├── database.rs ├── transaction.rs ├── index.rs ├── utils.rs ├── cursor.rs ├── factory.rs └── object_store.rs ├── .github └── workflows │ ├── ci.yml │ └── release-plz.yml ├── LICENSE-MIT ├── Cargo.toml ├── examples └── basic.rs ├── README.md └── LICENSE-APACHE /.envrc: -------------------------------------------------------------------------------- 1 | eval "$(lorri direnv)" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | runner = "wasm-bindgen-test-runner" 3 | -------------------------------------------------------------------------------- /tests/browser.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 2 | 3 | mod common; 4 | -------------------------------------------------------------------------------- /tests/worker.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_worker); 2 | 3 | mod common; 4 | -------------------------------------------------------------------------------- /tests/browser_panic.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 2 | 3 | mod common_panic; 4 | -------------------------------------------------------------------------------- /tests/worker_panic.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_worker); 2 | 3 | mod common_panic; 4 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | 2 | let 3 | sources = import ./sources.nix; 4 | in 5 | import sources.nixpkgs { 6 | overlays = [ 7 | (import (sources.fenix + "/overlay.nix")) 8 | ]; 9 | } 10 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | all: fmt doc test 2 | 3 | fmt: 4 | cargo fmt 5 | 6 | doc: 7 | cargo doc --target wasm32-unknown-unknown 8 | 9 | test: test-crate run-example 10 | 11 | test-crate: 12 | cargo test --target wasm32-unknown-unknown 13 | 14 | run-example: 15 | cargo run --target wasm32-unknown-unknown --example basic 16 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import ./nix; 3 | in 4 | pkgs.stdenv.mkDerivation { 5 | name = "indexed-db-rs"; 6 | buildInputs = ( 7 | (with pkgs; [ 8 | cargo-bolero 9 | cargo-nextest 10 | #chromedriver 11 | #chromium 12 | firefox 13 | geckodriver 14 | just 15 | niv 16 | wasm-bindgen-cli_0_2_100 17 | wasm-pack 18 | 19 | (fenix.combine (with fenix; [ 20 | minimal.cargo 21 | minimal.rustc 22 | complete.rust-src 23 | rust-analyzer 24 | targets.wasm32-unknown-unknown.latest.rust-std 25 | ])) 26 | ]) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![warn(missing_docs)] 3 | 4 | // Internal helper 5 | macro_rules! error_name { 6 | ($v:expr) => { 7 | crate::error::name($v).as_ref().map(|s| s as &str) 8 | }; 9 | } 10 | 11 | mod cursor; 12 | mod database; 13 | mod error; 14 | mod factory; 15 | mod index; 16 | mod object_store; 17 | mod transaction; 18 | mod utils; 19 | 20 | pub use cursor::{Cursor, CursorBuilder, CursorDirection}; 21 | pub use database::{Database, OwnedDatabase}; 22 | pub use error::{Error, Result}; 23 | pub use factory::{Factory, ObjectStoreBuilder, VersionChangeEvent}; 24 | pub use index::Index; 25 | pub use object_store::{IndexBuilder, ObjectStore}; 26 | pub use transaction::{Transaction, TransactionBuilder}; 27 | 28 | const POLLED_FORBIDDEN_THING_PANIC: &str = "Transaction blocked without any request under way. 29 | The developer probably called .await on something that is not an indexed-db-provided future inside a transaction. 30 | This would lead the transaction to be committed due to IndexedDB semantics."; 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | just-test: 11 | name: Tests 12 | runs-on: ubuntu-latest 13 | steps: 14 | # Checkout the repository 15 | - uses: actions/checkout@v4 16 | 17 | # Install cargo dependencies 18 | - uses: baptiste0928/cargo-install@21a18ba3bf4a184d1804e8b759930d3471b1c941 19 | with: 20 | crate: wasm-bindgen-cli 21 | 22 | # Setup cargo cache 23 | - uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43 24 | 25 | # Setup chromedriver 26 | - uses: nanasess/setup-chromedriver@480d644e773cd6d53e4cb76557c8ad5e5806d7da 27 | - run: sudo Xvfb -ac :0 -screen 0 1280x1024x24 > /dev/null 2>&1 & 28 | 29 | # Install wasm32-unknown-unknown nightly 30 | - run: rustup target add wasm32-unknown-unknown 31 | 32 | # Run the tests 33 | - run: cargo test --target wasm32-unknown-unknown 34 | - run: cargo run --example basic --target wasm32-unknown-unknown 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Leo Gaspard 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "fenix": { 3 | "branch": "main", 4 | "description": "Rust toolchains and rust analyzer nightly for nix [maintainer=@figsoda]", 5 | "homepage": "", 6 | "owner": "nix-community", 7 | "repo": "fenix", 8 | "rev": "edf7d9e431cda8782e729253835f178a356d3aab", 9 | "sha256": "184gsn263sc6642kpyfmwrrgixxc2ff6aggxmi7mgiw9wwicnn0g", 10 | "type": "tarball", 11 | "url": "https://github.com/nix-community/fenix/archive/edf7d9e431cda8782e729253835f178a356d3aab.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | }, 14 | "nixpkgs": { 15 | "branch": "nixpkgs-unstable", 16 | "description": "Nix Packages collection", 17 | "homepage": "", 18 | "owner": "NixOS", 19 | "repo": "nixpkgs", 20 | "rev": "3a05eebede89661660945da1f151959900903b6a", 21 | "sha256": "0n56l6v5k3lmrr4vjnp6xk1s46shkwdkvai05dzcbcabpl29yb9g", 22 | "type": "tarball", 23 | "url": "https://github.com/NixOS/nixpkgs/archive/3a05eebede89661660945da1f151959900903b6a.tar.gz", 24 | "url_template": "https://github.com///archive/.tar.gz" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "indexed-db" 3 | version = "0.5.0-alpha.1" 4 | edition = "2021" 5 | readme = "README.md" 6 | documentation = "https://docs.rs/indexed-db" 7 | description = "Bindings to IndexedDB that default the transactions to aborting and can work multi-threaded" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Ekleog/indexed-db" 10 | keywords = ["wasm", "indexeddb", "async", "web", "webassembly"] 11 | categories = ["asynchronous", "database", "wasm", "web-programming"] 12 | rust-version = "1.85" 13 | 14 | [dependencies] 15 | futures-channel = "0.3.30" 16 | futures-util = "0.3.30" 17 | pin-project-lite = "0.2.13" 18 | scoped-tls = "1.0" 19 | thiserror = "2.0" 20 | web-sys = { version = "0.3.66", features = [ 21 | "DomException", 22 | "DomStringList", 23 | "Event", 24 | "IdbCursor", 25 | "IdbCursorDirection", 26 | "IdbCursorWithValue", 27 | "IdbDatabase", 28 | "IdbFactory", 29 | "IdbIndex", 30 | "IdbIndexParameters", 31 | "IdbKeyRange", 32 | "IdbObjectStore", 33 | "IdbObjectStoreParameters", 34 | "IdbOpenDbRequest", 35 | "IdbTransaction", 36 | "IdbTransactionMode", 37 | "IdbVersionChangeEvent", 38 | "Window", 39 | "WorkerGlobalScope", 40 | ] } 41 | 42 | [dev-dependencies] 43 | anyhow = "1.0" 44 | console_error_panic_hook = "0.1.7" 45 | futures = "0.3.30" 46 | tracing = "0.1.40" 47 | tracing-wasm = "0.2.1" 48 | wasm-bindgen-test = "=0.3.50" 49 | web-sys = { version = "0.3.66", features = ["Performance"] } 50 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | # Release unpublished packages. 14 | release-plz-release: 15 | name: Release-Plz Release 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Install Rust toolchain 25 | uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 26 | with: 27 | toolchain: stable 28 | - name: Run release-plz 29 | uses: release-plz/action@8724d33cd97b8295051102e2e19ca592962238f5 30 | with: 31 | command: release 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 35 | 36 | # Create a PR with the new versions and changelog, preparing the next release. 37 | release-plz-pr: 38 | name: Release-Plz PR 39 | runs-on: ubuntu-latest 40 | permissions: 41 | contents: write 42 | pull-requests: write 43 | concurrency: 44 | group: release-plz-${{ github.ref }} 45 | cancel-in-progress: false 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v4 49 | with: 50 | fetch-depth: 0 51 | - name: Install Rust toolchain 52 | uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 53 | with: 54 | toolchain: stable 55 | - name: Run release-plz 56 | uses: release-plz/action@8724d33cd97b8295051102e2e19ca592962238f5 57 | with: 58 | command: release-pr 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 62 | -------------------------------------------------------------------------------- /tests/common_panic/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use indexed_db::Factory; 3 | use wasm_bindgen_test::wasm_bindgen_test; 4 | use web_sys::js_sys::JsString; 5 | 6 | #[wasm_bindgen_test] 7 | #[should_panic] // For some reason the error message is not detected here, but appears clearly with console_error_panic_hook 8 | async fn other_awaits_panic() { 9 | // tracing_wasm::set_as_global_default(); 10 | // std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 11 | 12 | let factory = Factory::get().unwrap(); 13 | 14 | let db = factory 15 | .open::<()>("baz", 1, async move |evt| { 16 | evt.build_object_store("data").auto_increment().create()?; 17 | Ok(()) 18 | }) 19 | .await 20 | .unwrap(); 21 | 22 | let (tx, rx) = futures_channel::oneshot::channel(); 23 | 24 | db.transaction(&["data"]) 25 | .rw() 26 | .run::<_, anyhow::Error>(async move |t| { 27 | t.object_store("data")?.add(&JsString::from("foo")).await?; 28 | rx.await.context("awaiting for something external")?; 29 | t.object_store("data")?.add(&JsString::from("bar")).await?; 30 | Ok(()) 31 | }) 32 | .await 33 | .unwrap(); 34 | 35 | tx.send(()).unwrap(); 36 | } 37 | 38 | #[wasm_bindgen_test] 39 | #[should_panic] // For some reason the error message is not detected here, but appears clearly with console_error_panic_hook 40 | async fn await_in_versionchange_panics() { 41 | // tracing_wasm::set_as_global_default(); 42 | // std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 43 | 44 | let factory = Factory::get().unwrap(); 45 | 46 | let (tx, rx) = futures_channel::oneshot::channel(); 47 | 48 | factory 49 | .open::("baz", 1, async move |evt| { 50 | evt.build_object_store("data").auto_increment().create()?; 51 | rx.await.context("awaiting for something external")?; 52 | Ok(()) 53 | }) 54 | .await 55 | .unwrap(); 56 | 57 | tx.send(()).unwrap(); 58 | } 59 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use anyhow::Context; 4 | use indexed_db::Factory; 5 | use web_sys::js_sys::JsString; 6 | 7 | async fn example() -> anyhow::Result<()> { 8 | // Obtain the database builder 9 | let factory = Factory::get().context("opening IndexedDB")?; 10 | 11 | // Open the database, creating it if needed 12 | let db = factory 13 | .open::("database", 1, async move |evt| { 14 | let store = evt.build_object_store("store").auto_increment().create()?; 15 | 16 | // You can also add objects from this callback 17 | store.add(&JsString::from("foo")).await?; 18 | 19 | Ok(()) 20 | }) 21 | .await 22 | .context("creating the 'database' IndexedDB")?; 23 | 24 | // In a transaction, add two records 25 | db.transaction(&["store"]) 26 | .rw() 27 | .run::<_, Infallible>(async move |t| { 28 | let store = t.object_store("store")?; 29 | store.add(&JsString::from("bar")).await?; 30 | store.add(&JsString::from("baz")).await?; 31 | Ok(()) 32 | }) 33 | .await?; 34 | 35 | // In another transaction, read the first record 36 | db.transaction(&["store"]) 37 | .run::<_, std::io::Error>(async move |t| { 38 | let data = t.object_store("store")?.get_all(Some(1)).await?; 39 | if data.len() != 1 { 40 | Err(std::io::Error::new( 41 | std::io::ErrorKind::Other, 42 | "Unexpected data length", 43 | ))?; 44 | } 45 | Ok(()) 46 | }) 47 | .await?; 48 | 49 | // If we return `Err` (or panic) from a transaction, then it will abort 50 | db.transaction(&["store"]) 51 | .rw() 52 | .run::<_, std::io::Error>(async move |t| { 53 | let store = t.object_store("store")?; 54 | store.add(&JsString::from("quux")).await?; 55 | if store.count().await? > 3 { 56 | // Oops! In this example, we have 4 items by this point 57 | Err(std::io::Error::new( 58 | std::io::ErrorKind::Other, 59 | "Too many objects in store", 60 | ))?; 61 | } 62 | Ok(()) 63 | }) 64 | .await 65 | .unwrap_err(); 66 | 67 | // And no write will have happened 68 | db.transaction(&["store"]) 69 | .run::<_, Infallible>(async move |t| { 70 | let num_items = t.object_store("store")?.count().await?; 71 | assert_eq!(num_items, 3); 72 | Ok(()) 73 | }) 74 | .await?; 75 | 76 | // More complex example: using cursors to iterate over a store 77 | db.transaction(&["store"]) 78 | .run::<_, Infallible>(async move |t| { 79 | let mut all_items = Vec::new(); 80 | let mut cursor = t.object_store("store")?.cursor().open().await?; 81 | while let Some(value) = cursor.value() { 82 | all_items.push(value); 83 | cursor.advance(1).await?; 84 | } 85 | assert_eq!(all_items.len(), 3); 86 | assert_eq!(all_items[0], **JsString::from("foo")); 87 | Ok(()) 88 | }) 89 | .await?; 90 | 91 | Ok(()) 92 | } 93 | 94 | use wasm_bindgen_test::*; 95 | wasm_bindgen_test_configure!(run_in_browser); 96 | #[wasm_bindgen_test] 97 | async fn test() { 98 | example().await.unwrap() 99 | } 100 | 101 | fn main() {} 102 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::err_from_event; 2 | use web_sys::{ 3 | wasm_bindgen::{JsCast, JsValue}, 4 | DomException, 5 | }; 6 | 7 | /// Type alias for convenience 8 | pub type Result = std::result::Result>; 9 | 10 | /// Error type for all errors from this crate 11 | /// 12 | /// The `E` generic argument is used for user-defined error types, eg. when 13 | /// the user provides a callback. 14 | #[derive(Clone, Debug, thiserror::Error)] 15 | #[non_exhaustive] 16 | pub enum Error { 17 | /// Not running in a browser window 18 | #[error("Not running in a browser window")] 19 | NotInBrowser, 20 | 21 | /// IndexedDB is disabled 22 | #[error("IndexedDB is disabled")] 23 | IndexedDbDisabled, 24 | 25 | /// Operation is not supported by the browser 26 | #[error("Operation is not supported by the browser")] 27 | OperationNotSupported, 28 | 29 | /// Operation is not allowed by the user agent 30 | #[error("Operation is not allowed by the user agent")] 31 | OperationNotAllowed, 32 | 33 | /// Provided key is not valid 34 | #[error("Provided key is not valid")] 35 | InvalidKey, 36 | 37 | /// Version must not be zero 38 | #[error("Version must not be zero")] 39 | VersionMustNotBeZero, 40 | 41 | /// Requested version is older than existing version 42 | #[error("Requested version is older than existing version")] 43 | VersionTooOld, 44 | 45 | /// The requested function cannot be called from this context 46 | #[error("The requested function cannot be called from this context")] 47 | InvalidCall, 48 | 49 | /// The provided arguments are invalid 50 | #[error("The provided arguments are invalid")] 51 | InvalidArgument, 52 | 53 | /// Cannot create something that already exists 54 | #[error("Cannot create something that already exists")] 55 | AlreadyExists, 56 | 57 | /// Cannot change something that does not exists 58 | #[error("Cannot change something that does not exists")] 59 | DoesNotExist, 60 | 61 | /// Database is closed 62 | #[error("Database is closed")] 63 | DatabaseIsClosed, 64 | 65 | /// Object store was removed 66 | #[error("Object store was removed")] 67 | ObjectStoreWasRemoved, 68 | 69 | /// Transaction is read-only 70 | #[error("Transaction is read-only")] 71 | ReadOnly, 72 | 73 | /// Unable to clone 74 | #[error("Unable to clone")] 75 | FailedClone, 76 | 77 | /// Invalid range 78 | #[error("Invalid range")] 79 | InvalidRange, 80 | 81 | /// Cursor finished its range 82 | #[error("Cursor finished its range")] 83 | CursorCompleted, 84 | 85 | /// User-provided error to pass through `indexed-db` code 86 | #[error(transparent)] 87 | User(#[from] E), 88 | } 89 | 90 | impl Error { 91 | pub(crate) fn from_dom_exception(err: DomException) -> Error { 92 | match &err.name() as &str { 93 | "NotSupportedError" => crate::Error::OperationNotSupported, 94 | "NotAllowedError" => crate::Error::OperationNotAllowed, 95 | "VersionError" => crate::Error::VersionTooOld, 96 | _ => panic!("Unexpected error: {err:?}"), 97 | } 98 | } 99 | 100 | pub(crate) fn from_js_value(v: JsValue) -> Error { 101 | let err = v 102 | .dyn_into::() 103 | .expect("Trying to parse indexed_db::Error from value that is not a DomException"); 104 | Error::from_dom_exception(err) 105 | } 106 | 107 | pub(crate) fn from_js_event(evt: web_sys::Event) -> Error { 108 | Error::from_dom_exception(err_from_event(evt)) 109 | } 110 | } 111 | 112 | pub(crate) fn name(v: &JsValue) -> Option { 113 | v.dyn_ref::().map(|v| v.name()) 114 | } 115 | -------------------------------------------------------------------------------- /src/transaction/unsafe_jar.rs: -------------------------------------------------------------------------------- 1 | //! This module holds all the `unsafe` implementation details of `transaction`. 2 | //! 3 | //! The API exposed from here is entirely safe, and this module's code should be properly audited. 4 | 5 | use std::{ 6 | cell::{Cell, OnceCell}, 7 | panic::AssertUnwindSafe, 8 | rc::{Rc, Weak}, 9 | }; 10 | 11 | use futures_util::FutureExt as _; 12 | 13 | use super::{runner::poll_it, RunnableTransaction}; 14 | 15 | struct DropFlag(Rc>); 16 | 17 | impl Drop for DropFlag { 18 | fn drop(&mut self) { 19 | self.0.set(true); 20 | } 21 | } 22 | 23 | pub struct ScopeCallback { 24 | state: Rc>>>, 25 | _dropped: DropFlag, 26 | maker: Box RunnableTransaction<'static>>, 27 | } 28 | 29 | impl ScopeCallback { 30 | pub fn run(self, args: Args) { 31 | let made_state = Rc::new((self.maker)(args)); 32 | let _ = self.state.set(Rc::downgrade(&made_state)); 33 | poll_it(&made_state); 34 | } 35 | } 36 | 37 | /// Panics and aborts the whole process if the transaction is not dropped before the end of `scope` 38 | pub async fn extend_lifetime_to_scope_and_run<'scope, MakerArgs, ScopeRet>( 39 | maker: Box RunnableTransaction<'scope>>, 40 | scope: impl 'scope + AsyncFnOnce(ScopeCallback) -> ScopeRet, 41 | ) -> ScopeRet { 42 | // SAFETY: We're extending the lifetime of `maker` as well as its return value to `'static`. 43 | // This is safe because the `RunnableTransaction` is not stored anywhere else, and it will be dropped 44 | // before the end of the enclosing `extend_lifetime_to_scope_and_run` call, at the `Weak::strong_count` check. 45 | // If it is not, we'll panic and abort the whole process. 46 | // `'scope` is also guaranteed to outlive `extend_lifetime_to_scope_and_run`. 47 | // Finally, `maker` itself is guaranteed to not escape `'scope` because it can only be consumed by `run`, 48 | // and the `ScopeCallback` itself is guaranteed to not escape `'scope` thanks to the check on `dropped`. 49 | let maker: Box RunnableTransaction<'static>> = 50 | unsafe { std::mem::transmute(maker) }; 51 | 52 | let state = Rc::new(OnceCell::new()); 53 | let dropped = Rc::new(Cell::new(false)); 54 | let callback = ScopeCallback { 55 | state: state.clone(), 56 | _dropped: DropFlag(dropped.clone()), 57 | maker, 58 | }; 59 | let result = AssertUnwindSafe((scope)(callback)).catch_unwind().await; 60 | if !dropped.get() { 61 | let _ = std::panic::catch_unwind(|| { 62 | panic!("Bug in the indexed-db crate: the ScopeCallback was not consumed before the end of its logical lifetime") 63 | }); 64 | std::process::abort(); 65 | } 66 | if let Some(state) = state.get() { 67 | if Weak::strong_count(&state) != 0 { 68 | // Make sure that regardless of what the user could be doing, if we're overextending the lifetime we'll panic and abort 69 | // 70 | // Note: we know this won't spuriously hit because: 71 | // - we're using `Rc`, so every `RunnableTransaction` operation is single-thread anyway 72 | // - when the scope completes, `finished_rx` will have resolved 73 | // - if `finished_tx` has been written to, it means that the `RunnableTransaction` has been dropped 74 | // Point 2 is enforced outside of the unsafe jar, but it's fine considering it will only result in a spurious panic/abort 75 | let _ = std::panic::catch_unwind(|| { 76 | panic!("Bug in the indexed-db crate: the transaction was not dropped before the end of its lifetime") 77 | }); 78 | std::process::abort(); 79 | } 80 | } 81 | match result { 82 | Ok(result) => result, 83 | Err(err) => std::panic::resume_unwind(err), 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use crate::transaction::TransactionBuilder; 2 | use web_sys::IdbDatabase; 3 | 4 | /// Wrapper for [`IDBDatabase`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase) 5 | /// 6 | /// Note that dropping this wrapper automatically calls [`IDBDatabase::close`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/close) 7 | /// to request the underlying database connection to be closed (the actual database close 8 | /// occuring asynchronously with no way for the client to identify when this happens). 9 | #[derive(Debug)] 10 | pub struct OwnedDatabase { 11 | /// This field only switches to `None` to prevent database close on drop when 12 | /// `OwnedDatabase::into_manual_close` is used. 13 | db: Option, 14 | } 15 | 16 | impl OwnedDatabase { 17 | pub(crate) fn make_auto_close(db: Database) -> OwnedDatabase { 18 | OwnedDatabase { db: Some(db) } 19 | } 20 | 21 | /// Convert this into a [`Database`] that does not automatically close the connection when dropped. 22 | /// 23 | /// The resulting [`Database`] is `Clone` without requiring reference-counting, which can be more convenient than refcounting [`OwnedDatabase`] 24 | pub fn into_manual_close(mut self) -> Database { 25 | self.db.take().expect("Database already taken") 26 | } 27 | 28 | /// Explicitly closes this database connection 29 | /// 30 | /// Calling this method is strictly equivalent to dropping the [`OwnedDatabase`] instance. 31 | /// This method is only provided for symmetry with [`Database::close`]. 32 | pub fn close(self) { 33 | // `self` is dropped here 34 | } 35 | } 36 | 37 | impl std::ops::Deref for OwnedDatabase { 38 | type Target = Database; 39 | 40 | fn deref(&self) -> &Self::Target { 41 | self.db.as_ref().expect("Database already taken") 42 | } 43 | } 44 | 45 | impl Drop for OwnedDatabase { 46 | fn drop(&mut self) { 47 | match self.db.take() { 48 | Some(db) => db.close(), 49 | None => {} // Database was taken with `into_manual_close` 50 | } 51 | } 52 | } 53 | 54 | /// Wrapper for [`IDBDatabase`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase) 55 | /// 56 | /// Unlike[``OwnedDatabase`], this does not automatically close the database connection when dropped. 57 | /// 58 | /// Note that failing to close the database connection prior to dropping this wrapper will let the connection 59 | /// remain open until the Javascript garbage collector kicks in, which typically can take tens of seconds 60 | /// during which any new attempt to e.g. delete or open with upgrade the database will hang. 61 | #[derive(Debug)] 62 | pub struct Database { 63 | sys: IdbDatabase, 64 | } 65 | 66 | impl Database { 67 | pub(crate) fn from_sys(sys: IdbDatabase) -> Database { 68 | Database { sys } 69 | } 70 | 71 | pub(crate) fn as_sys(&self) -> &IdbDatabase { 72 | &self.sys 73 | } 74 | 75 | /// The name of this database 76 | /// 77 | /// Internally, this uses [`IDBDatabase::name`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/name). 78 | pub fn name(&self) -> String { 79 | self.sys.name() 80 | } 81 | 82 | /// The version of this database, clamped at `u32::MAX` 83 | /// 84 | /// Internally, this uses [`IDBDatabase::version`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/version). 85 | pub fn version(&self) -> u32 { 86 | self.sys.version() as u32 87 | } 88 | 89 | /// The names of all [`ObjectStore`]s in this [`Database`] 90 | /// 91 | /// Internally, this uses [`IDBDatabase::objectStoreNames`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/objectStoreNames). 92 | pub fn object_store_names(&self) -> Vec { 93 | let names = self.sys.object_store_names(); 94 | let len = names.length(); 95 | let mut res = Vec::with_capacity(usize::try_from(len).unwrap()); 96 | for i in 0..len { 97 | res.push( 98 | names 99 | .get(i) 100 | .expect("DOMStringList did not contain as many elements as its length"), 101 | ); 102 | } 103 | res 104 | } 105 | 106 | /// Run a transaction 107 | /// 108 | /// This will open the object stores identified by `stores`. See the methods of [`TransactionBuilder`] 109 | /// for more details about how transactions actually happen. 110 | pub fn transaction(&self, stores: &[&str]) -> TransactionBuilder { 111 | TransactionBuilder::from_names(self.sys.clone(), stores) 112 | } 113 | 114 | /// Closes this database connection 115 | /// 116 | /// Note that the closing will actually happen asynchronously with no way for the client to 117 | /// identify when the database was closed. 118 | /// 119 | /// Internally, this uses [`IDBDatabase::close`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/close). 120 | pub fn close(&self) { 121 | self.sys.close(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # indexed-db 2 | 3 | Bindings for IndexedDB, that default transactions to aborting and can work multi-threaded. 4 | 5 | ## Why yet another IndexedDB crate? 6 | 7 | As of the time of my writing this crate, the alternatives have the default IndexedDB behavior of transaction committing. This is because IndexedDB transactions have strange committing semantics: they commit as soon as the application returns to the event loop without an ongoing request. 8 | 9 | This crate forces your transactions to respect the IndexedDB requirements, so as to make it possible to abort transactions upon errors, rather than having them auto-commit. 10 | 11 | Incidentally, this crate, at the time of publishing version 0.4.0, is the only IndexedDB crate that works fine under the multi-threaded executor of `wasm-bindgen`. You can find all the details in [this thread](https://github.com/rustwasm/wasm-bindgen/issues/3798). 12 | 13 | ## Error handling 14 | 15 | This crate uses an `Error` type. The `Err` generic argument is present on basically all the structs exposed by this crate. It is the type of users in code surrounding `indexed-db` usage, for convenience. 16 | 17 | In particular, if you ever want to recover one of your own errors (of type `Err`) that went through `indexed-db` code, you should just match the error with `Error::User(_)`, and you will be able to recover your own error details. 18 | 19 | On the other hand, when one of your callbacks wants to return an error of your own type through `indexed-db`, it can just use the `From for Error` implementation. This is done automatically by the `?` operator, or can be done manually for explicit returns with `return Err(e.into());`. 20 | 21 | ## Example 22 | 23 | ```rust 24 | use std::convert::Infallible; 25 | 26 | use anyhow::Context; 27 | use indexed_db::Factory; 28 | use web_sys::js_sys::JsString; 29 | 30 | async fn example() -> anyhow::Result<()> { 31 | // Obtain the database builder 32 | let factory = Factory::get().context("opening IndexedDB")?; 33 | 34 | // Open the database, creating it if needed 35 | let db = factory 36 | .open::("database", 1, async move |evt| { 37 | let store = evt.build_object_store("store").auto_increment().create()?; 38 | 39 | // You can also add objects from this callback 40 | store.add(&JsString::from("foo")).await?; 41 | 42 | Ok(()) 43 | }) 44 | .await 45 | .context("creating the 'database' IndexedDB")?; 46 | 47 | // In a transaction, add two records 48 | db.transaction(&["store"]) 49 | .rw() 50 | .run::<_, Infallible>(async move |t| { 51 | let store = t.object_store("store")?; 52 | store.add(&JsString::from("bar")).await?; 53 | store.add(&JsString::from("baz")).await?; 54 | Ok(()) 55 | }) 56 | .await?; 57 | 58 | // In another transaction, read the first record 59 | db.transaction(&["store"]) 60 | .run::<_, std::io::Error>(async move |t| { 61 | let data = t.object_store("store")?.get_all(Some(1)).await?; 62 | if data.len() != 1 { 63 | Err(std::io::Error::new( 64 | std::io::ErrorKind::Other, 65 | "Unexpected data length", 66 | ))?; 67 | } 68 | Ok(()) 69 | }) 70 | .await?; 71 | 72 | // If we return `Err` (or panic) from a transaction, then it will abort 73 | db.transaction(&["store"]) 74 | .rw() 75 | .run::<_, std::io::Error>(async move |t| { 76 | let store = t.object_store("store")?; 77 | store.add(&JsString::from("quux")).await?; 78 | if store.count().await? > 3 { 79 | // Oops! In this example, we have 4 items by this point 80 | Err(std::io::Error::new( 81 | std::io::ErrorKind::Other, 82 | "Too many objects in store", 83 | ))?; 84 | } 85 | Ok(()) 86 | }) 87 | .await 88 | .unwrap_err(); 89 | 90 | // And no write will have happened 91 | db.transaction(&["store"]) 92 | .run::<_, Infallible>(async move |t| { 93 | let num_items = t.object_store("store")?.count().await?; 94 | assert_eq!(num_items, 3); 95 | Ok(()) 96 | }) 97 | .await?; 98 | 99 | // More complex example: using cursors to iterate over a store 100 | db.transaction(&["store"]) 101 | .run::<_, Infallible>(async move |t| { 102 | let mut all_items = Vec::new(); 103 | let mut cursor = t.object_store("store")?.cursor().open().await?; 104 | while let Some(value) = cursor.value() { 105 | all_items.push(value); 106 | cursor.advance(1).await?; 107 | } 108 | assert_eq!(all_items.len(), 3); 109 | assert_eq!(all_items[0], **JsString::from("foo")); 110 | Ok(()) 111 | }) 112 | .await?; 113 | 114 | Ok(()) 115 | } 116 | ``` 117 | -------------------------------------------------------------------------------- /src/transaction/runner.rs: -------------------------------------------------------------------------------- 1 | //! All the required to run a transaction 2 | 3 | use std::{ 4 | cell::{Cell, RefCell}, 5 | future::Future, 6 | pin::Pin, 7 | rc::Rc, 8 | task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, 9 | }; 10 | 11 | use futures_channel::oneshot; 12 | use scoped_tls::scoped_thread_local; 13 | use web_sys::{ 14 | js_sys::Function, 15 | wasm_bindgen::{closure::Closure, JsCast as _}, 16 | IdbRequest, IdbTransaction, 17 | }; 18 | 19 | pub enum TransactionResult { 20 | PolledForbiddenThing, 21 | Done(R), 22 | } 23 | 24 | pub struct RunnableTransaction<'f> { 25 | transaction: IdbTransaction, 26 | inflight_requests: Cell, 27 | future: RefCell>>>, 28 | polled_forbidden_thing: Box, 29 | finished: RefCell>>, 30 | } 31 | 32 | impl<'f> RunnableTransaction<'f> { 33 | pub fn new( 34 | transaction: IdbTransaction, 35 | transaction_contents: impl 'f + Future>, 36 | result: &'f RefCell>>>, 37 | finished: oneshot::Sender<()>, 38 | ) -> RunnableTransaction<'f> 39 | where 40 | R: 'f, 41 | E: 'f, 42 | { 43 | RunnableTransaction { 44 | transaction: transaction.clone(), 45 | inflight_requests: Cell::new(0), 46 | future: RefCell::new(Box::pin(async move { 47 | let transaction_result = transaction_contents.await; 48 | if transaction_result.is_err() { 49 | // The transaction failed. We should abort it. 50 | let _ = transaction.abort(); 51 | } 52 | assert!( 53 | result 54 | .replace(Some(TransactionResult::Done(transaction_result))) 55 | .is_none(), 56 | "Transaction completed multiple times", 57 | ); 58 | })), 59 | polled_forbidden_thing: Box::new(move || { 60 | *result.borrow_mut() = Some(TransactionResult::PolledForbiddenThing); 61 | }), 62 | finished: RefCell::new(Some(finished)), 63 | } 64 | } 65 | } 66 | 67 | fn panic_waker() -> Waker { 68 | fn clone(_: *const ()) -> RawWaker { 69 | RawWaker::new( 70 | std::ptr::null(), 71 | &RawWakerVTable::new(clone, wake, wake, drop), 72 | ) 73 | } 74 | fn wake(_: *const ()) { 75 | panic!("IndexedDB transaction tried to await on something other than a request") 76 | } 77 | fn drop(_: *const ()) {} 78 | unsafe { 79 | Waker::new( 80 | std::ptr::null(), 81 | &RawWakerVTable::new(clone, wake, wake, drop), 82 | ) 83 | } 84 | } 85 | 86 | scoped_thread_local!(static CURRENT: Rc>); 87 | 88 | pub fn poll_it(state: &Rc>) { 89 | CURRENT.set(&state, || { 90 | // Poll once, in order to run the transaction until its next await on a request 91 | let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 92 | state 93 | .future 94 | .borrow_mut() 95 | .as_mut() 96 | .poll(&mut Context::from_waker(&panic_waker())) 97 | })); 98 | 99 | // Try catching the panic and aborting. This currently does not work in wasm due to panic=abort, but will 100 | // hopefully work some day. The transaction _should_ auto-abort if the wasm module aborts, so hopefully we're 101 | // fine around there. 102 | let res = match res { 103 | Ok(res) => res, 104 | Err(err) => { 105 | // The poll panicked, abort the transaction 106 | let _ = state.transaction.abort(); 107 | std::panic::resume_unwind(err); 108 | } 109 | }; 110 | 111 | // Finally, check the poll result 112 | match res { 113 | Poll::Pending => { 114 | // Still some work to do. Is there at least one request in flight? 115 | if state.inflight_requests.get() == 0 { 116 | // Returned `Pending` despite no request being inflight. This means there was 117 | // an `await` on something other than transaction requests. Abort in order to 118 | // avoid the default auto-commit behavior. 119 | let _ = state.transaction.abort(); 120 | let _ = (state.polled_forbidden_thing)(); 121 | } 122 | } 123 | Poll::Ready(()) => { 124 | // Everything went well! Just signal that we're done 125 | let finished = state 126 | .finished 127 | .borrow_mut() 128 | .take() 129 | .expect("Transaction finished multiple times"); 130 | if finished.send(()).is_err() { 131 | // Transaction aborted by not awaiting on it 132 | let _ = state.transaction.abort(); 133 | return; 134 | } 135 | } 136 | } 137 | }); 138 | } 139 | 140 | pub fn add_request( 141 | req: IdbRequest, 142 | result: &Rc>>>, 143 | ) -> impl Sized { 144 | CURRENT.with(move |state| { 145 | state 146 | .inflight_requests 147 | .set(state.inflight_requests.get() + 1); 148 | 149 | let on_success = Closure::once({ 150 | let state = state.clone(); 151 | let result = result.clone(); 152 | move |evt: web_sys::Event| { 153 | state 154 | .inflight_requests 155 | .set(state.inflight_requests.get() - 1); 156 | assert!(result.replace(Some(Ok(evt))).is_none()); 157 | poll_it(&state); 158 | } 159 | }); 160 | 161 | let on_error = Closure::once({ 162 | let state = state.clone(); 163 | let result = result.clone(); 164 | move |evt: web_sys::Event| { 165 | evt.prevent_default(); // Do not abort the transaction, we're dealing with it ourselves 166 | state 167 | .inflight_requests 168 | .set(state.inflight_requests.get() - 1); 169 | assert!(result.replace(Some(Err(evt))).is_none()); 170 | poll_it(&state); 171 | } 172 | }); 173 | 174 | req.set_onsuccess(Some(&on_success.as_ref().dyn_ref::().unwrap())); 175 | req.set_onerror(Some(&on_error.as_ref().dyn_ref::().unwrap())); 176 | 177 | // Keep the callbacks alive until they're no longer needed 178 | (on_success, on_error) 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /src/transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | utils::{err_from_event, str_slice_to_array}, 3 | ObjectStore, 4 | }; 5 | use std::{ 6 | cell::RefCell, 7 | future::Future, 8 | marker::PhantomData, 9 | pin::Pin, 10 | rc::Rc, 11 | task::{Context, Poll}, 12 | }; 13 | use web_sys::{ 14 | wasm_bindgen::{JsCast, JsValue}, 15 | IdbDatabase, IdbRequest, IdbTransaction, IdbTransactionMode, 16 | }; 17 | 18 | mod runner; 19 | pub(crate) mod unsafe_jar; 20 | 21 | pub use runner::{RunnableTransaction, TransactionResult}; 22 | 23 | /// Wrapper for [`IDBTransaction`](https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction) 24 | #[derive(Debug)] 25 | pub struct Transaction { 26 | sys: IdbTransaction, 27 | _phantom: PhantomData, 28 | } 29 | 30 | impl Transaction { 31 | pub(crate) fn from_sys(sys: IdbTransaction) -> Transaction { 32 | Transaction { 33 | sys, 34 | _phantom: PhantomData, 35 | } 36 | } 37 | 38 | pub(crate) fn as_sys(&self) -> &IdbTransaction { 39 | &self.sys 40 | } 41 | 42 | /// Returns an [`ObjectStore`] that can be used to operate on data in this transaction 43 | /// 44 | /// Internally, this uses [`IDBTransaction::objectStore`](https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/objectStore). 45 | pub fn object_store(&self, name: &str) -> crate::Result, Err> { 46 | Ok(ObjectStore::from_sys(self.sys.object_store(name).map_err( 47 | |err| match error_name!(&err) { 48 | Some("NotFoundError") => crate::Error::DoesNotExist, 49 | _ => crate::Error::from_js_value(err), 50 | }, 51 | )?)) 52 | } 53 | } 54 | 55 | /// Helper to build a transaction 56 | pub struct TransactionBuilder { 57 | db: IdbDatabase, 58 | stores: JsValue, 59 | mode: IdbTransactionMode, 60 | // TODO: add support for transaction durability when web-sys gets it 61 | } 62 | 63 | impl TransactionBuilder { 64 | pub(crate) fn from_names(db: IdbDatabase, names: &[&str]) -> TransactionBuilder { 65 | TransactionBuilder { 66 | db, 67 | stores: str_slice_to_array(names).into(), 68 | mode: IdbTransactionMode::Readonly, 69 | } 70 | } 71 | 72 | /// Allow writes in this transaction 73 | /// 74 | /// Without this, the transaction will only be allowed reads, and will error upon trying to 75 | /// write objects. 76 | pub fn rw(mut self) -> Self { 77 | self.mode = IdbTransactionMode::Readwrite; 78 | self 79 | } 80 | 81 | /// Actually execute the transaction 82 | /// 83 | /// The `transaction` argument defines what will be run in the transaction. Note that due to 84 | /// limitations of the IndexedDb API, the future returned by `transaction` cannot call `.await` 85 | /// on any future except the ones provided by the [`Transaction`] itself. This function will 86 | /// do its best to detect these cases to abort the transaction and panic, but you should avoid 87 | /// doing so anyway. Note also that these errors are not recoverable: even if wasm32 were not 88 | /// having `panic=abort`, once there is such a panic no `indexed-db` functions will work any 89 | /// longer. 90 | /// 91 | /// If `transaction` returns an `Ok` value, then the transaction will be committed. If it 92 | /// returns an `Err` value, then it will be aborted. 93 | /// 94 | /// Note that you should avoid sending requests that you do not await. If you do, it is hard 95 | /// to say whether the transaction will commit or abort, due to both the IndexedDB and the 96 | /// `wasm-bindgen` semantics. 97 | /// 98 | /// Note that transactions cannot be nested. 99 | /// 100 | /// Internally, this uses [`IDBDatabase::transaction`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/transaction). 101 | // For more details of what will happen if one does not await: 102 | // - If the `Closure` from `transaction_request` is not dropped yet, then the error will be 103 | // explicitly ignored, and thus transaction will commit. 104 | // - If the `Closure` from `transaction_request` has already been dropped, then the callback 105 | // will panic. Most likely this will lead to the transaction aborting, but this is an 106 | // untested and unsupported code path. 107 | pub async fn run( 108 | self, 109 | transaction: impl AsyncFnOnce(Transaction) -> crate::Result, 110 | ) -> crate::Result { 111 | let t = self 112 | .db 113 | .transaction_with_str_sequence_and_mode(&self.stores, self.mode) 114 | .map_err(|err| match error_name!(&err) { 115 | Some("InvalidStateError") => crate::Error::DatabaseIsClosed, 116 | Some("NotFoundError") => crate::Error::DoesNotExist, 117 | Some("InvalidAccessError") => crate::Error::InvalidArgument, 118 | _ => crate::Error::from_js_value(err), 119 | })?; 120 | let result = RefCell::new(None); 121 | let result = &result; 122 | let (finished_tx, finished_rx) = futures_channel::oneshot::channel(); 123 | unsafe_jar::extend_lifetime_to_scope_and_run( 124 | Box::new(move |()| { 125 | RunnableTransaction::new( 126 | t.clone(), 127 | transaction(Transaction::from_sys(t)), 128 | result, 129 | finished_tx, 130 | ) 131 | }), 132 | async move |s| { 133 | s.run(()); 134 | let _ = finished_rx.await; 135 | let result = result 136 | .borrow_mut() 137 | .take() 138 | .expect("Transaction finished without setting result"); 139 | match result { 140 | TransactionResult::PolledForbiddenThing => { 141 | panic!("{}", crate::POLLED_FORBIDDEN_THING_PANIC) 142 | } 143 | TransactionResult::Done(r) => r, 144 | } 145 | }, 146 | ) 147 | .await 148 | } 149 | } 150 | 151 | struct FakeFuture<'a, T> { 152 | watching: &'a RefCell>, 153 | } 154 | 155 | impl<'a, T> FakeFuture<'a, T> { 156 | fn new(watching: &'a RefCell>) -> FakeFuture<'a, T> { 157 | FakeFuture { watching } 158 | } 159 | } 160 | 161 | impl<'a, T> Future for FakeFuture<'a, T> { 162 | type Output = T; 163 | 164 | fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { 165 | // Don't do this at home! This only works thanks to our unsafe jar polling regardless of the waker 166 | match self.watching.borrow_mut().take() { 167 | None => Poll::Pending, 168 | Some(r) => Poll::Ready(r), 169 | } 170 | } 171 | } 172 | 173 | pub(crate) async fn transaction_request(req: IdbRequest) -> Result { 174 | let result = Rc::new(RefCell::new(None)); 175 | 176 | // Keep the callbacks alive until execution completed 177 | let _callbacks = runner::add_request(req, &result); 178 | 179 | match FakeFuture::new(&result).await { 180 | Ok(evt) => { 181 | let result = evt.target() 182 | .expect("Trying to parse indexed_db::Error from an event that has no target") 183 | .dyn_into::() 184 | .expect("Trying to parse indexed_db::Error from an event that is not from an IDBRequest") 185 | .result() 186 | .expect("Failed retrieving the result of successful IDBRequest"); 187 | Ok(result) 188 | } 189 | Err(evt) => Err(err_from_event(evt).into()), 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: name: spec: 10 | let 11 | name' = sanitizeName name + "-src"; 12 | in 13 | if spec.builtin or true then 14 | builtins_fetchurl { inherit (spec) url sha256; name = name'; } 15 | else 16 | pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; 17 | 18 | fetch_tarball = pkgs: name: spec: 19 | let 20 | name' = sanitizeName name + "-src"; 21 | in 22 | if spec.builtin or true then 23 | builtins_fetchTarball { name = name'; inherit (spec) url sha256; } 24 | else 25 | pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; 26 | 27 | fetch_git = name: spec: 28 | let 29 | ref = 30 | spec.ref or ( 31 | if spec ? branch then "refs/heads/${spec.branch}" else 32 | if spec ? tag then "refs/tags/${spec.tag}" else 33 | abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" 34 | ); 35 | submodules = spec.submodules or false; 36 | submoduleArg = 37 | let 38 | nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; 39 | emptyArgWithWarning = 40 | if submodules 41 | then 42 | builtins.trace 43 | ( 44 | "The niv input \"${name}\" uses submodules " 45 | + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " 46 | + "does not support them" 47 | ) 48 | { } 49 | else { }; 50 | in 51 | if nixSupportsSubmodules 52 | then { inherit submodules; } 53 | else emptyArgWithWarning; 54 | in 55 | builtins.fetchGit 56 | ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); 57 | 58 | fetch_local = spec: spec.path; 59 | 60 | fetch_builtin-tarball = name: throw 61 | ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. 62 | $ niv modify ${name} -a type=tarball -a builtin=true''; 63 | 64 | fetch_builtin-url = name: throw 65 | ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. 66 | $ niv modify ${name} -a type=file -a builtin=true''; 67 | 68 | # 69 | # Various helpers 70 | # 71 | 72 | # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 73 | sanitizeName = name: 74 | ( 75 | concatMapStrings (s: if builtins.isList s then "-" else s) 76 | ( 77 | builtins.split "[^[:alnum:]+._?=-]+" 78 | ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) 79 | ) 80 | ); 81 | 82 | # The set of packages used when specs are fetched using non-builtins. 83 | mkPkgs = sources: system: 84 | let 85 | sourcesNixpkgs = 86 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; 87 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 88 | hasThisAsNixpkgsPath = == ./.; 89 | in 90 | if builtins.hasAttr "nixpkgs" sources 91 | then sourcesNixpkgs 92 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 93 | import { } 94 | else 95 | abort 96 | '' 97 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 98 | add a package called "nixpkgs" to your sources.json. 99 | ''; 100 | 101 | # The actual fetching function. 102 | fetch = pkgs: name: spec: 103 | 104 | if ! builtins.hasAttr "type" spec then 105 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 106 | else if spec.type == "file" then fetch_file pkgs name spec 107 | else if spec.type == "tarball" then fetch_tarball pkgs name spec 108 | else if spec.type == "git" then fetch_git name spec 109 | else if spec.type == "local" then fetch_local spec 110 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball name 111 | else if spec.type == "builtin-url" then fetch_builtin-url name 112 | else 113 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 114 | 115 | # If the environment variable NIV_OVERRIDE_${name} is set, then use 116 | # the path directly as opposed to the fetched source. 117 | replace = name: drv: 118 | let 119 | saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; 120 | ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; 121 | in 122 | if ersatz == "" then drv else 123 | # this turns the string into an actual Nix path (for both absolute and 124 | # relative paths) 125 | if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; 126 | 127 | # Ports of functions for older nix versions 128 | 129 | # a Nix version of mapAttrs if the built-in doesn't exist 130 | mapAttrs = builtins.mapAttrs or ( 131 | f: set: with builtins; 132 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 133 | ); 134 | 135 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 136 | range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); 137 | 138 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 139 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 140 | 141 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 142 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 143 | concatMapStrings = f: list: concatStrings (map f list); 144 | concatStrings = builtins.concatStringsSep ""; 145 | 146 | # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 147 | optionalAttrs = cond: as: if cond then as else { }; 148 | 149 | # fetchTarball version that is compatible between all the versions of Nix 150 | builtins_fetchTarball = { url, name ? null, sha256 }@attrs: 151 | let 152 | inherit (builtins) lessThan nixVersion fetchTarball; 153 | in 154 | if lessThan nixVersion "1.12" then 155 | fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) 156 | else 157 | fetchTarball attrs; 158 | 159 | # fetchurl version that is compatible between all the versions of Nix 160 | builtins_fetchurl = { url, name ? null, sha256 }@attrs: 161 | let 162 | inherit (builtins) lessThan nixVersion fetchurl; 163 | in 164 | if lessThan nixVersion "1.12" then 165 | fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) 166 | else 167 | fetchurl attrs; 168 | 169 | # Create the final "sources" from the config 170 | mkSources = config: 171 | mapAttrs 172 | ( 173 | name: spec: 174 | if builtins.hasAttr "outPath" spec 175 | then 176 | abort 177 | "The values in sources.json should not have an 'outPath' attribute" 178 | else 179 | spec // { outPath = replace name (fetch config.pkgs name spec); } 180 | ) 181 | config.sources; 182 | 183 | # The "config" used by the fetchers 184 | mkConfig = 185 | { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null 186 | , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) 187 | , system ? builtins.currentSystem 188 | , pkgs ? mkPkgs sources system 189 | }: rec { 190 | # The sources, i.e. the attribute set of spec name to spec 191 | inherit sources; 192 | 193 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 194 | inherit pkgs; 195 | }; 196 | 197 | in 198 | mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } 199 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | transaction::transaction_request, 3 | utils::{ 4 | array_to_vec, make_key_range, map_count_err, map_count_res, map_get_err, none_if_undefined, 5 | }, 6 | CursorBuilder, 7 | }; 8 | use futures_util::future::{Either, FutureExt}; 9 | use std::{future::Future, marker::PhantomData, ops::RangeBounds}; 10 | use web_sys::{wasm_bindgen::JsValue, IdbIndex}; 11 | 12 | #[cfg(doc)] 13 | use crate::Cursor; 14 | 15 | /// Wrapper for [`IDBIndex`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex), 16 | /// for use in transactions 17 | /// 18 | /// Most of the functions here take a [`JsValue`] as the key(s) to use in the index. If the index was 19 | /// built with a compound key, then you should use eg. `js_sys::Array::from_iter([key_1, key_2])` as 20 | /// the key. 21 | pub struct Index { 22 | sys: IdbIndex, 23 | _phantom: PhantomData, 24 | } 25 | 26 | impl Index { 27 | pub(crate) fn from_sys(sys: IdbIndex) -> Index { 28 | Index { 29 | sys, 30 | _phantom: PhantomData, 31 | } 32 | } 33 | 34 | /// Checks whether the provided key (for this index) already exists 35 | /// 36 | /// Internally, this uses [`IDBIndex::count`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/count). 37 | pub fn contains(&self, key: &JsValue) -> impl Future> { 38 | match self.sys.count_with_key(key) { 39 | Ok(count_req) => Either::Right( 40 | transaction_request(count_req) 41 | .map(|res| res.map_err(map_count_err).map(|n| map_count_res(n) != 0)), 42 | ), 43 | Err(e) => Either::Left(std::future::ready(Err(map_count_err(e)))), 44 | } 45 | } 46 | 47 | /// Count all the keys (for this index) in the provided range 48 | /// 49 | /// Internally, this uses [`IDBIndex::count`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/count). 50 | pub fn count_in( 51 | &self, 52 | range: impl RangeBounds, 53 | ) -> impl Future> { 54 | let range = match make_key_range(range) { 55 | Ok(range) => range, 56 | Err(e) => return Either::Left(std::future::ready(Err(e))), 57 | }; 58 | match self.sys.count_with_key(&range) { 59 | Ok(count_req) => Either::Right( 60 | transaction_request(count_req) 61 | .map(|res| res.map_err(map_count_err).map(map_count_res)), 62 | ), 63 | Err(e) => Either::Left(std::future::ready(Err(map_count_err(e)))), 64 | } 65 | } 66 | 67 | /// Get the object with key `key` for this index 68 | /// 69 | /// Internally, this uses [`IDBIndex::get`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/get). 70 | pub fn get(&self, key: &JsValue) -> impl Future, Err>> { 71 | match self.sys.get(key) { 72 | Ok(get_req) => Either::Right( 73 | transaction_request(get_req) 74 | .map(|res| res.map_err(map_get_err).map(none_if_undefined)), 75 | ), 76 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 77 | } 78 | } 79 | 80 | /// Get the first value with a key (for this index) in `range`, ordered by key (for this index) 81 | /// 82 | /// Note that the unbounded range is not a valid range for IndexedDB. 83 | /// 84 | /// Internally, this uses [`IDBIndex::get`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/get). 85 | pub fn get_first_in( 86 | &self, 87 | range: impl RangeBounds, 88 | ) -> impl Future, Err>> { 89 | let range = match make_key_range(range) { 90 | Ok(range) => range, 91 | Err(e) => return Either::Left(std::future::ready(Err(e))), 92 | }; 93 | match self.sys.get(&range) { 94 | Ok(get_req) => Either::Right( 95 | transaction_request(get_req) 96 | .map(|res| res.map_err(map_get_err).map(none_if_undefined)), 97 | ), 98 | Err(e) => Either::Left(std::future::ready(Err(map_get_err(e)))), 99 | } 100 | } 101 | 102 | /// Get all the objects in the store, ordered by this index, with a maximum number of results of `limit` 103 | /// 104 | /// Internally, this uses [`IDBIndex::getAll`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/getAll). 105 | pub fn get_all( 106 | &self, 107 | limit: Option, 108 | ) -> impl Future, Err>> { 109 | let get_req = match limit { 110 | None => self.sys.get_all(), 111 | Some(limit) => self 112 | .sys 113 | .get_all_with_key_and_limit(&JsValue::UNDEFINED, limit), 114 | }; 115 | match get_req { 116 | Ok(get_req) => Either::Right( 117 | transaction_request(get_req).map(|res| res.map_err(map_get_err).map(array_to_vec)), 118 | ), 119 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 120 | } 121 | } 122 | 123 | /// Get all the objects with a key (for this index) in the provided range, with a maximum number of 124 | /// results of `limit`, ordered by this index 125 | /// 126 | /// Internally, this uses [`IDBIndex::getAll`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/getAll). 127 | pub fn get_all_in( 128 | &self, 129 | range: impl RangeBounds, 130 | limit: Option, 131 | ) -> impl Future, Err>> { 132 | let range = match make_key_range(range) { 133 | Ok(range) => range, 134 | Err(e) => return Either::Left(std::future::ready(Err(e))), 135 | }; 136 | let get_req = match limit { 137 | None => self.sys.get_all_with_key(&range), 138 | Some(limit) => self.sys.get_all_with_key_and_limit(&range, limit), 139 | }; 140 | match get_req { 141 | Ok(get_req) => Either::Right( 142 | transaction_request(get_req).map(|res| res.map_err(map_get_err).map(array_to_vec)), 143 | ), 144 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 145 | } 146 | } 147 | 148 | /// Get the first existing primary key for an object that has a key (for this index) in the provided range 149 | /// 150 | /// Internally, this uses [`IDBIndex::getKey`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/getKey). 151 | pub fn get_first_key_in( 152 | &self, 153 | range: impl RangeBounds, 154 | ) -> impl Future, Err>> { 155 | let range = match make_key_range(range) { 156 | Ok(range) => range, 157 | Err(e) => return Either::Left(std::future::ready(Err(e))), 158 | }; 159 | match self.sys.get_key(&range) { 160 | Ok(get_req) => Either::Right( 161 | transaction_request(get_req) 162 | .map(|res| res.map_err(map_get_err).map(none_if_undefined)), 163 | ), 164 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 165 | } 166 | } 167 | 168 | /// List all the primary keys in the object store, with a maximum number of results of `limit`, ordered by this index 169 | /// 170 | /// Internally, this uses [`IDBIndex::getAllKeys`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/getAllKeys). 171 | pub fn get_all_keys( 172 | &self, 173 | limit: Option, 174 | ) -> impl Future, Err>> { 175 | let get_req = match limit { 176 | None => self.sys.get_all_keys(), 177 | Some(limit) => self 178 | .sys 179 | .get_all_keys_with_key_and_limit(&JsValue::UNDEFINED, limit), 180 | }; 181 | match get_req { 182 | Ok(get_req) => Either::Right( 183 | transaction_request(get_req).map(|res| res.map_err(map_get_err).map(array_to_vec)), 184 | ), 185 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 186 | } 187 | } 188 | 189 | /// List all the primary keys of objects with a key (for this index)in the provided range, with a maximum number 190 | /// of results of `limit`, ordered by this index 191 | /// 192 | /// Internally, this uses [`IDBIndex::getAllKeys`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/getAllKeys). 193 | pub fn get_all_keys_in( 194 | &self, 195 | range: impl RangeBounds, 196 | limit: Option, 197 | ) -> impl Future, Err>> { 198 | let range = match make_key_range(range) { 199 | Ok(range) => range, 200 | Err(e) => return Either::Left(std::future::ready(Err(e))), 201 | }; 202 | let get_req = match limit { 203 | None => self.sys.get_all_keys_with_key(&range), 204 | Some(limit) => self.sys.get_all_keys_with_key_and_limit(&range, limit), 205 | }; 206 | match get_req { 207 | Ok(get_req) => Either::Right( 208 | transaction_request(get_req).map(|res| res.map_err(map_get_err).map(array_to_vec)), 209 | ), 210 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 211 | } 212 | } 213 | 214 | /// Open a [`Cursor`] on this index 215 | pub fn cursor(&self) -> CursorBuilder { 216 | CursorBuilder::from_index(self.sys.clone()) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use futures_channel::oneshot; 2 | use futures_util::future::{self, Either}; 3 | use std::ops::{Bound, RangeBounds}; 4 | use web_sys::{ 5 | js_sys::{Array, Function, JsString, Number, TypeError}, 6 | wasm_bindgen::{closure::Closure, JsCast, JsValue}, 7 | DomException, IdbKeyRange, IdbRequest, 8 | }; 9 | 10 | pub(crate) async fn non_transaction_request( 11 | req: IdbRequest, 12 | ) -> Result { 13 | let (success_tx, success_rx) = oneshot::channel(); 14 | let (error_tx, error_rx) = oneshot::channel(); 15 | 16 | let on_success = Closure::once(move |v| success_tx.send(v)); 17 | let on_error = Closure::once(move |v| error_tx.send(v)); 18 | 19 | req.set_onsuccess(Some(on_success.as_ref().dyn_ref::().unwrap())); 20 | req.set_onerror(Some(on_error.as_ref().dyn_ref::().unwrap())); 21 | 22 | match future::select(success_rx, error_rx).await { 23 | Either::Left((res, _)) => Ok(res.unwrap()), 24 | Either::Right((res, _)) => Err(res.unwrap()), 25 | } 26 | } 27 | 28 | pub(crate) fn none_if_undefined(v: JsValue) -> Option { 29 | if v.is_undefined() { 30 | None 31 | } else { 32 | Some(v) 33 | } 34 | } 35 | 36 | pub(crate) fn array_to_vec(v: JsValue) -> Vec { 37 | let array = v 38 | .dyn_into::() 39 | .expect("Value was not of the expected Array type"); 40 | let len = array.length(); 41 | let mut res = Vec::with_capacity(usize::try_from(len).unwrap()); 42 | for i in 0..len { 43 | res.push(array.get(i)); 44 | } 45 | res 46 | } 47 | 48 | pub(crate) fn str_slice_to_array(s: &[&str]) -> Array { 49 | let res = Array::new_with_length(u32::try_from(s.len()).unwrap()); 50 | for (i, v) in s.iter().enumerate() { 51 | res.set(u32::try_from(i).unwrap(), JsString::from(*v).into()); 52 | } 53 | res 54 | } 55 | 56 | pub(crate) fn err_from_event(evt: web_sys::Event) -> DomException { 57 | evt.prevent_default(); // Avoid the transaction aborting upon an error 58 | let idb_request = evt 59 | .target() 60 | .expect("Trying to parse indexed_db::Error from an event that has no target") 61 | .dyn_into::() 62 | .expect("Trying to parse indexed_db::Error from an event that is not from an IDBRequest"); 63 | idb_request 64 | .error() 65 | .expect("Failed to retrieve the error from the IDBRequest that called on_error") 66 | .expect("IDBRequest::error did not return a DOMException") 67 | } 68 | 69 | pub(crate) fn map_add_err(err: JsValue) -> crate::Error { 70 | match error_name!(&err) { 71 | Some("ReadOnlyError") => crate::Error::ReadOnly, 72 | Some("TransactionInactiveError") => { 73 | panic!("Tried adding to an ObjectStore while the transaction was inactive") 74 | } 75 | Some("DataError") => crate::Error::InvalidKey, 76 | Some("InvalidStateError") => crate::Error::ObjectStoreWasRemoved, 77 | Some("DataCloneError") => crate::Error::FailedClone, 78 | Some("ConstraintError") => crate::Error::AlreadyExists, 79 | _ => crate::Error::from_js_value(err), 80 | } 81 | } 82 | 83 | pub(crate) fn map_clear_err(err: JsValue) -> crate::Error { 84 | match error_name!(&err) { 85 | Some("ReadOnlyError") => crate::Error::ReadOnly, 86 | Some("TransactionInactiveError") => { 87 | panic!("Tried clearing an ObjectStore while the transaction was inactive") 88 | } 89 | _ => crate::Error::from_js_value(err), 90 | } 91 | } 92 | 93 | pub(crate) fn map_count_res(res: JsValue) -> usize { 94 | let num = res 95 | .dyn_into::() 96 | .expect("IDBObjectStore::count did not return a Number"); 97 | assert!( 98 | Number::is_integer(&num), 99 | "Number of elements in object store is not an integer" 100 | ); 101 | num.value_of() as usize 102 | } 103 | 104 | pub(crate) fn map_count_err(err: JsValue) -> crate::Error { 105 | match error_name!(&err) { 106 | Some("InvalidStateError") => crate::Error::ObjectStoreWasRemoved, 107 | Some("TransactionInactiveError") => { 108 | panic!("Tried counting in an ObjectStore while the transaction was inactive") 109 | } 110 | Some("DataError") => crate::Error::InvalidKey, 111 | _ => crate::Error::from_js_value(err), 112 | } 113 | } 114 | 115 | pub(crate) fn map_delete_err(err: JsValue) -> crate::Error { 116 | match error_name!(&err) { 117 | Some("ReadOnlyError") => crate::Error::ReadOnly, 118 | Some("InvalidStateError") => crate::Error::ObjectStoreWasRemoved, 119 | Some("TransactionInactiveError") => { 120 | panic!("Tried deleting from an ObjectStore while the transaction was inactive") 121 | } 122 | Some("DataError") => crate::Error::InvalidKey, 123 | _ => crate::Error::from_js_value(err), 124 | } 125 | } 126 | 127 | pub(crate) fn map_get_err(err: JsValue) -> crate::Error { 128 | match error_name!(&err) { 129 | Some("InvalidStateError") => crate::Error::ObjectStoreWasRemoved, 130 | Some("TransactionInactiveError") => { 131 | panic!("Tried getting from an ObjectStore while the transaction was inactive") 132 | } 133 | Some("DataError") => crate::Error::InvalidKey, 134 | _ => crate::Error::from_js_value(err), 135 | } 136 | } 137 | 138 | pub(crate) fn map_open_cursor_err(err: JsValue) -> crate::Error { 139 | match error_name!(&err) { 140 | Some("InvalidStateError") => crate::Error::ObjectStoreWasRemoved, 141 | Some("TransactionInactiveError") => { 142 | panic!("Tried opening a Cursor on an ObjectStore while the transaction was inactive") 143 | } 144 | Some("DataError") => crate::Error::InvalidKey, 145 | _ => crate::Error::from_js_value(err), 146 | } 147 | } 148 | 149 | pub(crate) fn map_cursor_advance_err(err: JsValue) -> crate::Error { 150 | match error_name!(&err) { 151 | Some("InvalidStateError") => crate::Error::CursorCompleted, 152 | Some("TransactionInactiveError") => { 153 | panic!("Tried advancing a Cursor on an ObjectStore while the transaction was inactive") 154 | } 155 | None if err.has_type::() => crate::Error::InvalidArgument, 156 | _ => crate::Error::from_js_value(err), 157 | } 158 | } 159 | 160 | pub(crate) fn map_cursor_advance_until_err(err: JsValue) -> crate::Error { 161 | match error_name!(&err) { 162 | Some("InvalidStateError") => crate::Error::CursorCompleted, 163 | Some("TransactionInactiveError") => { 164 | panic!("Tried advancing a Cursor on an ObjectStore while the transaction was inactive") 165 | } 166 | Some("DataError") => crate::Error::InvalidKey, 167 | _ => crate::Error::from_js_value(err), 168 | } 169 | } 170 | 171 | pub(crate) fn map_cursor_advance_until_primary_key_err(err: JsValue) -> crate::Error { 172 | match error_name!(&err) { 173 | Some("InvalidStateError") => crate::Error::CursorCompleted, 174 | Some("TransactionInactiveError") => { 175 | panic!("Tried advancing a Cursor on an ObjectStore while the transaction was inactive") 176 | } 177 | Some("DataError") => crate::Error::InvalidKey, 178 | Some("InvalidAccessError") => crate::Error::InvalidArgument, 179 | _ => crate::Error::from_js_value(err), 180 | } 181 | } 182 | 183 | pub(crate) fn map_cursor_delete_err(err: JsValue) -> crate::Error { 184 | match error_name!(&err) { 185 | Some("InvalidStateError") => crate::Error::CursorCompleted, 186 | Some("TransactionInactiveError") => { 187 | panic!("Tried advancing a Cursor on an ObjectStore while the transaction was inactive") 188 | } 189 | Some("ReadOnlyError") => crate::Error::ReadOnly, 190 | _ => crate::Error::from_js_value(err), 191 | } 192 | } 193 | 194 | pub(crate) fn map_cursor_update_err(err: JsValue) -> crate::Error { 195 | match error_name!(&err) { 196 | Some("InvalidStateError") => crate::Error::CursorCompleted, 197 | Some("TransactionInactiveError") => { 198 | panic!("Tried advancing a Cursor on an ObjectStore while the transaction was inactive") 199 | } 200 | Some("ReadOnlyError") => crate::Error::ReadOnly, 201 | Some("DataError") => crate::Error::InvalidKey, 202 | Some("DataCloneError") => crate::Error::FailedClone, 203 | _ => crate::Error::from_js_value(err), 204 | } 205 | } 206 | 207 | pub(crate) fn make_key_range(range: impl RangeBounds) -> crate::Result { 208 | match (range.start_bound(), range.end_bound()) { 209 | (Bound::Unbounded, Bound::Unbounded) => return Err(crate::Error::InvalidRange), 210 | (Bound::Unbounded, Bound::Included(b)) => IdbKeyRange::upper_bound_with_open(b, false), 211 | (Bound::Unbounded, Bound::Excluded(b)) => IdbKeyRange::upper_bound_with_open(b, true), 212 | (Bound::Included(b), Bound::Unbounded) => IdbKeyRange::lower_bound_with_open(b, false), 213 | (Bound::Excluded(b), Bound::Unbounded) => IdbKeyRange::lower_bound_with_open(b, true), 214 | (Bound::Included(l), Bound::Included(u)) => { 215 | IdbKeyRange::bound_with_lower_open_and_upper_open(l, u, false, false) 216 | } 217 | (Bound::Included(l), Bound::Excluded(u)) => { 218 | IdbKeyRange::bound_with_lower_open_and_upper_open(l, u, false, true) 219 | } 220 | (Bound::Excluded(l), Bound::Included(u)) => { 221 | IdbKeyRange::bound_with_lower_open_and_upper_open(l, u, true, false) 222 | } 223 | (Bound::Excluded(l), Bound::Excluded(u)) => { 224 | IdbKeyRange::bound_with_lower_open_and_upper_open(l, u, true, true) 225 | } 226 | } 227 | .map(|k| k.into()) 228 | .map_err(|err| match error_name!(&err) { 229 | Some("DataError") => crate::Error::InvalidKey, 230 | _ => crate::Error::from_js_value(err), 231 | }) 232 | } 233 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2024 Leo Gaspard 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/cursor.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | transaction::transaction_request, 3 | utils::{ 4 | make_key_range, map_cursor_advance_err, map_cursor_advance_until_err, 5 | map_cursor_advance_until_primary_key_err, map_cursor_delete_err, map_cursor_update_err, 6 | map_open_cursor_err, 7 | }, 8 | }; 9 | use futures_util::future::Either; 10 | use std::{future::Future, marker::PhantomData, ops::RangeBounds}; 11 | use web_sys::{ 12 | wasm_bindgen::{JsCast, JsValue}, 13 | IdbCursor, IdbCursorDirection, IdbCursorWithValue, IdbIndex, IdbObjectStore, IdbRequest, 14 | }; 15 | 16 | #[cfg(doc)] 17 | use crate::{Index, ObjectStore}; 18 | #[cfg(doc)] 19 | use web_sys::js_sys::Array; 20 | 21 | /// The direction for a cursor 22 | pub enum CursorDirection { 23 | /// Advance one by one 24 | Next, 25 | 26 | /// Advance, skipping duplicate elements 27 | NextUnique, 28 | 29 | /// Go back, one by one 30 | Prev, 31 | 32 | /// Go back, skipping duplicate elements 33 | PrevUnique, 34 | } 35 | 36 | impl CursorDirection { 37 | pub(crate) fn to_sys(&self) -> IdbCursorDirection { 38 | match self { 39 | CursorDirection::Next => IdbCursorDirection::Next, 40 | CursorDirection::NextUnique => IdbCursorDirection::Nextunique, 41 | CursorDirection::Prev => IdbCursorDirection::Prev, 42 | CursorDirection::PrevUnique => IdbCursorDirection::Prevunique, 43 | } 44 | } 45 | } 46 | 47 | /// Helper to build cursors over [`ObjectStore`]s 48 | pub struct CursorBuilder { 49 | source: Either, 50 | query: JsValue, 51 | direction: IdbCursorDirection, 52 | _phantom: PhantomData, 53 | } 54 | 55 | impl CursorBuilder { 56 | pub(crate) fn from_store(store: IdbObjectStore) -> CursorBuilder { 57 | CursorBuilder { 58 | source: Either::Left(store), 59 | query: JsValue::UNDEFINED, 60 | direction: IdbCursorDirection::Next, 61 | _phantom: PhantomData, 62 | } 63 | } 64 | 65 | pub(crate) fn from_index(index: IdbIndex) -> CursorBuilder { 66 | CursorBuilder { 67 | source: Either::Right(index), 68 | query: JsValue::UNDEFINED, 69 | direction: IdbCursorDirection::Next, 70 | _phantom: PhantomData, 71 | } 72 | } 73 | 74 | /// Open the cursor 75 | /// 76 | /// Internally, this uses [`IDBObjectStore::openCursor`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/openCursor). 77 | pub fn open(self) -> impl Future, Err>> { 78 | let req = match self.source { 79 | Either::Left(store) => { 80 | store.open_cursor_with_range_and_direction(&self.query, self.direction) 81 | } 82 | Either::Right(index) => { 83 | index.open_cursor_with_range_and_direction(&self.query, self.direction) 84 | } 85 | }; 86 | match req { 87 | Ok(open_req) => Either::Right(Cursor::from(open_req)), 88 | Err(err) => Either::Left(std::future::ready(Err(map_open_cursor_err(err)))), 89 | } 90 | } 91 | 92 | /// Open the cursor as a key-only cursor 93 | /// 94 | /// Internally, this uses [`IDBObjectStore::openKeyCursor`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/openKeyCursor). 95 | pub fn open_key(self) -> impl Future, Err>> { 96 | let req = match self.source { 97 | Either::Left(store) => { 98 | store.open_key_cursor_with_range_and_direction(&self.query, self.direction) 99 | } 100 | Either::Right(index) => { 101 | index.open_key_cursor_with_range_and_direction(&self.query, self.direction) 102 | } 103 | }; 104 | match req { 105 | Ok(open_req) => Either::Right(Cursor::from(open_req)), 106 | Err(err) => Either::Left(std::future::ready(Err(map_open_cursor_err(err)))), 107 | } 108 | } 109 | 110 | /// Limit the range of the cursor 111 | /// 112 | /// Internally, this sets [this property](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/openCursor#range). 113 | pub fn range(mut self, range: impl RangeBounds) -> crate::Result { 114 | self.query = make_key_range(range)?; 115 | Ok(self) 116 | } 117 | 118 | /// Define the direction of the cursor 119 | /// 120 | /// Internally, this sets [this property](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/openCursor#direction). 121 | pub fn direction(mut self, direction: CursorDirection) -> Self { 122 | self.direction = direction.to_sys(); 123 | self 124 | } 125 | } 126 | 127 | /// Wrapper for [`IDBCursorWithValue`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursorWithValue) 128 | pub struct Cursor { 129 | sys: Option, 130 | req: IdbRequest, 131 | _phantom: PhantomData, 132 | } 133 | 134 | impl Cursor { 135 | pub(crate) async fn from(req: IdbRequest) -> crate::Result, Err> { 136 | let res = transaction_request(req.clone()) 137 | .await 138 | .map_err(map_open_cursor_err)?; 139 | let is_already_over = res.is_null(); 140 | let sys = (!is_already_over).then(|| { 141 | res.dyn_into::() 142 | .expect("Cursor-returning request did not return an IDBCursor") 143 | }); 144 | Ok(Cursor { 145 | sys, 146 | req, 147 | _phantom: PhantomData, 148 | }) 149 | } 150 | 151 | /// Retrieve the value this [`Cursor`] is currently pointing at, or `None` if the cursor is completed 152 | /// 153 | /// If this cursor was opened as a key-only cursor, then trying to call this method will panic. 154 | /// 155 | /// Internally, this uses the [`IDBCursorWithValue::value`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursorWithValue/value) property. 156 | pub fn value(&self) -> Option { 157 | self.sys.as_ref().map(|sys| { 158 | sys.dyn_ref::() 159 | .expect("Called Cursor::value on a key-only cursor") 160 | .value() 161 | .expect("Unable to retrieve value from known-good cursor") 162 | }) 163 | } 164 | 165 | /// Retrieve the key this [`Cursor`] is currently pointing at, or `None` if the cursor is completed 166 | /// 167 | /// Internally, this uses the [`IDBCursor::key`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/key) property. 168 | pub fn key(&self) -> Option { 169 | self.sys.as_ref().map(|sys| { 170 | sys.key() 171 | .expect("Failed retrieving key from known-good cursor") 172 | }) 173 | } 174 | 175 | /// Retrieve the primary key this [`Cursor`] is currently pointing at, or `None` if the cursor is completed 176 | /// 177 | /// Internally, this uses the [`IDBCursor::primaryKey`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/key) property. 178 | pub fn primary_key(&self) -> Option { 179 | self.sys.as_ref().map(|sys| { 180 | sys.primary_key() 181 | .expect("Failed retrieving primary key from known-good cursor") 182 | }) 183 | } 184 | 185 | /// Advance this [`Cursor`] by `count` elements 186 | /// 187 | /// Internally, this uses [`IDBCursor::advance`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/advance). 188 | pub async fn advance(&mut self, count: u32) -> crate::Result<(), Err> { 189 | let Some(sys) = &self.sys else { 190 | return Err(crate::Error::CursorCompleted); 191 | }; 192 | sys.advance(count).map_err(map_cursor_advance_err)?; 193 | if transaction_request(self.req.clone()) 194 | .await 195 | .map_err(map_cursor_advance_err)? 196 | .is_null() 197 | { 198 | self.sys = None; 199 | } 200 | Ok(()) 201 | } 202 | 203 | /// Advance this [`Cursor`] until the provided key 204 | /// 205 | /// Internally, this uses [`IDBCursor::continue`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/continue). 206 | pub async fn advance_until(&mut self, key: &JsValue) -> crate::Result<(), Err> { 207 | let Some(sys) = &self.sys else { 208 | return Err(crate::Error::CursorCompleted); 209 | }; 210 | sys.continue_with_key(key) 211 | .map_err(map_cursor_advance_until_err)?; 212 | if transaction_request(self.req.clone()) 213 | .await 214 | .map_err(map_cursor_advance_until_err)? 215 | .is_null() 216 | { 217 | self.sys = None; 218 | } 219 | Ok(()) 220 | } 221 | 222 | /// Advance this [`Cursor`] until the provided primary key 223 | /// 224 | /// This is a helper function for cursors built on top of [`Index`]es. It allows for 225 | /// quick resumption of index walking, faster than [`Cursor::advance_until`] if the 226 | /// primary key for the wanted element is known. 227 | /// 228 | /// Note that this method does not work on cursors over object stores, nor on cursors 229 | /// which are set with a direction of anything other than `Next` or `Prev`. 230 | /// 231 | /// Internally, this uses [`IDBCursor::continuePrimaryKey`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/continuePrimaryKey). 232 | pub async fn advance_until_primary_key( 233 | &mut self, 234 | index_key: &JsValue, 235 | primary_key: &JsValue, 236 | ) -> crate::Result<(), Err> { 237 | let Some(sys) = &self.sys else { 238 | return Err(crate::Error::CursorCompleted); 239 | }; 240 | sys.continue_primary_key(&index_key, primary_key) 241 | .map_err(map_cursor_advance_until_primary_key_err)?; 242 | if transaction_request(self.req.clone()) 243 | .await 244 | .map_err(map_cursor_advance_until_primary_key_err)? 245 | .is_null() 246 | { 247 | self.sys = None; 248 | } 249 | Ok(()) 250 | } 251 | 252 | /// Deletes the value currently pointed by this [`Cursor`] 253 | /// 254 | /// Note that this method does not work on key-only cursors over indexes. 255 | /// 256 | /// Internally, this uses [`IDBCursor::delete`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/delete). 257 | pub async fn delete(&self) -> crate::Result<(), Err> { 258 | let Some(sys) = &self.sys else { 259 | return Err(crate::Error::CursorCompleted); 260 | }; 261 | let req = sys.delete().map_err(map_cursor_delete_err)?; 262 | transaction_request(req) 263 | .await 264 | .map_err(map_cursor_delete_err)?; 265 | Ok(()) 266 | } 267 | 268 | /// Update the value currently pointed by this [`Cursor`] to `value` 269 | /// 270 | /// Note that this method does not work on key-only cursors over indexes. 271 | /// 272 | /// Internally, this uses [`IDBCursor::update`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/update). 273 | pub async fn update(&self, value: &JsValue) -> crate::Result<(), Err> { 274 | let Some(sys) = &self.sys else { 275 | return Err(crate::Error::CursorCompleted); 276 | }; 277 | let req = sys.update(value).map_err(map_cursor_update_err)?; 278 | transaction_request(req) 279 | .await 280 | .map_err(map_cursor_update_err)?; 281 | Ok(()) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/factory.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | transaction::{unsafe_jar, RunnableTransaction, TransactionResult}, 3 | utils::{non_transaction_request, str_slice_to_array}, 4 | Database, ObjectStore, OwnedDatabase, Transaction, 5 | }; 6 | use futures_util::{pin_mut, FutureExt}; 7 | use std::{ 8 | cell::{Cell, RefCell}, 9 | convert::Infallible, 10 | marker::PhantomData, 11 | }; 12 | use web_sys::{ 13 | js_sys::{self, Function, JsString}, 14 | wasm_bindgen::{closure::Closure, JsCast, JsValue}, 15 | IdbDatabase, IdbFactory, IdbObjectStoreParameters, IdbOpenDbRequest, IdbTransaction, 16 | IdbVersionChangeEvent, WorkerGlobalScope, 17 | }; 18 | 19 | /// Wrapper for [`IDBFactory`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory) 20 | #[derive(Debug)] 21 | pub struct Factory { 22 | sys: IdbFactory, 23 | } 24 | 25 | impl Factory { 26 | /// Retrieve the global `Factory` from the browser 27 | /// 28 | /// This internally uses [`indexedDB`](https://developer.mozilla.org/en-US/docs/Web/API/indexedDB). 29 | pub fn get() -> crate::Result { 30 | let indexed_db = if let Some(window) = web_sys::window() { 31 | window.indexed_db() 32 | } else if let Ok(worker_scope) = js_sys::global().dyn_into::() { 33 | worker_scope.indexed_db() 34 | } else { 35 | return Err(crate::Error::NotInBrowser); 36 | }; 37 | 38 | let sys = indexed_db 39 | .map_err(|_| crate::Error::IndexedDbDisabled)? 40 | .ok_or(crate::Error::IndexedDbDisabled)?; 41 | 42 | Ok(Factory { sys }) 43 | } 44 | 45 | /// Compare two keys for ordering 46 | /// 47 | /// Returns an error if one of the two values would not be a valid IndexedDb key. 48 | /// 49 | /// This internally uses [`IDBFactory::cmp`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/cmp). 50 | pub fn cmp( 51 | &self, 52 | lhs: &JsValue, 53 | rhs: &JsValue, 54 | ) -> crate::Result { 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<(), Infallible> { 79 | non_transaction_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 | // TODO: once the try_trait_v2 feature is stabilized, we can finally stop carrying any `Err` generic 101 | pub async fn open( 102 | &self, 103 | name: &str, 104 | version: u32, 105 | on_upgrade_needed: impl AsyncFnOnce(VersionChangeEvent) -> crate::Result<(), Err>, 106 | ) -> crate::Result { 107 | if version == 0 { 108 | return Err(crate::Error::VersionMustNotBeZero); 109 | } 110 | 111 | let open_req = self 112 | .sys 113 | .open_with_u32(name, version) 114 | .map_err(crate::Error::from_js_value)?; 115 | 116 | let result = RefCell::new(None); 117 | let result = &result; 118 | let (finished_tx, finished_rx) = futures_channel::oneshot::channel(); 119 | let ran_upgrade_cb = Cell::new(false); 120 | let ran_upgrade_cb = &ran_upgrade_cb; 121 | 122 | unsafe_jar::extend_lifetime_to_scope_and_run( 123 | Box::new( 124 | move |(transaction, event): (IdbTransaction, VersionChangeEvent)| { 125 | let fut = async move { 126 | ran_upgrade_cb.set(true); 127 | on_upgrade_needed(event).await 128 | }; 129 | RunnableTransaction::new(transaction, fut, result, finished_tx) 130 | }, 131 | ), 132 | async move |s| { 133 | // Separate variable to keep the closure alive until opening completed 134 | let on_upgrade_needed = Closure::once(move |evt: IdbVersionChangeEvent| { 135 | let evt = VersionChangeEvent::from_sys(evt); 136 | let transaction = evt.transaction().as_sys().clone(); 137 | s.run((transaction, evt)) 138 | }); 139 | open_req.set_onupgradeneeded(Some( 140 | on_upgrade_needed.as_ref().dyn_ref::().unwrap(), 141 | )); 142 | 143 | let completion_res = non_transaction_request(open_req.clone().into()).await; 144 | if ran_upgrade_cb.get() { 145 | // The upgrade callback was run, so we need to wait for its result to reach us 146 | let _ = finished_rx.await; 147 | let result = result 148 | .borrow_mut() 149 | .take() 150 | .expect("Finished was called without the result being available"); 151 | match result { 152 | TransactionResult::PolledForbiddenThing => { 153 | panic!("{}", crate::POLLED_FORBIDDEN_THING_PANIC) 154 | } 155 | TransactionResult::Done(upgrade_res) => upgrade_res?, 156 | } 157 | } 158 | completion_res.map_err(crate::Error::from_js_event)?; 159 | 160 | let db = open_req 161 | .result() 162 | .map_err(crate::Error::from_js_value)? 163 | .dyn_into::() 164 | .expect("Result of successful IDBOpenDBRequest is not an IDBDatabase"); 165 | 166 | Ok(OwnedDatabase::make_auto_close(Database::from_sys(db))) 167 | }, 168 | ) 169 | .await 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 { 180 | let open_req = self.sys.open(name).map_err(crate::Error::from_js_value)?; 181 | 182 | let completion_fut = non_transaction_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::() 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)] 200 | pub struct VersionChangeEvent { 201 | sys: IdbVersionChangeEvent, 202 | db: Database, 203 | transaction: Transaction, 204 | } 205 | 206 | impl VersionChangeEvent { 207 | fn from_sys(sys: IdbVersionChangeEvent) -> VersionChangeEvent { 208 | let db_req = sys 209 | .target() 210 | .expect("IDBVersionChangeEvent had no target") 211 | .dyn_into::() 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::() 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 { 248 | &self.db 249 | } 250 | 251 | /// Build an [`ObjectStore`] 252 | /// 253 | /// This returns a builder, and calling the `create` method on this builder will perform the actual creation. 254 | /// 255 | /// Internally, this uses [`IDBDatabase::createObjectStore`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/createObjectStore). 256 | pub fn build_object_store<'a>(&self, name: &'a str) -> ObjectStoreBuilder<'a, Err> { 257 | ObjectStoreBuilder { 258 | db: self.db.as_sys().clone(), 259 | name, 260 | options: IdbObjectStoreParameters::new(), 261 | _phantom: PhantomData, 262 | } 263 | } 264 | 265 | /// Deletes an [`ObjectStore`] 266 | /// 267 | /// Internally, this uses [`IDBDatabase::deleteObjectStore`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/deleteObjectStore). 268 | pub fn delete_object_store(&self, name: &str) -> crate::Result<(), Err> { 269 | self.db.as_sys().delete_object_store(name).map_err(|err| match error_name!(&err) { 270 | Some("InvalidStateError") => crate::Error::InvalidCall, 271 | Some("TransactionInactiveError") => panic!("Tried to delete an object store with the `versionchange` transaction having already aborted"), 272 | Some("NotFoundError") => crate::Error::DoesNotExist, 273 | _ => crate::Error::from_js_value(err), 274 | }) 275 | } 276 | 277 | /// The `versionchange` transaction that triggered this event 278 | /// 279 | /// This transaction can be used to submit further requests. 280 | pub fn transaction(&self) -> &Transaction { 281 | &self.transaction 282 | } 283 | } 284 | 285 | /// Helper to build an object store 286 | pub struct ObjectStoreBuilder<'a, Err> { 287 | db: IdbDatabase, 288 | name: &'a str, 289 | options: IdbObjectStoreParameters, 290 | _phantom: PhantomData, 291 | } 292 | 293 | impl<'a, Err> ObjectStoreBuilder<'a, Err> { 294 | /// Create the object store 295 | /// 296 | /// Internally, this uses [`IDBDatabase::createObjectStore`](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/createObjectStore). 297 | pub fn create(self) -> crate::Result, Err> { 298 | self.db 299 | .create_object_store_with_optional_parameters(self.name, &self.options) 300 | .map_err( 301 | |err| match error_name!(&err) { 302 | Some("InvalidStateError") => crate::Error::InvalidCall, 303 | Some("TransactionInactiveError") => panic!("Tried to create an object store with the `versionchange` transaction having already aborted"), 304 | Some("ConstraintError") => crate::Error::AlreadyExists, 305 | Some("InvalidAccessError") => crate::Error::InvalidArgument, 306 | _ => crate::Error::from_js_value(err), 307 | }, 308 | ) 309 | .map(ObjectStore::from_sys) 310 | } 311 | 312 | /// Set the key path for out-of-line keys 313 | /// 314 | /// If you want to use a compound primary key made of multiple attributes, please see [`ObjectStoreBuilder::compound_key_path`]. 315 | /// 316 | /// Internally, this [sets this setting](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/createObjectStore#keypath). 317 | pub fn key_path(self, path: &str) -> Self { 318 | self.options.set_key_path(&JsString::from(path)); 319 | self 320 | } 321 | 322 | /// Set the key path for out-of-line keys 323 | /// 324 | /// Internally, this [sets this setting](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/createObjectStore#keypath). 325 | pub fn compound_key_path(self, paths: &[&str]) -> Self { 326 | self.options.set_key_path(&str_slice_to_array(paths)); 327 | self 328 | } 329 | 330 | /// Enable auto-increment for the key 331 | /// 332 | /// Internally, this [sets this setting](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/createObjectStore#autoincrement). 333 | pub fn auto_increment(self) -> Self { 334 | self.options.set_auto_increment(true); 335 | self 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use indexed_db::{Error, Factory}; 4 | use wasm_bindgen_test::wasm_bindgen_test; 5 | use web_sys::{ 6 | js_sys::{global, JsString, Number, Uint8Array}, 7 | wasm_bindgen::{JsCast, JsValue}, 8 | WorkerGlobalScope, 9 | }; 10 | 11 | /// Returns the duration in milliseconds, with the result 12 | async fn time_it(cb: impl std::future::Future) -> (f64, R) { 13 | let now = { 14 | let performance = if let Some(window) = web_sys::window() { 15 | window.performance() 16 | } else if let Ok(worker_scope) = global().dyn_into::() { 17 | worker_scope.performance() 18 | } else { 19 | None 20 | } 21 | .expect("No `performance` available (not in a browser environment?)"); 22 | move || performance.now() 23 | }; 24 | let start_ms = now(); 25 | let ret = cb.await; 26 | let elapsed_ms = now() - start_ms; 27 | (elapsed_ms, ret) 28 | } 29 | 30 | #[wasm_bindgen_test] 31 | async fn close_and_delete_before_reopen() { 32 | // tracing_wasm::set_as_global_default(); 33 | // std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 34 | 35 | const DATABASE_NAME: &str = "close_and_delete_before_reopen"; 36 | const ITERATIONS: usize = 10; 37 | 38 | let (delete_duration_ms, _) = time_it(async { 39 | for _ in 0..ITERATIONS { 40 | let factory = Factory::get().unwrap(); 41 | 42 | factory.delete_database(DATABASE_NAME).await.unwrap(); 43 | 44 | let _db = factory 45 | .open::<()>(DATABASE_NAME, 1, async move |_| Ok(())) 46 | .await 47 | .unwrap(); 48 | 49 | // Here the database wrapper got dropped which should trigger database close. 50 | } 51 | }) 52 | .await; 53 | 54 | // Deleting the database should be almost instantaneous in theory. 55 | // However this operation will hang as long as the database is still opened, 56 | // which can last for 10s of seconds if our code forget to close is (in 57 | // which case the close will only occur when the underlying javascript 58 | // object got garbage collected...). 59 | assert!( 60 | // 1s per iteration should be plenty under normal circumstances 61 | delete_duration_ms < 1000f64 * ITERATIONS as f64, 62 | "Deleting the database took too long: {}ms", 63 | delete_duration_ms 64 | ); 65 | } 66 | 67 | #[wasm_bindgen_test] 68 | async fn into_manual_close() { 69 | // tracing_wasm::set_as_global_default(); 70 | // std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 71 | 72 | const DATABASE_NAME: &str = "into_manual_close"; 73 | 74 | let factory = Factory::get().unwrap(); 75 | factory.delete_database(DATABASE_NAME).await.unwrap(); 76 | 77 | let manual_close_db = { 78 | let db = factory 79 | .open::<()>(DATABASE_NAME, 1, async move |evt| { 80 | evt.build_object_store("objects") 81 | .auto_increment() 82 | .create()?; 83 | Ok(()) 84 | }) 85 | .await 86 | .unwrap(); 87 | db.into_manual_close() 88 | }; 89 | 90 | // `db` has been dropped, but we should still be able to use `manual_close_db`. 91 | manual_close_db 92 | .transaction(&["objects"]) 93 | .run::<_, ()>(async move |t| { 94 | let objects = t.object_store("objects")?; 95 | assert!(objects.get(&JsString::from("nokey")).await?.is_none()); 96 | Ok(()) 97 | }) 98 | .await 99 | .unwrap(); 100 | 101 | manual_close_db.close(); 102 | 103 | // Now the database is fully closed 104 | let outcome = manual_close_db 105 | .transaction(&["objects"]) 106 | .run(async move |_| -> Result<(), Error<()>> { 107 | unreachable!("Database is closed"); 108 | }) 109 | .await; 110 | assert!( 111 | matches!(outcome, Err(Error::DatabaseIsClosed)), 112 | "Unexpected outcome: {:?}", 113 | outcome 114 | ); 115 | } 116 | 117 | #[wasm_bindgen_test] 118 | async fn smoke_test() { 119 | // tracing_wasm::set_as_global_default(); 120 | // std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 121 | 122 | // Factory::get 123 | let factory = Factory::get().unwrap(); 124 | 125 | // Factory::cmp 126 | assert_eq!( 127 | factory 128 | .cmp(&JsValue::from_str("foo"), &JsValue::from_str("bar")) 129 | .unwrap(), 130 | std::cmp::Ordering::Greater 131 | ); 132 | assert!(matches!( 133 | factory.cmp(&JsValue::TRUE, &JsValue::FALSE), 134 | Err(Error::InvalidKey), 135 | )); 136 | 137 | // Factory::delete_database 138 | factory.delete_database("foo").await.unwrap(); 139 | 140 | // Factory::open 141 | factory 142 | .open::<()>("foo", 0, async move |_| Ok(())) 143 | .await 144 | .unwrap_err(); 145 | factory 146 | .open::<()>("foo", 2, async move |_| Ok(())) 147 | .await 148 | .unwrap(); 149 | factory 150 | .open::<()>("foo", 1, async move |_| Ok(())) 151 | .await 152 | .unwrap_err(); 153 | 154 | // Factory::open_latest_version 155 | let db = factory.open_latest_version("foo").await.unwrap(); 156 | assert_eq!(db.name(), "foo"); 157 | assert_eq!(db.version(), 2); 158 | 159 | // Database::build_object_store 160 | let db = factory 161 | .open::<()>("bar", 1, async move |evt| { 162 | evt.build_object_store("objects").create()?; 163 | evt.build_object_store("things") 164 | .compound_key_path(&["foo", "bar"]) 165 | .create()?; 166 | let stuffs = evt.build_object_store("stuffs").auto_increment().create()?; 167 | stuffs.build_index("contents", "").create()?; 168 | Ok(()) 169 | }) 170 | .await 171 | .unwrap(); 172 | assert_eq!(db.name(), "bar"); 173 | assert_eq!(db.version(), 1); 174 | assert_eq!(db.object_store_names(), &["objects", "stuffs", "things"]); 175 | db.close(); // Close the database 176 | 177 | let db = factory 178 | .open::<()>("bar", 2, async move |evt| { 179 | evt.delete_object_store("things")?; 180 | Ok(()) 181 | }) 182 | .await 183 | .unwrap(); 184 | assert_eq!(db.name(), "bar"); 185 | assert_eq!(db.version(), 2); 186 | assert_eq!(db.object_store_names(), &["objects", "stuffs"]); 187 | 188 | // Transaction 189 | db.transaction(&["objects", "stuffs"]) 190 | .rw() 191 | .run::<_, ()>(async move |t| { 192 | let objects = t.object_store("objects")?; 193 | let stuffs = t.object_store("stuffs")?; 194 | 195 | // Run one simple addition 196 | stuffs.add(&JsString::from("foo")).await?; 197 | assert_eq!(stuffs.count().await?, 1); 198 | 199 | // Run two additions in parallel 200 | let a = stuffs.add(&JsString::from("bar")); 201 | let b = objects.add_kv(&JsString::from("key"), &JsString::from("value")); 202 | let (a, b) = futures::join!(a, b); 203 | a?; 204 | b?; 205 | assert_eq!(stuffs.count().await?, 2); 206 | assert_eq!(objects.count().await?, 1); 207 | assert!(objects.contains(&JsString::from("key")).await?); 208 | 209 | Ok(()) 210 | }) 211 | .await 212 | .unwrap(); 213 | db.transaction(&["objects", "stuffs"]) 214 | .rw() 215 | .run::<_, ()>(async move |t| { 216 | let objects = t.object_store("objects")?; 217 | let stuffs = t.object_store("stuffs")?; 218 | 219 | // Clear objects 220 | objects.clear().await?; 221 | assert_eq!(objects.count().await?, 0); 222 | 223 | // Count range 224 | assert_eq!( 225 | stuffs 226 | .count_in(Number::from(2).as_ref()..=Number::from(3).as_ref()) 227 | .await?, 228 | 1 229 | ); 230 | 231 | // Delete 232 | stuffs 233 | .delete_range(Number::from(2).as_ref()..=Number::from(3).as_ref()) 234 | .await?; 235 | assert_eq!(stuffs.count().await?, 1); 236 | stuffs.delete(&Number::from(1)).await?; 237 | assert_eq!(stuffs.count().await?, 0); 238 | 239 | Ok(()) 240 | }) 241 | .await 242 | .unwrap(); 243 | db.transaction(&["objects"]) 244 | .rw() 245 | .run::<_, ()>(async move |t| { 246 | let objects = t.object_store("objects")?; 247 | 248 | // Get 249 | objects 250 | .add_kv(&JsString::from("key"), &JsString::from("value")) 251 | .await?; 252 | assert_eq!( 253 | objects.get(&JsString::from("key")).await?.unwrap(), 254 | **JsString::from("value") 255 | ); 256 | assert!(objects.get(&JsString::from("nokey")).await?.is_none()); 257 | assert_eq!( 258 | objects 259 | .get_first_in(..JsString::from("zzz").as_ref()) 260 | .await? 261 | .unwrap(), 262 | **JsString::from("value") 263 | ); 264 | assert_eq!( 265 | objects.get_all(None).await?, 266 | vec![(**JsString::from("value")).clone()], 267 | ); 268 | assert_eq!( 269 | objects 270 | .get_all_in(JsString::from("zzz").as_ref().., None) 271 | .await?, 272 | Vec::::new(), 273 | ); 274 | 275 | Ok(()) 276 | }) 277 | .await 278 | .unwrap(); 279 | db.transaction(&["stuffs"]) 280 | .rw() 281 | .run::<_, ()>(async move |t| { 282 | let stuffs = t.object_store("stuffs")?; 283 | 284 | // Index 285 | stuffs.add(&JsString::from("value3")).await?; 286 | stuffs.put(&JsString::from("value2")).await?; 287 | stuffs.add(&JsString::from("value1")).await?; 288 | assert_eq!( 289 | stuffs.get_all(None).await?, 290 | vec![ 291 | (**JsString::from("value3")).clone(), 292 | (**JsString::from("value2")).clone(), 293 | (**JsString::from("value1")).clone() 294 | ] 295 | ); 296 | assert_eq!( 297 | stuffs.index("contents").unwrap().get_all(None).await?, 298 | vec![ 299 | (**JsString::from("value1")).clone(), 300 | (**JsString::from("value2")).clone(), 301 | (**JsString::from("value3")).clone() 302 | ] 303 | ); 304 | 305 | // Cursors 306 | let mut all = Vec::new(); 307 | let mut cursor = stuffs.cursor().open().await.unwrap(); 308 | while let Some(val) = cursor.value() { 309 | all.push((cursor.primary_key().unwrap(), val)); 310 | cursor.delete().await.unwrap(); 311 | cursor.advance(1).await.unwrap(); 312 | } 313 | assert_eq!( 314 | all, 315 | vec![ 316 | (JsValue::from(3), (**JsString::from("value3")).clone()), 317 | (JsValue::from(4), (**JsString::from("value2")).clone()), 318 | (JsValue::from(5), (**JsString::from("value1")).clone()) 319 | ] 320 | ); 321 | assert_eq!(stuffs.count().await.unwrap(), 0); 322 | 323 | Ok(()) 324 | }) 325 | .await 326 | .unwrap(); 327 | } 328 | 329 | #[wasm_bindgen_test] 330 | async fn auto_rollback() { 331 | // tracing_wasm::set_as_global_default(); 332 | // std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 333 | 334 | let factory = Factory::get().unwrap(); 335 | 336 | let db = factory 337 | .open::<()>("baz", 1, async move |evt| { 338 | evt.build_object_store("data").auto_increment().create()?; 339 | Ok(()) 340 | }) 341 | .await 342 | .unwrap(); 343 | 344 | db.transaction(&["data"]) 345 | .rw() 346 | .run::<_, ()>(async move |t| { 347 | t.object_store("data")?.add(&JsString::from("foo")).await?; 348 | t.object_store("data")?.add(&JsString::from("bar")).await?; 349 | if true { 350 | // Something went wrong! 351 | Err::<(), _>(())?; 352 | } 353 | Ok(()) 354 | }) 355 | .await 356 | .unwrap_err(); 357 | 358 | db.transaction(&["data"]) 359 | .rw() 360 | .run::<_, ()>(async move |t| { 361 | t.object_store("data")?.add(&JsString::from("baz")).await?; 362 | Ok::<_, indexed_db::Error<()>>(()) 363 | }) 364 | .await 365 | .unwrap(); 366 | 367 | db.transaction(&["data"]) 368 | .rw() 369 | .run::<_, ()>(async move |t| { 370 | assert_eq!(t.object_store("data")?.count().await?, 1); 371 | Ok::<_, indexed_db::Error<()>>(()) 372 | }) 373 | .await 374 | .unwrap(); 375 | } 376 | 377 | #[wasm_bindgen_test] 378 | async fn duplicate_insert_returns_proper_error_and_does_not_abort() { 379 | let factory = Factory::get().unwrap(); 380 | 381 | let db = factory 382 | .open::<()>("quux", 1, async move |evt| { 383 | evt.build_object_store("data").create()?; 384 | Ok(()) 385 | }) 386 | .await 387 | .unwrap(); 388 | 389 | db.transaction(&["data"]) 390 | .rw() 391 | .run::<_, ()>(async move |t| { 392 | t.object_store("data")? 393 | .add_kv(&JsString::from("key1"), &JsString::from("foo")) 394 | .await?; 395 | Ok(()) 396 | }) 397 | .await 398 | .unwrap(); 399 | 400 | db.transaction(&["data"]) 401 | .rw() 402 | .run::<_, ()>(async move |t| { 403 | assert!(matches!( 404 | t.object_store("data")? 405 | .add_kv(&JsString::from("key1"), &JsString::from("bar")) 406 | .await 407 | .unwrap_err(), 408 | indexed_db::Error::AlreadyExists 409 | )); 410 | t.object_store("data")? 411 | .add_kv(&JsString::from("key2"), &JsString::from("baz")) 412 | .await?; 413 | Ok(()) 414 | }) 415 | .await 416 | .unwrap(); 417 | 418 | db.transaction(&["data"]) 419 | .rw() 420 | .run::<_, ()>(async move |t| { 421 | assert_eq!( 422 | t.object_store("data")?.get_all_keys(None).await?, 423 | vec![JsValue::from("key1"), JsValue::from("key2")] 424 | ); 425 | assert_eq!( 426 | t.object_store("data")?.get_all(None).await?, 427 | vec![JsValue::from("foo"), JsValue::from("baz")] 428 | ); 429 | Ok(()) 430 | }) 431 | .await 432 | .unwrap(); 433 | } 434 | 435 | #[wasm_bindgen_test] 436 | async fn typed_array_keys() { 437 | let factory = Factory::get().unwrap(); 438 | 439 | let db = factory 440 | .open::<()>("db12", 1, async move |evt| { 441 | evt.build_object_store("data").create()?; 442 | Ok(()) 443 | }) 444 | .await 445 | .unwrap(); 446 | 447 | db.transaction(&["data"]) 448 | .rw() 449 | .run::<_, ()>(async move |t| { 450 | let data = t.object_store("data")?; 451 | data.add_kv(&Uint8Array::from(&b"key1"[..]), &JsString::from("foo")) 452 | .await?; 453 | data.add_kv(&Uint8Array::from(&b"key2"[..]), &JsString::from("bar")) 454 | .await?; 455 | data.add_kv(&Uint8Array::from(&b"key3"[..]), &JsString::from("baz")) 456 | .await?; 457 | assert_eq!( 458 | 2, 459 | data.count_in(Uint8Array::from(&b"key2"[..]).as_ref()..) 460 | .await? 461 | ); 462 | assert_eq!( 463 | 1, 464 | data.count_in(..Uint8Array::from(&b"key2"[..]).as_ref()) 465 | .await? 466 | ); 467 | 468 | Ok(()) 469 | }) 470 | .await 471 | .unwrap(); 472 | } 473 | 474 | #[wasm_bindgen_test] 475 | async fn borrowing_transaction() { 476 | let factory = Factory::get().unwrap(); 477 | 478 | let db = factory 479 | .open::("foo42", 1, async move |evt| { 480 | evt.build_object_store("data").create()?; 481 | Ok(()) 482 | }) 483 | .await 484 | .unwrap(); 485 | let data_from_outside = String::from("superlargedata"); 486 | db.transaction(&["data"]) 487 | .rw() 488 | .run::<_, Infallible>(async |t| { 489 | let data = t.object_store("data")?; 490 | data.add_kv( 491 | &JsString::from("data"), 492 | &JsString::from(&data_from_outside as &str), 493 | ) 494 | .await?; 495 | Ok(()) 496 | }) 497 | .await 498 | .unwrap(); 499 | db.transaction(&["data"]) 500 | .run::<_, Infallible>(async |t| { 501 | let data = t.object_store("data")?; 502 | assert_eq!( 503 | data.get(&JsString::from("data")) 504 | .await? 505 | .unwrap() 506 | .as_string() 507 | .unwrap(), 508 | data_from_outside 509 | ); 510 | Ok(()) 511 | }) 512 | .await 513 | .unwrap(); 514 | } 515 | -------------------------------------------------------------------------------- /src/object_store.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | transaction::transaction_request, 3 | utils::{ 4 | array_to_vec, make_key_range, map_add_err, map_clear_err, map_count_err, map_count_res, 5 | map_delete_err, map_get_err, none_if_undefined, str_slice_to_array, 6 | }, 7 | CursorBuilder, Index, 8 | }; 9 | use futures_util::future::{Either, FutureExt}; 10 | use std::{future::Future, marker::PhantomData, ops::RangeBounds}; 11 | use web_sys::{js_sys::JsString, wasm_bindgen::JsValue, IdbIndexParameters, IdbObjectStore}; 12 | 13 | #[cfg(doc)] 14 | use crate::Cursor; 15 | 16 | /// Wrapper for [`IDBObjectStore`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore), 17 | /// for use in transactions 18 | #[derive(Debug)] 19 | pub struct ObjectStore { 20 | sys: IdbObjectStore, 21 | _phantom: PhantomData, 22 | } 23 | 24 | impl ObjectStore { 25 | pub(crate) fn from_sys(sys: IdbObjectStore) -> ObjectStore { 26 | ObjectStore { 27 | sys, 28 | _phantom: PhantomData, 29 | } 30 | } 31 | 32 | /// Build an index over this object store 33 | /// 34 | /// Note that this method can only be called from within an `on_upgrade_needed` callback. It returns 35 | /// a builder, and calling the `create` method on this builder will perform the actual creation. 36 | /// 37 | /// If you want to make an index that searches multiple columns, please use [`ObjectStore::build_compound_index`]. 38 | /// 39 | /// Internally, this uses [`IDBObjectStore::createIndex`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex). 40 | pub fn build_index<'a>(&self, name: &'a str, key_path: &str) -> IndexBuilder<'a, Err> { 41 | IndexBuilder { 42 | store: self.sys.clone(), 43 | name, 44 | key_path: JsString::from(key_path).into(), 45 | options: IdbIndexParameters::new(), 46 | _phantom: PhantomData, 47 | } 48 | } 49 | 50 | /// Build a compound index over this object store 51 | /// 52 | /// Note that this method can only be called from within an `on_upgrade_needed` callback. It returns 53 | /// a builder, and calling the `create` method on this builder will perform the actual creation. 54 | /// 55 | /// Interesting points about indices: 56 | /// - It is not possible to index `bool` in IndexedDB. 57 | /// - If your index uses a column that does not exist, then the object will not be recorded in the index. 58 | /// This is useful for unique compound indices, usually when you would have conditionally indexed a `bool` column otherwise. 59 | /// - You cannot build a compound multi-entry index, it needs to be a regular index. 60 | /// 61 | /// Internally, this uses [`IDBObjectStore::createIndex`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex). 62 | pub fn build_compound_index<'a>( 63 | &self, 64 | name: &'a str, 65 | key_paths: &[&str], 66 | ) -> IndexBuilder<'a, Err> { 67 | IndexBuilder { 68 | store: self.sys.clone(), 69 | name, 70 | key_path: str_slice_to_array(key_paths).into(), 71 | options: IdbIndexParameters::new(), 72 | _phantom: PhantomData, 73 | } 74 | } 75 | 76 | /// Delete an index from this object store 77 | /// 78 | /// Note that this method can only be called from within an `on_upgrade_needed` callback. 79 | /// 80 | /// Internally, this uses [`IDBObjectStore::deleteIndex`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/deleteIndex). 81 | pub fn delete_index(&self, name: &str) -> crate::Result<(), Err> { 82 | self.sys 83 | .delete_index(name) 84 | .map_err(|err| match error_name!(&err) { 85 | Some("InvalidStateError") => crate::Error::ObjectStoreWasRemoved, 86 | Some("NotFoundError") => crate::Error::DoesNotExist, 87 | _ => crate::Error::from_js_value(err), 88 | }) 89 | } 90 | 91 | /// Add the value `value` to this object store, and return its auto-computed key 92 | /// 93 | /// This will error if the key already existed. 94 | /// 95 | /// Internally, this uses [`IDBObjectStore::add`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/add). 96 | pub fn add(&self, value: &JsValue) -> impl Future> { 97 | match self.sys.add(value) { 98 | Ok(add_req) => { 99 | Either::Left(transaction_request(add_req).map(|res| res.map_err(map_add_err))) 100 | } 101 | Err(e) => Either::Right(std::future::ready(Err(map_add_err(e)))), 102 | } 103 | } 104 | 105 | /// Add the value `value` to this object store, with key `key` 106 | /// 107 | /// This will error if the key already existed. 108 | /// 109 | /// Internally, this uses [`IDBObjectStore::add`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/add). 110 | pub fn add_kv( 111 | &self, 112 | key: &JsValue, 113 | value: &JsValue, 114 | ) -> impl Future> { 115 | match self.sys.add_with_key(value, key) { 116 | Ok(add_req) => Either::Left( 117 | transaction_request(add_req).map(|res| res.map_err(map_add_err).map(|_| ())), 118 | ), 119 | Err(e) => Either::Right(std::future::ready(Err(map_add_err(e)))), 120 | } 121 | } 122 | 123 | /// Add the value `value` to this object store, and return its auto-computed key 124 | /// 125 | /// This will overwrite the previous value if the key already existed. 126 | /// 127 | /// Internally, this uses [`IDBObjectStore::add`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/add). 128 | pub fn put(&self, value: &JsValue) -> impl Future> { 129 | match self.sys.put(value) { 130 | Ok(add_req) => { 131 | Either::Left(transaction_request(add_req).map(|res| res.map_err(map_add_err))) 132 | } 133 | Err(e) => Either::Right(std::future::ready(Err(map_add_err(e)))), 134 | } 135 | } 136 | 137 | /// Add the value `value` to this object store, with key `key` 138 | /// 139 | /// This will overwrite the previous value if the key already existed. 140 | /// 141 | /// Internally, this uses [`IDBObjectStore::add`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/add). 142 | pub fn put_kv( 143 | &self, 144 | key: &JsValue, 145 | value: &JsValue, 146 | ) -> impl Future> { 147 | match self.sys.put_with_key(value, key) { 148 | Ok(add_req) => Either::Left( 149 | transaction_request(add_req).map(|res| res.map_err(map_add_err).map(|_| ())), 150 | ), 151 | Err(e) => Either::Right(std::future::ready(Err(map_add_err(e)))), 152 | } 153 | } 154 | 155 | /// Clear this object store 156 | /// 157 | /// Internally, this uses [`IDBObjectStore::clear`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/clear). 158 | pub fn clear(&self) -> impl Future> { 159 | match self.sys.clear() { 160 | Ok(clear_req) => Either::Left( 161 | transaction_request(clear_req).map(|res| res.map_err(map_clear_err).map(|_| ())), 162 | ), 163 | Err(err) => Either::Right(std::future::ready(Err(map_clear_err(err)))), 164 | } 165 | } 166 | 167 | /// Count the number of objects in this store 168 | /// 169 | /// Internally, this uses [`IDBObjectStore::count`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/count). 170 | pub fn count(&self) -> impl Future> { 171 | match self.sys.count() { 172 | Ok(count_req) => Either::Left( 173 | transaction_request(count_req) 174 | .map(|res| res.map_err(map_count_err).map(map_count_res)), 175 | ), 176 | Err(e) => Either::Right(std::future::ready(Err(map_count_err(e)))), 177 | } 178 | } 179 | 180 | /// Checks whether the provided key exists in this object store 181 | /// 182 | /// Internally, this uses [`IDBObjectStore::count`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/count). 183 | pub fn contains(&self, key: &JsValue) -> impl Future> { 184 | match self.sys.count_with_key(key) { 185 | Ok(count_req) => Either::Left( 186 | transaction_request(count_req) 187 | .map(|res| res.map_err(map_count_err).map(|n| map_count_res(n) != 0)), 188 | ), 189 | Err(e) => Either::Right(std::future::ready(Err(map_count_err(e)))), 190 | } 191 | } 192 | 193 | /// Counts the number of objects with a key in `range` 194 | /// 195 | /// Note that the unbounded range is not a valid range for IndexedDB. 196 | /// 197 | /// Internally, this uses [`IDBObjectStore::count`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/count). 198 | pub fn count_in( 199 | &self, 200 | range: impl RangeBounds, 201 | ) -> impl Future> { 202 | let range = match make_key_range(range) { 203 | Ok(range) => range, 204 | Err(e) => return Either::Left(std::future::ready(Err(e))), 205 | }; 206 | match self.sys.count_with_key(&range) { 207 | Ok(count_req) => Either::Right( 208 | transaction_request(count_req) 209 | .map(|res| res.map_err(map_count_err).map(map_count_res)), 210 | ), 211 | Err(e) => Either::Left(std::future::ready(Err(map_count_err(e)))), 212 | } 213 | } 214 | 215 | /// Delete the object with key `key` 216 | /// 217 | /// Unfortunately, the IndexedDb API does not indicate whether an object was actually deleted. 218 | /// 219 | /// Internally, this uses [`IDBObjectStore::delete`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/delete). 220 | pub fn delete(&self, key: &JsValue) -> impl Future> { 221 | match self.sys.delete(key) { 222 | Ok(delete_req) => Either::Left( 223 | transaction_request(delete_req).map(|res| res.map_err(map_delete_err).map(|_| ())), 224 | ), 225 | Err(e) => Either::Right(std::future::ready(Err(map_delete_err(e)))), 226 | } 227 | } 228 | 229 | /// Delete all the objects with a key in `range` 230 | /// 231 | /// Note that the unbounded range is not a valid range for IndexedDB. 232 | /// Unfortunately, the IndexedDb API does not indicate whether an object was actually deleted. 233 | /// 234 | /// Internally, this uses [`IDBObjectStore::delete`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/delete). 235 | pub fn delete_range( 236 | &self, 237 | range: impl RangeBounds, 238 | ) -> impl Future> { 239 | let range = match make_key_range(range) { 240 | Ok(range) => range, 241 | Err(e) => return Either::Left(std::future::ready(Err(e))), 242 | }; 243 | match self.sys.delete(&range) { 244 | Ok(delete_req) => Either::Right( 245 | transaction_request(delete_req).map(|res| res.map_err(map_delete_err).map(|_| ())), 246 | ), 247 | Err(e) => Either::Left(std::future::ready(Err(map_delete_err(e)))), 248 | } 249 | } 250 | 251 | /// Get the object with key `key` 252 | /// 253 | /// Internally, this uses [`IDBObjectStore::get`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/get). 254 | pub fn get(&self, key: &JsValue) -> impl Future, Err>> { 255 | match self.sys.get(key) { 256 | Ok(get_req) => Either::Right( 257 | transaction_request(get_req) 258 | .map(|res| res.map_err(map_get_err).map(none_if_undefined)), 259 | ), 260 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 261 | } 262 | } 263 | 264 | /// Get the first value with a key in `range`, ordered by key 265 | /// 266 | /// Note that the unbounded range is not a valid range for IndexedDB. 267 | /// 268 | /// Internally, this uses [`IDBObjectStore::get`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/get). 269 | pub fn get_first_in( 270 | &self, 271 | range: impl RangeBounds, 272 | ) -> impl Future, Err>> { 273 | let range = match make_key_range(range) { 274 | Ok(range) => range, 275 | Err(e) => return Either::Left(std::future::ready(Err(e))), 276 | }; 277 | match self.sys.get(&range) { 278 | Ok(get_req) => Either::Right( 279 | transaction_request(get_req) 280 | .map(|res| res.map_err(map_get_err).map(none_if_undefined)), 281 | ), 282 | Err(e) => Either::Left(std::future::ready(Err(map_get_err(e)))), 283 | } 284 | } 285 | 286 | /// Get all the objects in the store, with a maximum number of results of `limit` 287 | /// 288 | /// Internally, this uses [`IDBObjectStore::getAll`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAll). 289 | pub fn get_all( 290 | &self, 291 | limit: Option, 292 | ) -> impl Future, Err>> { 293 | let get_req = match limit { 294 | None => self.sys.get_all(), 295 | Some(limit) => self 296 | .sys 297 | .get_all_with_key_and_limit(&JsValue::UNDEFINED, limit), 298 | }; 299 | match get_req { 300 | Ok(get_req) => Either::Right( 301 | transaction_request(get_req).map(|res| res.map_err(map_get_err).map(array_to_vec)), 302 | ), 303 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 304 | } 305 | } 306 | 307 | /// Get all the objects with a key in the provided range, with a maximum number of results of `limit` 308 | /// 309 | /// Internally, this uses [`IDBObjectStore::getAll`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAll). 310 | pub fn get_all_in( 311 | &self, 312 | range: impl RangeBounds, 313 | limit: Option, 314 | ) -> impl Future, Err>> { 315 | let range = match make_key_range(range) { 316 | Ok(range) => range, 317 | Err(e) => return Either::Left(std::future::ready(Err(e))), 318 | }; 319 | let get_req = match limit { 320 | None => self.sys.get_all_with_key(&range), 321 | Some(limit) => self.sys.get_all_with_key_and_limit(&range, limit), 322 | }; 323 | match get_req { 324 | Ok(get_req) => Either::Right( 325 | transaction_request(get_req).map(|res| res.map_err(map_get_err).map(array_to_vec)), 326 | ), 327 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 328 | } 329 | } 330 | 331 | /// Get the first existing key in the provided range 332 | /// 333 | /// Internally, this uses [`IDBObjectStore::getKey`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getKey). 334 | pub fn get_first_key_in( 335 | &self, 336 | range: impl RangeBounds, 337 | ) -> impl Future, Err>> { 338 | let range = match make_key_range(range) { 339 | Ok(range) => range, 340 | Err(e) => return Either::Left(std::future::ready(Err(e))), 341 | }; 342 | match self.sys.get_key(&range) { 343 | Ok(get_req) => Either::Right( 344 | transaction_request(get_req) 345 | .map(|res| res.map_err(map_get_err).map(none_if_undefined)), 346 | ), 347 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 348 | } 349 | } 350 | 351 | /// List all the keys in the object store, with a maximum number of results of `limit` 352 | /// 353 | /// Internally, this uses [`IDBObjectStore::getAllKeys`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAllKeys). 354 | pub fn get_all_keys( 355 | &self, 356 | limit: Option, 357 | ) -> impl Future, Err>> { 358 | let get_req = match limit { 359 | None => self.sys.get_all_keys(), 360 | Some(limit) => self 361 | .sys 362 | .get_all_keys_with_key_and_limit(&JsValue::UNDEFINED, limit), 363 | }; 364 | match get_req { 365 | Ok(get_req) => Either::Right( 366 | transaction_request(get_req).map(|res| res.map_err(map_get_err).map(array_to_vec)), 367 | ), 368 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 369 | } 370 | } 371 | 372 | /// List all the keys in the provided range, with a maximum number of results of `limit` 373 | /// 374 | /// Internally, this uses [`IDBObjectStore::getAllKeys`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAllKeys). 375 | pub fn get_all_keys_in( 376 | &self, 377 | range: impl RangeBounds, 378 | limit: Option, 379 | ) -> impl Future, Err>> { 380 | let range = match make_key_range(range) { 381 | Ok(range) => range, 382 | Err(e) => return Either::Left(std::future::ready(Err(e))), 383 | }; 384 | let get_req = match limit { 385 | None => self.sys.get_all_keys_with_key(&range), 386 | Some(limit) => self.sys.get_all_keys_with_key_and_limit(&range, limit), 387 | }; 388 | match get_req { 389 | Ok(get_req) => Either::Right( 390 | transaction_request(get_req).map(|res| res.map_err(map_get_err).map(array_to_vec)), 391 | ), 392 | Err(err) => Either::Left(std::future::ready(Err(map_get_err(err)))), 393 | } 394 | } 395 | 396 | /// Get the [`Index`] with the provided name 397 | /// 398 | /// Internally, this uses [`IDBObjectStore::index`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/index). 399 | pub fn index(&self, name: &str) -> crate::Result, Err> { 400 | Ok(Index::from_sys(self.sys.index(name).map_err( 401 | |err| match error_name!(&err) { 402 | Some("InvalidStateError") => crate::Error::ObjectStoreWasRemoved, 403 | Some("NotFoundError") => crate::Error::DoesNotExist, 404 | _ => crate::Error::from_js_value(err), 405 | }, 406 | )?)) 407 | } 408 | 409 | /// Open a [`Cursor`] on this object store 410 | pub fn cursor(&self) -> CursorBuilder { 411 | CursorBuilder::from_store(self.sys.clone()) 412 | } 413 | } 414 | 415 | /// Helper to build indexes over an [`ObjectStore`] 416 | pub struct IndexBuilder<'a, Err> { 417 | store: IdbObjectStore, 418 | name: &'a str, 419 | key_path: JsValue, 420 | options: IdbIndexParameters, 421 | _phantom: PhantomData, 422 | } 423 | 424 | impl<'a, Err> IndexBuilder<'a, Err> { 425 | /// Create the index 426 | /// 427 | /// Internally, this uses [`IDBObjectStore::createIndex`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex). 428 | pub fn create(self) -> crate::Result<(), Err> { 429 | self.store 430 | .create_index_with_str_sequence_and_optional_parameters( 431 | self.name, 432 | &self.key_path, 433 | &self.options, 434 | ) 435 | .map_err(|err| match error_name!(&err) { 436 | Some("ConstraintError") => crate::Error::AlreadyExists, 437 | Some("InvalidAccessError") => crate::Error::InvalidArgument, 438 | Some("InvalidStateError") => crate::Error::ObjectStoreWasRemoved, 439 | Some("SyntaxError") => crate::Error::InvalidKey, 440 | _ => crate::Error::from_js_value(err), 441 | }) 442 | .map(|_| ()) 443 | } 444 | 445 | /// Mark this index as unique 446 | /// 447 | /// Internally, this sets [this property](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex#unique). 448 | pub fn unique(self) -> Self { 449 | self.options.set_unique(true); 450 | self 451 | } 452 | 453 | /// Mark this index as multi-entry 454 | /// 455 | /// Internally, this sets [this property](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex#multientry). 456 | pub fn multi_entry(self) -> Self { 457 | self.options.set_multi_entry(true); 458 | self 459 | } 460 | } 461 | --------------------------------------------------------------------------------