├── .gitignore ├── .travis.yml ├── encoding.js ├── .jshintrc ├── package.json ├── LICENSE ├── README.md ├── level-ttl.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "4" 5 | - "3" 6 | - "2" 7 | - "1.8" 8 | - "1.0" 9 | - "0.12" 10 | - "0.10" 11 | branches: 12 | only: 13 | - master 14 | notifications: 15 | email: 16 | - rod@vagg.org 17 | 18 | -------------------------------------------------------------------------------- /encoding.js: -------------------------------------------------------------------------------- 1 | exports.create = function createEncoding (options) { 2 | options || (options = {}) 3 | 4 | if (options.ttlEncoding) 5 | return options.ttlEncoding 6 | 7 | const PATH_SEP = options.separator 8 | , INITIAL_SEP = options.sub ? '' : PATH_SEP 9 | 10 | function encodeElement(e) { 11 | // transform dates to timestamp strings 12 | return String(e instanceof Date ? +e : e) 13 | } 14 | 15 | return { 16 | buffer : false 17 | , encode : function (e) { 18 | // TODO: reexamine this with respect to level-sublevel@6's native codecs 19 | if (Array.isArray(e)) 20 | return new Buffer(INITIAL_SEP + e.map(encodeElement).join(PATH_SEP)) 21 | return new Buffer(encodeElement(e)) 22 | } 23 | , decode : function (e) { 24 | // TODO: detect and parse ttl records 25 | return e.toString('utf8') 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ ] 3 | , "bitwise": false 4 | , "camelcase": false 5 | , "curly": false 6 | , "eqeqeq": false 7 | , "forin": false 8 | , "immed": false 9 | , "latedef": false 10 | , "newcap": true 11 | , "noarg": true 12 | , "noempty": true 13 | , "nonew": true 14 | , "plusplus": false 15 | , "quotmark": true 16 | , "regexp": false 17 | , "undef": true 18 | , "unused": true 19 | , "strict": false 20 | , "trailing": true 21 | , "maxlen": 120 22 | , "asi": true 23 | , "boss": true 24 | , "debug": true 25 | , "eqnull": true 26 | , "esnext": true 27 | , "evil": true 28 | , "expr": true 29 | , "funcscope": false 30 | , "globalstrict": false 31 | , "iterator": false 32 | , "lastsemic": true 33 | , "laxbreak": true 34 | , "laxcomma": true 35 | , "loopfunc": true 36 | , "multistr": false 37 | , "onecase": false 38 | , "proto": false 39 | , "regexdash": false 40 | , "scripturl": true 41 | , "smarttabs": false 42 | , "shadow": false 43 | , "sub": true 44 | , "supernew": false 45 | , "validthis": true 46 | , "browser": true 47 | , "couch": false 48 | , "devel": false 49 | , "dojo": false 50 | , "mootools": false 51 | , "node": true 52 | , "nonstandard": true 53 | , "prototypejs": false 54 | , "rhino": false 55 | , "worker": true 56 | , "wsh": false 57 | , "nomen": false 58 | , "onevar": true 59 | , "passfail": false 60 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "level-ttl", 3 | "description": "Adds a 'ttl' option to LevelUP for puts and batches", 4 | "contributors": [ 5 | "Rod Vagg (https://github.com/rvagg)", 6 | "Matteo Collina (https://github.com/mcollina)", 7 | "Josh Duff (https://github.com/TehShrike)", 8 | "Erik Kristensen (https://github.com/ekristen)", 9 | "Lars-Magnus Skog (https://github.com/ralphtheninja)" 10 | ], 11 | "version": "3.1.0", 12 | "homepage": "https://github.com/level/level-ttl", 13 | "authors": [ 14 | "Rod Vagg (https://github.com/rvagg)" 15 | ], 16 | "keywords": [ 17 | "leveldb", 18 | "levelup", 19 | "level", 20 | "ttl", 21 | "whoa dude!" 22 | ], 23 | "main": "./level-ttl.js", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/level/level-ttl.git" 27 | }, 28 | "dependencies": { 29 | "after": ">=0.8.1 <0.9.0-0", 30 | "list-stream": ">=1.0.0 <1.1.0-0", 31 | "xtend": ">=4.0.0 <4.1.0-0" 32 | }, 33 | "peerDependencies": {}, 34 | "devDependencies": { 35 | "faucet": "0.0.1", 36 | "level-sublevel": "^6.4.6", 37 | "ltest": "^2.1.1", 38 | "slump": "^2.0.0", 39 | "tape": "^4.3.0", 40 | "bytewise": ">=0.8" 41 | }, 42 | "scripts": { 43 | "test": "tape test.js | faucet" 44 | }, 45 | "license": "MIT" 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013, Rod Vagg (the "Original Author") 2 | All rights reserved. 3 | 4 | MIT +no-false-attribs License 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | Distributions of all or part of the Software intended to be used 19 | by the recipients as they would use the unmodified Software, 20 | containing modifications that substantially alter, remove, or 21 | disable functionality of the Software, outside of the documented 22 | configuration mechanisms provided by the Software, shall be 23 | modified such that the Original Author's bug reporting email 24 | addresses and urls are either replaced with the contact information 25 | of the parties responsible for the changes, or removed entirely. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 34 | OTHER DEALINGS IN THE SOFTWARE. 35 | 36 | 37 | Except where noted, this license applies to any and all software 38 | programs and associated documentation files created by the 39 | Original Author, when distributed with the Software. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Level TTL 2 | 3 | LevelDB Logo 4 | 5 | **Add a `'ttl'` (time-to-live) option to LevelUP for `put()` and `batch()`** 6 | 7 | [![Build Status](https://travis-ci.org/Level/level-ttl.svg?branch=master)](https://travis-ci.org/Level/level-ttl) 8 | 9 | [![NPM](https://nodei.co/npm/level-ttl.png?downloads=true&downloadRank=true)](https://nodei.co/npm/level-ttl/) 10 | [![NPM](https://nodei.co/npm-dl/level-ttl.png?months=6&height=3)](https://nodei.co/npm/level-ttl/) 11 | 12 | Augment LevelUP to handle a new `'ttl'` option on `put()` and `batch()` that specifies the number of milliseconds an entry should remain in the data store. After the TTL, the entry will be automatically cleared for you. 13 | 14 | Requires [LevelUP](https://github.com/rvagg/node-levelup), [Level](https://github.com/level/level) or [level-hyper](https://github.com/Level/level-hyper) to be installed separately. 15 | 16 | ***Note 1: Version 1.0.0 data stores are not backward compatible with previous versions. If you have unexpired entries in a data store managed by pre-1.0.0, don't expect them to expire if you upgrade to 1.0.0+.*** *This is due to a level-sublevel change. It is also recommended that you only use level-sublevel 6.0.0+ with level-ttl.* 17 | 18 | ***Note 2: `level-ttl` has partial support for `level-spaces`. It should work fine as long as you don't use the `'defaultTTL'` feature, see below. This is being worked on so we can have full support for `level-spaces` as well.*** 19 | 20 | ```js 21 | var levelup = require('level') 22 | , ttl = require('level-ttl') 23 | 24 | var db = levelup('/tmp/foo.db') 25 | db = ttl(db) 26 | 27 | // --------------------------- put() --------------------------- // 28 | // this entry will only stay in the data store for 1 hour 29 | db.put('foo', 'bar', { ttl: 1000 * 60 * 60 }, function (err) { /* .. */ }) 30 | 31 | // -------------------------- batch() -------------------------- // 32 | // the two 'put' entries will only stay in the data store for 1 hour 33 | db.batch([ 34 | { type: 'put', key: 'foo', value: 'bar' } 35 | , { type: 'put', key: 'bam', value: 'boom' } 36 | , { type: 'del', key: 'w00t' } 37 | ], { ttl: 1000 * 60 * 60 }, function (err) { /* .. */ }) 38 | ``` 39 | 40 | If you put the same entry twice, you **refresh** the TTL to the *last* put operation. In this way you can build utilities like [session managers](https://github.com/rvagg/node-level-session/) for your web application where the user's session is refreshed with each visit but expires after a set period of time since their last visit. 41 | 42 | Alternatively, for a lower write-footprint you can use the `ttl()` method that is added to your LevelUP instance which can serve to insert or update a ttl for any given key in the database (even if that key doesn't exist but may in the future! Crazy!). 43 | 44 | ```js 45 | db.put('foo', 'bar', function (err) { /* .. */ }) 46 | db.ttl('foo', 1000 * 60 * 60, function (err) { /* .. */ }) 47 | ``` 48 | 49 | **Level TTL** uses an internal scan every 10 seconds by default, this limits the available resolution of your TTL values, possibly delaying a delete for up to 10 seconds. The resolution can be tuned by passing the `'checkFrequency'` option to the `ttl()` initialiser. 50 | 51 | ```js 52 | var db = levelup('/tmp/foo.db') 53 | // scan for deletables every second 54 | db = ttl(db, { checkFrequency: 1000 }) 55 | 56 | /* .. */ 57 | ``` 58 | 59 | Of course, a scan takes some resources, particularly on a data store that makes heavy use of TTLs. If you don't require high accuracy for actual deletions then you can increase the `'checkFrequency'`. Note though that a scan only involves invoking a LevelUP ReadStream that returns *only the entries due to expire*, so it doesn't have to manually check through all entries with a TTL. As usual, it's best to not do too much tuning until you have you have something worth tuning! 60 | 61 | ### Default TTL 62 | 63 | You can set a default ttl value for all your keys by passing the `'defaultTTL'` option to the `ttl()` initialiser. This can be overridden by explicitly setting the ttl value. 64 | 65 | In the following examle `'foo'` will expire in 15 minutes while `'beep'` will expire in one minute. 66 | 67 | ```js 68 | var db = levelup('/tmp/foo.db') 69 | db = ttl(db, { defaultTTL: 15 * 60 * 1000 }) 70 | db.put('foo', 'bar', function (err) { /* .. */ }) 71 | db.put('beep', 'boop', { ttl: 60 * 1000 }, function (err) { /* .. */ }) 72 | ``` 73 | 74 | ### `opts.sub` 75 | 76 | You can provide a custom storage for the meta data by using the `opts.sub` property. If it's set, that storage will contain all the ttl meta data. A use case for this would be to avoid mixing data and meta data in the same keyspace, since if it's not set, all data will be sharing the same keyspace. 77 | 78 | A db for the data and a separate to store the meta data: 79 | 80 | ```js 81 | var level = require('level') 82 | , ttl = require('level-ttl') 83 | , meta = level('./meta') 84 | , db = ttl(level('./db'), { sub: meta }) 85 | , batch = [ 86 | { type: 'put', key: 'foo', value: 'foovalue' } 87 | , { type: 'put', key: 'bar', value: 'barvalue' } 88 | ] 89 | 90 | db.batch(batch, { ttl: 100 }, function (err) { 91 | db.createReadStream() 92 | .on('data', function (data) { 93 | console.log('data', data) 94 | }) 95 | .on('end', function () { 96 | meta.createReadStream() 97 | .on('data', function (data) { 98 | console.log('meta', data) 99 | }) 100 | }) 101 | }) 102 | ``` 103 | 104 | For more examples on this please check the tests involving `level-sublevel`. 105 | 106 | 107 | ### Shutting down 108 | 109 | **Level TTL** uses a timer to regularly check for expiring entries (don't worry, the whole data store isn't scanned, it's very efficient!). The `db.close()` method is automatically wired to stop the timer but there is also a more explicit db.stop() method that will stop the timer and not pass on to a `close()` underlying LevelUP instance. 110 | 111 | ## Contributors 112 | 113 | **Level TTL** is powered by the following hackers: 114 | 115 | * [Rod Vagg](https://github.com/rvagg) 116 | * [Matteo Collina](https://github.com/mcollina) 117 | * [Josh Duff](https://github.com/TehShrike) 118 | * [Erik Kristensen](https://github.com/ekristen) 119 | * [Lars-Magnus Skog](https://github.com/ralphtheninja) 120 | 121 | ## Licence 122 | 123 | Level TTL is Copyright (c) 2013-2015 Rod Vagg [@rvagg](https://twitter.com/rvagg) and licensed under the MIT licence. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE file for more details. 124 | -------------------------------------------------------------------------------- /level-ttl.js: -------------------------------------------------------------------------------- 1 | const after = require('after') 2 | , xtend = require('xtend') 3 | , encoding = require('./encoding') 4 | 5 | function prefixKey (db, key) { 6 | return db._ttl.encoding.encode(db._ttl._prefixNs.concat(key)) 7 | } 8 | 9 | function expiryKey (db, exp, key) { 10 | return db._ttl.encoding.encode(db._ttl._expiryNs.concat(exp, key)) 11 | } 12 | 13 | function buildQuery (db) { 14 | const encode = db._ttl.encoding.encode 15 | , _expiryNs = db._ttl._expiryNs 16 | return { 17 | keyEncoding : 'binary' 18 | , valueEncoding : 'binary' 19 | , gte : encode(_expiryNs) 20 | , lte : encode(_expiryNs.concat(new Date())) 21 | } 22 | } 23 | 24 | function startTtl (db, checkFrequency) { 25 | db._ttl.intervalId = setInterval(function () { 26 | const batch = [] 27 | , subBatch = [] 28 | , sub = db._ttl.sub 29 | , query = buildQuery(db) 30 | , decode = db._ttl.encoding.decode 31 | var createReadStream 32 | 33 | db._ttl._checkInProgress = true 34 | 35 | if (sub) 36 | createReadStream = sub.createReadStream.bind(sub) 37 | else 38 | createReadStream = db.createReadStream.bind(db) 39 | 40 | var deletedKeys = []; 41 | createReadStream(query) 42 | .on('data', function (data) { 43 | // the value is the key! 44 | const key = decode(data.value) 45 | // expiryKey that matches this query 46 | subBatch.push({ type: 'del', key: data.key }) 47 | subBatch.push({ type: 'del', key: prefixKey(db, key) }) 48 | // the actual data that should expire now! 49 | batch.push({ type: 'del', key: key }) 50 | 51 | deletedKeys.push(key); 52 | }) 53 | .on('error', db.emit.bind(db, 'error')) 54 | .on('end', function () { 55 | if (!batch.length) 56 | return 57 | 58 | if (sub) { 59 | sub.batch( 60 | subBatch 61 | , { keyEncoding: 'binary' } 62 | , function (err) { 63 | if (err) 64 | db.emit('error', err) 65 | } 66 | ) 67 | 68 | db._ttl.batch( 69 | batch 70 | , { keyEncoding: 'binary' } 71 | , function (err) { 72 | if (err) { 73 | db.emit('error', err) 74 | } else { 75 | db.emit('expired', deletedKeys) 76 | } 77 | } 78 | ) 79 | } 80 | else { 81 | db._ttl.batch( 82 | subBatch.concat(batch) 83 | , { keyEncoding: 'binary' } 84 | , function (err) { 85 | if (err) { 86 | db.emit('error', err) 87 | } else { 88 | db.emit('expired', deletedKeys) 89 | } 90 | } 91 | ) 92 | } 93 | 94 | }) 95 | .on('close', function () { 96 | db._ttl._checkInProgress = false 97 | if (db._ttl._stopAfterCheck) { 98 | stopTtl(db, db._ttl._stopAfterCheck) 99 | db._ttl._stopAfterCheck = null 100 | } 101 | }) 102 | }, checkFrequency) 103 | if (db._ttl.intervalId.unref) 104 | db._ttl.intervalId.unref() 105 | } 106 | 107 | function stopTtl (db, callback) { 108 | // can't close a db while an interator is in progress 109 | // so if one is, defer 110 | if (db._ttl._checkInProgress) 111 | return db._ttl._stopAfterCheck = callback 112 | clearInterval(db._ttl.intervalId) 113 | callback && callback() 114 | } 115 | 116 | function ttlon (db, keys, ttl, callback) { 117 | const exp = new Date(Date.now() + ttl) 118 | , batch = [] 119 | , sub = db._ttl.sub 120 | , batchFn = (sub ? sub.batch.bind(sub) : db._ttl.batch) 121 | , encode = db._ttl.encoding.encode 122 | 123 | ttloff(db, keys, function () { 124 | keys.forEach(function (key) { 125 | batch.push({ type: 'put', key: expiryKey(db, exp, key), value: encode(key) }) 126 | batch.push({ type: 'put', key: prefixKey(db, key), value: encode(exp) }) 127 | }) 128 | 129 | if (!batch.length) 130 | return callback && callback() 131 | 132 | batchFn( 133 | batch 134 | , { keyEncoding: 'binary', valueEncoding: 'binary' } 135 | , function (err) { 136 | if (err) 137 | db.emit('error', err) 138 | callback && callback() 139 | } 140 | ) 141 | }) 142 | } 143 | 144 | function ttloff (db, keys, callback) { 145 | const batch = [] 146 | , sub = db._ttl.sub 147 | , getFn = (sub ? sub.get.bind(sub) : db.get.bind(db)) 148 | , batchFn = (sub ? sub.batch.bind(sub) : db._ttl.batch) 149 | , decode = db._ttl.encoding.decode 150 | , done = after(keys.length, function (err) { 151 | if (err) 152 | db.emit('error', err) 153 | 154 | if (!batch.length) 155 | return callback && callback() 156 | 157 | batchFn( 158 | batch 159 | , { keyEncoding: 'binary', valueEncoding: 'binary' } 160 | , function (err) { 161 | if (err) 162 | db.emit('error', err) 163 | callback && callback() 164 | } 165 | ) 166 | }) 167 | 168 | keys.forEach(function (key) { 169 | const prefixedKey = prefixKey(db, key) 170 | getFn( 171 | prefixedKey 172 | , { keyEncoding: 'binary', valueEncoding: 'binary' } 173 | , function (err, exp) { 174 | if (!err && exp) { 175 | batch.push({ type: 'del', key: expiryKey(db, decode(exp), key) }) 176 | batch.push({ type: 'del', key: prefixedKey }) 177 | } 178 | done(err && err.name != 'NotFoundError' && err) 179 | } 180 | ) 181 | }) 182 | } 183 | 184 | function put (db, key, value, options, callback) { 185 | if (typeof options == 'function') { 186 | callback = options 187 | options = {} 188 | } 189 | 190 | options || (options = {}) 191 | 192 | if (db._ttl.options.defaultTTL > 0 && !options.ttl && options.ttl != 0) { 193 | options.ttl = db._ttl.options.defaultTTL 194 | } 195 | 196 | var done 197 | , _callback = callback 198 | 199 | if (options.ttl > 0 && key != null && value != null) { 200 | done = after(2, _callback || function () {}) 201 | callback = done 202 | ttlon(db, [ key ], options.ttl, done) 203 | } 204 | 205 | db._ttl.put.call(db, key, value, options, callback) 206 | } 207 | 208 | function setTtl (db, key, ttl, callback) { 209 | if (ttl > 0 && key != null) 210 | ttlon(db, [ key ], ttl, callback) 211 | } 212 | 213 | function del (db, key, options, callback) { 214 | var done 215 | , _callback = callback 216 | if (key != null) { 217 | done = after(2, _callback || function () {}) 218 | callback = done 219 | ttloff(db, [ key ], done) 220 | } 221 | 222 | db._ttl.del.call(db, key, options, callback) 223 | } 224 | 225 | function batch (db, arr, options, callback) { 226 | if (typeof options == 'function') { 227 | callback = options 228 | options = {} 229 | } 230 | 231 | options || (options = {}) 232 | 233 | if (db._ttl.options.defaultTTL > 0 && !options.ttl && options.ttl != 0) { 234 | options.ttl = db._ttl.options.defaultTTL 235 | } 236 | 237 | var done 238 | , on 239 | , off 240 | , _callback = callback 241 | 242 | if (options.ttl > 0 && Array.isArray(arr)) { 243 | done = after(3, _callback || function () {}) 244 | callback = done 245 | 246 | on = [] 247 | off = [] 248 | arr.forEach(function (entry) { 249 | if (!entry || entry.key == null) 250 | return 251 | 252 | if (entry.type == 'put' && entry.value != null) 253 | on.push(entry.key) 254 | if (entry.type == 'del') 255 | off.push(entry.key) 256 | }) 257 | 258 | if (on.length) 259 | ttlon(db, on, options.ttl, done) 260 | else 261 | done() 262 | if (off.length) 263 | ttloff(db, off, done) 264 | else 265 | done() 266 | } 267 | 268 | db._ttl.batch.call(db, arr, options, callback) 269 | } 270 | 271 | function close (db, callback) { 272 | stopTtl(db, function () { 273 | if (db._ttl && typeof db._ttl.close == 'function') 274 | return db._ttl.close.call(db, callback) 275 | callback && callback() 276 | }) 277 | } 278 | 279 | function setup (db, options) { 280 | if (db._ttl) 281 | return 282 | 283 | options || (options = {}) 284 | 285 | options = xtend({ 286 | methodPrefix : '' 287 | , namespace : options.sub ? '' : 'ttl' 288 | , expiryNamespace : 'x' 289 | , separator : '!' 290 | , checkFrequency : 10000 291 | , defaultTTL : 0 292 | }, options) 293 | 294 | const _prefixNs = options.namespace ? [ options.namespace ] : [] 295 | 296 | db._ttl = { 297 | put : db.put.bind(db) 298 | , del : db.del.bind(db) 299 | , batch : db.batch.bind(db) 300 | , close : db.close.bind(db) 301 | , sub : options.sub 302 | , options : options 303 | , encoding : encoding.create(options) 304 | , _prefixNs : _prefixNs 305 | , _expiryNs : _prefixNs.concat(options.expiryNamespace) 306 | } 307 | 308 | db[options.methodPrefix + 'put'] = put.bind(null, db) 309 | db[options.methodPrefix + 'del'] = del.bind(null, db) 310 | db[options.methodPrefix + 'batch'] = batch.bind(null, db) 311 | db[options.methodPrefix + 'ttl'] = setTtl.bind(null, db) 312 | db[options.methodPrefix + 'stop'] = stopTtl.bind(null, db) 313 | // we must intercept close() 314 | db.close = close.bind(null, db) 315 | 316 | startTtl(db, options.checkFrequency) 317 | 318 | return db 319 | } 320 | 321 | module.exports = setup 322 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | , ltest = require('ltest')(tape) 3 | , listStream = require('list-stream') 4 | , ttl = require('./') 5 | , xtend = require('xtend') 6 | , sublevel = require('level-sublevel') 7 | , bwSublevel = require('level-sublevel/bytewise') 8 | , random = require('slump') 9 | , bytewise = require('bytewise') 10 | , bwEncode = bytewise.encode 11 | 12 | function test (name, fn, opts) { 13 | ltest(name, opts, function (t, _db, createReadStream) { 14 | var db 15 | , close = _db.close.bind(_db) // unmolested close() 16 | 17 | db = ttl(_db, xtend({ checkFrequency: 50 }, opts)) 18 | fn(t, db, createReadStream, close) 19 | }) 20 | } 21 | 22 | function db2arr (createReadStream, t, callback, opts) { 23 | createReadStream(opts) 24 | .pipe(listStream.obj (function (err, arr) { 25 | if (err) 26 | return t.fail(err) 27 | callback(arr) 28 | })) 29 | } 30 | 31 | function bufferEq (a, b) { 32 | if (a instanceof Buffer && b instanceof Buffer) { 33 | return a.toString('hex') === b.toString('hex') 34 | } 35 | } 36 | 37 | function isRange (range) { 38 | return range && (range.gt || range.lt || range.gte || range.lte) 39 | } 40 | 41 | function matchRange (range, buffer) { 42 | var target = buffer.toString('hex') 43 | , match = true 44 | 45 | if (range.gt) 46 | match = match && target > range.gt.toString('hex') 47 | else if (range.gte) 48 | match = match && target >= range.gte.toString('hex') 49 | if (range.lt) 50 | match = match && target < range.lt.toString('hex') 51 | else if (range.lte) 52 | match = match && target <= range.lte.toString('hex') 53 | 54 | return match 55 | } 56 | 57 | function bwRange (prefix, resolution) { 58 | const now = Date.now() 59 | , min = new Date(resolution ? now - resolution : 0) 60 | , max = new Date(resolution ? now + resolution : 9999999999999) 61 | return { 62 | gte : bwEncode(prefix ? prefix.concat(min) : min) 63 | , lte : bwEncode(prefix ? prefix.concat(max) : max) 64 | } 65 | } 66 | 67 | function formatRecord (key, value) { 68 | if (isRange(key)) 69 | key.source = '[object KeyRange]' 70 | if (isRange(value)) 71 | value.source = '[object ValueRange]' 72 | return '{' + (key.source || key) + ', ' + (value.source || value) + '}' 73 | } 74 | 75 | function contains (t, arr, key, value) { 76 | for (var i = 0; i < arr.length; i++) { 77 | if (typeof key == 'string' && arr[i].key != key) 78 | continue 79 | if (typeof value == 'string' && arr[i].value != value) 80 | continue 81 | if (key instanceof RegExp && !key.test(arr[i].key)) 82 | continue 83 | if (value instanceof RegExp && !value.test(arr[i].value)) 84 | continue 85 | if (key instanceof Buffer && !bufferEq(key, arr[i].key)) 86 | continue 87 | if (value instanceof Buffer && !bufferEq(value, arr[i].value)) 88 | continue 89 | if (isRange(key) && !matchRange(key, arr[i].key)) 90 | continue 91 | if (isRange(value) && !matchRange(value, arr[i].value)) 92 | continue 93 | return t.pass('contains ' + formatRecord(key, value)) 94 | } 95 | return t.fail('does not contain ' + formatRecord(key, value)) 96 | } 97 | 98 | function randomPutBatch (length) { 99 | var batch = [] 100 | , randomize = function () { 101 | return random.string({ enc: 'base58', length: 10 }) 102 | } 103 | for (var i = 0; i < length; ++i) { 104 | batch.push({ type: 'put', key: randomize(), value: randomize() }) 105 | } 106 | return batch 107 | } 108 | 109 | function verifyIn (delay, createReadStream, t, cb, opts) { 110 | setTimeout(function () { 111 | db2arr(createReadStream, t, cb, opts) 112 | }, delay) 113 | } 114 | 115 | test('single ttl entry', function (t, db) { 116 | t.throws(db.put.bind(db), { name: 'WriteError', message: 'put() requires key and value arguments' }) 117 | t.throws(db.del.bind(db), { name: 'WriteError', message: 'del() requires a key argument' }) 118 | t.end() 119 | }) 120 | 121 | test('single ttl entry with put', function (t, db, createReadStream) { 122 | db.put('foo', 'foovalue', function (err) { 123 | t.notOk(err, 'no error') 124 | db.put('bar', 'barvalue', { ttl: 100 }, function (err) { 125 | t.notOk(err, 'no error') 126 | db2arr(createReadStream, t, function (arr) { 127 | contains(t, arr, /!ttl!x!\d{13}!bar/, 'bar') 128 | contains(t, arr, '!ttl!bar', /\d{13}/) 129 | contains(t, arr, 'bar', 'barvalue') 130 | contains(t, arr, 'foo', 'foovalue') 131 | verifyIn(150, createReadStream, t, function (arr) { 132 | t.deepEqual(arr, [ 133 | { key: 'foo', value: 'foovalue' } 134 | ]) 135 | t.end() 136 | }) 137 | }) 138 | }) 139 | }) 140 | }) 141 | 142 | test('single ttl entry with put (custom ttlEncoding)', function (t, db, createReadStream) { 143 | db.put('foo', 'foovalue', function (err) { 144 | t.notOk(err, 'no error') 145 | db.put('bar', 'barvalue', { ttl: 100 }, function (err) { 146 | t.notOk(err, 'no error') 147 | db2arr(createReadStream, t, function (arr) { 148 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar')) 149 | contains(t, arr, bwEncode([ 'ttl', 'bar' ]), bwRange()) 150 | contains(t, arr, new Buffer('bar'), new Buffer('barvalue')) 151 | contains(t, arr, new Buffer('foo'), new Buffer('foovalue')) 152 | 153 | verifyIn(150, createReadStream, t, function (arr) { 154 | t.deepEqual(arr, [ 155 | { key: 'foo', value: 'foovalue' } 156 | ]) 157 | t.end() 158 | }) 159 | }, { keyEncoding: 'binary', valueEncoding: 'binary' }) 160 | }) 161 | }) 162 | }, { ttlEncoding: bytewise }) 163 | 164 | test('multiple ttl entries with put', function (t, db, createReadStream) { 165 | var expect = function (delay, keys, cb) { 166 | verifyIn(delay, createReadStream, t, function (arr) { 167 | t.equal(arr.length, 1 + keys * 3, 'correct number of entries in db') 168 | contains(t, arr, 'afoo', 'foovalue') 169 | if (keys >= 1) { 170 | contains(t, arr, 'bar1', 'barvalue1') 171 | contains(t, arr, /^!ttl!x!\d{13}!bar1$/, 'bar1') 172 | contains(t, arr, '!ttl!bar1', /^\d{13}$/) 173 | } 174 | if (keys >= 2) { 175 | contains(t, arr, 'bar2', 'barvalue2') 176 | contains(t, arr, /^!ttl!x!\d{13}!bar2$/, 'bar2') 177 | contains(t, arr, '!ttl!bar2', /^\d{13}$/) 178 | } 179 | if (keys >= 3) { 180 | contains(t, arr, 'bar3', 'barvalue3') 181 | contains(t, arr, /^!ttl!x!\d{13}!bar3$/, 'bar3') 182 | contains(t, arr, '!ttl!bar3', /^\d{13}$/) 183 | } 184 | cb && cb() 185 | }) 186 | } 187 | 188 | db.put('afoo', 'foovalue') 189 | db.put('bar1', 'barvalue1', { ttl: 400 }) 190 | db.put('bar2', 'barvalue2', { ttl: 250 }) 191 | db.put('bar3', 'barvalue3', { ttl: 100 }) 192 | 193 | expect(25, 3) 194 | expect(200, 2) 195 | expect(350, 1) 196 | expect(500, 0, t.end.bind(t)) 197 | }) 198 | 199 | test('multiple ttl entries with put (custom ttlEncoding)', function (t, db, createReadStream) { 200 | var expect = function (delay, keys, cb) { 201 | verifyIn(delay, createReadStream, t, function (arr) { 202 | t.equal(arr.length, 1 + keys * 3, 'correct number of entries in db') 203 | contains(t, arr, new Buffer('afoo'), new Buffer('foovalue')) 204 | if (keys >= 1) { 205 | contains(t, arr, new Buffer('bar1'), new Buffer('barvalue1')) 206 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar1')) 207 | contains(t, arr, bwEncode([ 'ttl', 'bar1' ]), bwRange()) 208 | } 209 | if (keys >= 2) { 210 | contains(t, arr, new Buffer('bar2'), new Buffer('barvalue2')) 211 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar2')) 212 | contains(t, arr, bwEncode([ 'ttl', 'bar2' ]), bwRange()) 213 | } 214 | if (keys >= 3) { 215 | contains(t, arr, new Buffer('bar3'), new Buffer('barvalue3')) 216 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar3')) 217 | contains(t, arr, bwEncode([ 'ttl', 'bar3' ]), bwRange()) 218 | } 219 | cb && cb() 220 | }, { keyEncoding: 'binary', valueEncoding: 'binary' }) 221 | } 222 | 223 | db.put('afoo', 'foovalue') 224 | db.put('bar1', 'barvalue1', { ttl: 400 }) 225 | db.put('bar2', 'barvalue2', { ttl: 250 }) 226 | db.put('bar3', 'barvalue3', { ttl: 100 }) 227 | 228 | expect(25, 3) 229 | expect(200, 2) 230 | expect(350, 1) 231 | expect(500, 0, t.end.bind(t)) 232 | }, { ttlEncoding: bytewise }) 233 | 234 | test('multiple ttl entries with batch-put', function (t, db, createReadStream) { 235 | var expect = function (delay, keys, cb) { 236 | verifyIn(delay, createReadStream, t, function (arr) { 237 | t.equal(arr.length, 1 + keys * 3, 'correct number of entries in db') 238 | contains(t, arr, 'afoo', 'foovalue') 239 | if (keys >= 1) { 240 | contains(t, arr, 'bar1', 'barvalue1') 241 | contains(t, arr, /^!ttl!x!\d{13}!bar1$/, 'bar1') 242 | contains(t, arr, '!ttl!bar1', /^\d{13}$/) 243 | } 244 | if (keys >= 2) { 245 | contains(t, arr, 'bar2', 'barvalue2') 246 | contains(t, arr, /^!ttl!x!\d{13}!bar2$/, 'bar2') 247 | contains(t, arr, '!ttl!bar2', /^\d{13}$/) 248 | } 249 | if (keys >= 3) { 250 | contains(t, arr, 'bar3', 'barvalue3') 251 | contains(t, arr, /^!ttl!x!\d{13}!bar3$/, 'bar3') 252 | contains(t, arr, '!ttl!bar3', /^\d{13}$/) 253 | } 254 | if (keys >= 3) { 255 | contains(t, arr, 'bar4', 'barvalue4') 256 | contains(t, arr, /^!ttl!x!\d{13}!bar4$/, 'bar4') 257 | contains(t, arr, '!ttl!bar4', /^\d{13}$/) 258 | } 259 | cb && cb() 260 | }) 261 | } 262 | 263 | db.put('afoo', 'foovalue') 264 | db.batch([ 265 | { type: 'put', key: 'bar1', value: 'barvalue1' } 266 | , { type: 'put', key: 'bar2', value: 'barvalue2' } 267 | ], { ttl: 60 }) 268 | db.batch([ 269 | { type: 'put', key: 'bar3', value: 'barvalue3' } 270 | , { type: 'put', key: 'bar4', value: 'barvalue4' } 271 | ], { ttl: 120 }) 272 | 273 | expect(20, 4, t.end.bind(t)) 274 | }) 275 | 276 | test('multiple ttl entries with batch-put (custom ttlEncoding)', function (t, db, createReadStream) { 277 | var expect = function (delay, keys, cb) { 278 | verifyIn(delay, createReadStream, t, function (arr) { 279 | t.equal(arr.length, 1 + keys * 3, 'correct number of entries in db') 280 | contains(t, arr, new Buffer('afoo'), new Buffer('foovalue')) 281 | if (keys >= 1) { 282 | contains(t, arr, new Buffer('bar1'), new Buffer('barvalue1')) 283 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar1')) 284 | contains(t, arr, bwEncode([ 'ttl', 'bar1' ]), bwRange()) 285 | } 286 | if (keys >= 2) { 287 | contains(t, arr, new Buffer('bar2'), new Buffer('barvalue2')) 288 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar2')) 289 | contains(t, arr, bwEncode([ 'ttl', 'bar2' ]), bwRange()) 290 | } 291 | if (keys >= 3) { 292 | contains(t, arr, new Buffer('bar3'), new Buffer('barvalue3')) 293 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar3')) 294 | contains(t, arr, bwEncode([ 'ttl', 'bar3' ]), bwRange()) 295 | } 296 | if (keys >= 3) { 297 | contains(t, arr, new Buffer('bar4'), new Buffer('barvalue4')) 298 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar4')) 299 | contains(t, arr, bwEncode([ 'ttl', 'bar4' ]), bwRange()) 300 | } 301 | cb && cb() 302 | }, { keyEncoding: 'binary', valueEncoding: 'binary' }) 303 | } 304 | 305 | db.put('afoo', 'foovalue') 306 | db.batch([ 307 | { type: 'put', key: 'bar1', value: 'barvalue1' } 308 | , { type: 'put', key: 'bar2', value: 'barvalue2' } 309 | ], { ttl: 60 }) 310 | db.batch([ 311 | { type: 'put', key: 'bar3', value: 'barvalue3' } 312 | , { type: 'put', key: 'bar4', value: 'barvalue4' } 313 | ], { ttl: 120 }) 314 | 315 | expect(20, 4, t.end.bind(t)) 316 | }, { ttlEncoding: bytewise }) 317 | 318 | test('prolong entry life with additional put', function (t, db, createReadStream) { 319 | var retest = function (delay, cb) { 320 | setTimeout(function () { 321 | db.put('bar', 'barvalue', { ttl: 250 }) 322 | verifyIn(50, createReadStream, t, function (arr) { 323 | contains(t, arr, 'foo', 'foovalue') 324 | contains(t, arr, 'bar', 'barvalue') 325 | contains(t, arr, /!ttl!x!\d{13}!bar/, 'bar') 326 | contains(t, arr, '!ttl!bar', /\d{13}/) 327 | cb && cb() 328 | }) 329 | }, delay) 330 | } 331 | , i 332 | 333 | db.put('foo', 'foovalue') 334 | for (i = 0; i < 180; i += 20) 335 | retest(i) 336 | retest(180, t.end.bind(t)) 337 | }) 338 | 339 | test('prolong entry life with additional put (custom ttlEncoding)', function (t, db, createReadStream) { 340 | var retest = function (delay, cb) { 341 | setTimeout(function () { 342 | db.put('bar', 'barvalue', { ttl: 250 }) 343 | verifyIn(50, createReadStream, t, function (arr) { 344 | contains(t, arr, new Buffer('foo'), new Buffer('foovalue')) 345 | contains(t, arr, new Buffer('bar'), new Buffer('barvalue')) 346 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar')) 347 | contains(t, arr, bwEncode([ 'ttl', 'bar' ]), bwRange()) 348 | cb && cb() 349 | }, { keyEncoding: 'binary', valueEncoding: 'binary' }) 350 | }, delay) 351 | } 352 | , i 353 | 354 | db.put('foo', 'foovalue') 355 | for (i = 0; i < 180; i += 20) 356 | retest(i) 357 | retest(180, t.end.bind(t)) 358 | }, { ttlEncoding: bytewise }) 359 | 360 | test('prolong entry life with ttl(key, ttl)', function (t, db, createReadStream) { 361 | var retest = function (delay, cb) { 362 | setTimeout(function () { 363 | db.ttl('bar', 250) 364 | verifyIn(25, createReadStream, t, function (arr) { 365 | contains(t, arr, 'bar', 'barvalue') 366 | contains(t, arr, 'foo', 'foovalue') 367 | contains(t, arr, /!ttl!x!\d{13}!bar/, 'bar') 368 | contains(t, arr, '!ttl!bar', /\d{13}/) 369 | cb && cb() 370 | }) 371 | }, delay) 372 | } 373 | , i 374 | 375 | db.put('foo', 'foovalue') 376 | db.put('bar', 'barvalue') 377 | for (i = 0; i < 180; i += 20) 378 | retest(i) 379 | retest(180, t.end.bind(t)) 380 | }) 381 | 382 | test('prolong entry life with ttl(key, ttl) (custom ttlEncoding)', function (t, db, createReadStream) { 383 | var retest = function (delay, cb) { 384 | setTimeout(function () { 385 | db.ttl('bar', 250) 386 | verifyIn(25, createReadStream, t, function (arr) { 387 | contains(t, arr, new Buffer('bar'), new Buffer('barvalue')) 388 | contains(t, arr, new Buffer('foo'), new Buffer('foovalue')) 389 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar')) 390 | contains(t, arr, bwEncode([ 'ttl', 'bar' ]), bwRange()) 391 | cb && cb() 392 | }, { keyEncoding: 'binary', valueEncoding: 'binary' }) 393 | }, delay) 394 | } 395 | , i 396 | 397 | db.put('foo', 'foovalue') 398 | db.put('bar', 'barvalue') 399 | for (i = 0; i < 180; i += 20) 400 | retest(i) 401 | retest(180, t.end.bind(t)) 402 | }, { ttlEncoding: bytewise }) 403 | 404 | test('del removes both key and its ttl meta data', function (t, db, createReadStream) { 405 | db.put('foo', 'foovalue') 406 | db.put('bar', 'barvalue', { ttl: 250 }) 407 | 408 | verifyIn(150, createReadStream, t, function (arr) { 409 | contains(t, arr, 'foo', 'foovalue') 410 | contains(t, arr, 'bar', 'barvalue') 411 | contains(t, arr, /!ttl!x!\d{13}!bar/, 'bar') 412 | contains(t, arr, '!ttl!bar', /\d{13}/) 413 | }) 414 | 415 | setTimeout(function () { 416 | db.del('bar') 417 | }, 250) 418 | 419 | verifyIn(350, createReadStream, t, function (arr) { 420 | t.deepEqual(arr, [ 421 | { key: 'foo', value: 'foovalue' } 422 | ]) 423 | t.end() 424 | }) 425 | }) 426 | 427 | test('del removes both key and its ttl meta data (value encoding)', function (t, db, createReadStream) { 428 | db.put('foo', { v: 'foovalue' }) 429 | db.put('bar', { v: 'barvalue' }, { ttl: 250 }) 430 | 431 | verifyIn(50, createReadStream, t, function (arr) { 432 | contains(t, arr, 'foo', '{"v":"foovalue"}') 433 | contains(t, arr, 'bar', '{"v":"barvalue"}') 434 | contains(t, arr, /!ttl!x!\d{13}!bar/, 'bar') 435 | contains(t, arr, '!ttl!bar', /\d{13}/) 436 | }, { valueEncoding: 'utf8' }) 437 | 438 | setTimeout(function () { 439 | db.del('bar') 440 | }, 175) 441 | 442 | verifyIn(350, createReadStream, t, function (arr) { 443 | t.deepEqual(arr, [ 444 | { key: 'foo', value: '{"v":"foovalue"}' } 445 | ]) 446 | t.end() 447 | }, { valueEncoding: 'utf8' }) 448 | 449 | }, { keyEncoding: 'utf8', valueEncoding: 'json' }) 450 | 451 | test('del removes both key and its ttl meta data (custom ttlEncoding)', function (t, db, createReadStream) { 452 | db.put('foo', { v: 'foovalue' }) 453 | db.put('bar', { v: 'barvalue' }, { ttl: 250 }) 454 | 455 | verifyIn(50, createReadStream, t, function (arr) { 456 | contains(t, arr, new Buffer('foo'), new Buffer('{"v":"foovalue"}')) 457 | contains(t, arr, new Buffer('bar'), new Buffer('{"v":"barvalue"}')) 458 | contains(t, arr, bwRange([ 'ttl', 'x' ]), bwEncode('bar')) 459 | contains(t, arr, bwEncode([ 'ttl', 'bar' ]), bwRange()) 460 | }, { keyEncoding: 'binary', valueEncoding: 'binary' }) 461 | 462 | setTimeout(function () { 463 | db.del('bar') 464 | }, 175) 465 | 466 | verifyIn(350, createReadStream, t, function (arr) { 467 | t.deepEqual(arr, [ 468 | { key: 'foo', value: '{"v":"foovalue"}' } 469 | ]) 470 | t.end() 471 | }, { valueEncoding: 'utf8' }) 472 | 473 | }, { keyEncoding: 'utf8', valueEncoding: 'json', ttlEncoding: bytewise }) 474 | 475 | function wrappedTest () { 476 | var intervals = 0 477 | , _setInterval = global.setInterval 478 | , _clearInterval = global.clearInterval 479 | 480 | global.setInterval = function () { 481 | intervals++ 482 | return _setInterval.apply(global, arguments) 483 | } 484 | 485 | global.clearInterval = function () { 486 | intervals-- 487 | return _clearInterval.apply(global, arguments) 488 | } 489 | 490 | test('test stop() method stops interval and doesn\'t hold process up', function (t, db, createReadStream, close) { 491 | t.equals(intervals, 1, '1 interval timer') 492 | db.put( 'foo', 'bar1', { ttl: 25 }) 493 | 494 | setTimeout(function () { 495 | db.get('foo', function (err, value) { 496 | t.notOk(err, 'no error') 497 | t.equal('bar1', value) 498 | }) 499 | }, 40) 500 | 501 | setTimeout(function () { 502 | db.get('foo', function (err, value) { 503 | t.ok(err && err.notFound, 'not found error') 504 | t.notOk(value, 'no value') 505 | }) 506 | }, 80) 507 | 508 | setTimeout(function () { 509 | db.stop(function () { 510 | close(function () { 511 | global.setInterval = _setInterval 512 | global.clearInterval = _clearInterval 513 | t.equals(0, intervals, 'all interval timers cleared') 514 | t.end() 515 | }) 516 | }) 517 | }, 120) 518 | }) 519 | } 520 | 521 | wrappedTest() 522 | 523 | function testSinglePutWithDefaultTtl (t, db, createReadStream) { 524 | db.put('foo', 'foovalue', function(err) { 525 | t.ok(!err, 'no error') 526 | 527 | setTimeout(function () { 528 | db.get('foo', function (err, value) { 529 | t.notOk(err, 'no error') 530 | t.equal('foovalue', value) 531 | }) 532 | }, 50) 533 | 534 | setTimeout(function () { 535 | db.get('foo', function (err, value) { 536 | t.ok(err && err.notFound, 'not found error') 537 | t.notOk(value, 'no value') 538 | t.end() 539 | }) 540 | }, 175) 541 | }) 542 | 543 | } 544 | 545 | test('single put with default ttl set' 546 | , testSinglePutWithDefaultTtl 547 | , { defaultTTL: 75 } 548 | ) 549 | 550 | test('single put with default ttl set (custom ttlEncoding)' 551 | , testSinglePutWithDefaultTtl 552 | , { defaultTTL: 75, ttlEncoding: bytewise } 553 | ) 554 | 555 | function testSinglePutWithTtlOverride (t, db, createReadStream) { 556 | db.put('foo', 'foovalue', { ttl: 99 }, function(err) { 557 | t.ok(!err, 'no error') 558 | setTimeout(function () { 559 | db.get('foo', function (err, value) { 560 | t.notOk(err, 'no error') 561 | t.equal('foovalue', value) 562 | }) 563 | }, 50) 564 | 565 | setTimeout(function () { 566 | db.get('foo', function (err, value) { 567 | t.ok(err && err.notFound, 'not found error') 568 | t.notOk(value, 'no value') 569 | t.end() 570 | }) 571 | }, 200) 572 | }) 573 | } 574 | 575 | test('single put with overridden ttl set' 576 | , testSinglePutWithTtlOverride 577 | , { defaultTTL: 75 } 578 | ) 579 | 580 | test('single put with overridden ttl set (custom ttlEncoding)' 581 | , testSinglePutWithTtlOverride 582 | , { defaultTTL: 75, ttlEncoding: bytewise } 583 | ) 584 | 585 | function testBatchPutWithDefaultTtl (t, db, createReadStream) { 586 | db.batch([ 587 | { type: 'put', key: 'foo', value: 'foovalue' }, 588 | { type: 'put', key: 'bar', value: 'barvalue' } 589 | ], function(err) { 590 | t.ok(!err, 'no error') 591 | setTimeout(function () { 592 | db.get('foo', function (err, value) { 593 | t.notOk(err, 'no error') 594 | t.equal('foovalue', value) 595 | db.get('bar', function(err, value) { 596 | t.notOk(err, 'no error') 597 | t.equal('barvalue', value) 598 | }) 599 | }) 600 | }, 50) 601 | 602 | setTimeout(function () { 603 | db.get('foo', function (err, value) { 604 | t.ok(err && err.notFound, 'not found error') 605 | t.notOk(value, 'no value') 606 | db.get('bar', function(err, value) { 607 | t.ok(err && err.notFound, 'not found error') 608 | t.notOk(value, 'no value') 609 | t.end() 610 | }) 611 | }) 612 | }, 175) 613 | }) 614 | } 615 | 616 | test('batch put with default ttl set' 617 | , testBatchPutWithDefaultTtl 618 | , { defaultTTL: 75 } 619 | ) 620 | 621 | test('batch put with default ttl set (custom ttlEncoding)' 622 | , testBatchPutWithDefaultTtl 623 | , { defaultTTL: 75, ttlEncoding: bytewise } 624 | ) 625 | 626 | function testBatchPutWithTtlOverride (t, db, createReadStream) { 627 | db.batch([ 628 | { type: 'put', key: 'foo', value: 'foovalue' }, 629 | { type: 'put', key: 'bar', value: 'barvalue' } 630 | ], { ttl: 99 }, function(err) { 631 | setTimeout(function () { 632 | db.get('foo', function (err, value) { 633 | t.notOk(err, 'no error') 634 | t.equal('foovalue', value) 635 | db.get('bar', function(err, value) { 636 | t.notOk(err, 'no error') 637 | t.equal('barvalue', value) 638 | }) 639 | }) 640 | }, 50) 641 | 642 | setTimeout(function () { 643 | db.get('foo', function (err, value) { 644 | t.ok(err && err.notFound, 'not found error') 645 | t.notOk(value, 'no value') 646 | db.get('bar', function(err, value) { 647 | t.ok(err && err.notFound, 'not found error') 648 | t.notOk(value, 'no value') 649 | t.end() 650 | }) 651 | }) 652 | }, 200) 653 | }) 654 | } 655 | 656 | test('batch put with overriden ttl set' 657 | , testBatchPutWithTtlOverride 658 | , { defaultTTL: 75 } 659 | ) 660 | 661 | test('batch put with overriden ttl set (custom ttlEncoding)' 662 | , testBatchPutWithTtlOverride 663 | , { defaultTTL: 75, ttlEncoding: bytewise } 664 | ) 665 | 666 | ltest('without options', function (t, db, createReadStream) { 667 | try { 668 | ttl(db) 669 | } catch(err) { 670 | t.notOk(err, 'no error on ttl()') 671 | } 672 | t.end() 673 | }) 674 | 675 | ltest('data and level-sublevel ttl meta data separation', function (t, db, createReadStream) { 676 | var subDb = sublevel(db) 677 | , meta = subDb.sublevel('meta') 678 | , ttldb = ttl(db, { sub: meta }) 679 | , batch = randomPutBatch(5) 680 | 681 | ttldb.batch(batch, { ttl: 10000 }, function (err) { 682 | t.ok(!err, 'no error') 683 | db2arr(createReadStream, t, function (arr) { 684 | batch.forEach(function (item) { 685 | contains(t, arr, '!meta!' + item.key, /\d{13}/) 686 | contains(t, arr, new RegExp("!meta!x!\\d{13}!" + item.key), item.key) 687 | }) 688 | t.end() 689 | }) 690 | }) 691 | }) 692 | 693 | ltest('data and level-sublevel ttl meta data separation (custom ttlEncoding)', function (t, db, createReadStream) { 694 | var subDb = sublevel(db) 695 | , meta = subDb.sublevel('meta') 696 | , ttldb = ttl(db, { sub: meta, ttlEncoding: bytewise }) 697 | , batch = randomPutBatch(5) 698 | 699 | ttldb.batch(batch, { ttl: 10000 }, function (err) { 700 | t.ok(!err, 'no error') 701 | db2arr(createReadStream, t, function (arr) { 702 | batch.forEach(function (item) { 703 | contains(t, arr, '!meta!' + bwEncode([ item.key ]), bwRange()) 704 | contains(t, arr, { 705 | gt: '!meta!' + bwEncode([ 'x', new Date(0), item.key ]) 706 | , lt: '!meta!' + bwEncode([ 'x', new Date(9999999999999), item.key ]) 707 | }, bwEncode(item.key)) 708 | }) 709 | t.end() 710 | }, { valueEncoding: 'binary' }) 711 | }) 712 | }) 713 | 714 | ltest('data and level-sublevel ttl meta data separation (custom sublevel encoding)', function (t, db, createReadStream) { 715 | var subDb = bwSublevel(db) 716 | , meta = subDb.sublevel('meta') 717 | , ttldb = ttl(db, { sub: meta, ttlEncoding: bytewise }) 718 | , batch = randomPutBatch(5) 719 | 720 | ttldb.batch(batch, { ttl: 10000 }, function (err) { 721 | t.ok(!err, 'no error') 722 | db2arr(createReadStream, t, function (arr) { 723 | batch.forEach(function (item) { 724 | // bytewise keys in bytewise sublevels are double-encoded for now 725 | contains(t, arr, bwEncode([ [ 'meta' ], bwEncode([ item.key ]) ]), bwRange()) 726 | contains(t, arr, { 727 | gt: bwEncode([ [ 'meta' ], bwEncode([ 'x', new Date(0), item.key ]) ]) 728 | , lt: bwEncode([ [ 'meta' ], bwEncode([ 'x', new Date(9999999999999), item.key ]) ]) 729 | }, bwEncode(item.key)) 730 | }) 731 | t.end() 732 | }, { keyEncoding: 'binary', valueEncoding: 'binary' }) 733 | }) 734 | }) 735 | 736 | ltest('that level-sublevel data expires properly', function (t, db, createReadStream) { 737 | var subDb = sublevel(db) 738 | , meta = subDb.sublevel('meta') 739 | , ttldb = ttl(db, { checkFrequency: 25, sub: meta }) 740 | 741 | ttldb.batch(randomPutBatch(50), { ttl: 100 }, function (err) { 742 | t.ok(!err, 'no error') 743 | verifyIn(200, createReadStream, t, function (arr) { 744 | t.equal(arr.length, 0, 'should be empty array') 745 | t.end() 746 | }) 747 | }) 748 | }) 749 | 750 | ltest('that level-sublevel data expires properly (custom ttlEncoding)', function (t, db, createReadStream) { 751 | var subDb = sublevel(db) 752 | , meta = subDb.sublevel('meta') 753 | , ttldb = ttl(db, { checkFrequency: 25, sub: meta, ttlEncoding: bytewise }) 754 | 755 | ttldb.batch(randomPutBatch(50), { ttl: 100 }, function (err) { 756 | t.ok(!err, 'no error') 757 | verifyIn(200, createReadStream, t, function (arr) { 758 | t.equal(arr.length, 0, 'should be empty array') 759 | t.end() 760 | }) 761 | }) 762 | }) 763 | --------------------------------------------------------------------------------