@taukirsheikh/rate-limiter 1.1.0
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/LICENSE +21 -0
- package/README.md +490 -0
- package/dist/event-emitter.d.ts +32 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/event-emitter.js +72 -0
- package/dist/event-emitter.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/priority-queue.d.ts +66 -0
- package/dist/priority-queue.d.ts.map +1 -0
- package/dist/priority-queue.js +147 -0
- package/dist/priority-queue.js.map +1 -0
- package/dist/rate-limiter.d.ts +135 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +455 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/redis/distributed-rate-limiter.d.ts +149 -0
- package/dist/redis/distributed-rate-limiter.d.ts.map +1 -0
- package/dist/redis/distributed-rate-limiter.js +423 -0
- package/dist/redis/distributed-rate-limiter.js.map +1 -0
- package/dist/redis/index.d.ts +8 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +11 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/lua-scripts.d.ts +62 -0
- package/dist/redis/lua-scripts.d.ts.map +1 -0
- package/dist/redis/lua-scripts.js +229 -0
- package/dist/redis/lua-scripts.js.map +1 -0
- package/dist/redis/redis-storage.d.ts +134 -0
- package/dist/redis/redis-storage.d.ts.map +1 -0
- package/dist/redis/redis-storage.js +255 -0
- package/dist/redis/redis-storage.js.map +1 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lua scripts for atomic Redis operations
|
|
4
|
+
* These ensure race-condition-free distributed rate limiting
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.HEARTBEAT = exports.CLEAR_STATE = exports.INIT_STATE = exports.INCREMENT_RESERVOIR = exports.UPDATE_RESERVOIR = exports.GET_STATE = exports.RELEASE_SLOT = exports.ACQUIRE_SLOT = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* Acquire a slot for job execution
|
|
10
|
+
* Returns: [allowed: 0|1, running: number, waitTime: number]
|
|
11
|
+
*
|
|
12
|
+
* KEYS[1]: limiter state key (hash)
|
|
13
|
+
* ARGV[1]: max concurrent
|
|
14
|
+
* ARGV[2]: min time (ms)
|
|
15
|
+
* ARGV[3]: max per interval
|
|
16
|
+
* ARGV[4]: interval (ms)
|
|
17
|
+
* ARGV[5]: current timestamp (ms)
|
|
18
|
+
* ARGV[6]: job weight
|
|
19
|
+
* ARGV[7]: job id
|
|
20
|
+
*/
|
|
21
|
+
exports.ACQUIRE_SLOT = `
|
|
22
|
+
local stateKey = KEYS[1]
|
|
23
|
+
local maxConcurrent = tonumber(ARGV[1])
|
|
24
|
+
local minTime = tonumber(ARGV[2])
|
|
25
|
+
local maxPerInterval = tonumber(ARGV[3])
|
|
26
|
+
local interval = tonumber(ARGV[4])
|
|
27
|
+
local now = tonumber(ARGV[5])
|
|
28
|
+
local weight = tonumber(ARGV[6])
|
|
29
|
+
local jobId = ARGV[7]
|
|
30
|
+
|
|
31
|
+
-- Get current state
|
|
32
|
+
local running = tonumber(redis.call('HGET', stateKey, 'running') or '0')
|
|
33
|
+
local currentWeight = tonumber(redis.call('HGET', stateKey, 'currentWeight') or '0')
|
|
34
|
+
local lastJobTime = tonumber(redis.call('HGET', stateKey, 'lastJobTime') or '0')
|
|
35
|
+
local intervalStart = tonumber(redis.call('HGET', stateKey, 'intervalStart') or '0')
|
|
36
|
+
local intervalCount = tonumber(redis.call('HGET', stateKey, 'intervalCount') or '0')
|
|
37
|
+
local reservoir = redis.call('HGET', stateKey, 'reservoir')
|
|
38
|
+
|
|
39
|
+
-- Check concurrency limit
|
|
40
|
+
if currentWeight + weight > maxConcurrent then
|
|
41
|
+
return {0, running, 0, 'concurrency'}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
-- Check reservoir
|
|
45
|
+
if reservoir ~= false and tonumber(reservoir) <= 0 then
|
|
46
|
+
return {0, running, 0, 'reservoir'}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
-- Check interval rate limit
|
|
50
|
+
if now - intervalStart >= interval then
|
|
51
|
+
intervalStart = now
|
|
52
|
+
intervalCount = 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if intervalCount >= maxPerInterval then
|
|
56
|
+
local waitTime = interval - (now - intervalStart)
|
|
57
|
+
return {0, running, waitTime, 'interval'}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
-- Check min time between jobs
|
|
61
|
+
local timeSinceLastJob = now - lastJobTime
|
|
62
|
+
if timeSinceLastJob < minTime then
|
|
63
|
+
local waitTime = minTime - timeSinceLastJob
|
|
64
|
+
return {0, running, waitTime, 'minTime'}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
-- All checks passed - acquire slot
|
|
68
|
+
redis.call('HINCRBY', stateKey, 'running', 1)
|
|
69
|
+
redis.call('HINCRBY', stateKey, 'currentWeight', weight)
|
|
70
|
+
redis.call('HSET', stateKey, 'lastJobTime', now)
|
|
71
|
+
redis.call('HSET', stateKey, 'intervalStart', intervalStart)
|
|
72
|
+
redis.call('HINCRBY', stateKey, 'intervalCount', 1)
|
|
73
|
+
|
|
74
|
+
-- Decrement reservoir if set
|
|
75
|
+
if reservoir ~= false then
|
|
76
|
+
redis.call('HINCRBY', stateKey, 'reservoir', -1)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
-- Track active job
|
|
80
|
+
redis.call('HSET', stateKey .. ':jobs', jobId, weight)
|
|
81
|
+
|
|
82
|
+
-- Set TTL on state (cleanup after inactivity)
|
|
83
|
+
redis.call('EXPIRE', stateKey, 3600)
|
|
84
|
+
redis.call('EXPIRE', stateKey .. ':jobs', 3600)
|
|
85
|
+
|
|
86
|
+
return {1, running + 1, 0, 'ok'}
|
|
87
|
+
`;
|
|
88
|
+
/**
|
|
89
|
+
* Release a slot after job completion
|
|
90
|
+
* KEYS[1]: limiter state key
|
|
91
|
+
* ARGV[1]: job weight
|
|
92
|
+
* ARGV[2]: job id
|
|
93
|
+
* ARGV[3]: success (1) or failure (0)
|
|
94
|
+
*/
|
|
95
|
+
exports.RELEASE_SLOT = `
|
|
96
|
+
local stateKey = KEYS[1]
|
|
97
|
+
local weight = tonumber(ARGV[1])
|
|
98
|
+
local jobId = ARGV[2]
|
|
99
|
+
local success = tonumber(ARGV[3])
|
|
100
|
+
|
|
101
|
+
-- Decrement running count
|
|
102
|
+
local running = redis.call('HINCRBY', stateKey, 'running', -1)
|
|
103
|
+
redis.call('HINCRBY', stateKey, 'currentWeight', -weight)
|
|
104
|
+
|
|
105
|
+
-- Update stats
|
|
106
|
+
if success == 1 then
|
|
107
|
+
redis.call('HINCRBY', stateKey, 'done', 1)
|
|
108
|
+
else
|
|
109
|
+
redis.call('HINCRBY', stateKey, 'failed', 1)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
-- Remove from active jobs
|
|
113
|
+
redis.call('HDEL', stateKey .. ':jobs', jobId)
|
|
114
|
+
|
|
115
|
+
return running
|
|
116
|
+
`;
|
|
117
|
+
/**
|
|
118
|
+
* Get current limiter state
|
|
119
|
+
* KEYS[1]: limiter state key
|
|
120
|
+
*/
|
|
121
|
+
exports.GET_STATE = `
|
|
122
|
+
local stateKey = KEYS[1]
|
|
123
|
+
|
|
124
|
+
local running = tonumber(redis.call('HGET', stateKey, 'running') or '0')
|
|
125
|
+
local currentWeight = tonumber(redis.call('HGET', stateKey, 'currentWeight') or '0')
|
|
126
|
+
local done = tonumber(redis.call('HGET', stateKey, 'done') or '0')
|
|
127
|
+
local failed = tonumber(redis.call('HGET', stateKey, 'failed') or '0')
|
|
128
|
+
local reservoir = redis.call('HGET', stateKey, 'reservoir')
|
|
129
|
+
|
|
130
|
+
if reservoir == false then
|
|
131
|
+
reservoir = -1
|
|
132
|
+
else
|
|
133
|
+
reservoir = tonumber(reservoir)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
return {running, currentWeight, done, failed, reservoir}
|
|
137
|
+
`;
|
|
138
|
+
/**
|
|
139
|
+
* Update reservoir value
|
|
140
|
+
* KEYS[1]: limiter state key
|
|
141
|
+
* ARGV[1]: new reservoir value
|
|
142
|
+
*/
|
|
143
|
+
exports.UPDATE_RESERVOIR = `
|
|
144
|
+
local stateKey = KEYS[1]
|
|
145
|
+
local value = tonumber(ARGV[1])
|
|
146
|
+
|
|
147
|
+
redis.call('HSET', stateKey, 'reservoir', value)
|
|
148
|
+
redis.call('EXPIRE', stateKey, 3600)
|
|
149
|
+
|
|
150
|
+
return value
|
|
151
|
+
`;
|
|
152
|
+
/**
|
|
153
|
+
* Increment reservoir value
|
|
154
|
+
* KEYS[1]: limiter state key
|
|
155
|
+
* ARGV[1]: amount to add
|
|
156
|
+
*/
|
|
157
|
+
exports.INCREMENT_RESERVOIR = `
|
|
158
|
+
local stateKey = KEYS[1]
|
|
159
|
+
local amount = tonumber(ARGV[1])
|
|
160
|
+
|
|
161
|
+
local current = tonumber(redis.call('HGET', stateKey, 'reservoir') or '0')
|
|
162
|
+
local newValue = current + amount
|
|
163
|
+
|
|
164
|
+
redis.call('HSET', stateKey, 'reservoir', newValue)
|
|
165
|
+
redis.call('EXPIRE', stateKey, 3600)
|
|
166
|
+
|
|
167
|
+
return newValue
|
|
168
|
+
`;
|
|
169
|
+
/**
|
|
170
|
+
* Initialize limiter state
|
|
171
|
+
* KEYS[1]: limiter state key
|
|
172
|
+
* ARGV[1]: reservoir (or -1 for null)
|
|
173
|
+
*/
|
|
174
|
+
exports.INIT_STATE = `
|
|
175
|
+
local stateKey = KEYS[1]
|
|
176
|
+
local reservoir = tonumber(ARGV[1])
|
|
177
|
+
|
|
178
|
+
-- Only initialize if not exists
|
|
179
|
+
if redis.call('EXISTS', stateKey) == 0 then
|
|
180
|
+
redis.call('HSET', stateKey, 'running', 0)
|
|
181
|
+
redis.call('HSET', stateKey, 'currentWeight', 0)
|
|
182
|
+
redis.call('HSET', stateKey, 'lastJobTime', 0)
|
|
183
|
+
redis.call('HSET', stateKey, 'intervalStart', 0)
|
|
184
|
+
redis.call('HSET', stateKey, 'intervalCount', 0)
|
|
185
|
+
redis.call('HSET', stateKey, 'done', 0)
|
|
186
|
+
redis.call('HSET', stateKey, 'failed', 0)
|
|
187
|
+
|
|
188
|
+
if reservoir >= 0 then
|
|
189
|
+
redis.call('HSET', stateKey, 'reservoir', reservoir)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
redis.call('EXPIRE', stateKey, 3600)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
return 1
|
|
196
|
+
`;
|
|
197
|
+
/**
|
|
198
|
+
* Clear limiter state (for testing/reset)
|
|
199
|
+
* KEYS[1]: limiter state key
|
|
200
|
+
*/
|
|
201
|
+
exports.CLEAR_STATE = `
|
|
202
|
+
local stateKey = KEYS[1]
|
|
203
|
+
|
|
204
|
+
redis.call('DEL', stateKey)
|
|
205
|
+
redis.call('DEL', stateKey .. ':jobs')
|
|
206
|
+
redis.call('DEL', stateKey .. ':queue')
|
|
207
|
+
|
|
208
|
+
return 1
|
|
209
|
+
`;
|
|
210
|
+
/**
|
|
211
|
+
* Heartbeat - extend TTL and clean up stale jobs
|
|
212
|
+
* KEYS[1]: limiter state key
|
|
213
|
+
* ARGV[1]: current timestamp
|
|
214
|
+
* ARGV[2]: job timeout (ms)
|
|
215
|
+
*/
|
|
216
|
+
exports.HEARTBEAT = `
|
|
217
|
+
local stateKey = KEYS[1]
|
|
218
|
+
local now = tonumber(ARGV[1])
|
|
219
|
+
local timeout = tonumber(ARGV[2])
|
|
220
|
+
|
|
221
|
+
-- Extend TTL
|
|
222
|
+
redis.call('EXPIRE', stateKey, 3600)
|
|
223
|
+
redis.call('EXPIRE', stateKey .. ':jobs', 3600)
|
|
224
|
+
|
|
225
|
+
-- Could add stale job cleanup here if needed
|
|
226
|
+
|
|
227
|
+
return redis.call('HGET', stateKey, 'running') or '0'
|
|
228
|
+
`;
|
|
229
|
+
//# sourceMappingURL=lua-scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lua-scripts.js","sourceRoot":"","sources":["../../src/redis/lua-scripts.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH;;;;;;;;;;;;GAYG;AACU,QAAA,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkE3B,CAAC;AAEF;;;;;;GAMG;AACU,QAAA,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;CAqB3B,CAAC;AAEF;;;GAGG;AACU,QAAA,SAAS,GAAG;;;;;;;;;;;;;;;;CAgBxB,CAAC;AAEF;;;;GAIG;AACU,QAAA,gBAAgB,GAAG;;;;;;;;CAQ/B,CAAC;AAEF;;;;GAIG;AACU,QAAA,mBAAmB,GAAG;;;;;;;;;;;CAWlC,CAAC;AAEF;;;;GAIG;AACU,QAAA,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsBzB,CAAC;AAEF;;;GAGG;AACU,QAAA,WAAW,GAAG;;;;;;;;CAQ1B,CAAC;AAEF;;;;;GAKG;AACU,QAAA,SAAS,GAAG;;;;;;;;;;;;CAYxB,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Redis, RedisOptions, Cluster, ClusterOptions } from 'ioredis';
|
|
2
|
+
/**
|
|
3
|
+
* Result of acquiring a slot
|
|
4
|
+
*/
|
|
5
|
+
export interface AcquireResult {
|
|
6
|
+
allowed: boolean;
|
|
7
|
+
running: number;
|
|
8
|
+
waitTime: number;
|
|
9
|
+
reason: 'ok' | 'concurrency' | 'reservoir' | 'interval' | 'minTime';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Distributed state from Redis
|
|
13
|
+
*/
|
|
14
|
+
export interface DistributedState {
|
|
15
|
+
running: number;
|
|
16
|
+
currentWeight: number;
|
|
17
|
+
done: number;
|
|
18
|
+
failed: number;
|
|
19
|
+
reservoir: number | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Redis connection options
|
|
23
|
+
*/
|
|
24
|
+
export interface RedisConnectionOptions {
|
|
25
|
+
/** Redis connection URL (redis://...) */
|
|
26
|
+
url?: string;
|
|
27
|
+
/** Redis host */
|
|
28
|
+
host?: string;
|
|
29
|
+
/** Redis port */
|
|
30
|
+
port?: number;
|
|
31
|
+
/** Redis password */
|
|
32
|
+
password?: string;
|
|
33
|
+
/** Redis database number */
|
|
34
|
+
db?: number;
|
|
35
|
+
/** Key prefix for this limiter */
|
|
36
|
+
keyPrefix?: string;
|
|
37
|
+
/** Use Redis Cluster */
|
|
38
|
+
cluster?: boolean;
|
|
39
|
+
/** Cluster nodes */
|
|
40
|
+
clusterNodes?: Array<{
|
|
41
|
+
host: string;
|
|
42
|
+
port: number;
|
|
43
|
+
}>;
|
|
44
|
+
/** Additional ioredis options */
|
|
45
|
+
redisOptions?: RedisOptions;
|
|
46
|
+
/** Cluster options */
|
|
47
|
+
clusterOptions?: ClusterOptions;
|
|
48
|
+
/** Existing Redis client to use */
|
|
49
|
+
client?: Redis | Cluster;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Redis storage adapter for distributed rate limiting
|
|
53
|
+
*/
|
|
54
|
+
export declare class RedisStorage {
|
|
55
|
+
private client;
|
|
56
|
+
private readonly keyPrefix;
|
|
57
|
+
private readonly ownClient;
|
|
58
|
+
private initialized;
|
|
59
|
+
private scripts;
|
|
60
|
+
constructor(options: RedisConnectionOptions);
|
|
61
|
+
private createClient;
|
|
62
|
+
/**
|
|
63
|
+
* Execute a Lua script with automatic fallback to EVAL if EVALSHA fails
|
|
64
|
+
*/
|
|
65
|
+
private execScript;
|
|
66
|
+
/**
|
|
67
|
+
* Initialize storage and load Lua scripts
|
|
68
|
+
*/
|
|
69
|
+
initialize(limiterId: string, reservoir: number | null): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Get the Redis key for a limiter
|
|
72
|
+
*/
|
|
73
|
+
private getKey;
|
|
74
|
+
/**
|
|
75
|
+
* Try to acquire a slot for job execution
|
|
76
|
+
*/
|
|
77
|
+
acquireSlot(limiterId: string, options: {
|
|
78
|
+
maxConcurrent: number;
|
|
79
|
+
minTime: number;
|
|
80
|
+
maxPerInterval: number;
|
|
81
|
+
interval: number;
|
|
82
|
+
weight: number;
|
|
83
|
+
jobId: string;
|
|
84
|
+
}): Promise<AcquireResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Release a slot after job completion
|
|
87
|
+
*/
|
|
88
|
+
releaseSlot(limiterId: string, jobId: string, weight: number, success: boolean): Promise<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Get current distributed state
|
|
91
|
+
*/
|
|
92
|
+
getState(limiterId: string): Promise<DistributedState>;
|
|
93
|
+
/**
|
|
94
|
+
* Update reservoir value
|
|
95
|
+
*/
|
|
96
|
+
updateReservoir(limiterId: string, value: number): Promise<number>;
|
|
97
|
+
/**
|
|
98
|
+
* Increment reservoir value
|
|
99
|
+
*/
|
|
100
|
+
incrementReservoir(limiterId: string, amount: number): Promise<number>;
|
|
101
|
+
/**
|
|
102
|
+
* Clear all state for a limiter
|
|
103
|
+
*/
|
|
104
|
+
clear(limiterId: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Send heartbeat to keep state alive
|
|
107
|
+
*/
|
|
108
|
+
heartbeat(limiterId: string, timeout: number): Promise<number>;
|
|
109
|
+
/**
|
|
110
|
+
* Subscribe to reservoir depletion events
|
|
111
|
+
*/
|
|
112
|
+
subscribeToEvents(limiterId: string, callback: (event: string, data: unknown) => void): Promise<() => void>;
|
|
113
|
+
/**
|
|
114
|
+
* Publish an event
|
|
115
|
+
*/
|
|
116
|
+
publishEvent(limiterId: string, event: string, data: unknown): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Get the underlying Redis client
|
|
119
|
+
*/
|
|
120
|
+
getClient(): Redis | Cluster;
|
|
121
|
+
/**
|
|
122
|
+
* Check if connected to Redis
|
|
123
|
+
*/
|
|
124
|
+
isConnected(): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Wait for Redis connection
|
|
127
|
+
*/
|
|
128
|
+
waitForConnection(timeout?: number): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Close the Redis connection
|
|
131
|
+
*/
|
|
132
|
+
close(): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=redis-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-storage.d.ts","sourceRoot":"","sources":["../../src/redis/redis-storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisStorage = void 0;
|
|
4
|
+
const lua_scripts_js_1 = require("./lua-scripts.js");
|
|
5
|
+
/**
|
|
6
|
+
* Redis storage adapter for distributed rate limiting
|
|
7
|
+
*/
|
|
8
|
+
class RedisStorage {
|
|
9
|
+
client;
|
|
10
|
+
keyPrefix;
|
|
11
|
+
ownClient;
|
|
12
|
+
initialized = false;
|
|
13
|
+
// Scripts with their source and cached SHAs
|
|
14
|
+
scripts = new Map([
|
|
15
|
+
['acquire', { source: lua_scripts_js_1.ACQUIRE_SLOT }],
|
|
16
|
+
['release', { source: lua_scripts_js_1.RELEASE_SLOT }],
|
|
17
|
+
['getState', { source: lua_scripts_js_1.GET_STATE }],
|
|
18
|
+
['updateReservoir', { source: lua_scripts_js_1.UPDATE_RESERVOIR }],
|
|
19
|
+
['incrementReservoir', { source: lua_scripts_js_1.INCREMENT_RESERVOIR }],
|
|
20
|
+
['initState', { source: lua_scripts_js_1.INIT_STATE }],
|
|
21
|
+
['clearState', { source: lua_scripts_js_1.CLEAR_STATE }],
|
|
22
|
+
['heartbeat', { source: lua_scripts_js_1.HEARTBEAT }],
|
|
23
|
+
]);
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.keyPrefix = options.keyPrefix ?? 'ratelimit';
|
|
26
|
+
this.ownClient = !options.client;
|
|
27
|
+
if (options.client) {
|
|
28
|
+
this.client = options.client;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Dynamically import ioredis
|
|
32
|
+
this.client = this.createClient(options);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
createClient(options) {
|
|
36
|
+
// Dynamic import to make Redis optional
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
38
|
+
const IORedis = require('ioredis');
|
|
39
|
+
if (options.cluster && options.clusterNodes) {
|
|
40
|
+
return new IORedis.Cluster(options.clusterNodes, {
|
|
41
|
+
redisOptions: {
|
|
42
|
+
password: options.password,
|
|
43
|
+
...options.redisOptions,
|
|
44
|
+
},
|
|
45
|
+
...options.clusterOptions,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (options.url) {
|
|
49
|
+
return new IORedis.default(options.url, options.redisOptions);
|
|
50
|
+
}
|
|
51
|
+
return new IORedis.default({
|
|
52
|
+
host: options.host ?? 'localhost',
|
|
53
|
+
port: options.port ?? 6379,
|
|
54
|
+
password: options.password,
|
|
55
|
+
db: options.db ?? 0,
|
|
56
|
+
...options.redisOptions,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Execute a Lua script with automatic fallback to EVAL if EVALSHA fails
|
|
61
|
+
*/
|
|
62
|
+
async execScript(name, numKeys, ...args) {
|
|
63
|
+
const script = this.scripts.get(name);
|
|
64
|
+
if (!script) {
|
|
65
|
+
throw new Error(`Unknown script: ${name}`);
|
|
66
|
+
}
|
|
67
|
+
// Try EVALSHA first if we have a cached SHA
|
|
68
|
+
if (script.sha) {
|
|
69
|
+
try {
|
|
70
|
+
return await this.client.evalsha(script.sha, numKeys, ...args);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// If NOSCRIPT error, fall through to EVAL
|
|
74
|
+
if (!(error instanceof Error) || !error.message.includes('NOSCRIPT')) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
// Clear the cached SHA
|
|
78
|
+
script.sha = undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Use EVAL and cache the SHA for next time
|
|
82
|
+
const result = await this.client.eval(script.source, numKeys, ...args);
|
|
83
|
+
// Cache SHA for future calls (load script)
|
|
84
|
+
try {
|
|
85
|
+
script.sha = await this.client.script('LOAD', script.source);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Ignore errors loading script - we'll just use EVAL next time
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Initialize storage and load Lua scripts
|
|
94
|
+
*/
|
|
95
|
+
async initialize(limiterId, reservoir) {
|
|
96
|
+
if (this.initialized)
|
|
97
|
+
return;
|
|
98
|
+
// Pre-load all scripts
|
|
99
|
+
for (const [name, script] of this.scripts) {
|
|
100
|
+
try {
|
|
101
|
+
script.sha = await this.client.script('LOAD', script.source);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Ignore - we'll use EVAL as fallback
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Initialize state
|
|
108
|
+
const key = this.getKey(limiterId);
|
|
109
|
+
await this.execScript('initState', 1, key, reservoir ?? -1);
|
|
110
|
+
this.initialized = true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get the Redis key for a limiter
|
|
114
|
+
*/
|
|
115
|
+
getKey(limiterId) {
|
|
116
|
+
return `${this.keyPrefix}:${limiterId}`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Try to acquire a slot for job execution
|
|
120
|
+
*/
|
|
121
|
+
async acquireSlot(limiterId, options) {
|
|
122
|
+
const key = this.getKey(limiterId);
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const result = await this.execScript('acquire', 1, key, options.maxConcurrent, options.minTime, options.maxPerInterval, options.interval, now, options.weight, options.jobId);
|
|
125
|
+
return {
|
|
126
|
+
allowed: result[0] === 1,
|
|
127
|
+
running: result[1],
|
|
128
|
+
waitTime: result[2],
|
|
129
|
+
reason: result[3],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Release a slot after job completion
|
|
134
|
+
*/
|
|
135
|
+
async releaseSlot(limiterId, jobId, weight, success) {
|
|
136
|
+
const key = this.getKey(limiterId);
|
|
137
|
+
return await this.execScript('release', 1, key, weight, jobId, success ? 1 : 0);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get current distributed state
|
|
141
|
+
*/
|
|
142
|
+
async getState(limiterId) {
|
|
143
|
+
const key = this.getKey(limiterId);
|
|
144
|
+
const result = await this.execScript('getState', 1, key);
|
|
145
|
+
return {
|
|
146
|
+
running: result[0],
|
|
147
|
+
currentWeight: result[1],
|
|
148
|
+
done: result[2],
|
|
149
|
+
failed: result[3],
|
|
150
|
+
reservoir: result[4] === -1 ? null : result[4],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Update reservoir value
|
|
155
|
+
*/
|
|
156
|
+
async updateReservoir(limiterId, value) {
|
|
157
|
+
const key = this.getKey(limiterId);
|
|
158
|
+
return await this.execScript('updateReservoir', 1, key, value);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Increment reservoir value
|
|
162
|
+
*/
|
|
163
|
+
async incrementReservoir(limiterId, amount) {
|
|
164
|
+
const key = this.getKey(limiterId);
|
|
165
|
+
return await this.execScript('incrementReservoir', 1, key, amount);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Clear all state for a limiter
|
|
169
|
+
*/
|
|
170
|
+
async clear(limiterId) {
|
|
171
|
+
const key = this.getKey(limiterId);
|
|
172
|
+
await this.execScript('clearState', 1, key);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Send heartbeat to keep state alive
|
|
176
|
+
*/
|
|
177
|
+
async heartbeat(limiterId, timeout) {
|
|
178
|
+
const key = this.getKey(limiterId);
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
return await this.execScript('heartbeat', 1, key, now, timeout);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Subscribe to reservoir depletion events
|
|
184
|
+
*/
|
|
185
|
+
async subscribeToEvents(limiterId, callback) {
|
|
186
|
+
const channel = `${this.keyPrefix}:${limiterId}:events`;
|
|
187
|
+
// Create a subscriber connection
|
|
188
|
+
const subscriber = this.client.duplicate();
|
|
189
|
+
await subscriber.subscribe(channel);
|
|
190
|
+
subscriber.on('message', (ch, message) => {
|
|
191
|
+
if (ch === channel) {
|
|
192
|
+
try {
|
|
193
|
+
const { event, data } = JSON.parse(message);
|
|
194
|
+
callback(event, data);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Ignore parse errors
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
return () => {
|
|
202
|
+
subscriber.unsubscribe(channel);
|
|
203
|
+
subscriber.quit();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Publish an event
|
|
208
|
+
*/
|
|
209
|
+
async publishEvent(limiterId, event, data) {
|
|
210
|
+
const channel = `${this.keyPrefix}:${limiterId}:events`;
|
|
211
|
+
await this.client.publish(channel, JSON.stringify({ event, data }));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get the underlying Redis client
|
|
215
|
+
*/
|
|
216
|
+
getClient() {
|
|
217
|
+
return this.client;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Check if connected to Redis
|
|
221
|
+
*/
|
|
222
|
+
isConnected() {
|
|
223
|
+
return this.client.status === 'ready';
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Wait for Redis connection
|
|
227
|
+
*/
|
|
228
|
+
async waitForConnection(timeout = 5000) {
|
|
229
|
+
if (this.isConnected())
|
|
230
|
+
return;
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
reject(new Error('Redis connection timeout'));
|
|
234
|
+
}, timeout);
|
|
235
|
+
this.client.once('ready', () => {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
resolve();
|
|
238
|
+
});
|
|
239
|
+
this.client.once('error', (err) => {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
reject(err);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Close the Redis connection
|
|
247
|
+
*/
|
|
248
|
+
async close() {
|
|
249
|
+
if (this.ownClient) {
|
|
250
|
+
await this.client.quit();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
exports.RedisStorage = RedisStorage;
|
|
255
|
+
//# sourceMappingURL=redis-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-storage.js","sourceRoot":"","sources":["../../src/redis/redis-storage.ts"],"names":[],"mappings":"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"}
|