239 lines
6.7 KiB
JavaScript
239 lines
6.7 KiB
JavaScript
|
|
'use strict'
|
||
|
|
|
||
|
|
const { AbstractIterator } = require('abstract-level')
|
||
|
|
const createKeyRange = require('./util/key-range')
|
||
|
|
const deserialize = require('./util/deserialize')
|
||
|
|
|
||
|
|
const kCache = Symbol('cache')
|
||
|
|
const kFinished = Symbol('finished')
|
||
|
|
const kOptions = Symbol('options')
|
||
|
|
const kCurrentOptions = Symbol('currentOptions')
|
||
|
|
const kPosition = Symbol('position')
|
||
|
|
const kLocation = Symbol('location')
|
||
|
|
const kFirst = Symbol('first')
|
||
|
|
const emptyOptions = {}
|
||
|
|
|
||
|
|
class Iterator extends AbstractIterator {
|
||
|
|
constructor (db, location, options) {
|
||
|
|
super(db, options)
|
||
|
|
|
||
|
|
this[kCache] = []
|
||
|
|
this[kFinished] = this.limit === 0
|
||
|
|
this[kOptions] = options
|
||
|
|
this[kCurrentOptions] = { ...options }
|
||
|
|
this[kPosition] = undefined
|
||
|
|
this[kLocation] = location
|
||
|
|
this[kFirst] = true
|
||
|
|
}
|
||
|
|
|
||
|
|
// Note: if called by _all() then size can be Infinity. This is an internal
|
||
|
|
// detail; by design AbstractIterator.nextv() does not support Infinity.
|
||
|
|
_nextv (size, options, callback) {
|
||
|
|
this[kFirst] = false
|
||
|
|
|
||
|
|
if (this[kFinished]) {
|
||
|
|
return this.nextTick(callback, null, [])
|
||
|
|
} else if (this[kCache].length > 0) {
|
||
|
|
// TODO: mixing next and nextv is not covered by test suite
|
||
|
|
size = Math.min(size, this[kCache].length)
|
||
|
|
return this.nextTick(callback, null, this[kCache].splice(0, size))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Adjust range by what we already visited
|
||
|
|
if (this[kPosition] !== undefined) {
|
||
|
|
if (this[kOptions].reverse) {
|
||
|
|
this[kCurrentOptions].lt = this[kPosition]
|
||
|
|
this[kCurrentOptions].lte = undefined
|
||
|
|
} else {
|
||
|
|
this[kCurrentOptions].gt = this[kPosition]
|
||
|
|
this[kCurrentOptions].gte = undefined
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let keyRange
|
||
|
|
|
||
|
|
try {
|
||
|
|
keyRange = createKeyRange(this[kCurrentOptions])
|
||
|
|
} catch (_) {
|
||
|
|
// The lower key is greater than the upper key.
|
||
|
|
// IndexedDB throws an error, but we'll just return 0 results.
|
||
|
|
this[kFinished] = true
|
||
|
|
return this.nextTick(callback, null, [])
|
||
|
|
}
|
||
|
|
|
||
|
|
const transaction = this.db.db.transaction([this[kLocation]], 'readonly')
|
||
|
|
const store = transaction.objectStore(this[kLocation])
|
||
|
|
const entries = []
|
||
|
|
|
||
|
|
if (!this[kOptions].reverse) {
|
||
|
|
let keys
|
||
|
|
let values
|
||
|
|
|
||
|
|
const complete = () => {
|
||
|
|
// Wait for both requests to complete
|
||
|
|
if (keys === undefined || values === undefined) return
|
||
|
|
|
||
|
|
const length = Math.max(keys.length, values.length)
|
||
|
|
|
||
|
|
if (length === 0 || size === Infinity) {
|
||
|
|
this[kFinished] = true
|
||
|
|
} else {
|
||
|
|
this[kPosition] = keys[length - 1]
|
||
|
|
}
|
||
|
|
|
||
|
|
// Resize
|
||
|
|
entries.length = length
|
||
|
|
|
||
|
|
// Merge keys and values
|
||
|
|
for (let i = 0; i < length; i++) {
|
||
|
|
const key = keys[i]
|
||
|
|
const value = values[i]
|
||
|
|
|
||
|
|
entries[i] = [
|
||
|
|
this[kOptions].keys && key !== undefined ? deserialize(key) : undefined,
|
||
|
|
this[kOptions].values && value !== undefined ? deserialize(value) : undefined
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
maybeCommit(transaction)
|
||
|
|
}
|
||
|
|
|
||
|
|
// If keys were not requested and size is Infinity, we don't have to keep
|
||
|
|
// track of position and can thus skip getting keys.
|
||
|
|
if (this[kOptions].keys || size < Infinity) {
|
||
|
|
store.getAllKeys(keyRange, size < Infinity ? size : undefined).onsuccess = (ev) => {
|
||
|
|
keys = ev.target.result
|
||
|
|
complete()
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
keys = []
|
||
|
|
this.nextTick(complete)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this[kOptions].values) {
|
||
|
|
store.getAll(keyRange, size < Infinity ? size : undefined).onsuccess = (ev) => {
|
||
|
|
values = ev.target.result
|
||
|
|
complete()
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
values = []
|
||
|
|
this.nextTick(complete)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// Can't use getAll() in reverse, so use a slower cursor that yields one item at a time
|
||
|
|
// TODO: test if all target browsers support openKeyCursor
|
||
|
|
const method = !this[kOptions].values && store.openKeyCursor ? 'openKeyCursor' : 'openCursor'
|
||
|
|
|
||
|
|
store[method](keyRange, 'prev').onsuccess = (ev) => {
|
||
|
|
const cursor = ev.target.result
|
||
|
|
|
||
|
|
if (cursor) {
|
||
|
|
const { key, value } = cursor
|
||
|
|
this[kPosition] = key
|
||
|
|
|
||
|
|
entries.push([
|
||
|
|
this[kOptions].keys && key !== undefined ? deserialize(key) : undefined,
|
||
|
|
this[kOptions].values && value !== undefined ? deserialize(value) : undefined
|
||
|
|
])
|
||
|
|
|
||
|
|
if (entries.length < size) {
|
||
|
|
cursor.continue()
|
||
|
|
} else {
|
||
|
|
maybeCommit(transaction)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
this[kFinished] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If an error occurs (on the request), the transaction will abort.
|
||
|
|
transaction.onabort = () => {
|
||
|
|
callback(transaction.error || new Error('aborted by user'))
|
||
|
|
callback = null
|
||
|
|
}
|
||
|
|
|
||
|
|
transaction.oncomplete = () => {
|
||
|
|
callback(null, entries)
|
||
|
|
callback = null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_next (callback) {
|
||
|
|
if (this[kCache].length > 0) {
|
||
|
|
const [key, value] = this[kCache].shift()
|
||
|
|
this.nextTick(callback, null, key, value)
|
||
|
|
} else if (this[kFinished]) {
|
||
|
|
this.nextTick(callback)
|
||
|
|
} else {
|
||
|
|
let size = Math.min(100, this.limit - this.count)
|
||
|
|
|
||
|
|
if (this[kFirst]) {
|
||
|
|
// It's common to only want one entry initially or after a seek()
|
||
|
|
this[kFirst] = false
|
||
|
|
size = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
this._nextv(size, emptyOptions, (err, entries) => {
|
||
|
|
if (err) return callback(err)
|
||
|
|
this[kCache] = entries
|
||
|
|
this._next(callback)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_all (options, callback) {
|
||
|
|
this[kFirst] = false
|
||
|
|
|
||
|
|
// TODO: mixing next and all is not covered by test suite
|
||
|
|
const cache = this[kCache].splice(0, this[kCache].length)
|
||
|
|
const size = this.limit - this.count - cache.length
|
||
|
|
|
||
|
|
if (size <= 0) {
|
||
|
|
return this.nextTick(callback, null, cache)
|
||
|
|
}
|
||
|
|
|
||
|
|
this._nextv(size, emptyOptions, (err, entries) => {
|
||
|
|
if (err) return callback(err)
|
||
|
|
if (cache.length > 0) entries = cache.concat(entries)
|
||
|
|
callback(null, entries)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
_seek (target, options) {
|
||
|
|
this[kFirst] = true
|
||
|
|
this[kCache] = []
|
||
|
|
this[kFinished] = false
|
||
|
|
this[kPosition] = undefined
|
||
|
|
|
||
|
|
// TODO: not covered by test suite
|
||
|
|
this[kCurrentOptions] = { ...this[kOptions] }
|
||
|
|
|
||
|
|
let keyRange
|
||
|
|
|
||
|
|
try {
|
||
|
|
keyRange = createKeyRange(this[kOptions])
|
||
|
|
} catch (_) {
|
||
|
|
this[kFinished] = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if (keyRange !== null && !keyRange.includes(target)) {
|
||
|
|
this[kFinished] = true
|
||
|
|
} else if (this[kOptions].reverse) {
|
||
|
|
this[kCurrentOptions].lte = target
|
||
|
|
} else {
|
||
|
|
this[kCurrentOptions].gte = target
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
exports.Iterator = Iterator
|
||
|
|
|
||
|
|
function maybeCommit (transaction) {
|
||
|
|
// Commit (meaning close) now instead of waiting for auto-commit
|
||
|
|
if (typeof transaction.commit === 'function') {
|
||
|
|
transaction.commit()
|
||
|
|
}
|
||
|
|
}
|