Browser IndexedDB API

Paweł Kasperowicz

What is it?

IndexedDB is persistent storage key-value object-oriented database. It supports transactions. It has asynchronous API and can be used in Web Workers

Limitations

There is no the internationalized sorting, no synchronizing with server-side database and no full text searching. There are some libraries that can deal with this limitations (here and here). Also, the data could be wiped out any time by user or by end of the session when user uses private browsing mode.

The database could be in two storages modes:

  • Persistent – data is evicted only when users say so.
  • Temporary – data is cleared when DB reaches browser quota using last recently used policy

Storage limit is browser specific.

Sample application with IndexedDB

Simple application will be storing images in IndexedDB. Also, it will support simple search and exporting IPTC photo tags to the database. Added photos will be stored in the browser database.

I will be using dexie.js as a simple wrapper for the IndexedDB. It has simpler and cleaner API. In sources, there will be flow typing. For UI I used react.

Sources are available at https://github.com/pkasperowicz/indexedDB. To see a demo type npm start.

Opening database

Below you can see opening database in dexie.js. It opens Database images. Database schema contains definitions of tables with primary key and indexed keys. Table images contain primary key id and two indexed keys name and size. Notice that not indexed fields doesn’t have to be defined. 

const Database = new Dexie("images");

Database.version(1).stores({
    images: '++id,name,size'
});

Database.open().catch(function (e) {
    console.error ("Open failed: " + e);
});

Modifying database schema

We could change database schema. To do that we are adding new definition:

Database.version(2).stores({
    images: '++id,caption,city,country,*keywords'
}).upgrade(function () {
    return db.images.modify(image => {
        image.city = null;
        image.country = null;
        image.caption = image.name;
        image.keywords = [];
        delete image.name;
        delete image.size;
    });
});

We are not deleting the old definition, but adding new one, also adding upgrade function which updates schema, to make sure the old data is compatible with old one.

Saving the data

To get images from user File API will be used. Here I used Dropzone component, which returns me list of selected File objects. Than we receive binary data using asynchronous API for reading files and stored in the Uint8Array.

function readFile (file: File) : Promise<Uint8Array>  {
    return new Promise(function (resolve) {
        let reader = new FileReader();
        reader.onload = e => resolve(new Uint8Array(e.target.result));
        reader.readAsArrayBuffer(file);
    });
}

After that we can save the data:

function addFile (file: File) {
    readFile(file)
        .then(data => Database.images.add({ name: file.name, 
			size: file.size, 
			data}));
}

Saving data can be either plain Object or other custom data model.

function addFile (file: File) : Promise<number> {
    return readFile(file)
        .then(arrayBuffer => Database.images.add(new Image(arrayBuffer)));
}

Reading the data

Database.images.mapToClass (Image);

function getImages () : Promise<Image[]> {
    return Database.images
        .toCollection()
        .toArray();
}

MapToCalass method says to what class model should we map the data. When we don’t specify the class, plain Object will be used to map the data. Database supports data types such as the string, number, Object, boolean and TypedArrays for binary data. To map data to other object for example like Date or use some more complicated class hierarchy Typeson could be used.

There are also more complicated queries:

function searchImages (query: string) : Promise<Image[]> {
    return Database.images
        .where('keywords').startsWithIgnoreCase(query)
        .or('city').startsWithIgnoreCase(query)
        .or('country').startsWithIgnoreCase(query)
        .or('caption').startsWithIgnoreCase(query)
        .toArray();
}

Here we search through Database indexes, note that keywords here are array index, which means that it matches any element of array. It can be useful for implementing full text search, feature which is not backed up by IndexedDB API, look here and here.

See also

https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API

https://github.com/dfahlander/Dexie.js/wiki/Samples

https://github.com/explorigin/persistent-redux persistent redux store using PouchDB

https://github.com/rt2zz/redux-persist persistent redux store uses many store engines (e.g. localForge), has custom data transforms before saving such as encryption or compression

https://github.com/localForage/localForage

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami