ioredis-toolkit 0.0.9 → 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 -1058
  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 -796
  198. package/dist/cache.js +0 -1120
  199. package/dist/client.d.ts +0 -284
  200. package/dist/client.js +0 -1114
  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 -248
  210. package/dist/lock.js +0 -397
  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 -110
  230. package/dist/session/scripts/rotate.lua +0 -122
  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 -335
  237. package/dist/session/session-config.js +0 -162
  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 -51
  247. package/dist/session/session-keys.js +0 -113
  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 -33
  251. package/dist/session/session-metrics.js +0 -112
  252. package/dist/session/session-repository.d.ts +0 -161
  253. package/dist/session/session-repository.js +0 -683
  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 -248
  258. package/dist/session/session-service.d.ts +0 -104
  259. package/dist/session/session-service.js +0 -611
  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 -253
  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,1114 +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
- function isClusterConfig(config) {
19
- return config.mode === "cluster";
20
- }
21
- function isSentinelConfig(config) {
22
- return config.mode === "sentinel";
23
- }
24
- 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
- void this._session.init();
299
- this.config.sessionOptions = params;
300
- }
301
- return this._session;
302
- }
303
- withSession(value) {
304
- const revocationRedisStore = this.revocationStore;
305
- const config = deepMerge(this.config.sessionOptions ?? {}, value);
306
- const newConfig = {
307
- ...config,
308
- client: this,
309
- revocationStore: revocationRedisStore,
310
- };
311
- this.config.sessionOptions = newConfig;
312
- this._session = undefined;
313
- return this;
314
- }
315
- // ========================================================================
316
- // Client Creation
317
- // ========================================================================
318
- createClient() {
319
- const common = this.buildCommonRedisOptions();
320
- const retryStrategy = (times) => {
321
- if (times > this.config.maxRetries) {
322
- return null;
323
- }
324
- return Math.min(times * this.config.retryDelay, 5_000);
325
- };
326
- switch (this.config.mode) {
327
- case "cluster":
328
- return this.createClusterClient(this.config, common, retryStrategy);
329
- case "sentinel":
330
- return this.createSentinelClient(this.config, common, retryStrategy);
331
- case "standalone":
332
- return this.createStandaloneClient(this.config, common, retryStrategy);
333
- }
334
- }
335
- createClusterClient(config, common, retryStrategy) {
336
- return new Cluster(config.clusterNodes.map(({ host, port }) => ({
337
- host,
338
- port,
339
- })), {
340
- clusterRetryStrategy: retryStrategy,
341
- enableOfflineQueue: true,
342
- scaleReads: "master",
343
- redisOptions: {
344
- ...common,
345
- connectTimeout: config.connectionTimeout,
346
- maxRetriesPerRequest: config.maxRetries,
347
- },
348
- });
349
- }
350
- createSentinelClient(config, common, retryStrategy) {
351
- return new RedisClient({
352
- ...common,
353
- connectTimeout: config.connectionTimeout,
354
- maxRetriesPerRequest: config.maxRetries,
355
- retryStrategy,
356
- // sentinel: true,
357
- sentinels: config.sentinelNodes.map(({ host, port }) => ({
358
- host,
359
- port,
360
- })),
361
- name: config.sentinelMasterName,
362
- });
363
- }
364
- createStandaloneClient(config, common, retryStrategy) {
365
- const connectionOptions = {
366
- ...common,
367
- connectTimeout: config.connectionTimeout,
368
- maxRetriesPerRequest: config.maxRetries,
369
- retryStrategy,
370
- };
371
- if (config.url !== undefined) {
372
- return new RedisClient(config.url, connectionOptions);
373
- }
374
- return new RedisClient({
375
- ...connectionOptions,
376
- host: config.host,
377
- port: config.port,
378
- db: config.database,
379
- });
380
- }
381
- buildCommonRedisOptions() {
382
- const options = {
383
- enableReadyCheck: true,
384
- enableOfflineQueue: true,
385
- lazyConnect: false,
386
- };
387
- if (this.config.username !== undefined) {
388
- options.username = this.config.username;
389
- }
390
- if (this.config.password !== undefined) {
391
- options.password = this.config.password;
392
- }
393
- if (this.config.tls) {
394
- options.tls =
395
- this.config.tlsOptions ?? {
396
- rejectUnauthorized: true,
397
- };
398
- }
399
- return options;
400
- }
401
- // ========================================================================
402
- // Events
403
- // ========================================================================
404
- setupEventHandlers() {
405
- this.client.on("connect", () => {
406
- this.logger.info("Redis connected");
407
- });
408
- this.client.on("ready", () => {
409
- this.isReady = true;
410
- this.logger.info("Redis ready");
411
- });
412
- this.client.on("error", (error) => {
413
- this.isReady = false;
414
- this.logger.error("Redis error:", {
415
- error,
416
- });
417
- });
418
- this.client.on("close", () => {
419
- this.isReady = false;
420
- this.logger.warn("Redis connection closed");
421
- });
422
- this.client.on("reconnecting", () => {
423
- this.isReady = false;
424
- this.logger.info("Redis reconnecting...");
425
- });
426
- }
427
- // ========================================================================
428
- // Raw Client
429
- // ========================================================================
430
- getRawClient() {
431
- return this.client;
432
- }
433
- get raw() {
434
- return this.client;
435
- }
436
- // ========================================================================
437
- // Connection
438
- // ========================================================================
439
- async ping() {
440
- try {
441
- const result = await this.exec("PING", [], () => this.client.ping());
442
- return result === "PONG";
443
- }
444
- catch {
445
- return false;
446
- }
447
- }
448
- async close() {
449
- try {
450
- await this.client.quit();
451
- }
452
- finally {
453
- this.isReady = false;
454
- }
455
- }
456
- // ========================================================================
457
- // Internal Helpers
458
- // ========================================================================
459
- isClusterClient(client) {
460
- return client instanceof Cluster;
461
- }
462
- async exec(command, args, operation) {
463
- const startedAt = Date.now();
464
- try {
465
- const result = await operation();
466
- const duration = Date.now() - startedAt;
467
- if (duration >
468
- this.config.slowCommandThreshold) {
469
- this.logger.warn(`Slow command: ${command} took ${duration}ms`, {
470
- command,
471
- args: args.slice(0, 5),
472
- duration,
473
- });
474
- }
475
- return result;
476
- }
477
- catch (error) {
478
- this.logger.error(`Command failed: ${command}`, {
479
- command,
480
- error,
481
- });
482
- throw error;
483
- }
484
- }
485
- // ========================================================================
486
- // Basic Commands
487
- // ========================================================================
488
- async get(key) {
489
- return this.exec("GET", [key], () => this.client.get(key));
490
- }
491
- async set(key, value, ttl) {
492
- if (ttl !== undefined) {
493
- return this.exec("SET", [key, value, "EX", ttl], () => this.client.set(key, value, "EX", ttl));
494
- }
495
- return this.exec("SET", [key, value], () => this.client.set(key, value));
496
- }
497
- async setexnx(key, value, ttl) {
498
- if (ttl !== undefined) {
499
- return this.exec("SET", [key, value, "EX", ttl, "NX"], () => this.client.set(key, value, "EX", ttl, "NX"));
500
- }
501
- return this.exec("SET", [key, value, "NX"], () => this.client.set(key, value, "NX"));
502
- }
503
- defineCommand(...args) {
504
- return this.client.defineCommand(...args);
505
- }
506
- async eval(script, numKeys, ...args) {
507
- return this.exec("EVAL", [script, numKeys, ...args], () => this.client.eval(script, numKeys, ...args));
508
- }
509
- async evalsha(sha, script, numKeys, ...args) {
510
- try {
511
- return await this.exec("EVALSHA", [sha, numKeys, ...args], () => this.client.evalsha(sha, numKeys, ...args));
512
- }
513
- catch (error) {
514
- const code = error instanceof Error &&
515
- "code" in error &&
516
- typeof error.code === "string"
517
- ? error.code
518
- : undefined;
519
- const message = error instanceof Error
520
- ? error.message
521
- : String(error);
522
- if (code === "NOSCRIPT" ||
523
- message.includes("NOSCRIPT")) {
524
- return this.eval(script, numKeys, ...args);
525
- }
526
- throw error;
527
- }
528
- }
529
- async scriptLoad(script) {
530
- return this.exec("SCRIPT LOAD", [script], () => this.client.script("LOAD", script));
531
- }
532
- async setnx(key, value, ttl) {
533
- const result = ttl !== undefined
534
- ? await this.exec("SET", [key, value, "EX", ttl, "NX"], () => this.client.set(key, value, "EX", ttl, "NX"))
535
- : await this.exec("SET", [key, value, "NX"], () => this.client.set(key, value, "NX"));
536
- return result === "OK" ? 1 : 0;
537
- }
538
- async del(...keys) {
539
- if (keys.length === 0) {
540
- return 0;
541
- }
542
- return this.exec("DEL", keys, () => this.client.del(...keys));
543
- }
544
- async getdel(key) {
545
- return this.exec("GETDEL", [key], () => this.client.getdel(key));
546
- }
547
- async exists(key) {
548
- return this.exec("EXISTS", [key], () => this.client.exists(key));
549
- }
550
- async expire(key, ttl) {
551
- return this.exec("EXPIRE", [key, ttl], () => this.client.expire(key, ttl));
552
- }
553
- async ttl(key) {
554
- return this.exec("TTL", [key], () => this.client.ttl(key));
555
- }
556
- async incr(key) {
557
- return this.exec("INCR", [key], () => this.client.incr(key));
558
- }
559
- async decr(key) {
560
- return this.exec("DECR", [key], () => this.client.decr(key));
561
- }
562
- async incrby(key, amount) {
563
- if (!Number.isSafeInteger(amount) ||
564
- amount <= 0) {
565
- throw new RedisError("INCRBY amount must be a positive safe integer", "INVALID_AMOUNT");
566
- }
567
- return this.exec("INCRBY", [key, amount], () => this.client.incrby(key, amount));
568
- }
569
- async decrby(key, amount) {
570
- if (!Number.isSafeInteger(amount) ||
571
- amount <= 0) {
572
- throw new RedisError("DECRBY amount must be a positive safe integer", "INVALID_AMOUNT");
573
- }
574
- return this.exec("DECRBY", [key, amount], () => this.client.decrby(key, amount));
575
- }
576
- async time() {
577
- const result = await this.exec("TIME", [], () => this.client.time());
578
- if (Array.isArray(result) &&
579
- result.length > 0) {
580
- const seconds = Number(result[0]);
581
- if (Number.isSafeInteger(seconds)) {
582
- return seconds;
583
- }
584
- }
585
- throw new RedisError("Unexpected TIME response from Redis", "TIME_ERROR");
586
- }
587
- // ========================================================================
588
- // Multi-Key Commands
589
- // ========================================================================
590
- async mget(...keys) {
591
- if (keys.length === 0) {
592
- return [];
593
- }
594
- if (this.isClusterClient(this.client)) {
595
- return this.mgetClusterAware(keys);
596
- }
597
- return this.exec("MGET", keys, () => this.client.mget(...keys));
598
- }
599
- async mset(...pairs) {
600
- if (pairs.length === 0) {
601
- return "OK";
602
- }
603
- if (this.isClusterClient(this.client)) {
604
- const groups = new Map();
605
- for (const [key, value,] of pairs) {
606
- const slot = this.calculateSlot(key);
607
- const group = groups.get(slot) ?? [];
608
- group.push(key, value);
609
- groups.set(slot, group);
610
- }
611
- const tasks = [...groups.values()];
612
- await this.runWithConcurrency(tasks, this.config.maxFanOutConcurrency, async (flat) => {
613
- await this.exec("MSET", flat, () => this.client.mset(flat));
614
- });
615
- return "OK";
616
- }
617
- const flat = pairs.flat();
618
- return this.exec("MSET", flat, () => this.client.mset(flat));
619
- }
620
- // ========================================================================
621
- // Hash Commands
622
- // ========================================================================
623
- async hget(key, field) {
624
- return this.exec("HGET", [key, field], () => this.client.hget(key, field));
625
- }
626
- async hset(key, field, value) {
627
- return this.exec("HSET", [key, field, value], () => this.client.hset(key, field, value));
628
- }
629
- async hgetall(key) {
630
- return this.exec("HGETALL", [key], () => this.client.hgetall(key));
631
- }
632
- async hdel(key, ...fields) {
633
- if (fields.length === 0) {
634
- return 0;
635
- }
636
- return this.exec("HDEL", [key, ...fields], () => this.client.hdel(key, ...fields));
637
- }
638
- // ========================================================================
639
- // Set Commands
640
- // ========================================================================
641
- async sadd(key, ...members) {
642
- if (members.length === 0) {
643
- return 0;
644
- }
645
- return this.exec("SADD", [key, ...members], () => this.client.sadd(key, ...members));
646
- }
647
- async srem(key, ...members) {
648
- if (members.length === 0) {
649
- return 0;
650
- }
651
- return this.exec("SREM", [key, ...members], () => this.client.srem(key, ...members));
652
- }
653
- async smembers(key) {
654
- return this.exec("SMEMBERS", [key], () => this.client.smembers(key));
655
- }
656
- async sismember(key, member) {
657
- return this.exec("SISMEMBER", [key, member], () => this.client.sismember(key, member));
658
- }
659
- // ========================================================================
660
- // Sorted Set Commands
661
- // ========================================================================
662
- async zadd(key, score, member) {
663
- return this.exec("ZADD", [key, score, member], () => this.client.zadd(key, score, member));
664
- }
665
- async zrange(key, start, stop) {
666
- return this.exec("ZRANGE", [key, start, stop], () => this.client.zrange(key, start, stop));
667
- }
668
- async zcard(key) {
669
- return this.exec("ZCARD", [key], () => this.client.zcard(key));
670
- }
671
- async zrem(key, ...members) {
672
- if (members.length === 0) {
673
- return 0;
674
- }
675
- return this.exec("ZREM", [key, ...members], () => this.client.zrem(key, ...members));
676
- }
677
- // ========================================================================
678
- // Pipeline
679
- // ========================================================================
680
- pipeline() {
681
- return this.client.pipeline();
682
- }
683
- // ========================================================================
684
- // Scanning
685
- // ========================================================================
686
- async *scanIterator(pattern, count = 100) {
687
- for await (const batch of this.scanCluster(pattern, {
688
- count,
689
- batchSize: 1,
690
- })) {
691
- for (const key of batch) {
692
- yield key;
693
- }
694
- }
695
- }
696
- async *scanCluster(pattern, options = {}) {
697
- const count = options.count ?? 100;
698
- const batchSize = Math.max(1, options.batchSize ?? 100);
699
- const scanNode = async function* (node) {
700
- let cursor = "0";
701
- let buffer = [];
702
- do {
703
- const [nextCursor, keys,] = await node.scan(cursor, "MATCH", pattern, "COUNT", count);
704
- cursor = nextCursor;
705
- buffer.push(...keys);
706
- while (buffer.length >=
707
- batchSize) {
708
- yield buffer.splice(0, batchSize);
709
- }
710
- } while (cursor !== "0");
711
- if (buffer.length > 0) {
712
- yield buffer;
713
- }
714
- };
715
- const nodes = this.isClusterClient(this.client)
716
- ? this.getClusterNodes()
717
- : [this.client];
718
- for (const node of nodes) {
719
- yield* scanNode(node);
720
- }
721
- }
722
- // ========================================================================
723
- // Cluster
724
- // ========================================================================
725
- isCluster() {
726
- return this.isClusterClient(this.client);
727
- }
728
- getClusterNodes() {
729
- if (!this.isClusterClient(this.client)) {
730
- return [];
731
- }
732
- return this.client.nodes("master");
733
- }
734
- async getClusterSlots() {
735
- if (!this.isClusterClient(this.client)) {
736
- return [];
737
- }
738
- const raw = (await this.exec("CLUSTER SLOTS", [], () => this.client.cluster("SLOTS")));
739
- if (!Array.isArray(raw)) {
740
- return [];
741
- }
742
- return raw.flatMap((entry) => {
743
- if (!Array.isArray(entry) ||
744
- entry.length < 3) {
745
- return [];
746
- }
747
- const [start, end, master, ...replicas] = entry;
748
- if (!Number.isInteger(start) ||
749
- !Number.isInteger(end) ||
750
- !Array.isArray(master)) {
751
- return [];
752
- }
753
- const parseNode = (node) => {
754
- if (!Array.isArray(node) ||
755
- node.length < 2) {
756
- return null;
757
- }
758
- const host = typeof node[0] === "string"
759
- ? node[0]
760
- : String(node[0]);
761
- const port = typeof node[1] === "number"
762
- ? node[1]
763
- : Number(node[1]);
764
- if (host.length === 0 ||
765
- !Number.isInteger(port) ||
766
- port < 1 ||
767
- port > 65_535) {
768
- return null;
769
- }
770
- const nodeId = node[2] === undefined
771
- ? undefined
772
- : String(node[2]);
773
- return nodeId === undefined
774
- ? {
775
- host,
776
- port,
777
- }
778
- : {
779
- host,
780
- port,
781
- nodeId,
782
- };
783
- };
784
- const parsedMaster = parseNode(master);
785
- if (!parsedMaster) {
786
- return [];
787
- }
788
- const parsedReplicas = replicas
789
- .map(parseNode)
790
- .filter((node) => node !== null);
791
- return [
792
- {
793
- start,
794
- end,
795
- master: parsedMaster,
796
- replicas: parsedReplicas,
797
- },
798
- ];
799
- });
800
- }
801
- calculateSlot(key) {
802
- return calculateRedisClusterSlot(key);
803
- }
804
- async getNodeForKey(key) {
805
- if (!this.isClusterClient(this.client)) {
806
- return null;
807
- }
808
- const nodes = this.client.nodes("master");
809
- if (nodes.length === 0) {
810
- return null;
811
- }
812
- try {
813
- const slot = this.calculateSlot(key);
814
- const raw = await this.client.cluster("SLOTS");
815
- if (!Array.isArray(raw)) {
816
- return null;
817
- }
818
- for (const entry of raw) {
819
- if (!Array.isArray(entry) ||
820
- entry.length < 3) {
821
- continue;
822
- }
823
- const [startSlot, endSlot, master,] = entry;
824
- if (typeof startSlot !==
825
- "number" ||
826
- typeof endSlot !==
827
- "number" ||
828
- !Array.isArray(master)) {
829
- continue;
830
- }
831
- if (slot < startSlot ||
832
- slot > endSlot) {
833
- continue;
834
- }
835
- const host = String(master[2] ?? "");
836
- const port = Number(master[1]);
837
- if (host.length === 0 ||
838
- !Number.isInteger(port)) {
839
- continue;
840
- }
841
- const node = nodes.find((candidate) => candidate.options
842
- .host === host &&
843
- candidate.options
844
- .port === port);
845
- if (node) {
846
- return node;
847
- }
848
- }
849
- }
850
- catch (error) {
851
- this.logger.debug("Unable to resolve cluster node for key", {
852
- error,
853
- });
854
- }
855
- return null;
856
- }
857
- async getSlotRanges() {
858
- const result = new Map();
859
- const ranges = await this.getClusterSlots();
860
- for (const range of ranges) {
861
- const nodeId = `${range.master.host}:${range.master.port}`;
862
- for (let slot = range.start; slot <= range.end; slot++) {
863
- result.set(slot, [
864
- nodeId,
865
- ]);
866
- }
867
- }
868
- return result;
869
- }
870
- async isKeyServed(key) {
871
- if (!this.isClusterClient(this.client)) {
872
- return true;
873
- }
874
- try {
875
- const node = await this.getNodeForKey(key);
876
- return node !== null;
877
- }
878
- catch {
879
- return false;
880
- }
881
- }
882
- async executeOnNode(key, command, ...args) {
883
- if (this.isClusterClient(this.client)) {
884
- const node = await this.getNodeForKey(key);
885
- if (node) {
886
- return this.executeCommandOnClient(node, command, args);
887
- }
888
- }
889
- return this.executeCommandOnClient(this.client, command, args);
890
- }
891
- executeCommandOnClient(client, command, args) {
892
- const method = Reflect.get(client, command);
893
- if (typeof method !==
894
- "function") {
895
- throw new RedisError(`Unsupported Redis command: ${command}`, "UNSUPPORTED_COMMAND");
896
- }
897
- return Reflect.apply(method, client, args);
898
- }
899
- // ========================================================================
900
- // Cluster-Aware MGET
901
- // ========================================================================
902
- async mgetClusterAware(keys) {
903
- if (keys.length === 0) {
904
- return [];
905
- }
906
- if (!this.isClusterClient(this.client)) {
907
- return this.mget(...keys);
908
- }
909
- const groups = new Map();
910
- for (const key of keys) {
911
- const slot = this.calculateSlot(key);
912
- const group = groups.get(slot) ?? [];
913
- group.push(key);
914
- groups.set(slot, group);
915
- }
916
- const values = new Map();
917
- await this.runWithConcurrency([...groups.values()], this.config
918
- .maxFanOutConcurrency, async (slotKeys) => {
919
- const result = await this.exec("MGET", slotKeys, () => this.client.mget(...slotKeys));
920
- slotKeys.forEach((key, index) => {
921
- values.set(key, result[index] ??
922
- null);
923
- });
924
- });
925
- return keys.map((key) => values.get(key) ??
926
- null);
927
- }
928
- // ========================================================================
929
- // Namespace Operations
930
- // ========================================================================
931
- async deletePattern(pattern, options = {}) {
932
- const batchSize = Math.max(1, options.batchSize ??
933
- 100);
934
- const scanCount = Math.max(1, options.scanCount ??
935
- 100);
936
- let deleted = 0;
937
- for await (const batch of this.scanCluster(pattern, {
938
- count: scanCount,
939
- batchSize,
940
- })) {
941
- if (batch.length === 0) {
942
- continue;
943
- }
944
- const commands = batch.map((key) => ({
945
- command: "del",
946
- args: [key],
947
- slot: this.calculateSlot(key),
948
- }));
949
- const results = await executeBySlot(this, commands, {
950
- concurrency: this.config
951
- .maxFanOutConcurrency,
952
- retry: 1,
953
- });
954
- for (const result of results) {
955
- if (!result[0] &&
956
- typeof result[1] ===
957
- "number") {
958
- deleted += result[1];
959
- }
960
- }
961
- }
962
- return deleted;
963
- }
964
- async clearNamespace(prefix, options = {}) {
965
- return this.deletePattern(`${prefix}*`, options);
966
- }
967
- async clearNamespaceClusterAware(prefix, options = {}) {
968
- return this.clearNamespace(prefix, options);
969
- }
970
- // ========================================================================
971
- // Cluster Information
972
- // ========================================================================
973
- getClusterInfo() {
974
- if (this.isClusterClient(this.client)) {
975
- try {
976
- const nodes = this.client.nodes("master");
977
- return {
978
- mode: "cluster",
979
- status: this.isReady
980
- ? "ready"
981
- : "connecting",
982
- nodeCount: nodes.length,
983
- nodes: nodes.map((node) => ({
984
- host: String(node.options
985
- .host ??
986
- ""),
987
- port: Number(node.options
988
- .port),
989
- role: "master",
990
- })),
991
- };
992
- }
993
- catch (error) {
994
- return {
995
- mode: "cluster",
996
- status: "error",
997
- error: error instanceof Error
998
- ? error.message
999
- : String(error),
1000
- };
1001
- }
1002
- }
1003
- if (isStandaloneConfig(this.config)) {
1004
- return {
1005
- mode: "standalone",
1006
- host: this.config.host,
1007
- port: this.config.port,
1008
- status: this.isReady
1009
- ? "ready"
1010
- : "connecting",
1011
- };
1012
- }
1013
- return {
1014
- mode: this.config.mode,
1015
- status: this.isReady
1016
- ? "ready"
1017
- : "connecting",
1018
- };
1019
- }
1020
- // ========================================================================
1021
- // INFO
1022
- // ========================================================================
1023
- async info(section) {
1024
- if (section !== undefined) {
1025
- return this.exec("INFO", [section], () => this.client.info(section));
1026
- }
1027
- return this.exec("INFO", [], () => this.client.info());
1028
- }
1029
- // ========================================================================
1030
- // SELECT
1031
- // ========================================================================
1032
- async select(database) {
1033
- if (this.isClusterClient(this.client)) {
1034
- throw new RedisError("SELECT is not supported in Redis Cluster mode", "CLUSTER_MODE");
1035
- }
1036
- if (!Number.isInteger(database) ||
1037
- database < 0 ||
1038
- database > 15) {
1039
- throw new RedisError("Database must be an integer between 0 and 15", "INVALID_DATABASE");
1040
- }
1041
- return this.client.select(database);
1042
- }
1043
- // ========================================================================
1044
- // Connection Status
1045
- // ========================================================================
1046
- getConnectionStatus() {
1047
- const state = this.isReady
1048
- ? "connected"
1049
- : this.client.status ===
1050
- "connecting"
1051
- ? "connecting"
1052
- : this.client.status ===
1053
- "end"
1054
- ? "closed"
1055
- : this.client.status ===
1056
- "ready"
1057
- ? "connected"
1058
- : "disconnected";
1059
- return {
1060
- state,
1061
- connected: this.isReady,
1062
- ready: this.isReady,
1063
- reconnectAttempts: 0,
1064
- uptime: 0,
1065
- };
1066
- }
1067
- // ========================================================================
1068
- // Concurrency Helper
1069
- // ========================================================================
1070
- async runWithConcurrency(items, concurrency, worker) {
1071
- if (items.length === 0) {
1072
- return;
1073
- }
1074
- const limit = Math.max(1, Math.min(concurrency, items.length));
1075
- let cursor = 0;
1076
- const workers = Array.from({
1077
- length: limit,
1078
- }, async () => {
1079
- while (true) {
1080
- const index = cursor++;
1081
- if (index >=
1082
- items.length) {
1083
- return;
1084
- }
1085
- await worker(items[index]);
1086
- }
1087
- });
1088
- await Promise.all(workers);
1089
- }
1090
- }
1091
- /**
1092
- * Generic runtime factory.
1093
- */
1094
- export function createRedisClient(config, logger) {
1095
- const client = new RedisClientWrapper(config, logger);
1096
- /**
1097
- * The runtime configuration has already been validated by
1098
- * RedisConfigSchema inside RedisClientWrapper.
1099
- *
1100
- * The specialized public API is therefore safe to expose according to
1101
- * the validated discriminator.
1102
- *
1103
- * The cast is isolated to this factory boundary instead of being spread
1104
- * throughout the implementation.
1105
- */
1106
- switch (client.mode) {
1107
- case "cluster":
1108
- return client;
1109
- case "sentinel":
1110
- return client;
1111
- case "standalone":
1112
- return client;
1113
- }
1114
- }