idb-kv-store
Version:
Persistent key-value store for web browsers backed by IndexedDB
793 lines (685 loc) • 17.7 kB
JavaScript
/* eslint-env browser */
var IdbKvStore = require('.')
var test = require('tape')
var runParallel = require('run-parallel-limit')
if (navigator.storage && navigator.storage.estimate) {
navigator.storage.estimate().then(function (se) {
console.log('Storage Estimate:', 'usage=' + se.usage, 'quota=' + se.quota)
})
}
test('create/get/set pre-ready', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.set('abc', 'def', function (err) {
t.equal(err, null)
store.get('abc', function (err, value) {
t.equal(err, null)
t.equal(value, 'def')
t.end()
})
})
})
test('create/get/set post-ready', function (t) {
t.timeoutAfter(3000)
var store = createStore(onopen)
function onopen (err) {
t.equal(err, null)
store.set('abc', 'def', function (err) {
t.equal(err, null)
store.get('abc', function (err, value) {
t.equal(err, null)
t.equal(value, 'def')
t.end()
})
})
}
})
test('set/get object', function (t) {
t.timeoutAfter(3000)
var store = createStore()
var val = {somekey: 'someval'}
store.set('abc', val, function (err) {
t.equal(err, null)
store.get('abc', function (err, value) {
t.equal(err, null)
t.deepEqual(value, val)
t.end()
})
})
})
test('set/getMultiple objects', function (t) {
t.timeoutAfter(3000)
var store = createStore()
var a = {a: 'a'}
var b = {b: 'b'}
var c = {c: 'c'}
store.set('a', a, function (err) {
t.equal(err, null)
store.set('b', b, function (err) {
t.equal(err, null)
store.set('c', c, function (err) {
t.equal(err, null)
store.getMultiple(['c', 'badkey', 'a', 'c', 'another-bad-key'], function (err, results) {
t.equal(err, null)
t.deepEqual(results[0], c)
t.deepEqual(results[1], undefined)
t.deepEqual(results[2], a)
t.deepEqual(results[3], c)
t.equal(results.length, 5)
t.end()
})
})
})
})
})
test('get empty', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.get('badkey', function (err, value) {
t.equal(err, null)
t.equal(value, undefined)
t.end()
})
})
test('promises', function (t) {
t.timeoutAfter(3000)
if (typeof Promise !== 'function') {
t.skip('Promises not supported')
t.end()
return
}
var store = createStore()
store.set('a', 1)
.then(function () {
return store.get('a')
})
.then(function (value) {
t.equal(value, 1)
return store.json()
})
.then(function (json) {
t.deepEqual(json, {a: 1})
return store.remove('a')
})
.then(function () {
return store.clear()
})
.then(function () {
return store.add('b', 2)
})
.then(function () {
return store.count()
})
.then(function (count) {
t.equal(count, 1)
return store.keys()
})
.then(function (keys) {
t.deepEqual(keys, ['b'])
t.end()
})
.catch(function (err) {
t.fail(err)
t.end()
})
})
test('json()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.json(function (err, json) {
t.equal(err, null)
t.deepEqual(json, {})
store.set('abc', 'def', function (err) {
t.equal(err, null)
store.json(function (err, json) {
t.equal(err, null)
t.deepEqual(json, {abc: 'def'})
t.end()
})
})
})
})
test('keys()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.keys(function (err, keys) {
t.equal(err, null)
t.deepEqual(keys, [])
store.set('abc', 'def', function (err) {
t.equal(err, null)
store.keys(function (err, keys) {
t.equal(err, null)
t.deepEqual(keys, ['abc'])
t.end()
})
})
})
})
test('error cases', function (t) {
t.timeoutAfter(3000)
t.throws(function () { return new IdbKvStore() })
t.throws(function () { return new IdbKvStore({}) })
var store = createStore()
t.throws(function () { store.get() })
t.throws(function () { store.set() })
t.throws(function () { store.set('key') })
t.throws(function () { store.remove() })
t.throws(function () { store.iterator() })
t.throws(function () { store.add() })
t.throws(function () { store.transaction('foo') })
t.end()
})
test('remove()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.remove('abc', function (err) {
t.equal(err, null)
store.set('abc', 'def', function (err) {
t.equal(err, null)
store.remove('abc', function (err) {
t.equal(err, null)
store.get('abc', function (err, value) {
t.equal(err, null)
t.equal(value, undefined)
t.end()
})
})
})
})
})
test('clear()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.clear(function (err) {
t.equal(err, null)
store.set('abc', 'def', function (err) {
t.equal(err, null)
store.clear(function (err) {
t.equal(err, null)
store.get('abc', function (err, value) {
t.equal(err, null)
t.equal(value, undefined)
t.end()
})
})
})
})
})
test('count()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.count(function (err, count) {
t.equal(err, null)
t.equal(count, 0)
store.set('abc', 'def', function (err) {
t.equal(err, null)
store.count(function (err, count) {
t.equal(err, null)
t.equal(count, 1)
t.end()
})
})
})
})
test('add()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
t.throws(function () { store.add() })
store.add('abc', 'def', function (err) {
t.equal(err, null)
store.add('abc', 'def', function (err) {
t.notEqual(err, null)
t.end()
})
})
})
test('single arg add()', function (t) {
t.timeoutAfter(3000)
var name = '' + (Math.round(9e16 * Math.random()))
var store = new IdbKvStore(name, function (err) {
t.equal(err, null)
store.add('foobar')
store.close()
})
store.on('close', function () {
store = new IdbKvStore(name)
store.json(function (err, json) {
t.equal(err, null)
t.deepEqual({1: 'foobar'}, json)
t.end()
})
})
})
test('close()', function (t) {
t.timeoutAfter(3000)
var store = createStore(function () {
store.close()
t.throws(function () { store.get('foo') })
t.throws(function () { store.set('foo', 'foo') })
t.end()
})
})
test('SUPPORT constants', function (t) {
t.equal(IdbKvStore.INDEXEDDB_SUPPORT, true)
t.ok('BROADCAST_SUPPORT' in IdbKvStore)
t.end()
})
test('open/close event', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.on('open', onOpen)
store.on('close', onClose)
function onOpen () {
store.close()
}
function onClose () {
t.end()
}
})
test('listen on add/set/remove events fail if not supported', function (t) {
t.timeoutAfter(3000)
if (IdbKvStore.BROADCAST_SUPPORT) return t.end()
var store = createStore()
store.once('error', addError)
store.on('add', function () {})
function addError (err) {
t.ok(err instanceof Error)
store.once('error', setError)
store.on('set', function () {})
}
function setError (err) {
t.ok(err instanceof Error)
store.once('error', removeError)
store.on('remove', function () {})
}
function removeError (err) {
t.ok(err instanceof Error)
t.end()
}
})
test('add/set/remove events', function (t) {
t.timeoutAfter(3000)
if (!IdbKvStore.BROADCAST_SUPPORT) return t.end()
var name = '' + (Math.round(9e16 * Math.random()))
var storeA = IdbKvStore(name)
var storeB = IdbKvStore(name)
storeA.on('add', fail)
storeA.on('set', fail)
storeA.on('remove', fail)
storeB.on('add', onAdd)
storeB.on('set', onSet)
storeB.on('remove', onRemove)
storeA.add('foo', 'bar', function (err) {
t.equal(err, null)
})
function onAdd (change) {
t.deepEqual({
method: 'add',
key: 'foo',
value: 'bar'
}, change)
storeA.set('foo', 'barbar', function (err) {
t.equal(err, null)
})
}
function onSet (change) {
t.deepEqual({
method: 'set',
key: 'foo',
value: 'barbar'
}, change)
storeA.remove('foo', function (err) {
t.equal(err, null)
})
}
function onRemove (change) {
t.deepEqual({
method: 'remove',
key: 'foo'
}, change)
t.end()
}
function fail () {
t.fail()
}
})
test('add() - autoIncremement key', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.set('foo', 'bar', function (err) {
t.equal(err, null)
store.add('foobar', function (err) {
t.equal(err, null)
store.json(function (err, json) {
t.equal(err, null)
t.deepEqual(json, {1: 'foobar', 'foo': 'bar'})
t.end()
})
})
})
})
test('values()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
store.add('A', function (err) {
t.equal(err, null)
store.add('B', function (err) {
t.equal(err, null)
store.values(function (err, values) {
t.equal(err, null)
store.json(console.log)
t.deepEqual(values, ['A', 'B'])
t.end()
})
})
})
})
test('broadcast event event with no listener', function (t) {
t.timeoutAfter(3000)
if (!IdbKvStore.BROADCAST_SUPPORT) return t.end()
var name = '' + (Math.round(9e16 * Math.random()))
var storeA = IdbKvStore(name)
var storeB = IdbKvStore(name)
storeB.on('add', onAdd)
storeA.add('foo', 'bar')
function onAdd (change) {
t.deepEqual({
method: 'add',
key: 'foo',
value: 'bar'
}, change)
t.end()
}
})
test('transaction', function (t) {
t.timeoutAfter(3000)
var store = createStore()
var transaction = store.transaction()
transaction.onfinish = onfinish
transaction.add('foo')
transaction.set(1, 'bar')
transaction.set(2, 'bar')
transaction.remove(2)
transaction.json(function (err, json) {
t.equal(err, null)
t.deepEqual({1: 'bar'}, json)
})
function onfinish (err) {
t.equal(err, null)
t.end()
}
})
test('transaction abort', function (t) {
t.timeoutAfter(3000)
t.plan(5)
var store = createStore()
var transaction = store.transaction()
transaction.onfinish = onfinish
transaction.add('foo', function (err) {
t.ok(err instanceof Error)
})
transaction.abort()
t.throws(function () { transaction.add('bar') })
function onfinish (err) {
t.ok(err instanceof Error)
store.json(function (err, json) {
t.equal(err, null)
t.deepEqual({}, json)
})
}
})
test('iterator()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
var transaction = store.transaction()
var count = 0
transaction.onfinish = onfinish
transaction.add('foo')
transaction.add('bar')
function onfinish (err) {
t.equal(err, null)
store.iterator(iter)
}
function iter (err, cursor) {
t.equal(err, null)
count++
if (count === 1) {
t.notEqual(cursor, null)
t.equal(cursor.key, 1)
t.equal(cursor.value, 'foo')
cursor.continue()
} else if (count === 2) {
t.notEqual(cursor, null)
t.equal(cursor.key, 2)
t.equal(cursor.value, 'bar')
cursor.continue()
} else if (count === 3) {
t.equal(cursor, null)
t.end()
}
}
})
test('write on readonly fails', function (t) {
t.timeoutAfter(3000)
var store = createStore()
var transaction = store.transaction('readonly')
transaction.add('foobar', function (err) {
t.ok(err instanceof DOMException)
transaction.set('foo', 'bar', function (err) {
t.ok(err instanceof DOMException)
transaction.remove('foobar', function (err) {
t.ok(err instanceof DOMException)
transaction.clear(function (err) {
t.ok(err instanceof DOMException)
t.end()
})
})
})
})
})
test('transaction cleanup', function (t) {
t.timeoutAfter(3000)
var store = createStore()
var trans = store.transaction('readonly')
trans.onfinish = function (err) {
t.equal(err, null)
t.equal(trans.finished, true)
t.throws(function () { trans.get('foo') })
t.end()
}
})
test('close db closes transactions', function (t) {
t.timeoutAfter(3000)
t.plan(3)
var store = createStore()
var trans = store.transaction('readonly')
trans.get('foo', function (err, result) {
t.ok(err instanceof Error)
t.equal(result, undefined)
})
trans.onfinish = function (err) {
t.ok(err instanceof Error)
}
store.close()
})
test('transaction before close completes', function (t) {
t.timeoutAfter(3000)
t.plan(4)
var name = '' + (Math.round(9e16 * Math.random()))
var store = new IdbKvStore(name, function (err) {
t.equal(err, null)
store.add('foobar', function (err) {
t.equal(err, null)
})
store.close()
store = new IdbKvStore(name)
store.json(function (err, json) {
t.equal(err, null)
t.deepEqual({1: 'foobar'}, json)
})
})
})
test('close then open is successful', function (t) {
t.timeoutAfter(3000)
var name = '' + (Math.round(9e16 * Math.random()))
var store = new IdbKvStore(name)
store.close()
store.close()
store = new IdbKvStore(name, function (err) {
t.equal(err, null)
t.end()
})
})
test('ranged iterator()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
var transaction = store.transaction()
transaction.add(1)
transaction.add(2)
transaction.add(3)
transaction.onfinish = function (err) {
t.equal(err, null)
var count = 1
store.iterator(IDBKeyRange.upperBound(2), function (err, cursor) {
t.equal(err, null)
if (count < 3) {
t.notEqual(cursor, null)
t.equal(cursor.key, count)
t.equal(cursor.value, count)
cursor.continue()
} else if (count === 3) {
t.equal(cursor, null)
t.end()
} else {
t.fail()
}
count++
})
}
})
test('ranged keys(), values(), json(), and count()', function (t) {
t.timeoutAfter(3000)
var store = createStore()
var transaction = store.transaction()
transaction.add(1)
transaction.add(2)
transaction.add(3)
transaction.onfinish = function (err) {
t.equal(err, null)
store.json(IDBKeyRange.only(2), function (err, json) {
t.equal(err, null)
t.deepEqual(json, {2: 2})
store.values(IDBKeyRange.lowerBound(2), function (err, values) {
t.equal(err, null)
t.deepEqual(values, [2, 3])
store.keys(IDBKeyRange.upperBound(2), function (err, keys) {
t.equal(err, null)
t.deepEqual(keys, [1, 2])
store.count(IDBKeyRange.only(2), function (err, count) {
t.equal(err, null)
t.equal(count, 1)
t.end()
})
})
})
})
}
})
test('transaction ordering on open', function (t) {
t.timeoutAfter(3000)
// NOTE: Sometimes IE & Edge will fail this due to not following idb spec
var store = createStore(function (err) {
t.equal(err, null)
store.add('second')
})
store.once('open', function () {
store.add('third', function (err) {
t.equal(err, null)
store.json(function (err, json) {
t.equal(err, null)
t.deepEqual(json, {
1: 'first',
2: 'second',
3: 'third'
})
t.end()
})
})
})
store.add('first')
})
test('transaction cb', function (t) {
t.timeoutAfter(3000)
t.plan(5)
var store = createStore()
var trans1 = store.transaction(onfinish1)
t.ok(trans1.done == null)
trans1.set('foo', 'bar')
function onfinish1 (err) {
t.equal(err, null)
var trans2 = store.transaction('readonly', onfinish2)
trans2.get('foo', function (err, val) {
t.equal(err, null)
t.equal(val, 'bar')
})
}
function onfinish2 (err) {
t.equal(err, null)
}
})
test('transaction promise', function (t) {
if (typeof Promise !== 'function') {
t.skip('Promises not supported')
t.end()
return
}
t.timeoutAfter(3000)
t.plan(4)
var store = createStore()
var trans1 = store.transaction()
trans1.done
.then(onfinish1)
.catch(function () {
t.fail()
})
trans1.set('foo', 'bar', function (err) {
t.equal(err, null)
})
function onfinish1 () {
var trans2 = store.transaction('readonly')
trans2.done
.then(onfinish2)
.catch(function () { t.fail() })
trans2.get('foo', function (err, val) {
t.equal(err, null)
t.equal(val, 'bar')
})
}
function onfinish2 () {
t.pass('transaction onfinish called')
}
})
test.skip('benchmark', function (t) {
var buffSize = 4 * 1024
var storeCount = 10000
var buffer = Buffer.alloc(buffSize)
var store = createStore()
var tasks = []
for (var i = 0; i < storeCount; i++) {
tasks.push(function (cb) { store.add(buffer, cb) })
}
console.log('Storing', storeCount, 'elements at', buffSize / 1024, 'kB per elements')
var start = new Date().getTime()
runParallel(tasks, 10, function (err) {
t.equal(err, null)
var totalTime = (new Date().getTime() - start) / 1000
var totalSize = buffSize * storeCount / 1024
var throughput = Math.round(totalSize / totalTime)
console.log('total time =', totalTime, 'seconds. throughput =', throughput, 'kB/seconds')
t.end()
})
})
function createStore (cb) {
var name = '' + (Math.round(9e16 * Math.random()))
return new IdbKvStore(name, cb)
}