UNPKG

idb-kv-store

Version:

Persistent key-value store for web browsers backed by IndexedDB

574 lines (470 loc) 14.4 kB
/* eslint-env browser */ module.exports = IdbKvStore var EventEmitter = require('events').EventEmitter var inherits = require('inherits') var promisize = require('promisize') var global = typeof window === 'undefined' ? self : window var IDB = global.indexedDB || global.mozIndexedDB || global.webkitIndexedDB || global.msIndexedDB IdbKvStore.INDEXEDDB_SUPPORT = IDB != null IdbKvStore.BROADCAST_SUPPORT = global.BroadcastChannel != null inherits(IdbKvStore, EventEmitter) function IdbKvStore (name, opts, cb) { var self = this if (typeof name !== 'string') throw new Error('A name must be supplied of type string') if (!IDB) throw new Error('IndexedDB not supported') if (typeof opts === 'function') return new IdbKvStore(name, null, opts) if (!(self instanceof IdbKvStore)) return new IdbKvStore(name, opts, cb) if (!opts) opts = {} EventEmitter.call(self) self._db = null self._closed = false self._channel = null self._waiters = [] if (opts.disableBroadcast !== true) { var Channel = opts.channel || global.BroadcastChannel if (Channel) { self._channel = new Channel(name) self._channel.onmessage = onChange } } var request = IDB.open(name) request.onerror = onerror request.onsuccess = onsuccess request.onupgradeneeded = onupgradeneeded self.on('newListener', onNewListener) function onerror (event) { handleError(event) self._close(event.target.error) if (cb) cb(event.target.error) } function onDbError (event) { handleError(event) self._close(event.target.error) } function onsuccess (event) { if (self._closed) { event.target.result.close() } else { self._db = event.target.result self._db.onclose = onclose self._db.onerror = onDbError for (var i in self._waiters) self._waiters[i]._init(null) self._waiters = null if (cb) cb(null) self.emit('open') } } function onupgradeneeded (event) { var db = event.target.result db.createObjectStore('kv', {autoIncrement: true}) } function onclose () { self._close() } function onNewListener (event) { if (event !== 'add' && event !== 'set' && event !== 'remove') return if (!self._channel) return self.emit('error', new Error('No BroadcastChannel support')) } function onChange (event) { if (event.data.method === 'add') self.emit('add', event.data) else if (event.data.method === 'set') self.emit('set', event.data) else if (event.data.method === 'remove') self.emit('remove', event.data) } } IdbKvStore.prototype.get = function (key, cb) { return this.transaction('readonly').get(key, cb) } IdbKvStore.prototype.getMultiple = function (keys, cb) { return this.transaction('readonly').getMultiple(keys, cb) } IdbKvStore.prototype.set = function (key, value, cb) { cb = promisize(cb) var error = null var t = this.transaction('readwrite', function (err) { error = error || err cb(error) }) t.set(key, value, function (err) { error = err }) return cb.promise } IdbKvStore.prototype.json = function (range, cb) { return this.transaction('readonly').json(range, cb) } IdbKvStore.prototype.keys = function (range, cb) { return this.transaction('readonly').keys(range, cb) } IdbKvStore.prototype.values = function (range, cb) { return this.transaction('readonly').values(range, cb) } IdbKvStore.prototype.remove = function (key, cb) { cb = promisize(cb) var error = null var t = this.transaction('readwrite', function (err) { error = error || err cb(error) }) t.remove(key, function (err) { error = err }) return cb.promise } IdbKvStore.prototype.clear = function (cb) { cb = promisize(cb) var error = null var t = this.transaction('readwrite', function (err) { error = error || err cb(error) }) t.clear(function (err) { error = err }) return cb.promise } IdbKvStore.prototype.count = function (range, cb) { return this.transaction('readonly').count(range, cb) } IdbKvStore.prototype.add = function (key, value, cb) { cb = promisize(cb) var error = null var t = this.transaction('readwrite', function (err) { error = error || err cb(error) }) t.add(key, value, function (err) { error = err }) return cb.promise } IdbKvStore.prototype.iterator = function (range, next) { return this.transaction('readonly').iterator(range, next) } IdbKvStore.prototype.transaction = function (mode, onfinish) { if (this._closed) throw new Error('Database is closed') var transaction = new Transaction(this, mode, onfinish) if (this._db) transaction._init(null) else this._waiters.push(transaction) return transaction } IdbKvStore.prototype.close = function () { this._close() } IdbKvStore.prototype._close = function (err) { if (this._closed) return this._closed = true if (this._db) this._db.close() if (this._channel) this._channel.close() this._db = null this._channel = null if (err) this.emit('error', err) this.emit('close') for (var i in this._waiters) this._waiters[i]._init(err || new Error('Database is closed')) this._waiters = null this.removeAllListeners() } function Transaction (kvStore, mode, cb) { if (typeof mode === 'function') return new Transaction(kvStore, null, mode) this._kvStore = kvStore this._mode = mode || 'readwrite' this._objectStore = null this._waiters = null this.finished = false this.onfinish = promisize(cb) // `onfinish` public variable for backwards compatibility with v4.3.1 this.done = this.onfinish.promise if (this._mode !== 'readonly' && this._mode !== 'readwrite') { throw new Error('mode must be either "readonly" or "readwrite"') } } Transaction.prototype._init = function (err) { var self = this if (self.finished) return if (err) return self._close(err) var transaction = self._kvStore._db.transaction('kv', self._mode) transaction.oncomplete = oncomplete transaction.onerror = onerror transaction.onabort = onerror self._objectStore = transaction.objectStore('kv') for (var i in self._waiters) self._waiters[i](null, self._objectStore) self._waiters = null function oncomplete () { self._close(null) } function onerror (event) { handleError(event) self._close(event.target.error) } } Transaction.prototype._getObjectStore = function (cb) { if (this.finished) throw new Error('Transaction is finished') if (this._objectStore) return cb(null, this._objectStore) this._waiters = this._waiters || [] this._waiters.push(cb) } Transaction.prototype.set = function (key, value, cb) { var self = this if (key == null || value == null) throw new Error('A key and value must be given') cb = promisize(cb) self._getObjectStore(function (err, objectStore) { if (err) return cb(err) try { var request = objectStore.put(value, key) } catch (e) { return cb(e) } request.onerror = handleError.bind(this, cb) request.onsuccess = function () { if (self._kvStore._channel) { self._kvStore._channel.postMessage({ method: 'set', key: key, value: value }) } cb(null) } }) return cb.promise } Transaction.prototype.add = function (key, value, cb) { var self = this if (value == null && key != null) return self.add(undefined, key, cb) if (typeof value === 'function' || (value == null && cb == null)) return self.add(undefined, key, value) if (value == null) throw new Error('A value must be provided as an argument') cb = promisize(cb) self._getObjectStore(function (err, objectStore) { if (err) return cb(err) try { var request = key == null ? objectStore.add(value) : objectStore.add(value, key) } catch (e) { return cb(e) } request.onerror = handleError.bind(this, cb) request.onsuccess = function () { if (self._kvStore._channel) { self._kvStore._channel.postMessage({ method: 'add', key: key, value: value }) } cb(null) } }) return cb.promise } Transaction.prototype.get = function (key, cb) { var self = this if (key == null) throw new Error('A key must be given as an argument') cb = promisize(cb) self._getObjectStore(function (err, objectStore) { if (err) return cb(err) try { var request = objectStore.get(key) } catch (e) { return cb(e) } request.onerror = handleError.bind(this, cb) request.onsuccess = function (event) { cb(null, event.target.result) } }) return cb.promise } Transaction.prototype.getMultiple = function (keys, cb) { var self = this if (keys == null) throw new Error('An array of keys must be given as an argument') cb = promisize(cb) if (keys.length === 0) { cb(null, []) return cb.promise } self._getObjectStore(function (err, objectStore) { if (err) return cb(err) // Implementation mostly taken from https://www.codeproject.com/Articles/744986/How-to-do-some-magic-with-indexedDB var sortedKeys = keys.slice().sort() var i = 0 var resultsMap = {} var getReturnValue = function () { return keys.map(function (key) { return resultsMap[key] }) } var cursorReq = objectStore.openCursor() cursorReq.onerror = handleError.bind(this, cb) cursorReq.onsuccess = function (event) { var cursor = event.target.result if (!cursor) { cb(null, getReturnValue()) return } var key = cursor.key while (key > sortedKeys[i]) { // The cursor has passed beyond this key. Check next. ++i if (i === sortedKeys.length) { // There is no next. Stop searching. cb(null, getReturnValue()) return } } if (key === sortedKeys[i]) { resultsMap[key] = cursor.value // The current cursor value should be included and we should continue // a single step in case next item has the same key or possibly our // next key in sortedKeys. cursor.continue() } else { // cursor.key not yet at sortedKeys[i]. Forward cursor to the next key to hunt for. cursor.continue(sortedKeys[i]) } } }) return cb.promise } Transaction.prototype.json = function (range, cb) { var self = this if (typeof range === 'function') return self.json(null, range) cb = promisize(cb) var json = {} self.iterator(range, function (err, cursor) { if (err) return cb(err) if (cursor) { json[cursor.key] = cursor.value cursor.continue() } else { cb(null, json) } }) return cb.promise } Transaction.prototype.keys = function (range, cb) { var self = this if (typeof range === 'function') return self.keys(null, range) cb = promisize(cb) var keys = [] self.iterator(range, function (err, cursor) { if (err) return cb(err) if (cursor) { keys.push(cursor.key) cursor.continue() } else { cb(null, keys) } }) return cb.promise } Transaction.prototype.values = function (range, cb) { var self = this if (typeof range === 'function') return self.values(null, range) cb = promisize(cb) var values = [] self.iterator(range, function (err, cursor) { if (err) return cb(err) if (cursor) { values.push(cursor.value) cursor.continue() } else { cb(null, values) } }) return cb.promise } Transaction.prototype.remove = function (key, cb) { var self = this if (key == null) throw new Error('A key must be given as an argument') cb = promisize(cb) self._getObjectStore(function (err, objectStore) { if (err) return cb(err) try { var request = objectStore.delete(key) } catch (e) { return cb(e) } request.onerror = handleError.bind(this, cb) request.onsuccess = function () { if (self._kvStore._channel) { self._kvStore._channel.postMessage({ method: 'remove', key: key }) } cb(null) } }) return cb.promise } Transaction.prototype.clear = function (cb) { var self = this cb = promisize(cb) self._getObjectStore(function (err, objectStore) { if (err) return cb(err) try { var request = objectStore.clear() } catch (e) { return cb(e) } request.onerror = handleError.bind(this, cb) request.onsuccess = function () { cb(null) } }) return cb.promise } Transaction.prototype.count = function (range, cb) { var self = this if (typeof range === 'function') return self.count(null, range) cb = promisize(cb) self._getObjectStore(function (err, objectStore) { if (err) return cb(err) try { var request = range == null ? objectStore.count() : objectStore.count(range) } catch (e) { return cb(e) } request.onerror = handleError.bind(this, cb) request.onsuccess = function (event) { cb(null, event.target.result) } }) return cb.promise } Transaction.prototype.iterator = function (range, next) { var self = this if (typeof range === 'function') return self.iterator(null, range) if (typeof next !== 'function') throw new Error('A function must be given') self._getObjectStore(function (err, objectStore) { if (err) return next(err) try { var request = range == null ? objectStore.openCursor() : objectStore.openCursor(range) } catch (e) { return next(e) } request.onerror = handleError.bind(this, next) request.onsuccess = function (event) { var cursor = event.target.result next(null, cursor) } }) } Transaction.prototype.abort = function () { if (this.finished) throw new Error('Transaction is finished') if (this._objectStore) this._objectStore.transaction.abort() this._close(new Error('Transaction aborted')) } Transaction.prototype._close = function (err) { if (this.finished) return this.finished = true this._kvStore = null this._objectStore = null for (var i in this._waiters) this._waiters[i](err || new Error('Transaction is finished')) this._waiters = null if (this.onfinish) this.onfinish(err) this.onfinish = null } function handleError (cb, event) { if (event == null) return handleError(null, cb) event.preventDefault() event.stopPropagation() if (cb) cb(event.target.error) }