#indexed-db

matrix_indexed_db_futures

A fork of indexed_db_futures — Future bindings for IndexedDB via web_sys

1 unstable release

0.7.0 Dec 3, 2025

#198 in WebAssembly

Download history 734/week @ 2025-12-01 1661/week @ 2025-12-08 2366/week @ 2025-12-15 632/week @ 2025-12-22 646/week @ 2025-12-29 2233/week @ 2026-01-05 2575/week @ 2026-01-12 2786/week @ 2026-01-19 2792/week @ 2026-01-26

10,454 downloads per month
Used in 5 crates (via matrix-sdk-indexeddb)

MIT license

265KB
6.5K SLoC

A fork of indexed_db_futures

This is a fork of indexed_db_futures while we are waiting on some patches to be merged.


lib.rs:

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 matrix_indexed_db_futures::database::{Database, VersionChangeEvent};
use matrix_indexed_db_futures::prelude::*;
use matrix_indexed_db_futures::transaction::{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(async |event: VersionChangeEvent, tx: &Transaction<'_>| {
        let db = tx.db();
        // Convert versions from floats to integers to allow using them in match expressions
        let old_version = event.old_version() as u64;
        let new_version = event.new_version().map(|v| v as u64);

        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.

Environment Chrome Firefox Safari
Browser
Dedicated Worker
Shared Worker
Worker
Service worker

Feature table

Feature Description
async-upgrade Enable async closures in upgradeneeded event listeners.
cursors Enable opening IndexedDB cursors.
dates Enable SystemTime & Date handling.
indices Enable IndexedDB indices.
list-databases Enable getting a list of defined databases.
serde Enable serde integration.
streams Implement Stream where applicable.
switch Enable switches.
tx-done Enable waiting for transactions to complete without consuming them.
typed-arrays Enable typed array handling.
version-change Enable listening for versionchange events.

Dependencies

~9–12MB
~218K SLoC