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.

372 lines
8.8 KiB

var test = require('tape')
var leveldown = require('leveldown')
var iteratorStream = require('./')
var through2 = require('through2')
var tempy = require('tempy')
var addSecretListener = require('secret-event-listener')
var db
var data = [
{ type: 'put', key: 'foobatch1', value: 'bar1' },
{ type: 'put', key: 'foobatch2', value: 'bar2' },
{ type: 'put', key: 'foobatch3', value: 'bar3' }
]
test('setup', function (t) {
db = leveldown(tempy.directory())
db.open(function (err) {
t.error(err, 'no error')
db.batch(data, function (err) {
t.error(err, 'no error')
t.end()
})
})
})
test('keys and values', function (t) {
var idx = 0
var stream = iteratorStream(db.iterator())
stream.pipe(through2.obj(function (kv, _, done) {
t.ok(Buffer.isBuffer(kv.key))
t.ok(Buffer.isBuffer(kv.value))
t.equal(kv.key.toString(), data[idx].key)
t.equal(kv.value.toString(), data[idx].value)
idx++
done()
}, function () {
t.equal(idx, data.length)
stream.on('close', function () {
t.end()
})
}))
})
test('normal event order', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order.filter(withoutDataEvents), ['_end', 'end', 'close'])
t.end()
})
stream.resume()
})
test('error from iterator.next', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'error: next', 'close'], 'event order')
t.end()
})
iterator.next = function (cb) {
process.nextTick(cb, new Error('next'))
}
stream.resume()
})
test('error from iterator end', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var _end = iterator._end
var order = monitor(iterator, stream, function () {
t.same(order.filter(withoutDataEvents), ['_end', 'end', 'error: end', 'close'])
t.end()
})
iterator._end = function (cb) {
order.push('_end')
_end.call(this, function (err) {
t.ifError(err)
cb(new Error('end'))
})
}
stream.resume()
})
test('.destroy', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'close'])
t.end()
})
stream.destroy()
})
test('.destroy(err)', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'error: user', 'close'])
t.end()
})
stream.destroy(new Error('user'))
})
test('.destroy(err, callback)', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'callback', 'close'])
t.end()
})
stream.destroy(new Error('user'), function (err) {
order.push('callback')
t.is(err.message, 'user', 'got error')
})
})
test('.destroy(null, callback)', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'callback', 'close'])
t.end()
})
stream.destroy(null, function (err) {
order.push('callback')
t.ifError(err, 'no error')
})
})
test('.destroy() during iterator.next', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'close'], 'event order')
t.end()
})
iterator.next = function () {
stream.destroy()
}
stream.resume()
})
test('.destroy(err) during iterator.next', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'error: user', 'close'], 'event order')
t.end()
})
iterator.next = function (cb) {
stream.destroy(new Error('user'))
}
stream.resume()
})
test('.destroy(err, callback) during iterator.next', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'callback', 'close'], 'event order')
t.end()
})
iterator.next = function (cb) {
stream.destroy(new Error('user'), function (err) {
order.push('callback')
t.is(err.message, 'user', 'got error')
})
}
stream.resume()
})
test('.destroy(null, callback) during iterator.next', function (t) {
var iterator = db.iterator()
var stream = iteratorStream(iterator)
var order = monitor(iterator, stream, function () {
t.same(order, ['_end', 'callback', 'close'], 'event order')
t.end()
})
iterator.next = function (cb) {
stream.destroy(null, function (err) {
order.push('callback')
t.ifError(err, 'no error')
})
}
stream.resume()
})
test('.destroy during iterator.next 1', function (t) {
var stream
var iterator = db.iterator()
var next = iterator.next.bind(iterator)
iterator.next = function (cb) {
t.pass('should be called once')
next(cb)
stream.destroy()
}
stream = iteratorStream(iterator)
stream.on('data', function (data) {
t.fail('should not be called')
})
stream.on('close', t.end.bind(t))
})
test('.destroy during iterator.next 2', function (t) {
var stream
var iterator = db.iterator()
var next = iterator.next.bind(iterator)
var count = 0
iterator.next = function (cb) {
t.pass('should be called')
next(cb)
if (++count === 2) {
stream.destroy()
}
}
stream = iteratorStream(iterator)
stream.on('data', function (data) {
t.pass('should be called')
})
stream.on('close', t.end.bind(t))
})
test('.destroy after iterator.next 1', function (t) {
var stream
var iterator = db.iterator()
var next = iterator.next.bind(iterator)
iterator.next = function (cb) {
next(function (err, key, value) {
stream.destroy()
cb(err, key, value)
t.pass('should be called')
})
}
stream = iteratorStream(iterator)
stream.on('data', function (data) {
t.fail('should not be called')
})
stream.on('close', t.end.bind(t))
})
test('.destroy after iterator.next 2', function (t) {
var stream
var iterator = db.iterator()
var next = iterator.next.bind(iterator)
var count = 0
iterator.next = function (cb) {
next(function (err, key, value) {
if (++count === 2) {
stream.destroy()
}
cb(err, key, value)
t.pass('should be called')
})
}
stream = iteratorStream(iterator)
stream.on('data', function (data) {
t.pass('should be called')
})
stream.on('close', t.end.bind(t))
})
test('keys=false', function (t) {
var stream = iteratorStream(db.iterator(), { keys: false })
stream.once('data', function (value) {
stream.destroy()
t.equal(value.toString(), 'bar1')
t.end()
})
})
test('values=false', function (t) {
var stream = iteratorStream(db.iterator(), { values: false })
stream.once('data', function (key) {
stream.destroy()
t.equal(key.toString(), 'foobatch1')
t.end()
})
})
// It's important to keep a reference to the iterator at least until we end,
// to prevent GC of the iterator and therefor its db (esp. for native addons).
test('keeps a reference to the iterator', function (t) {
var it = db.iterator()
var stream = iteratorStream(it)
stream.on('close', function () {
t.is(stream._iterator, it, 'has reference')
t.end()
})
stream.resume()
})
// Note: also serves as teardown of above tests
test('it is safe to close db on end of stream', function (t) {
// Set highWaterMark to 0 so that we don't preemptively fetch.
var it = db.iterator({ highWaterMark: 0 })
var stream = iteratorStream(it)
stream.on('end', function () {
// Although the underlying iterator is still alive at this point (before
// the 'close' event has been emitted) it's safe to close the db because
// leveldown (v5) ends any open iterators before closing.
db.close(function (err) {
t.ifError(err, 'no error')
t.end()
})
})
stream.resume()
})
function monitor (iterator, stream, onClose) {
var order = []
;['_next', '_end'].forEach(function (method) {
var original = iterator[method]
iterator[method] = function () {
order.push(method)
original.apply(this, arguments)
}
})
;['data', 'end', 'error', 'close'].forEach(function (event) {
// Add listener without side effects (like triggering flowing mode)
addSecretListener(stream, event, function (err) {
if (event === 'error') order.push('error: ' + err.message)
else order.push(event)
})
})
if (onClose) {
addSecretListener(stream, 'close', onClose)
}
return order
}
function withoutDataEvents (event) {
return event !== '_next' && event !== 'data'
}