ioredis-toolkit 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -75,15 +75,120 @@ export declare class RedisClientWrapper {
75
75
  private readonly logger;
76
76
  private isReady;
77
77
  constructor(config: RedisConfigInput, logger?: LoggerLike);
78
+ /**
79
+ * Creates a new Redis client wrapper supporting standalone, sentinel, and cluster topologies.
80
+ *
81
+ * The client automatically adapts to the configured Redis topology and lazily creates
82
+ * and shares sub-components: `cache`, `pubsub`, `lock`, `rateLimiter`, and `session`.
83
+ *
84
+ * @param config - Redis configuration (validated with Zod `RedisConfigSchema`).
85
+ * Must include `mode` (standalone/sentinel/cluster), and topology-specific fields.
86
+ * @param logger - Optional pino-compatible logger; defaults to `console`.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const client = new RedisClientWrapper({
91
+ * mode: 'standalone',
92
+ * host: 'localhost',
93
+ * port: 6379,
94
+ * });
95
+ * ```
96
+ */
97
+ /**
98
+ * Returns the Redis topology mode of this client.
99
+ *
100
+ * @returns `'standalone'`, `'sentinel'`, or `'cluster'`
101
+ */
78
102
  get mode(): RedisMode;
103
+ /**
104
+ * Returns the shared Cache instance (lazily created on first access).
105
+ *
106
+ * The Cache provides JSON serialization, optional gzip compression, namespace
107
+ * support, TTLs, hash helpers, and pattern-based cleanup. All multi-key
108
+ * operations are slot-aware and cluster-safe.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * const cache = client.cache;
113
+ * await cache.set('user:1', { name: 'alice' });
114
+ * const user = await cache.get('user:1');
115
+ * ```
116
+ */
79
117
  get cache(): Cache;
80
118
  withCache(value: CacheInputConfig): RedisClientWrapper;
119
+ /**
120
+ * Returns the shared Pub/Sub instance (lazily created on first access).
121
+ *
122
+ * Publishing works immediately; subscribing requires calling {@link connectSubscriber}
123
+ * first. Messages are JSON-serialized on publish and auto-parsed on delivery.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const pubsub = client.pubsub;
128
+ * await pubsub.connectSubscriber({ mode: 'standalone', host: 'localhost', port: 6379 });
129
+ * await pubsub.subscribe('orders:created', (message) => {
130
+ * console.log(message); // { id: 1 }
131
+ * });
132
+ * await pubsub.publish('orders:created', { id: 1 });
133
+ * ```
134
+ */
81
135
  get pubsub(): PubSub;
136
+ /**
137
+ * Returns the shared DistributedLock instance (lazily created on first access).
138
+ *
139
+ * Provides atomic distributed mutual-exclusion locks backed by Redis. Works in
140
+ * standalone, sentinel, and cluster modes. Acquisition uses atomic `SET ... PX NX`;
141
+ * release and extension use Lua scripts so only the lock owner can release or extend.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const lock = client.lock;
146
+ * const acquired = await lock.acquire('order:42');
147
+ * if (acquired) {
148
+ * try {
149
+ * // critical section
150
+ * } finally {
151
+ * await lock.release('order:42');
152
+ * }
153
+ * }
154
+ * ```
155
+ */
82
156
  get lock(): DistributedLock;
83
157
  withLock(value: DistributedLockInputOptions): RedisClientWrapper;
158
+ /**
159
+ * Returns the shared RateLimiter instance (lazily created on first access).
160
+ *
161
+ * Generic rate limiting for any resource — routes, API endpoints, users, IPs,
162
+ * API keys, database writes, email sends, webhooks. Supports fixed and sliding
163
+ * window algorithms. Fails open when Redis is unavailable.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * const limiter = client.rateLimiter;
168
+ * const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
169
+ * if (!result.allowed) {
170
+ * // HTTP 429, set Retry-After: result.retryAfter
171
+ * }
172
+ * ```
173
+ */
84
174
  get rateLimiter(): RateLimiter;
85
175
  withRateLimiter(value: RateLimitOptionsInput): RedisClientWrapper;
86
176
  get revocationStore(): RedisRevocationStore;
177
+ /**
178
+ * Returns the shared SessionManager instance (lazily created on first access).
179
+ *
180
+ * The production session stack: validation with fail-closed semantics, rotation
181
+ * with retry-safe idempotency, throttled touches, idle/absolute expiry, per-user
182
+ * eviction ceilings, security versioning, optional AES-256-GCM encryption at rest,
183
+ * fail-closed circuit breaker, metrics and health. Cluster-safe by construction.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * const manager = client.session;
188
+ * const { token, session } = await manager.service.create({ userId: 'user-42' });
189
+ * const result = await manager.service.validate(token, { userId: 'user-42' });
190
+ * ```
191
+ */
87
192
  get session(): SessionManager;
88
193
  withSession(value: WithSessionManagerOptions): RedisClientWrapper;
89
194
  private createClient;
package/dist/client.js CHANGED
@@ -81,15 +81,53 @@ export class RedisClientWrapper {
81
81
  this.client = this.createClient();
82
82
  this.setupEventHandlers();
83
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
+ */
84
103
  // ========================================================================
85
104
  // Configuration
86
105
  // ========================================================================
106
+ /**
107
+ * Returns the Redis topology mode of this client.
108
+ *
109
+ * @returns `'standalone'`, `'sentinel'`, or `'cluster'`
110
+ */
87
111
  get mode() {
88
112
  return this.config.mode;
89
113
  }
90
114
  // ========================================================================
91
115
  // Cache
92
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
+ */
93
131
  get cache() {
94
132
  if (!this._cache) {
95
133
  this._cache = new Cache(this, this.config.cacheOptions ?? {}, this.logger);
@@ -114,6 +152,22 @@ export class RedisClientWrapper {
114
152
  // ========================================================================
115
153
  // Pub/Sub
116
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
+ */
117
171
  get pubsub() {
118
172
  if (!this._pubsub) {
119
173
  this._pubsub = new PubSub(this, this.logger);
@@ -123,6 +177,26 @@ export class RedisClientWrapper {
123
177
  // ========================================================================
124
178
  // Distributed Lock
125
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
+ */
126
200
  get lock() {
127
201
  if (!this._lock) {
128
202
  this._lock = new DistributedLock(this, this.logger, this.config.lockOptions);
@@ -147,6 +221,22 @@ export class RedisClientWrapper {
147
221
  // ========================================================================
148
222
  // Rate Limiter
149
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
+ */
150
240
  get rateLimiter() {
151
241
  if (!this._rateLimiter) {
152
242
  this._rateLimiter = new RateLimiter(this, this.config.rateLimit);
@@ -181,6 +271,21 @@ export class RedisClientWrapper {
181
271
  // ========================================================================
182
272
  // Session
183
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
+ */
184
289
  get session() {
185
290
  if (!this._session) {
186
291
  const options = this.config.sessionOptions ?? {};
package/dist/health.d.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  import { RedisClientWrapper } from './client.js';
2
2
  import { LoggerLike } from './logger.js';
3
3
  export interface HealthStatus {
4
+ /** Whether the system is healthy. */
4
5
  healthy: boolean;
6
+ /** Current status label. */
5
7
  status: 'healthy' | 'degraded' | 'unhealthy';
8
+ /** Latency of the last ping check in milliseconds. */
6
9
  latency: number;
10
+ /** Timestamp of when the status was recorded. */
7
11
  timestamp: Date;
12
+ /** Additional details about the health check. */
8
13
  details: {
14
+ /** Whether a PING command succeeded. */
9
15
  ping: boolean;
16
+ /** Number of connected clients (when available). */
10
17
  connections?: number;
18
+ /** Memory usage information (when available). */
11
19
  memory?: string;
12
20
  };
13
21
  }
@@ -17,9 +25,82 @@ export declare class HealthChecker {
17
25
  private timer;
18
26
  private callbacks;
19
27
  private lastStatus;
28
+ /**
29
+ * Creates a health checker instance.
30
+ *
31
+ * @param client - The underlying {@link RedisClientWrapper}.
32
+ * @param logger - Optional pino-compatible logger; defaults to `console`.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const health = new HealthChecker(client);
37
+ * ```
38
+ */
20
39
  constructor(client: RedisClientWrapper, logger?: LoggerLike);
40
+ /**
41
+ * Starts periodic health checks.
42
+ *
43
+ * **Behavior:**
44
+ * - If a timer is already running, it is cleared and replaced with the new interval.
45
+ * - Health checks run at the specified `interval` in milliseconds.
46
+ * - Each check runs asynchronously; errors are logged but do not stop the interval.
47
+ * - The first check runs immediately when `start()` is called (depending on setInterval timing).
48
+ *
49
+ * **Parameters:**
50
+ * - `interval` - Check interval in milliseconds. Default: `10000` (10 seconds).
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * // Check every 5 seconds
55
+ * health.start(5000);
56
+ *
57
+ * // Check every 30 seconds (default)
58
+ * health.start();
59
+ * ```
60
+ *
61
+ * @returns `void`
62
+ */
21
63
  start(interval?: number): void;
64
+ /**
65
+ * Stops the health checker.
66
+ *
67
+ * **Behavior:**
68
+ * - Clears the internal timer, stopping further health checks.
69
+ * - Logs a warning if no timer was active.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * health.stop();
74
+ * ```
75
+ *
76
+ * @returns `void`
77
+ */
22
78
  stop(): void;
79
+ /**
80
+ * Runs a single health check.
81
+ *
82
+ * **Behavior:**
83
+ * - Performs a PING command to verify Redis connectivity.
84
+ * - Attempts to fetch Redis INFO for additional details (connections, memory).
85
+ * In cluster mode, INFO may not be available and is silently ignored.
86
+ * - Measures latency of the PING command.
87
+ * - Updates the internal `lastStatus` and notifies all registered callbacks.
88
+ *
89
+ * **Returns:**
90
+ * - A {@link HealthStatus} object with the current health state.
91
+ *
92
+ * **Example:**
93
+ * ```ts
94
+ * const status = await health.check();
95
+ * console.log(status.healthy, status.latency);
96
+ * // healthy === true, latency === 1.2 (ms)
97
+ * ```
98
+ *
99
+ * **Parameters:**
100
+ * - None
101
+ *
102
+ * @returns Current health status.
103
+ */
23
104
  check(): Promise<HealthStatus>;
24
105
  /**
25
106
  * Returns the most recent health check result.
@@ -32,8 +113,68 @@ export declare class HealthChecker {
32
113
  * console.log(status?.healthy, status?.latency);
33
114
  * ```
34
115
  */
116
+ /**
117
+ * Returns the most recent health check result.
118
+ *
119
+ * **Returns:**
120
+ * - The last {@link HealthStatus}, or `null` before the first check.
121
+ *
122
+ * **Example:**
123
+ * ```ts
124
+ * const status = health.getStatus();
125
+ * console.log(status?.healthy, status?.latency);
126
+ * ```
127
+ *
128
+ * **Parameters:**
129
+ * - None
130
+ *
131
+ * @returns The last result, or `null`.
132
+ */
35
133
  getStatus(): HealthStatus | null;
134
+ /**
135
+ * Registers a callback for health status changes.
136
+ *
137
+ * **Behavior:**
138
+ * - The callback is invoked whenever a health check runs and the status changes.
139
+ * - Callbacks are invoked synchronously within the `check()` method.
140
+ * - Multiple callbacks can be registered; they are invoked in registration order.
141
+ *
142
+ * **Parameters:**
143
+ * - `callback` - A function receiving a {@link HealthStatus} object.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * health.onChange((status) => {
148
+ * console.log(`Health status: ${status.status}, latency: ${status.latency}ms`);
149
+ * });
150
+ * ```
151
+ *
152
+ * @returns `void`
153
+ */
36
154
  onChange(callback: (status: HealthStatus) => void): void;
37
155
  private notifyCallbacks;
156
+ /**
157
+ * Waits until the Redis connection is healthy.
158
+ *
159
+ * **Behavior:**
160
+ * - Polls {@link check} at 1-second intervals.
161
+ * - Returns `true` as soon as `status.healthy` is `true`.
162
+ * - Returns `false` if the timeout is reached without becoming healthy.
163
+ *
164
+ * **Parameters:**
165
+ * - `timeout` - Maximum time to wait in milliseconds. Default: `30000` (30 seconds).
166
+ *
167
+ * **Returns:**
168
+ * - `true` if the connection became healthy within the timeout.
169
+ * - `false` if the timeout was reached without the connection becoming healthy.
170
+ *
171
+ * **Example:**
172
+ * ```ts
173
+ * const healthy = await health.waitForHealthy(10000);
174
+ * // healthy === true if Redis became healthy within 10 seconds
175
+ * ```
176
+ *
177
+ * @returns `true` if became healthy within timeout.
178
+ */
38
179
  waitForHealthy(timeout?: number): Promise<boolean>;
39
180
  }
package/dist/health.js CHANGED
@@ -5,10 +5,44 @@ export class HealthChecker {
5
5
  timer = null;
6
6
  callbacks = [];
7
7
  lastStatus = null;
8
+ /**
9
+ * Creates a health checker instance.
10
+ *
11
+ * @param client - The underlying {@link RedisClientWrapper}.
12
+ * @param logger - Optional pino-compatible logger; defaults to `console`.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const health = new HealthChecker(client);
17
+ * ```
18
+ */
8
19
  constructor(client, logger = defaultLogger) {
9
20
  this.client = client;
10
21
  this.logger = logger.child({ component: 'HealthChecker' });
11
22
  }
23
+ /**
24
+ * Starts periodic health checks.
25
+ *
26
+ * **Behavior:**
27
+ * - If a timer is already running, it is cleared and replaced with the new interval.
28
+ * - Health checks run at the specified `interval` in milliseconds.
29
+ * - Each check runs asynchronously; errors are logged but do not stop the interval.
30
+ * - The first check runs immediately when `start()` is called (depending on setInterval timing).
31
+ *
32
+ * **Parameters:**
33
+ * - `interval` - Check interval in milliseconds. Default: `10000` (10 seconds).
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * // Check every 5 seconds
38
+ * health.start(5000);
39
+ *
40
+ * // Check every 30 seconds (default)
41
+ * health.start();
42
+ * ```
43
+ *
44
+ * @returns `void`
45
+ */
12
46
  start(interval = 10000) {
13
47
  if (this.timer) {
14
48
  clearInterval(this.timer);
@@ -20,6 +54,20 @@ export class HealthChecker {
20
54
  }, interval);
21
55
  this.logger.info(`Health checker started (interval: ${interval}ms)`);
22
56
  }
57
+ /**
58
+ * Stops the health checker.
59
+ *
60
+ * **Behavior:**
61
+ * - Clears the internal timer, stopping further health checks.
62
+ * - Logs a warning if no timer was active.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * health.stop();
67
+ * ```
68
+ *
69
+ * @returns `void`
70
+ */
23
71
  stop() {
24
72
  if (this.timer) {
25
73
  clearInterval(this.timer);
@@ -27,6 +75,31 @@ export class HealthChecker {
27
75
  this.logger.info('Health checker stopped');
28
76
  }
29
77
  }
78
+ /**
79
+ * Runs a single health check.
80
+ *
81
+ * **Behavior:**
82
+ * - Performs a PING command to verify Redis connectivity.
83
+ * - Attempts to fetch Redis INFO for additional details (connections, memory).
84
+ * In cluster mode, INFO may not be available and is silently ignored.
85
+ * - Measures latency of the PING command.
86
+ * - Updates the internal `lastStatus` and notifies all registered callbacks.
87
+ *
88
+ * **Returns:**
89
+ * - A {@link HealthStatus} object with the current health state.
90
+ *
91
+ * **Example:**
92
+ * ```ts
93
+ * const status = await health.check();
94
+ * console.log(status.healthy, status.latency);
95
+ * // healthy === true, latency === 1.2 (ms)
96
+ * ```
97
+ *
98
+ * **Parameters:**
99
+ * - None
100
+ *
101
+ * @returns Current health status.
102
+ */
30
103
  async check() {
31
104
  const start = Date.now();
32
105
  const details = {
@@ -77,9 +150,46 @@ export class HealthChecker {
77
150
  * console.log(status?.healthy, status?.latency);
78
151
  * ```
79
152
  */
153
+ /**
154
+ * Returns the most recent health check result.
155
+ *
156
+ * **Returns:**
157
+ * - The last {@link HealthStatus}, or `null` before the first check.
158
+ *
159
+ * **Example:**
160
+ * ```ts
161
+ * const status = health.getStatus();
162
+ * console.log(status?.healthy, status?.latency);
163
+ * ```
164
+ *
165
+ * **Parameters:**
166
+ * - None
167
+ *
168
+ * @returns The last result, or `null`.
169
+ */
80
170
  getStatus() {
81
171
  return this.lastStatus;
82
172
  }
173
+ /**
174
+ * Registers a callback for health status changes.
175
+ *
176
+ * **Behavior:**
177
+ * - The callback is invoked whenever a health check runs and the status changes.
178
+ * - Callbacks are invoked synchronously within the `check()` method.
179
+ * - Multiple callbacks can be registered; they are invoked in registration order.
180
+ *
181
+ * **Parameters:**
182
+ * - `callback` - A function receiving a {@link HealthStatus} object.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * health.onChange((status) => {
187
+ * console.log(`Health status: ${status.status}, latency: ${status.latency}ms`);
188
+ * });
189
+ * ```
190
+ *
191
+ * @returns `void`
192
+ */
83
193
  onChange(callback) {
84
194
  this.callbacks.push(callback);
85
195
  }
@@ -93,6 +203,29 @@ export class HealthChecker {
93
203
  }
94
204
  }
95
205
  }
206
+ /**
207
+ * Waits until the Redis connection is healthy.
208
+ *
209
+ * **Behavior:**
210
+ * - Polls {@link check} at 1-second intervals.
211
+ * - Returns `true` as soon as `status.healthy` is `true`.
212
+ * - Returns `false` if the timeout is reached without becoming healthy.
213
+ *
214
+ * **Parameters:**
215
+ * - `timeout` - Maximum time to wait in milliseconds. Default: `30000` (30 seconds).
216
+ *
217
+ * **Returns:**
218
+ * - `true` if the connection became healthy within the timeout.
219
+ * - `false` if the timeout was reached without the connection becoming healthy.
220
+ *
221
+ * **Example:**
222
+ * ```ts
223
+ * const healthy = await health.waitForHealthy(10000);
224
+ * // healthy === true if Redis became healthy within 10 seconds
225
+ * ```
226
+ *
227
+ * @returns `true` if became healthy within timeout.
228
+ */
96
229
  async waitForHealthy(timeout = 30000) {
97
230
  const start = Date.now();
98
231
  while (Date.now() - start < timeout) {