@mhbdev/redis 0.2.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/bullmq/adapter.d.ts +49 -0
- package/dist/bullmq/adapter.d.ts.map +1 -0
- package/dist/bullmq/adapter.js +56 -0
- package/dist/bullmq/adapter.js.map +1 -0
- package/dist/bullmq/index.d.ts +3 -0
- package/dist/bullmq/index.d.ts.map +1 -0
- package/dist/bullmq/index.js +2 -0
- package/dist/bullmq/index.js.map +1 -0
- package/dist/cache/cache.d.ts +31 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +78 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +2 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/core/codec.d.ts +7 -0
- package/dist/core/codec.d.ts.map +1 -0
- package/dist/core/codec.js +24 -0
- package/dist/core/codec.js.map +1 -0
- package/dist/core/errors.d.ts +43 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +84 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/executor.d.ts +28 -0
- package/dist/core/executor.d.ts.map +1 -0
- package/dist/core/executor.js +27 -0
- package/dist/core/executor.js.map +1 -0
- package/dist/core/keys.d.ts +4 -0
- package/dist/core/keys.d.ts.map +1 -0
- package/dist/core/keys.js +15 -0
- package/dist/core/keys.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/ioredis/executor.d.ts +30 -0
- package/dist/ioredis/executor.d.ts.map +1 -0
- package/dist/ioredis/executor.js +52 -0
- package/dist/ioredis/executor.js.map +1 -0
- package/dist/ioredis/index.d.ts +5 -0
- package/dist/ioredis/index.d.ts.map +1 -0
- package/dist/ioredis/index.js +3 -0
- package/dist/ioredis/index.js.map +1 -0
- package/dist/ioredis/pubsub.d.ts +16 -0
- package/dist/ioredis/pubsub.d.ts.map +1 -0
- package/dist/ioredis/pubsub.js +42 -0
- package/dist/ioredis/pubsub.js.map +1 -0
- package/dist/locks/index.d.ts +3 -0
- package/dist/locks/index.d.ts.map +1 -0
- package/dist/locks/index.js +2 -0
- package/dist/locks/index.js.map +1 -0
- package/dist/locks/locks.d.ts +25 -0
- package/dist/locks/locks.d.ts.map +1 -0
- package/dist/locks/locks.js +77 -0
- package/dist/locks/locks.js.map +1 -0
- package/dist/node/client.d.ts +47 -0
- package/dist/node/client.d.ts.map +1 -0
- package/dist/node/client.js +155 -0
- package/dist/node/client.js.map +1 -0
- package/dist/node/errors.d.ts +3 -0
- package/dist/node/errors.d.ts.map +1 -0
- package/dist/node/errors.js +16 -0
- package/dist/node/errors.js.map +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +4 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/pool.d.ts +31 -0
- package/dist/node/pool.d.ts.map +1 -0
- package/dist/node/pool.js +131 -0
- package/dist/node/pool.js.map +1 -0
- package/dist/node/pubsub.d.ts +16 -0
- package/dist/node/pubsub.d.ts.map +1 -0
- package/dist/node/pubsub.js +41 -0
- package/dist/node/pubsub.js.map +1 -0
- package/dist/pubsub/contracts.d.ts +6 -0
- package/dist/pubsub/contracts.d.ts.map +1 -0
- package/dist/pubsub/contracts.js +2 -0
- package/dist/pubsub/contracts.js.map +1 -0
- package/dist/pubsub/index.d.ts +3 -0
- package/dist/pubsub/index.d.ts.map +1 -0
- package/dist/pubsub/index.js +2 -0
- package/dist/pubsub/index.js.map +1 -0
- package/dist/pubsub/pubsub.d.ts +15 -0
- package/dist/pubsub/pubsub.d.ts.map +1 -0
- package/dist/pubsub/pubsub.js +62 -0
- package/dist/pubsub/pubsub.js.map +1 -0
- package/dist/queue/index.d.ts +3 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +2 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/queue.d.ts +78 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +260 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/rate-limit/contracts.d.ts +93 -0
- package/dist/rate-limit/contracts.d.ts.map +1 -0
- package/dist/rate-limit/contracts.js +304 -0
- package/dist/rate-limit/contracts.js.map +1 -0
- package/dist/rate-limit/index.d.ts +3 -0
- package/dist/rate-limit/index.d.ts.map +1 -0
- package/dist/rate-limit/index.js +2 -0
- package/dist/rate-limit/index.js.map +1 -0
- package/dist/testing/fake-executor.d.ts +9 -0
- package/dist/testing/fake-executor.d.ts.map +1 -0
- package/dist/testing/fake-executor.js +15 -0
- package/dist/testing/fake-executor.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -0
- package/docs/adapters.md +41 -0
- package/docs/architecture.md +41 -0
- package/docs/errors.md +27 -0
- package/docs/migration.md +21 -0
- package/docs/queues.md +13 -0
- package/docs/rate-limiting.md +23 -0
- package/docs/releasing.md +45 -0
- package/package.json +135 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { RedisClientStateError, RedisConfigurationError, RedisConnectionError, RedisProtocolError, RedisTimeoutError, RedisUnavailableError, } from "../core/errors.js";
|
|
2
|
+
import { withTimeout } from "../core/executor.js";
|
|
3
|
+
import { assertKeyPart, hashIdentifier } from "../core/keys.js";
|
|
4
|
+
export function fixedWindow(limit, windowMs) {
|
|
5
|
+
assertAlgorithmValues(limit, windowMs);
|
|
6
|
+
return { kind: "fixed-window", limit, windowMs };
|
|
7
|
+
}
|
|
8
|
+
export function slidingWindow(limit, windowMs) {
|
|
9
|
+
assertAlgorithmValues(limit, windowMs);
|
|
10
|
+
return { kind: "sliding-window", limit, windowMs };
|
|
11
|
+
}
|
|
12
|
+
export function tokenBucket(refillRate, windowMs, maxTokens) {
|
|
13
|
+
assertAlgorithmValues(refillRate, windowMs);
|
|
14
|
+
if (!Number.isSafeInteger(maxTokens) || maxTokens <= 0)
|
|
15
|
+
throw new RedisConfigurationError("Token bucket maximum must be positive");
|
|
16
|
+
return { kind: "token-bucket", limit: maxTokens, windowMs, maxTokens, refillRate };
|
|
17
|
+
}
|
|
18
|
+
export function createRateLimiter(algorithm, options) {
|
|
19
|
+
return new RedisRateLimiter(algorithm, options);
|
|
20
|
+
}
|
|
21
|
+
/** A discoverable alias for applications that prefer Redis-prefixed factory names. */
|
|
22
|
+
export const createRedisRateLimiter = createRateLimiter;
|
|
23
|
+
export class RedisRateLimiter {
|
|
24
|
+
algorithm;
|
|
25
|
+
options;
|
|
26
|
+
prefix;
|
|
27
|
+
timeoutMs;
|
|
28
|
+
failureMode;
|
|
29
|
+
cache;
|
|
30
|
+
now;
|
|
31
|
+
constructor(algorithm, options) {
|
|
32
|
+
this.algorithm = algorithm;
|
|
33
|
+
this.options = options;
|
|
34
|
+
this.prefix = options.prefix ?? "mhbdev:rate-limit";
|
|
35
|
+
this.timeoutMs = options.timeoutMs ?? 3_000;
|
|
36
|
+
this.failureMode = options.failureMode ?? "closed";
|
|
37
|
+
this.cache =
|
|
38
|
+
options.ephemeralCache === false
|
|
39
|
+
? null
|
|
40
|
+
: options.ephemeralCache instanceof Map
|
|
41
|
+
? options.ephemeralCache
|
|
42
|
+
: new Map();
|
|
43
|
+
this.now = options.now ?? Date.now;
|
|
44
|
+
if (!this.prefix.trim())
|
|
45
|
+
throw new RedisConfigurationError("Rate-limit prefix is required");
|
|
46
|
+
}
|
|
47
|
+
async limit(identifier, request = {}) {
|
|
48
|
+
assertIdentifier(identifier);
|
|
49
|
+
const weight = request.rate ?? 1;
|
|
50
|
+
if (!Number.isSafeInteger(weight) || weight <= 0)
|
|
51
|
+
throw new RedisConfigurationError("Rate-limit request rate must be positive");
|
|
52
|
+
const cachedUntil = this.cache?.get(identifier);
|
|
53
|
+
if (cachedUntil !== undefined) {
|
|
54
|
+
if (cachedUntil > this.now())
|
|
55
|
+
return this.response(false, this.algorithm.limit, 0, cachedUntil, "cache");
|
|
56
|
+
this.cache?.delete(identifier);
|
|
57
|
+
}
|
|
58
|
+
const started = this.now();
|
|
59
|
+
try {
|
|
60
|
+
const dynamicLimit = this.options.dynamicLimits
|
|
61
|
+
? await this.getDynamicLimit()
|
|
62
|
+
: null;
|
|
63
|
+
const result = await withTimeout(() => this.runAlgorithm(identifier, dynamicLimit ?? this.algorithm.limit, weight, started), this.timeoutMs);
|
|
64
|
+
if (!result.allowed && this.cache)
|
|
65
|
+
this.cache.set(identifier, result.reset);
|
|
66
|
+
const response = this.response(result.allowed, result.limit, result.remaining, result.reset, result.allowed ? "success" : "limit");
|
|
67
|
+
const pending = this.options.analytics?.record({
|
|
68
|
+
identifier,
|
|
69
|
+
algorithm: this.algorithm.kind,
|
|
70
|
+
allowed: result.allowed,
|
|
71
|
+
limit: result.limit,
|
|
72
|
+
remaining: result.remaining,
|
|
73
|
+
timestamp: started,
|
|
74
|
+
});
|
|
75
|
+
return { ...response, pending: Promise.resolve(pending) };
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (this.failureMode === "open" && isAvailabilityFailure(error))
|
|
79
|
+
return this.response(true, this.algorithm.limit, this.algorithm.limit, started + this.algorithm.windowMs, error instanceof RedisTimeoutError ? "timeout" : "redis");
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async check(identity, policy) {
|
|
84
|
+
assertKeyPart(policy.name, "Rate-limit policy name");
|
|
85
|
+
const limiter = new RedisRateLimiter(fixedWindow(policy.maxRequests, policy.windowMs), {
|
|
86
|
+
...this.options,
|
|
87
|
+
prefix: `${this.prefix}:${policy.name}`,
|
|
88
|
+
failureMode: policy.failureMode,
|
|
89
|
+
});
|
|
90
|
+
const result = await limiter.limit(identity);
|
|
91
|
+
return {
|
|
92
|
+
allowed: result.allowed,
|
|
93
|
+
limit: result.limit,
|
|
94
|
+
remaining: result.remaining,
|
|
95
|
+
resetAt: result.resetAt,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async getRemaining(identifier) {
|
|
99
|
+
assertIdentifier(identifier);
|
|
100
|
+
const now = this.now();
|
|
101
|
+
const digest = await hashIdentifier(identifier);
|
|
102
|
+
const limit = this.options.dynamicLimits
|
|
103
|
+
? ((await this.getDynamicLimit()) ?? this.algorithm.limit)
|
|
104
|
+
: this.algorithm.limit;
|
|
105
|
+
if (this.algorithm.kind === "fixed-window") {
|
|
106
|
+
const start = Math.floor(now / this.algorithm.windowMs) * this.algorithm.windowMs;
|
|
107
|
+
const count = Number((await this.options.redis.execute([
|
|
108
|
+
"GET",
|
|
109
|
+
`${this.prefix}:${digest}:${start}`,
|
|
110
|
+
])) ?? 0);
|
|
111
|
+
return {
|
|
112
|
+
remaining: Math.max(0, limit - count),
|
|
113
|
+
reset: start + this.algorithm.windowMs,
|
|
114
|
+
resetAt: new Date(start + this.algorithm.windowMs),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (this.algorithm.kind === "sliding-window") {
|
|
118
|
+
const start = Math.floor(now / this.algorithm.windowMs) * this.algorithm.windowMs;
|
|
119
|
+
const values = await this.options.redis.execute([
|
|
120
|
+
"MGET",
|
|
121
|
+
`${this.prefix}:${digest}:${start}`,
|
|
122
|
+
`${this.prefix}:${digest}:${start - this.algorithm.windowMs}`,
|
|
123
|
+
]);
|
|
124
|
+
const current = Number(values?.[0] ?? 0);
|
|
125
|
+
const previous = Number(values?.[1] ?? 0);
|
|
126
|
+
const weighted = previous * ((this.algorithm.windowMs - (now - start)) / this.algorithm.windowMs) +
|
|
127
|
+
current;
|
|
128
|
+
return {
|
|
129
|
+
remaining: Math.max(0, Math.floor(limit - weighted)),
|
|
130
|
+
reset: start + this.algorithm.windowMs,
|
|
131
|
+
resetAt: new Date(start + this.algorithm.windowMs),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const values = await this.options.redis.execute([
|
|
135
|
+
"HMGET",
|
|
136
|
+
`${this.prefix}:bucket:${digest}`,
|
|
137
|
+
"tokens",
|
|
138
|
+
"updated",
|
|
139
|
+
]);
|
|
140
|
+
const tokens = Number(values?.[0] ?? limit);
|
|
141
|
+
const updated = Number(values?.[1] ?? now);
|
|
142
|
+
const refilled = Math.min(limit, tokens +
|
|
143
|
+
(Math.max(0, now - updated) * (this.algorithm.refillRate ?? 0)) /
|
|
144
|
+
this.algorithm.windowMs);
|
|
145
|
+
const reset = now +
|
|
146
|
+
Math.max(0, Math.ceil(((1 - refilled) * this.algorithm.windowMs) / (this.algorithm.refillRate ?? 1)));
|
|
147
|
+
return { remaining: Math.floor(refilled), reset, resetAt: new Date(reset) };
|
|
148
|
+
}
|
|
149
|
+
async reset(identifier) {
|
|
150
|
+
assertIdentifier(identifier);
|
|
151
|
+
const keys = await this.keysFor(identifier, this.now());
|
|
152
|
+
if (keys.length)
|
|
153
|
+
await this.options.redis.execute(["DEL", ...keys]);
|
|
154
|
+
this.cache?.delete(identifier);
|
|
155
|
+
}
|
|
156
|
+
async blockUntilReady(identifier, timeoutMs, request = {}) {
|
|
157
|
+
const deadline = this.now() + timeoutMs;
|
|
158
|
+
while (this.now() <= deadline) {
|
|
159
|
+
const result = await this.limit(identifier, request);
|
|
160
|
+
if (result.success)
|
|
161
|
+
return result;
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(Math.max(result.retryAfterMs, 10), Math.max(10, deadline - this.now()))));
|
|
163
|
+
}
|
|
164
|
+
return this.limit(identifier, request);
|
|
165
|
+
}
|
|
166
|
+
async setDynamicLimit(limit) {
|
|
167
|
+
if (!this.options.dynamicLimits)
|
|
168
|
+
throw new RedisConfigurationError("Dynamic limits are not enabled");
|
|
169
|
+
const key = `${this.prefix}:dynamic-limit`;
|
|
170
|
+
if (limit === false)
|
|
171
|
+
await this.options.redis.execute(["DEL", key]);
|
|
172
|
+
else {
|
|
173
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
174
|
+
throw new RedisConfigurationError("Dynamic limit must be positive");
|
|
175
|
+
await this.options.redis.execute(["SET", key, String(limit)]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async getDynamicLimit() {
|
|
179
|
+
if (!this.options.dynamicLimits)
|
|
180
|
+
return null;
|
|
181
|
+
const value = await this.options.redis.execute([
|
|
182
|
+
"GET",
|
|
183
|
+
`${this.prefix}:dynamic-limit`,
|
|
184
|
+
]);
|
|
185
|
+
if (value === null)
|
|
186
|
+
return null;
|
|
187
|
+
const limit = Number(value);
|
|
188
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
189
|
+
throw new RedisProtocolError("Invalid dynamic rate-limit response");
|
|
190
|
+
return limit;
|
|
191
|
+
}
|
|
192
|
+
async runAlgorithm(identifier, limit, weight, now) {
|
|
193
|
+
const digest = await hashIdentifier(identifier);
|
|
194
|
+
if (this.algorithm.kind === "fixed-window") {
|
|
195
|
+
const start = Math.floor(now / this.algorithm.windowMs) * this.algorithm.windowMs;
|
|
196
|
+
const reset = start + this.algorithm.windowMs;
|
|
197
|
+
const value = await this.options.redis.eval(FIXED_WINDOW_SCRIPT, {
|
|
198
|
+
keys: [`${this.prefix}:${digest}:${start}`],
|
|
199
|
+
arguments: [String(weight), String(reset - now)],
|
|
200
|
+
});
|
|
201
|
+
const count = parseInteger(value, "fixed-window");
|
|
202
|
+
return {
|
|
203
|
+
allowed: count <= limit,
|
|
204
|
+
limit,
|
|
205
|
+
remaining: Math.max(0, limit - count),
|
|
206
|
+
reset,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (this.algorithm.kind === "sliding-window") {
|
|
210
|
+
const start = Math.floor(now / this.algorithm.windowMs) * this.algorithm.windowMs;
|
|
211
|
+
const value = await this.options.redis.eval(SLIDING_WINDOW_SCRIPT, {
|
|
212
|
+
keys: [
|
|
213
|
+
`${this.prefix}:${digest}:${start}`,
|
|
214
|
+
`${this.prefix}:${digest}:${start - this.algorithm.windowMs}`,
|
|
215
|
+
],
|
|
216
|
+
arguments: [
|
|
217
|
+
String(weight),
|
|
218
|
+
String(this.algorithm.windowMs),
|
|
219
|
+
String(now - start),
|
|
220
|
+
String(start + this.algorithm.windowMs - now),
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
if (!Array.isArray(value) || value.length < 2)
|
|
224
|
+
throw new RedisProtocolError("Invalid sliding-window response");
|
|
225
|
+
const weighted = Number(value[1]);
|
|
226
|
+
return {
|
|
227
|
+
allowed: weighted <= limit,
|
|
228
|
+
limit,
|
|
229
|
+
remaining: Math.max(0, Math.floor(limit - weighted)),
|
|
230
|
+
reset: start + this.algorithm.windowMs,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const value = await this.options.redis.eval(TOKEN_BUCKET_SCRIPT, {
|
|
234
|
+
keys: [`${this.prefix}:bucket:${digest}`],
|
|
235
|
+
arguments: [
|
|
236
|
+
String(limit),
|
|
237
|
+
String(this.algorithm.refillRate),
|
|
238
|
+
String(this.algorithm.windowMs),
|
|
239
|
+
String(weight),
|
|
240
|
+
String(now),
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
if (!Array.isArray(value) || value.length < 3)
|
|
244
|
+
throw new RedisProtocolError("Invalid token-bucket response");
|
|
245
|
+
const allowed = Number(value[0]) === 1;
|
|
246
|
+
const remaining = Math.max(0, Math.floor(Number(value[1])));
|
|
247
|
+
const reset = Number(value[2]);
|
|
248
|
+
return { allowed, limit, remaining, reset };
|
|
249
|
+
}
|
|
250
|
+
async keysFor(identifier, now) {
|
|
251
|
+
const digest = await hashIdentifier(identifier);
|
|
252
|
+
if (this.algorithm.kind === "fixed-window")
|
|
253
|
+
return [
|
|
254
|
+
`${this.prefix}:${digest}:${Math.floor(now / this.algorithm.windowMs) * this.algorithm.windowMs}`,
|
|
255
|
+
];
|
|
256
|
+
if (this.algorithm.kind === "sliding-window") {
|
|
257
|
+
const start = Math.floor(now / this.algorithm.windowMs) * this.algorithm.windowMs;
|
|
258
|
+
return [
|
|
259
|
+
`${this.prefix}:${digest}:${start}`,
|
|
260
|
+
`${this.prefix}:${digest}:${start - this.algorithm.windowMs}`,
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
return [`${this.prefix}:bucket:${digest}`];
|
|
264
|
+
}
|
|
265
|
+
response(allowed, limit, remaining, reset, reason) {
|
|
266
|
+
return {
|
|
267
|
+
success: allowed,
|
|
268
|
+
allowed,
|
|
269
|
+
limit,
|
|
270
|
+
remaining,
|
|
271
|
+
reset,
|
|
272
|
+
resetAt: new Date(reset),
|
|
273
|
+
retryAfterMs: Math.max(0, reset - this.now()),
|
|
274
|
+
reason,
|
|
275
|
+
pending: Promise.resolve(),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const FIXED_WINDOW_SCRIPT = `local current = redis.call("INCRBY", KEYS[1], ARGV[1]) if current == tonumber(ARGV[1]) then redis.call("PEXPIRE", KEYS[1], ARGV[2]) end return current`;
|
|
280
|
+
const SLIDING_WINDOW_SCRIPT = `local current = redis.call("INCRBY", KEYS[1], ARGV[1]) if current == tonumber(ARGV[1]) then redis.call("PEXPIRE", KEYS[1], ARGV[2]) redis.call("PEXPIRE", KEYS[2], ARGV[2]) end local previous = tonumber(redis.call("GET", KEYS[2]) or "0") local weighted = previous * ((tonumber(ARGV[2]) - tonumber(ARGV[3])) / tonumber(ARGV[2])) + current return {current, weighted}`;
|
|
281
|
+
const TOKEN_BUCKET_SCRIPT = `local state = redis.call("HMGET", KEYS[1], "tokens", "updated") local tokens = tonumber(state[1]) or tonumber(ARGV[1]) local updated = tonumber(state[2]) or tonumber(ARGV[5]) local elapsed = math.max(0, tonumber(ARGV[5]) - updated) local refill = elapsed * tonumber(ARGV[2]) / tonumber(ARGV[3]) tokens = math.min(tonumber(ARGV[1]), tokens + refill) local requested = tonumber(ARGV[4]) local allowed = 0 if tokens >= requested then tokens = tokens - requested allowed = 1 end local reset = tonumber(ARGV[5]) + math.ceil((requested - tokens) * tonumber(ARGV[3]) / tonumber(ARGV[2])) redis.call("HSET", KEYS[1], "tokens", tokens, "updated", ARGV[5]) redis.call("PEXPIRE", KEYS[1], ARGV[3]) return {allowed, tokens, reset}`;
|
|
282
|
+
function assertIdentifier(identifier) {
|
|
283
|
+
if (!identifier.trim() || identifier.length > 512)
|
|
284
|
+
throw new RedisConfigurationError("Rate-limit identifier must be between one and 512 characters");
|
|
285
|
+
}
|
|
286
|
+
function assertAlgorithmValues(limit, windowMs) {
|
|
287
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
288
|
+
throw new RedisConfigurationError("Rate-limit limit must be positive");
|
|
289
|
+
if (!Number.isSafeInteger(windowMs) || windowMs <= 0)
|
|
290
|
+
throw new RedisConfigurationError("Rate-limit window must be positive");
|
|
291
|
+
}
|
|
292
|
+
function parseInteger(value, name) {
|
|
293
|
+
const result = Number(value);
|
|
294
|
+
if (!Number.isSafeInteger(result))
|
|
295
|
+
throw new RedisProtocolError(`Invalid ${name} response`);
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
function isAvailabilityFailure(error) {
|
|
299
|
+
return (error instanceof RedisTimeoutError ||
|
|
300
|
+
error instanceof RedisUnavailableError ||
|
|
301
|
+
error instanceof RedisConnectionError ||
|
|
302
|
+
error instanceof RedisClientStateError);
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.js","sourceRoot":"","sources":["../../src/rate-limit/contracts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { RateLimitAlgorithm, RateLimitAnalyticsEvent, RateLimitAnalyticsSink, RateLimiter, RateLimiterOptions, RateLimitFailureMode, RateLimitPolicy, RateLimitReason, RateLimitRequest, RateLimitResponse, RateLimitResult, } from "./contracts.js";
|
|
2
|
+
export { createRateLimiter, createRedisRateLimiter, fixedWindow, RedisRateLimiter, slidingWindow, tokenBucket, } from "./contracts.js";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rate-limit/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EACtB,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,GACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,iBAAiB,EACjB,sBAAsB,EACtB,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,WAAW,GACX,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rate-limit/index.ts"],"names":[],"mappings":"AAaA,OAAO,EACN,iBAAiB,EACjB,sBAAsB,EACtB,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,WAAW,GACX,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RedisCommand, RedisExecutor, RedisScriptOptions } from "../core/executor.js";
|
|
2
|
+
export declare class FakeRedisExecutor implements RedisExecutor {
|
|
3
|
+
private readonly scriptResult;
|
|
4
|
+
readonly commands: RedisCommand[];
|
|
5
|
+
constructor(scriptResult?: unknown);
|
|
6
|
+
execute<T = unknown>(command: RedisCommand): Promise<T>;
|
|
7
|
+
eval<T = unknown>(_script: string, _options: RedisScriptOptions): Promise<T>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=fake-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-executor.d.ts","sourceRoot":"","sources":["../../src/testing/fake-executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,MAAM,qBAAqB,CAAC;AAE7B,qBAAa,iBAAkB,YAAW,aAAa;IAE1C,OAAO,CAAC,QAAQ,CAAC,YAAY;IADzC,QAAQ,CAAC,QAAQ,EAAE,YAAY,EAAE,CAAM;IACvC,YAA6B,YAAY,GAAE,OAAoB,EAAI;IACnE,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,CAGtD;IACD,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,CAAC,CAAC,CAE3E;CACD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class FakeRedisExecutor {
|
|
2
|
+
scriptResult;
|
|
3
|
+
commands = [];
|
|
4
|
+
constructor(scriptResult = [1, 1_000]) {
|
|
5
|
+
this.scriptResult = scriptResult;
|
|
6
|
+
}
|
|
7
|
+
execute(command) {
|
|
8
|
+
this.commands.push(command);
|
|
9
|
+
return Promise.resolve(undefined);
|
|
10
|
+
}
|
|
11
|
+
eval(_script, _options) {
|
|
12
|
+
return Promise.resolve(this.scriptResult);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=fake-executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-executor.js","sourceRoot":"","sources":["../../src/testing/fake-executor.ts"],"names":[],"mappings":"AAMA,MAAM,OAAO,iBAAiB;IAEA,YAAY;IADhC,QAAQ,GAAmB,EAAE,CAAC;IACvC,YAA6B,YAAY,GAAY,CAAC,CAAC,EAAE,KAAK,CAAC;4BAAlC,YAAY;IAAyB,CAAC;IACnE,OAAO,CAAc,OAAqB;QACzC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,SAAc,CAAC,CAAC;IACxC,CAAC;IACD,IAAI,CAAc,OAAe,EAAE,QAA4B;QAC9D,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,YAAiB,CAAC,CAAC;IAChD,CAAC;CACD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/docs/adapters.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Adapters
|
|
2
|
+
|
|
3
|
+
Feature modules are transport-neutral. They accept `RedisExecutor` or
|
|
4
|
+
`RedisSubscriptionTransport`, which means a project can replace its Redis client
|
|
5
|
+
without rewriting application-facing cache, limiter, lock, pub/sub, or queue code.
|
|
6
|
+
|
|
7
|
+
## node-redis
|
|
8
|
+
|
|
9
|
+
`createRedisClient` and `createRedisPool` are the default Node/Bun adapters and are
|
|
10
|
+
included with the package's normal `redis` dependency.
|
|
11
|
+
|
|
12
|
+
## ioredis
|
|
13
|
+
|
|
14
|
+
Install ioredis in the consuming project and pass its configured client to
|
|
15
|
+
`createIoredisExecutor`. The adapter does not create an ioredis client or read
|
|
16
|
+
environment variables, so authentication, TLS, retry, and sentinel/cluster choices
|
|
17
|
+
remain under application control.
|
|
18
|
+
|
|
19
|
+
`createIoredisPubSubTransport` accepts a publisher plus an explicit subscriber or a
|
|
20
|
+
publisher with `duplicate()`.
|
|
21
|
+
|
|
22
|
+
The bridge applies a command timeout (3 seconds by default); configure it with
|
|
23
|
+
`commandTimeoutMs`. If an application uses ioredis-specific error classes, pass a
|
|
24
|
+
typed `normalizeError` function to map those classes to this package's exported
|
|
25
|
+
errors. The hook receives the original error object, so message or stack matching is
|
|
26
|
+
not required.
|
|
27
|
+
|
|
28
|
+
## BullMQ
|
|
29
|
+
|
|
30
|
+
`createBullMqQueueAdapter` accepts an existing BullMQ queue and an injected worker
|
|
31
|
+
factory. BullMQ remains optional and is not imported by the Redis package at runtime.
|
|
32
|
+
This keeps the package installable for consumers who use the native Streams queue or
|
|
33
|
+
only the lower-level modules.
|
|
34
|
+
|
|
35
|
+
Both queue implementations expose the same `JobQueue<T>` and `QueueWorker` contracts.
|
|
36
|
+
Both are at-least-once systems; handlers must be idempotent.
|
|
37
|
+
|
|
38
|
+
All adapters are structural contracts. They do not create hidden clients, read
|
|
39
|
+
environment variables, or force a retry policy, so switching from node-redis to
|
|
40
|
+
ioredis or BullMQ is an application-composition change rather than a feature-module
|
|
41
|
+
rewrite.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
The package is organized around a small transport contract and independent feature
|
|
4
|
+
modules. Modules do not create connections, read environment variables, or choose
|
|
5
|
+
application-level failure policy.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
application
|
|
9
|
+
|
|
|
10
|
+
v
|
|
11
|
+
feature module (cache, limiter, lock, pub/sub, queue)
|
|
12
|
+
|
|
|
13
|
+
v
|
|
14
|
+
RedisExecutor / RedisSubscriptionTransport
|
|
15
|
+
|
|
|
16
|
+
v
|
|
17
|
+
node-redis adapter, pool, or user adapter
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`RedisExecutor` exposes command and script execution without coupling the feature
|
|
21
|
+
modules to node-redis. `@mhbdev/redis/node` owns TCP/TLS connection lifecycle,
|
|
22
|
+
timeouts, bounded reconnects, pooling, and the explicit raw-client escape hatch.
|
|
23
|
+
|
|
24
|
+
Every public module validates configuration at construction or before issuing a
|
|
25
|
+
command. Redis responses from scripts are validated before becoming public values.
|
|
26
|
+
Errors preserve their causes while avoiding connection URLs, credentials, raw
|
|
27
|
+
identifiers, and payloads in messages.
|
|
28
|
+
|
|
29
|
+
## Lifecycle
|
|
30
|
+
|
|
31
|
+
Create one managed client or pool per process and close it during graceful shutdown.
|
|
32
|
+
Do not create a connection per request. Pub/sub uses dedicated subscriber connections
|
|
33
|
+
because Redis subscriber clients cannot safely execute ordinary commands while
|
|
34
|
+
subscribed.
|
|
35
|
+
|
|
36
|
+
## Compatibility boundary
|
|
37
|
+
|
|
38
|
+
The new API is intentionally clean rather than a copy of the private
|
|
39
|
+
`@dordoone/redis-runtime` package. The existing atomic rate-limit behavior is
|
|
40
|
+
preserved through `RateLimiter.check`, `RateLimitPolicy`, and `RateLimitResult`; the
|
|
41
|
+
migration guide maps the old connection manager to `createRedisClient`.
|
package/docs/errors.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Errors and failure handling
|
|
2
|
+
|
|
3
|
+
Every package error is an instance of `RedisError` with a stable `code`, a safe
|
|
4
|
+
human-readable message, and an optional `cause`. Use `instanceof` or
|
|
5
|
+
`isRedisErrorCode(error, code)` for control flow; do not inspect `message`, `stack`, or
|
|
6
|
+
serialized causes.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { isRedisErrorCode } from "@mhbdev/redis";
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await redis.ping();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (isRedisErrorCode(error, "REDIS_TIMEOUT")) return retry();
|
|
15
|
+
if (isRedisErrorCode(error, "REDIS_UNAVAILABLE")) return useFallback();
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The node-redis adapter maps its exported error classes directly. The ioredis bridge
|
|
21
|
+
maps unknown failures to `REDIS_UNAVAILABLE` by default and accepts an optional typed
|
|
22
|
+
normalizer for application-specific or plugin-specific error classes. No adapter uses
|
|
23
|
+
error-message or stack-trace matching to determine behavior.
|
|
24
|
+
|
|
25
|
+
Configuration, protocol, serialization, lock, and queue errors are not converted into
|
|
26
|
+
availability failures. This distinction prevents `failureMode: "open"` in the rate
|
|
27
|
+
limiter from masking a programming error or malformed Redis response.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Migration from `@dordoone/redis-runtime`
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
npm uninstall @dordoone/redis-runtime
|
|
5
|
+
npm install @mhbdev/redis
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
The primary mapping is:
|
|
9
|
+
|
|
10
|
+
| Internal package | `@mhbdev/redis` |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `new RedisClientManager({ url })` | `createRedisClient({ url })` |
|
|
13
|
+
| `manager.getClient()` | `client.execute()` or `client.raw()` |
|
|
14
|
+
| `manager.ping()` | `client.ping()` |
|
|
15
|
+
| `manager.close()` | `client.close()` |
|
|
16
|
+
| `new RedisRateLimiter(manager)` | `createRateLimiter(fixedWindow(...), { redis: client })` |
|
|
17
|
+
| `RateLimitPolicy` / `RateLimitResult` | same names from `@mhbdev/redis/rate-limit` |
|
|
18
|
+
|
|
19
|
+
The package remains application-policy neutral. The application still decides whether
|
|
20
|
+
an endpoint fails open or closed; the limiter only exposes the configured behavior and
|
|
21
|
+
typed result.
|
package/docs/queues.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Queues
|
|
2
|
+
|
|
3
|
+
`RedisQueue` stores immediate work in a Redis Stream and delayed work in a sorted set.
|
|
4
|
+
Workers consume through Redis consumer groups. A failed job is acknowledged and
|
|
5
|
+
re-enqueued with an incremented attempt count until `maxAttempts` is reached; then it
|
|
6
|
+
is written to the dead-letter stream.
|
|
7
|
+
|
|
8
|
+
Delivery is at-least-once. A process crash can leave a pending message, and handlers
|
|
9
|
+
must be safe to run more than once. Use application-level idempotency keys for external
|
|
10
|
+
side effects.
|
|
11
|
+
|
|
12
|
+
Use a pool when workers need blocking reads or concurrent command execution. Keep queue
|
|
13
|
+
names and prefixes stable during rolling deployments.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Rate limiting
|
|
2
|
+
|
|
3
|
+
The package implements three atomic algorithms:
|
|
4
|
+
|
|
5
|
+
| Algorithm | Storage | Best for |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| Fixed window | One counter per time boundary | Lowest cost and simple quotas |
|
|
8
|
+
| Sliding window | Current and previous counters | Fewer boundary bursts |
|
|
9
|
+
| Token bucket | Tokens and last-refill timestamp | Controlled bursts and sustained rate |
|
|
10
|
+
|
|
11
|
+
All algorithms execute their state transition in one Redis script. Identifiers are
|
|
12
|
+
SHA-256 hashed and namespaced with the configured prefix. Weighted requests subtract
|
|
13
|
+
more than one unit from the current quota.
|
|
14
|
+
|
|
15
|
+
Optional local blocked-result caching reduces Redis calls after an identifier has been
|
|
16
|
+
denied. Keep the limiter outside hot serverless handlers when using that cache.
|
|
17
|
+
|
|
18
|
+
`failureMode: "closed"` propagates Redis failures. `failureMode: "open"` returns a
|
|
19
|
+
successful timeout/Redis result and should only be used when availability is the
|
|
20
|
+
stronger requirement.
|
|
21
|
+
|
|
22
|
+
Analytics are delivered to an injected `RateLimitAnalyticsSink`; the package does not
|
|
23
|
+
force a logging or metrics vendor.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Releasing to npm
|
|
2
|
+
|
|
3
|
+
## One-time initial publication
|
|
4
|
+
|
|
5
|
+
The package is intentionally published once by an authorized maintainer before npm
|
|
6
|
+
can attach a trusted publisher to it. From this package directory, after reviewing the
|
|
7
|
+
dry-run output:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm whoami
|
|
11
|
+
npm run pack:check
|
|
12
|
+
npm publish --access public --provenance=false
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The one-time local publication explicitly disables provenance because a local shell has
|
|
16
|
+
no OIDC provider. The package still declares provenance by default, and the GitHub
|
|
17
|
+
workflow will generate provenance through npm trusted publishing. Do not add an npm
|
|
18
|
+
token to the repository or GitHub secrets for that workflow.
|
|
19
|
+
|
|
20
|
+
## Configure npm trusted publishing
|
|
21
|
+
|
|
22
|
+
After `@mhbdev/redis` exists on npm, open the package's npm settings and add a GitHub
|
|
23
|
+
Actions trusted publisher with:
|
|
24
|
+
|
|
25
|
+
- GitHub organization/user: `mhbdev`
|
|
26
|
+
- Repository: `redis`
|
|
27
|
+
- Workflow filename: `.github/workflows/release.yml`
|
|
28
|
+
- Environment: `npm-release`
|
|
29
|
+
|
|
30
|
+
The GitHub repository must also have the `npm-release` environment available. The
|
|
31
|
+
release job already requests only the permissions required for Changesets and npm
|
|
32
|
+
provenance: repository contents, pull requests, and the OIDC `id-token`. It does not
|
|
33
|
+
provide `NODE_AUTH_TOKEN`, so npm uses trusted publishing rather than a long-lived
|
|
34
|
+
automation token.
|
|
35
|
+
|
|
36
|
+
## Subsequent releases
|
|
37
|
+
|
|
38
|
+
1. Add a Changeset describing the public change.
|
|
39
|
+
2. Merge the Changesets version pull request created by `release.yml`.
|
|
40
|
+
3. The next push to `main` runs the release gate and publishes the version through npm
|
|
41
|
+
trusted publishing.
|
|
42
|
+
|
|
43
|
+
The workflow validates linting, types, tests, build output, declaration packaging,
|
|
44
|
+
`publint`, `arethetypeswrong`, tarball imports, and npm audit before Changesets is
|
|
45
|
+
allowed to version or publish the package.
|