ioredis-toolkit 0.0.10 → 0.5.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 (267) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +1 -1
  3. package/README.md +68 -1613
  4. package/dist/cache/cache.d.ts +30 -0
  5. package/dist/cache/cache.d.ts.map +1 -0
  6. package/dist/cache/cache.js +59 -0
  7. package/dist/cache/cache.js.map +1 -0
  8. package/dist/cache/config.d.ts +12 -0
  9. package/dist/cache/config.d.ts.map +1 -0
  10. package/dist/cache/config.js +13 -0
  11. package/dist/cache/config.js.map +1 -0
  12. package/dist/cache/types.d.ts +32 -0
  13. package/dist/cache/types.d.ts.map +1 -0
  14. package/dist/cache/types.js +5 -0
  15. package/dist/cache/types.js.map +1 -0
  16. package/dist/index.d.ts +43 -51
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +32 -44
  19. package/dist/index.js.map +1 -0
  20. package/dist/lock/config.d.ts +12 -0
  21. package/dist/lock/config.d.ts.map +1 -0
  22. package/dist/lock/config.js +8 -0
  23. package/dist/lock/config.js.map +1 -0
  24. package/dist/lock/lock.d.ts +20 -0
  25. package/dist/lock/lock.d.ts.map +1 -0
  26. package/dist/lock/lock.js +44 -0
  27. package/dist/lock/lock.js.map +1 -0
  28. package/dist/lock/types.d.ts +19 -0
  29. package/dist/lock/types.d.ts.map +1 -0
  30. package/dist/lock/types.js +2 -0
  31. package/dist/lock/types.js.map +1 -0
  32. package/dist/modules-config.d.ts +3 -0
  33. package/dist/modules-config.d.ts.map +1 -0
  34. package/dist/modules-config.js +2 -0
  35. package/dist/modules-config.js.map +1 -0
  36. package/dist/pubsub/config.d.ts +11 -0
  37. package/dist/pubsub/config.d.ts.map +1 -0
  38. package/dist/pubsub/config.js +6 -0
  39. package/dist/pubsub/config.js.map +1 -0
  40. package/dist/pubsub/pubsub.d.ts +20 -0
  41. package/dist/pubsub/pubsub.d.ts.map +1 -0
  42. package/dist/pubsub/pubsub.js +54 -0
  43. package/dist/pubsub/pubsub.js.map +1 -0
  44. package/dist/pubsub/types.d.ts +24 -0
  45. package/dist/pubsub/types.d.ts.map +1 -0
  46. package/dist/pubsub/types.js +2 -0
  47. package/dist/pubsub/types.js.map +1 -0
  48. package/dist/rate-limit/config.d.ts +12 -0
  49. package/dist/rate-limit/config.d.ts.map +1 -0
  50. package/dist/rate-limit/config.js +6 -0
  51. package/dist/rate-limit/config.js.map +1 -0
  52. package/dist/rate-limit/rate-limiter.d.ts +18 -0
  53. package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
  54. package/dist/rate-limit/rate-limiter.js +37 -0
  55. package/dist/rate-limit/rate-limiter.js.map +1 -0
  56. package/dist/rate-limit/types.d.ts +27 -0
  57. package/dist/rate-limit/types.d.ts.map +1 -0
  58. package/dist/rate-limit/types.js +2 -0
  59. package/dist/rate-limit/types.js.map +1 -0
  60. package/dist/redis/client-facade.d.ts +77 -0
  61. package/dist/redis/client-facade.d.ts.map +1 -0
  62. package/dist/redis/client-facade.js +102 -0
  63. package/dist/redis/client-facade.js.map +1 -0
  64. package/dist/redis/client.d.ts +10 -0
  65. package/dist/redis/client.d.ts.map +1 -0
  66. package/dist/redis/client.js +29 -0
  67. package/dist/redis/client.js.map +1 -0
  68. package/dist/redis/cluster.d.ts +7 -0
  69. package/dist/redis/cluster.d.ts.map +1 -0
  70. package/dist/redis/cluster.js +47 -0
  71. package/dist/redis/cluster.js.map +1 -0
  72. package/dist/redis/config.d.ts +39 -0
  73. package/dist/redis/config.d.ts.map +1 -0
  74. package/dist/redis/config.js +52 -0
  75. package/dist/redis/config.js.map +1 -0
  76. package/dist/redis/errors.d.ts +5 -0
  77. package/dist/redis/errors.d.ts.map +1 -0
  78. package/dist/redis/errors.js +5 -0
  79. package/dist/redis/errors.js.map +1 -0
  80. package/dist/redis/types.d.ts +135 -0
  81. package/dist/redis/types.d.ts.map +1 -0
  82. package/dist/redis/types.js +2 -0
  83. package/dist/redis/types.js.map +1 -0
  84. package/dist/redis/wrapper.d.ts +88 -0
  85. package/dist/redis/wrapper.d.ts.map +1 -0
  86. package/dist/redis/wrapper.js +206 -0
  87. package/dist/redis/wrapper.js.map +1 -0
  88. package/dist/session/config.d.ts +47 -0
  89. package/dist/session/config.d.ts.map +1 -0
  90. package/dist/session/config.js +101 -0
  91. package/dist/session/config.js.map +1 -0
  92. package/dist/session/cookie.d.ts +16 -0
  93. package/dist/session/cookie.d.ts.map +1 -0
  94. package/dist/session/cookie.js +28 -0
  95. package/dist/session/cookie.js.map +1 -0
  96. package/dist/session/errors.d.ts +56 -0
  97. package/dist/session/errors.d.ts.map +1 -0
  98. package/dist/session/errors.js +58 -0
  99. package/dist/session/errors.js.map +1 -0
  100. package/dist/session/factory.d.ts +21 -0
  101. package/dist/session/factory.d.ts.map +1 -0
  102. package/dist/session/factory.js +30 -0
  103. package/dist/session/factory.js.map +1 -0
  104. package/dist/session/health.d.ts +12 -0
  105. package/dist/session/health.d.ts.map +1 -0
  106. package/dist/session/health.js +23 -0
  107. package/dist/session/health.js.map +1 -0
  108. package/dist/session/keys.d.ts +23 -0
  109. package/dist/session/keys.d.ts.map +1 -0
  110. package/dist/session/keys.js +27 -0
  111. package/dist/session/keys.js.map +1 -0
  112. package/dist/session/manager.d.ts +34 -0
  113. package/dist/session/manager.d.ts.map +1 -0
  114. package/dist/session/manager.js +31 -0
  115. package/dist/session/manager.js.map +1 -0
  116. package/dist/session/metrics.d.ts +11 -0
  117. package/dist/session/metrics.d.ts.map +1 -0
  118. package/dist/session/metrics.js +10 -0
  119. package/dist/session/metrics.js.map +1 -0
  120. package/dist/session/repository.d.ts +49 -0
  121. package/dist/session/repository.d.ts.map +1 -0
  122. package/dist/session/repository.js +203 -0
  123. package/dist/session/repository.js.map +1 -0
  124. package/dist/session/revocation.d.ts +22 -0
  125. package/dist/session/revocation.d.ts.map +1 -0
  126. package/dist/session/revocation.js +41 -0
  127. package/dist/session/revocation.js.map +1 -0
  128. package/dist/session/script-sources.d.ts +11 -0
  129. package/dist/session/script-sources.d.ts.map +1 -0
  130. package/dist/session/script-sources.js +140 -0
  131. package/dist/session/script-sources.js.map +1 -0
  132. package/dist/session/scripts.d.ts +15 -0
  133. package/dist/session/scripts.d.ts.map +1 -0
  134. package/dist/session/scripts.js +41 -0
  135. package/dist/session/scripts.js.map +1 -0
  136. package/dist/session/serializer.d.ts +12 -0
  137. package/dist/session/serializer.d.ts.map +1 -0
  138. package/dist/session/serializer.js +77 -0
  139. package/dist/session/serializer.js.map +1 -0
  140. package/dist/session/service.d.ts +48 -0
  141. package/dist/session/service.d.ts.map +1 -0
  142. package/dist/session/service.js +235 -0
  143. package/dist/session/service.js.map +1 -0
  144. package/dist/session/token.d.ts +16 -0
  145. package/dist/session/token.d.ts.map +1 -0
  146. package/dist/session/token.js +32 -0
  147. package/dist/session/token.js.map +1 -0
  148. package/dist/session/types.d.ts +134 -0
  149. package/dist/session/types.d.ts.map +1 -0
  150. package/dist/session/types.js +2 -0
  151. package/dist/session/types.js.map +1 -0
  152. package/dist/streams/config.d.ts +12 -0
  153. package/dist/streams/config.d.ts.map +1 -0
  154. package/dist/streams/config.js +6 -0
  155. package/dist/streams/config.js.map +1 -0
  156. package/dist/streams/streams.d.ts +24 -0
  157. package/dist/streams/streams.d.ts.map +1 -0
  158. package/dist/streams/streams.js +55 -0
  159. package/dist/streams/streams.js.map +1 -0
  160. package/dist/streams/types.d.ts +32 -0
  161. package/dist/streams/types.d.ts.map +1 -0
  162. package/dist/streams/types.js +2 -0
  163. package/dist/streams/types.js.map +1 -0
  164. package/docs/ACCEPTANCE-REPORT.md +70 -0
  165. package/docs/ARCHITECTURE.md +61 -0
  166. package/docs/CAPACITY.md +33 -0
  167. package/docs/DEPLOYMENT.md +22 -0
  168. package/docs/README-API.md +15 -0
  169. package/docs/STATE-MACHINE.md +38 -0
  170. package/docs/TESTING.md +37 -0
  171. package/docs/THREAT-MODEL.md +23 -0
  172. package/docs/TYPE-SAFETY.md +34 -0
  173. package/docs/modules/cache/README.md +7 -0
  174. package/docs/modules/cache/usage.md +156 -0
  175. package/docs/modules/lock/README.md +7 -0
  176. package/docs/modules/lock/usage.md +105 -0
  177. package/docs/modules/pubsub/README.md +7 -0
  178. package/docs/modules/pubsub/usage.md +106 -0
  179. package/docs/modules/rate-limit/README.md +7 -0
  180. package/docs/modules/rate-limit/usage.md +100 -0
  181. package/docs/modules/sessions/README.md +7 -0
  182. package/docs/modules/sessions/usage.md +262 -0
  183. package/docs/modules/streams/README.md +7 -0
  184. package/docs/modules/streams/usage.md +141 -0
  185. package/package.json +50 -60
  186. package/src/scripts/cleanup-index.lua +4 -0
  187. package/src/scripts/conditional-update.lua +21 -0
  188. package/src/scripts/consume-session.lua +21 -0
  189. package/src/scripts/create-session.lua +28 -0
  190. package/src/scripts/delete.lua +2 -0
  191. package/src/scripts/destroy-user.lua +13 -0
  192. package/src/scripts/enforce-limit.lua +17 -0
  193. package/src/scripts/revoke-session.lua +13 -0
  194. package/src/scripts/rotate.lua +24 -0
  195. package/src/scripts/touch-session.lua +28 -0
  196. package/src/scripts/update-session.lua +18 -0
  197. package/dist/cache.d.ts +0 -797
  198. package/dist/cache.js +0 -1115
  199. package/dist/client.d.ts +0 -287
  200. package/dist/client.js +0 -1113
  201. package/dist/cluster-slot.d.ts +0 -4
  202. package/dist/cluster-slot.js +0 -31
  203. package/dist/cluster.d.ts +0 -79
  204. package/dist/cluster.js +0 -156
  205. package/dist/errors.d.ts +0 -30
  206. package/dist/errors.js +0 -63
  207. package/dist/health.d.ts +0 -180
  208. package/dist/health.js +0 -239
  209. package/dist/lock.d.ts +0 -233
  210. package/dist/lock.js +0 -440
  211. package/dist/logger.d.ts +0 -12
  212. package/dist/logger.js +0 -40
  213. package/dist/pubsub.d.ts +0 -423
  214. package/dist/pubsub.js +0 -537
  215. package/dist/ratelimiter.d.ts +0 -441
  216. package/dist/ratelimiter.js +0 -539
  217. package/dist/session/index.d.ts +0 -23
  218. package/dist/session/index.js +0 -16
  219. package/dist/session/revocation-store.d.ts +0 -176
  220. package/dist/session/revocation-store.js +0 -318
  221. package/dist/session/scripts/cleanup-index.lua +0 -21
  222. package/dist/session/scripts/conditional-update-encrypted.lua +0 -60
  223. package/dist/session/scripts/conditional-update.lua +0 -63
  224. package/dist/session/scripts/create.lua +0 -83
  225. package/dist/session/scripts/delete-by-user.lua +0 -29
  226. package/dist/session/scripts/delete.lua +0 -15
  227. package/dist/session/scripts/enforce-limit.lua +0 -38
  228. package/dist/session/scripts/revoke.lua +0 -61
  229. package/dist/session/scripts/rotate-encrypted.lua +0 -149
  230. package/dist/session/scripts/rotate.lua +0 -167
  231. package/dist/session/scripts/touch-encrypted.lua +0 -89
  232. package/dist/session/scripts/touch.lua +0 -72
  233. package/dist/session/scripts/validate.lua +0 -90
  234. package/dist/session/session-circuit-breaker.d.ts +0 -42
  235. package/dist/session/session-circuit-breaker.js +0 -129
  236. package/dist/session/session-config.d.ts +0 -355
  237. package/dist/session/session-config.js +0 -171
  238. package/dist/session/session-cookie.d.ts +0 -72
  239. package/dist/session/session-cookie.js +0 -101
  240. package/dist/session/session-encryption.d.ts +0 -87
  241. package/dist/session/session-encryption.js +0 -139
  242. package/dist/session/session-errors.d.ts +0 -85
  243. package/dist/session/session-errors.js +0 -145
  244. package/dist/session/session-health.d.ts +0 -38
  245. package/dist/session/session-health.js +0 -60
  246. package/dist/session/session-keys.d.ts +0 -64
  247. package/dist/session/session-keys.js +0 -128
  248. package/dist/session/session-manager.d.ts +0 -73
  249. package/dist/session/session-manager.js +0 -94
  250. package/dist/session/session-metrics.d.ts +0 -41
  251. package/dist/session/session-metrics.js +0 -135
  252. package/dist/session/session-repository.d.ts +0 -184
  253. package/dist/session/session-repository.js +0 -763
  254. package/dist/session/session-scripts.d.ts +0 -36
  255. package/dist/session/session-scripts.js +0 -130
  256. package/dist/session/session-serializer.d.ts +0 -42
  257. package/dist/session/session-serializer.js +0 -267
  258. package/dist/session/session-service.d.ts +0 -123
  259. package/dist/session/session-service.js +0 -670
  260. package/dist/session/session-token.d.ts +0 -38
  261. package/dist/session/session-token.js +0 -86
  262. package/dist/session/session-types.d.ts +0 -281
  263. package/dist/session/session-types.js +0 -16
  264. package/dist/types.d.ts +0 -924
  265. package/dist/types.js +0 -151
  266. package/dist/utils/deepmerge.d.ts +0 -9
  267. package/dist/utils/deepmerge.js +0 -61
package/dist/client.js DELETED
@@ -1,1113 +0,0 @@
1
- import { Cluster, Redis as RedisClient, } from "ioredis";
2
- import { ConfigurationError, RedisError } from "./errors.js";
3
- import { RateLimitOptionsSchema, RedisConfigSchema, DistributedLockOptionsSchema, CacheOptionsSchema, } from "./types.js";
4
- import { defaultLogger } from "./logger.js";
5
- import { executeBySlot } from "./cluster.js";
6
- import { calculateRedisClusterSlot } from "./cluster-slot.js";
7
- import { Cache } from "./cache.js";
8
- import { PubSub } from "./pubsub.js";
9
- import { DistributedLock } from "./lock.js";
10
- import { RateLimiter } from "./ratelimiter.js";
11
- import { createSessionManager, } from "./session/session-manager.js";
12
- import { prettifyError } from "zod";
13
- import { RedisRevocationStore } from "./session/revocation-store.js";
14
- import { deepMerge } from "./utils/deepmerge.js";
15
- // ============================================================================
16
- // Type Guards
17
- // ============================================================================
18
- export function isClusterConfig(config) {
19
- return config.mode === "cluster";
20
- }
21
- export function isSentinelConfig(config) {
22
- return config.mode === "sentinel";
23
- }
24
- export function isStandaloneConfig(config) {
25
- return config.mode === "standalone";
26
- }
27
- // ============================================================================
28
- // Redis Client
29
- // ============================================================================
30
- /**
31
- * Production-grade Redis client wrapper supporting:
32
- *
33
- * - standalone Redis
34
- * - Redis Sentinel
35
- * - Redis Cluster
36
- * - connection lifecycle handling
37
- * - slow-command tracking
38
- * - cluster-aware multi-key operations
39
- * - cache
40
- * - pub/sub
41
- * - distributed locks
42
- * - rate limiting
43
- * - sessions
44
- */
45
- export class RedisClientWrapper {
46
- client;
47
- _cache;
48
- _pubsub;
49
- _lock;
50
- _rateLimiter;
51
- _session;
52
- _revocationStore;
53
- config;
54
- logger;
55
- isReady = false;
56
- // ========================================================================
57
- // Constructor
58
- // ========================================================================
59
- constructor(config, logger = defaultLogger) {
60
- const parsed = RedisConfigSchema.safeParse(config);
61
- if (!parsed.success) {
62
- throw new ConfigurationError(`Invalid Redis config: ${parsed.error.message}`, {
63
- config,
64
- message: prettifyError(parsed.error),
65
- });
66
- }
67
- /**
68
- * IMPORTANT:
69
- *
70
- * `parsed.data` is already `RedisConfig`.
71
- *
72
- * Do NOT cast it back to `RedisConfigInput`.
73
- * Do NOT normalize it manually.
74
- *
75
- * The Zod schema is the normalization boundary.
76
- */
77
- this.config = parsed.data;
78
- this.logger = logger.child({
79
- component: "RedisClient",
80
- });
81
- this.client = this.createClient();
82
- this.setupEventHandlers();
83
- }
84
- /**
85
- * Creates a new Redis client wrapper supporting standalone, sentinel, and cluster topologies.
86
- *
87
- * The client automatically adapts to the configured Redis topology and lazily creates
88
- * and shares sub-components: `cache`, `pubsub`, `lock`, `rateLimiter`, and `session`.
89
- *
90
- * @param config - Redis configuration (validated with Zod `RedisConfigSchema`).
91
- * Must include `mode` (standalone/sentinel/cluster), and topology-specific fields.
92
- * @param logger - Optional pino-compatible logger; defaults to `console`.
93
- *
94
- * @example
95
- * ```ts
96
- * const client = new RedisClientWrapper({
97
- * mode: 'standalone',
98
- * host: 'localhost',
99
- * port: 6379,
100
- * });
101
- * ```
102
- */
103
- // ========================================================================
104
- // Configuration
105
- // ========================================================================
106
- /**
107
- * Returns the Redis topology mode of this client.
108
- *
109
- * @returns `'standalone'`, `'sentinel'`, or `'cluster'`
110
- */
111
- get mode() {
112
- return this.config.mode;
113
- }
114
- // ========================================================================
115
- // Cache
116
- // ========================================================================
117
- /**
118
- * Returns the shared Cache instance (lazily created on first access).
119
- *
120
- * The Cache provides JSON serialization, optional gzip compression, namespace
121
- * support, TTLs, hash helpers, and pattern-based cleanup. All multi-key
122
- * operations are slot-aware and cluster-safe.
123
- *
124
- * @example
125
- * ```ts
126
- * const cache = client.cache;
127
- * await cache.set('user:1', { name: 'alice' });
128
- * const user = await cache.get('user:1');
129
- * ```
130
- */
131
- get cache() {
132
- if (!this._cache) {
133
- this._cache = new Cache(this, this.config.cacheOptions ?? {}, this.logger);
134
- }
135
- return this._cache;
136
- }
137
- withCache(value) {
138
- const parsed = CacheOptionsSchema.safeParse(value);
139
- if (parsed.success) {
140
- const data = { ...this.config.cacheOptions, ...parsed.data };
141
- this.config.cacheOptions = data;
142
- }
143
- else {
144
- this.logger.error("Invalid cache options", { error: parsed.error });
145
- throw new ConfigurationError(`Invalid cache options: ${parsed.error.message}`, {
146
- message: prettifyError(parsed.error),
147
- });
148
- }
149
- this._cache = undefined;
150
- return this;
151
- }
152
- // ========================================================================
153
- // Pub/Sub
154
- // ========================================================================
155
- /**
156
- * Returns the shared Pub/Sub instance (lazily created on first access).
157
- *
158
- * Publishing works immediately; subscribing requires calling {@link connectSubscriber}
159
- * first. Messages are JSON-serialized on publish and auto-parsed on delivery.
160
- *
161
- * @example
162
- * ```ts
163
- * const pubsub = client.pubsub;
164
- * await pubsub.connectSubscriber({ mode: 'standalone', host: 'localhost', port: 6379 });
165
- * await pubsub.subscribe('orders:created', (message) => {
166
- * console.log(message); // { id: 1 }
167
- * });
168
- * await pubsub.publish('orders:created', { id: 1 });
169
- * ```
170
- */
171
- get pubsub() {
172
- if (!this._pubsub) {
173
- this._pubsub = new PubSub(this, this.logger);
174
- }
175
- return this._pubsub;
176
- }
177
- // ========================================================================
178
- // Distributed Lock
179
- // ========================================================================
180
- /**
181
- * Returns the shared DistributedLock instance (lazily created on first access).
182
- *
183
- * Provides atomic distributed mutual-exclusion locks backed by Redis. Works in
184
- * standalone, sentinel, and cluster modes. Acquisition uses atomic `SET ... PX NX`;
185
- * release and extension use Lua scripts so only the lock owner can release or extend.
186
- *
187
- * @example
188
- * ```ts
189
- * const lock = client.lock;
190
- * const acquired = await lock.acquire('order:42');
191
- * if (acquired) {
192
- * try {
193
- * // critical section
194
- * } finally {
195
- * await lock.release('order:42');
196
- * }
197
- * }
198
- * ```
199
- */
200
- get lock() {
201
- if (!this._lock) {
202
- this._lock = new DistributedLock(this, this.logger, this.config.lockOptions);
203
- }
204
- return this._lock;
205
- }
206
- withLock(value) {
207
- const parseResult = DistributedLockOptionsSchema.safeParse(value);
208
- if (parseResult.success) {
209
- const data = { ...this.config.lockOptions, ...parseResult.data };
210
- this.config.lockOptions = data;
211
- }
212
- else {
213
- this.logger.error("Invalid lock options", { error: parseResult.error });
214
- throw new ConfigurationError(`Invalid lock options: ${parseResult.error.message}`, {
215
- message: prettifyError(parseResult.error),
216
- });
217
- }
218
- this._lock = undefined;
219
- return this;
220
- }
221
- // ========================================================================
222
- // Rate Limiter
223
- // ========================================================================
224
- /**
225
- * Returns the shared RateLimiter instance (lazily created on first access).
226
- *
227
- * Generic rate limiting for any resource — routes, API endpoints, users, IPs,
228
- * API keys, database writes, email sends, webhooks. Supports fixed and sliding
229
- * window algorithms. Fails open when Redis is unavailable.
230
- *
231
- * @example
232
- * ```ts
233
- * const limiter = client.rateLimiter;
234
- * const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
235
- * if (!result.allowed) {
236
- * // HTTP 429, set Retry-After: result.retryAfter
237
- * }
238
- * ```
239
- */
240
- get rateLimiter() {
241
- if (!this._rateLimiter) {
242
- this._rateLimiter = new RateLimiter(this, this.config.rateLimit);
243
- }
244
- return this._rateLimiter;
245
- }
246
- withRateLimiter(value) {
247
- const parseResult = RateLimitOptionsSchema.safeParse(value);
248
- if (parseResult.success) {
249
- const data = { ...this.config.rateLimit, ...parseResult.data };
250
- this.config.rateLimit = data;
251
- }
252
- else {
253
- this.logger.error("Invalid rate limit options", { error: parseResult.error });
254
- throw new ConfigurationError(`Invalid rate limit options: ${parseResult.error.message}`, {
255
- message: prettifyError(parseResult.error),
256
- });
257
- }
258
- this._rateLimiter = undefined;
259
- return this;
260
- }
261
- get revocationStore() {
262
- if (!this._revocationStore) {
263
- this._revocationStore = new RedisRevocationStore({
264
- client: this
265
- });
266
- this.config.sessionOptions = { ...(this.config.sessionOptions ?? {}), revocationStore: this._revocationStore };
267
- }
268
- ;
269
- return this._revocationStore;
270
- }
271
- // ========================================================================
272
- // Session
273
- // ========================================================================
274
- /**
275
- * Returns the shared SessionManager instance (lazily created on first access).
276
- *
277
- * The production session stack: validation with fail-closed semantics, rotation
278
- * with retry-safe idempotency, throttled touches, idle/absolute expiry, per-user
279
- * eviction ceilings, security versioning, optional AES-256-GCM encryption at rest,
280
- * fail-closed circuit breaker, metrics and health. Cluster-safe by construction.
281
- *
282
- * @example
283
- * ```ts
284
- * const manager = client.session;
285
- * const { token, session } = await manager.service.create({ userId: 'user-42' });
286
- * const result = await manager.service.validate(token, { userId: 'user-42' });
287
- * ```
288
- */
289
- get session() {
290
- if (!this._session) {
291
- const options = this.config.sessionOptions ?? {};
292
- const params = {
293
- ...options,
294
- client: this,
295
- revocationStore: options.revocationStore ?? this.revocationStore,
296
- };
297
- this._session = createSessionManager(params);
298
- this.config.sessionOptions = params;
299
- }
300
- return this._session;
301
- }
302
- withSession(value) {
303
- const revocationRedisStore = this.revocationStore;
304
- const config = deepMerge(this.config.sessionOptions ?? {}, value);
305
- const newConfig = {
306
- ...config,
307
- client: this,
308
- revocationStore: revocationRedisStore,
309
- };
310
- this.config.sessionOptions = newConfig;
311
- this._session = undefined;
312
- return this;
313
- }
314
- // ========================================================================
315
- // Client Creation
316
- // ========================================================================
317
- createClient() {
318
- const common = this.buildCommonRedisOptions();
319
- const retryStrategy = (times) => {
320
- if (times > this.config.maxRetries) {
321
- return null;
322
- }
323
- return Math.min(times * this.config.retryDelay, 5_000);
324
- };
325
- switch (this.config.mode) {
326
- case "cluster":
327
- return this.createClusterClient(this.config, common, retryStrategy);
328
- case "sentinel":
329
- return this.createSentinelClient(this.config, common, retryStrategy);
330
- case "standalone":
331
- return this.createStandaloneClient(this.config, common, retryStrategy);
332
- }
333
- }
334
- createClusterClient(config, common, retryStrategy) {
335
- return new Cluster(config.clusterNodes.map(({ host, port }) => ({
336
- host,
337
- port,
338
- })), {
339
- clusterRetryStrategy: retryStrategy,
340
- enableOfflineQueue: true,
341
- scaleReads: "master",
342
- redisOptions: {
343
- ...common,
344
- connectTimeout: config.connectionTimeout,
345
- maxRetriesPerRequest: config.maxRetries,
346
- },
347
- });
348
- }
349
- createSentinelClient(config, common, retryStrategy) {
350
- return new RedisClient({
351
- ...common,
352
- connectTimeout: config.connectionTimeout,
353
- maxRetriesPerRequest: config.maxRetries,
354
- retryStrategy,
355
- // sentinel: true,
356
- sentinels: config.sentinelNodes.map(({ host, port }) => ({
357
- host,
358
- port,
359
- })),
360
- name: config.sentinelMasterName,
361
- });
362
- }
363
- createStandaloneClient(config, common, retryStrategy) {
364
- const connectionOptions = {
365
- ...common,
366
- connectTimeout: config.connectionTimeout,
367
- maxRetriesPerRequest: config.maxRetries,
368
- retryStrategy,
369
- };
370
- if (config.url !== undefined) {
371
- return new RedisClient(config.url, connectionOptions);
372
- }
373
- return new RedisClient({
374
- ...connectionOptions,
375
- host: config.host,
376
- port: config.port,
377
- db: config.database,
378
- });
379
- }
380
- buildCommonRedisOptions() {
381
- const options = {
382
- enableReadyCheck: true,
383
- enableOfflineQueue: true,
384
- lazyConnect: false,
385
- };
386
- if (this.config.username !== undefined) {
387
- options.username = this.config.username;
388
- }
389
- if (this.config.password !== undefined) {
390
- options.password = this.config.password;
391
- }
392
- if (this.config.tls) {
393
- options.tls =
394
- this.config.tlsOptions ?? {
395
- rejectUnauthorized: true,
396
- };
397
- }
398
- return options;
399
- }
400
- // ========================================================================
401
- // Events
402
- // ========================================================================
403
- setupEventHandlers() {
404
- this.client.on("connect", () => {
405
- this.logger.info("Redis connected");
406
- });
407
- this.client.on("ready", () => {
408
- this.isReady = true;
409
- this.logger.info("Redis ready");
410
- });
411
- this.client.on("error", (error) => {
412
- this.isReady = false;
413
- this.logger.error("Redis error:", {
414
- error,
415
- });
416
- });
417
- this.client.on("close", () => {
418
- this.isReady = false;
419
- this.logger.warn("Redis connection closed");
420
- });
421
- this.client.on("reconnecting", () => {
422
- this.isReady = false;
423
- this.logger.info("Redis reconnecting...");
424
- });
425
- }
426
- // ========================================================================
427
- // Raw Client
428
- // ========================================================================
429
- getRawClient() {
430
- return this.client;
431
- }
432
- get raw() {
433
- return this.client;
434
- }
435
- // ========================================================================
436
- // Connection
437
- // ========================================================================
438
- async ping() {
439
- try {
440
- const result = await this.exec("PING", [], () => this.client.ping());
441
- return result === "PONG";
442
- }
443
- catch {
444
- return false;
445
- }
446
- }
447
- async close() {
448
- try {
449
- await this.client.quit();
450
- }
451
- finally {
452
- this.isReady = false;
453
- }
454
- }
455
- // ========================================================================
456
- // Internal Helpers
457
- // ========================================================================
458
- isClusterClient(client) {
459
- return client instanceof Cluster;
460
- }
461
- async exec(command, args, operation) {
462
- const startedAt = Date.now();
463
- try {
464
- const result = await operation();
465
- const duration = Date.now() - startedAt;
466
- if (duration >
467
- this.config.slowCommandThreshold) {
468
- this.logger.warn(`Slow command: ${command} took ${duration}ms`, {
469
- command,
470
- args: args.slice(0, 5),
471
- duration,
472
- });
473
- }
474
- return result;
475
- }
476
- catch (error) {
477
- this.logger.error(`Command failed: ${command}`, {
478
- command,
479
- error,
480
- });
481
- throw error;
482
- }
483
- }
484
- // ========================================================================
485
- // Basic Commands
486
- // ========================================================================
487
- async get(key) {
488
- return this.exec("GET", [key], () => this.client.get(key));
489
- }
490
- async set(key, value, ttl) {
491
- if (ttl !== undefined) {
492
- return this.exec("SET", [key, value, "EX", ttl], () => this.client.set(key, value, "EX", ttl));
493
- }
494
- return this.exec("SET", [key, value], () => this.client.set(key, value));
495
- }
496
- async setexnx(key, value, ttl) {
497
- if (ttl !== undefined) {
498
- return this.exec("SET", [key, value, "EX", ttl, "NX"], () => this.client.set(key, value, "EX", ttl, "NX"));
499
- }
500
- return this.exec("SET", [key, value, "NX"], () => this.client.set(key, value, "NX"));
501
- }
502
- defineCommand(...args) {
503
- return this.client.defineCommand(...args);
504
- }
505
- async eval(script, numKeys, ...args) {
506
- return this.exec("EVAL", [script, numKeys, ...args], () => this.client.eval(script, numKeys, ...args));
507
- }
508
- async evalsha(sha, script, numKeys, ...args) {
509
- try {
510
- return await this.exec("EVALSHA", [sha, numKeys, ...args], () => this.client.evalsha(sha, numKeys, ...args));
511
- }
512
- catch (error) {
513
- const code = error instanceof Error &&
514
- "code" in error &&
515
- typeof error.code === "string"
516
- ? error.code
517
- : undefined;
518
- const message = error instanceof Error
519
- ? error.message
520
- : String(error);
521
- if (code === "NOSCRIPT" ||
522
- message.includes("NOSCRIPT")) {
523
- return this.eval(script, numKeys, ...args);
524
- }
525
- throw error;
526
- }
527
- }
528
- async scriptLoad(script) {
529
- return this.exec("SCRIPT LOAD", [script], () => this.client.script("LOAD", script));
530
- }
531
- async setnx(key, value, ttl) {
532
- const result = ttl !== undefined
533
- ? await this.exec("SET", [key, value, "EX", ttl, "NX"], () => this.client.set(key, value, "EX", ttl, "NX"))
534
- : await this.exec("SET", [key, value, "NX"], () => this.client.set(key, value, "NX"));
535
- return result === "OK" ? 1 : 0;
536
- }
537
- async del(...keys) {
538
- if (keys.length === 0) {
539
- return 0;
540
- }
541
- return this.exec("DEL", keys, () => this.client.del(...keys));
542
- }
543
- async getdel(key) {
544
- return this.exec("GETDEL", [key], () => this.client.getdel(key));
545
- }
546
- async exists(key) {
547
- return this.exec("EXISTS", [key], () => this.client.exists(key));
548
- }
549
- async expire(key, ttl) {
550
- return this.exec("EXPIRE", [key, ttl], () => this.client.expire(key, ttl));
551
- }
552
- async ttl(key) {
553
- return this.exec("TTL", [key], () => this.client.ttl(key));
554
- }
555
- async incr(key) {
556
- return this.exec("INCR", [key], () => this.client.incr(key));
557
- }
558
- async decr(key) {
559
- return this.exec("DECR", [key], () => this.client.decr(key));
560
- }
561
- async incrby(key, amount) {
562
- if (!Number.isSafeInteger(amount) ||
563
- amount <= 0) {
564
- throw new RedisError("INCRBY amount must be a positive safe integer", "INVALID_AMOUNT");
565
- }
566
- return this.exec("INCRBY", [key, amount], () => this.client.incrby(key, amount));
567
- }
568
- async decrby(key, amount) {
569
- if (!Number.isSafeInteger(amount) ||
570
- amount <= 0) {
571
- throw new RedisError("DECRBY amount must be a positive safe integer", "INVALID_AMOUNT");
572
- }
573
- return this.exec("DECRBY", [key, amount], () => this.client.decrby(key, amount));
574
- }
575
- async time() {
576
- const result = await this.exec("TIME", [], () => this.client.time());
577
- if (Array.isArray(result) &&
578
- result.length > 0) {
579
- const seconds = Number(result[0]);
580
- if (Number.isSafeInteger(seconds)) {
581
- return seconds;
582
- }
583
- }
584
- throw new RedisError("Unexpected TIME response from Redis", "TIME_ERROR");
585
- }
586
- // ========================================================================
587
- // Multi-Key Commands
588
- // ========================================================================
589
- async mget(...keys) {
590
- if (keys.length === 0) {
591
- return [];
592
- }
593
- if (this.isClusterClient(this.client)) {
594
- return this.mgetClusterAware(keys);
595
- }
596
- return this.exec("MGET", keys, () => this.client.mget(...keys));
597
- }
598
- async mset(...pairs) {
599
- if (pairs.length === 0) {
600
- return "OK";
601
- }
602
- if (this.isClusterClient(this.client)) {
603
- const groups = new Map();
604
- for (const [key, value,] of pairs) {
605
- const slot = this.calculateSlot(key);
606
- const group = groups.get(slot) ?? [];
607
- group.push(key, value);
608
- groups.set(slot, group);
609
- }
610
- const tasks = [...groups.values()];
611
- await this.runWithConcurrency(tasks, this.config.maxFanOutConcurrency, async (flat) => {
612
- await this.exec("MSET", flat, () => this.client.mset(flat));
613
- });
614
- return "OK";
615
- }
616
- const flat = pairs.flat();
617
- return this.exec("MSET", flat, () => this.client.mset(flat));
618
- }
619
- // ========================================================================
620
- // Hash Commands
621
- // ========================================================================
622
- async hget(key, field) {
623
- return this.exec("HGET", [key, field], () => this.client.hget(key, field));
624
- }
625
- async hset(key, field, value) {
626
- return this.exec("HSET", [key, field, value], () => this.client.hset(key, field, value));
627
- }
628
- async hgetall(key) {
629
- return this.exec("HGETALL", [key], () => this.client.hgetall(key));
630
- }
631
- async hdel(key, ...fields) {
632
- if (fields.length === 0) {
633
- return 0;
634
- }
635
- return this.exec("HDEL", [key, ...fields], () => this.client.hdel(key, ...fields));
636
- }
637
- // ========================================================================
638
- // Set Commands
639
- // ========================================================================
640
- async sadd(key, ...members) {
641
- if (members.length === 0) {
642
- return 0;
643
- }
644
- return this.exec("SADD", [key, ...members], () => this.client.sadd(key, ...members));
645
- }
646
- async srem(key, ...members) {
647
- if (members.length === 0) {
648
- return 0;
649
- }
650
- return this.exec("SREM", [key, ...members], () => this.client.srem(key, ...members));
651
- }
652
- async smembers(key) {
653
- return this.exec("SMEMBERS", [key], () => this.client.smembers(key));
654
- }
655
- async sismember(key, member) {
656
- return this.exec("SISMEMBER", [key, member], () => this.client.sismember(key, member));
657
- }
658
- // ========================================================================
659
- // Sorted Set Commands
660
- // ========================================================================
661
- async zadd(key, score, member) {
662
- return this.exec("ZADD", [key, score, member], () => this.client.zadd(key, score, member));
663
- }
664
- async zrange(key, start, stop) {
665
- return this.exec("ZRANGE", [key, start, stop], () => this.client.zrange(key, start, stop));
666
- }
667
- async zcard(key) {
668
- return this.exec("ZCARD", [key], () => this.client.zcard(key));
669
- }
670
- async zrem(key, ...members) {
671
- if (members.length === 0) {
672
- return 0;
673
- }
674
- return this.exec("ZREM", [key, ...members], () => this.client.zrem(key, ...members));
675
- }
676
- // ========================================================================
677
- // Pipeline
678
- // ========================================================================
679
- pipeline() {
680
- return this.client.pipeline();
681
- }
682
- // ========================================================================
683
- // Scanning
684
- // ========================================================================
685
- async *scanIterator(pattern, count = 100) {
686
- for await (const batch of this.scanCluster(pattern, {
687
- count,
688
- batchSize: 1,
689
- })) {
690
- for (const key of batch) {
691
- yield key;
692
- }
693
- }
694
- }
695
- async *scanCluster(pattern, options = {}) {
696
- const count = options.count ?? 100;
697
- const batchSize = Math.max(1, options.batchSize ?? 100);
698
- const scanNode = async function* (node) {
699
- let cursor = "0";
700
- let buffer = [];
701
- do {
702
- const [nextCursor, keys,] = await node.scan(cursor, "MATCH", pattern, "COUNT", count);
703
- cursor = nextCursor;
704
- buffer.push(...keys);
705
- while (buffer.length >=
706
- batchSize) {
707
- yield buffer.splice(0, batchSize);
708
- }
709
- } while (cursor !== "0");
710
- if (buffer.length > 0) {
711
- yield buffer;
712
- }
713
- };
714
- const nodes = this.isClusterClient(this.client)
715
- ? this.getClusterNodes()
716
- : [this.client];
717
- for (const node of nodes) {
718
- yield* scanNode(node);
719
- }
720
- }
721
- // ========================================================================
722
- // Cluster
723
- // ========================================================================
724
- isCluster() {
725
- return this.isClusterClient(this.client);
726
- }
727
- getClusterNodes() {
728
- if (!this.isClusterClient(this.client)) {
729
- return [];
730
- }
731
- return this.client.nodes("master");
732
- }
733
- async getClusterSlots() {
734
- if (!this.isClusterClient(this.client)) {
735
- return [];
736
- }
737
- const raw = (await this.exec("CLUSTER SLOTS", [], () => this.client.cluster("SLOTS")));
738
- if (!Array.isArray(raw)) {
739
- return [];
740
- }
741
- return raw.flatMap((entry) => {
742
- if (!Array.isArray(entry) ||
743
- entry.length < 3) {
744
- return [];
745
- }
746
- const [start, end, master, ...replicas] = entry;
747
- if (!Number.isInteger(start) ||
748
- !Number.isInteger(end) ||
749
- !Array.isArray(master)) {
750
- return [];
751
- }
752
- const parseNode = (node) => {
753
- if (!Array.isArray(node) ||
754
- node.length < 2) {
755
- return null;
756
- }
757
- const host = typeof node[0] === "string"
758
- ? node[0]
759
- : String(node[0]);
760
- const port = typeof node[1] === "number"
761
- ? node[1]
762
- : Number(node[1]);
763
- if (host.length === 0 ||
764
- !Number.isInteger(port) ||
765
- port < 1 ||
766
- port > 65_535) {
767
- return null;
768
- }
769
- const nodeId = node[2] === undefined
770
- ? undefined
771
- : String(node[2]);
772
- return nodeId === undefined
773
- ? {
774
- host,
775
- port,
776
- }
777
- : {
778
- host,
779
- port,
780
- nodeId,
781
- };
782
- };
783
- const parsedMaster = parseNode(master);
784
- if (!parsedMaster) {
785
- return [];
786
- }
787
- const parsedReplicas = replicas
788
- .map(parseNode)
789
- .filter((node) => node !== null);
790
- return [
791
- {
792
- start,
793
- end,
794
- master: parsedMaster,
795
- replicas: parsedReplicas,
796
- },
797
- ];
798
- });
799
- }
800
- calculateSlot(key) {
801
- return calculateRedisClusterSlot(key);
802
- }
803
- async getNodeForKey(key) {
804
- if (!this.isClusterClient(this.client)) {
805
- return null;
806
- }
807
- const nodes = this.client.nodes("master");
808
- if (nodes.length === 0) {
809
- return null;
810
- }
811
- try {
812
- const slot = this.calculateSlot(key);
813
- const raw = await this.client.cluster("SLOTS");
814
- if (!Array.isArray(raw)) {
815
- return null;
816
- }
817
- for (const entry of raw) {
818
- if (!Array.isArray(entry) ||
819
- entry.length < 3) {
820
- continue;
821
- }
822
- const [startSlot, endSlot, master,] = entry;
823
- if (typeof startSlot !==
824
- "number" ||
825
- typeof endSlot !==
826
- "number" ||
827
- !Array.isArray(master)) {
828
- continue;
829
- }
830
- if (slot < startSlot ||
831
- slot > endSlot) {
832
- continue;
833
- }
834
- const host = String(master[2] ?? "");
835
- const port = Number(master[1]);
836
- if (host.length === 0 ||
837
- !Number.isInteger(port)) {
838
- continue;
839
- }
840
- const node = nodes.find((candidate) => candidate.options
841
- .host === host &&
842
- candidate.options
843
- .port === port);
844
- if (node) {
845
- return node;
846
- }
847
- }
848
- }
849
- catch (error) {
850
- this.logger.debug("Unable to resolve cluster node for key", {
851
- error,
852
- });
853
- }
854
- return null;
855
- }
856
- async getSlotRanges() {
857
- const result = new Map();
858
- const ranges = await this.getClusterSlots();
859
- for (const range of ranges) {
860
- const nodeId = `${range.master.host}:${range.master.port}`;
861
- for (let slot = range.start; slot <= range.end; slot++) {
862
- result.set(slot, [
863
- nodeId,
864
- ]);
865
- }
866
- }
867
- return result;
868
- }
869
- async isKeyServed(key) {
870
- if (!this.isClusterClient(this.client)) {
871
- return true;
872
- }
873
- try {
874
- const node = await this.getNodeForKey(key);
875
- return node !== null;
876
- }
877
- catch {
878
- return false;
879
- }
880
- }
881
- async executeOnNode(key, command, ...args) {
882
- if (this.isClusterClient(this.client)) {
883
- const node = await this.getNodeForKey(key);
884
- if (node) {
885
- return this.executeCommandOnClient(node, command, args);
886
- }
887
- }
888
- return this.executeCommandOnClient(this.client, command, args);
889
- }
890
- executeCommandOnClient(client, command, args) {
891
- const method = Reflect.get(client, command);
892
- if (typeof method !==
893
- "function") {
894
- throw new RedisError(`Unsupported Redis command: ${command}`, "UNSUPPORTED_COMMAND");
895
- }
896
- return Reflect.apply(method, client, args);
897
- }
898
- // ========================================================================
899
- // Cluster-Aware MGET
900
- // ========================================================================
901
- async mgetClusterAware(keys) {
902
- if (keys.length === 0) {
903
- return [];
904
- }
905
- if (!this.isClusterClient(this.client)) {
906
- return this.mget(...keys);
907
- }
908
- const groups = new Map();
909
- for (const key of keys) {
910
- const slot = this.calculateSlot(key);
911
- const group = groups.get(slot) ?? [];
912
- group.push(key);
913
- groups.set(slot, group);
914
- }
915
- const values = new Map();
916
- await this.runWithConcurrency([...groups.values()], this.config
917
- .maxFanOutConcurrency, async (slotKeys) => {
918
- const result = await this.exec("MGET", slotKeys, () => this.client.mget(...slotKeys));
919
- slotKeys.forEach((key, index) => {
920
- values.set(key, result[index] ??
921
- null);
922
- });
923
- });
924
- return keys.map((key) => values.get(key) ??
925
- null);
926
- }
927
- // ========================================================================
928
- // Namespace Operations
929
- // ========================================================================
930
- async deletePattern(pattern, options = {}) {
931
- const batchSize = Math.max(1, options.batchSize ??
932
- 100);
933
- const scanCount = Math.max(1, options.scanCount ??
934
- 100);
935
- let deleted = 0;
936
- for await (const batch of this.scanCluster(pattern, {
937
- count: scanCount,
938
- batchSize,
939
- })) {
940
- if (batch.length === 0) {
941
- continue;
942
- }
943
- const commands = batch.map((key) => ({
944
- command: "del",
945
- args: [key],
946
- slot: this.calculateSlot(key),
947
- }));
948
- const results = await executeBySlot(this, commands, {
949
- concurrency: this.config
950
- .maxFanOutConcurrency,
951
- retry: 1,
952
- });
953
- for (const result of results) {
954
- if (!result[0] &&
955
- typeof result[1] ===
956
- "number") {
957
- deleted += result[1];
958
- }
959
- }
960
- }
961
- return deleted;
962
- }
963
- async clearNamespace(prefix, options = {}) {
964
- return this.deletePattern(`${prefix}*`, options);
965
- }
966
- async clearNamespaceClusterAware(prefix, options = {}) {
967
- return this.clearNamespace(prefix, options);
968
- }
969
- // ========================================================================
970
- // Cluster Information
971
- // ========================================================================
972
- getClusterInfo() {
973
- if (this.isClusterClient(this.client)) {
974
- try {
975
- const nodes = this.client.nodes("master");
976
- return {
977
- mode: "cluster",
978
- status: this.isReady
979
- ? "ready"
980
- : "connecting",
981
- nodeCount: nodes.length,
982
- nodes: nodes.map((node) => ({
983
- host: String(node.options
984
- .host ??
985
- ""),
986
- port: Number(node.options
987
- .port),
988
- role: "master",
989
- })),
990
- };
991
- }
992
- catch (error) {
993
- return {
994
- mode: "cluster",
995
- status: "error",
996
- error: error instanceof Error
997
- ? error.message
998
- : String(error),
999
- };
1000
- }
1001
- }
1002
- if (isStandaloneConfig(this.config)) {
1003
- return {
1004
- mode: "standalone",
1005
- host: this.config.host,
1006
- port: this.config.port,
1007
- status: this.isReady
1008
- ? "ready"
1009
- : "connecting",
1010
- };
1011
- }
1012
- return {
1013
- mode: this.config.mode,
1014
- status: this.isReady
1015
- ? "ready"
1016
- : "connecting",
1017
- };
1018
- }
1019
- // ========================================================================
1020
- // INFO
1021
- // ========================================================================
1022
- async info(section) {
1023
- if (section !== undefined) {
1024
- return this.exec("INFO", [section], () => this.client.info(section));
1025
- }
1026
- return this.exec("INFO", [], () => this.client.info());
1027
- }
1028
- // ========================================================================
1029
- // SELECT
1030
- // ========================================================================
1031
- async select(database) {
1032
- if (this.isClusterClient(this.client)) {
1033
- throw new RedisError("SELECT is not supported in Redis Cluster mode", "CLUSTER_MODE");
1034
- }
1035
- if (!Number.isInteger(database) ||
1036
- database < 0 ||
1037
- database > 15) {
1038
- throw new RedisError("Database must be an integer between 0 and 15", "INVALID_DATABASE");
1039
- }
1040
- return this.client.select(database);
1041
- }
1042
- // ========================================================================
1043
- // Connection Status
1044
- // ========================================================================
1045
- getConnectionStatus() {
1046
- const state = this.isReady
1047
- ? "connected"
1048
- : this.client.status ===
1049
- "connecting"
1050
- ? "connecting"
1051
- : this.client.status ===
1052
- "end"
1053
- ? "closed"
1054
- : this.client.status ===
1055
- "ready"
1056
- ? "connected"
1057
- : "disconnected";
1058
- return {
1059
- state,
1060
- connected: this.isReady,
1061
- ready: this.isReady,
1062
- reconnectAttempts: 0,
1063
- uptime: 0,
1064
- };
1065
- }
1066
- // ========================================================================
1067
- // Concurrency Helper
1068
- // ========================================================================
1069
- async runWithConcurrency(items, concurrency, worker) {
1070
- if (items.length === 0) {
1071
- return;
1072
- }
1073
- const limit = Math.max(1, Math.min(concurrency, items.length));
1074
- let cursor = 0;
1075
- const workers = Array.from({
1076
- length: limit,
1077
- }, async () => {
1078
- while (true) {
1079
- const index = cursor++;
1080
- if (index >=
1081
- items.length) {
1082
- return;
1083
- }
1084
- await worker(items[index]);
1085
- }
1086
- });
1087
- await Promise.all(workers);
1088
- }
1089
- }
1090
- /**
1091
- * Generic runtime factory.
1092
- */
1093
- export function createRedisClient(config, logger) {
1094
- const client = new RedisClientWrapper(config, logger);
1095
- /**
1096
- * The runtime configuration has already been validated by
1097
- * RedisConfigSchema inside RedisClientWrapper.
1098
- *
1099
- * The specialized public API is therefore safe to expose according to
1100
- * the validated discriminator.
1101
- *
1102
- * The cast is isolated to this factory boundary instead of being spread
1103
- * throughout the implementation.
1104
- */
1105
- switch (client.mode) {
1106
- case "cluster":
1107
- return client;
1108
- case "sentinel":
1109
- return client;
1110
- case "standalone":
1111
- return client;
1112
- }
1113
- }