web-gatekeeper-js 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/RateLimiter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { slidingWindowScript } from "./scripts/slidingWindow.lua.js";
|
|
2
|
-
import { tokenBucketScript } from "./scripts/tokenBucket.lua.js"
|
|
2
|
+
import { tokenBucketScript } from "./scripts/tokenBucket.lua.js";
|
|
3
3
|
import { RedisStore } from "./store/RedisStore.js";
|
|
4
4
|
|
|
5
5
|
export class RateLimiter {
|
|
@@ -13,8 +13,16 @@ export class RateLimiter {
|
|
|
13
13
|
if (!redisClient) throw new Error("redisClient is required");
|
|
14
14
|
if (!windowSize) throw new Error("windowSize is required");
|
|
15
15
|
if (!limit) throw new Error("limit is required");
|
|
16
|
-
if (
|
|
17
|
-
if (
|
|
16
|
+
if (maxToken == null) throw new Error("maxToken is required");
|
|
17
|
+
if (refillRate == null) throw new Error("refillRate is required");
|
|
18
|
+
|
|
19
|
+
if (refillRate <= 0) {
|
|
20
|
+
throw new Error("refillRate must be greater than 0");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (maxToken <= 0) {
|
|
24
|
+
throw new Error("maxToken must be greater than 0");
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
this.#store = new RedisStore(redisClient);
|
|
20
28
|
this.#windowSize = windowSize;
|
|
@@ -23,72 +31,86 @@ export class RateLimiter {
|
|
|
23
31
|
this.#refillRate = refillRate;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
async consume(identifier){
|
|
27
|
-
const now = Date.now()
|
|
34
|
+
async consume(identifier) {
|
|
35
|
+
const now = Date.now();
|
|
28
36
|
|
|
29
|
-
const slidingResult = await this.#runSlidingWindow(identifier, now)
|
|
30
|
-
if(!slidingResult.allowed){
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
const slidingResult = await this.#runSlidingWindow(identifier, now);
|
|
38
|
+
if (!slidingResult.allowed) {
|
|
39
|
+
return {
|
|
40
|
+
allowed: false,
|
|
41
|
+
reason: "rate_limit_exceeded",
|
|
42
|
+
resetAfter: slidingResult.resetAfter,
|
|
43
|
+
};
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
const bucketResult = await this.#runTokenBucket(identifier, now)
|
|
39
|
-
if(!bucketResult.allowed){
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const bucketResult = await this.#runTokenBucket(identifier, now);
|
|
47
|
+
if (!bucketResult.allowed) {
|
|
48
|
+
return {
|
|
49
|
+
allowed: false,
|
|
50
|
+
reason: "burst_limit_exceeded",
|
|
51
|
+
retryAfter: bucketResult.retryAfter,
|
|
52
|
+
};
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
return {
|
|
48
|
-
allowed
|
|
49
|
-
remaining
|
|
50
|
-
tokensRemaining
|
|
51
|
-
}
|
|
56
|
+
allowed: true,
|
|
57
|
+
remaining: slidingResult.remaining,
|
|
58
|
+
tokensRemaining: bucketResult.tokensLeft,
|
|
59
|
+
};
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
async #runSlidingWindow(identifier,now){
|
|
55
|
-
const key = `rl:sliding:${identifier}
|
|
56
|
-
const ttl = Math.ceil((this.#windowSize / 1000) * 2)
|
|
62
|
+
async #runSlidingWindow(identifier, now) {
|
|
63
|
+
const key = `rl:sliding:${identifier}`;
|
|
64
|
+
const ttl = Math.ceil((this.#windowSize / 1000) * 2);
|
|
65
|
+
console.log(typeof ttl, ttl);
|
|
66
|
+
console.log({
|
|
67
|
+
now,
|
|
68
|
+
maxToken : this.#maxToken,
|
|
69
|
+
refillRate : this.refillRate,
|
|
70
|
+
ttl,
|
|
71
|
+
});
|
|
57
72
|
|
|
58
73
|
const result = await this.#store.evalScript(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
74
|
+
slidingWindowScript,
|
|
75
|
+
key,
|
|
76
|
+
this.#windowSize,
|
|
77
|
+
this.#limit,
|
|
78
|
+
now,
|
|
79
|
+
ttl,
|
|
80
|
+
);
|
|
66
81
|
|
|
67
82
|
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
83
|
+
allowed: result[0] === 1,
|
|
84
|
+
current: result[1],
|
|
85
|
+
remaining: result[2],
|
|
86
|
+
resetAfter: result[3],
|
|
87
|
+
};
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
async #runTokenBucket(identifier, now) {
|
|
76
|
-
const key = `rl:bucket:${identifier}
|
|
77
|
-
const ttl = Math.max(Math.ceil(this.#maxToken / this.#refillRate) * 2, 60)
|
|
78
|
-
|
|
91
|
+
const key = `rl:bucket:${identifier}`;
|
|
92
|
+
const ttl = Math.max(Math.ceil(this.#maxToken / this.#refillRate) * 2, 60);
|
|
93
|
+
|
|
94
|
+
console.log({
|
|
95
|
+
now,
|
|
96
|
+
maxToken : this.#maxToken,
|
|
97
|
+
refillRate : this.refillRate,
|
|
98
|
+
ttl,
|
|
99
|
+
});
|
|
100
|
+
|
|
79
101
|
const result = await this.#store.evalScript(
|
|
80
102
|
tokenBucketScript,
|
|
81
103
|
key,
|
|
82
|
-
now,
|
|
83
|
-
this.#maxToken,
|
|
84
|
-
this.#refillRate,
|
|
85
|
-
ttl
|
|
86
|
-
)
|
|
104
|
+
now,
|
|
105
|
+
this.#maxToken,
|
|
106
|
+
this.#refillRate,
|
|
107
|
+
ttl,
|
|
108
|
+
);
|
|
87
109
|
|
|
88
110
|
return {
|
|
89
|
-
allowed
|
|
90
|
-
tokensLeft
|
|
91
|
-
retryAfter
|
|
92
|
-
}
|
|
93
|
-
}
|
|
111
|
+
allowed: result[0] === 1,
|
|
112
|
+
tokensLeft: result[1],
|
|
113
|
+
retryAfter: result[2],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
94
116
|
}
|
|
@@ -21,7 +21,7 @@ export const slidingWindowScript = `
|
|
|
21
21
|
'currentCount', 1,
|
|
22
22
|
'previousCount', 0
|
|
23
23
|
)
|
|
24
|
-
redis.call('
|
|
24
|
+
redis.call('PEXPIRE', key, math.floor(ttl))
|
|
25
25
|
return { 1, 1, limit - 1, resetAfter } -- ← 4 values
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -55,7 +55,7 @@ export const slidingWindowScript = `
|
|
|
55
55
|
'currentCount', newCurrentCount,
|
|
56
56
|
'previousCount', newPreviousCount
|
|
57
57
|
)
|
|
58
|
-
redis.call('
|
|
58
|
+
redis.call('PEXPIRE', key, math.floor(ttl))
|
|
59
59
|
|
|
60
60
|
local remaining = math.floor(limit - effectiveCount - 1)
|
|
61
61
|
return { 1, newCurrentCount, remaining, resetAfter } -- ← 4 values
|
|
@@ -27,7 +27,7 @@ export const throttlerScript = `
|
|
|
27
27
|
|
|
28
28
|
-- save and set TTL
|
|
29
29
|
redis.call('HSET', key, 'nextAllowedTime', newNextAllowedTime)
|
|
30
|
-
redis.call('
|
|
30
|
+
redis.call('PEXPIRE', key, math.floor(ttl))
|
|
31
31
|
|
|
32
32
|
-- return allowed + waitTime so Node.js knows how long to delay
|
|
33
33
|
return { 1, math.max(waitTime, 0) }
|
|
@@ -15,7 +15,7 @@ export const tokenBucketScript = `
|
|
|
15
15
|
'time', now,
|
|
16
16
|
'tokenLeft', maxToken - 1
|
|
17
17
|
)
|
|
18
|
-
redis.call('
|
|
18
|
+
redis.call('PEXPIRE', key, math.floor(ttl))
|
|
19
19
|
return { 1, maxToken - 1 }
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -29,7 +29,7 @@ export const tokenBucketScript = `
|
|
|
29
29
|
'time', now,
|
|
30
30
|
'tokenLeft', updatedToken
|
|
31
31
|
)
|
|
32
|
-
redis.call('
|
|
32
|
+
redis.call('PEXPIRE', key, math.floor(ttl))
|
|
33
33
|
|
|
34
34
|
local retryAfter = math.ceil((1 - updatedToken) / refillRate)
|
|
35
35
|
return { 0, 0, retryAfter }
|
|
@@ -40,7 +40,7 @@ export const tokenBucketScript = `
|
|
|
40
40
|
'time', now,
|
|
41
41
|
'tokenLeft', updatedToken - 1
|
|
42
42
|
)
|
|
43
|
-
redis.call('
|
|
43
|
+
redis.call('PEXPIRE', key, math.floor(ttl))
|
|
44
44
|
|
|
45
45
|
return { 1, updatedToken - 1, 0 }
|
|
46
46
|
`
|