npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

backboard

v1.0.11

Published

A Promise-based wrapper around IndexedDB with sane error and transaction handling

Downloads

9

Readme

backboard Build Status

Backboard is a thin promise-based wrapper around the IndexedDB API, designed to let you mix promises and IndexedDB without sacrificing performance or writing ridiculously messy code.

There are other similar projects, but most suffer from one or both of these flaws:

  1. They support less features than the raw IndexedDB API, which is not good for a DB-heavy app that is already struggling to deal with IndexedDB's limited feature set.

  2. They support extra stuff beyond the raw IndexedDB API, like caching or joins or advanced filtering. That's all great, but then you have this black box sitting between your application and your database, and I don't want a black box potentially interfering with performance or portability or anything like that.

The most similar projects to Backboard are Dexie.js and indexeddb-promised, but they don't provide quite the API that I want. Whether you like one of those libraries better is mainly up to personal preference.

The name "Backboard" comes from the fact that this project is an offshoot of a basketball video game I wrote.

Example Usage

(Also see the full API documentation.)

Installation

npm install backboard

Database Creation

Import the module:

const backboard = require('backboard');

Define your database schema, including upgrades.

const upgradeCallback = upgradeDB => {
    // Create object stores and indexes just like the raw IndexedDB API

    if (upgradeDB.oldVersion <= 0) {
        const playerStore = upgradeDB.createObjectStore('players', {keyPath: 'pid', autoIncrement: true});
        playerStore.createIndex('tid', 'tid');

        const teamStore = upgradeDB.createObjectStore('teams', {keyPath: 'tid', autoIncrement: true});
    }

    if (upgradeDB.oldVersion <= 1) {
        // You can use upgradeDB like a normal db described below, except all operations automatically happen in one transaction that commits when the upgrade is complete
        upgradeDB.players.iterate(player => {
            player.foo = 'updated';
            return player;
        });
    }
}

Finally, connect to the database and set up some error handling for the cases when the disk space quota is exceeded and another database connection is trying to upgrade (like if another tab has a newer version of your app open). I could have added some default behavior here, but for the reasons described in the Error Handling section below, I think it is quite important that you explicitly consider these specific error cases when developing your app.

backboard.on('quotaexceeded', () => alert('Quota exceeded! Fuck.'));
backboard.on('blocked', () => alert('Databace connection blocked. Please close any other tabs or windows with this website open.'));
backboard.open('database-name', 2, upgradeCallback)
    .then(db => {
        db.on('versionchange', () => db.close());

        // Now you can do stuff with db
    });

Transaction-Free API

Each command is in its own transaction, which is conceptually simpler but slower. All your favorite IndexedDB methods are available from db.objectStoreName.methodName, like add, clear, count, delete, get, getAll, and put, and they take the same arguments as their equivalent IndexedDB methods. They return promises that resolve or reject based on the success of the underlying database query.

You can also access indexes and their methods by db.objectStoreName.index('indexName').methodName, which similarly supports the count, get, and getAll methods.

return db.players.add({
        pid: 4,
        name: 'Bob Jones',
        tid: 0
    })
    .then(key => {
        console.log(key);
        return db.players.index('tid').get(0);
    })
    .then(player => console.log(player));

Transaction-Based API

Transaction can be reused across many queries, which can provide a huge performance boost for IO-heavy tasks. Once a transaction is created with the db.tx function (same arguments as native IndexedDB transaction creation), the API is identical to the Transaction-Free API, just with tx instead of db.

return db.tx('players', 'readwrite', tx =>
        return tx.players.add({
                name: 'Bob Jones',
                tid: 0
            })
            .then(() => {
                return tx.players.index('tid').getAll(0);
            });
    })
    .then(players => console.log('Transaction completed', players));
    .catch(err => console.error('Transaction aborted'));

Iteration

Iteration is the part of backboard that is most different from the IndexedDB API. Instead of a verbose cursor-based API, there is a slightly more complex iterate method available on object stores and indexes that lets you concisely iterate over records, optionally updating them as you go by simply returning a value or a promise in the callback function.

return db.players.index('tid')
    .iterate(backboard.lowerBound(0), 'prev', (player, shortCircuit) => {
        // Use the shortCircuit function to stop iteration after this callback runs
        if (player.pid > 10) {
            shortCircuit();
        }

        // Return undefined (or nothing) and it'll just go to the next object
        // Return a value (or a promise that resolves to a value) and it'll replace the object in the database
        player.foo = 'updated';
        return player;
    });

(Also see the full API documentation.)

Browser Compatibility

It's a bit tricky due to the interaction between promises and IndexedDB transactions. The current (early 2016) situation is:

Chrome: works out of the box.

Firefox: works if you use a third-party promises library that resolves promises with microtasks. Bluebird and es6-promise seem to work, and you can make backboard use them by doing

backboard.setPromiseConstructor(require('bluebird'));

or

backboard.setPromiseConstructor(require('es6-promise').Promise);

If you want to do feature detection to conditionally load a third-party promises library in Firefox but not in Chrome, take a look at the code in test/root.js which does exactly that.

Edge/IE: works only if you use a third-party promises library with synchronous promise resolution (which is not a good thing). If you want to go down that path, here's how to do it with Bluebird:

const BPromise = require('bluebird');
BPromise.setScheduler(fn => fn());
backboard.setPromiseConstructor(BPromise);

Also Edge has a buggy IndexedDB implementation in general, so you might run into errors caused by that.

Safari: who the fuck knows.

Error Handling

Error handling in IndexedDB is kind of complicated. An error in an individual operation (like an add when an object with that key already exists) triggers an error event at the request level which then bubbles up to the transaction and then the database. So the same error might appear in 3 different places, of course assuming that you're listening in all 3 places and that you don't manually stop the event propagation in one of the event handlers. Then you also have to worry about the distinction between error and abort events and about errors that happen only at the transaction and database levels (quick quiz: when you go over the disk space quota, is that an error or abort event, and at which level(s) does it occur?). So yeah... as you can imagine, a lot of the time, people don't really understand how all that works, and that can lead to errors being inadvertently missed.

Backboard removes some of that complexity (or call it "flexibilty" if you want to be more positive) at the expense of becoming slightly more opinionated. There's basically 3 things you have to know.

  1. Errors in read/write operations don't bubble up. They just cause the promise for that operation to be rejected. This is because, as in all promise-based code, you should be chaining them together so errors don't get lost. For example:

         return db.players.put(x1)
             .then(() => db.players.add(x2))
             .then(() => db.players.get(4))
             .then(player => console.log(player));
             .catch(err => console.error(err)); // Logs an error from any of the above functions
  2. If a transaction is aborted due to an error, the transaction promise is rejected. However if a transaction is manually aborted by calling tx.abort(), the transaction promise is not rejected unless there is some other uncaught rejection in the promise chain.

     return db.tx('players', 'readwrite', tx => {
             return tx.players.put(x1)
                 .then(() => tx.players.add(x2))
                 .then(() => tx.players.get(4))
                 .then(player => console.log(player));
                 .catch(err => console.error(err));
         })
         .catch(err => console.error(err)); // Will contain an AbortError if the transaction aborts

    Also, if a request in a transaction fails, it always aborts the transaction. There is no equivalent to how you can use event.preventDefault() in the request's event handler to still commit the transaction like you can in the raw IndexedDB API. If someone actually uses this feature, we can think about how to add it, but I've never used it.

  3. Once the database connection is open, basically no errors propagate down to the database or backboard objects. There are three exceptions, and you almost definitely want to handle these cases in your app. First, QuotaExceededError, which happens when your app uses too much disk space. In the raw IndexedDB API, you get a QuotaExceededError in a transaction's abort event, which then bubbles up to the database's abort event. IMHO, this is a very special type of abort because you probably do want to have some kind of central handling of quota errors, since you likely don't want to add that kind of logic to every single transaction. So I made quota aborts special: all other aborts appear as rejected transactions, but quota aborts trigger an event at the database level. Listen to them like this:

     const cb = event => {
         // Do whatever you want here, such as displaying a notification that this error is probably caused by https://code.google.com/p/chromium/issues/detail?id=488851
     };
     backboard.on('quotaexceeded', cb);

    There is a similar db.off function you can use to unsubscribe, like db.off('quotaexceeded', cb);.

    Another event you can listen for is the versionchange event, which you get when another instance of the database is trying to upgrade, such as in another tab. At a minimum, you probably want to close the connection so the upgrade can proceed:

     db.on('versionchange', () => db.close());

    Additionally, you can do things like saving data, reloading the page, etc. This is exactly the same as the versionchange event in the raw IndexedDB API.

    The final event you can listen for is the blocked event, which happens when you open a new connection and an upgrade wants to happen but you have an old database connection open that does not close when it recieves a versionchange event. This is exactly the same as the blocked event in the raw IndexedDB API. In theory if you handle versionchange appropriately, this will never happen. But just in case, you can do something like this:

     backboard.on('blocked', () => alert('Database connection blocked. Please close any other tabs or windows with this website open.'));

That's it! I guess that is still a lot of text to describe error handling, so it's still kind of complicated. But I think it's less complicated than the raw IndexedDB API, and it does everything I want it to. Hopefully you feel the same way.