You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
6.5 KiB
270 lines
6.5 KiB
/* 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)
|
|
}
|
|
}
|