/* global indexedDB */ 'use strict' module.exports = Level var AbstractLevelDOWN = require('abstract-leveldown').AbstractLevelDOWN var inherits = require('inherits') var Iterator = require('./iterator') var serialize = require('./util/serialize') var deserialize = require('./util/deserialize') var support = require('./util/support') var clear = require('./util/clear') var createKeyRange = require('./util/key-range') var DEFAULT_PREFIX = 'level-js-' function Level (location, opts) { if (!(this instanceof Level)) return new Level(location, opts) AbstractLevelDOWN.call(this, { bufferKeys: support.bufferKeys(indexedDB), snapshots: true, permanence: true, clear: true }) opts = opts || {} if (typeof location !== 'string') { throw new Error('constructor requires a location string argument') } this.location = location this.prefix = opts.prefix == null ? DEFAULT_PREFIX : opts.prefix this.version = parseInt(opts.version || 1, 10) } inherits(Level, AbstractLevelDOWN) Level.prototype.type = 'level-js' Level.prototype._open = function (options, callback) { var req = indexedDB.open(this.prefix + this.location, this.version) var self = this req.onerror = function () { callback(req.error || new Error('unknown error')) } req.onsuccess = function () { self.db = req.result callback() } req.onupgradeneeded = function (ev) { var db = ev.target.result if (!db.objectStoreNames.contains(self.location)) { db.createObjectStore(self.location) } } } Level.prototype.store = function (mode) { var transaction = this.db.transaction([this.location], mode) return transaction.objectStore(this.location) } Level.prototype.await = function (request, callback) { var transaction = request.transaction // Take advantage of the fact that a non-canceled request error aborts // the transaction. I.e. no need to listen for "request.onerror". transaction.onabort = function () { callback(transaction.error || new Error('aborted by user')) } transaction.oncomplete = function () { callback(null, request.result) } } Level.prototype._get = function (key, options, callback) { var store = this.store('readonly') try { var req = store.get(key) } catch (err) { return this._nextTick(callback, err) } this.await(req, function (err, value) { if (err) return callback(err) if (value === undefined) { // 'NotFound' error, consistent with LevelDOWN API return callback(new Error('NotFound')) } callback(null, deserialize(value, options.asBuffer)) }) } Level.prototype._del = function (key, options, callback) { var store = this.store('readwrite') try { var req = store.delete(key) } catch (err) { return this._nextTick(callback, err) } this.await(req, callback) } Level.prototype._put = function (key, value, options, callback) { var store = this.store('readwrite') try { // Will throw a DataError or DataCloneError if the environment // does not support serializing the key or value respectively. var req = store.put(value, key) } catch (err) { return this._nextTick(callback, err) } this.await(req, callback) } Level.prototype._serializeKey = function (key) { return serialize(key, this.supports.bufferKeys) } Level.prototype._serializeValue = function (value) { return serialize(value, true) } Level.prototype._iterator = function (options) { return new Iterator(this, this.location, options) } Level.prototype._batch = function (operations, options, callback) { if (operations.length === 0) return this._nextTick(callback) var store = this.store('readwrite') var transaction = store.transaction var index = 0 var error transaction.onabort = function () { callback(error || transaction.error || new Error('aborted by user')) } transaction.oncomplete = function () { callback() } // Wait for a request to complete before making the next, saving CPU. function loop () { var op = operations[index++] var key = op.key try { var req = op.type === 'del' ? store.delete(key) : store.put(op.value, key) } catch (err) { error = err transaction.abort() return } if (index < operations.length) { req.onsuccess = loop } } loop() } Level.prototype._clear = function (options, callback) { try { var keyRange = createKeyRange(options) } catch (e) { // The lower key is greater than the upper key. // IndexedDB throws an error, but we'll just do nothing. return this._nextTick(callback) } if (options.limit >= 0) { // IDBObjectStore#delete(range) doesn't have such an option. // Fall back to cursor-based implementation. return clear(this, this.location, keyRange, options, callback) } try { var store = this.store('readwrite') var req = keyRange ? store.delete(keyRange) : store.clear() } catch (err) { return this._nextTick(callback, err) } this.await(req, callback) } Level.prototype._close = function (callback) { this.db.close() this._nextTick(callback) } // NOTE: remove in a next major release Level.prototype.upgrade = function (callback) { if (this.status !== 'open') { return this._nextTick(callback, new Error('cannot upgrade() before open()')) } var it = this.iterator() var batchOptions = {} var self = this it._deserializeKey = it._deserializeValue = identity next() function next (err) { if (err) return finish(err) it.next(each) } function each (err, key, value) { if (err || key === undefined) { return finish(err) } var newKey = self._serializeKey(deserialize(key, true)) var newValue = self._serializeValue(deserialize(value, true)) // To bypass serialization on the old key, use _batch() instead of batch(). // NOTE: if we disable snapshotting (#86) this could lead to a loop of // inserting and then iterating those same entries, because the new keys // possibly sort after the old keys. self._batch([ { type: 'del', key: key }, { type: 'put', key: newKey, value: newValue } ], batchOptions, next) } function finish (err) { it.end(function (err2) { callback(err || err2) }) } function identity (data) { return data } } Level.destroy = function (location, prefix, callback) { if (typeof prefix === 'function') { callback = prefix prefix = DEFAULT_PREFIX } var request = indexedDB.deleteDatabase(prefix + location) request.onsuccess = function () { callback() } request.onerror = function (err) { callback(err) } }