@@ -9,12 +9,12 @@ import { RateLimiter, RateLimiterResponse, RedisWindow } from '../@types/rateLim
99 * takeup in each.
1010 *
1111 * Whenever a user makes a request the following steps are performed:
12- * 1. Fixed minute windows are defined along with redis caches if previously undefined.
13- * 2. Rolling minute windows are defined or updated based on the timestamp of the new request.
12+ * 1. Fixed windows are defined along with redis caches if previously undefined.
13+ * 2. Rolling windows are defined or updated based on the timestamp of the new request.
1414 * 3. Counter of the current fixed window is updated with the new request's token usage.
1515 * 4. If a new minute interval is reached, the averaging formula is run to prevent fixed window's flaw
1616 * of flooded requests around window borders
17- * (ex. 10 token capacity: 1m59s 10 reqs 2m2s 10 reqs)
17+ * (ex. 1m windows, 10 token capacity: 1m59s 10 reqs 2m2s 10 reqs)
1818 */
1919class SlidingWindowCounter implements RateLimiter {
2020 private windowSize : number ;
@@ -24,10 +24,10 @@ class SlidingWindowCounter implements RateLimiter {
2424 private client : Redis ;
2525
2626 /**
27- * Create a new instance of a TokenBucket rate limiter that can be connected to any database store
28- * @param windowSize - size of each window in milliseconds (fixed and rolling)
29- * @param capacity - max capacity of tokens allowed per fixed window
30- * @param client - redis client where rate limiter will cache information
27+ * Create a new instance of a SlidingWindowCounter rate limiter that can be connected to any database store
28+ * @param windowSize size of each window in milliseconds (fixed and rolling)
29+ * @param capacity max capacity of tokens allowed per fixed window
30+ * @param client redis client where rate limiter will cache information
3131 */
3232 constructor ( windowSize : number , capacity : number , client : Redis ) {
3333 this . windowSize = windowSize ;
@@ -38,12 +38,13 @@ class SlidingWindowCounter implements RateLimiter {
3838 }
3939
4040 /**
41- * @function processRequest - current timestamp and number of tokens required for
42- * the request to go through are passed in. We first check if a window exists in the redis
43- * cache.
41+ * @function processRequest - Sliding window counter algorithm to allow or block
42+ * based on the depth/complexity (in amount of tokens) of incoming requests.
4443 *
45- * If not, then fixedWindowStart is set as the current timestamp, and currentTokens
46- * is checked against capacity. If we have enough capacity for the request, we return
44+ * First, checks if a window exists in the redis cache.
45+ *
46+ * If not, then `fixedWindowStart` is set as the current timestamp, and `currentTokens`
47+ * is checked against `capacity`. If enough room exists for the request, returns
4748 * success as true and tokens as how many tokens remain in the current fixed window.
4849 *
4950 * If a window does exist in the cache, we first check if the timestamp is greater than
@@ -66,6 +67,8 @@ class SlidingWindowCounter implements RateLimiter {
6667 * @param {number } timestamp - time the request was recieved
6768 * @param {number } [tokens=1] - complexity of the query for throttling requests
6869 * @return {* } {Promise<RateLimiterResponse>}
70+ * RateLimiterResponse: {success: boolean, tokens: number}
71+ * (tokens represents the remaining available capacity of the window)
6972 * @memberof SlidingWindowCounter
7073 */
7174 async processRequest (
@@ -79,31 +82,90 @@ class SlidingWindowCounter implements RateLimiter {
7982 // attempt to get the value for the uuid from the redis cache
8083 const windowJSON = await this . client . get ( uuid ) ;
8184
82- // // if the response is null, we need to create a window for the user
83- // if (windowJSON === null) {
84- // // rolling window is 1 minute long
85- // const rollingWindowEnd = timestamp + 60000;
86-
87- // // grabs the actual minute from the timestamp to create fixed window
88- // const fixedWindowStart = timestamp - (timestamp % 10000);
89- // const fixedWindowEnd = fixedWindowStart + 60000;
90-
91- // const newUserWindow: RedisWindow = {
92- // // conditionally set tokens depending on how many are requested compared to the capacity
93- // tokens: tokens > this.capacity ? this.capacity : this.capacity - tokens,
94- // timestamp,
95- // };
96-
97- // // reject the request, not enough tokens could even be in the bucket
98- // if (tokens > this.capacity) {
99- // await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
100- // return { success: false, tokens: this.capacity };
101- // }
102- // await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
103- // return { success: true, tokens: newUserWindow.tokens };
104- // }
105-
106- return { success : true , tokens : 0 } ;
85+ // if the response is null, we need to create a window for the user
86+ if ( windowJSON === null ) {
87+ const newUserWindow : RedisWindow = {
88+ // current and previous tokens represent how many tokens are in each window
89+ currentTokens : tokens <= this . capacity ? tokens : 0 ,
90+ previousTokens : 0 ,
91+ fixedWindowStart : timestamp ,
92+ } ;
93+
94+ if ( tokens <= this . capacity ) {
95+ await this . client . setex ( uuid , keyExpiry , JSON . stringify ( newUserWindow ) ) ;
96+ return { success : true , tokens : this . capacity - newUserWindow . currentTokens } ;
97+ }
98+
99+ await this . client . setex ( uuid , keyExpiry , JSON . stringify ( newUserWindow ) ) ;
100+ // tokens property represents how much capacity remains
101+ return { success : false , tokens : this . capacity } ;
102+ }
103+
104+ // if the cache is populated
105+
106+ const window : RedisWindow = await JSON . parse ( windowJSON ) ;
107+
108+ const updatedUserWindow : RedisWindow = {
109+ currentTokens : window . currentTokens ,
110+ previousTokens : window . previousTokens ,
111+ fixedWindowStart : window . fixedWindowStart ,
112+ } ;
113+
114+ // if request time is in a new window
115+ if ( window . fixedWindowStart && timestamp >= window . fixedWindowStart + this . windowSize ) {
116+ // if more than one window was skipped
117+ if ( timestamp >= window . fixedWindowStart + this . windowSize * 2 ) {
118+ // if one or more windows was skipped, reset new window to be at current timestamp
119+ updatedUserWindow . previousTokens = 0 ;
120+ updatedUserWindow . currentTokens = 0 ;
121+ updatedUserWindow . fixedWindowStart = timestamp ;
122+ } else {
123+ updatedUserWindow . previousTokens = updatedUserWindow . currentTokens ;
124+ updatedUserWindow . currentTokens = 0 ;
125+ updatedUserWindow . fixedWindowStart = window . fixedWindowStart + this . windowSize ;
126+ }
127+ }
128+
129+ // assigned to avoid TS error, this var will never be used as 0
130+ // var is declared here so that below can be inside a conditional for efficiency's sake
131+ let rollingWindowProportion = 0 ;
132+ let previousRollingTokens = 0 ;
133+
134+ if ( updatedUserWindow . fixedWindowStart && updatedUserWindow . previousTokens ) {
135+ // proportion of rolling window present in previous window
136+ rollingWindowProportion =
137+ ( this . windowSize - ( timestamp - updatedUserWindow . fixedWindowStart ) ) /
138+ this . windowSize ;
139+
140+ // remove unecessary decimals, 0.xx is enough
141+ // rollingWindowProportion -= rollingWindowProportion % 0.01;
142+
143+ // # of tokens present in rolling & previous window
144+ previousRollingTokens = Math . floor (
145+ updatedUserWindow . previousTokens * rollingWindowProportion
146+ ) ;
147+ }
148+
149+ // # of tokens present in rolling and/or current window
150+ // if previous tokens is null, previousRollingTokens will be 0
151+ const rollingTokens = updatedUserWindow . currentTokens + previousRollingTokens ;
152+
153+ // if request is allowed
154+ if ( tokens + rollingTokens <= this . capacity ) {
155+ updatedUserWindow . currentTokens += tokens ;
156+ await this . client . setex ( uuid , keyExpiry , JSON . stringify ( updatedUserWindow ) ) ;
157+ return {
158+ success : true ,
159+ tokens : this . capacity - ( updatedUserWindow . currentTokens + previousRollingTokens ) ,
160+ } ;
161+ }
162+
163+ // if request is blocked
164+ await this . client . setex ( uuid , keyExpiry , JSON . stringify ( updatedUserWindow ) ) ;
165+ return {
166+ success : false ,
167+ tokens : this . capacity - ( updatedUserWindow . currentTokens + previousRollingTokens ) ,
168+ } ;
107169 }
108170
109171 /**
0 commit comments