rolling-rate-limiter
Advanced tools
Comparing version 0.1.6 to 0.1.7
22
index.js
@@ -6,6 +6,6 @@ var assert = require("assert"); | ||
var redis = options.redis, | ||
interval = options.interval * 1000, // in microseconds | ||
maxInInterval = options.maxInInterval, | ||
minDifference = options.minDifference ? 1000 * options.minDifference : null, // also in microseconds | ||
namespace = options.namespace || (options.redis && ("rate-limiter-" + Math.random().toString(36).slice(2))) || null; | ||
interval = options.interval * 1000, // in microseconds | ||
maxInInterval = options.maxInInterval, | ||
minDifference = options.minDifference ? 1000 * options.minDifference : null, // also in microseconds | ||
namespace = options.namespace || (options.redis && (`rate-limiter-${ Math.random().toString(36).slice(2)}`)) || null; | ||
@@ -37,3 +37,3 @@ assert(interval > 0, "Must pass a positive integer for `options.interval`"); | ||
return function (id, cb) { | ||
return function(id, cb) { | ||
if (!cb) { | ||
@@ -56,3 +56,3 @@ cb = id; | ||
batch.expire(key, Math.ceil(interval / 1000000)); // convert to seconds, as used by redis ttl. | ||
batch.exec(function (err, resultArr) { | ||
batch.exec(function(err, resultArr) { | ||
if (err) return cb(err); | ||
@@ -63,3 +63,3 @@ | ||
var tooManyInInterval = userSet.length >= maxInInterval; | ||
var timeSinceLastRequest = minDifference && (now - userSet[userSet.length - 1]); | ||
var timeSinceLastRequest = now - userSet[userSet.length - 1]; | ||
@@ -70,3 +70,3 @@ var result; | ||
if (tooManyInInterval || timeSinceLastRequest < minDifference) { | ||
result = Math.min(userSet[0] - now + interval, minDifference ? minDifference - timeSinceLastRequest : Infinity); | ||
result = Math.max(tooManyInInterval ? userSet[userSet.length - maxInInterval] - now + interval : 0, minDifference ? minDifference : 0); | ||
result = Math.floor(result / 1000); // convert to miliseconds for user readability. | ||
@@ -81,3 +81,3 @@ } else { | ||
} else { | ||
return function () { | ||
return function() { | ||
var args = Array.prototype.slice.call(arguments); | ||
@@ -104,3 +104,3 @@ var cb = args.pop(); | ||
var tooManyInInterval = userSet.length >= maxInInterval; | ||
var timeSinceLastRequest = minDifference && (now - userSet[userSet.length - 1]); | ||
var timeSinceLastRequest = now - userSet[userSet.length - 1]; | ||
@@ -111,3 +111,3 @@ var result; | ||
if (tooManyInInterval || timeSinceLastRequest < minDifference) { | ||
result = Math.min(userSet[0] - now + interval, minDifference ? minDifference - timeSinceLastRequest : Infinity); | ||
result = Math.max(tooManyInInterval ? userSet[userSet.length - maxInInterval] - now + interval : 0, minDifference ? minDifference : 0); | ||
result = Math.floor(result / 1000); // convert from microseconds for user readability. | ||
@@ -114,0 +114,0 @@ } else { |
{ | ||
"name": "rolling-rate-limiter", | ||
"version": "0.1.6", | ||
"version": "0.1.7", | ||
"description": "Rate limiter that supports a rolling window, either in-memory or backed by redis", | ||
@@ -31,7 +31,10 @@ "main": "index.js", | ||
"devDependencies": { | ||
"async": "~0.9.0", | ||
"chai": "~1.10.0", | ||
"eslint": "^3.19.0", | ||
"eslint-config-classdojo": "^1.2.6", | ||
"fakeredis": "~0.3.0", | ||
"chai": "~1.10.0", | ||
"async": "~0.9.0", | ||
"lodash": "^4.17.4", | ||
"mocha": "~2.1.0" | ||
} | ||
} |
@@ -1,16 +0,16 @@ | ||
var expect = require("chai").expect; | ||
var async = require("async"); | ||
var redis = require("fakeredis"); | ||
const expect = require("chai").expect; | ||
const async = require("async"); | ||
const redis = require("fakeredis"); | ||
var RateLimiter = require("../"); | ||
const RateLimiter = require("../"); | ||
var RateLimitedCounter = function(options) { | ||
var rateLimiter = RateLimiter(options); | ||
var counts = {}; | ||
const RateLimitedCounter = function(options) { | ||
const rateLimiter = RateLimiter(options); | ||
const counts = {}; | ||
return { | ||
increment: function() { | ||
var args = Array.prototype.slice.call(arguments); | ||
var cb = args.pop(); | ||
var userId; | ||
increment () { | ||
const args = Array.prototype.slice.call(arguments); | ||
let cb = args.pop(); | ||
let userId; | ||
if (typeof cb === "function") { | ||
@@ -23,21 +23,22 @@ userId = args[0] || ""; | ||
counts[userId] = counts[userId] || 0; | ||
var limit = userId ? rateLimiter.bind(null, userId) : rateLimiter; | ||
const limit = userId ? rateLimiter.bind(null, userId) : rateLimiter; | ||
if (cb) { | ||
limit(function(err, blocked) { | ||
if (!blocked) { | ||
limit(function(err, timeLeft) { | ||
if (!timeLeft) { | ||
counts[userId]++; | ||
} | ||
cb(err); | ||
cb(err, timeLeft); | ||
}); | ||
} else { | ||
var blocked = limit(); | ||
if (!blocked) { | ||
const timeLeft = limit(); | ||
if (!timeLeft) { | ||
counts[userId]++; | ||
} | ||
return timeLeft; | ||
} | ||
}, | ||
getCount: function(userId) { | ||
getCount (userId) { | ||
return counts[userId || ""]; | ||
} | ||
}, | ||
}; | ||
@@ -47,8 +48,8 @@ | ||
describe("rateLimiter", function () { | ||
describe("rateLimiter", function() { | ||
describe("options validation", function() { | ||
var options; | ||
let options; | ||
beforeEach(function() { | ||
@@ -59,3 +60,3 @@ options = { | ||
minDifference: 500, | ||
namespace: "MyNamespace" | ||
namespace: "MyNamespace", | ||
}; | ||
@@ -97,9 +98,19 @@ }); | ||
it("allows requests that don't exceed the maximum over the interval", function() { | ||
const counter = RateLimitedCounter({ | ||
interval: 100, | ||
maxInInterval: 30, | ||
}); | ||
for (let n = 0; n < 100; n++) { | ||
counter.increment(); | ||
} | ||
expect(counter.getCount()).to.equal(30); | ||
}); | ||
it("prevents requests that exceed the maximum over the interval", function() { | ||
var counter = RateLimitedCounter({ | ||
interval: 300, | ||
maxInInterval: 30 | ||
const counter = RateLimitedCounter({ | ||
interval: 100, | ||
maxInInterval: 30, | ||
}); | ||
for (var n = 0; n < 100; n++) { | ||
for (let n = 0; n < 100; n++) { | ||
counter.increment(); | ||
@@ -111,8 +122,7 @@ } | ||
it("keeps seperate counts for multiple users", function() { | ||
var counter = RateLimitedCounter({ | ||
interval: 300, | ||
maxInInterval: 30 | ||
const counter = RateLimitedCounter({ | ||
interval: 100, | ||
maxInInterval: 30, | ||
}); | ||
for (var n = 0; n < 300; n++) { | ||
for (let n = 0; n < 300; n++) { | ||
counter.increment(n % 3); | ||
@@ -127,14 +137,14 @@ } | ||
it("allows requests after the interval has passed", function(done) { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
interval: 100, | ||
maxInInterval: 30 | ||
maxInInterval: 30, | ||
}); | ||
for (var n = 0; n < 300; n++) { | ||
for (let n = 0; n < 300; n++) { | ||
counter.increment(n % 3); | ||
} | ||
setTimeout(function() { | ||
for (var n = 0; n < 300; n++) { | ||
for (let n = 0; n < 300; n++) { | ||
counter.increment(n % 3); | ||
} | ||
} | ||
expect(counter.getCount(0)).to.equal(60); | ||
@@ -148,9 +158,9 @@ expect(counter.getCount(1)).to.equal(60); | ||
it("doesn't allow consecutive requests less than the minDifferent apart", function() { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
interval: 1000000, | ||
maxInInterval: 1000, | ||
minDifference: 100 | ||
minDifference: 100, | ||
}); | ||
for (var n = 0; n < 300; n++) { | ||
for (let n = 0; n < 300; n++) { | ||
counter.increment(n % 3); | ||
@@ -164,10 +174,10 @@ } | ||
it("returns the time after which actions will be allowed", function() { | ||
var limiter1 = RateLimiter({ | ||
const limiter1 = RateLimiter({ | ||
interval: 10000, | ||
maxInInterval: 2 | ||
maxInInterval: 2, | ||
}); | ||
var first = limiter1(); | ||
var second = limiter1(); | ||
var third = limiter1(); | ||
let first = limiter1(); | ||
let second = limiter1(); | ||
const third = limiter1(); | ||
expect(first).to.equal(0); | ||
@@ -178,6 +188,6 @@ expect(second).to.equal(0); | ||
var limiter2 = RateLimiter({ | ||
const limiter2 = RateLimiter({ | ||
interval: 10000, | ||
maxInInterval: 100, | ||
minDifference: 100 | ||
minDifference: 100, | ||
}); | ||
@@ -197,5 +207,5 @@ | ||
it("prevents requests that exceed the maximum over the interval", function(done) { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
interval: 300, | ||
maxInInterval: 30 | ||
maxInInterval: 30, | ||
}); | ||
@@ -213,5 +223,5 @@ | ||
it("keeps seperate counts for multiple users", function(done) { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
interval: 300, | ||
maxInInterval: 30 | ||
maxInInterval: 30, | ||
}); | ||
@@ -232,5 +242,5 @@ | ||
it("allows requests after the interval has passed", function(done) { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
interval: 150, | ||
maxInInterval: 30 | ||
maxInInterval: 30, | ||
}); | ||
@@ -245,3 +255,3 @@ | ||
counter.increment(n % 3, next); | ||
}, function(err, results) { | ||
}, function(err) { | ||
if (err) throw err; | ||
@@ -258,6 +268,6 @@ expect(counter.getCount(0)).to.equal(60); | ||
it("doesn't allow consecutive requests less than the minDifferent apart", function(done) { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
interval: 1000000, | ||
maxInInterval: 1000, | ||
minDifference: 100 | ||
minDifference: 100, | ||
}); | ||
@@ -285,7 +295,7 @@ | ||
it("prevents requests that exceed the maximum over the interval", function(done) { | ||
var client = redis.createClient(); | ||
var counter = RateLimitedCounter({ | ||
const client = redis.createClient(); | ||
const counter = RateLimitedCounter({ | ||
redis: client, | ||
interval: 300, | ||
maxInInterval: 30 | ||
maxInInterval: 30, | ||
}); | ||
@@ -303,10 +313,10 @@ | ||
it("works when redis is in buffer mode", function(done) { | ||
var client = redis.createClient({return_buffers: true}); | ||
const client = redis.createClient({return_buffers: true}); | ||
// fakeredis seems to hide this option. | ||
client.options = {}; | ||
client.options.return_buffers = true; | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
redis: client, | ||
interval: 300, | ||
maxInInterval: 30 | ||
maxInInterval: 30, | ||
}); | ||
@@ -324,6 +334,6 @@ | ||
it("keeps seperate counts for multiple users", function(done) { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
redis: redis.createClient(), | ||
interval: 300, | ||
maxInInterval: 30 | ||
maxInInterval: 30, | ||
}); | ||
@@ -344,6 +354,6 @@ | ||
it("allows requests after the interval has passed", function(done) { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
redis: redis.createClient(), | ||
interval: 150, | ||
maxInInterval: 30 | ||
maxInInterval: 30, | ||
}); | ||
@@ -358,3 +368,3 @@ | ||
counter.increment(n % 3, next); | ||
}, function(err, results) { | ||
}, function(err) { | ||
if (err) throw err; | ||
@@ -371,7 +381,7 @@ expect(counter.getCount(0)).to.equal(60); | ||
it("doesn't allow consecutive requests less than the minDifferent apart", function(done) { | ||
var counter = RateLimitedCounter({ | ||
const counter = RateLimitedCounter({ | ||
redis: redis.createClient(), | ||
interval: 1000000, | ||
maxInInterval: 1000, | ||
minDifference: 100 | ||
minDifference: 100, | ||
}); | ||
@@ -391,4 +401,4 @@ | ||
it("can share a redis between multiple rate limiters in different namespaces", function(done) { | ||
var client = redis.createClient(); | ||
var counters = [ | ||
const client = redis.createClient(); | ||
const counters = [ | ||
RateLimitedCounter({ | ||
@@ -403,6 +413,6 @@ redis: client, | ||
maxInInterval: 15, | ||
}) | ||
}), | ||
]; | ||
async.times(200, function(n, next) { | ||
var counter = counters[n % 2]; | ||
const counter = counters[n % 2]; | ||
counter.increment(n % 3, next); | ||
@@ -422,8 +432,8 @@ }, function(err) { | ||
it("can share a redis between multiple rate limiters in the same namespace", function(done) { | ||
var client = redis.createClient(); | ||
var namespace = Math.random().toString(36).slice(2); | ||
var counters = [ | ||
const client = redis.createClient(); | ||
const namespace = Math.random().toString(36).slice(2); | ||
const counters = [ | ||
RateLimitedCounter({ | ||
redis: client, | ||
namespace: namespace, | ||
namespace, | ||
interval: 300, | ||
@@ -434,9 +444,9 @@ maxInInterval: 30, | ||
redis: client, | ||
namespace: namespace, | ||
namespace, | ||
interval: 300, | ||
maxInInterval: 30, | ||
}) | ||
}), | ||
]; | ||
async.times(200, function(n, next) { | ||
var counter = counters[(n + 1) % 2]; | ||
const counter = counters[(n + 1) % 2]; | ||
counter.increment(n % 3, next); | ||
@@ -447,8 +457,8 @@ }, function(err) { | ||
// CountXY is the count for counter x and user y. | ||
var count00 = counters[0].getCount(0); | ||
var count01 = counters[0].getCount(1); | ||
var count02 = counters[0].getCount(2); | ||
var count10 = counters[1].getCount(0); | ||
var count11 = counters[1].getCount(1); | ||
var count12 = counters[1].getCount(2); | ||
const count00 = counters[0].getCount(0); | ||
const count01 = counters[0].getCount(1); | ||
const count02 = counters[0].getCount(2); | ||
const count10 = counters[1].getCount(0); | ||
const count11 = counters[1].getCount(1); | ||
const count12 = counters[1].getCount(2); | ||
@@ -469,6 +479,6 @@ expect(count00 + count10).to.equal(30); | ||
it("returns the time after which actions will be allowed", function(done) { | ||
var limiter1 = RateLimiter({ | ||
const limiter1 = RateLimiter({ | ||
redis: redis.createClient(), | ||
interval: 10000, | ||
maxInInterval: 2 | ||
maxInInterval: 2, | ||
}); | ||
@@ -485,6 +495,6 @@ async.times(3, function(n, next) { | ||
var limiter2 = RateLimiter({ | ||
const limiter2 = RateLimiter({ | ||
interval: 10000, | ||
maxInInterval: 100, | ||
minDifference: 100 | ||
minDifference: 100, | ||
}); | ||
@@ -503,12 +513,12 @@ async.times(3, function(n, next) { | ||
it("ttl functions properly", function(done) { | ||
var client = redis.createClient(); | ||
var namespace = Math.random().toString(36).slice(2); | ||
var limiter = RateLimiter({ | ||
const client = redis.createClient(); | ||
const namespace = Math.random().toString(36).slice(2); | ||
const limiter = RateLimiter({ | ||
redis: client, | ||
interval: 10000, | ||
maxInInterval: 5, | ||
namespace: namespace | ||
namespace, | ||
}); | ||
limiter("1", function(err, result) { | ||
var key = namespace + "1"; | ||
limiter("1", function() { | ||
const key = `${namespace }1`; | ||
client.ttl(key, function(err, result) { | ||
@@ -515,0 +525,0 @@ expect(result).to.equal(10); |
24816
8
550
7