express-adaptive-rate-limiter 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +434 -17
- package/dist/algorithms/baseAlgorithm.js +1 -1
- package/dist/algorithms/fixedWindow.js +1 -1
- package/dist/algorithms/leakyBucket.js +1 -1
- package/dist/algorithms/slidingWindowCounter.js +1 -1
- package/dist/algorithms/slidingWindowLog.js +1 -1
- package/dist/algorithms/tokenBucket.js +1 -1
- package/dist/core/blockedKeys.js +1 -0
- package/dist/core/coreMiddleware.js +1 -0
- package/dist/core/limiters/clusterLimiter.js +1 -0
- package/dist/core/limiters/dynamoDbLimiter.js +1 -0
- package/dist/core/limiters/etcdLimiter.js +1 -0
- package/dist/core/limiters/memcachedLimiter.js +1 -0
- package/dist/core/limiters/memoryLimiter.js +1 -0
- package/dist/core/limiters/mongoLimiter.js +1 -0
- package/dist/core/limiters/mongooseLimiter.js +1 -0
- package/dist/core/limiters/mySqlLimiter.js +12 -0
- package/dist/core/limiters/nodeClusterLimiter.js +1 -0
- package/dist/core/limiters/postgresLimiter.js +13 -0
- package/dist/core/limiters/prismaLimiter.js +24 -0
- package/dist/core/limiters/redisLimiter.js +16 -0
- package/dist/core/limiters/sqliteLimiter.js +13 -0
- package/dist/core/limiters/valkeyLimiter.js +16 -0
- package/dist/core/rateLimiter.js +1 -0
- package/dist/core/rateLimiterRes.js +1 -0
- package/dist/core/rateLimiterStore.js +1 -0
- package/dist/core/wrappers/blackAndWhiteLimiter.js +1 -0
- package/dist/core/wrappers/burstyRateLimiter.js +1 -0
- package/dist/core/wrappers/rateLimiterQueue.js +1 -0
- package/dist/core/wrappers/rateLimiterUnion.js +1 -0
- package/dist/core/wrappers/timeoutLimiter.js +1 -0
- package/dist/index.js +1 -1
- package/dist/middleware/expressMiddleware.js +1 -1
- package/dist/middleware/headers.js +1 -1
- package/dist/rateLimit.js +1 -1
- package/dist/rateLimitManager.js +1 -1
- package/dist/stores/baseStore.js +1 -1
- package/dist/stores/luaScripts.js +379 -1
- package/dist/stores/memoryStore.js +1 -1
- package/dist/stores/redisStore.js +1 -1
- package/dist/types/index.d.ts +525 -1
- package/dist/utils/errors.js +1 -1
- package/dist/utils/graphQLParser.js +1 -1
- package/dist/utils/ipUtils.js +1 -1
- package/dist/utils/keyGenerator.js +1 -1
- package/dist/utils/redisClient.js +1 -0
- package/dist/utils/retryAfter.js +1 -1
- package/dist/utils/skipUtils.js +1 -1
- package/dist/utils/time.js +1 -1
- package/dist/utils/validation.js +1 -1
- package/package.json +72 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{BaseAlgorithm}from'./baseAlgorithm.js';export class SlidingWindowCounter extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}async consume(key,cost=1){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-counter',[key],[this.limit,this.windowMs,cost,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}return this.withLock(key,async()=>{const now=Date.now();const currWindowStart=Math.floor(now/this.windowMs)*this.windowMs;const resetTime=currWindowStart+this.windowMs;const state=await this.store.get(key);let prevCount=0;let currCount=0;if(state){if(state.windowStart===currWindowStart){prevCount=state.prevCount||0;currCount=state.currCount||0;}else if(currWindowStart===state.windowStart+this.windowMs){prevCount=state.currCount||0;currCount=0;}}const elapsed=now-currWindowStart;const weight=Math.max(0,1-elapsed/this.windowMs);const estimated=prevCount*weight+currCount;if(estimated+cost<=this.limit){currCount+=cost;const consumed=Math.round(estimated+cost);const remaining=Math.max(0,this.limit-consumed);await this.store.set(key,{windowStart:currWindowStart,prevCount,currCount},this.windowMs*2);return this.normalizeResult(true,this.limit,remaining,consumed,0,resetTime);}const consumed=Math.round(estimated);const remaining=Math.max(0,this.limit-consumed);let waitMs=resetTime-now;if(prevCount>0){const neededWeight=(this.limit-currCount-cost)/prevCount;if(neededWeight<1&&neededWeight>=0){const t=this.windowMs*(1-neededWeight);waitMs=Math.max(1,Math.ceil(t-elapsed));}}if(waitMs<=0||waitMs>this.windowMs){waitMs=Math.max(1,resetTime-now);}const retryAfter=Math.max(1,Math.ceil(waitMs/1000));await this.store.set(key,{windowStart:currWindowStart,prevCount,currCount},this.windowMs*2);return this.normalizeResult(false,this.limit,remaining,consumed,retryAfter,resetTime);});}async getState(key){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-counter',[key],[this.limit,this.windowMs,0,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const currWindowStart=Math.floor(now/this.windowMs)*this.windowMs;const resetTime=currWindowStart+this.windowMs;const state=await this.store.get(key);let prevCount=0;let currCount=0;if(state){if(state.windowStart===currWindowStart){prevCount=state.prevCount||0;currCount=state.currCount||0;}else if(currWindowStart===state.windowStart+this.windowMs){prevCount=state.currCount||0;currCount=0;}}const elapsed=now-currWindowStart;const weight=Math.max(0,1-elapsed/this.windowMs);const estimated=prevCount*weight+currCount;const consumed=Math.round(estimated);const remaining=Math.max(0,this.limit-consumed);const retryAfter=estimated>=this.limit?Math.max(1,Math.ceil((resetTime-now)/1000)):0;return this.normalizeResult(estimated<this.limit,this.limit,remaining,consumed,retryAfter,resetTime);}}export default SlidingWindowCounter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{BaseAlgorithm}from'./baseAlgorithm.js';export class SlidingWindowLog extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}async consume(key,cost=1){if(this.isRedis()){const uid=`${Date.now()}:${Math.random().toString(36).slice(2,8)}`;const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-log',[key],[this.limit,this.windowMs,cost,uid,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}return this.withLock(key,async()=>{const now=Date.now();const windowStart=now-this.windowMs;const state=await this.store.get(key);let log=[];if(state&&Array.isArray(state.log)){log=state.log.filter(entry=>entry.time>windowStart);}let current=0;for(let i=0;i<log.length;i++){current+=log[i].cost;}if(current+cost<=this.limit){log.push({time:now,cost});const consumed=current+cost;const remaining=this.limit-consumed;const oldestTime=log[0].time;const resetTime=oldestTime+this.windowMs;await this.store.set(key,{log},this.windowMs);return this.normalizeResult(true,this.limit,remaining,consumed,0,resetTime);}const deficit=current+cost-this.limit;let accumulated=0;let expireTime=now;for(let i=0;i<log.length;i++){accumulated+=log[i].cost;if(accumulated>=deficit){expireTime=log[i].time;break;}}const resetTime=expireTime+this.windowMs;const waitMs=Math.max(1,resetTime-now);const retryAfter=Math.max(1,Math.ceil(waitMs/1000));const remaining=Math.max(0,this.limit-current);await this.store.set(key,{log},this.windowMs);return this.normalizeResult(false,this.limit,remaining,current,retryAfter,resetTime);});}async getState(key){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-log',[key],[this.limit,this.windowMs,0,'inspect',nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const windowStart=now-this.windowMs;const state=await this.store.get(key);let log=[];if(state&&Array.isArray(state.log)){log=state.log.filter(entry=>entry.time>windowStart);}let current=0;for(let i=0;i<log.length;i++){current+=log[i].cost;}const remaining=Math.max(0,this.limit-current);const oldestTime=log.length>0?log[0].time:now;const resetTime=oldestTime+this.windowMs;const retryAfter=current>=this.limit?Math.max(1,Math.ceil((resetTime-now)/1000)):0;return this.normalizeResult(current<this.limit,this.limit,remaining,current,retryAfter,resetTime);}}export default SlidingWindowLog;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{BaseAlgorithm}from'./baseAlgorithm.js';export class TokenBucket extends BaseAlgorithm{constructor(options,store){super(options,store);this.capacity=options.capacity;this.refillRate=options.refillRate;this.refillInterval=options.refillInterval;this.initialTokens=options.initialTokens!==undefined?options.initialTokens:this.capacity;this.rate=this.refillRate/this.refillInterval;}async consume(key,cost=1){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('token-bucket',[key],[this.capacity,this.refillRate,this.refillInterval,cost,this.initialTokens,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}return this.withLock(key,async()=>{const now=Date.now();const state=await this.store.get(key);let tokens=this.initialTokens;let lastRefill=now;if(state&&typeof state.tokens==='number'){const elapsed=Math.max(0,now-state.lastRefill);tokens=Math.min(this.capacity,state.tokens+elapsed*this.rate);lastRefill=now;}const ttlMs=Math.max(1000,Math.ceil(this.capacity/this.rate*2.0));if(tokens>=cost){tokens-=cost;const consumed=this.capacity-Math.floor(tokens);const remaining=Math.floor(tokens);const resetTime=now+Math.ceil((this.capacity-tokens)/this.rate);await this.store.set(key,{tokens,lastRefill},ttlMs);return this.normalizeResult(true,this.capacity,remaining,consumed,0,resetTime);}const consumed=this.capacity-Math.floor(tokens);const remaining=Math.floor(tokens);const deficit=cost-tokens;const waitMs=Math.ceil(deficit/this.rate);const retryAfter=Math.max(1,Math.ceil(waitMs/1000));const resetTime=now+Math.ceil((this.capacity-tokens)/this.rate);await this.store.set(key,{tokens,lastRefill},ttlMs);return this.normalizeResult(false,this.capacity,remaining,consumed,retryAfter,resetTime);});}async getState(key){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('token-bucket',[key],[this.capacity,this.refillRate,this.refillInterval,0,this.initialTokens,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const state=await this.store.get(key);let tokens=this.initialTokens;if(state&&typeof state.tokens==='number'){const elapsed=Math.max(0,now-state.lastRefill);tokens=Math.min(this.capacity,state.tokens+elapsed*this.rate);}const consumed=this.capacity-Math.floor(tokens);const remaining=Math.floor(tokens);const resetTime=now+Math.ceil((this.capacity-tokens)/this.rate);const retryAfter=tokens<1?Math.max(1,Math.ceil((1-tokens)/this.rate/1000)):0;return this.normalizeResult(tokens>=1,this.capacity,remaining,consumed,retryAfter,resetTime);}}export default TokenBucket;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class BlockedKeys{constructor(){this._keys=new Map();}add(key,durationSec){const durationMs=Math.max(0,Number(durationSec)||0)*1000;this.addMs(key,durationMs);}_sweep(now=Date.now()){for(const[key,expiresAt]of this._keys.entries()){if(expiresAt<=now){this._keys.delete(key);}}}addMs(key,durationMs){const now=Date.now();if(this._keys.size>999){this._sweep(now);}const ms=Math.max(0,Number(durationMs)||0);this._keys.set(key,now+ms);}msBeforeExpire(key){const expiresAt=this._keys.get(key);if(expiresAt===undefined)return 0;const remaining=expiresAt-Date.now();if(remaining<=0){this._keys.delete(key);return 0;}return remaining;}isBlocked(key){return this.msBeforeExpire(key)>0;}delete(key){return this._keys.delete(key);}deleteAll(){this._keys.clear();}get size(){return this._keys.size;}}export default BlockedKeys;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defaultKeyGenerator,ipKeyGenerator,graphQLKeyGenerator}from'../utils/keyGenerator.js';import{evaluateSkipRule}from'../utils/skipUtils.js';import{RateLimiterRes}from'../core/rateLimiterRes.js';export function createCoreMiddleware(limiter,options={}){if(!limiter||typeof limiter.consume!=='function'){throw new TypeError('createCoreMiddleware requires a limiter with a consume() method');}let keyGen=defaultKeyGenerator;if(typeof options.keyGenerator==='function'){keyGen=options.keyGenerator;}else if(options.keyGenerator==='ip'){keyGen=ipKeyGenerator;}else if(options.keyGenerator==='graphql'){keyGen=graphQLKeyGenerator;}const rawSkip=options.skip!==undefined?options.skip:options.customSkip!==undefined?options.customSkip:options.customskip;const skipFn=typeof rawSkip==='function'?rawSkip:typeof rawSkip==='object'&&rawSkip!==null?(req,res)=>evaluateSkipRule(rawSkip,req,res,{generateKey:keyGen}):()=>false;const defaultHandler=(req,res,next,opts)=>{res.status(opts.statusCode);const msg=typeof opts.message==='function'?opts.message(req,res):opts.message;if(typeof msg==='object'){res.json(msg);}else{res.send(msg);}};const handler=options.handler||defaultHandler;const resolvedOptions={standardHeaders:options.standardHeaders!==undefined?options.standardHeaders:true,legacyHeaders:options.legacyHeaders!==undefined?options.legacyHeaders:false,...options,statusCode:options.statusCode!==undefined?options.statusCode:429,message:options.message!==undefined?options.message:'Too many requests, please try again later.'};const middleware=async(req,res,next)=>{try{const shouldSkip=await Promise.resolve(skipFn(req,res));if(shouldSkip){return next();}const key=await Promise.resolve(keyGen(req,res));let cost=1;if(typeof options.cost==='function'){cost=await Promise.resolve(options.cost(req,res));}else if(typeof options.cost==='number'){cost=options.cost;}if(!Number.isFinite(cost)||cost<=0){cost=1;}let res1;try{res1=await limiter.consume(key,cost,{req,res});}catch(err){if(err instanceof RateLimiterRes){res1=err;}else if(err&&typeof err==='object'&&Object.values(err).some(v=>v instanceof RateLimiterRes)){const resValues=Object.values(err).filter(v=>v instanceof RateLimiterRes);const maxMsBeforeNext=Math.max(...resValues.map(v=>v.msBeforeNext||0));const minRemaining=Math.min(...resValues.map(v=>v.remainingPoints||0));const maxConsumed=Math.max(...resValues.map(v=>v.consumedPoints||0));res1=new RateLimiterRes(minRemaining,maxMsBeforeNext,maxConsumed,false);res1._unionRejections=err;}else{throw err;}}if(res1&&!(res1 instanceof RateLimiterRes)&&typeof res1==='object'&&Object.values(res1).every(v=>v instanceof RateLimiterRes)){const resValues=Object.values(res1);const maxMsBeforeNext=Math.max(...resValues.map(v=>v.msBeforeNext||0));const minRemaining=Math.min(...resValues.map(v=>v.remainingPoints||0));const maxConsumed=Math.max(...resValues.map(v=>v.consumedPoints||0));res1=new RateLimiterRes(minRemaining,maxMsBeforeNext,maxConsumed,resValues.some(v=>v.isFirstInDuration));}const result=toMiddlewareResult(res1,limiter,cost);req.rateLimit=result;setCoreHeaders(res,result,{...resolvedOptions,limiter});if(result.allowed){return next();}return handler(req,res,next,resolvedOptions);}catch(err){if(options.failOpen){req.rateLimit={allowed:true,limit:limiter.points||60,remaining:1,consumed:0,retryAfter:0,resetTime:new Date(Date.now()+(limiter.msDuration||60000)),failedOpen:true};if(typeof options.onError==='function'){options.onError(err,req,res,next);}return next();}if(typeof options.onError==='function'){return options.onError(err,req,res,next);}return next(err);}};middleware.resetKey=async keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;if(typeof limiter.delete==='function'){return limiter.delete(k);}return false;};middleware.getState=async keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;if(typeof limiter.get==='function'){return limiter.get(k);}return null;};middleware.limiter=limiter;return middleware;}function toMiddlewareResult(res,limiter,cost){const limit=typeof limiter.points==='number'?limiter.points:typeof res.consumedPoints==='number'&&typeof res.remainingPoints==='number'?res.consumedPoints+res.remainingPoints:60;const consumed=typeof res.consumedPoints==='number'?res.consumedPoints:cost;const msBeforeNext=typeof res.msBeforeNext==='number'?res.msBeforeNext:0;const rawReset=Number.isFinite(msBeforeNext)&&msBeforeNext>0?Date.now()+Math.min(msBeforeNext,31536000000):Date.now();const resetTime=new Date(rawReset);return{allowed:consumed<=limit,limit,remaining:Math.max(0,limit-consumed),consumed,retryAfter:msBeforeNext>0?Math.max(1,Math.ceil(msBeforeNext/1000)):0,resetTime:Number.isNaN(resetTime.getTime())?new Date(Date.now()+3600000):resetTime,cost};}function setCoreHeaders(res,result,options={}){if(!res||typeof res.setHeader!=='function'||res.headersSent){return;}const rawDiff=result.resetTime?(result.resetTime.getTime()-Date.now())/1000:0;const resetSeconds=Number.isFinite(rawDiff)?Math.max(0,Math.ceil(rawDiff)):0;if(options.standardHeaders){res.setHeader('RateLimit-Limit',String(result.limit));res.setHeader('RateLimit-Remaining',String(result.remaining));res.setHeader('RateLimit-Reset',String(resetSeconds));const policyWindowMs=options.windowMs||options.limiter&&(options.limiter.msDuration||(options.limiter.duration?options.limiter.duration*1000:null))||60000;if(options.standardHeaders!=='draft-6'){const windowSec=Math.ceil(policyWindowMs/1000);res.setHeader('RateLimit-Policy',`${result.limit};w=${windowSec}`);}}if(options.legacyHeaders){res.setHeader('X-RateLimit-Limit',String(result.limit));res.setHeader('X-RateLimit-Remaining',String(result.remaining));res.setHeader('X-RateLimit-Reset',String(resetSeconds));}if(!result.allowed){const retryAfter=Math.max(1,Number(result.retryAfter)||resetSeconds||1);res.setHeader('Retry-After',String(retryAfter));}}export default createCoreMiddleware;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RedisLimiter}from'./redisLimiter.js';import{RedisConnectionError}from'../../utils/errors.js';export class ClusterLimiter extends RedisLimiter{constructor(opts={}){const client=opts.storeClient||opts.client||opts.redis||null;if(!client){throw new RedisConnectionError('Redis cluster client is not configured. Pass it via the "storeClient" option.');}super({...opts,isCluster:opts.isCluster!==undefined?opts.isCluster:true,hashTag:opts.hashTag!==undefined?Boolean(opts.hashTag):true});}}export const RedisClusterLimiter=ClusterLimiter;export const RateLimiterRedisCluster=ClusterLimiter;export default ClusterLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{ConfigurationError}from'../../utils/errors.js';const DEFAULT_TABLE_NAME='rate-limiter';const AWS_DYNAMODB_MODULE='@aws-sdk/client-dynamodb';const COMMAND_NAMES={UpdateItem:'UpdateItemCommand',GetItem:'GetItemCommand',DeleteItem:'DeleteItemCommand'};const DIRECT_METHODS={UpdateItem:'updateItem',GetItem:'getItem',DeleteItem:'deleteItem'};export class DynamoDbLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.client=opts.storeClient||opts.client||opts.dynamoDb||null;if(!this.client){throw new ConfigurationError('DynamoDB client is not configured. Pass it via the "storeClient" option.');}this.tableName=opts.tableName!==undefined?opts.tableName:DEFAULT_TABLE_NAME;this._commands=opts.commands||null;this._initPromise=this._init();}async _init(){if(this._commands||typeof this.client.send!=='function'){return;}try{const sdk=await import(AWS_DYNAMODB_MODULE);if(sdk&&sdk.UpdateItemCommand){this._commands={UpdateItemCommand:sdk.UpdateItemCommand,GetItemCommand:sdk.GetItemCommand,DeleteItemCommand:sdk.DeleteItemCommand};}}catch{}}async _send(operation,input){await this._initPromise;const client=this.client;if(typeof client.send==='function'){const CommandClass=this._commands?this._commands[COMMAND_NAMES[operation]]:null;if(CommandClass){return client.send(new CommandClass(input));}return client.send({name:COMMAND_NAMES[operation],input});}const methodName=DIRECT_METHODS[operation];if(typeof client[methodName]==='function'){const res=await client[methodName](input);return res&&typeof res.promise==='function'?res.promise():res;}throw new ConfigurationError('DynamoDB client does not support "send" or the DynamoDB item methods.');}_toResult(item){if(!item){return{consumedPoints:0,expireAt:null};}const points=item.points;const expire=item.expire;return{consumedPoints:points===null||points===undefined?0:Number(points),expireAt:expire===null||expire===undefined?null:Number(expire)};}async _upsert(rlKey,points,msDuration,forceExpire=false,_options={}){const expireAt=msDuration>0?Date.now()+msDuration:null;if(forceExpire){const res=await this._send('UpdateItem',{TableName:this.tableName,Key:{key:rlKey},UpdateExpression:'SET #points = :points, #expire = :expire',ExpressionAttributeNames:{'#points':'points','#expire':'expire'},ExpressionAttributeValues:{':points':points,':expire':expireAt},ReturnValues:'ALL_NEW'});return this._toResult(res&&res.Attributes?res.Attributes:null);}if(points<0){return this._decrement(rlKey,points,expireAt);}const now=Date.now();try{const res=await this._send('UpdateItem',{TableName:this.tableName,Key:{key:rlKey},UpdateExpression:'SET #expire = if_not_exists(#expire, :expire) ADD #points :inc',ConditionExpression:'attribute_not_exists(#expire) OR #expire > :now',ExpressionAttributeNames:{'#points':'points','#expire':'expire'},ExpressionAttributeValues:{':expire':expireAt,':inc':points,':now':now},ReturnValues:'ALL_NEW'});return this._toResult(res&&res.Attributes?res.Attributes:null);}catch(err){if(err&&(err.name==='ConditionalCheckFailedException'||err.code==='ConditionalCheckFailedException'||typeof err.message==='string'&&err.message.includes('ConditionalCheckFailed'))){return this._upsert(rlKey,points,msDuration,true,_options);}throw err;}}async _decrement(rlKey,points,expireAt){const item=await this._getItem(rlKey);const currentPoints=item&&item.points!==null&&item.points!==undefined?Number(item.points):0;const res=await this._send('UpdateItem',{TableName:this.tableName,Key:{key:rlKey},UpdateExpression:'SET #points = :points, #expire = if_not_exists(#expire, :expire)',ExpressionAttributeNames:{'#points':'points','#expire':'expire'},ExpressionAttributeValues:{':points':Math.max(0,currentPoints+points),':expire':expireAt},ReturnValues:'ALL_NEW'});return this._toResult(res&&res.Attributes?res.Attributes:null);}async _getItem(rlKey){const res=await this._send('GetItem',{TableName:this.tableName,Key:{key:rlKey},ConsistentRead:true});return res&&res.Item?res.Item:null;}async _get(rlKey,_options={}){const item=await this._getItem(rlKey);if(!item){return null;}return this._toResult(item);}async _delete(rlKey,_options={}){const res=await this._send('DeleteItem',{TableName:this.tableName,Key:{key:rlKey},ReturnValues:'ALL_OLD'});return Boolean(res&&res.Attributes);}}export default DynamoDbLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';export class EtcdLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.client=opts.storeClient||opts.client||opts.etcd||null;if(!this.client){throw new Error('etcd client is not configured. Pass it via the "storeClient" option.');}this.maxRetries=opts.maxRetries!==undefined?opts.maxRetries:10;}async _call(method,args){const fn=this.client[method];if(typeof fn!=='function'){throw new Error(`etcd client does not support "${method}"`);}const result=fn.apply(this.client,args);if(result&&typeof result.then==='function'){return result;}if(result&&typeof result.string==='function'){return result.string();}return result;}async _read(rlKey){const raw=await this._call('get',[rlKey]);if(raw===null||raw===undefined||raw===false)return null;let kv=raw;if(kv&&typeof kv==='object'&&'kv'in kv&&kv.kv){kv=kv.kv;}let text=null;if(typeof kv==='string'){text=kv;}else if(kv&&typeof kv==='object'){text=kv.value!==undefined?kv.value:null;if(text&&typeof text!=='string'){text=String(text);}}if(!text)return null;let parsed;try{parsed=JSON.parse(text);}catch{return null;}if(!parsed||typeof parsed.points!=='number')return null;const revision=kv&&typeof kv==='object'&&kv.mod_revision!==undefined?String(kv.mod_revision):null;return{points:parsed.points,expireAt:typeof parsed.expireAt==='number'?parsed.expireAt:null,revision};}async _write(rlKey,value,msDuration){if(!(msDuration>0)){await this._call('put',[rlKey,value]);return;}const leaseId=await this._grantLease(Math.ceil(msDuration/1000));if(leaseId===null||leaseId===undefined){await this._call('put',[rlKey,value]);return;}await this._call('put',[rlKey,value,leaseId]);}async _grantLease(ttlSeconds){const leaseOwner=this.client.lease||this.client;const grant=leaseOwner.grant;if(typeof grant!=='function'){return null;}const lease=await grant.call(leaseOwner,ttlSeconds);if(lease===null||lease===undefined)return null;if(typeof lease==='number'||typeof lease==='string')return lease;if(lease.ID!==undefined)return lease.ID;if(lease.lease!==undefined)return lease.lease;return null;}async _upsert(rlKey,points,msDuration,forceExpire=false){const now=Date.now();const expireAt=msDuration>0?now+msDuration:null;for(let attempt=0;attempt<this.maxRetries;attempt++){const current=await this._read(rlKey);const isLive=current&&(current.expireAt===null||current.expireAt>now);let nextPoints;let nextExpireAt;if(forceExpire){nextPoints=points;nextExpireAt=expireAt;}else if(isLive){nextPoints=current.points+points;nextExpireAt=current.expireAt;}else{nextPoints=points;nextExpireAt=expireAt;}const committed=await this._compareAndSwap(rlKey,current,nextPoints,nextExpireAt,msDuration);if(committed){return{consumedPoints:nextPoints,expireAt:nextExpireAt};}}throw new Error('etcd: could not update the rate limit counter after several attempts');}async _compareAndSwap(rlKey,current,nextPoints,nextExpireAt,msDuration){const value=JSON.stringify({points:nextPoints,expireAt:nextExpireAt});if(typeof this.client.txn!=='function'){await this._write(rlKey,value,msDuration);return true;}const ttlMs=nextExpireAt!==null?Math.max(0,nextExpireAt-Date.now()):msDuration;const leaseId=ttlMs>0?await this._grantLease(Math.ceil(ttlMs/1000)):null;const txn=this.client.txn();if(!current||current.revision===null||current.revision===undefined){txn.if(this.client.modRevision(rlKey).isNaN());}else{txn.if(this.client.modRevision(rlKey).equals(current.revision));}const response=await txn.then(op=>{let putOp=op.put(rlKey,value);if(leaseId!==null&&leaseId!==undefined){if(typeof putOp.lease==='function'){putOp=putOp.lease(leaseId);}else if(typeof putOp.withLease==='function'){putOp=putOp.withLease(leaseId);}}return putOp;}).commit().catch(()=>null);if(response===null||response===undefined){return false;}if(typeof response.succeeded==='function'){return response.succeeded();}if(typeof response.succeeded==='boolean'){return response.succeeded;}return true;}async _get(rlKey){const current=await this._read(rlKey);if(!current)return null;if(current.expireAt!==null&¤t.expireAt<=Date.now()){return null;}return{consumedPoints:current.points,expireAt:current.expireAt};}async _delete(rlKey){const current=await this._read(rlKey);await this._call('delete',[rlKey]).catch(()=>false);return Boolean(current);}}export default EtcdLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';export class MemcachedLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.client=opts.storeClient||opts.client||opts.memcached||null;if(!this.client){throw new Error('Memcached client is not configured. Pass it via the "storeClient" option.');}this._expiries=new Map();}_setExpiry(rlKey,expiresAt){if(this._expiries.size>999){const now=Date.now();for(const[key,exp]of this._expiries.entries()){if(exp<=now){this._expiries.delete(key);}}}this._expiries.set(rlKey,expiresAt);}_call(method,args){const fn=this.client[method];if(typeof fn!=='function'){return Promise.reject(new Error(`Memcached client does not support "${method}"`));}const result=fn.apply(this.client,args);if(result&&typeof result.then==='function'){return result;}return new Promise((resolve,reject)=>{fn.call(this.client,...args,(err,value)=>{if(err){reject(err);return;}resolve(value);});});}_toTtlSeconds(msDuration){if(!(msDuration>0))return 0;const seconds=Math.ceil(msDuration/1000);const THIRTY_DAYS=60*60*24*30;if(seconds<=THIRTY_DAYS){return seconds;}return Math.floor(Date.now()/1000)+seconds;}async _upsert(rlKey,points,msDuration,forceExpire=false){const now=Date.now();const ttl=this._toTtlSeconds(msDuration);if(forceExpire){await this._call('set',[rlKey,String(points),ttl]);if(msDuration>0){this._setExpiry(rlKey,now+msDuration);}else{this._expiries.delete(rlKey);}return{consumedPoints:points,expireAt:msDuration>0?now+msDuration:null};}if(points===0){const existing=await this._read(rlKey);if(existing)return existing;}if(points>0){const added=await this._call('add',[rlKey,'0',ttl]).catch(()=>false);if(added){if(msDuration>0){this._setExpiry(rlKey,now+msDuration);}const consumed=await this._call('incr',[rlKey,points]);const value=typeof consumed==='number'?consumed:points;return{consumedPoints:value,expireAt:this._expireAt(rlKey)};}}else{const existing=await this._read(rlKey);if(!existing){const added=await this._call('add',[rlKey,'0',ttl]).catch(()=>false);if(added&&msDuration>0){this._setExpiry(rlKey,now+msDuration);}return{consumedPoints:0,expireAt:this._expireAt(rlKey)};}const decremented=await this._call('decr',[rlKey,Math.abs(points)]);const value=typeof decremented==='number'?Math.max(0,decremented):0;return{consumedPoints:value,expireAt:this._expireAt(rlKey)};}const consumed=await this._call('incr',[rlKey,points]);const value=typeof consumed==='number'?consumed:points;return{consumedPoints:value,expireAt:this._expireAt(rlKey)};}_expireAt(rlKey){const expireAt=this._expiries.get(rlKey);if(expireAt===undefined)return null;return expireAt;}async _read(rlKey){const raw=await this._call('get',[rlKey]);if(raw===null||raw===undefined||raw===false)return null;const consumedPoints=Number(raw);if(!Number.isFinite(consumedPoints))return null;const expireAt=this._expireAt(rlKey);if(expireAt!==null&&expireAt<=Date.now()){await this._call('del',[rlKey]);this._expiries.delete(rlKey);return null;}return{consumedPoints,expireAt};}async _get(rlKey){return this._read(rlKey);}async _delete(rlKey){this._expiries.delete(rlKey);const existed=await this._read(rlKey);const deleted=await this._call('del',[rlKey]).catch(()=>false);return Boolean(existed||deleted);}clear(){this._expiries.clear();}}export default MemcachedLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';export class MemoryLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this._storage=new Map();this._sweepIntervalMs=opts.sweepIntervalMs!==undefined?opts.sweepIntervalMs:0;this._maxEntries=opts.maxEntries!==undefined?opts.maxEntries:0;if(this._sweepIntervalMs>0){this._sweepTimer=setInterval(()=>this._sweep(),this._sweepIntervalMs);if(this._sweepTimer.unref){this._sweepTimer.unref();}}}_sweep(){const now=Date.now();for(const[key,record]of this._storage){if(record.expireAt!==null&&record.expireAt<=now){this._storage.delete(key);}}}async _upsert(rlKey,points,msDuration,forceExpire=false){const now=Date.now();const existing=this._storage.get(rlKey);if(!forceExpire&&existing&&(existing.expireAt===null||existing.expireAt>now)){existing.consumedPoints+=points;return existing;}const record={consumedPoints:points,expireAt:msDuration>0?now+msDuration:null};if(this._maxEntries>0&&this._storage.size>=this._maxEntries){this._evictOldest();}this._storage.set(rlKey,record);return record;}_evictOldest(){const now=Date.now();for(const[key,record]of this._storage){if(record.expireAt!==null&&record.expireAt<=now){this._storage.delete(key);}}if(this._storage.size<this._maxEntries){return;}for(const key of this._storage.keys()){this._storage.delete(key);break;}}async _get(rlKey){const record=this._storage.get(rlKey);if(!record)return null;if(record.expireAt!==null&&record.expireAt<=Date.now()){this._storage.delete(rlKey);return null;}return record;}async _delete(rlKey){return this._storage.delete(rlKey);}async clear(){this._storage.clear();}dump(){const now=Date.now();const storage=[];for(const[key,record]of this._storage){if(record.expireAt===null||record.expireAt>now){storage.push({key,value:record.consumedPoints,expiresAt:record.expireAt});}}return{version:1,dumpedAt:now,storage};}restore(dumpData,detailResponse=false){const result={invalid:0,expired:0,restored:0};const detailed={invalid:[],expired:[],restored:[]};if(!dumpData||!Array.isArray(dumpData.storage)){return detailResponse?detailed:result;}const now=Date.now();for(const item of dumpData.storage){if(!item||typeof item.key!=='string'||typeof item.value!=='number'||!Number.isFinite(item.value)||item.expiresAt!==null&&item.expiresAt!==undefined&&(typeof item.expiresAt!=='number'||!Number.isFinite(item.expiresAt))){result.invalid++;detailed.invalid.push(item?.key??String(item));continue;}if(item.expiresAt!==null&&item.expiresAt!==undefined&&item.expiresAt<=now){result.expired++;detailed.expired.push(item.key);continue;}this._storage.set(item.key,{consumedPoints:item.value,expireAt:item.expiresAt??null});result.restored++;detailed.restored.push(item.key);}return detailResponse?detailed:result;}async close(){if(this._sweepTimer){clearInterval(this._sweepTimer);this._sweepTimer=null;}this._storage.clear();}}export default MemoryLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{ConfigurationError}from'../../utils/errors.js';const DEFAULT_DB_NAME='express-adaptive-rate-limiter';const DEFAULT_TABLE_NAME='rate-limiter';const MAX_DUPLICATE_KEY_RETRIES=3;export class MongoLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.client=opts.storeClient||opts.mongo||opts.client||null;if(!this.client){throw new ConfigurationError('Mongo client is not configured. Pass it via the "storeClient" option.');}this.dbName=opts.dbName!==undefined?opts.dbName:DEFAULT_DB_NAME;this.tableName=opts.tableName!==undefined?opts.tableName:DEFAULT_TABLE_NAME;this.disableIndexesCreation=Boolean(opts.disableIndexesCreation);this.clearExpiredByTimeout=opts.clearExpiredByTimeout!==undefined?Boolean(opts.clearExpiredByTimeout):false;this.clearExpiredTimeoutMs=opts.clearExpiredTimeoutMs!==undefined?Number(opts.clearExpiredTimeoutMs):300000;this._clearExpiredTimer=null;if(this.clearExpiredByTimeout&&this.clearExpiredTimeoutMs>0){this._clearExpiredTimer=setInterval(()=>{this.clearExpired().catch(()=>{});},this.clearExpiredTimeoutMs);if(this._clearExpiredTimer.unref){this._clearExpiredTimer.unref();}}this._collection=this._resolveCollection();this._indexesPromise=this._createIndexes();}_resolveCollection(){const db=typeof this.client.db==='function'?this.client.db(this.dbName):this.client;if(!db||typeof db.collection!=='function'){throw new ConfigurationError('Mongo client must expose "db(name)" or be an already resolved database instance.');}return db.collection(this.tableName);}async _createIndexes(){if(this.disableIndexesCreation){return;}try{await this._collection.createIndex({expire:1},{expireAfterSeconds:0});await this._collection.createIndex({key:1},{unique:true});}catch{}}_isDuplicateKeyError(err){return Boolean(err&&(err.code===11000||err.code===11001));}_extractDocument(res){if(!res){return null;}if('value'in res){return res.value;}if(Array.isArray(res.ops)&&res.ops.length>0){return res.ops[0];}return res;}_toResult(doc){if(!doc){return{consumedPoints:0,expireAt:null};}const expire=doc.expire;return{consumedPoints:Number(doc.points)||0,expireAt:expire===null||expire===undefined?null:new Date(expire).getTime()};}async _upsert(rlKey,points,msDuration,forceExpire=false,_options={}){await this._indexesPromise;if(forceExpire){const res=await this._collection.findOneAndUpdate({key:rlKey},{$set:{key:rlKey,points,expire:this._expireDate(msDuration)}},{upsert:true,returnDocument:'after'});return this._toResult(this._extractDocument(res));}return this._increment(rlKey,points,msDuration,0);}async _increment(rlKey,points,msDuration,attempt){const now=Date.now();try{const res=await this._collection.findOneAndUpdate({key:rlKey,$or:[{expire:{$gt:new Date(now)}},{expire:{$eq:null}}]},{$setOnInsert:{key:rlKey,expire:this._expireDate(msDuration,now)},$inc:{points}},{upsert:true,returnDocument:'after'});return this._toResult(this._extractDocument(res));}catch(err){if(!this._isDuplicateKeyError(err)||attempt>=MAX_DUPLICATE_KEY_RETRIES){throw err;}return this._replaceExpired(rlKey,points,msDuration,attempt+1);}}async _replaceExpired(rlKey,points,msDuration,attempt){try{const res=await this._collection.findOneAndUpdate({key:rlKey,$or:[{expire:{$lte:new Date()}},{expire:{$eq:null}}]},{$set:{key:rlKey,points,expire:this._expireDate(msDuration)}},{upsert:true,returnDocument:'after'});return this._toResult(this._extractDocument(res));}catch(err){if(!this._isDuplicateKeyError(err)||attempt>=MAX_DUPLICATE_KEY_RETRIES){throw err;}return this._increment(rlKey,points,msDuration,attempt);}}_expireDate(msDuration,now=Date.now()){return msDuration>0?new Date(now+msDuration):null;}async _get(rlKey,_options={}){await this._indexesPromise;const doc=await this._collection.findOne({key:rlKey,$or:[{expire:{$gt:new Date()}},{expire:{$eq:null}}]});if(!doc){return null;}return this._toResult(doc);}async _delete(rlKey,_options={}){await this._indexesPromise;const res=await this._collection.deleteOne({key:rlKey});return this._extractDeletedCount(res)>0;}_extractDeletedCount(res){if(!res){return 0;}if(typeof res.deletedCount==='number'){return res.deletedCount;}if(res.result&&typeof res.result.n==='number'){return res.result.n;}return 0;}async clearExpired(){await this._indexesPromise;await this._collection.deleteMany({expire:{$lte:new Date(),$ne:null}});}async close(){if(this._clearExpiredTimer){clearInterval(this._clearExpiredTimer);this._clearExpiredTimer=null;}}}export default MongoLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{ConfigurationError}from'../../utils/errors.js';const DEFAULT_MODEL_NAME='RateLimit';const MAX_DUPLICATE_KEY_RETRIES=3;export class MongooseLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.modelName=opts.modelName!==undefined?opts.modelName:DEFAULT_MODEL_NAME;this.disableIndexesCreation=Boolean(opts.disableIndexesCreation);this.model=opts.model||this._resolveModel(opts);if(!this.model){throw new ConfigurationError('Mongoose model is not configured. Pass it via the "model" or the "storeClient" option.');}this._indexesPromise=this._createIndexes();}_resolveModel(opts){const mongoose=opts.storeClient||opts.mongoose||opts.client||null;if(!mongoose){return null;}const connection=mongoose.connection||mongoose;if(!connection||typeof connection.model!=='function'){return null;}const registry=connection.models||mongoose.models||{};if(registry[this.modelName]){return registry[this.modelName];}try{if(opts.schema){return connection.model(this.modelName,opts.schema);}return connection.model(this.modelName);}catch(err){throw new ConfigurationError(`Unable to resolve the Mongoose model "${this.modelName}": ${err.message}`);}}async _createIndexes(){if(this.disableIndexesCreation){return;}const schema=this.model.schema;if(!schema||typeof schema.index!=='function'){return;}try{schema.index({expire:1},{expireAfterSeconds:0});if(typeof this.model.createIndexes==='function'){await this.model.createIndexes();}else if(typeof this.model.ensureIndexes==='function'){await this.model.ensureIndexes();}}catch{}}_isDuplicateKeyError(err){return Boolean(err&&(err.code===11000||err.code===11001));}_toResult(doc){if(!doc){return{consumedPoints:0,expireAt:null};}const expire=doc.expire;return{consumedPoints:Number(doc.points)||0,expireAt:expire===null||expire===undefined?null:new Date(expire).getTime()};}async _upsert(rlKey,points,msDuration,forceExpire=false,_options={}){await this._indexesPromise;if(forceExpire){const doc=await this.model.findOneAndUpdate({key:rlKey},{$set:{key:rlKey,points,expire:this._expireDate(msDuration)}},{upsert:true,new:true});return this._toResult(doc);}return this._increment(rlKey,points,msDuration,0);}async _increment(rlKey,points,msDuration,attempt){const now=Date.now();try{const doc=await this.model.findOneAndUpdate({key:rlKey,$or:[{expire:{$gt:new Date(now)}},{expire:{$eq:null}}]},{$setOnInsert:{key:rlKey,expire:this._expireDate(msDuration,now)},$inc:{points}},{upsert:true,new:true});return this._toResult(doc);}catch(err){if(!this._isDuplicateKeyError(err)||attempt>=MAX_DUPLICATE_KEY_RETRIES){throw err;}return this._replaceExpired(rlKey,points,msDuration,attempt+1);}}async _replaceExpired(rlKey,points,msDuration,attempt){try{const doc=await this.model.findOneAndUpdate({key:rlKey,$or:[{expire:{$lte:new Date()}},{expire:{$eq:null}}]},{$set:{key:rlKey,points,expire:this._expireDate(msDuration)}},{upsert:true,new:true});return this._toResult(doc);}catch(err){if(!this._isDuplicateKeyError(err)||attempt>=MAX_DUPLICATE_KEY_RETRIES){throw err;}return this._increment(rlKey,points,msDuration,attempt);}}_expireDate(msDuration,now=Date.now()){return msDuration>0?new Date(now+msDuration):null;}async _get(rlKey,_options={}){await this._indexesPromise;const doc=await this.model.findOne({key:rlKey,$or:[{expire:{$gt:new Date()}},{expire:{$eq:null}}]});if(!doc){return null;}return this._toResult(doc);}async _delete(rlKey,_options={}){await this._indexesPromise;const res=await this.model.deleteOne({key:rlKey});return this._extractDeletedCount(res)>0;}_extractDeletedCount(res){if(!res){return 0;}if(typeof res.deletedCount==='number'){return res.deletedCount;}if(res.result&&typeof res.result.n==='number'){return res.result.n;}return 0;}}export default MongooseLimiter;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{ConfigurationError}from'../../utils/errors.js';const DEFAULT_TABLE_NAME='rate_limiter';const TABLE_NAME_PATTERN=/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/;export class MySqlLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.storeClient=opts.storeClient||opts.client||null;if(!this.storeClient){throw new ConfigurationError('MySQL client is not configured. Pass it via the "storeClient" option.');}this.tableName=opts.tableName!==undefined?opts.tableName:DEFAULT_TABLE_NAME;if(typeof this.tableName!=='string'||!TABLE_NAME_PATTERN.test(this.tableName)){throw new ConfigurationError('tableName must be a plain SQL identifier, optionally prefixed with a schema name');}this.tableCreated=opts.tableCreated!==undefined?Boolean(opts.tableCreated):false;this.clearExpiredByTimeout=opts.clearExpiredByTimeout!==undefined?Boolean(opts.clearExpiredByTimeout):false;this.clearExpiredTimeoutMs=opts.clearExpiredTimeoutMs!==undefined?Number(opts.clearExpiredTimeoutMs):300000;this._clearExpiredTimer=null;if(this.clearExpiredByTimeout&&this.clearExpiredTimeoutMs>0){this._clearExpiredTimer=setInterval(()=>{this.clearExpired().catch(()=>{});},this.clearExpiredTimeoutMs);if(this._clearExpiredTimer.unref){this._clearExpiredTimer.unref();}}if(!this.tableCreated){this.createTable().catch(()=>undefined);}}_quotedTable(){return this.tableName.split('.').map(part=>`\`${part}\``).join('.');}_createTableSql(){return`CREATE TABLE IF NOT EXISTS ${this._quotedTable()} (
|
|
2
|
+
\`key\` VARCHAR(255) NOT NULL,
|
|
3
|
+
\`points\` BIGINT NOT NULL DEFAULT 0,
|
|
4
|
+
\`expire\` BIGINT DEFAULT NULL,
|
|
5
|
+
PRIMARY KEY (\`key\`)
|
|
6
|
+
)`;}_incrementSql(){return`INSERT INTO ${this._quotedTable()} (\`key\`, \`points\`, \`expire\`) VALUES (?, ?, ?)
|
|
7
|
+
ON DUPLICATE KEY UPDATE
|
|
8
|
+
\`points\` = IF(\`expire\` IS NULL OR \`expire\` > ?, \`points\` + VALUES(points), VALUES(points)),
|
|
9
|
+
\`expire\` = IF(\`expire\` IS NULL OR \`expire\` > ?, \`expire\`, VALUES(expire))`;}_forceExpireSql(){return`INSERT INTO ${this._quotedTable()} (\`key\`, \`points\`, \`expire\`) VALUES (?, ?, ?)
|
|
10
|
+
ON DUPLICATE KEY UPDATE
|
|
11
|
+
\`points\` = VALUES(points),
|
|
12
|
+
\`expire\` = VALUES(expire)`;}_selectSql(){return`SELECT \`points\`, \`expire\` FROM ${this._quotedTable()} WHERE \`key\` = ? AND (\`expire\` IS NULL OR \`expire\` > ?)`;}_selectRowSql(){return`SELECT \`points\`, \`expire\` FROM ${this._quotedTable()} WHERE \`key\` = ?`;}async _query(sql,params=[]){const client=this.storeClient;if(typeof client.query==='function'){return this._unwrap(await client.query(sql,params));}if(typeof client.execute==='function'){return this._unwrap(await client.execute(sql,params));}throw new ConfigurationError('MySQL client must expose a query() or execute() method');}_unwrap(result){if(Array.isArray(result)&&result.length===2&&(result[1]===undefined||result[1]===null||Array.isArray(result[1]))){return result[0];}return result;}async _queryRow(sql,params=[]){const result=await this._query(sql,params);if(Array.isArray(result)&&result.length>0){return result[0];}return null;}_toResult(row){if(!row){return{consumedPoints:0,expireAt:null};}const expireAt=row.expire===null||row.expire===undefined?null:Number(row.expire);return{consumedPoints:Number(row.points),expireAt:Number.isFinite(expireAt)?expireAt:null};}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}const now=Date.now();const expire=msDuration>0?now+msDuration:null;if(forceExpire){await this._query(this._forceExpireSql(),[rlKey,points,expire]);return{consumedPoints:points,expireAt:expire};}await this._query(this._incrementSql(),[rlKey,points,expire,now,now]);const row=await this._queryRow(this._selectRowSql(),[rlKey]);return this._toResult(row);}async _get(rlKey){const row=await this._queryRow(this._selectSql(),[rlKey,Date.now()]);if(!row){return null;}return this._toResult(row);}async _delete(rlKey){const result=await this._query(`DELETE FROM ${this._quotedTable()} WHERE \`key\` = ?`,[rlKey]);const affectedRows=result&&typeof result.affectedRows==='number'?result.affectedRows:0;return affectedRows>0;}createTable(){try{return Promise.resolve(this._query(this._createTableSql(),[]));}catch(err){return Promise.reject(err);}}async clear(){await this._query(`DELETE FROM ${this._quotedTable()}`,[]);}async clearExpired(){const now=Date.now();await this._query(`DELETE FROM ${this._quotedTable()} WHERE \`expire\` IS NOT NULL AND \`expire\` <= ?`,[now]);}async close(){if(this._clearExpiredTimer){clearInterval(this._clearExpiredTimer);this._clearExpiredTimer=null;}}}export default MySqlLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import cluster from'node:cluster';import{RateLimiter,isRateLimiterCompatible}from'../rateLimiter.js';import{RateLimiterRes}from'../rateLimiterRes.js';import{MemoryLimiter}from'./memoryLimiter.js';const IPC_CHANNEL='rate_limiter_flexible';const ALLOWED_IPC_METHODS=new Set(['consume','penalty','reward','block','set','get','delete']);export class RateLimiterClusterMaster{constructor(targetCluster=cluster){if(RateLimiterClusterMaster.instance&&(!targetCluster||RateLimiterClusterMaster.instance.cluster===targetCluster)){return RateLimiterClusterMaster.instance;}if(RateLimiterClusterMaster.instance){RateLimiterClusterMaster.instance.close();}RateLimiterClusterMaster.instance=this;this.cluster=targetCluster;this._limiters=new Map();this._onMessage=(worker,message)=>{try{if(!message||typeof message!=='object'||message.channel!==IPC_CHANNEL){return;}this._handleMessage(worker,message).catch(()=>{});}catch{}};if(this.cluster&&typeof this.cluster.on==='function'){this.cluster.on('message',this._onMessage);}}_getLimiter(limiterOptions={}){const rawPrefix=limiterOptions.keyPrefix||'rl:';const normalizedPrefix=rawPrefix.endsWith(':')?rawPrefix:`${rawPrefix}:`;let limiter=this._limiters.get(normalizedPrefix);if(!limiter){limiter=new MemoryLimiter({...limiterOptions,keyPrefix:normalizedPrefix});this._limiters.set(normalizedPrefix,limiter);}return limiter;}async _handleMessage(worker,message){try{if(!message||typeof message!=='object'){return;}const{id,method,args,limiterOptions}=message;if(!id){return;}if(typeof method!=='string'||!ALLOWED_IPC_METHODS.has(method)){if(worker&&typeof worker.send==='function'){try{worker.send({channel:IPC_CHANNEL,id,resolved:false,data:{message:`Method "${method}" is not allowed`}});}catch{}}return;}const limiter=this._getLimiter(limiterOptions);if(typeof limiter[method]!=='function'){if(worker&&typeof worker.send==='function'){try{worker.send({channel:IPC_CHANNEL,id,resolved:false,data:{message:`Method "${method}" is not supported`}});}catch{}}return;}const safeArgs=Array.isArray(args)?args:[];let res;try{res=await limiter[method](...safeArgs);}catch(err){if(worker&&typeof worker.send==='function'){try{worker.send({channel:IPC_CHANNEL,id,resolved:false,data:err instanceof RateLimiterRes?err.toJSON():err?.message||String(err)});}catch{}}return;}if(worker&&typeof worker.send==='function'){try{worker.send({channel:IPC_CHANNEL,id,resolved:true,data:res instanceof RateLimiterRes?res.toJSON():res});}catch{}}}catch{}}close(){if(this.cluster&&typeof this.cluster.removeListener==='function'){this.cluster.removeListener('message',this._onMessage);}else if(this.cluster&&typeof this.cluster.off==='function'){this.cluster.off('message',this._onMessage);}if(RateLimiterClusterMaster.instance===this){RateLimiterClusterMaster.instance=null;}this._limiters.clear();}}export class RateLimiterClusterMasterPM2{constructor(pm2){this.pm2=pm2;this._limiters=new Map();this._onMessage=packet=>{try{if(!packet||!packet.data||packet.data.channel!==IPC_CHANNEL){return;}this._handlePacket(packet).catch(()=>{});}catch{}};if(this.pm2&&typeof this.pm2.launchBus==='function'){this.pm2.launchBus((err,bus)=>{if(!err&&bus){this._bus=bus;bus.on('process:msg',this._onMessage);}});}else if(typeof process!=='undefined'&&typeof process.on==='function'){this._onProcessMessage=msg=>{try{if(msg&&msg.channel===IPC_CHANNEL){this._handlePacket({data:msg,process:{pm_id:msg.processId}}).catch(()=>{});}}catch{}};process.on('message',this._onProcessMessage);}}_getLimiter(limiterOptions={}){const rawPrefix=limiterOptions.keyPrefix||'rl:';const normalizedPrefix=rawPrefix.endsWith(':')?rawPrefix:`${rawPrefix}:`;let limiter=this._limiters.get(normalizedPrefix);if(!limiter){limiter=new MemoryLimiter({...limiterOptions,keyPrefix:normalizedPrefix});this._limiters.set(normalizedPrefix,limiter);}return limiter;}async _handlePacket(packet){try{if(!packet||!packet.data||typeof packet.data!=='object'){return;}const{id,method,args,limiterOptions}=packet.data;if(!id){return;}if(typeof method!=='string'||!ALLOWED_IPC_METHODS.has(method)){this._reply(packet,{channel:IPC_CHANNEL,id,resolved:false,data:{message:`Method "${method}" is not allowed`}});return;}const limiter=this._getLimiter(limiterOptions);if(typeof limiter[method]!=='function'){this._reply(packet,{channel:IPC_CHANNEL,id,resolved:false,data:{message:`Method "${method}" is not supported`}});return;}const safeArgs=Array.isArray(args)?args:[];let res;try{res=await limiter[method](...safeArgs);}catch(err){this._reply(packet,{channel:IPC_CHANNEL,id,resolved:false,data:err instanceof RateLimiterRes?err.toJSON():err?.message||String(err)});return;}this._reply(packet,{channel:IPC_CHANNEL,id,resolved:true,data:res instanceof RateLimiterRes?res.toJSON():res});}catch{}}_reply(packet,payload){try{if(this.pm2&&typeof this.pm2.sendDataToProcessId==='function'&&packet.process?.pm_id!==undefined){this.pm2.sendDataToProcessId(packet.process.pm_id,payload,()=>{});}else if(typeof process!=='undefined'&&typeof process.send==='function'){process.send(payload);}}catch{}}close(){if(this._bus&&typeof this._bus.removeListener==='function'){this._bus.removeListener('process:msg',this._onMessage);}if(this._onProcessMessage&&typeof process!=='undefined'&&typeof process.removeListener==='function'){process.removeListener('message',this._onProcessMessage);this._onProcessMessage=null;}this._limiters.clear();}}export class RateLimiterCluster extends RateLimiter{constructor(opts={}){super(opts);this.timeoutMs=opts.timeoutMs!==undefined?Number(opts.timeoutMs):5000;this._process=opts.customProcess||(typeof process!=='undefined'?process:null);this._requests=new Map();this._reqId=0;this._onMessage=message=>{if(!message||message.channel!==IPC_CHANNEL||!message.id){return;}const req=this._requests.get(message.id);if(!req)return;this._requests.delete(message.id);if(req.timer){clearTimeout(req.timer);}if(message.resolved){const data=message.data;const res=data&&typeof data==='object'&&('remainingPoints'in data||'consumedPoints'in data)?new RateLimiterRes(data.remainingPoints,data.msBeforeNext,data.consumedPoints,data.isFirstInDuration):data;req.resolve(res);}else{const data=message.data;const err=data&&typeof data==='object'&&('remainingPoints'in data||'consumedPoints'in data)?new RateLimiterRes(data.remainingPoints,data.msBeforeNext,data.consumedPoints,data.isFirstInDuration):typeof data==='string'?new Error(data):data;req.reject(err);}};if(this._process&&typeof this._process.on==='function'){this._process.on('message',this._onMessage);}}_send(method,args){if(!this._process||typeof this._process.send!=='function'){if(isRateLimiterCompatible(this.insuranceLimiter)){return this.insuranceLimiter[method](...args);}return Promise.reject(new Error('Process does not support IPC send or master process is not available'));}const id=++this._reqId;const limiterOptions={points:this.points,duration:this.duration,blockDuration:this.blockDuration,execEvenly:this.execEvenly,execEvenlyMinDelayMs:this.execEvenlyMinDelayMs,keyPrefix:this.keyPrefix};return new Promise((resolve,reject)=>{let timer=null;if(this.timeoutMs>0){timer=setTimeout(()=>{this._requests.delete(id);if(isRateLimiterCompatible(this.insuranceLimiter)){this.insuranceLimiter[method](...args).then(resolve).catch(reject);}else{reject(new Error(`RateLimiterCluster timeout after ${this.timeoutMs}ms`));}},this.timeoutMs);if(timer.unref){timer.unref();}}this._requests.set(id,{resolve,reject,timer});this._process.send({channel:IPC_CHANNEL,id,method,args,limiterOptions});});}consume(key,points=1,options={}){return this._send('consume',[key,points,options]);}penalty(key,points=1,options={}){return this._send('penalty',[key,points,options]);}reward(key,points=1,options={}){return this._send('reward',[key,points,options]);}get(key,options={}){return this._send('get',[key,options]);}set(key,points,secDuration,options={}){return this._send('set',[key,points,secDuration,options]);}block(key,secDuration,options={}){return this._send('block',[key,secDuration,options]);}delete(key,options={}){return this._send('delete',[key,options]);}close(){if(this._process&&typeof this._process.removeListener==='function'){this._process.removeListener('message',this._onMessage);}for(const[,req]of this._requests){if(req.timer)clearTimeout(req.timer);}this._requests.clear();}}export default RateLimiterCluster;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{ConfigurationError}from'../../utils/errors.js';const DEFAULT_TABLE_NAME='rate_limiter';const TABLE_NAME_PATTERN=/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/;export class PostgresLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.storeClient=opts.storeClient||opts.client||null;if(!this.storeClient){throw new ConfigurationError('PostgreSQL client is not configured. Pass it via the "storeClient" option.');}this.tableName=opts.tableName!==undefined?opts.tableName:DEFAULT_TABLE_NAME;if(typeof this.tableName!=='string'||!TABLE_NAME_PATTERN.test(this.tableName)){throw new ConfigurationError('tableName must be a plain SQL identifier, optionally prefixed with a schema name');}this.tableCreated=opts.tableCreated!==undefined?Boolean(opts.tableCreated):false;this.clearExpiredByTimeout=opts.clearExpiredByTimeout!==undefined?Boolean(opts.clearExpiredByTimeout):false;this.clearExpiredTimeoutMs=opts.clearExpiredTimeoutMs!==undefined?Number(opts.clearExpiredTimeoutMs):300000;this._clearExpiredTimer=null;if(this.clearExpiredByTimeout&&this.clearExpiredTimeoutMs>0){this._clearExpiredTimer=setInterval(()=>{this.clearExpired().catch(()=>{});},this.clearExpiredTimeoutMs);if(this._clearExpiredTimer.unref){this._clearExpiredTimer.unref();}}if(!this.tableCreated){this.createTable().catch(()=>undefined);}}_quotedTable(){return this.tableName.split('.').map(part=>`"${part}"`).join('.');}_createTableSql(){return`CREATE TABLE IF NOT EXISTS ${this._quotedTable()} (
|
|
2
|
+
"key" TEXT PRIMARY KEY,
|
|
3
|
+
"points" BIGINT NOT NULL DEFAULT 0,
|
|
4
|
+
"expire" BIGINT DEFAULT NULL
|
|
5
|
+
)`;}_incrementSql(){const table=this._quotedTable();return`INSERT INTO ${table} ("key", "points", "expire") VALUES ($1, $2, $3)
|
|
6
|
+
ON CONFLICT ("key") DO UPDATE SET
|
|
7
|
+
"points" = CASE WHEN ${table}."expire" IS NULL OR ${table}."expire" > $4 THEN ${table}."points" + EXCLUDED.points ELSE EXCLUDED.points END,
|
|
8
|
+
"expire" = CASE WHEN ${table}."expire" IS NULL OR ${table}."expire" > $4 THEN ${table}."expire" ELSE EXCLUDED."expire" END
|
|
9
|
+
RETURNING "points", "expire"`;}_forceExpireSql(){return`INSERT INTO ${this._quotedTable()} ("key", "points", "expire") VALUES ($1, $2, $3)
|
|
10
|
+
ON CONFLICT ("key") DO UPDATE SET
|
|
11
|
+
"points" = EXCLUDED.points,
|
|
12
|
+
"expire" = EXCLUDED."expire"
|
|
13
|
+
RETURNING "points", "expire"`;}_selectSql(){return`SELECT "points", "expire" FROM ${this._quotedTable()} WHERE "key" = $1 AND ("expire" IS NULL OR "expire" > $2)`;}_deleteSql(){return`DELETE FROM ${this._quotedTable()} WHERE "key" = $1`;}async _query(sql,params=[]){const client=this.storeClient;if(typeof client.query!=='function'){throw new ConfigurationError('PostgreSQL client must expose a query() method');}return client.query(sql,params);}_toResult(row){if(!row){return{consumedPoints:0,expireAt:null};}const expireAt=row.expire===null||row.expire===undefined?null:Number(row.expire);return{consumedPoints:Number(row.points),expireAt:Number.isFinite(expireAt)?expireAt:null};}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}const now=Date.now();const expire=msDuration>0?now+msDuration:null;const sql=forceExpire?this._forceExpireSql():this._incrementSql();const params=forceExpire?[rlKey,points,expire]:[rlKey,points,expire,now,now];const result=await this._query(sql,params);const rows=result&&Array.isArray(result.rows)?result.rows:[];const row=rows.length>0?rows[0]:null;return this._toResult(row);}async _get(rlKey){const result=await this._query(this._selectSql(),[rlKey,Date.now()]);const rows=result&&Array.isArray(result.rows)?result.rows:[];if(rows.length===0){return null;}return this._toResult(rows[0]);}async _delete(rlKey){const result=await this._query(this._deleteSql(),[rlKey]);const rowCount=result&&typeof result.rowCount==='number'?result.rowCount:0;return rowCount>0;}createTable(){try{return Promise.resolve(this._query(this._createTableSql(),[]));}catch(err){return Promise.reject(err);}}async clear(){await this._query(`DELETE FROM ${this._quotedTable()}`,[]);}async clearExpired(){const now=Date.now();await this._query(`DELETE FROM ${this._quotedTable()} WHERE "expire" IS NOT NULL AND "expire" <= $1`,[now]);}async close(){if(this._clearExpiredTimer){clearInterval(this._clearExpiredTimer);this._clearExpiredTimer=null;}}}export default PostgresLimiter;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{ConfigurationError}from'../../utils/errors.js';const DEFAULT_TABLE_NAME='rate_limiter';const TABLE_NAME_PATTERN=/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/;const SUPPORTED_DIALECTS=['postgres','postgresql','mysql'];export class PrismaLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.storeClient=opts.storeClient||opts.client||opts.prisma||null;if(!this.storeClient){throw new ConfigurationError('Prisma client is not configured. Pass it via the "storeClient" option.');}if(typeof this.storeClient.$queryRawUnsafe!=='function'){throw new ConfigurationError('Prisma client must expose a $queryRawUnsafe() method');}this.dialect=opts.dialect!==undefined?String(opts.dialect).toLowerCase():'postgres';if(!SUPPORTED_DIALECTS.includes(this.dialect)){throw new ConfigurationError('dialect must be "postgres" or "mysql"');}if(this.dialect==='postgresql'){this.dialect='postgres';}this.tableName=opts.tableName!==undefined?opts.tableName:DEFAULT_TABLE_NAME;if(typeof this.tableName!=='string'||!TABLE_NAME_PATTERN.test(this.tableName)){throw new ConfigurationError('tableName must be a plain SQL identifier, optionally prefixed with a schema name');}this.tableCreated=opts.tableCreated!==undefined?Boolean(opts.tableCreated):false;if(!this.tableCreated){this.createTable().catch(()=>undefined);}}get _isMysql(){return this.dialect==='mysql';}_quotedTable(){const quote=this._isMysql?'`':'"';return this.tableName.split('.').map(part=>`${quote}${part}${quote}`).join('.');}_createTableSql(){if(this._isMysql){return`CREATE TABLE IF NOT EXISTS ${this._quotedTable()} (
|
|
2
|
+
\`key\` VARCHAR(255) NOT NULL,
|
|
3
|
+
\`points\` BIGINT NOT NULL DEFAULT 0,
|
|
4
|
+
\`expire\` BIGINT DEFAULT NULL,
|
|
5
|
+
PRIMARY KEY (\`key\`)
|
|
6
|
+
)`;}return`CREATE TABLE IF NOT EXISTS ${this._quotedTable()} (
|
|
7
|
+
"key" TEXT PRIMARY KEY,
|
|
8
|
+
"points" BIGINT NOT NULL DEFAULT 0,
|
|
9
|
+
"expire" BIGINT DEFAULT NULL
|
|
10
|
+
)`;}_incrementSql(){if(this._isMysql){return`INSERT INTO ${this._quotedTable()} (\`key\`, \`points\`, \`expire\`) VALUES (?, ?, ?)
|
|
11
|
+
ON DUPLICATE KEY UPDATE
|
|
12
|
+
\`points\` = IF(\`expire\` IS NULL OR \`expire\` > ?, \`points\` + VALUES(points), VALUES(points)),
|
|
13
|
+
\`expire\` = IF(\`expire\` IS NULL OR \`expire\` > ?, \`expire\`, VALUES(expire))`;}const table=this._quotedTable();return`INSERT INTO ${table} ("key", "points", "expire") VALUES ($1, $2, $3)
|
|
14
|
+
ON CONFLICT ("key") DO UPDATE SET
|
|
15
|
+
"points" = CASE WHEN ${table}."expire" IS NULL OR ${table}."expire" > $4 THEN ${table}."points" + EXCLUDED.points ELSE EXCLUDED.points END,
|
|
16
|
+
"expire" = CASE WHEN ${table}."expire" IS NULL OR ${table}."expire" > $4 THEN ${table}."expire" ELSE EXCLUDED."expire" END
|
|
17
|
+
RETURNING "points", "expire"`;}_forceExpireSql(){if(this._isMysql){return`INSERT INTO ${this._quotedTable()} (\`key\`, \`points\`, \`expire\`) VALUES (?, ?, ?)
|
|
18
|
+
ON DUPLICATE KEY UPDATE
|
|
19
|
+
\`points\` = VALUES(points),
|
|
20
|
+
\`expire\` = VALUES(expire)`;}return`INSERT INTO ${this._quotedTable()} ("key", "points", "expire") VALUES ($1, $2, $3)
|
|
21
|
+
ON CONFLICT ("key") DO UPDATE SET
|
|
22
|
+
"points" = EXCLUDED.points,
|
|
23
|
+
"expire" = EXCLUDED."expire"
|
|
24
|
+
RETURNING "points", "expire"`;}_selectSql(){if(this._isMysql){return`SELECT \`points\`, \`expire\` FROM ${this._quotedTable()} WHERE \`key\` = ? AND (\`expire\` IS NULL OR \`expire\` > ?)`;}return`SELECT "points", "expire" FROM ${this._quotedTable()} WHERE "key" = $1 AND ("expire" IS NULL OR "expire" > $2)`;}_selectRowSql(){if(this._isMysql){return`SELECT \`points\`, \`expire\` FROM ${this._quotedTable()} WHERE \`key\` = ?`;}return`SELECT "points", "expire" FROM ${this._quotedTable()} WHERE "key" = $1`;}_deleteSql(){if(this._isMysql){return`DELETE FROM ${this._quotedTable()} WHERE \`key\` = ?`;}return`DELETE FROM ${this._quotedTable()} WHERE "key" = $1`;}async _queryRows(sql,params=[]){const rows=await this.storeClient.$queryRawUnsafe(sql,...params);return Array.isArray(rows)?rows:[];}async _execute(sql,params=[]){if(typeof this.storeClient.$executeRawUnsafe!=='function'){throw new ConfigurationError('Prisma client must expose an $executeRawUnsafe() method');}const affected=await this.storeClient.$executeRawUnsafe(sql,...params);return typeof affected==='number'?affected:0;}_toResult(row){if(!row){return{consumedPoints:0,expireAt:null};}const expireAt=row.expire===null||row.expire===undefined?null:Number(row.expire);return{consumedPoints:Number(row.points),expireAt:Number.isFinite(expireAt)?expireAt:null};}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}const now=Date.now();const expire=msDuration>0?now+msDuration:null;if(forceExpire){await this._execute(this._forceExpireSql(),[rlKey,points,expire]);return{consumedPoints:points,expireAt:expire};}if(this._isMysql){await this._execute(this._incrementSql(),[rlKey,points,expire,now,now]);const rows=await this._queryRows(this._selectRowSql(),[rlKey]);return this._toResult(rows.length>0?rows[0]:null);}const rows=await this._queryRows(this._incrementSql(),[rlKey,points,expire,now,now]);return this._toResult(rows.length>0?rows[0]:null);}async _get(rlKey){const rows=await this._queryRows(this._selectSql(),[rlKey,Date.now()]);if(rows.length===0){return null;}return this._toResult(rows[0]);}async _delete(rlKey){const affected=await this._execute(this._deleteSql(),[rlKey]);return affected>0;}createTable(){try{return Promise.resolve(this._queryRows(this._createTableSql(),[]));}catch(err){return Promise.reject(err);}}async clear(){await this._execute(`DELETE FROM ${this._quotedTable()}`,[]);}}export default PrismaLimiter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{RedisConnectionError}from'../../utils/errors.js';import{RedisClientAdapter}from'../../utils/redisClient.js';const INCR_TTL_LUA=`
|
|
2
|
+
local ttl = redis.call('pttl', KEYS[1])
|
|
3
|
+
if ttl == -2 then
|
|
4
|
+
redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])
|
|
5
|
+
return {ARGV[1], ARGV[2]}
|
|
6
|
+
end
|
|
7
|
+
local consumed = redis.call('incrby', KEYS[1], ARGV[1])
|
|
8
|
+
if ttl == -1 then
|
|
9
|
+
redis.call('pexpire', KEYS[1], ARGV[2])
|
|
10
|
+
ttl = tonumber(ARGV[2])
|
|
11
|
+
end
|
|
12
|
+
return {consumed, ttl}
|
|
13
|
+
`;const SET_TTL_LUA=`
|
|
14
|
+
redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])
|
|
15
|
+
return {ARGV[1], ARGV[2]}
|
|
16
|
+
`;export class RedisLimiter extends RateLimiterStore{constructor(opts={}){super(opts);const client=opts.storeClient||opts.client||opts.redis||null;if(!client){throw new RedisConnectionError('Redis client is not configured. Pass it via the "storeClient" option.');}this.client=client;this.adapter=new RedisClientAdapter(client,{clientKind:opts.clientKind,isCluster:opts.isCluster});this.hashTag=Boolean(opts.hashTag);this.rejectIfRedisNotReady=Boolean(opts.rejectIfRedisNotReady);this._incrTtlScript=opts.customIncrTtlLuaScript||INCR_TTL_LUA;this._setTtlScript=opts.customSetTtlLuaScript||SET_TTL_LUA;}getKey(key){const prefixed=super.getKey(key);if(!this.hashTag)return prefixed;if(prefixed.includes('{'))return prefixed;return`{${prefixed}}`;}parseKey(rlKey){if(!rlKey)return rlKey;let key=rlKey;if(this.hashTag&&key.startsWith('{')&&key.endsWith('}')){key=key.slice(1,-1);}return super.parseKey(key);}_isRedisReady(rlKey,isReadonly){if(!this.rejectIfRedisNotReady){return true;}return this.adapter.isReady(rlKey,isReadonly);}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}if(!this._isRedisReady(rlKey,false)){throw new RedisConnectionError('Redis connection is not ready');}const ttl=Math.max(1,Math.floor(msDuration));const script=forceExpire?this._setTtlScript:this._incrTtlScript;const raw=await this.adapter.evalScript(script,[rlKey],[points,ttl]);const consumed=Array.isArray(raw)?Number(raw[0]):Number(raw);const resTtlMs=Array.isArray(raw)?Number(raw[1]):ttl;return{consumedPoints:consumed,expireAt:msDuration>0?Date.now()+resTtlMs:null};}async _get(rlKey){if(!this._isRedisReady(rlKey,true)){throw new RedisConnectionError('Redis connection is not ready');}const entry=await this.adapter.getWithTtl(rlKey);if(!entry){return null;}const consumedPoints=Number(entry.value);if(!Number.isFinite(consumedPoints)){return null;}return{consumedPoints,expireAt:entry.ttlMs>0?Date.now()+entry.ttlMs:null};}async _delete(rlKey){return this.adapter.del(rlKey);}}export default RedisLimiter;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{ConfigurationError}from'../../utils/errors.js';const DEFAULT_TABLE_NAME='rate_limiter';const TABLE_NAME_PATTERN=/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/;export class SqliteLimiter extends RateLimiterStore{constructor(opts={}){super(opts);this.storeClient=opts.storeClient||opts.client||null;if(!this.storeClient){throw new ConfigurationError('SQLite client is not configured. Pass it via the "storeClient" option.');}if(typeof this.storeClient.prepare!=='function'){throw new ConfigurationError('SQLite client must expose a prepare(sql) method (node:sqlite or better-sqlite3)');}this.tableName=opts.tableName!==undefined?opts.tableName:DEFAULT_TABLE_NAME;if(typeof this.tableName!=='string'||!TABLE_NAME_PATTERN.test(this.tableName)){throw new ConfigurationError('tableName must be a plain SQL identifier, optionally prefixed with a schema name');}this.tableCreated=opts.tableCreated!==undefined?Boolean(opts.tableCreated):false;this.clearExpiredByTimeout=opts.clearExpiredByTimeout!==undefined?Boolean(opts.clearExpiredByTimeout):false;this.clearExpiredTimeoutMs=opts.clearExpiredTimeoutMs!==undefined?Number(opts.clearExpiredTimeoutMs):300000;this._clearExpiredTimer=null;if(this.clearExpiredByTimeout&&this.clearExpiredTimeoutMs>0){this._clearExpiredTimer=setInterval(()=>{this.clearExpired().catch(()=>{});},this.clearExpiredTimeoutMs);if(this._clearExpiredTimer.unref){this._clearExpiredTimer.unref();}}this._statements=new Map();if(!this.tableCreated){this.createTable().catch(()=>undefined);}}_quotedTable(){return this.tableName.split('.').map(part=>`"${part}"`).join('.');}_createTableSql(){return`CREATE TABLE IF NOT EXISTS ${this._quotedTable()} (
|
|
2
|
+
"key" TEXT PRIMARY KEY,
|
|
3
|
+
"points" INTEGER NOT NULL DEFAULT 0,
|
|
4
|
+
"expire" INTEGER DEFAULT NULL
|
|
5
|
+
)`;}_incrementSql(){return`INSERT INTO ${this._quotedTable()} ("key", "points", "expire") VALUES (?, ?, ?)
|
|
6
|
+
ON CONFLICT ("key") DO UPDATE SET
|
|
7
|
+
"points" = CASE WHEN "expire" IS NULL OR "expire" > ? THEN "points" + excluded."points" ELSE excluded."points" END,
|
|
8
|
+
"expire" = CASE WHEN "expire" IS NULL OR "expire" > ? THEN "expire" ELSE excluded."expire" END
|
|
9
|
+
RETURNING "points", "expire"`;}_forceExpireSql(){return`INSERT INTO ${this._quotedTable()} ("key", "points", "expire") VALUES (?, ?, ?)
|
|
10
|
+
ON CONFLICT ("key") DO UPDATE SET
|
|
11
|
+
"points" = excluded."points",
|
|
12
|
+
"expire" = excluded."expire"
|
|
13
|
+
RETURNING "points", "expire"`;}_selectSql(){return`SELECT "points", "expire" FROM ${this._quotedTable()} WHERE "key" = ? AND ("expire" IS NULL OR "expire" > ?)`;}_deleteSql(){return`DELETE FROM ${this._quotedTable()} WHERE "key" = ?`;}_prepare(sql){const cached=this._statements.get(sql);if(cached){return cached;}const statement=this.storeClient.prepare(sql);this._statements.set(sql,statement);return statement;}_toResult(row){if(!row){return{consumedPoints:0,expireAt:null};}const expireAt=row.expire===null||row.expire===undefined?null:Number(row.expire);return{consumedPoints:Number(row.points),expireAt:Number.isFinite(expireAt)?expireAt:null};}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}const now=Date.now();const expire=msDuration>0?now+msDuration:null;const sql=forceExpire?this._forceExpireSql():this._incrementSql();const params=forceExpire?[rlKey,points,expire]:[rlKey,points,expire,now,now];const row=this._prepare(sql).get(...params);return this._toResult(row);}async _get(rlKey){const row=this._prepare(this._selectSql()).get(rlKey,Date.now());if(!row){return null;}return this._toResult(row);}async _delete(rlKey){const info=this._prepare(this._deleteSql()).run(rlKey);return Number(info.changes)>0;}createTable(){try{this._prepare(this._createTableSql()).run();return Promise.resolve();}catch(err){return Promise.reject(err);}}async clear(){this._prepare(`DELETE FROM ${this._quotedTable()}`).run();}async clearExpired(){const now=Date.now();this._prepare(`DELETE FROM ${this._quotedTable()} WHERE "expire" IS NOT NULL AND "expire" <= ?`).run(now);}async close(){if(this._clearExpiredTimer){clearInterval(this._clearExpiredTimer);this._clearExpiredTimer=null;}}}export default SqliteLimiter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import{RateLimiterStore}from'../rateLimiterStore.js';import{RedisClientAdapter}from'../../utils/redisClient.js';const INCR_TTL_LUA=`
|
|
2
|
+
local ttl = redis.call('pttl', KEYS[1])
|
|
3
|
+
if ttl == -2 then
|
|
4
|
+
redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])
|
|
5
|
+
return {ARGV[1], ARGV[2]}
|
|
6
|
+
end
|
|
7
|
+
local consumed = redis.call('incrby', KEYS[1], ARGV[1])
|
|
8
|
+
if ttl == -1 then
|
|
9
|
+
redis.call('pexpire', KEYS[1], ARGV[2])
|
|
10
|
+
ttl = tonumber(ARGV[2])
|
|
11
|
+
end
|
|
12
|
+
return {consumed, ttl}
|
|
13
|
+
`;const SET_TTL_LUA=`
|
|
14
|
+
redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])
|
|
15
|
+
return {ARGV[1], ARGV[2]}
|
|
16
|
+
`;export class ValkeyLimiter extends RateLimiterStore{constructor(opts={}){super(opts);const client=opts.storeClient||opts.client||opts.valkey||opts.redis||null;if(!client){throw new Error('Valkey client is not configured. Pass it via the "storeClient" option.');}this.client=client;this.adapter=new RedisClientAdapter(client,{clientKind:opts.clientKind});this.rejectIfRedisNotReady=Boolean(opts.rejectIfRedisNotReady);this._incrTtlScript=opts.customIncrTtlLuaScript||INCR_TTL_LUA;this._setTtlScript=opts.customSetTtlLuaScript||SET_TTL_LUA;}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}const ttl=Math.max(1,Math.floor(msDuration));const script=forceExpire?this._setTtlScript:this._incrTtlScript;const raw=await this.adapter.evalScript(script,[rlKey],[points,ttl]);const consumed=Array.isArray(raw)?Number(raw[0]):Number(raw);const resTtlMs=Array.isArray(raw)?Number(raw[1]):ttl;return{consumedPoints:consumed,expireAt:msDuration>0?Date.now()+resTtlMs:null};}async _get(rlKey){const entry=await this.adapter.getWithTtl(rlKey);if(!entry)return null;const consumedPoints=Number(entry.value);if(!Number.isFinite(consumedPoints))return null;return{consumedPoints,expireAt:entry.ttlMs>0?Date.now()+entry.ttlMs:null};}async _delete(rlKey){return this.adapter.del(rlKey);}}export default ValkeyLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ConfigurationError}from'../utils/errors.js';export class RateLimiter{constructor(opts={}){const rawPoints=opts.points!==undefined?opts.points:opts.limit;const rawDuration=opts.duration!==undefined?opts.duration:opts.windowMs!==undefined?opts.windowMs/1000:undefined;this.points=rawPoints!==undefined?rawPoints:60;this.duration=rawDuration!==undefined?rawDuration:60;this.blockDuration=opts.blockDuration!==undefined?opts.blockDuration:0;this.execEvenly=opts.execEvenly!==undefined?Boolean(opts.execEvenly):false;this.execEvenlyMinDelayMs=opts.execEvenlyMinDelayMs;this.keyPrefix=opts.keyPrefix!==undefined?opts.keyPrefix:'rl:';this.insuranceLimiter=opts.insuranceLimiter;}get points(){return this._points;}set points(value){if(typeof value!=='number'||!Number.isFinite(value)||!Number.isInteger(value)||value<0){throw new ConfigurationError('points must be set and must be a non-negative integer');}this._points=value;}get duration(){return this._duration;}set duration(value){if(typeof value!=='number'||!Number.isFinite(value)||value<0){throw new ConfigurationError('duration must be set and must be a finite, non-negative number');}this._duration=value;}get msDuration(){return this.duration*1000;}get blockDuration(){return this._blockDuration;}set blockDuration(value){if(value!==undefined&&(typeof value!=='number'||!Number.isFinite(value)||value<0)){throw new ConfigurationError('blockDuration must be a finite, non-negative number');}this._blockDuration=value===undefined?0:value;}get msBlockDuration(){return this.blockDuration*1000;}get execEvenly(){return this._execEvenly;}set execEvenly(value){this._execEvenly=value===undefined?false:Boolean(value);}get execEvenlyMinDelayMs(){return this._execEvenlyMinDelayMs===undefined?this._getExecEvenlyMinDelayMsDefault():this._execEvenlyMinDelayMs;}set execEvenlyMinDelayMs(value){this._execEvenlyMinDelayMs=value;}_getExecEvenlyMinDelayMsDefault(){return this.points>0?Math.ceil(this.msDuration/this.points):0;}get keyPrefix(){return this._keyPrefix;}set keyPrefix(value){if(value===undefined){value='rl:';}if(typeof value!=='string'){throw new ConfigurationError('keyPrefix must be a string');}this._keyPrefix=value;}get insuranceLimiter(){return this._insuranceLimiter;}set insuranceLimiter(value){if(value!==undefined&&value!==null&&!isRateLimiterCompatible(value)){throw new ConfigurationError('insuranceLimiter must be a RateLimiter instance or expose consume/get/set/block/delete');}this._insuranceLimiter=value||null;if(this._insuranceLimiter){this._insuranceLimiter.blockDuration=this.blockDuration;this._insuranceLimiter.execEvenly=this.execEvenly;}}_getKeySecDuration(options={}){return options&&options.customDuration>=0?options.customDuration:this.duration;}getKey(key){if(this.keyPrefix.length===0)return key;const prefix=this.keyPrefix.endsWith(':')?this.keyPrefix:`${this.keyPrefix}:`;return`${prefix}${key}`;}parseKey(rlKey){if(this.keyPrefix.length===0)return rlKey;const prefix=this.keyPrefix.endsWith(':')?this.keyPrefix:`${this.keyPrefix}:`;return rlKey.startsWith(prefix)?rlKey.substring(prefix.length):rlKey;}}export function isRateLimiterCompatible(limiter){return Boolean(limiter&&typeof limiter.consume==='function'&&typeof limiter.penalty==='function'&&typeof limiter.reward==='function'&&typeof limiter.get==='function'&&typeof limiter.set==='function'&&typeof limiter.block==='function'&&typeof limiter.delete==='function');}export default RateLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class RateLimiterRes{constructor(remainingPoints=0,msBeforeNext=0,consumedPoints=0,isFirstInDuration=false){this.remainingPoints=remainingPoints;this.msBeforeNext=msBeforeNext;this.consumedPoints=consumedPoints;this.isFirstInDuration=isFirstInDuration;}get msBeforeNext(){return this._msBeforeNext;}set msBeforeNext(ms){this._msBeforeNext=ms;}get remainingPoints(){return this._remainingPoints;}set remainingPoints(points){this._remainingPoints=points;}get consumedPoints(){return this._consumedPoints;}set consumedPoints(points){this._consumedPoints=points;}get isFirstInDuration(){return this._isFirstInDuration;}set isFirstInDuration(value){this._isFirstInDuration=Boolean(value);}_getDecoratedProperties(){return{remainingPoints:this.remainingPoints,msBeforeNext:this.msBeforeNext,consumedPoints:this.consumedPoints,isFirstInDuration:this.isFirstInDuration};}[Symbol.for('nodejs.util.inspect.custom')](){return this._getDecoratedProperties();}toString(){return JSON.stringify(this._getDecoratedProperties());}toJSON(){return this._getDecoratedProperties();}}export default RateLimiterRes;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiter,isRateLimiterCompatible}from'./rateLimiter.js';import{RateLimiterRes}from'./rateLimiterRes.js';import{BlockedKeys}from'./blockedKeys.js';export class RateLimiterStore extends RateLimiter{constructor(opts={}){super(opts);this._inMemoryBlockedKeys=new BlockedKeys();this.inMemoryBlockOnConsumed=opts.inMemoryBlockOnConsumed;this.inMemoryBlockDuration=opts.inMemoryBlockDuration;}get inMemoryBlockOnConsumed(){return this._inMemoryBlockOnConsumed;}set inMemoryBlockOnConsumed(value){this._inMemoryBlockOnConsumed=value?parseInt(value,10):0;if(this._inMemoryBlockOnConsumed>0&&this.points>this._inMemoryBlockOnConsumed){throw new Error('inMemoryBlockOnConsumed option must be greater or equal "points" option');}}get inMemoryBlockDuration(){return this._inMemoryBlockDuration;}set inMemoryBlockDuration(value){this._inMemoryBlockDuration=value?parseInt(value,10):0;if(this._inMemoryBlockDuration>0&&this._inMemoryBlockOnConsumed===0){throw new Error('inMemoryBlockOnConsumed option must be set up');}}get msInMemoryBlockDuration(){return this.inMemoryBlockDuration*1000;}getInMemoryBlockMsBeforeExpire(rlKey){if(this.inMemoryBlockOnConsumed>0){return this._inMemoryBlockedKeys.msBeforeExpire(rlKey);}return 0;}deleteInMemoryBlocked(key){const rlKey=this.getKey(key);return this._inMemoryBlockedKeys.delete(rlKey);}deleteInMemoryBlockedAll(){this._inMemoryBlockedKeys.deleteAll();}consume(key,pointsToConsume=1,options={}){return this._operation('_consume','consume',[key,pointsToConsume,options]);}penalty(key,points=1,options={}){return this._operation('_penalty','penalty',[key,points,options]);}reward(key,points=1,options={}){return this._operation('_reward','reward',[key,points,options]);}get(key,options={}){return this._operation('_getState','get',[key,options]);}set(key,points,secDuration,options={}){return this._operation('_setState','set',[key,points,secDuration,options]);}block(key,secDuration,options={}){return this._operation('_blockState','block',[key,secDuration,options]);}delete(key,options={}){return this._operation('_deleteState','delete',[key,options]);}_operation(internalName,publicName,params){let promise;try{promise=this[internalName](...params);}catch(syncErr){promise=Promise.reject(syncErr);}return new Promise((resolve,reject)=>{promise.then(res=>resolve(res)).catch(err=>{if(err instanceof RateLimiterRes){reject(err);return;}if(!isRateLimiterCompatible(this.insuranceLimiter)){reject(err);return;}try{Promise.resolve(this.insuranceLimiter[publicName](...params)).then(res=>resolve(res)).catch(fallbackErr=>reject(fallbackErr));}catch(syncErr){reject(syncErr);}});});}_afterConsume(resolve,reject,rlKey,changedPoints,storeResult,options){const res=this._getRateLimiterRes(rlKey,changedPoints,storeResult);if(this.inMemoryBlockOnConsumed>0&&res.consumedPoints>=this.inMemoryBlockOnConsumed){if(this.inMemoryBlockDuration>0){this._inMemoryBlockedKeys.add(rlKey,this.inMemoryBlockDuration);res.msBeforeNext=this.msInMemoryBlockDuration;}else{this._inMemoryBlockedKeys.addMs(rlKey,res.msBeforeNext);}}if(res.consumedPoints>this.points){const shouldBlock=this.blockDuration>0&&res.consumedPoints<=this.points+changedPoints;const blockPromise=shouldBlock?this._blockState(rlKey,this.blockDuration,options).then(()=>{res.msBeforeNext=this.msBlockDuration;}):Promise.resolve();blockPromise.then(()=>reject(res)).catch(err=>reject(err));return;}if(this.execEvenly&&this.points>0&&res.msBeforeNext>0&&!res.isFirstInDuration){let delay=Math.ceil(res.msBeforeNext/(res.remainingPoints+2));if(delay<this.execEvenlyMinDelayMs){delay=res.consumedPoints*this.execEvenlyMinDelayMs;}res.msBeforeNext=Math.max(res.msBeforeNext-delay,0);setTimeout(resolve,delay,res);return;}resolve(res);}async _consume(key,pointsToConsume=1,options={}){if(typeof pointsToConsume==='number'&&Number.isFinite(pointsToConsume)&&!Number.isInteger(pointsToConsume)){throw new Error('Consuming decimal number of points is not supported by this package');}if(typeof pointsToConsume!=='number'||!Number.isFinite(pointsToConsume)||pointsToConsume<0){throw new Error('pointsToConsume must be a non-negative integer');}const rlKey=this.getKey(key);const inMemoryBlockMsBeforeExpire=this.getInMemoryBlockMsBeforeExpire(rlKey);if(inMemoryBlockMsBeforeExpire>0){throw new RateLimiterRes(0,inMemoryBlockMsBeforeExpire);}const storeResult=await this._upsert(rlKey,pointsToConsume,this._getKeySecDuration(options)*1000,false,options);return new Promise((resolve,reject)=>{this._afterConsume(resolve,reject,rlKey,pointsToConsume,storeResult,options);});}async _penalty(key,points=1,options={}){if(typeof points==='number'&&Number.isFinite(points)&&!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}if(typeof points!=='number'||!Number.isFinite(points)||points<0){throw new Error('points must be a non-negative integer');}const rlKey=this.getKey(key);const storeResult=await this._upsert(rlKey,points,this._getKeySecDuration(options)*1000,false,options);return this._getRateLimiterRes(rlKey,points,storeResult);}async _reward(key,points=1,options={}){if(typeof points==='number'&&Number.isFinite(points)&&!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}if(typeof points!=='number'||!Number.isFinite(points)||points<0){throw new Error('points must be a non-negative integer');}const rlKey=this.getKey(key);const storeResult=await this._upsert(rlKey,-points,this._getKeySecDuration(options)*1000,false,options);return this._getRateLimiterRes(rlKey,-points,storeResult);}async _getState(key,options={}){const rlKey=this.getKey(key);const storeResult=await this._get(rlKey,options);if(storeResult===null||storeResult===undefined){return null;}return this._getRateLimiterRes(rlKey,0,storeResult,true);}async _deleteState(key,options={}){const rlKey=this.getKey(key);const res=await this._delete(rlKey,options);this._inMemoryBlockedKeys.delete(rlKey);return res;}async _setState(key,points,secDuration,options={}){const rlKey=this.getKey(key);const msDuration=(secDuration>=0?secDuration:this.duration)*1000;await this._upsert(rlKey,points,msDuration,true,options);return new RateLimiterRes(0,msDuration>0?msDuration:-1,points);}async _blockState(key,secDuration,options={}){if(typeof secDuration==='number'&&secDuration<=0){return new RateLimiterRes(this.points,0,0,false);}const rlKey=this.getKey(key);const msDuration=secDuration*1000;const initPoints=this.points+1;await this._upsert(rlKey,initPoints,msDuration,true,options);return new RateLimiterRes(0,msDuration>0?msDuration:-1,initPoints);}_getRateLimiterRes(_rlKey,changedPoints,storeResult,isRead=false){const res=new RateLimiterRes();const rawConsumed=Number(storeResult.consumedPoints);const consumed=Number.isFinite(rawConsumed)?rawConsumed:0;res.consumedPoints=consumed;res.isFirstInDuration=isRead?false:consumed===changedPoints;res.remainingPoints=Math.min(this.points,Math.max(this.points-Math.max(0,consumed),0));const expireAt=storeResult.expireAt;if(expireAt===null||expireAt===undefined){res.msBeforeNext=-1;}else{const expireMs=typeof expireAt==='number'?expireAt:new Date(expireAt).getTime();res.msBeforeNext=Math.max(expireMs-Date.now(),0);}return res;}async _upsert(_rlKey,_points,_msDuration,_forceExpire=false,_options={}){throw new Error("You have to implement the method '_upsert'!");}async _get(_rlKey,_options={}){throw new Error("You have to implement the method '_get'!");}async _delete(_rlKey,_options={}){throw new Error("You have to implement the method '_delete'!");}async close(){}}export default RateLimiterStore;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterRes}from'../rateLimiterRes.js';export class BlackAndWhiteLimiter{constructor(opts={}){if(!opts.limiter){throw new TypeError('BlackAndWhiteLimiter requires a limiter');}this.limiter=opts.limiter;this.blackList=opts.blackList||[];this.whiteList=opts.whiteList||[];this.isBlackListed=opts.isBlackListed;this.isWhiteListed=opts.isWhiteListed;this.runActionAnyway=opts.runActionAnyway||false;}get points(){return this.limiter.points;}get duration(){return this.limiter.duration;}get msDuration(){return this.limiter.msDuration;}async consume(key,points=1,options={}){if(this._isBlackListed(key)){if(this.runActionAnyway){await this._runAnyway(()=>this.limiter.consume(key,points,options));}throw this._blackListedRes();}if(this._isWhiteListed(key)){if(this.runActionAnyway){await this._runAnyway(()=>this.limiter.consume(key,points,options));}return new RateLimiterRes(Number.MAX_SAFE_INTEGER,0);}return this.limiter.consume(key,points,options);}async penalty(key,points=1,options={}){return this._forward(key,()=>this.limiter.penalty(key,points,options));}async reward(key,points=1,options={}){return this._forward(key,()=>this.limiter.reward(key,points,options));}async get(key,options={}){return this._forward(key,()=>this.limiter.get(key,options));}async set(key,points,secDuration,options={}){return this._forward(key,()=>this.limiter.set(key,points,secDuration,options));}async block(key,secDuration,options={}){return this._forward(key,()=>this.limiter.block(key,secDuration,options));}async delete(key,options={}){return this._forward(key,()=>this.limiter.delete(key,options));}_isBlackListed(key){if(Array.isArray(this.blackList)&&this.blackList.includes(key)){return true;}return typeof this.isBlackListed==='function'&&Boolean(this.isBlackListed(key));}_isWhiteListed(key){if(Array.isArray(this.whiteList)&&this.whiteList.includes(key)){return true;}return typeof this.isWhiteListed==='function'&&Boolean(this.isWhiteListed(key));}_blackListedRes(){return new RateLimiterRes(0,Number.MAX_SAFE_INTEGER,(this.points||0)+1,false);}async _forward(key,operation){if(this._isBlackListed(key)){if(this.runActionAnyway){await this._runAnyway(operation);}throw this._blackListedRes();}return operation();}async _runAnyway(operation){try{await operation();}catch{}}}export default BlackAndWhiteLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterRes}from'../rateLimiterRes.js';export class BurstyRateLimiter{constructor(rateLimiter,burstLimiter){if(!rateLimiter||!burstLimiter){throw new Error('BurstyRateLimiter requires a rate limiter and a burst limiter');}this.rateLimiter=rateLimiter;this.burstLimiter=burstLimiter;}get points(){return(this.rateLimiter.points||0)+(this.burstLimiter.points||0);}get duration(){return Math.max(this.rateLimiter.duration||0,this.burstLimiter.duration||0);}get msDuration(){return Math.max(this.rateLimiter.msDuration||0,this.burstLimiter.msDuration||0);}async delete(key,options={}){const[a,b]=await Promise.all([typeof this.rateLimiter.delete==='function'?this.rateLimiter.delete(key,options):true,typeof this.burstLimiter.delete==='function'?this.burstLimiter.delete(key,options):true]);return Boolean(a&&b);}async consume(key,points=1,options={}){try{return await this.rateLimiter.consume(key,points,options);}catch(err){if(!(err instanceof RateLimiterRes)){throw err;}try{const burstRes=await this.burstLimiter.consume(key,points,options);return this._merge(err,burstRes);}catch(burstErr){if(!(burstErr instanceof RateLimiterRes)){throw burstErr;}throw this._merge(err,burstErr);}}}async get(key){const[steadyRes,burstRes]=await Promise.all([this.rateLimiter.get(key),this.burstLimiter.get(key)]);return this._merge(steadyRes,burstRes);}_merge(steadyRes,burstRes){const steady=steadyRes instanceof RateLimiterRes?steadyRes:new RateLimiterRes(0,0);const burst=burstRes instanceof RateLimiterRes?burstRes:new RateLimiterRes(0,0);return new RateLimiterRes(steady.remainingPoints,Math.min(steady.msBeforeNext,burst.msBeforeNext),steady.consumedPoints,steady.isFirstInDuration);}}export default BurstyRateLimiter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RateLimiterError}from'../../utils/errors.js';import{RateLimiterRes}from'../rateLimiterRes.js';export class RateLimiterQueueError extends RateLimiterError{constructor(message='Rate limiter queue error'){super(message);this.name='RateLimiterQueueError';}static[Symbol.hasInstance](instance){return Boolean(instance&&instance.name==='RateLimiterQueueError');}}const DEFAULT_MAX_QUEUE_SIZE=4294967295;const DEFAULT_RETRY_DELAY_MS=100;export class RateLimiterQueue{constructor(limiter,opts={}){if(!limiter||typeof limiter.consume!=='function'||typeof limiter.get!=='function'){throw new TypeError('RateLimiterQueue requires a limiter exposing consume() and get()');}this.limiter=limiter;this.maxQueueSize=opts.maxQueueSize!==undefined?opts.maxQueueSize:DEFAULT_MAX_QUEUE_SIZE;this._queues=new Map();}get points(){return this.limiter.points;}get duration(){return this.limiter.duration;}get msDuration(){return this.limiter.msDuration;}async consume(key='limiter',points=1){const remaining=await this.removeTokens(points,key);const limit=typeof this.points==='number'?this.points:points+remaining;return new RateLimiterRes(remaining,0,Math.max(0,limit-remaining),false);}async get(key='limiter'){return this.limiter.get(key);}async delete(key='limiter'){if(typeof this.limiter.delete==='function'){return this.limiter.delete(key);}return false;}removeTokens(tokens,key='limiter',expiresUnixAt=0){if(typeof this.limiter.points==='number'&&tokens>this.limiter.points){return Promise.reject(new Error(`Can't remove more tokens than points: ${tokens} > ${this.limiter.points}`));}if(this._isExpired(expiresUnixAt)){return Promise.reject(new RateLimiterQueueError('Rate limiter request expired before it was queued'));}const state=this._getState(key);return new Promise((resolve,reject)=>{const item={tokens,expiresUnixAt,resolve,reject,settled:false,expiryTimer:null};state.items.push(item);if(state.items.length>this.maxQueueSize){state.items.pop();this._settle(item,()=>reject(new RateLimiterQueueError('Rate limiter queue is full')));return;}this._armExpiry(item,key);this._kick(key);});}async getTokensRemaining(key='limiter'){const res=await this.limiter.get(key);if(!res){return typeof this.limiter.points==='number'?this.limiter.points:0;}return res.remainingPoints;}_getState(key){let state=this._queues.get(key);if(!state){state={items:[],inFlight:false,retryTimer:null};this._queues.set(key,state);}return state;}_isExpired(expiresUnixAt){return expiresUnixAt>0&&expiresUnixAt*1000<=Date.now();}_kick(key){const state=this._queues.get(key);if(!state||state.inFlight||state.items.length===0){return;}this._drain(key).catch(()=>{});}async _drain(key){const state=this._queues.get(key);if(!state){return;}state.inFlight=true;try{while(state.items.length>0){const item=state.items[0];if(item.settled){state.items.shift();continue;}if(this._isExpired(item.expiresUnixAt)){state.items.shift();this._settle(item,()=>item.reject(new RateLimiterQueueError('Rate limiter request expired while it was queued')));continue;}let res;try{res=await this.limiter.consume(key,item.tokens);}catch(err){if(item.settled){continue;}if(err instanceof RateLimiterRes){const delay=this._retryDelay(err);if(delay<0){state.items.shift();this._settle(item,()=>item.reject(err));continue;}this._scheduleRetry(key,delay);return;}state.items.shift();this._settle(item,()=>item.reject(err));continue;}if(item.settled){continue;}if(state.items[0]===item){state.items.shift();}const remainingPoints=res&&typeof res.remainingPoints==='number'?res.remainingPoints:0;this._settle(item,()=>item.resolve(remainingPoints));}}finally{state.inFlight=false;this._cleanupState(key,state);}}_retryDelay(res){if(res.msBeforeNext>0){return res.msBeforeNext;}if(this.limiter&&this.limiter.duration===0){return-1;}if(res.msBeforeNext===0){return 0;}return DEFAULT_RETRY_DELAY_MS;}_scheduleRetry(key,delay){const state=this._queues.get(key);if(!state||state.retryTimer){return;}state.retryTimer=setTimeout(()=>{state.retryTimer=null;this._kick(key);},delay);if(state.retryTimer.unref){state.retryTimer.unref();}}_armExpiry(item,key){if(item.expiresUnixAt<=0){return;}const delay=item.expiresUnixAt*1000-Date.now();item.expiryTimer=setTimeout(()=>this._expire(item,key),Math.max(delay,0));if(item.expiryTimer.unref){item.expiryTimer.unref();}}_expire(item,key){if(item.settled){return;}const state=this._queues.get(key);if(state){const index=state.items.indexOf(item);if(index!==-1){state.items.splice(index,1);}}this._settle(item,()=>item.reject(new RateLimiterQueueError('Rate limiter request expired while it was queued')));this._cleanupState(key,state);}_settle(item,fn){item.settled=true;if(item.expiryTimer){clearTimeout(item.expiryTimer);item.expiryTimer=null;}fn();}_cleanupState(key,state){if(!state||state.items.length>0||state.inFlight){return;}if(state.retryTimer){clearTimeout(state.retryTimer);state.retryTimer=null;}if(this._queues.get(key)===state){this._queues.delete(key);}}}export default RateLimiterQueue;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ConfigurationError}from'../../utils/errors.js';import{isRateLimiterCompatible}from'../rateLimiter.js';export class RateLimiterUnion{constructor(...limiters){if(limiters.length<1){throw new ConfigurationError('RateLimiterUnion requires at least one limiter');}for(const limiter of limiters){if(!isRateLimiterCompatible(limiter)){throw new ConfigurationError('RateLimiterUnion requires limiters exposing consume/penalty/reward/get/set/block/delete');}}this.limiters=limiters;}get points(){const pts=this.limiters.map(l=>typeof l.points==='number'?l.points:Infinity).filter(n=>Number.isFinite(n));return pts.length>0?Math.min(...pts):0;}get duration(){const durs=this.limiters.map(l=>typeof l.duration==='number'?l.duration:0).filter(n=>n>0);return durs.length>0?Math.max(...durs):0;}get msDuration(){return this.duration*1000;}async consume(key,points=1){return this._runAll(limiter=>limiter.consume(key,points));}async get(key){return this._runAll(limiter=>limiter.get(key));}async delete(key){return this._runAll(limiter=>typeof limiter.delete==='function'?limiter.delete(key):true);}async _runAll(operation){const results=await Promise.allSettled(this.limiters.map(limiter=>operation(limiter)));const fulfilled={};const rejected={};results.forEach((result,index)=>{const prefix=this._prefixOf(index);if(result.status==='fulfilled'){fulfilled[prefix]=result.value;}else{rejected[prefix]=result.reason;}});if(Object.keys(rejected).length>0){throw rejected;}return fulfilled;}_prefixOf(index){const limiter=this.limiters[index];if(limiter&&typeof limiter.keyPrefix==='string'&&limiter.keyPrefix.length>0){return limiter.keyPrefix;}return`limiter${index}`;}}export default RateLimiterUnion;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TimeoutLimiter{constructor(opts={}){const{limiter,timeoutMs}=opts;if(!limiter){throw new TypeError('TimeoutLimiter requires a limiter');}if(typeof timeoutMs!=='number'||!Number.isFinite(timeoutMs)||timeoutMs<0){throw new TypeError('TimeoutLimiter requires timeoutMs to be a non-negative number');}this.limiter=limiter;this.timeoutMs=timeoutMs;}get points(){return this.limiter.points;}get duration(){return this.limiter.duration;}get msDuration(){return this.limiter.msDuration;}consume(key,points=1,options={}){return this._withTimeout(()=>this.limiter.consume(key,points,options));}penalty(key,points=1,options={}){return this._withTimeout(()=>this.limiter.penalty(key,points,options));}reward(key,points=1,options={}){return this._withTimeout(()=>this.limiter.reward(key,points,options));}get(key,options={}){return this._withTimeout(()=>this.limiter.get(key,options));}set(key,points,secDuration,options={}){return this._withTimeout(()=>this.limiter.set(key,points,secDuration,options));}block(key,secDuration,options={}){return this._withTimeout(()=>this.limiter.block(key,secDuration,options));}delete(key,options={}){return this._withTimeout(()=>this.limiter.delete(key,options));}_withTimeout(operation){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{reject(new Error('Operation timed out'));},this.timeoutMs);if(timer.unref){timer.unref();}Promise.resolve().then(operation).then(res=>{clearTimeout(timer);resolve(res);},err=>{clearTimeout(timer);reject(err);});});}}export default TimeoutLimiter;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{rateLimit,createLimiter}from'./rateLimit.js';import{RateLimitManager,createRateLimiters}from'./rateLimitManager.js';import{BaseStore}from'./stores/baseStore.js';import{MemoryStore}from'./stores/memoryStore.js';import{RedisStore}from'./stores/redisStore.js';import{BaseAlgorithm}from'./algorithms/baseAlgorithm.js';import{FixedWindow}from'./algorithms/fixedWindow.js';import{SlidingWindowLog}from'./algorithms/slidingWindowLog.js';import{SlidingWindowCounter}from'./algorithms/slidingWindowCounter.js';import{TokenBucket}from'./algorithms/tokenBucket.js';import{LeakyBucket}from'./algorithms/leakyBucket.js';import*as errors from'./utils/errors.js';import{defaultKeyGenerator,ipKeyGenerator,getEndpoint,normalizeIp,graphQLKeyGenerator}from'./utils/keyGenerator.js';import{extractGraphQLRootFieldFast}from'./utils/graphQLParser.js';import{validateIpRangeV4,validateSecondaryIps,compileWhitelist,isIpInWhitelist,cleanIp,ipToInt}from'./utils/ipUtils.js';import{evaluateSkipRule,checkHeaderCriteria,checkOriginCriteria,checkIpCriteria,checkKeyCriteria}from'./utils/skipUtils.js';import{RateLimiter}from'./core/rateLimiter.js';import{RateLimiterRes}from'./core/rateLimiterRes.js';import{RateLimiterStore}from'./core/rateLimiterStore.js';import{BlockedKeys}from'./core/blockedKeys.js';import{createCoreMiddleware}from'./core/coreMiddleware.js';import{MemoryLimiter}from'./core/limiters/memoryLimiter.js';import{RedisLimiter}from'./core/limiters/redisLimiter.js';import{ValkeyLimiter}from'./core/limiters/valkeyLimiter.js';import{ClusterLimiter,RedisClusterLimiter,RateLimiterRedisCluster}from'./core/limiters/clusterLimiter.js';import{RateLimiterCluster,RateLimiterClusterMaster,RateLimiterClusterMasterPM2}from'./core/limiters/nodeClusterLimiter.js';import{MongoLimiter}from'./core/limiters/mongoLimiter.js';import{MongooseLimiter}from'./core/limiters/mongooseLimiter.js';import{DynamoDbLimiter}from'./core/limiters/dynamoDbLimiter.js';import{MySqlLimiter}from'./core/limiters/mySqlLimiter.js';import{PostgresLimiter}from'./core/limiters/postgresLimiter.js';import{SqliteLimiter}from'./core/limiters/sqliteLimiter.js';import{PrismaLimiter}from'./core/limiters/prismaLimiter.js';import{MemcachedLimiter}from'./core/limiters/memcachedLimiter.js';import{EtcdLimiter}from'./core/limiters/etcdLimiter.js';import{RateLimiterQueue,RateLimiterQueueError}from'./core/wrappers/rateLimiterQueue.js';import{RateLimiterUnion}from'./core/wrappers/rateLimiterUnion.js';import{BurstyRateLimiter}from'./core/wrappers/burstyRateLimiter.js';import{BlackAndWhiteLimiter}from'./core/wrappers/blackAndWhiteLimiter.js';import{TimeoutLimiter}from'./core/wrappers/timeoutLimiter.js';import{RedisClientAdapter,detectRedisClientKind}from'./utils/redisClient.js';const RateLimiterMemory=MemoryLimiter;const RateLimiterRedis=RedisLimiter;const RateLimiterMongo=MongoLimiter;const RateLimiterMongoose=MongooseLimiter;const RateLimiterMySQL=MySqlLimiter;const RateLimiterPostgres=PostgresLimiter;const RateLimiterSQLite=SqliteLimiter;const RateLimiterPrisma=PrismaLimiter;const RateLimiterMemcache=MemcachedLimiter;const RateLimiterMemcached=MemcachedLimiter;const RateLimiterDynamo=DynamoDbLimiter;const RateLimiterDynamoDb=DynamoDbLimiter;const RateLimiterValkey=ValkeyLimiter;const RateLimiterEtcd=EtcdLimiter;const RateLimiterAbstract=RateLimiter;const RateLimiterStoreAbstract=RateLimiterStore;const RateLimiterBlackAndWhite=BlackAndWhiteLimiter;export{rateLimit,rateLimit as default,createLimiter,RateLimitManager,createRateLimiters,BaseStore,MemoryStore,RedisStore,BaseAlgorithm,FixedWindow,SlidingWindowLog,SlidingWindowCounter,TokenBucket,LeakyBucket,errors,defaultKeyGenerator,ipKeyGenerator,getEndpoint,normalizeIp,graphQLKeyGenerator,extractGraphQLRootFieldFast,validateIpRangeV4,validateSecondaryIps,compileWhitelist,isIpInWhitelist,cleanIp,ipToInt,evaluateSkipRule,checkHeaderCriteria,checkOriginCriteria,checkIpCriteria,checkKeyCriteria,RateLimiter,RateLimiterRes,RateLimiterStore,BlockedKeys,createCoreMiddleware,MemoryLimiter,RedisLimiter,ValkeyLimiter,ClusterLimiter,RedisClusterLimiter,RateLimiterRedisCluster,RateLimiterCluster,RateLimiterClusterMaster,RateLimiterClusterMasterPM2,MongoLimiter,MongooseLimiter,DynamoDbLimiter,MySqlLimiter,PostgresLimiter,SqliteLimiter,PrismaLimiter,MemcachedLimiter,EtcdLimiter,RateLimiterMemory,RateLimiterRedis,RateLimiterMongo,RateLimiterMongoose,RateLimiterMySQL,RateLimiterPostgres,RateLimiterSQLite,RateLimiterPrisma,RateLimiterMemcache,RateLimiterMemcached,RateLimiterDynamo,RateLimiterDynamoDb,RateLimiterValkey,RateLimiterEtcd,RateLimiterAbstract,RateLimiterStoreAbstract,RateLimiterBlackAndWhite,RateLimiterQueue,RateLimiterQueueError,RateLimiterUnion,BurstyRateLimiter,BlackAndWhiteLimiter,TimeoutLimiter,RedisClientAdapter,detectRedisClientKind};export*from'./utils/errors.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{defaultKeyGenerator,ipKeyGenerator,graphQLKeyGenerator}from'../utils/keyGenerator.js';import{setRateLimitHeaders}from'./headers.js';import{evaluateSkipRule}from'../utils/skipUtils.js';export function createMiddleware(algorithm,options){let keyGen=defaultKeyGenerator;if(typeof options.keyGenerator==='function'){keyGen=options.keyGenerator;}else if(options.keyGenerator==='ip'){keyGen=ipKeyGenerator;}else if(options.keyGenerator==='graphql'){keyGen=graphQLKeyGenerator;}else if(options.keyGenerator==='ip-endpoint'){keyGen=defaultKeyGenerator;}const rawSkip=options.skip!==undefined?options.skip:options.customSkip!==undefined?options.customSkip:options.customskip;const skipFn=typeof rawSkip==='function'?rawSkip:typeof rawSkip==='object'&&rawSkip!==null?(req,res)=>evaluateSkipRule(rawSkip,req,res,{generateKey:keyGen}):()=>false;const defaultHandler=(req,res,next,opts)=>{res.status(opts.statusCode);const msg=typeof opts.message==='function'?opts.message(req,res):opts.message;if(typeof msg==='object'){res.json(msg);}else{res.send(msg);}};const handler=options.handler||defaultHandler;const middleware=async(req,res,next)=>{try{const shouldSkip=await Promise.resolve(skipFn(req,res));if(shouldSkip){return next();}const key=await Promise.resolve(keyGen(req,res));let cost=1;if(typeof options.cost==='function'){cost=await Promise.resolve(options.cost(req,res));}else if(typeof options.cost==='number'){cost=options.cost;}if(!Number.isFinite(cost)||cost<=0){cost=1;}const result=await algorithm.consume(key,cost,{req,res});req.rateLimit=result;setRateLimitHeaders(res,result,options);if(result.allowed){return next();}return handler(req,res,next,options);}catch(err){if(options.failOpen){req.rateLimit={allowed:true,limit:options.limit||options.capacity||0,remaining:1,consumed:0,retryAfter:0,resetTime:new Date(Date.now()+60000),failedOpen:true};if(typeof options.onError==='function'){options.onError(err,req,res,next);}return next();}if(typeof options.onError==='function'){return options.onError(err,req,res,next);}return next(err);}};middleware.resetKey=keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;return algorithm.reset(k);};middleware.getState=keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;return algorithm.getState(k);};middleware.close=()=>algorithm.close();middleware.algorithm=algorithm;middleware.store=algorithm.store;return middleware;}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export function setRateLimitHeaders(res,result,options={}){if(!res||typeof res.setHeader!=='function'||res.headersSent){return;}const now=Date.now();const resetMs=result.resetTime instanceof Date?result.resetTime.getTime():Number(result.resetTime);const resetSeconds=Math.max(0,Math.ceil((resetMs-now)/1000));if(options.standardHeaders){res.setHeader('RateLimit-Limit',String(result.limit));res.setHeader('RateLimit-Remaining',String(result.remaining));res.setHeader('RateLimit-Reset',String(resetSeconds));if(options.standardHeaders!=='draft-6'&&options.windowMs){const windowSec=Math.ceil(options.windowMs/1000);res.setHeader('RateLimit-Policy',`${result.limit};w=${windowSec}`);}}if(options.legacyHeaders){res.setHeader('X-RateLimit-Limit',String(result.limit));res.setHeader('X-RateLimit-Remaining',String(result.remaining));res.setHeader('X-RateLimit-Reset',String(Math.ceil(resetMs/1000)));}if(!result.allowed){const retryAfter=Math.max(1,Number(result.retryAfter)||resetSeconds||1);res.setHeader('Retry-After',String(retryAfter));}}
|
package/dist/rateLimit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{validateOptions}from'./utils/validation.js';import{MemoryStore}from'./stores/memoryStore.js';import{RedisStore}from'./stores/redisStore.js';import{FixedWindow}from'./algorithms/fixedWindow.js';import{SlidingWindowLog}from'./algorithms/slidingWindowLog.js';import{SlidingWindowCounter}from'./algorithms/slidingWindowCounter.js';import{TokenBucket}from'./algorithms/tokenBucket.js';import{LeakyBucket}from'./algorithms/leakyBucket.js';import{createMiddleware}from'./middleware/expressMiddleware.js';const ALGORITHM_MAP={'fixed-window':FixedWindow,'sliding-window-log':SlidingWindowLog,'sliding-window-counter':SlidingWindowCounter,'token-bucket':TokenBucket,'leaky-bucket':LeakyBucket};export function rateLimit(rawOptions={}){const options=validateOptions(rawOptions);let store;if(typeof options.store==='string'){const s=options.store.toLowerCase();if(s==='redis'||s==='redis-cluster'||s==='cluster'){const isClusterStore=s==='redis-cluster'||s==='cluster';store=new RedisStore({...options,isCluster:options.isCluster!==undefined?options.isCluster:isClusterStore,hashTag:options.hashTag!==undefined?options.hashTag:isClusterStore?true:undefined,redis:options.redis||{}});}else{store=new MemoryStore(options);}}else if(options.store){store=options.store;}else if(options.client||options.redisClient||options.clusterClient||options.redisCluster||options.cluster||options.clusters||options.clusterNodes){store=new RedisStore(options);}else{store=new MemoryStore(options);}const AlgorithmClass=ALGORITHM_MAP[options.algorithm];const algorithm=new AlgorithmClass(options,store);return createMiddleware(algorithm,options);}export const createLimiter=rateLimit;export default rateLimit;
|