ioredis-toolkit 0.0.1
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 +645 -0
- package/dist/cache.d.ts +298 -0
- package/dist/cache.js +606 -0
- package/dist/client.d.ts +177 -0
- package/dist/client.js +958 -0
- package/dist/cluster-slot.d.ts +4 -0
- package/dist/cluster-slot.js +31 -0
- package/dist/cluster.d.ts +79 -0
- package/dist/cluster.js +156 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +63 -0
- package/dist/health.d.ts +39 -0
- package/dist/health.js +106 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +44 -0
- package/dist/lock.d.ts +215 -0
- package/dist/lock.js +385 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +40 -0
- package/dist/pubsub.d.ts +171 -0
- package/dist/pubsub.js +285 -0
- package/dist/ratelimiter.d.ts +162 -0
- package/dist/ratelimiter.js +289 -0
- package/dist/session/index.d.ts +23 -0
- package/dist/session/index.js +16 -0
- package/dist/session/revocation-store.d.ts +171 -0
- package/dist/session/revocation-store.js +310 -0
- package/dist/session/scripts/cleanup-index.lua +21 -0
- package/dist/session/scripts/conditional-update-encrypted.lua +60 -0
- package/dist/session/scripts/conditional-update.lua +63 -0
- package/dist/session/scripts/create.lua +68 -0
- package/dist/session/scripts/delete-by-user.lua +29 -0
- package/dist/session/scripts/delete.lua +15 -0
- package/dist/session/scripts/enforce-limit.lua +38 -0
- package/dist/session/scripts/revoke.lua +61 -0
- package/dist/session/scripts/rotate-encrypted.lua +107 -0
- package/dist/session/scripts/rotate.lua +119 -0
- package/dist/session/scripts/touch-encrypted.lua +89 -0
- package/dist/session/scripts/touch.lua +72 -0
- package/dist/session/scripts/validate.lua +90 -0
- package/dist/session/session-circuit-breaker.d.ts +42 -0
- package/dist/session/session-circuit-breaker.js +129 -0
- package/dist/session/session-config.d.ts +335 -0
- package/dist/session/session-config.js +162 -0
- package/dist/session/session-cookie.d.ts +72 -0
- package/dist/session/session-cookie.js +101 -0
- package/dist/session/session-encryption.d.ts +87 -0
- package/dist/session/session-encryption.js +139 -0
- package/dist/session/session-errors.d.ts +85 -0
- package/dist/session/session-errors.js +145 -0
- package/dist/session/session-health.d.ts +38 -0
- package/dist/session/session-health.js +60 -0
- package/dist/session/session-keys.d.ts +51 -0
- package/dist/session/session-keys.js +113 -0
- package/dist/session/session-manager.d.ts +59 -0
- package/dist/session/session-manager.js +94 -0
- package/dist/session/session-metrics.d.ts +33 -0
- package/dist/session/session-metrics.js +112 -0
- package/dist/session/session-repository.d.ts +161 -0
- package/dist/session/session-repository.js +683 -0
- package/dist/session/session-scripts.d.ts +36 -0
- package/dist/session/session-scripts.js +130 -0
- package/dist/session/session-serializer.d.ts +42 -0
- package/dist/session/session-serializer.js +248 -0
- package/dist/session/session-service.d.ts +104 -0
- package/dist/session/session-service.js +611 -0
- package/dist/session/session-token.d.ts +38 -0
- package/dist/session/session-token.js +86 -0
- package/dist/session/session-types.d.ts +253 -0
- package/dist/session/session-types.js +16 -0
- package/dist/types.d.ts +782 -0
- package/dist/types.js +140 -0
- package/package.json +97 -0
package/dist/pubsub.d.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { RedisClientWrapper } from './client.js';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { RedisConfig } from './types.js';
|
|
4
|
+
import { LoggerLike } from './logger.js';
|
|
5
|
+
/**
|
|
6
|
+
* Redis Pub/Sub with a dedicated publisher and subscriber connection.
|
|
7
|
+
*
|
|
8
|
+
* Messages are JSON-serialized on publish and auto-parsed on delivery.
|
|
9
|
+
* Extends `EventEmitter` and emits `'error'` on subscriber failures.
|
|
10
|
+
*
|
|
11
|
+
* Works in all three modes (standalone, sentinel, cluster).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const pubsub = new PubSub(client);
|
|
16
|
+
* await pubsub.connectSubscriber(redisConfig);
|
|
17
|
+
*
|
|
18
|
+
* await pubsub.subscribe('orders:created', (message) => {
|
|
19
|
+
* console.log(message); // { id: 1 }
|
|
20
|
+
* });
|
|
21
|
+
* await pubsub.publish('orders:created', { id: 1 });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class PubSub extends EventEmitter {
|
|
25
|
+
private publisher;
|
|
26
|
+
private subscriber;
|
|
27
|
+
private logger;
|
|
28
|
+
private subscriptions;
|
|
29
|
+
private patternSubscriptions;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a pub/sub instance. Publishing works immediately; subscribing
|
|
32
|
+
* requires calling {@link connectSubscriber} first.
|
|
33
|
+
*
|
|
34
|
+
* @param publisher - A {@link RedisClientWrapper} used for publishing.
|
|
35
|
+
* @param logger - Optional pino-compatible logger; defaults to `console`.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const pubsub = new PubSub(client);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
constructor(publisher: RedisClientWrapper, logger?: LoggerLike);
|
|
43
|
+
/**
|
|
44
|
+
* Opens a dedicated subscriber connection.
|
|
45
|
+
*
|
|
46
|
+
* Idempotent: a second call is a no-op while a subscriber is connected.
|
|
47
|
+
* Subscriber errors are emitted as `'error'` events on the instance.
|
|
48
|
+
*
|
|
49
|
+
* @param config - Redis config for the subscriber connection (any mode).
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* await pubsub.connectSubscriber({ mode: 'standalone', host: 'localhost', port: 6379 });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
connectSubscriber(config: RedisConfig): Promise<void>;
|
|
57
|
+
private setupSubscriber;
|
|
58
|
+
private handleMessage;
|
|
59
|
+
private handlePatternMessage;
|
|
60
|
+
/**
|
|
61
|
+
* Publishes a message to a channel.
|
|
62
|
+
*
|
|
63
|
+
* Non-string values are JSON-serialized.
|
|
64
|
+
*
|
|
65
|
+
* @param channel - The channel name.
|
|
66
|
+
* @param message - The message payload (string or any JSON-serializable value).
|
|
67
|
+
* @returns The number of subscribers that received the message.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const receivers = await pubsub.publish('orders:created', { id: 1, total: 99 });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
publish<T = any>(channel: string, message: T): Promise<number>;
|
|
75
|
+
/**
|
|
76
|
+
* Subscribes a handler to a channel.
|
|
77
|
+
*
|
|
78
|
+
* Multiple handlers per channel are supported; the channel is subscribed on
|
|
79
|
+
* Redis only once. Delivered payloads are JSON-parsed when possible.
|
|
80
|
+
*
|
|
81
|
+
* @param channel - The channel name.
|
|
82
|
+
* @param handler - Callback receiving the (parsed) message.
|
|
83
|
+
* @throws `Error` if the subscriber connection is not open.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* await pubsub.subscribe('orders:created', (order) => {
|
|
88
|
+
* console.log(order.id);
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
subscribe<T = any>(channel: string, handler: (data: T) => void): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Removes a handler (or all handlers) from a channel.
|
|
95
|
+
*
|
|
96
|
+
* The Redis subscription is dropped once the last handler for the channel is
|
|
97
|
+
* removed. Without a handler, the whole channel is unsubscribed.
|
|
98
|
+
*
|
|
99
|
+
* @param channel - The channel name.
|
|
100
|
+
* @param handler - Optional specific handler to remove; when omitted all
|
|
101
|
+
* handlers for the channel are removed.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* await pubsub.unsubscribe('orders:created', myHandler);
|
|
106
|
+
* await pubsub.unsubscribe('orders:created'); // remove everything
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
unsubscribe<T = any>(channel: string, handler?: (data: T) => void): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Subscribes a handler to all channels matching a glob pattern.
|
|
112
|
+
*
|
|
113
|
+
* Pattern handlers receive `{ channel, message }` (message JSON-parsed).
|
|
114
|
+
*
|
|
115
|
+
* @param pattern - Glob pattern, e.g. `'orders:*'`.
|
|
116
|
+
* @param handler - Callback receiving `{ channel, message }`.
|
|
117
|
+
* @throws `Error` if the subscriber connection is not open.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* await pubsub.psubscribe('orders:*', ({ channel, message }) => {
|
|
122
|
+
* console.log(channel, message);
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
psubscribe<T = any>(pattern: string, handler: (data: {
|
|
127
|
+
channel: string;
|
|
128
|
+
message: T;
|
|
129
|
+
}) => void): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Removes a handler (or all handlers) from a pattern subscription.
|
|
132
|
+
*
|
|
133
|
+
* @param pattern - The glob pattern.
|
|
134
|
+
* @param handler - Optional specific handler to remove; when omitted all
|
|
135
|
+
* handlers for the pattern are removed.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* await pubsub.punsubscribe('orders:*', myHandler);
|
|
140
|
+
* await pubsub.punsubscribe('orders:*');
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
punsubscribe(pattern: string, handler?: (data: any) => void): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Closes the subscriber connection and clears all subscriptions.
|
|
146
|
+
*
|
|
147
|
+
* The publisher client is not closed (it is owned by the caller).
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* await pubsub.close();
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
close(): Promise<void>;
|
|
155
|
+
/**
|
|
156
|
+
* Returns subscription statistics.
|
|
157
|
+
*
|
|
158
|
+
* @returns `{ subscriptions, patternSubscriptions, connected }`.
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* const stats = pubsub.getStats();
|
|
163
|
+
* // { subscriptions: 2, patternSubscriptions: 1, connected: true }
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
getStats(): {
|
|
167
|
+
subscriptions: number;
|
|
168
|
+
patternSubscriptions: number;
|
|
169
|
+
connected: boolean;
|
|
170
|
+
};
|
|
171
|
+
}
|
package/dist/pubsub.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { RedisClientWrapper } from './client.js';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { defaultLogger } from './logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Redis Pub/Sub with a dedicated publisher and subscriber connection.
|
|
6
|
+
*
|
|
7
|
+
* Messages are JSON-serialized on publish and auto-parsed on delivery.
|
|
8
|
+
* Extends `EventEmitter` and emits `'error'` on subscriber failures.
|
|
9
|
+
*
|
|
10
|
+
* Works in all three modes (standalone, sentinel, cluster).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const pubsub = new PubSub(client);
|
|
15
|
+
* await pubsub.connectSubscriber(redisConfig);
|
|
16
|
+
*
|
|
17
|
+
* await pubsub.subscribe('orders:created', (message) => {
|
|
18
|
+
* console.log(message); // { id: 1 }
|
|
19
|
+
* });
|
|
20
|
+
* await pubsub.publish('orders:created', { id: 1 });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export class PubSub extends EventEmitter {
|
|
24
|
+
publisher;
|
|
25
|
+
subscriber = null;
|
|
26
|
+
logger;
|
|
27
|
+
subscriptions = new Map();
|
|
28
|
+
patternSubscriptions = new Map();
|
|
29
|
+
/**
|
|
30
|
+
* Creates a pub/sub instance. Publishing works immediately; subscribing
|
|
31
|
+
* requires calling {@link connectSubscriber} first.
|
|
32
|
+
*
|
|
33
|
+
* @param publisher - A {@link RedisClientWrapper} used for publishing.
|
|
34
|
+
* @param logger - Optional pino-compatible logger; defaults to `console`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const pubsub = new PubSub(client);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
constructor(publisher, logger = defaultLogger) {
|
|
42
|
+
super();
|
|
43
|
+
this.publisher = publisher;
|
|
44
|
+
this.logger = logger.child({ component: 'PubSub' });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Opens a dedicated subscriber connection.
|
|
48
|
+
*
|
|
49
|
+
* Idempotent: a second call is a no-op while a subscriber is connected.
|
|
50
|
+
* Subscriber errors are emitted as `'error'` events on the instance.
|
|
51
|
+
*
|
|
52
|
+
* @param config - Redis config for the subscriber connection (any mode).
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* await pubsub.connectSubscriber({ mode: 'standalone', host: 'localhost', port: 6379 });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
async connectSubscriber(config) {
|
|
60
|
+
if (this.subscriber)
|
|
61
|
+
return;
|
|
62
|
+
this.subscriber = new RedisClientWrapper(config, this.logger);
|
|
63
|
+
this.setupSubscriber();
|
|
64
|
+
}
|
|
65
|
+
setupSubscriber() {
|
|
66
|
+
if (!this.subscriber)
|
|
67
|
+
return;
|
|
68
|
+
const raw = this.subscriber.raw;
|
|
69
|
+
raw.on('message', (channel, message) => {
|
|
70
|
+
this.handleMessage(channel, message);
|
|
71
|
+
});
|
|
72
|
+
raw.on('pmessage', (pattern, channel, message) => {
|
|
73
|
+
this.handlePatternMessage(pattern, channel, message);
|
|
74
|
+
});
|
|
75
|
+
raw.on('error', (error) => {
|
|
76
|
+
this.logger.error('Subscriber error:', error);
|
|
77
|
+
this.emit('error', error);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
handleMessage(channel, message) {
|
|
81
|
+
const handlers = this.subscriptions.get(channel);
|
|
82
|
+
if (!handlers)
|
|
83
|
+
return;
|
|
84
|
+
let parsed = message;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(message);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
for (const handler of handlers) {
|
|
90
|
+
try {
|
|
91
|
+
handler(parsed);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.logger.error('Handler error:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
handlePatternMessage(pattern, channel, message) {
|
|
99
|
+
const handlers = this.patternSubscriptions.get(pattern);
|
|
100
|
+
if (!handlers)
|
|
101
|
+
return;
|
|
102
|
+
let parsed = message;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(message);
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
for (const handler of handlers) {
|
|
108
|
+
try {
|
|
109
|
+
handler({ channel, message: parsed });
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.logger.error('Handler error:', error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Publishes a message to a channel.
|
|
118
|
+
*
|
|
119
|
+
* Non-string values are JSON-serialized.
|
|
120
|
+
*
|
|
121
|
+
* @param channel - The channel name.
|
|
122
|
+
* @param message - The message payload (string or any JSON-serializable value).
|
|
123
|
+
* @returns The number of subscribers that received the message.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const receivers = await pubsub.publish('orders:created', { id: 1, total: 99 });
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
async publish(channel, message) {
|
|
131
|
+
const raw = typeof message === 'string' ? message : JSON.stringify(message);
|
|
132
|
+
return this.publisher.raw.publish(channel, raw);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Subscribes a handler to a channel.
|
|
136
|
+
*
|
|
137
|
+
* Multiple handlers per channel are supported; the channel is subscribed on
|
|
138
|
+
* Redis only once. Delivered payloads are JSON-parsed when possible.
|
|
139
|
+
*
|
|
140
|
+
* @param channel - The channel name.
|
|
141
|
+
* @param handler - Callback receiving the (parsed) message.
|
|
142
|
+
* @throws `Error` if the subscriber connection is not open.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* await pubsub.subscribe('orders:created', (order) => {
|
|
147
|
+
* console.log(order.id);
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
async subscribe(channel, handler) {
|
|
152
|
+
if (!this.subscriber) {
|
|
153
|
+
throw new Error('Subscriber not connected');
|
|
154
|
+
}
|
|
155
|
+
if (!this.subscriptions.has(channel)) {
|
|
156
|
+
this.subscriptions.set(channel, new Set());
|
|
157
|
+
await this.subscriber.raw.subscribe(channel);
|
|
158
|
+
}
|
|
159
|
+
this.subscriptions.get(channel).add(handler);
|
|
160
|
+
this.logger.debug('Subscribed to channel', { channel });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Removes a handler (or all handlers) from a channel.
|
|
164
|
+
*
|
|
165
|
+
* The Redis subscription is dropped once the last handler for the channel is
|
|
166
|
+
* removed. Without a handler, the whole channel is unsubscribed.
|
|
167
|
+
*
|
|
168
|
+
* @param channel - The channel name.
|
|
169
|
+
* @param handler - Optional specific handler to remove; when omitted all
|
|
170
|
+
* handlers for the channel are removed.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```ts
|
|
174
|
+
* await pubsub.unsubscribe('orders:created', myHandler);
|
|
175
|
+
* await pubsub.unsubscribe('orders:created'); // remove everything
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
async unsubscribe(channel, handler) {
|
|
179
|
+
if (!this.subscriber)
|
|
180
|
+
return;
|
|
181
|
+
if (handler && this.subscriptions.has(channel)) {
|
|
182
|
+
const handlers = this.subscriptions.get(channel);
|
|
183
|
+
handlers.delete(handler);
|
|
184
|
+
if (handlers.size === 0) {
|
|
185
|
+
this.subscriptions.delete(channel);
|
|
186
|
+
await this.subscriber.raw.unsubscribe(channel);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.subscriptions.delete(channel);
|
|
191
|
+
await this.subscriber.raw.unsubscribe(channel);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Subscribes a handler to all channels matching a glob pattern.
|
|
196
|
+
*
|
|
197
|
+
* Pattern handlers receive `{ channel, message }` (message JSON-parsed).
|
|
198
|
+
*
|
|
199
|
+
* @param pattern - Glob pattern, e.g. `'orders:*'`.
|
|
200
|
+
* @param handler - Callback receiving `{ channel, message }`.
|
|
201
|
+
* @throws `Error` if the subscriber connection is not open.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* await pubsub.psubscribe('orders:*', ({ channel, message }) => {
|
|
206
|
+
* console.log(channel, message);
|
|
207
|
+
* });
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
async psubscribe(pattern, handler) {
|
|
211
|
+
if (!this.subscriber) {
|
|
212
|
+
throw new Error('Subscriber not connected');
|
|
213
|
+
}
|
|
214
|
+
if (!this.patternSubscriptions.has(pattern)) {
|
|
215
|
+
this.patternSubscriptions.set(pattern, new Set());
|
|
216
|
+
await this.subscriber.raw.psubscribe(pattern);
|
|
217
|
+
}
|
|
218
|
+
this.patternSubscriptions.get(pattern).add(handler);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Removes a handler (or all handlers) from a pattern subscription.
|
|
222
|
+
*
|
|
223
|
+
* @param pattern - The glob pattern.
|
|
224
|
+
* @param handler - Optional specific handler to remove; when omitted all
|
|
225
|
+
* handlers for the pattern are removed.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* await pubsub.punsubscribe('orders:*', myHandler);
|
|
230
|
+
* await pubsub.punsubscribe('orders:*');
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
async punsubscribe(pattern, handler) {
|
|
234
|
+
if (!this.subscriber)
|
|
235
|
+
return;
|
|
236
|
+
if (handler && this.patternSubscriptions.has(pattern)) {
|
|
237
|
+
const handlers = this.patternSubscriptions.get(pattern);
|
|
238
|
+
handlers.delete(handler);
|
|
239
|
+
if (handlers.size === 0) {
|
|
240
|
+
this.patternSubscriptions.delete(pattern);
|
|
241
|
+
await this.subscriber.raw.punsubscribe(pattern);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.patternSubscriptions.delete(pattern);
|
|
246
|
+
await this.subscriber.raw.punsubscribe(pattern);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Closes the subscriber connection and clears all subscriptions.
|
|
251
|
+
*
|
|
252
|
+
* The publisher client is not closed (it is owned by the caller).
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```ts
|
|
256
|
+
* await pubsub.close();
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
async close() {
|
|
260
|
+
if (this.subscriber) {
|
|
261
|
+
await this.subscriber.close();
|
|
262
|
+
this.subscriber = null;
|
|
263
|
+
}
|
|
264
|
+
this.subscriptions.clear();
|
|
265
|
+
this.patternSubscriptions.clear();
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Returns subscription statistics.
|
|
269
|
+
*
|
|
270
|
+
* @returns `{ subscriptions, patternSubscriptions, connected }`.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```ts
|
|
274
|
+
* const stats = pubsub.getStats();
|
|
275
|
+
* // { subscriptions: 2, patternSubscriptions: 1, connected: true }
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
getStats() {
|
|
279
|
+
return {
|
|
280
|
+
subscriptions: this.subscriptions.size,
|
|
281
|
+
patternSubscriptions: this.patternSubscriptions.size,
|
|
282
|
+
connected: this.subscriber !== null,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { RedisClientWrapper } from './client.js';
|
|
2
|
+
import { LoggerLike } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Window algorithm used by the rate limiter.
|
|
5
|
+
* - `fixed` - fixed window via `INCR`/`EXPIRE` (simple, cheapest)
|
|
6
|
+
* - `sliding` - sliding window via an atomic Lua script over a sorted set (smoothest)
|
|
7
|
+
*/
|
|
8
|
+
export type RateLimitAlgorithm = 'fixed' | 'sliding';
|
|
9
|
+
/**
|
|
10
|
+
* Options for a rate limiter instance or an individual call.
|
|
11
|
+
*/
|
|
12
|
+
export interface RateLimitOptions {
|
|
13
|
+
/** Maximum allowed requests within `duration`. Default: `100`. */
|
|
14
|
+
limit?: number;
|
|
15
|
+
/** Window length in seconds. Default: `60`. */
|
|
16
|
+
duration?: number;
|
|
17
|
+
/** Window algorithm. Default: `'sliding'`. */
|
|
18
|
+
algorithm?: RateLimitAlgorithm;
|
|
19
|
+
/** Redis key prefix. Default: `'ratelimit'`. */
|
|
20
|
+
namespace?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Result of a rate limit `consume`/`check` call.
|
|
24
|
+
*/
|
|
25
|
+
export interface RateLimitResult {
|
|
26
|
+
/** `true` when the request is within the limit. */
|
|
27
|
+
allowed: boolean;
|
|
28
|
+
/** The configured maximum within the window. */
|
|
29
|
+
limit: number;
|
|
30
|
+
/** Requests already counted in the current window. */
|
|
31
|
+
used: number;
|
|
32
|
+
/** Requests still available (`limit - used`, floored at `0`). */
|
|
33
|
+
remaining: number;
|
|
34
|
+
/** Epoch milliseconds when the window resets (0 retry hint when allowed). */
|
|
35
|
+
resetAt: number;
|
|
36
|
+
/** Seconds to wait before retrying; `0` when allowed. */
|
|
37
|
+
retryAfter: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Generic Redis-backed rate limiter that works for any resource: routes, API
|
|
41
|
+
* endpoints, users, IPs, databases, email sending, etc.
|
|
42
|
+
*
|
|
43
|
+
* Keys are namespaced as `ratelimit:{namespace}:{resource}:{identifier}` so each
|
|
44
|
+
* resource + identifier combination is tracked independently. Supports fixed-window
|
|
45
|
+
* (`INCR`/`EXPIRE`) and sliding-window (atomic Lua over a sorted set) algorithms.
|
|
46
|
+
* Fails open when Redis is unavailable.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const limiter = new RateLimiter(client, { limit: 100, duration: 60 });
|
|
51
|
+
*
|
|
52
|
+
* const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
|
|
53
|
+
* if (!result.allowed) {
|
|
54
|
+
* throw new Error(`Slow down, retry in ${result.retryAfter}s`);
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare class RateLimiter {
|
|
59
|
+
private client;
|
|
60
|
+
private logger;
|
|
61
|
+
private defaultLimit;
|
|
62
|
+
private defaultDuration;
|
|
63
|
+
private defaultAlgorithm;
|
|
64
|
+
private defaultNamespace;
|
|
65
|
+
/**
|
|
66
|
+
* Creates a rate limiter bound to a Redis client.
|
|
67
|
+
*
|
|
68
|
+
* @param client - The underlying {@link RedisClientWrapper}.
|
|
69
|
+
* @param options - Defaults applied when a call does not override them:
|
|
70
|
+
* `limit` (default `100`), `duration` in seconds (default `60`),
|
|
71
|
+
* `algorithm` (default `'sliding'`), `namespace` (default `'ratelimit'`).
|
|
72
|
+
* @param logger - Optional pino-compatible logger; defaults to `console`.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const limiter = new RateLimiter(client, { limit: 10, duration: 1, algorithm: 'fixed' });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
constructor(client: RedisClientWrapper, options?: RateLimitOptions, logger?: LoggerLike);
|
|
80
|
+
/**
|
|
81
|
+
* Builds the Redis key for a resource + identifier combination.
|
|
82
|
+
*
|
|
83
|
+
* @param resource - The rate-limited resource, e.g. a route `'/api/login'` or
|
|
84
|
+
* a resource name `'email:send'`.
|
|
85
|
+
* @param identifier - The caller identity, e.g. an IP, user id or API key.
|
|
86
|
+
* @param namespace - Key prefix (defaults to the limiter's namespace).
|
|
87
|
+
* @returns The full key, e.g. `'ratelimit:/api/login:ip-10.0.0.1'`.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* limiter.makeKey('/api/login', 'ip-10.0.0.1');
|
|
92
|
+
* // 'ratelimit:/api/login:ip-10.0.0.1'
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
makeKey(resource: string, identifier: string, namespace?: string): string;
|
|
96
|
+
/**
|
|
97
|
+
* Consumes one unit of capacity for a resource + identifier and returns the
|
|
98
|
+
* resulting limit state.
|
|
99
|
+
*
|
|
100
|
+
* When the limit is reached the request is not recorded and `allowed` is
|
|
101
|
+
* `false` with `retryAfter` (seconds) and `resetAt` (epoch ms) hints.
|
|
102
|
+
* Fails open (allows the request) if Redis errors.
|
|
103
|
+
*
|
|
104
|
+
* @param resource - The rate-limited resource, e.g. a route `'/api/login'` or
|
|
105
|
+
* a resource name `'db:write'`.
|
|
106
|
+
* @param identifier - The caller identity, e.g. an IP, user id or API key.
|
|
107
|
+
* @param options - Per-call overrides for `limit`, `duration`, `algorithm`,
|
|
108
|
+
* and `namespace`.
|
|
109
|
+
* @returns The limit state: `allowed`, `limit`, `used`, `remaining`,
|
|
110
|
+
* `resetAt` (epoch ms), `retryAfter` (seconds).
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const result = await limiter.consume('/api/orders', 'user-7', { limit: 5, duration: 60 });
|
|
115
|
+
* if (!result.allowed) {
|
|
116
|
+
* res.setHeader('Retry-After', String(result.retryAfter));
|
|
117
|
+
* return res.status(429).json({ error: 'Too many requests' });
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
consume(resource: string, identifier: string, options?: RateLimitOptions): Promise<RateLimitResult>;
|
|
122
|
+
/**
|
|
123
|
+
* Peeks at the current limit state without consuming capacity.
|
|
124
|
+
*
|
|
125
|
+
* Useful for pre-flight checks (e.g. showing "limit reached" in a UI before
|
|
126
|
+
* the actual request). Also fails open on Redis errors.
|
|
127
|
+
*
|
|
128
|
+
* @param resource - The rate-limited resource.
|
|
129
|
+
* @param identifier - The caller identity.
|
|
130
|
+
* @param options - Per-call overrides for `limit`, `duration`, `algorithm`,
|
|
131
|
+
* and `namespace`.
|
|
132
|
+
* @returns The current limit state; `used` is not incremented.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const state = await limiter.check('/api/search', 'user-1');
|
|
137
|
+
* if (state.remaining === 0) {
|
|
138
|
+
* // disable the search button
|
|
139
|
+
* }
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
check(resource: string, identifier: string, options?: RateLimitOptions): Promise<RateLimitResult>;
|
|
143
|
+
/**
|
|
144
|
+
* Resets the counter for a resource + identifier, granting full capacity again.
|
|
145
|
+
*
|
|
146
|
+
* @param resource - The rate-limited resource.
|
|
147
|
+
* @param identifier - The caller identity.
|
|
148
|
+
* @param namespace - Key prefix (defaults to the limiter's namespace).
|
|
149
|
+
* @returns `true` if a counter existed and was removed.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* // user upgraded to a premium plan, lift their limits
|
|
154
|
+
* await limiter.reset('/api/export', 'user-7');
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
reset(resource: string, identifier: string, namespace?: string): Promise<boolean>;
|
|
158
|
+
private consumeFixed;
|
|
159
|
+
private consumeSliding;
|
|
160
|
+
private checkFixed;
|
|
161
|
+
private checkSliding;
|
|
162
|
+
}
|