Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 26204dbbaddf4c8979c95ff342872ea3e0d9dfef @rvagg rvagg committed Apr 20, 2013
Showing with 681 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +61 −0 .jshintrc
  3. +39 −0 LICENSE
  4. +47 −0 README.md
  5. +227 −0 level-ttl.js
  6. +31 −0 package.json
  7. +275 −0 test.js
@@ -0,0 +1 @@
+node_modules
@@ -0,0 +1,61 @@
+{
+ "predef": [ ]
+ , "bitwise": false
+ , "camelcase": false
+ , "curly": false
+ , "eqeqeq": false
+ , "forin": false
+ , "immed": false
+ , "latedef": false
+ , "newcap": true
+ , "noarg": true
+ , "noempty": true
+ , "nonew": true
+ , "plusplus": false
+ , "quotmark": true
+ , "regexp": false
+ , "undef": true
+ , "unused": true
+ , "strict": false
+ , "trailing": true
+ , "maxlen": 120
+ , "asi": true
+ , "boss": true
+ , "debug": true
+ , "eqnull": true
+ , "es5": true
+ , "esnext": true
+ , "evil": true
+ , "expr": true
+ , "funcscope": false
+ , "globalstrict": false
+ , "iterator": false
+ , "lastsemic": true
+ , "laxbreak": true
+ , "laxcomma": true
+ , "loopfunc": true
+ , "multistr": false
+ , "onecase": false
+ , "proto": false
+ , "regexdash": false
+ , "scripturl": true
+ , "smarttabs": false
+ , "shadow": false
+ , "sub": true
+ , "supernew": false
+ , "validthis": true
+ , "browser": true
+ , "couch": false
+ , "devel": false
+ , "dojo": false
+ , "mootools": false
+ , "node": true
+ , "nonstandard": true
+ , "prototypejs": false
+ , "rhino": false
+ , "worker": true
+ , "wsh": false
+ , "nomen": false
+ , "onevar": true
+ , "passfail": false
+}
39 LICENSE
@@ -0,0 +1,39 @@
+Copyright 2013, Rod Vagg (the "Original Author")
+All rights reserved.
+
+MIT +no-false-attribs License
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+Distributions of all or part of the Software intended to be used
+by the recipients as they would use the unmodified Software,
+containing modifications that substantially alter, remove, or
+disable functionality of the Software, outside of the documented
+configuration mechanisms provided by the Software, shall be
+modified such that the Original Author's bug reporting email
+addresses and urls are either replaced with the contact information
+of the parties responsible for the changes, or removed entirely.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
+Except where noted, this license applies to any and all software
+programs and associated documentation files created by the
+Original Author, when distributed with the Software.
@@ -0,0 +1,47 @@
+# Level TTL [![Build Status](https://secure.travis-ci.org/rvagg/node-level-ttl.png)](http://travis-ci.org/rvagg/node-level-ttl)
+
+![LevelDB Logo](https://twimg0-a.akamaihd.net/profile_images/3360574989/92fc472928b444980408147e5e5db2fa_bigger.png)
+
+**Add a `'ttl'` (time-to-live) option to LevelUP for `put()` and `batch()`**
+
+Augment the standard implementations to handle a new `'ttl'` option 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.
+
+```js
+var levelup = require('levelup')
+ , ttl = require('level-ttl')
+
+levelup('/tmp/foo.db', function (err, db) {
+ db = ttl(db)
+
+ // ------ put() -------- //
+ // this entry will only stay in the data store for 1 hour
+ db.put('foo', 'bar', { ttl: 1000 * 60 * 60 }, function (err) { /* .. */ })
+
+ // ------ batch() -------- //
+ // the two 'put' entries will only stay in the data store for 1 hour
+ db.batch([
+ { type: 'put', key: 'foo', value: 'bar' }
+ , { type: 'put', key: 'bam', value: 'boom' }
+ , { type: 'del', key: 'w00t' }
+ ], { ttl: 1000 * 60 * 60 }, function (err) { /* .. */ })
+})
+```
+
+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.
+
+**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.
+
+```js
+levelup('/tmp/foo.db', function (err, db) {
+ // scan for deletables every second
+ db = ttl(db, { checkFrequency: 1000 })
+
+ /* .. */
+})
+```
+
+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. So it may be best to not do too much tuning until you have you have something worth tuning.
+
+## Licence
+
+Level TTL is Copyright (c) 2013 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.
@@ -0,0 +1,227 @@
+const after = require('after')
+ , xtend = require('xtend')
+ , sublevel = require('level-sublevel')
+
+ , DEFAULT_FREQUENCY = 10000
+
+var startTtl = function (db, checkFrequency) {
+ db._ttl.intervalId = setInterval(function () {
+ var batch = []
+ , subBatch = []
+
+ db._ttl._checkInProgress = true
+ db._ttl.sub.createReadStream({ end: String(Date.now()) })
+ .on('data', function (data) {
+ subBatch.push({ type: 'del', key: data.value })
+ subBatch.push({ type: 'del', key: data.key })
+ batch.push({ type: 'del', key: data.value })
+ })
+ .on('error', db.emit.bind(db, 'error'))
+ .on('end', function () {
+ if (batch.length) {
+ db._ttl.sub.batch(
+ subBatch
+ , { keyEncoding: 'utf8' }
+ , function (err) {
+ if (err)
+ db.emit('error', err)
+ }
+ )
+ db._ttl.batch(
+ batch
+ , { keyEncoding: 'utf8' }
+ , function (err) {
+ if (err)
+ db.emit('error', err)
+ }
+ )
+ }
+ })
+ .on('close', function () {
+ db._ttl._checkInProgress = false
+ if (db._ttl._stopAfterCheck) {
+ stopTtl(db, db._ttl._stopAfterCheck)
+ db._ttl._stopAfterCheck = null
+ }
+ })
+ }, checkFrequency)
+ }
+
+ , stopTtl = function (db, callback) {
+ // can't close a db while an interator is in progress
+ // so if one is, defer
+ if (db._ttl._checkInProgress)
+ return db._ttl._stopAfterCheck = callback
+ clearInterval(db._ttl.intervalId)
+ callback()
+ }
+
+ , ttlon = function ttlon (db, keys, ttl, callback) {
+ var exp = String(Date.now() + ttl)
+ , batch = []
+
+ if (!Array.isArray(keys))
+ keys = [ keys ]
+
+ ttloff(db, keys, function () {
+ keys.forEach(function (key) {
+ if (typeof key != 'string')
+ key = key.toString()
+ batch.push({ type: 'put', key: key , value: exp })
+ batch.push({ type: 'put', key: exp + '\xff' + key, value: key })
+ })
+
+ if (!batch.length)
+ return callback && callback()
+
+ db._ttl.sub.batch(
+ batch
+ , { keyEncoding: 'utf8', valueEncoding: 'utf8' }
+ , function (err) {
+ if (err)
+ db.emit('error', err)
+ callback && callback()
+ }
+ )
+ })
+ }
+
+ , ttloff = function ttloff (db, keys, callback) {
+ if (!Array.isArray(keys))
+ keys = [ keys ]
+
+ var batch = []
+ , done = after(keys.length, function (err) {
+ if (err)
+ db.emit('error', err)
+
+ if (!batch.length)
+ return callback && callback()
+
+ db._ttl.sub.batch(
+ batch
+ , { keyEncoding: 'utf8', valueEncoding: 'utf8' }
+ , function (err) {
+ if (err)
+ db.emit('error', err)
+ callback && callback()
+ }
+ )
+ })
+
+ keys.forEach(function (key) {
+ if (typeof key != 'string')
+ key = key.toString()
+
+ db._ttl.sub.get(
+ key
+ , { keyEncoding: 'utf8', valueEncoding: 'utf8' }
+ , function (err, exp) {
+ if (!err && exp > 0) {
+ batch.push({ type: 'del', key: key })
+ batch.push({ type: 'del', key: exp + '\xff' + key })
+ }
+ done(err && err.name != 'NotFoundError' && err)
+ }
+ )
+ })
+ }
+
+ , put = function (db, key, value, options, callback) {
+ var ttl
+ , done
+ , _callback = callback
+
+ if (typeof options == 'object' && (ttl = options.ttl) > 0
+ && key !== null && key !== undefined
+ && value !== null && value !== undefined) {
+
+ done = after(2, _callback || function () {})
+ callback = done
+ ttlon(db, key, options.ttl, done)
+ }
+
+ db._ttl.put.call(db, key, value, options, callback)
+ }
+
+ , del = function (db, key, options, callback) {
+ var done
+ , _callback = callback
+ if (key !== null && key !== undefined) {
+ done = after(2, _callback || function () {})
+ callback = done
+ ttloff(db, key, done)
+ }
+
+ db._ttl.del.call(db, key, options, callback)
+ }
+
+ , batch = function (db, arr, options, callback) {
+ var ttl
+ , done
+ , on
+ , off
+ , _callback = callback
+
+ if (typeof options == 'object' && (ttl = options.ttl) > 0 && Array.isArray(arr)) {
+ done = after(3, _callback || function () {})
+ callback = done
+
+ on = []
+ off = []
+ arr.forEach(function (entry) {
+ if (!entry || entry.key === null || entry.key === undefined)
+ return
+
+ if (entry.type == 'put' && entry.value !== null && entry.value !== undefined)
+ on.push(entry.key)
+ if (entry.type == 'del')
+ off.push(entry.key)
+ })
+ if (on.length)
+ ttlon(db, on, options.ttl, done)
+ if (off.length)
+ ttloff(db, off, done)
+ }
+
+ db._ttl.batch.call(db, arr, options, callback)
+ }
+
+ , close = function (db, callback) {
+ stopTtl(db, function () {
+ db._ttl.close.call(db, callback)
+ })
+ }
+
+ , setup = function (db, options) {
+ if (db._ttl)
+ return
+
+ options = xtend({
+ methodPrefix : ''
+ , sublevel : 'ttl'
+ , checkFrequency : DEFAULT_FREQUENCY
+ }, options)
+
+ db = sublevel(db)
+
+ db._ttl = {
+ put : db.put
+ , del : db.del
+ , batch : db.batch
+ , close : db.close
+ , sub : db.sublevel(options.sublevel)
+ }
+
+ db[options.methodPrefix + 'put'] = put.bind(null, db)
+ db[options.methodPrefix + 'del'] = del.bind(null, db)
+ db[options.methodPrefix + 'batch'] = batch.bind(null, db)
+ // we must intercept close()
+ db['close'] = close.bind(null, db)
+
+ startTtl(db, options.checkFrequency)
+
+ return db
+ }
+
+module.exports = setup
Oops, something went wrong.

0 comments on commit 26204db

Please sign in to comment.