diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..74d8390 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +services: + redis: + image: 'redis:6' + command: --save "" --appendonly no + ports: + - "6379:6379" diff --git a/lib/cb.js b/lib/cb.js new file mode 100644 index 0000000..621ac06 --- /dev/null +++ b/lib/cb.js @@ -0,0 +1,45 @@ +// Based on the work of Jeremy Martin: https://github.com/jmar777/cb +// Added a second timeout(1) to force spinning again the event loop lap and verify if the initial operation has been successful +module.exports = function(callback) { + + var cb = function() { + if (timedout || (once && count)) return; + count += 1; + tid && clearTimeout(tid); + + var args = Array.prototype.slice.call(arguments); + process.nextTick(function() { + if (!errback) return callback.apply(this, args); + args[0] ? errback(args[0]) : callback.apply(this, args.slice(1)); + }); + + }, count = 0, once = false, timedout = false, errback, tid; + + cb.timeout = function(ms) { + tid && clearTimeout(tid); + tid = setTimeout(function() { + // force another second timeout to verify if the operation has been successful + // No need to clear timeout since it has been triggered + tid = setTimeout(function() { + cb(new TimeoutError(ms)); + timedout = true; + }, 1); + }, ms - 1); + return cb; + }; + + cb.error = function(func) { errback = func; return cb; }; + + cb.once = function() { once = true; return cb; }; + + return cb; + +}; + +var TimeoutError = module.exports.TimeoutError = function TimeoutError(ms) { + this.message = 'Specified timeout of ' + ms + 'ms was reached'; + Error.captureStackTrace(this, this.constructor); +}; +TimeoutError.prototype = new Error; +TimeoutError.prototype.constructor = TimeoutError; +TimeoutError.prototype.name = 'TimeoutError'; \ No newline at end of file diff --git a/lib/client.js b/lib/client.js index ce01e42..2bc5a9b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const retry = require('retry'); -const cbControl = require('cb'); +const cbControl = require('./cb'); const validation = require('./validation'); const LimitDBRedis = require('./db'); const disyuntor = require('disyuntor'); diff --git a/package.json b/package.json index 9470efb..e40fe3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "limitd-redis", - "version": "7.1.0", + "version": "7.2.0", "description": "A database client for limits on top of redis", "main": "index.js", "repository": { @@ -8,13 +8,12 @@ "url": "http://github.com/auth0/limitd-redis.git" }, "scripts": { - "test": "NODE_ENV=test nyc mocha --exit" + "test": "trap 'docker-compose down --remove-orphans -v' EXIT; docker-compose up -d && NODE_ENV=test nyc mocha --exit" }, "author": "Auth0", "license": "MIT", "dependencies": { "async": "^2.6.1", - "cb": "^0.1.0", "disyuntor": "^3.5.0", "ioredis": "^4.5.1", "lodash": "^4.17.15", diff --git a/test/cb.tests.js b/test/cb.tests.js new file mode 100644 index 0000000..41b4a33 --- /dev/null +++ b/test/cb.tests.js @@ -0,0 +1,124 @@ +var assert = require('assert'), + cb = require('../lib/cb'); + +function invokeAsync(callback) { + setTimeout(function() { + callback(null, 'foo'); + }, 100); +} + +function invokeAsyncError(callback) { + setTimeout(function() { + callback(new Error()); + }, 100); +} + +function invokeAsyncTwice(callback) { + setTimeout(function() { + callback(null, 'foo'); + callback(null, 'foo'); + }, 100); +} + +describe('cb(callback)', function() { + + it('should invoke the provided callback', function(done) { + invokeAsync(cb(function(err, res) { + assert.strictEqual(res, 'foo'); + done(); + })); + }); + + it('shouldn\'t mess with errors', function(done) { + invokeAsyncError(cb(function(err, res) { + assert(err); + done(); + })); + }); + + it('should allow multiple executions', function(done) { + var count = 0; + invokeAsyncTwice(cb(function(err, res) { + count++; + if (count === 2) done(); + })); + }); + +}); + +describe('cb(callback).timeout(ms)', function() { + + it('should complete successfully within timeout period', function(done) { + invokeAsync(cb(function(err, res) { + assert.strictEqual(res, 'foo'); + done(); + }).timeout(200)); + }); + + it('should complete with an error after timeout period', function(done) { + invokeAsync(cb(function(err, res) { + assert(err); + done(); + }).timeout(50)); + }); + + it('error resulting from a timeout should be instanceof cb.TimeoutError', function(done) { + invokeAsync(cb(function(err, res) { + assert(err instanceof cb.TimeoutError); + done(); + }).timeout(50)); + }); +}); + +describe('cb(callback).error(errback)', function() { + + it('should skip the err argument when invoking callback', function(done) { + invokeAsync(cb(function(res) { + assert.strictEqual(res, 'foo'); + done(); + }).error(assert.ifError)); + }); + + it('should pass errors to provided errback', function(done) { + invokeAsyncError(cb(function(res) { + throw new Error('should not be invoked'); + }).error(function(err) { + assert(err); + done(); + })); + }); + +}); + +describe('cb(callback).error(errback).timeout(ms)', function() { + + it('should skip the err argument when invoking callback', function(done) { + invokeAsync(cb(function(res) { + assert.strictEqual(res, 'foo'); + done(); + }).error(assert.ifError).timeout(200)); + }); + + it('should pass timeout error to provided errback', function(done) { + invokeAsyncError(cb(function(res) { + throw new Error('should not be invoked'); + }).error(function(err) { + assert(err); + done(); + }).timeout(50)); + }); + +}); + +describe('cb(callback).once()', function() { + + it('should allow multiple executions', function(done) { + var count = 0; + invokeAsyncTwice(cb(function(err, res) { + count++; + assert.notEqual(count, 2); + setTimeout(done, 100); + }).once()); + }); + +});