├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - '0.12' 5 | - 'iojs' 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rollbackdb 2 | 3 | A simple key/value database with fast rollback support. 4 | 5 | ``` 6 | npm install rollbackdb 7 | ``` 8 | 9 | [![build status](http://img.shields.io/travis/mafintosh/rollbackdb.svg?style=flat)](http://travis-ci.org/mafintosh/rollbackdb) 10 | ![dat](http://img.shields.io/badge/Development%20sponsored%20by-dat-green.svg?style=flat) 11 | 12 | ## Usage 13 | 14 | ``` js 15 | var rollbackdb = require('rollbackdb') 16 | // OBS: currently you have use the latest leveldown (>=1.2.0) 17 | var db = require('levelup')('db', {db: require('leveldown')}) 18 | 19 | var rdb = rollbackdb(db) 20 | 21 | rdb.put('hello', 'world', function () { 22 | var oldChange = rdb.changes 23 | console.log('added hello world at change', rdb.changes) 24 | rdb.put('hello', 'not world', function () { 25 | console.log('added hello not world at change', rdb.changes) 26 | rdb.get('hello', {version: oldChange}, console.log) // prints 'world' 27 | }) 28 | }) 29 | ``` 30 | 31 | ## API 32 | 33 | #### `rdb = rollbackdb(levelup, [options])` 34 | 35 | Create a new rollbackdb instance. You can set `version` in options 36 | if you want to rollback the database to a previous version 37 | 38 | #### `checkoutRdb = rdb.checkout(version)` 39 | 40 | Shorthand for setting the `version` option in the constructor. 41 | Returns a new instance. 42 | 43 | #### `rdb.get(key, [options], cb)` 44 | 45 | Get a key. Set `version` in the options map to get the key that 46 | was added in a previous version of this database. 47 | 48 | #### `rdb.put(key, value, [cb])` 49 | 50 | Insert a key. 51 | 52 | #### `rdb.del(key, [cb])` 53 | 54 | Delete a key. 55 | 56 | #### `rdb.batch(batch, [cb])` 57 | 58 | Insert and/or deletes a batch of key/values 59 | 60 | ``` js 61 | rdb.batch([{ 62 | type: 'put', 63 | key: 'a', 64 | value: 'a' 65 | }, { 66 | type: 'del', 67 | key: 'b', 68 | value: 'b' 69 | }], function (err) { 70 | console.log('batch finished') 71 | }) 72 | ``` 73 | 74 | #### `rdb.changes` 75 | 76 | A property containing the total number of changes/versions added to this database 77 | 78 | #### `rs = rdb.createChangesStream([options])` 79 | 80 | Returns a readable stream of all changes added to this database. 81 | 82 | You can limit the range of changes returned by using the following options 83 | 84 | ``` js 85 | { 86 | gt: changeNumber, 87 | gte: changeNumber, 88 | lt: changeNumber, 89 | lte: changeNumber 90 | } 91 | ``` 92 | 93 | If you only want a single change stream returned set `options.change = changeNumber` 94 | 95 | #### `rs = rdb.createReadStream([options])` 96 | 97 | Create a readable stream of all keys and values in the database. 98 | Options include 99 | 100 | ``` js 101 | { 102 | gt: 'keys-must-be-greater-than-me', 103 | gte: 'keys-must-be-greater-or-equal-to-me', 104 | lt: 'keys-must-be-less-than-me', 105 | lte: 'keys-must-be-less-or-equal-to-me', 106 | version: aVersionNumber // read all values in the database at this version 107 | } 108 | ``` 109 | 110 | #### `ws = rdb.createWriteStream()` 111 | 112 | Returns a writable stream that you can write `{type: 'put'|'del', key: key, value: value}` pairs to. 113 | All values written will be added to the same version. 114 | 115 | ## License 116 | 117 | MIT 118 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var thunky = require('thunky') 2 | var lexint = require('lexicographic-integer') 3 | var from = require('from2') 4 | var bulk = require('bulk-write-stream') 5 | 6 | var noop = function () {} 7 | 8 | var deleted = function (value) { 9 | return value && value.length === 1 && value[0] === 0x20 10 | } 11 | 12 | var checkSeek = function (ite) { 13 | if (ite.seek) return 14 | throw new Error('Sorry! rollbackdb currently only works with the latest version of leveldown. This might change in the future') 15 | } 16 | 17 | var rollbackdb = function (db, opts) { 18 | if (!opts) opts = {} 19 | 20 | var that = {} 21 | var freeIterators = [] 22 | var pf = opts.prefix ? '!' + opts.prefix + '!' : '' 23 | 24 | var open = thunky(function (cb) { 25 | var onopen = function () { 26 | var ite = db.db.iterator({ 27 | gt: pf + 'changes!', 28 | lt: pf + 'changes!\xff', 29 | reverse: true, 30 | limit: 1 31 | }) 32 | ite.next(function (err, key) { 33 | ite.end(function () { 34 | if (err) return cb(err) 35 | that.changes = key ? lexint.unpack(key.toString().slice(pf.length + 'changes!'.length).split('!')[0], 'hex') : 0 36 | cb(null, db.db, db) 37 | }) 38 | }) 39 | } 40 | 41 | if (db.isOpen()) onopen() 42 | else db.open(onopen) 43 | }) 44 | 45 | that.version = opts.checkout || opts.version || 0 46 | that.changes = 0 47 | 48 | that.checkout = function (change) { 49 | return rollbackdb(db, {checkout: change}) 50 | } 51 | 52 | that.del = function (key, cb) { 53 | that.put(key, ' ', cb) // use ' ' as whiteout for now ... 54 | } 55 | 56 | that.put = function (key, value, cb) { 57 | if (!cb) cb = noop 58 | open(function (err, down) { 59 | if (err) return cb(err) 60 | 61 | var change = ++that.changes 62 | var dbKey = pf + 'data!' + key + '!' + lexint.pack(change, 'hex') + '!' 63 | 64 | down.batch([{ 65 | type: 'put', 66 | key: pf + 'changes!' + lexint.pack(change, 'hex') + '!' + lexint.pack(0, 'hex'), 67 | value: dbKey 68 | }, { 69 | type: 'put', 70 | key: dbKey, 71 | value: value 72 | }], cb) 73 | }) 74 | } 75 | 76 | that.status = function (cb) { 77 | open(function (err) { 78 | if (err) return cb(err) 79 | cb(null, {changes: that.changes}) 80 | }) 81 | } 82 | 83 | that.batch = function (batch, opts, cb) { 84 | if (typeof opts === 'function') return that.batch(batch, null, opts) 85 | if (!opts) opts = {} 86 | if (!cb) cb = noop 87 | 88 | open(function (err, down) { 89 | if (err) return cb(err) 90 | 91 | var change = opts.change || (that.changes + 1) 92 | if (!opts.tick) opts.tick = 0 93 | if (change > that.changes) that.changes = change 94 | 95 | var wrap = new Array(2 * batch.length) 96 | var suffix = '!' + lexint.pack(change, 'hex') + '!' 97 | var changePrefix = pf + 'changes!' + lexint.pack(change, 'hex') + '!' 98 | var dataPrefix = pf + 'data!' 99 | 100 | for (var i = 0; i < batch.length; i++) { 101 | var key = batch[i].key 102 | var tick = lexint.pack(opts.tick++, 'hex') 103 | var dbKey = dataPrefix + key + suffix + tick 104 | 105 | wrap[2 * i] = { 106 | type: 'put', 107 | key: dbKey, 108 | value: batch[i].value || ' ' 109 | } 110 | wrap[2 * i + 1] = { 111 | type: 'put', 112 | key: changePrefix + tick, 113 | value: dbKey 114 | } 115 | } 116 | 117 | down.batch(wrap, cb) 118 | }) 119 | } 120 | 121 | var free = function (ite) { 122 | if (freeIterators.length < 10) freeIterators.push(ite) 123 | else ite.end(noop) 124 | } 125 | 126 | that.get = function (key, opts, cb) { 127 | if (typeof opts === 'function') return that.get(key, null, opts) 128 | if (!opts) opts = {} 129 | 130 | open(function (err, down) { 131 | if (err) return cb(err) 132 | 133 | var ite = freeIterators.shift() || down.iterator({reverse: true}) 134 | var prefix = pf + 'data!' + key + '!' 135 | 136 | checkSeek(ite) 137 | ite.seek(prefix + lexint.pack(opts.version || that.version || that.changes, 'hex') + '!~') 138 | ite.next(function (err, key, value) { 139 | if (err) return cb(err) 140 | free(ite) 141 | if (key && key.toString('utf-8', 0, prefix.length) !== prefix) return cb(null, null) 142 | if (deleted(value)) return cb(null, null) 143 | cb(null, value || null) 144 | }) 145 | }) 146 | } 147 | 148 | that.createChangesStream = function (opts) { 149 | if (!opts) opts = {} 150 | 151 | var openIterator = thunky(function (cb) { 152 | open(function (err, down) { 153 | if (err) return cb(err) 154 | 155 | var prefix = pf + 'changes!' 156 | if (opts.change) prefix += lexint.pack(opts.change, 'hex') + '!' 157 | 158 | var ite = down.iterator({ 159 | gte: opts.gt === undefined ? (prefix + (opts.gte !== undefined ? lexint.pack(opts.gte, 'hex') : '')) : undefined, 160 | lte: opts.lt === undefined ? (prefix + (opts.lte !== undefined ? lexint.pack(opts.lte, 'hex') + '!~' : '~')) : undefined, 161 | gt: opts.gte === undefined ? (prefix + (opts.gt !== undefined ? lexint.pack(opts.gt, 'hex') + '!~' : '')) : undefined, 162 | lt: opts.lte === undefined ? (prefix + (opts.lt !== undefined ? lexint.pack(opts.lt, 'hex') : '~')) : undefined 163 | }) 164 | 165 | cb(null, ite, down) 166 | }) 167 | }) 168 | 169 | var end = function (ite, cb) { 170 | ite.end(function () { 171 | cb(null, null) 172 | }) 173 | } 174 | 175 | return from.obj(function (size, cb) { 176 | openIterator(function (err, ite, down) { 177 | if (err) return cb(err) 178 | 179 | ite.next(function (err, key, value) { 180 | if (err) return cb(err) 181 | if (!key) return end(ite, cb) 182 | 183 | key = key.toString() 184 | 185 | var i = key.lastIndexOf('!') 186 | var index = lexint.unpack(key.slice(i + 1), 'hex') 187 | var j = key.lastIndexOf('!', i - 1) 188 | var change = lexint.unpack(key.slice(j + 1, i), 'hex') 189 | 190 | var dbKey = value.toString() 191 | i = dbKey.lastIndexOf('!', dbKey.lastIndexOf('!') - 1) 192 | j = dbKey.lastIndexOf('!', i - 1) 193 | 194 | down.get(dbKey, function (err, val) { 195 | if (err) return cb(err) 196 | 197 | cb(null, { 198 | change: change, 199 | index: index, 200 | key: dbKey.slice(j + 1, i), 201 | value: deleted(val) ? null : val 202 | }) 203 | }) 204 | }) 205 | }) 206 | }) 207 | } 208 | 209 | that.createWriteStream = function () { 210 | var opts = {change: 0, tick: 0} 211 | return bulk.obj(function (batch, cb) { 212 | open(function () { 213 | if (!opts.change) opts.change = that.changes + 1 214 | that.batch(batch, opts, cb) 215 | }) 216 | }) 217 | } 218 | 219 | that.createReadStream = function (opts) { 220 | if (!opts) opts = {} 221 | 222 | var openIterator = thunky(function (cb) { 223 | open(function (err, down) { 224 | if (err) return cb(err) 225 | var forward = down.iterator({highWaterMark: 1}) 226 | var backward = down.iterator({reverse: true, highWaterMark: 1}) 227 | checkSeek(forward) 228 | cb(null, forward, backward) 229 | }) 230 | }) 231 | 232 | var next = pf + 'data!' 233 | if (opts.gt) next = pf + 'data!' + opts.gt + '!~' 234 | if (opts.gte) next = pf + 'data!' + opts.gte + '!' 235 | 236 | var lt = opts.lt && pf + 'data!' + opts.lt + '!' 237 | var lte = opts.lte && pf + 'data!' + opts.lte + '!' 238 | 239 | var end = function (cb) { 240 | openIterator(function (err, backward, forward) { 241 | if (err) return cb(err) 242 | backward.end(function () { 243 | forward.end(function () { 244 | cb(null, null) 245 | }) 246 | }) 247 | }) 248 | } 249 | 250 | return from.obj(function read (size, cb) { 251 | openIterator(function (err, forward, backward) { 252 | if (err) return cb(err) 253 | 254 | var checkout = opts.version || that.version || that.changes 255 | 256 | forward.seek(next) 257 | forward.next(function (err, key, val) { 258 | if (err) return cb(err) 259 | if (!key) return end(cb) 260 | 261 | key = key.toString() 262 | var i = key.indexOf('!', 5 + pf.length) // data! 263 | var prefix = key.slice(0, i + 1) 264 | 265 | if (lt && prefix >= lt) return end(cb) 266 | if (lte && prefix > lte) return end(cb) 267 | 268 | next = prefix + '~' 269 | backward.seek(prefix + lexint.pack(checkout, 'hex') + '!~') 270 | backward.next(function (err, key, value) { 271 | if (key.toString('utf-8', 0, prefix.length) !== prefix) return read(size, cb) 272 | if (deleted(value)) return read(size, cb) 273 | if (lte && prefix === lte) lt = lte 274 | 275 | cb(null, { 276 | key: key.toString('utf-8', 5 + pf.length, i), 277 | value: value 278 | }) 279 | }) 280 | }) 281 | }) 282 | }) 283 | } 284 | 285 | return that 286 | } 287 | 288 | module.exports = rollbackdb 289 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollbackdb", 3 | "version": "2.0.0", 4 | "description": "A simple key/value database with fast rollback support.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "bulk-write-stream": "^1.0.0", 8 | "from2": "^1.3.0", 9 | "lexicographic-integer": "^1.1.0", 10 | "through2": "^0.6.5", 11 | "thunky": "^0.1.0" 12 | }, 13 | "devDependencies": { 14 | "leveldown": "^1.2.0", 15 | "levelup": "^1.1.1", 16 | "rimraf": "^2.3.4", 17 | "tape": "^4.0.0" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/mafintosh/rollbackdb.git" 22 | }, 23 | "scripts": { 24 | "test": "tape test.js" 25 | }, 26 | "author": "Mathias Buus (@mafintosh)", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/mafintosh/rollbackdb/issues" 30 | }, 31 | "homepage": "https://github.com/mafintosh/rollbackdb" 32 | } 33 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var rollbackdb = require('./') 3 | var leveldown = require('leveldown') 4 | var levelup = require('levelup') 5 | var rimraf = require('rimraf') 6 | var os = require('os') 7 | var path = require('path') 8 | 9 | var tick = 0 10 | var prev 11 | 12 | var create = function (opts) { 13 | var tmp = path.join(os.tmpdir(), 'rollbackdb-' + process.pid + '-' + ++tick) 14 | rimraf.sync(tmp) 15 | if (prev) rimraf.sync(prev) 16 | prev = tmp 17 | return rollbackdb(levelup(tmp, {db: leveldown}), opts) 18 | } 19 | 20 | tape('put + get', function (t) { 21 | var db = create() 22 | db.put('hello', 'world', function (err) { 23 | t.error(err, 'no error') 24 | t.same(db.changes, 1, 'one change') 25 | db.get('hello', function (err, val) { 26 | t.same(val.toString(), 'world') 27 | t.end() 28 | }) 29 | }) 30 | }) 31 | 32 | tape('put + put + get', function (t) { 33 | var db = create() 34 | db.put('hello', 'world', function (err) { 35 | t.error(err, 'no error') 36 | db.put('hello', 'not world', function (err) { 37 | db.get('hello', function (err, val) { 38 | t.same(val.toString(), 'not world') 39 | t.end() 40 | }) 41 | }) 42 | }) 43 | }) 44 | 45 | tape('put + put + checkout get', function (t) { 46 | var db = create() 47 | db.put('hello', 'world', function (err) { 48 | t.error(err, 'no error') 49 | db.put('hello', 'not world', function (err) { 50 | db.get('hello', {version: 1}, function (err, val) { 51 | t.same(val.toString(), 'world') 52 | t.end() 53 | }) 54 | }) 55 | }) 56 | }) 57 | 58 | tape('readstream', function (t) { 59 | var db = create() 60 | db.batch([{ 61 | type: 'put', 62 | key: 'a', 63 | value: 'a' 64 | }, { 65 | type: 'put', 66 | key: 'b', 67 | value: 'b' 68 | }, { 69 | type: 'put', 70 | key: 'c', 71 | value: 'c' 72 | }], function (err) { 73 | t.error(err, 'no error') 74 | 75 | var rs = db.createReadStream() 76 | var expected = ['a', 'b', 'c'] 77 | 78 | rs.on('data', function (data) { 79 | t.same(expected.shift(), data.value.toString()) 80 | }) 81 | 82 | rs.on('end', function () { 83 | t.same(expected.length, 0, 'no more data') 84 | t.end() 85 | }) 86 | }) 87 | }) 88 | 89 | tape('readstream override', function (t) { 90 | var db = create() 91 | db.batch([{ 92 | type: 'put', 93 | key: 'a', 94 | value: 'a' 95 | }, { 96 | type: 'put', 97 | key: 'b', 98 | value: 'b' 99 | }, { 100 | type: 'put', 101 | key: 'c', 102 | value: 'c' 103 | }], function (err) { 104 | t.error(err, 'no error') 105 | 106 | db.put('b', 'B', function () { 107 | var rs = db.createReadStream() 108 | var expected = ['a', 'B', 'c'] 109 | 110 | rs.on('data', function (data) { 111 | t.same(expected.shift(), data.value.toString()) 112 | }) 113 | 114 | rs.on('end', function () { 115 | t.same(expected.length, 0, 'no more data') 116 | t.end() 117 | }) 118 | }) 119 | }) 120 | }) 121 | 122 | tape('readstream override prefix', function (t) { 123 | var db = create({prefix: 'test'}) 124 | db.batch([{ 125 | type: 'put', 126 | key: 'a', 127 | value: 'a' 128 | }, { 129 | type: 'put', 130 | key: 'b', 131 | value: 'b' 132 | }, { 133 | type: 'put', 134 | key: 'c', 135 | value: 'c' 136 | }], function (err) { 137 | t.error(err, 'no error') 138 | 139 | db.put('b', 'B', function () { 140 | var rs = db.createReadStream({gt: 'a'}) 141 | var expected = ['B', 'c'] 142 | 143 | rs.on('data', function (data) { 144 | t.same(expected.shift(), data.value.toString()) 145 | }) 146 | 147 | rs.on('end', function () { 148 | t.same(expected.length, 0, 'no more data') 149 | t.end() 150 | }) 151 | }) 152 | }) 153 | }) 154 | 155 | tape('readstream checkout', function (t) { 156 | var db = create() 157 | db.batch([{ 158 | type: 'put', 159 | key: 'a', 160 | value: 'a' 161 | }, { 162 | type: 'put', 163 | key: 'b', 164 | value: 'b' 165 | }, { 166 | type: 'put', 167 | key: 'c', 168 | value: 'c' 169 | }], function (err) { 170 | t.error(err, 'no error') 171 | 172 | db.put('b', 'B', function () { 173 | var rs = db.createReadStream({version: 1}) 174 | var expected = ['a', 'b', 'c'] 175 | 176 | rs.on('data', function (data) { 177 | t.same(expected.shift(), data.value.toString()) 178 | }) 179 | 180 | rs.on('end', function () { 181 | t.same(expected.length, 0, 'no more data') 182 | t.end() 183 | }) 184 | }) 185 | }) 186 | }) 187 | 188 | tape('cleanup', function (t) { 189 | if (prev) rimraf.sync(prev) 190 | t.ok(true, 'all cleaned up') 191 | t.end() 192 | }) --------------------------------------------------------------------------------