diff --git a/lib/connectors/kv-memory.js b/lib/connectors/kv-memory.js index bd4355bb..9cdb497b 100644 --- a/lib/connectors/kv-memory.js +++ b/lib/connectors/kv-memory.js @@ -64,19 +64,23 @@ KeyValueMemoryConnector.prototype._getStoreForModel = function(modelName) { return this._store[modelName]; }; -KeyValueMemoryConnector.prototype.get = -function(modelName, key, options, callback) { +KeyValueMemoryConnector.prototype._removeIfExpired = function(modelName, key) { var store = this._getStoreForModel(modelName); var item = store[key]; - if (item && item.isExpired()) { debug('Removing expired key', key); delete store[key]; item = undefined; } +}; +KeyValueMemoryConnector.prototype.get = +function(modelName, key, options, callback) { + this._removeIfExpired(modelName, key); + + var store = this._getStoreForModel(modelName); + var item = store[key]; var value = item ? item.value : null; - debug('GET %j %j -> %s', modelName, key, value); if (/^buffer:/.test(value)) { @@ -127,6 +131,29 @@ function(modelName, key, ttl, options, callback) { process.nextTick(callback); }; +KeyValueMemoryConnector.prototype.ttl = +function(modelName, key, options, callback) { + this._removeIfExpired(modelName, key); + + var store = this._getStoreForModel(modelName); + + // key is unknown + if (!(key in store)) { + return process.nextTick(function() { + var err = new Error('Cannot get TTL for unknown key ' + key); + err.statusCode = 404; + callback(err); + }); + } + + var ttl = store[key].getTtl(); + debug('TTL %j %j -> %s', modelName, key, ttl); + + process.nextTick(function() { + callback(null, ttl); + }); +}; + KeyValueMemoryConnector.prototype.disconnect = function(callback) { if (this._cleanupTimer) clearInterval(this._cleanupTimer); @@ -150,3 +177,7 @@ StoreItem.prototype.setTtl = function(ttl) { this.expires = undefined; } }; + +StoreItem.prototype.getTtl = function() { + return !this.expires ? undefined : this.expires - Date.now(); +}; diff --git a/lib/kvao/index.js b/lib/kvao/index.js index 7f537b74..3305a4d3 100644 --- a/lib/kvao/index.js +++ b/lib/kvao/index.js @@ -8,6 +8,7 @@ module.exports = KeyValueAccessObject; KeyValueAccessObject.get = require('./get'); KeyValueAccessObject.set = require('./set'); KeyValueAccessObject.expire = require('./expire'); +KeyValueAccessObject.ttl = require('./ttl'); KeyValueAccessObject.getConnector = function() { return this.getDataSource().connector; diff --git a/lib/kvao/ttl.js b/lib/kvao/ttl.js new file mode 100644 index 00000000..5c2d8abf --- /dev/null +++ b/lib/kvao/ttl.js @@ -0,0 +1,32 @@ +'use strict'; + +var assert = require('assert'); +var utils = require('../utils'); + +/** + * Get remaining expiration (TTL) for a given key. + * + * @param {String} key + * @param {Object} options + * @callback cb + * @param {Error} error + * @param {Number} ttl The remaining TTL for the given key. `undefined` if TTL + * was not initially set. + * + * @header KVAO.ttl(key, cb) + */ +module.exports = function keyValueTtl(key, options, callback) { + if (callback == undefined && typeof options === 'function') { + callback = options; + options = {}; + } else if (!options) { + options = {}; + } + + assert(typeof key === 'string' && key, 'key must be a non-empty string'); + assert(typeof options === 'object', 'options must be an object'); + + callback = callback || utils.createPromiseCallback(); + this.getConnector().ttl(this.modelName, key, options, callback); + return callback.promise; +}; diff --git a/test/kvao/ttl.suite.js b/test/kvao/ttl.suite.js new file mode 100644 index 00000000..cd563a46 --- /dev/null +++ b/test/kvao/ttl.suite.js @@ -0,0 +1,65 @@ +'use strict'; + +var should = require('should'); +var helpers = require('./_helpers'); +var Promise = require('bluebird'); + +module.exports = function(dataSourceFactory, connectorCapabilities) { + describe('ttl', function() { + var CacheItem; + beforeEach(function unpackContext() { + CacheItem = helpers.givenCacheItem(dataSourceFactory); + }); + + it('returns an error when key does not exist', function() { + return CacheItem.ttl('key-does-not-exist').then( + function() { throw new Error('ttl() should have failed'); }, + function(err) { + err.message.should.match(/key-does-not-exist/); + err.should.have.property('statusCode', 404); + }); + }); + + it('returns `undefined` when key does not expire', function() { + return CacheItem.set('a-key', 'a-value') + .then(function() { return CacheItem.ttl('a-key'); }) + .then(function(ttl) { should.not.exist(ttl); }); + }); + + context('existing key with expire before expiration time', function() { + it('returns ttl - Callback API', function(done) { + CacheItem.set('a-key', 'a-value', 10, function(err) { + if (err) return done(err); + CacheItem.ttl('a-key', function(err, ttl) { + if (err) return done(err); + ttl.should.be.within(0, 10); + done(); + }); + }); + }); + + it('returns ttl - Promise API', function() { + return CacheItem.set('a-key', 'a-value', 10) + .delay(1) + .then(function() { return CacheItem.ttl('a-key'); }) + .then(function(ttl) { ttl.should.be.within(0, 10); }); + }); + }); + + context('existing key with expire after expiration time', function(done) { + it('returns an error', function() { + return CacheItem.set('key-does-not-exist', 'a-value', 10) + .delay(20) + .then(function() { + return CacheItem.ttl('key-does-not-exist'); + }) + .then( + function() { throw new Error('ttl() should have failed'); }, + function(err) { + err.message.should.match(/key-does-not-exist/); + err.should.have.property('statusCode', 404); + }); + }); + }); + }); +};