@@ -9,11 +9,11 @@ import { RedisClientType } from 'redis';
99 * 4. Otherwise, disallow the request and do not update the token total.
1010 */
1111class TokenBucket implements RateLimiter {
12- capacity : number ;
12+ private capacity : number ;
1313
14- refillRate : number ;
14+ private refillRate : number ;
1515
16- client : RedisClientType ;
16+ private client : RedisClientType ;
1717
1818 /**
1919 * Create a new instance of a TokenBucket rate limiter that can be connected to any database store
@@ -29,64 +29,74 @@ class TokenBucket implements RateLimiter {
2929 throw Error ( 'TokenBucket refillRate and capacity must be positive' ) ;
3030 }
3131
32- async processRequest (
32+ public async processRequest (
3333 uuid : string ,
3434 timestamp : number ,
3535 tokens = 1
3636 ) : Promise < RateLimiterResponse > {
37+ // set the expiry of key-value pairs in the cache to 24 hours
38+ const keyExpiry = 86400000 ;
39+
3740 // attempt to get the value for the uuid from the redis cache
3841 const bucketJSON = await this . client . get ( uuid ) ;
3942 // if the response is null, we need to create bucket for the user
40- if ( bucketJSON === undefined || bucketJSON === null ) {
43+ if ( bucketJSON === null ) {
4144 if ( tokens > this . capacity ) {
42- // reject the request, not enough tokens in bucket
43- return {
44- success : false ,
45- tokens : 10 ,
46- } ;
45+ // reject the request, not enough tokens could even be in the bucket
46+ // TODO: add key to cache for next request.
47+ return this . processRequestResponse ( false , this . capacity ) ;
4748 }
4849 const newUserBucket : RedisBucket = {
4950 tokens : this . capacity - tokens ,
5051 timestamp,
5152 } ;
52- await this . client . set ( uuid , JSON . stringify ( newUserBucket ) ) ;
53- return {
54- success : true ,
55- tokens : newUserBucket . tokens ,
56- } ;
53+ await this . client . setEx ( uuid , keyExpiry , JSON . stringify ( newUserBucket ) ) ;
54+ return this . processRequestResponse ( true , newUserBucket . tokens ) ;
5755 }
5856
57+ // parse the returned thring form redis and update their token budget based on the time lapse between queries
5958 const bucket : RedisBucket = await JSON . parse ( bucketJSON ) ;
60-
61- const timeSinceLastQueryInSeconds : number = Math . min ( ( timestamp - bucket . timestamp ) / 60 ) ;
62- const tokensToAdd = timeSinceLastQueryInSeconds * this . refillRate ;
63- const updatedTokenCount = bucket . tokens + tokensToAdd ;
64- bucket . tokens = updatedTokenCount > this . capacity ? 10 : updatedTokenCount ;
59+ bucket . tokens = this . calculateTokenBudgetFormTimestamp ( bucket , timestamp ) ;
6560
6661 if ( bucket . tokens < tokens ) {
6762 // reject the request, not enough tokens in bucket
68- return {
69- success : false ,
70- tokens : bucket . tokens ,
71- } ;
63+ // TODO upadte expirey and timestamp despite rejected request
64+ return this . processRequestResponse ( false , bucket . tokens ) ;
7265 }
7366 const updatedUserBucket = {
7467 tokens : bucket . tokens - tokens ,
7568 timestamp,
7669 } ;
77- await this . client . set ( uuid , JSON . stringify ( updatedUserBucket ) ) ;
78- return {
79- success : true ,
80- tokens : updatedUserBucket . tokens ,
81- } ;
70+ await this . client . setEx ( uuid , keyExpiry , JSON . stringify ( updatedUserBucket ) ) ;
71+ return this . processRequestResponse ( true , updatedUserBucket . tokens ) ;
8272 }
8373
8474 /**
8575 * Resets the rate limiter to the intial state by clearing the redis store.
8676 */
87- reset ( ) : void {
77+ public reset ( ) : void {
8878 this . client . flushAll ( ) ;
8979 }
80+
81+ /**
82+ * Calculates the tokens a user bucket should have given the time lapse between requests.
83+ */
84+ private calculateTokenBudgetFormTimestamp = (
85+ bucket : RedisBucket ,
86+ timestamp : number
87+ ) : number => {
88+ const timeSinceLastQueryInSeconds : number = Math . min ( ( timestamp - bucket . timestamp ) / 60 ) ;
89+ const tokensToAdd = timeSinceLastQueryInSeconds * this . refillRate ;
90+ const updatedTokenCount = bucket . tokens + tokensToAdd ;
91+ return updatedTokenCount > this . capacity ? 10 : updatedTokenCount ;
92+ } ;
93+
94+ private processRequestResponse = ( success : boolean , tokens : number ) : RateLimiterResponse => {
95+ return {
96+ success,
97+ tokens,
98+ } ;
99+ } ;
90100}
91101
92102export default TokenBucket ;
0 commit comments