asyncstorage-down
Version:
A leveldown API implementation that maps to AsyncStorage in React Native, originally a fork of no9/localstorage-down
452 lines (382 loc) • 10.9 kB
JavaScript
'use strict';
// require('./platform');
var inherits = require('util').inherits;
var AbstractLevelDOWN = require('abstract-leveldown').AbstractLevelDOWN;
var AbstractIterator = require('abstract-leveldown').AbstractIterator;
var Storage = require('./asyncstorage').Storage;
var StorageCore = require('./asyncstorage-core');
var utils = require('./utils');
var ltgt = require('ltgt');
// see http://stackoverflow.com/a/15349865/680742
var nextTick = global.setImmediate || process.nextTick;
var batchSize = 20
function ADIterator(db, options) {
AbstractIterator.call(this, db);
this._reverse = !!options.reverse;
this._endkey = options.end;
this._startkey = options.start;
this._gt = options.gt;
this._gte = options.gte;
this._lt = options.lt;
this._lte = options.lte;
this._keyAsBuffer = options.keyAsBuffer;
this._valueAsBuffer = options.valueAsBuffer;
this._limit = options.limit;
this._keysOnly = options.values === false
this._count = 0;
this._cache = [];
this._cacheExtinguished = false;
this.onInitCompleteListeners = [];
this.initStarted = true;
var self = this;
this.db.container.keys(function (err, keys) {
if (err) {
self.initError = err;
}
self._keys = keys;
if (keys.length === 0) {
self._pos = 0;
} else {
if (!self._reverse) {
self._startkey = ltgt.lowerBound(options)
self._endkey = ltgt.upperBound(options)
} else {
self._startkey = ltgt.upperBound(options)
self._endkey = ltgt.lowerBound(options)
}
if (self._startkey) {
self._pos = utils.sortedIndexOf(self._keys, self._startkey);
if (self._reverse) {
if (self._pos === self._keys.length) {
self._pos--;
}
else if (self._lt && utils.keyGte(self._keys[self._pos], self._lt)) {
self._pos--;
}
else if (self._lte && utils.keyGt(self._keys[self._pos], self._lte)) {
self._pos--;
}
else if (!self._lt && utils.keyGt(self._keys[self._pos], self._startkey)) {
self._pos--;
}
}
else {
if (self._pos < 0) {
self._pos = 0;
}
else if (self._gt && utils.keyLte(self._keys[self._pos], self._gt)) {
self._pos++;
}
else if (self._gte && utils.keyLt(self._keys[self._pos], self._gt)) {
self._pos++;
}
else if (!self._gt && utils.keyLt(self._keys[self._pos], self._startkey)) {
self._pos++;
}
}
} else {
self._pos = self._reverse ? self._keys.length - 1 : 0;
}
if (self._endkey) {
self._endIndex = utils.sortedIndexOf(self._keys, self._endkey);
if (self._reverse && utils.keyLt(self._keys[self._endIndex], self._endkey)) {
self._endIndex++;
}
else if (!self._reverse && utils.keyGt(self._keys[self._endIndex], self._endkey)) {
self._endIndex--;
}
}
}
self._fillCache(function () {
self.initCompleted = true;
var i = -1;
while (++i < self.onInitCompleteListeners.length) {
nextTick(self.onInitCompleteListeners[i]);
}
})
});
}
inherits(ADIterator, AbstractIterator);
ADIterator.prototype._fillCache = function fillCache(callback) {
var batch = []
for (var i = 0; i < batchSize; i++) {
if (this._limit === 0) {
break;
}
if (this._pos >= this._keys.length || this._pos < 0) { // done reading
break;
}
var key = this._keys[this._pos];
if (!!this._limit && this._limit > 0 && this._count++ >= this._limit) {
break;
}
if (typeof this._endIndex === 'number'
&& (this._reverse ? this._pos < this._endIndex : this._pos > this._endIndex)) {
break;
}
if ((this._lt && utils.keyGte(key, this._lt))
|| (this._lte && utils.keyGt(key, this._lte))
|| (this._gt && utils.keyLte(key, this._gt))
|| (this._gte && utils.keyLt(key, this._gte))) {
break;
}
this._pos += this._reverse ? -1 : 1;
batch.push(key)
}
if (!batch.length) {
this._cache = []
this._cacheExtinguished = true;
return callback()
}
if (this._keysOnly) {
this._cache = batch
return callback()
}
var self = this;
this.db.container.getItems(batch, function (errs, values) {
self._cache = values.map(function (v, idx) {
return {
key: batch[idx],
value: v,
error: errs[idx]
}
})
callback()
});
}
ADIterator.prototype._next = function (callback) {
var self = this;
callback = asyncify(callback)
if (self.initError) {
callback(self.initError);
return;
}
if (self.initStarted) {
if (self.initCompleted) {
getFromCache();
} else {
self.onInitCompleteListeners.push(getFromCache);
}
return;
}
function getFromCache() {
if (self._cacheExtinguished) {
return callback()
}
if (self._cache.length) {
var cached = self._cache.shift()
if (self._keysOnly) {
return callback(null, self._keyAsBuffer ? new Buffer(cached) : cached)
}
if (cached.error) {
if (cached.error.message == 'NotFound') {
return nextTick(function () {
self._next(callback)
})
} else {
return callback(cached.error)
}
}
callback(null,
self._keyAsBuffer ? new Buffer(cached.key) : cached.key,
self._valueAsBuffer ? new Buffer(cached.value) : cached.value)
return
} else {
self._fillCache(getFromCache)
}
}
};
function AD(location, opts) {
if (!(this instanceof AD)) {
return new AD(location, opts);
}
AbstractLevelDOWN.call(this, location);
this.container = new Storage(location, opts);
}
inherits(AD, AbstractLevelDOWN);
AD.prototype._open = function (options, callback) {
this.container.init(callback);
};
AD.prototype._multiPut = function (pairs, options, callback) {
var normalized = []
var err
pairs.every(([key, value]) => {
err = checkKeyValue(key, 'key');
if (err) return
if (checkKeyValue(value, 'value')) {
normalized.push([key, ''])
return true
}
if (value !== null
&& typeof value === 'object'
&& !Buffer.isBuffer(value)
&& value.buffer === undefined) {
var obj = {};
obj.storetype = 'json';
obj.data = value;
value = JSON.stringify(obj);
}
normalized.push([key, value])
return true
})
if (err) {
nextTick(() => callback(err))
} else {
this.container.setItems(normalized, callback)
}
}
AD.prototype._put = function (key, value, options, callback) {
return this._multiPut([[key, value]], options, callback)
};
AD.prototype._get = function (key, options, callback) {
var err = checkKeyValue(key, 'key');
if (err) {
return nextTick(() => callback(err));
}
if (!Buffer.isBuffer(key)) {
key = String(key);
}
this.container.getItem(key, function (err, value) {
if (err) {
return callback(err);
}
if (options.asBuffer !== false && !Buffer.isBuffer(value)) {
value = new Buffer(value);
}
if (options.asBuffer === false) {
if (value.indexOf('{"storetype":"json","data"') > -1) {
var res = JSON.parse(value);
value = res.data;
}
}
callback(null, value);
});
};
AD.prototype._multiDel = function (keys, options, callback) {
// Next tick so that any ADIterator has had enough time to make a snapshot.
// This is for sure a crappy solution, but neither iterator snapshot nor
// delete seem to be timely/urgent features. PRs welcomed :D
nextTick(() => {
var normalized = []
var err
keys.every((key) => {
err = checkKeyValue(key, 'key');
if (err) return
if (!Buffer.isBuffer(key)) {
key = String(key);
}
normalized.push(key)
return true
})
if (err) {
nextTick(() => callback(err))
} else {
this.container.removeItems(keys, callback)
}
})
}
AD.prototype._del = function (key, options, callback) {
return this._multiDel([key], options, callback)
};
AD.prototype._batch = function (array, options, callback) {
var self = this;
nextTick(function () {
var err;
var key;
var value;
if (!Array.isArray(array) || !array.length) return callback()
var toPut = []
var toDel = []
var deleted = {}
for (var i = 0; i < array.length; i++) {
var task = array[i];
if (!task) continue
key = Buffer.isBuffer(task.key) ? task.key : String(task.key);
err = checkKeyValue(key, 'key');
if (err) return callback(err)
if (task.type === 'del') {
deleted[task.key] = true
toDel.push(task.key)
} else if (task.type === 'put') {
value = Buffer.isBuffer(task.value) ? task.value : String(task.value);
if (checkKeyValue(value, 'value')) {
toPut.push([key, ''])
} else {
toPut.push([key, value])
}
}
}
var togo = 0
if (toDel.length) {
togo++
self._multiDel(toDel, null, checkDone)
}
if (toPut.length) {
toPut = toPut.filter(([key]) => !(key in deleted))
}
if (toPut.length) {
togo++
self._multiPut(toPut, null, checkDone)
}
togo++
checkDone() // kick things off
function checkDone (err) {
if (err) {
togo = 0
callback(err)
} else if (--togo === 0) {
callback()
}
}
});
};
AD.prototype._iterator = function (options) {
return new ADIterator(this, options);
};
AD.destroy = function (name, callback) {
StorageCore.destroy(name, callback);
};
function checkKeyValue(obj, type) {
if (obj === null || obj === undefined) {
return new Error(type + ' cannot be `null` or `undefined`');
}
if (obj instanceof Boolean) {
return new Error(type + ' cannot be a boolean');
}
if (obj === '') {
return new Error(type + ' cannot be an empty string');
}
if (obj.toString().indexOf('[object ArrayBuffer]') === 0) {
if (obj.byteLength === 0 || obj.byteLength === undefined) {
return new Error(type + ' cannot be an empty Buffer');
}
}
if (Buffer.isBuffer(obj)) {
if (obj.length === 0) {
return new Error(type + ' cannot be an empty Buffer');
}
} else if (String(obj) === '') {
return new Error(type + ' cannot be an empty String');
}
}
function asyncify (fn) {
if (fn._isAsync) {
return fn
}
var sync = true
nextTick(() => sync = false)
var ret = function () {
var ctx = this
var args = arguments
if (sync) {
nextTick(function () {
fn.apply(ctx, args)
})
} else {
fn.apply(ctx, args)
}
}
ret._isAsync = true
return ret
}
module.exports = AD;