rate-limiter-flexible
Advanced tools
Comparing version 0.11.0 to 0.12.0
@@ -39,2 +39,3 @@ const Record = require('./Record'); | ||
}, durationMs); | ||
this._storage[key].timeoutId.unref(); | ||
@@ -41,0 +42,0 @@ return new RateLimiterRes(0, durationMs, this._storage[key].value, true); |
@@ -78,2 +78,6 @@ module.exports = class RateLimiterAbstract { | ||
parseKey(rlKey) { | ||
return rlKey.substring(this.keyPrefix.length); | ||
} | ||
consume() { | ||
@@ -80,0 +84,0 @@ throw new Error("You have to implement the method 'consume'!"); |
@@ -57,3 +57,3 @@ /** | ||
const workerSendToMaster = function (func, id, key, points) { | ||
const workerSendToMaster = function (func, id, key, arg) { | ||
const payload = { | ||
@@ -66,3 +66,3 @@ channel, | ||
key, | ||
points, | ||
arg, | ||
}, | ||
@@ -88,10 +88,13 @@ }; | ||
case 'consume': | ||
promise = this._rateLimiters[msg.keyPrefix].consume(msg.data.key, msg.data.points); | ||
promise = this._rateLimiters[msg.keyPrefix].consume(msg.data.key, msg.data.arg); | ||
break; | ||
case 'penalty': | ||
promise = this._rateLimiters[msg.keyPrefix].penalty(msg.data.key, msg.data.points); | ||
promise = this._rateLimiters[msg.keyPrefix].penalty(msg.data.key, msg.data.arg); | ||
break; | ||
case 'reward': | ||
promise = this._rateLimiters[msg.keyPrefix].reward(msg.data.key, msg.data.points); | ||
promise = this._rateLimiters[msg.keyPrefix].reward(msg.data.key, msg.data.arg); | ||
break; | ||
case 'block': | ||
promise = this._rateLimiters[msg.keyPrefix].block(msg.data.key, msg.data.arg); | ||
break; | ||
default: | ||
@@ -268,2 +271,11 @@ return false; | ||
} | ||
block(key, secDuration) { | ||
return new Promise((resolve, reject) => { | ||
const id = getPromiseId.call(this); | ||
savePromise.call(this, id, resolve, reject); | ||
workerSendToMaster.call(this, 'block', id, key, secDuration); | ||
}); | ||
} | ||
} | ||
@@ -270,0 +282,0 @@ |
const RateLimiterAbstract = require('./RateLimiterAbstract'); | ||
const MemoryStorage = require('./component/MemoryStorage/MemoryStorage'); | ||
const RateLimiterRes = require('./RateLimiterRes'); | ||
@@ -57,2 +58,16 @@ class RateLimiterMemory extends RateLimiterAbstract { | ||
} | ||
/** | ||
* Block any key for secDuration seconds | ||
* | ||
* @param key | ||
* @param secDuration | ||
*/ | ||
block(key, secDuration) { | ||
const msDuration = secDuration * 1000; | ||
const initPoints = this.points + 1; | ||
this._memoryStorage.set(this.getKey(key), initPoints, msDuration); | ||
return Promise.resolve(new RateLimiterRes(0, msDuration, initPoints)); | ||
} | ||
} | ||
@@ -59,0 +74,0 @@ |
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract'); | ||
const RateLimiterRes = require('./RateLimiterRes'); | ||
const getRateLimiterRes = function (points, result) { | ||
const res = new RateLimiterRes(); | ||
res.isFirstInDuration = result.value === null; | ||
res.consumedPoints = res.isFirstInDuration ? points : result.value.points; | ||
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); | ||
res.msBeforeNext = res.isFirstInDuration | ||
? this.duration * 1000 | ||
: Math.max(new Date(result.value.expire).getTime() - Date.now(), 0); | ||
return res; | ||
}; | ||
const update = function (key, points) { | ||
@@ -51,26 +37,2 @@ return this._collection.findOneAndUpdate( | ||
const afterConsume = function (resolve, reject, rlKey, changedPoints, result) { | ||
const res = getRateLimiterRes.call(this, changedPoints, result); | ||
if (res.consumedPoints > this.points) { | ||
if (this.inmemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inmemoryBlockOnConsumed) { | ||
// Block key for this.inmemoryBlockDuration seconds | ||
this._inmemoryBlockedKeys.add(rlKey, this.inmemoryBlockDuration); | ||
res.msBeforeNext = this.msInmemoryBlockDuration; | ||
// Block only first time when consumed more than points | ||
} else if (this.blockDuration > 0 && res.consumedPoints <= (this.points + changedPoints)) { | ||
upsertExpire.call(this, rlKey, res.consumedPoints, this.msBlockDuration); | ||
res.msBeforeNext = this.msBlockDuration; | ||
} | ||
reject(res); | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) { | ||
const delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2)); | ||
setTimeout(resolve, delay, res); | ||
} else { | ||
resolve(res); | ||
} | ||
}; | ||
class RateLimiterMongo extends RateLimiterStoreAbstract { | ||
@@ -113,2 +75,28 @@ /** | ||
_getRateLimiterRes(rlKey, changedPoints, result) { | ||
const res = new RateLimiterRes(); | ||
res.isFirstInDuration = result.value === null; | ||
res.consumedPoints = res.isFirstInDuration ? changedPoints : result.value.points; | ||
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); | ||
res.msBeforeNext = res.isFirstInDuration | ||
? this.duration * 1000 | ||
: Math.max(new Date(result.value.expire).getTime() - Date.now(), 0); | ||
return res; | ||
} | ||
_block(rlKey, initPoints, msDuration) { | ||
return new Promise((resolve, reject) => { | ||
upsertExpire.call(this, rlKey, initPoints, msDuration) | ||
.then(() => { | ||
resolve(new RateLimiterRes(0, msDuration, initPoints)); | ||
}) | ||
.catch((err) => { | ||
this._handleError(err, 'block', resolve, reject, this.parseKey(rlKey), initPoints); | ||
}); | ||
}); | ||
} | ||
/** | ||
@@ -131,6 +119,6 @@ * | ||
.then((res) => { | ||
afterConsume.call(this, resolve, reject, rlKey, pointsToConsume, res); | ||
this._afterConsume(resolve, reject, rlKey, pointsToConsume, res); | ||
}) | ||
.catch((err) => { | ||
this.handleError(err, 'consume', resolve, reject, key, pointsToConsume); | ||
this._handleError(err, 'consume', resolve, reject, key, pointsToConsume); | ||
}); | ||
@@ -145,6 +133,6 @@ }); | ||
.then((res) => { | ||
resolve(getRateLimiterRes.call(this, points, res)); | ||
resolve(this._getRateLimiterRes(rlKey, points, res)); | ||
}) | ||
.catch((err) => { | ||
this.handleError(err, 'penalty', resolve, reject, key, points); | ||
this._handleError(err, 'penalty', resolve, reject, key, points); | ||
}); | ||
@@ -159,6 +147,6 @@ }); | ||
.then((res) => { | ||
resolve(getRateLimiterRes.call(this, points, res)); | ||
resolve(this._getRateLimiterRes(rlKey, points, res)); | ||
}) | ||
.catch((err) => { | ||
this.handleError(err, 'reward', resolve, reject, key, points); | ||
this._handleError(err, 'reward', resolve, reject, key, points); | ||
}); | ||
@@ -165,0 +153,0 @@ }); |
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract'); | ||
const RateLimiterRes = require('./RateLimiterRes'); | ||
const afterConsume = function (resolve, reject, rlKey, changedPoints, results) { | ||
let [resSet, consumed, resTtlMs] = results; | ||
// Support ioredis results format | ||
if (Array.isArray(resSet)) { | ||
[, resSet] = resSet; | ||
[, consumed] = consumed; | ||
[, resTtlMs] = resTtlMs; | ||
} | ||
const res = new RateLimiterRes(); | ||
res.consumedPoints = consumed; | ||
res.isFirstInDuration = resSet === 'OK'; | ||
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); | ||
if (resTtlMs === -1) { // If rlKey created by incrby() not by set() | ||
res.isFirstInDuration = true; | ||
res.msBeforeNext = this.duration; | ||
this.redis.expire(rlKey, this.duration); | ||
} else { | ||
res.msBeforeNext = resTtlMs; | ||
} | ||
if (res.consumedPoints > this.points) { | ||
if (this.inmemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inmemoryBlockOnConsumed) { | ||
// Block key in memory for this.inmemoryBlockDuration seconds | ||
this._inmemoryBlockedKeys.add(rlKey, this.inmemoryBlockDuration); | ||
res.msBeforeNext = this.msInmemoryBlockDuration; | ||
} else if (this.blockDuration > 0 && res.consumedPoints <= (this.points + changedPoints)) { | ||
// Block key | ||
this.redis.set(rlKey, res.consumedPoints, 'EX', this.blockDuration, () => {}); | ||
res.msBeforeNext = this.msBlockDuration; | ||
} | ||
reject(res); | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) { | ||
const delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2)); | ||
setTimeout(resolve, delay, res); | ||
} else { | ||
resolve(res); | ||
} | ||
}; | ||
class RateLimiterRedis extends RateLimiterStoreAbstract { | ||
@@ -72,2 +31,37 @@ /** | ||
_getRateLimiterRes(rlKey, changedPoints, result) { | ||
let [resSet, consumed, resTtlMs] = result; | ||
// Support ioredis results format | ||
if (Array.isArray(resSet)) { | ||
[, resSet] = resSet; | ||
[, consumed] = consumed; | ||
[, resTtlMs] = resTtlMs; | ||
} | ||
const res = new RateLimiterRes(); | ||
res.consumedPoints = consumed; | ||
res.isFirstInDuration = resSet === 'OK'; | ||
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); | ||
if (resTtlMs === -1) { // If rlKey created by incrby() not by set() | ||
res.isFirstInDuration = true; | ||
res.msBeforeNext = this.duration; | ||
this.redis.expire(rlKey, this.duration); | ||
} else { | ||
res.msBeforeNext = resTtlMs; | ||
} | ||
return res; | ||
} | ||
_block(rlKey, initPoints, msDuration) { | ||
return new Promise((resolve, reject) => { | ||
this.redis.set(rlKey, initPoints, 'EX', Math.floor(msDuration / 1000), (err) => { | ||
if (err) { | ||
return this._handleError(err, 'block', resolve, reject, this.parseKey(rlKey), initPoints); | ||
} | ||
resolve(new RateLimiterRes(0, msDuration, initPoints)); | ||
}); | ||
}); | ||
} | ||
/** | ||
@@ -94,5 +88,5 @@ * | ||
if (err) { | ||
this.handleError(err, 'consume', resolve, reject, key, pointsToConsume); | ||
this._handleError(err, 'consume', resolve, reject, key, pointsToConsume); | ||
} else { | ||
afterConsume.call(this, resolve, reject, rlKey, pointsToConsume, results); | ||
this._afterConsume(resolve, reject, rlKey, pointsToConsume, results); | ||
} | ||
@@ -108,3 +102,3 @@ }); | ||
if (err) { | ||
this.handleError(err, 'penalty', resolve, reject, key, points); | ||
this._handleError(err, 'penalty', resolve, reject, key, points); | ||
} else { | ||
@@ -122,3 +116,3 @@ resolve(new RateLimiterRes(this.points - consumedPoints, 0, consumedPoints)); | ||
if (err) { | ||
this.handleError(err, 'reward', resolve, reject, key, points); | ||
this._handleError(err, 'reward', resolve, reject, key, points); | ||
} else { | ||
@@ -125,0 +119,0 @@ resolve(new RateLimiterRes(this.points - consumedPoints, 0, consumedPoints)); |
@@ -32,3 +32,46 @@ const RateLimiterAbstract = require('./RateLimiterAbstract'); | ||
handleError(err, funcName, resolve, reject, key, points) { | ||
/** | ||
* Have to be launched after consume | ||
* It blocks key and execute evenly depending on result from store | ||
* | ||
* @param resolve | ||
* @param reject | ||
* @param rlKey | ||
* @param changedPoints | ||
* @param storeResult | ||
* @private | ||
*/ | ||
_afterConsume(resolve, reject, rlKey, changedPoints, storeResult) { | ||
const res = this._getRateLimiterRes(rlKey, changedPoints, storeResult); | ||
if (res.consumedPoints > this.points) { | ||
if (this.inmemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inmemoryBlockOnConsumed) { | ||
// Block key for this.inmemoryBlockDuration seconds | ||
this._inmemoryBlockedKeys.add(rlKey, this.inmemoryBlockDuration); | ||
res.msBeforeNext = this.msInmemoryBlockDuration; | ||
reject(res); | ||
// Block only first time when consumed more than points | ||
} else if (this.blockDuration > 0 && res.consumedPoints <= (this.points + changedPoints)) { | ||
this._block(rlKey, res.consumedPoints, this.msBlockDuration) | ||
.then(() => { | ||
res.msBeforeNext = this.msBlockDuration; | ||
reject(res); | ||
}) | ||
.catch((err) => { | ||
this._handleError(err, 'block', resolve, reject, this.parseKey(rlKey), res.consumedPoints); | ||
}); | ||
} else { | ||
reject(res); | ||
} | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) { | ||
const delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2)); | ||
setTimeout(resolve, delay, res); | ||
} else { | ||
resolve(res); | ||
} | ||
} | ||
_handleError(err, funcName, resolve, reject, key, points) { | ||
if (!(this.insuranceLimiter instanceof RateLimiterAbstract)) { | ||
@@ -83,2 +126,41 @@ reject(err); | ||
} | ||
/** | ||
* Block any key for secDuration seconds | ||
* | ||
* @param key | ||
* @param secDuration | ||
* | ||
* @return Promise<any> | ||
*/ | ||
block(key, secDuration) { | ||
const msDuration = secDuration * 1000; | ||
return this._block(this.getKey(key), this.points + 1, msDuration); | ||
} | ||
/** | ||
* Get RateLimiterRes object filled depending on storeResult, which specific for exact store | ||
* | ||
* @param rlKey | ||
* @param changedPoints | ||
* @param storeResult | ||
* @private | ||
*/ | ||
_getRateLimiterRes(rlKey, changedPoints, storeResult) { // eslint-disable-line no-unused-vars | ||
throw new Error("You have to implement the method '_getRateLimiterRes'!"); | ||
} | ||
/** | ||
* Block key for this.msBlockDuration milliseconds | ||
* Usually, it just prolongs lifetime of key | ||
* | ||
* @param rlKey | ||
* @param points | ||
* @param msDuration | ||
* | ||
* @return Promise<any> | ||
*/ | ||
_block(rlKey, points, msDuration) { // eslint-disable-line no-unused-vars | ||
return Promise.reject(new Error("You have to implement the method '_block'!")); | ||
} | ||
}; |
{ | ||
"name": "rate-limiter-flexible", | ||
"version": "0.11.0", | ||
"version": "0.12.0", | ||
"description": "Flexible API rate limiter backed by Redis for distributed node.js applications", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -28,3 +28,3 @@ [![Build Status](https://travis-ci.org/animir/node-rate-limiter-flexible.png)](https://travis-ci.org/animir/node-rate-limiter-flexible) | ||
* Redis and Mongo errors don't result to broken app if `insuranceLimiter` set up | ||
* useful `penalty` and `reward` methods to change limits on some results of an action | ||
* useful `block`, `penalty` and `reward` methods | ||
@@ -425,2 +425,11 @@ ### Links | ||
### rateLimiter.block(key, secDuration) | ||
Block `key` for `secDuration` seconds | ||
Returns Promise, which: | ||
* **resolved** with `RateLimiterRes` | ||
* **rejected** only for Redis and Mongo if `insuranceLimiter` isn't setup: when some error happened, where reject reason `rejRes` is Error object | ||
* **rejected** only for RateLimiterCluster if `insuranceLimiter` isn't setup: when `timeoutMs` exceeded, where reject reason `rejRes` is Error object | ||
## Contribution | ||
@@ -427,0 +436,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
123112
29
1014
440