@@ -31,38 +31,72 @@ export class ThrottlerStorageRedisService implements ThrottlerStorageRedis, OnMo
31
31
// Credits to wyattjoh for the fast implementation you see below.
32
32
// https://github.com/wyattjoh/rate-limit-redis/blob/main/src/lib.ts
33
33
return `
34
- local totalHits = redis.call("INCR", KEYS[1])
35
- local timeToExpire = redis.call("PTTL", KEYS[1])
36
- if timeToExpire <= 0
37
- then
38
- redis.call("PEXPIRE", KEYS[1], tonumber(ARGV[1]))
39
- timeToExpire = tonumber(ARGV[1])
40
- end
41
- return { totalHits, timeToExpire }
34
+ local hitKey = KEYS[1]
35
+ local blockKey = KEYS[2]
36
+ local throttlerName = ARGV[1]
37
+ local ttl = tonumber(ARGV[2])
38
+ local limit = tonumber(ARGV[3])
39
+ local blockDuration = tonumber(ARGV[4])
40
+
41
+ local totalHits = redis.call('INCR', hitKey)
42
+ local timeToExpire = redis.call('PTTL', hitKey)
43
+
44
+ if timeToExpire <= 0 then
45
+ redis.call('PEXPIRE', hitKey, ttl)
46
+ timeToExpire = ttl
47
+ end
48
+
49
+ local isBlocked = redis.call('GET', blockKey)
50
+ local timeToBlockExpire = 0
51
+
52
+ if isBlocked then
53
+ timeToBlockExpire = redis.call('PTTL', blockKey)
54
+ elseif totalHits > limit then
55
+ redis.call('SET', blockKey, 1, 'PX', blockDuration)
56
+ isBlocked = '1'
57
+ timeToBlockExpire = blockDuration
58
+ end
59
+
60
+ if isBlocked and timeToBlockExpire <= 0 then
61
+ redis.call('DEL', blockKey)
62
+ redis.call('SET', hitKey, 1, 'PX', ttl)
63
+ totalHits = 1
64
+ timeToExpire = ttl
65
+ isBlocked = false
66
+ end
67
+
68
+ return { totalHits, timeToExpire, isBlocked and 1 or 0, timeToBlockExpire }
42
69
`
43
70
. replace ( / ^ \s + / gm, '' )
44
71
. trim ( ) ;
45
72
}
46
73
47
- async increment ( key : string , ttl : number ) : Promise < ThrottlerStorageRecord > {
48
- // Use EVAL instead of EVALSHA to support both redis instances and clusters.
74
+ async increment (
75
+ key : string ,
76
+ ttl : number ,
77
+ limit : number ,
78
+ blockDuration : number ,
79
+ throttlerName : string ,
80
+ ) : Promise < ThrottlerStorageRecord > {
81
+ const hitKey = `${ this . redis . options . keyPrefix } {${ key } :${ throttlerName } }:hits` ;
82
+ const blockKey = `${ this . redis . options . keyPrefix } {${ key } :${ throttlerName } }:blocked` ;
49
83
const results : number [ ] = ( await this . redis . call (
50
84
'EVAL' ,
51
85
this . scriptSrc ,
52
- 1 ,
53
- `${ this . redis . options . keyPrefix } ${ key } ` ,
86
+ 2 ,
87
+ hitKey ,
88
+ blockKey ,
89
+ throttlerName ,
54
90
ttl ,
91
+ limit ,
92
+ blockDuration ,
55
93
) ) as number [ ] ;
56
94
57
95
if ( ! Array . isArray ( results ) ) {
58
96
throw new TypeError ( `Expected result to be array of values, got ${ results } ` ) ;
59
97
}
60
98
61
- if ( results . length !== 2 ) {
62
- throw new Error ( `Expected 2 values, got ${ results . length } ` ) ;
63
- }
64
-
65
- const [ totalHits , timeToExpire ] = results ;
99
+ const [ totalHits , timeToExpire , isBlocked , timeToBlockExpire ] = results ;
66
100
67
101
if ( typeof totalHits !== 'number' ) {
68
102
throw new TypeError ( 'Expected totalHits to be a number' ) ;
@@ -72,9 +106,19 @@ export class ThrottlerStorageRedisService implements ThrottlerStorageRedis, OnMo
72
106
throw new TypeError ( 'Expected timeToExpire to be a number' ) ;
73
107
}
74
108
109
+ if ( typeof isBlocked !== 'number' ) {
110
+ throw new TypeError ( 'Expected isBlocked to be a number' ) ;
111
+ }
112
+
113
+ if ( typeof timeToBlockExpire !== 'number' ) {
114
+ throw new TypeError ( 'Expected timeToBlockExpire to be a number' ) ;
115
+ }
116
+
75
117
return {
76
118
totalHits,
77
119
timeToExpire : Math . ceil ( timeToExpire / 1000 ) ,
120
+ isBlocked : isBlocked === 1 ,
121
+ timeToBlockExpire : Math . ceil ( timeToBlockExpire / 1000 ) ,
78
122
} ;
79
123
}
80
124
0 commit comments