Skip to content

Commit 1ce7232

Browse files
committed
feat: add defineCommand
1 parent 9880285 commit 1ce7232

File tree

2 files changed

+87
-71
lines changed

2 files changed

+87
-71
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ Default: `this.max`
128128

129129
How long keep records of requests in milliseconds. If provided, it overrides the default `duration` value.
130130

131+
## defineCommand
132+
133+
It provides the command definition so you can load it into any [ioredis](https://github.com/redis/ioredis) instance:
134+
135+
```js
136+
const Redis = require('ioredis')
137+
const redis = new Redis(uri, {
138+
scripts: { ratelimiter: require('async-ratelimiter').defineCommand }
139+
})
140+
```
141+
131142
## Related
132143

133144
- [express-slow-down](https://github.com/nfriedly/express-slow-down) – Slow down repeated requests; use as an alternative (or addition) to express-rate-limit.

src/index.js

Lines changed: 76 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,77 @@
33
const assert = require('assert')
44
const 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+
677
module.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

Comments
 (0)