Crate indexed_db_futures

Source
Expand description

Wraps the web_sys Indexed DB API in a Future-based API and removes the pain of dealing with JS callbacks or JSValue in Rust.

master CI badge crates.io badge docs.rs badge dependencies badge

§Overall API design

This library implements the same structs and methods as the JavaScript API - there should be no learning curve involved if you’re familiar with it.

§Primitives

In the context of this library, primitives refer to types that would be considered scalar primitives in JavaScript (bar some feature-flagged exceptions) and are converted using the TryToJs & TryFromJs traits. They are meant to be quickly derivable from JsValue, e.g. String is easily derivable via JsValue::as_string.

§Builders

Most API calls are constructed using builders which, in turn, get built using one of the following traits:

  • BuildPrimitive - implemented for requests use primitive serialisation.
  • BuildSerde - implemented for requests that use serde serialisation.
  • Build - implemented for requests that aren’t serde or primitive-serialisable (e.g. creating an index). Implemented automatically for any type that implements BuildPrimitive. As a convenience method, types that implement Build or BuildPrimitive also implement IntoFuture.

Note that API requests go out immediately after being built, not after being awaited.

§Transactions default to rolling back

❗ Unlike Javascript, transactions will roll back by default instead of committing - this design choice was made to allow code to use ?s. There is one browser compatibility-related caveat, however - see comment on Transaction::abort for more details.

§Multi-threaded executor

You will likely run into issues if your app is compiled with #[cfg(target_feature = "atomics")] as reported in #33.

Transactions auto-commit on JavasScript’s end on the next tick of the event loop if there are no outstanding requests active; this isn’t a problem in the default single-threaded executor, but, in a multi-threaded environment, wasm-bindgen-futures needs to schedule our closures on the next tick as well which causes transactions to prematurely auto-commit.

As a workaround, you can try only awaiting individual requests after committing your transaction (requests go out after being built, not after being polled).

let transaction = db.transaction("my_store").with_mode(TransactionMode::Readwrite).build()?;
let object_store = transaction.object_store("my_store")?;

let req1 = object_store.add("foo").primitive()?;
let req2 = object_store.add("bar").primitive()?;

transaction.commit().await?;

req1.await?;
req2.await?;

Alternatively, you can check out the indexed_db crate which explicitly focuses on multi-threaded support at the cost of ergonomics.

§Examples

§Opening a database & making some schema changes

use indexed_db_futures::database::Database;
use indexed_db_futures::prelude::*;
use indexed_db_futures::transaction::TransactionMode;

let db = Database::open("my_db")
    .with_version(2u8)
    .with_on_blocked(|event| {
      log::debug!("DB upgrade blocked: {:?}", event);
      Ok(())
    })
    .with_on_upgrade_needed_fut(|event, db| async move {
        // Convert versions from floats to integers to allow using them in match expressions
        let old_version = event.old_version() as u64;
        let new_version = event.new_version().map(|v| v as u64);

        match (old_version, new_version) {
            (0, Some(1)) => {
                db.create_object_store("my_store")
                    .with_auto_increment(true)
                    .build()?;
            }
            (prev, Some(2)) => {
                if prev == 1 {
                    if let Err(e) = db.delete_object_store("my_store") {
                      log::error!("Error deleting v1 object store: {}", e);
                    }
                }

                // Create an object store and await its transaction before inserting data.
                db.create_object_store("my_other_store")
                  .with_auto_increment(true)
                  .build()?
                  .transaction()
                  .on_done()?
                  .await
                  .into_result()?;

                //- Start a new transaction & add some data
                let tx = db.transaction("my_other_store")
                  .with_mode(TransactionMode::Readwrite)
                  .build()?;
                let store = tx.object_store("my_other_store")?;
                store.add("foo").await?;
                store.add("bar").await?;
                tx.commit().await?;
            }
            _ => {}
        }

        Ok(())
    })
    .await?;

§Reading/writing with serde

#[derive(Serialize, Deserialize)]
struct UserRef {
  id: u32,
  name: String,
}

object_store.put(UserRef { id: 1, name: "Bobby Tables".into() }).serde()?.await?;
let user: Option<UserRef> = object_store.get(1u32).serde()?.await?;

§Iterating a cursor

let Some(mut cursor) = object_store.open_cursor().await? else {
  log::debug!("Cursor empty");
  return Ok(());
};

// Retrieve the next record in the stream, expecting a String
let next: Option<String> = cursor.next_record().await?;

§Iterating an index as a stream

use futures::TryStreamExt;

let index = object_store.index("my_index")?;
let Some(cursor) = index.open_cursor().with_query(10u32..=100u32).serde()?.await? else {
  log::debug!("Cursor empty");
  return Ok(());
};
let stream = cursor.stream_ser::<UserRef>();
let records = stream.try_collect::<Vec<_>>().await?;

§Environment support

The following table is populated as a best effort attempt based on the crate’s unit tests succeeding/failing under different configurations.

EnvironmentChromeFirefoxSafari
Browser
Dedicated Worker
Shared Worker
Worker
Service worker

§Feature table

FeatureDescription
async-upgradeEnable async closures in upgradeneeded event listeners.
cursorsEnable opening IndexedDB cursors.
datesEnable SystemTime & Date handling.
indicesEnable IndexedDB indices.
list-databasesEnable getting a list of defined databases.
serdeEnable serde integration.
streamsImplement Stream where applicable.
switchEnable switches.
tx-doneEnable waiting for transactions to complete without consuming them.
typed-arraysEnable typed array handling.
version-changeEnable listening for versionchange events.

Modules§

cursorcursors
IDBCursor & IDBCursorWithValue implementations.
database
An IDBDatabase implementation.
datedates
Date handling. Re-exports the web_time crate.
error
Crate errors.
factory
An IDBFactory implementation.
future
Futures in use by the crate.
indexindices
An IDBIndex implementation.
internals
Internal type representations
iter
Iterators used by the crate.
object_store
An IDBObjectStore implementation.
prelude
Things to use::*.
primitive
Types for working with wasm-bindgen and js-sys primitive that don’t require serde for converting between Rust & JS.
query_source
Common functionality for making queries.
transaction
An IDBTransaction implementation.
typed_arraytyped-arrays
Partial TypedArray implementations. Currently only aims to offer convenient conversion to/from Vec.

Enums§

KeyPath
A key path representation.
KeyRange
An IDBKeyRange implementation.

Traits§

Build
Finalise the builder.
BuildPrimitive
Finalise the builder for returning a primitive result.
BuildSerde
Finalise the builder for returning a deserialisable result.
DeserialiseFromJsserde
A type that’s convertible from JsValue using serde, but not necessarily deserialisable as a whole.
SerialiseToJsserde
A type that’s convertible to JsValue using serde, but not necessarily serialisable as a whole.

Type Aliases§

KeyPathSeq
Alias for the SmallVecs used for Sequence key paths.
OpenDbResult
A Result with an OpenDbError as the error type.
Result
A Result with an Error as the error type.