JSPM

  • Created
  • Published
  • Downloads 7504828
  • Score
    100M100P100Q205993F
  • License ISC

A small wrapper that makes IndexedDB usable

Package Exports

  • idb
  • idb/with-async-ittr.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (idb) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

IndexedDB with usability.

This is a tiny (~1.17k) library that mostly mirrors the IndexedDB API, but with small improvements that make a big difference to usability.

  1. Installation
  2. Changes
  3. API
    1. openDB
    2. deleteDB
    3. unwrap
    4. wrap
    5. General enhancements
    6. IDBDatabase enhancements
    7. IDBTransaction enhancements
    8. IDBCursor enhancements
    9. Async iterators
  4. Examples
  5. TypeScript

Installation

npm install idb

Then, assuming you're using a module-compatible system (like Webpack, Rollup etc):

import { openDB, deleteDB, wrap, unwrap } from 'idb';

async function doDatabaseStuff() {
  const db = await openDB();
}

Changes

This is a rewrite from 3.x. See changes.

API

openDB

This method opens a database, and returns a promise for an enhanced IDBDatabase.

const db = await openDB(name, version, {
  upgrade(db, oldVersion, newVersion, transaction) {
    // …
  },
  blocked() {
    // …
  },
  blocking() {
    // …
  }
});
  • name: Name of the database.
  • version: Schema version.
  • upgrade (optional): Called if this version of the database has never been opened before. Use it to specify the schema for the database. This is similar to the upgradeneeded event in plain IndexedDB.
    • db: An enhanced IDBDatabase.
    • oldVersion: Last version of the database opened by the user.
    • newVersion: Whatever new version you provided.
    • transaction: The transaction for this upgrade. This is useful if you need to get data from other stores as part of a migration.
  • blocked (optional): Called if there are older versions of the database open on the origin, so this version cannot open.
  • blocking (optional): Called if this connection is blocking a future version of the database from opening.

deleteDB

Delete a database.

await deleteDB(name);
  • name: Name of the database.

unwrap

const unwrapped = unwrap(wrapped);

If for some reason you want to drop back into plain IndexedDB, give one of the enhanced objects to unwrap and you'll get the unmodified version back.

Promises will also be converted back into IDBRequest objects.

wrap

const wrapped = wrap(unwrapped);

Use this to convert a plain IDB object to one enhanced by this library. This is useful if some third party code gives you an IDBDatabase object and you want it to have the features of this library.

This doesn't work with IDBCursor, due to missing primitives. Also, if you wrap an IDBTransaction, tx.store and tx.objectStoreNames will not work in Edge. To avoid these issues, wrap the IDBDatabase object, and use the wrapped object to create a new transaction.

General enhancements

Once you've opened the database the API is the same as IndexedDB, except for a few changes to make things easier.

Any method that usually returns an IDBRequest object will now return a promise for the result.

const store = db.transaction(storeName).objectStore(storeName);
const value = await store.get(key);

IDBDatabase enhancements

Shortcuts to get/set from an object store

It's common to create a transaction for a single action, so helper methods are included for this:

// Get a value from a store:
const value = await db.get(storeName, key);
// Set a value in a store:
await db.put(storeName, value, key);

The shortcuts are: get, getKey, getAll, getAllKeys, count, put, add, delete, and clear. Each method takes a storeName argument, the name of the object store, and the rest of the arguments are the same as the equivalent IDBObjectStore method.

These methods depend on the same methods on IDBObjectStore, therefore getKey, getAll, and getAllKeys are missing in Edge as it lacks support.

Shortcuts to get from an index

The shortcuts are: getFromIndex, getKeyFromIndex, getAllFromIndex, getAllKeysFromIndex, and countFromIndex.

// Get a value from an index:
const value = await db.getFromIndex(storeName, indexName, key);

Each method takes storeName and indexName arguments, followed by the rest of the arguments from the equivalent IDBIndex method. Again, these methods depend on the equivalent methods on IDBIndex, so Edge does not support getKeyFromIndex, getAllFromIndex, or getAllKeysFromIndex.

IDBTransaction enhancements

tx.store

If a transaction involves a single store, the store property will reference that store.

const tx = db.transaction('whatever');
const store = tx.store;

If a transaction involves multiple stores, tx.store is undefined.

tx.done

Transactions have a .done promise which resolves when the transaction completes successfully, and otherwise rejects.

const tx = db.transaction(storeName, 'readwrite');
tx.store.put('foo', 'bar');
await tx.done;

IDBCursor enhancements

Cursor advance methods (advance, continue, continuePrimaryKey) return a promise for the cursor, or null if there are no further values to provide.

const store = db.transaction(storeName).objectStore(storeName);
let cursor = await store.openCursor();

while (cursor) {
  console.log(cursor.key, cursor.value);
  cursor = await cursor.continue();
}

Async iterators

Async iterator support isn't included by default (Edge doesn't support them). To include them, import idb/with-async-ittr.mjs (~1.39k) instead of idb:

import { openDB } from 'idb/with-async-ittr.mjs';

Now you can iterate over stores, indexes, and cursors:

const tx = db.transaction(storeName);

for await (const cursor of tx.store) {
  // …
}

Each yielded object is an IDBCursor. You can optionally use the advance methods to skip items (within an async iterator they return void):

const tx = db.transaction(storeName);

for await (const cursor of tx.store) {
  console.log(cursor.value);
  // Skip the next item
  cursor.advance(2);
}

If you don't manually advance the cursor, cursor.continue() is called for you.

Stores and indexes also have an iterate method which has the same signiture as openCursor, but returns an async iterator:

const tx = db.transaction('books');
const index = tx.store.index('author');

for await (const cursor of index.iterate('Douglas Adams')) {
  console.log(cursor.value);
}

Examples

Keyval Store

This is very similar to localStorage, but async. If this is all you need, you may be interested in idb-keyval, you can always upgrade to this library later.

const dbPromise = openDB('keyval-store', 1, {
  upgrade(db) {
    db.createObjectStore('keyval');
  }
});

const idbKeyval = {
  async get(key) {
    return (await dbPromise).get('keyval', key);
  },
  async set(key, val) {
    return (await dbPromise).put('keyval', val, key);
  },
  async delete(key) {
    return (await dbPromise).delete('keyval', key);
  },
  async clear() {
    return (await dbPromise).clear('keyval');
  },
  async keys() {
    return (await dbPromise).getAllKeys('keyval');
  },
};

TypeScript

This library is fully typed, and you can improve things by providing types for your database:

import { openDB, DBSchema } from 'idb';

interface MyDB extends DBSchema {
  'favourite-numbers': {
    key: string,
    value: number[],
  },
  'products': {
    value: {
      name: string,
      price: number,
      productCode: string,
    },
    key: string,
    indexes: { 'by-price': number },
  }
}

async function demo() {
  const db = await openDB<MyDB>('my-db', 1, {
    upgrade(db) {
      db.createObjectStore('favourite-numbers');

      const productStore = db.createObjectStore('products', { keyPath: 'productCode' });
      productStore.createIndex('by-price', 'price');
    }
  });

  // This works
  await db.put('favourite-numbers', [7, 95, 1023], 'Jen');
  // This fails, as the 'favourite-numbers' store expects an array of numbers.
  await db.put('favourite-numbers', ['Twelve'], 'Jake');
}

To define types for your database, extend DBSchema with an interface where the keys are the names of your object stores.

For each value, provide an object where value is the type of values within the store, and key is the type of keys within the store.

Optionally, indexes can contain a map of index names, to the type of key within that index.

Provide this interface when calling openDB, and from then on your database will be strongly typed. This also allows your IDE to autocomplete the names of stores and indexes.

Opting out of types

If you call openDB without providing types, your database will use basic types. However, sometimes you'll need to interact with stores that aren't in your schema, perhaps during upgrades. In that case you can cast.

Let's say we were renaming the 'favourite-numbers' store to 'fave-nums':

import { openDB, DBSchema, IDBPDatabase, IDBPTransaction } from 'idb';

interface MyDBV1 extends DBSchema {
  'favourite-numbers': { key: string, value: number[] },
}

interface MyDBV2 extends DBSchema {
  'fave-nums': { key: string, value: number[] },
}

const db = await openDB<MyDBV2>('my-db', 2, {
  async upgrade(db, oldVersion) {
    // Cast a reference of the database to the old schema.
    const v1Db = db as unknown as IDBPDatabase<MyDBV1>;

    if (oldVersion < 1) {
      v1Db.createObjectStore('favourite-numbers');
    }
    if (oldVersion < 2) {
      const store = v1Db.createObjectStore('favourite-numbers');
      store.name = 'fave-nums';
    }
  }
});

You can also cast to a typeless database by omiting the type, eg db as IDBPDatabase.