@@ -38,12 +38,37 @@ class SlidingWindowCounter implements RateLimiter {
3838 }
3939
4040 /**
41+ * @function processRequest - Sliding window counter algorithm to allow or block
42+ * based on the depth/complexity (in amount of tokens) of incoming requests.
4143 *
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
48+ * success as true and tokens as how many tokens remain in the current fixed window.
49+ *
50+ * If a window does exist in the cache, we first check if the timestamp is greater than
51+ * the fixedWindowStart + windowSize.
52+ *
53+ * If it isn't then we check the number of tokens in the arguments as well as in the cache
54+ * against the capacity and return success or failure from there while updating the cache.
55+ *
56+ * If the timestamp is over the windowSize beyond the fixedWindowStart, then we update fixedWindowStart
57+ * to be fixedWindowStart + windowSize (to create a new fixed window) and
58+ * make previousTokens = currentTokens, and currentTokens equal to the number of tokens in args, if
59+ * not over capacity.
60+ *
61+ * Once previousTokens is not null, we then run functionality using the rolling window to compute
62+ * the formula this entire limiting algorithm is distinguished by:
63+ *
64+ * currentTokens + previousTokens * overlap % of rolling window over previous fixed window
4265 *
4366 * @param {string } uuid - unique identifer used to throttle requests
4467 * @param {number } timestamp - time the request was recieved
4568 * @param {number } [tokens=1] - complexity of the query for throttling requests
4669 * @return {* } {Promise<RateLimiterResponse>}
70+ * RateLimiterResponse: {success: boolean, tokens: number}
71+ * (tokens represents the remaining available capacity of the window)
4772 * @memberof SlidingWindowCounter
4873 */
4974 async processRequest (
@@ -57,31 +82,80 @@ class SlidingWindowCounter implements RateLimiter {
5782 // attempt to get the value for the uuid from the redis cache
5883 const windowJSON = await this . client . get ( uuid ) ;
5984
60- // // if the response is null, we need to create a window for the user
61- // if (windowJSON === null) {
62- // // rolling window is 1 minute long
63- // const rollingWindowEnd = timestamp + 60000;
64-
65- // // grabs the actual minute from the timestamp to create fixed window
66- // const fixedWindowStart = timestamp - (timestamp % 10000);
67- // const fixedWindowEnd = fixedWindowStart + 60000;
68-
69- // const newUserWindow: RedisWindow = {
70- // // conditionally set tokens depending on how many are requested compared to the capacity
71- // tokens: tokens > this.capacity ? this.capacity : this.capacity - tokens,
72- // timestamp,
73- // };
74-
75- // // reject the request, not enough tokens could even be in the bucket
76- // if (tokens > this.capacity) {
77- // await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
78- // return { success: false, tokens: this.capacity };
79- // }
80- // await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
81- // return { success: true, tokens: newUserWindow.tokens };
82- // }
83-
84- 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 : null ,
91+ fixedWindowStart : timestamp ,
92+ } ;
93+
94+ if ( tokens > this . capacity ) {
95+ await this . client . setex ( uuid , keyExpiry , JSON . stringify ( newUserWindow ) ) ;
96+ // tokens property represents how much capacity remains
97+ return { success : false , tokens : this . capacity } ;
98+ }
99+
100+ await this . client . setex ( uuid , keyExpiry , JSON . stringify ( newUserWindow ) ) ;
101+ return { success : true , tokens : this . capacity - newUserWindow . currentTokens } ;
102+ }
103+
104+ // if the cache is populated
105+
106+ const window : RedisWindow = await JSON . parse ( windowJSON ) ;
107+
108+ let 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 ( timestamp > window . fixedWindowStart + this . windowSize + 1 ) {
116+ updatedUserWindow . previousTokens = updatedUserWindow . currentTokens ;
117+ updatedUserWindow . currentTokens = 0 ;
118+ updatedUserWindow . fixedWindowStart = window . fixedWindowStart + this . windowSize ;
119+ }
120+
121+ // assigned to avoid TS error, this var will never be used as 0
122+ // var is declared here so that below can be inside a conditional for efficiency's sake
123+ let rollingWindowProportion : number = 0 ;
124+
125+ if ( updatedUserWindow . previousTokens ) {
126+ // subtract window size by current time less fixed window's start
127+ // current time less fixed window start is the amount that rolling window is in current window
128+ // to get amount that rolling window is in previous window, we subtract this difference by window size
129+ // then we divide this amount by window size to get the proportion of rolling window in previous window
130+ // ex. 60000 - (1million+5 - 1million) = 59995 / 60000 = 0.9999
131+ rollingWindowProportion =
132+ ( this . windowSize - ( timestamp - updatedUserWindow . fixedWindowStart ) ) /
133+ this . windowSize ;
134+
135+ // remove unecessary decimals, 0.xx is enough
136+ rollingWindowProportion = rollingWindowProportion - ( rollingWindowProportion % 0.01 ) ;
137+ }
138+
139+ // the sliding window counter formula
140+ // ex. tokens(1) + currentTokens(2) + previousTokens(4) * RWP(.75) = 6 < capacity(10)
141+ // adjusts formula if previousTokens is null
142+ const rollingWindowAllowal = updatedUserWindow . previousTokens
143+ ? tokens +
144+ updatedUserWindow . currentTokens +
145+ updatedUserWindow . previousTokens * rollingWindowProportion <=
146+ this . capacity
147+ : tokens + updatedUserWindow . currentTokens <= this . capacity ;
148+
149+ // if request is allowed
150+ if ( rollingWindowAllowal ) {
151+ updatedUserWindow . currentTokens += tokens ;
152+ await this . client . setex ( uuid , keyExpiry , JSON . stringify ( updatedUserWindow ) ) ;
153+ return { success : true , tokens : this . capacity - updatedUserWindow . currentTokens } ;
154+ }
155+
156+ // if request is blocked
157+ await this . client . setex ( uuid , keyExpiry , JSON . stringify ( updatedUserWindow ) ) ;
158+ return { success : false , tokens : this . capacity - updatedUserWindow . currentTokens } ;
85159 }
86160
87161 /**
0 commit comments