@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +173 -0
  4. package/dist/bullmq/adapter.d.ts +49 -0
  5. package/dist/bullmq/adapter.d.ts.map +1 -0
  6. package/dist/bullmq/adapter.js +56 -0
  7. package/dist/bullmq/adapter.js.map +1 -0
  8. package/dist/bullmq/index.d.ts +3 -0
  9. package/dist/bullmq/index.d.ts.map +1 -0
  10. package/dist/bullmq/index.js +2 -0
  11. package/dist/bullmq/index.js.map +1 -0
  12. package/dist/cache/cache.d.ts +31 -0
  13. package/dist/cache/cache.d.ts.map +1 -0
  14. package/dist/cache/cache.js +78 -0
  15. package/dist/cache/cache.js.map +1 -0
  16. package/dist/cache/index.d.ts +3 -0
  17. package/dist/cache/index.d.ts.map +1 -0
  18. package/dist/cache/index.js +2 -0
  19. package/dist/cache/index.js.map +1 -0
  20. package/dist/core/codec.d.ts +7 -0
  21. package/dist/core/codec.d.ts.map +1 -0
  22. package/dist/core/codec.js +24 -0
  23. package/dist/core/codec.js.map +1 -0
  24. package/dist/core/errors.d.ts +43 -0
  25. package/dist/core/errors.d.ts.map +1 -0
  26. package/dist/core/errors.js +84 -0
  27. package/dist/core/errors.js.map +1 -0
  28. package/dist/core/executor.d.ts +28 -0
  29. package/dist/core/executor.d.ts.map +1 -0
  30. package/dist/core/executor.js +27 -0
  31. package/dist/core/executor.js.map +1 -0
  32. package/dist/core/keys.d.ts +4 -0
  33. package/dist/core/keys.d.ts.map +1 -0
  34. package/dist/core/keys.js +15 -0
  35. package/dist/core/keys.js.map +1 -0
  36. package/dist/index.d.ts +15 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +11 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/ioredis/executor.d.ts +30 -0
  41. package/dist/ioredis/executor.d.ts.map +1 -0
  42. package/dist/ioredis/executor.js +52 -0
  43. package/dist/ioredis/executor.js.map +1 -0
  44. package/dist/ioredis/index.d.ts +5 -0
  45. package/dist/ioredis/index.d.ts.map +1 -0
  46. package/dist/ioredis/index.js +3 -0
  47. package/dist/ioredis/index.js.map +1 -0
  48. package/dist/ioredis/pubsub.d.ts +16 -0
  49. package/dist/ioredis/pubsub.d.ts.map +1 -0
  50. package/dist/ioredis/pubsub.js +42 -0
  51. package/dist/ioredis/pubsub.js.map +1 -0
  52. package/dist/locks/index.d.ts +3 -0
  53. package/dist/locks/index.d.ts.map +1 -0
  54. package/dist/locks/index.js +2 -0
  55. package/dist/locks/index.js.map +1 -0
  56. package/dist/locks/locks.d.ts +25 -0
  57. package/dist/locks/locks.d.ts.map +1 -0
  58. package/dist/locks/locks.js +77 -0
  59. package/dist/locks/locks.js.map +1 -0
  60. package/dist/node/client.d.ts +47 -0
  61. package/dist/node/client.d.ts.map +1 -0
  62. package/dist/node/client.js +155 -0
  63. package/dist/node/client.js.map +1 -0
  64. package/dist/node/errors.d.ts +3 -0
  65. package/dist/node/errors.d.ts.map +1 -0
  66. package/dist/node/errors.js +16 -0
  67. package/dist/node/errors.js.map +1 -0
  68. package/dist/node/index.d.ts +6 -0
  69. package/dist/node/index.d.ts.map +1 -0
  70. package/dist/node/index.js +4 -0
  71. package/dist/node/index.js.map +1 -0
  72. package/dist/node/pool.d.ts +31 -0
  73. package/dist/node/pool.d.ts.map +1 -0
  74. package/dist/node/pool.js +131 -0
  75. package/dist/node/pool.js.map +1 -0
  76. package/dist/node/pubsub.d.ts +16 -0
  77. package/dist/node/pubsub.d.ts.map +1 -0
  78. package/dist/node/pubsub.js +41 -0
  79. package/dist/node/pubsub.js.map +1 -0
  80. package/dist/pubsub/contracts.d.ts +6 -0
  81. package/dist/pubsub/contracts.d.ts.map +1 -0
  82. package/dist/pubsub/contracts.js +2 -0
  83. package/dist/pubsub/contracts.js.map +1 -0
  84. package/dist/pubsub/index.d.ts +3 -0
  85. package/dist/pubsub/index.d.ts.map +1 -0
  86. package/dist/pubsub/index.js +2 -0
  87. package/dist/pubsub/index.js.map +1 -0
  88. package/dist/pubsub/pubsub.d.ts +15 -0
  89. package/dist/pubsub/pubsub.d.ts.map +1 -0
  90. package/dist/pubsub/pubsub.js +62 -0
  91. package/dist/pubsub/pubsub.js.map +1 -0
  92. package/dist/queue/index.d.ts +3 -0
  93. package/dist/queue/index.d.ts.map +1 -0
  94. package/dist/queue/index.js +2 -0
  95. package/dist/queue/index.js.map +1 -0
  96. package/dist/queue/queue.d.ts +78 -0
  97. package/dist/queue/queue.d.ts.map +1 -0
  98. package/dist/queue/queue.js +260 -0
  99. package/dist/queue/queue.js.map +1 -0
  100. package/dist/rate-limit/contracts.d.ts +93 -0
  101. package/dist/rate-limit/contracts.d.ts.map +1 -0
  102. package/dist/rate-limit/contracts.js +304 -0
  103. package/dist/rate-limit/contracts.js.map +1 -0
  104. package/dist/rate-limit/index.d.ts +3 -0
  105. package/dist/rate-limit/index.d.ts.map +1 -0
  106. package/dist/rate-limit/index.js +2 -0
  107. package/dist/rate-limit/index.js.map +1 -0
  108. package/dist/testing/fake-executor.d.ts +9 -0
  109. package/dist/testing/fake-executor.d.ts.map +1 -0
  110. package/dist/testing/fake-executor.js +15 -0
  111. package/dist/testing/fake-executor.js.map +1 -0
  112. package/dist/testing/index.d.ts +2 -0
  113. package/dist/testing/index.d.ts.map +1 -0
  114. package/dist/testing/index.js +2 -0
  115. package/dist/testing/index.js.map +1 -0
  116. package/docs/adapters.md +41 -0
  117. package/docs/architecture.md +41 -0
  118. package/docs/errors.md +27 -0
  119. package/docs/migration.md +21 -0
  120. package/docs/queues.md +13 -0
  121. package/docs/rate-limiting.md +23 -0
  122. package/docs/releasing.md +45 -0
  123. 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,2 @@
1
+ export { createRateLimiter, createRedisRateLimiter, fixedWindow, RedisRateLimiter, slidingWindow, tokenBucket, } from "./contracts.js";
2
+ //# sourceMappingURL=index.js.map
@@ -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,2 @@
1
+ export { FakeRedisExecutor } from "./fake-executor.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { FakeRedisExecutor } from "./fake-executor.js";
2
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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.