@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +490 -0
  3. package/dist/event-emitter.d.ts +32 -0
  4. package/dist/event-emitter.d.ts.map +1 -0
  5. package/dist/event-emitter.js +72 -0
  6. package/dist/event-emitter.js.map +1 -0
  7. package/dist/index.d.ts +47 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +59 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/priority-queue.d.ts +66 -0
  12. package/dist/priority-queue.d.ts.map +1 -0
  13. package/dist/priority-queue.js +147 -0
  14. package/dist/priority-queue.js.map +1 -0
  15. package/dist/rate-limiter.d.ts +135 -0
  16. package/dist/rate-limiter.d.ts.map +1 -0
  17. package/dist/rate-limiter.js +455 -0
  18. package/dist/rate-limiter.js.map +1 -0
  19. package/dist/redis/distributed-rate-limiter.d.ts +149 -0
  20. package/dist/redis/distributed-rate-limiter.d.ts.map +1 -0
  21. package/dist/redis/distributed-rate-limiter.js +423 -0
  22. package/dist/redis/distributed-rate-limiter.js.map +1 -0
  23. package/dist/redis/index.d.ts +8 -0
  24. package/dist/redis/index.d.ts.map +1 -0
  25. package/dist/redis/index.js +11 -0
  26. package/dist/redis/index.js.map +1 -0
  27. package/dist/redis/lua-scripts.d.ts +62 -0
  28. package/dist/redis/lua-scripts.d.ts.map +1 -0
  29. package/dist/redis/lua-scripts.js +229 -0
  30. package/dist/redis/lua-scripts.js.map +1 -0
  31. package/dist/redis/redis-storage.d.ts +134 -0
  32. package/dist/redis/redis-storage.d.ts.map +1 -0
  33. package/dist/redis/redis-storage.js +255 -0
  34. package/dist/redis/redis-storage.js.map +1 -0
  35. package/dist/types.d.ts +207 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +15 -0
  38. package/dist/types.js.map +1 -0
  39. 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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"}