33const assert = require ( 'assert' )
44const microtime = require ( './microtime' )
55
6+ const defineCommand = {
7+ numberOfKeys : 1 ,
8+ lua : `
9+ local key = KEYS[1]
10+ local now = tonumber(ARGV[1])
11+ local duration = tonumber(ARGV[2])
12+ local max = tonumber(ARGV[3])
13+ local start = now - duration
14+
15+ -- Check if the key exists
16+ local exists = redis.call('EXISTS', key)
17+
18+ local count = 0
19+ local oldest = now
20+
21+ if exists == 1 then
22+ -- Remove expired entries based on the current duration
23+ redis.call('ZREMRANGEBYSCORE', key, 0, start)
24+
25+ -- Get count
26+ count = redis.call('ZCARD', key)
27+
28+ -- Get oldest timestamp if we have entries
29+ if count > 0 then
30+ local oldest_result = redis.call('ZRANGE', key, 0, 0)
31+ oldest = tonumber(oldest_result[1])
32+ end
33+ end
34+
35+ -- Calculate remaining (before adding current request)
36+ local remaining = max - count
37+
38+ -- Early return if already at limit
39+ if remaining <= 0 then
40+ local resetMicro = oldest + duration
41+ return {0, math.floor(resetMicro / 1000), max}
42+ end
43+
44+ -- Add current request with current timestamp
45+ redis.call('ZADD', key, now, now)
46+
47+ -- Calculate reset time and handle trimming if needed
48+ local resetMicro
49+
50+ -- Only perform trim if we're at or over max (based on count before adding)
51+ if count >= max then
52+ -- Get the entry at position -max for reset time calculation
53+ local oldest_in_range_result = redis.call('ZRANGE', key, -max, -max)
54+ local oldestInRange = oldest
55+
56+ if #oldest_in_range_result > 0 then
57+ oldestInRange = tonumber(oldest_in_range_result[1])
58+ end
59+
60+ -- Trim the set
61+ redis.call('ZREMRANGEBYRANK', key, 0, -(max + 1))
62+
63+ -- Calculate reset time based on the entry at position -max
64+ resetMicro = oldestInRange + duration
65+ else
66+ -- We're under the limit, use the oldest entry for reset time
67+ resetMicro = oldest + duration
68+ end
69+
70+ -- Set expiration using the provided duration
71+ redis.call('PEXPIRE', key, duration)
72+
73+ return {remaining, math.floor(resetMicro / 1000), max}
74+ `
75+ }
76+
677module . exports = class Limiter {
778 constructor ( { id, db, max = 2500 , duration = 3600000 , namespace = 'limit' } ) {
879 assert ( db , 'db required' )
@@ -11,77 +82,9 @@ module.exports = class Limiter {
1182 this . max = max
1283 this . duration = duration
1384 this . namespace = namespace
14-
15- this . db . defineCommand ( 'ratelimiter' , {
16- numberOfKeys : 1 ,
17- lua : `
18- local key = KEYS[1]
19- local now = tonumber(ARGV[1])
20- local duration = tonumber(ARGV[2])
21- local max = tonumber(ARGV[3])
22- local start = now - duration
23-
24- -- Check if the key exists
25- local exists = redis.call('EXISTS', key)
26-
27- local count = 0
28- local oldest = now
29-
30- if exists == 1 then
31- -- Remove expired entries based on the current duration
32- redis.call('ZREMRANGEBYSCORE', key, 0, start)
33-
34- -- Get count
35- count = redis.call('ZCARD', key)
36-
37- -- Get oldest timestamp if we have entries
38- if count > 0 then
39- local oldest_result = redis.call('ZRANGE', key, 0, 0)
40- oldest = tonumber(oldest_result[1])
41- end
42- end
43-
44- -- Calculate remaining (before adding current request)
45- local remaining = max - count
46-
47- -- Early return if already at limit
48- if remaining <= 0 then
49- local resetMicro = oldest + duration
50- return {0, math.floor(resetMicro / 1000), max}
51- end
52-
53- -- Add current request with current timestamp
54- redis.call('ZADD', key, now, now)
55-
56- -- Calculate reset time and handle trimming if needed
57- local resetMicro
58-
59- -- Only perform trim if we're at or over max (based on count before adding)
60- if count >= max then
61- -- Get the entry at position -max for reset time calculation
62- local oldest_in_range_result = redis.call('ZRANGE', key, -max, -max)
63- local oldestInRange = oldest
64-
65- if #oldest_in_range_result > 0 then
66- oldestInRange = tonumber(oldest_in_range_result[1])
67- end
68-
69- -- Trim the set
70- redis.call('ZREMRANGEBYRANK', key, 0, -(max + 1))
71-
72- -- Calculate reset time based on the entry at position -max
73- resetMicro = oldestInRange + duration
74- else
75- -- We're under the limit, use the oldest entry for reset time
76- resetMicro = oldest + duration
77- end
78-
79- -- Set expiration using the provided duration
80- redis.call('PEXPIRE', key, duration)
81-
82- return {remaining, math.floor(resetMicro / 1000), max}
83- `
84- } )
85+ if ( ! this . db . ratelimiter ) {
86+ this . db . defineCommand ( 'ratelimiter' , defineCommand )
87+ }
8588 }
8689
8790 async get ( { id = this . id , max = this . max , duration = this . duration } = { } ) {
@@ -103,3 +106,5 @@ module.exports = class Limiter {
103106 }
104107 }
105108}
109+
110+ module . exports . defineCommand = defineCommand
0 commit comments