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

/* 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)
}
}