@memberjunction/redis-provider 0.0.1 → 5.9.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.
@@ -0,0 +1,491 @@
1
+ /**
2
+ * @fileoverview Redis-backed implementation of {@link ILocalStorageProvider}.
3
+ *
4
+ * This module provides a drop-in replacement for `InMemoryLocalStorageProvider`
5
+ * that persists data in Redis, enabling:
6
+ *
7
+ * - **Shared caching** across multiple MJAPI server instances (horizontal scaling)
8
+ * - **Persistence** across process restarts
9
+ * - **Native TTL** via Redis `EXPIRE` — expired keys are automatically reclaimed
10
+ *
11
+ * Compatible with any Redis-protocol service: self-hosted Redis, Azure Managed Redis,
12
+ * AWS ElastiCache, Redis Cloud, Upstash, etc.
13
+ *
14
+ * @module @memberjunction/redis-provider
15
+ */
16
+ import Redis from 'ioredis';
17
+ import type { RedisOptions } from 'ioredis';
18
+ import { ILocalStorageProvider } from '@memberjunction/core';
19
+ import type { CacheChangedEvent } from '@memberjunction/core';
20
+ /**
21
+ * Configuration options for the Redis local storage provider.
22
+ *
23
+ * Accepts either a Redis connection URL string or an `ioredis` options object,
24
+ * plus optional MemberJunction-specific settings.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * // Simple URL connection (works with Azure, AWS, self-hosted)
29
+ * const config: RedisProviderConfig = {
30
+ * url: 'rediss://default:password@my-redis.example.com:6380'
31
+ * };
32
+ *
33
+ * // Full options object with MJ settings
34
+ * const config: RedisProviderConfig = {
35
+ * options: {
36
+ * host: 'localhost',
37
+ * port: 6379,
38
+ * password: 'secret',
39
+ * tls: {},
40
+ * db: 0,
41
+ * },
42
+ * keyPrefix: 'myapp',
43
+ * defaultTTLSeconds: 600,
44
+ * };
45
+ * ```
46
+ */
47
+ export interface RedisProviderConfig {
48
+ /**
49
+ * Redis connection URL (e.g., `redis://localhost:6379`, `rediss://user:pass@host:6380`).
50
+ * Mutually exclusive with `options`.
51
+ * The `rediss://` scheme enables TLS, which is required for most cloud-hosted Redis services.
52
+ */
53
+ url?: string;
54
+ /**
55
+ * Full `ioredis` options object for fine-grained connection control.
56
+ * Mutually exclusive with `url`.
57
+ *
58
+ * @see https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis
59
+ */
60
+ options?: RedisOptions;
61
+ /**
62
+ * Optional prefix prepended to all Redis keys. Useful for isolating
63
+ * MemberJunction data in a shared Redis instance.
64
+ *
65
+ * Keys are stored as `{keyPrefix}:{category}:{key}`.
66
+ *
67
+ * @default 'mj'
68
+ */
69
+ keyPrefix?: string;
70
+ /**
71
+ * Default time-to-live in seconds applied to every `SetItem` call
72
+ * unless overridden by the `ttlSeconds` parameter.
73
+ *
74
+ * Set to `0` or `undefined` to store keys without expiration (persistent).
75
+ *
76
+ * @default undefined (no expiration)
77
+ */
78
+ defaultTTLSeconds?: number;
79
+ /**
80
+ * Maximum number of connection retry attempts before giving up.
81
+ * Each retry uses exponential backoff (doubling delay up to 30 seconds).
82
+ *
83
+ * @default 10
84
+ */
85
+ maxRetries?: number;
86
+ /**
87
+ * Whether to log connection events (connect, disconnect, error) via
88
+ * MemberJunction's `LogStatus` / `LogError` functions.
89
+ *
90
+ * @default true
91
+ */
92
+ enableLogging?: boolean;
93
+ /**
94
+ * Whether to enable Redis pub/sub for cross-server cache invalidation.
95
+ * When enabled, the provider will:
96
+ * - **Publish** a {@link CacheChangedEvent} on every `SetItem`, `Remove`, and `ClearCategory` call
97
+ * - **Subscribe** (via a dedicated second Redis connection) for events from other servers
98
+ * - **Emit** local events so consumers (like `LocalCacheManager`) can dispatch to registered callbacks
99
+ *
100
+ * Pub/sub is **not** started automatically — call {@link RedisLocalStorageProvider.StartListening}
101
+ * after construction to begin subscribing.
102
+ *
103
+ * @default false
104
+ */
105
+ enablePubSub?: boolean;
106
+ }
107
+ /**
108
+ * Redis-backed implementation of the MemberJunction {@link ILocalStorageProvider} interface.
109
+ *
110
+ * Provides persistent, shared caching for server-side environments using Redis.
111
+ * This is a drop-in replacement for `InMemoryLocalStorageProvider` — all consumers
112
+ * (like `LocalCacheManager`, `ProviderBase` metadata caching, etc.) work without
113
+ * any code changes.
114
+ *
115
+ * ### Key Structure
116
+ *
117
+ * All keys follow the pattern: `{prefix}:{category}:{key}`
118
+ *
119
+ * - **prefix** — configurable, defaults to `"mj"` to isolate MJ data in shared Redis instances
120
+ * - **category** — maps to the MJ cache category (`RunViewCache`, `Metadata`, `DatasetCache`, etc.)
121
+ * - **key** — the original key from the caller
122
+ *
123
+ * Categories are tracked in a Redis Set at `{prefix}:__categories__:{category}` so that
124
+ * `ClearCategory()` and `GetCategoryKeys()` operations are efficient.
125
+ *
126
+ * ### TTL Support
127
+ *
128
+ * Redis has native key expiration. The provider supports TTL at two levels:
129
+ * 1. **`defaultTTLSeconds`** in config — applied to all `SetItem` calls
130
+ * 2. **`ttlSeconds` parameter** on `SetItem` — overrides the default per-call
131
+ *
132
+ * ### Error Handling
133
+ *
134
+ * Redis operations are wrapped in try/catch blocks. Connection errors are logged
135
+ * via `LogError()` but do not throw — the provider gracefully returns `null` for
136
+ * reads and silently skips writes. This prevents a Redis outage from crashing the
137
+ * application. The `ioredis` client handles automatic reconnection.
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
142
+ *
143
+ * const provider = new RedisLocalStorageProvider({
144
+ * url: 'redis://localhost:6379',
145
+ * defaultTTLSeconds: 300 // 5-minute default TTL
146
+ * });
147
+ *
148
+ * await provider.SetItem('user:123', JSON.stringify(userData), 'UserCache');
149
+ * const cached = await provider.GetItem('user:123', 'UserCache');
150
+ * ```
151
+ */
152
+ export declare class RedisLocalStorageProvider implements ILocalStorageProvider {
153
+ private _client;
154
+ private _keyPrefix;
155
+ private _defaultTTLSeconds;
156
+ private _enableLogging;
157
+ private _connected;
158
+ private _enablePubSub;
159
+ private _subscriber;
160
+ private _pubSubChannel;
161
+ private _eventEmitter;
162
+ private _subscriberConnected;
163
+ private _config;
164
+ /**
165
+ * Creates a new Redis local storage provider and establishes a connection.
166
+ *
167
+ * The constructor sets up the `ioredis` client with automatic reconnection,
168
+ * error handling, and optional logging. The client connects lazily on the
169
+ * first command, so construction itself does not block.
170
+ *
171
+ * @param config - Redis connection and behavior configuration.
172
+ * At minimum, provide either `url` or `options`.
173
+ * If neither is provided, connects to `localhost:6379`.
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * // Connect to local Redis
178
+ * const provider = new RedisLocalStorageProvider({});
179
+ *
180
+ * // Connect to Azure Managed Redis with TLS
181
+ * const provider = new RedisLocalStorageProvider({
182
+ * url: 'rediss://default:ACCESS_KEY@myredis.redis.cache.windows.net:6380',
183
+ * defaultTTLSeconds: 600
184
+ * });
185
+ *
186
+ * // Connect to AWS ElastiCache
187
+ * const provider = new RedisLocalStorageProvider({
188
+ * options: {
189
+ * host: 'my-cluster.abc123.use1.cache.amazonaws.com',
190
+ * port: 6379,
191
+ * tls: {}
192
+ * }
193
+ * });
194
+ * ```
195
+ */
196
+ constructor(config?: RedisProviderConfig);
197
+ /**
198
+ * Exponential backoff retry strategy for Redis connections.
199
+ * Doubles the delay on each attempt (capped at 30 seconds) and gives up
200
+ * after `maxRetries` attempts.
201
+ *
202
+ * @param times - Current retry attempt number (1-based)
203
+ * @param maxRetries - Maximum number of retries before giving up
204
+ * @returns Delay in milliseconds, or `null` to stop retrying
205
+ * @internal
206
+ */
207
+ private retryStrategy;
208
+ /**
209
+ * Registers event handlers on the ioredis client for logging connection
210
+ * lifecycle events (connect, ready, close, error, reconnecting).
211
+ * @internal
212
+ */
213
+ private setupEventHandlers;
214
+ /**
215
+ * Builds the full Redis key from a category and key name.
216
+ *
217
+ * Format: `{prefix}:{category}:{key}`
218
+ *
219
+ * @param key - The storage key
220
+ * @param category - The category for key isolation
221
+ * @returns The fully-qualified Redis key string
222
+ * @internal
223
+ */
224
+ private buildKey;
225
+ /**
226
+ * Builds the Redis Set key used to track all keys in a category.
227
+ *
228
+ * Format: `{prefix}:__categories__:{category}`
229
+ *
230
+ * @param category - The category name
231
+ * @returns The Redis key for the category's membership set
232
+ * @internal
233
+ */
234
+ private buildCategorySetKey;
235
+ /**
236
+ * Retrieves a value from Redis by key and optional category.
237
+ *
238
+ * @param key - The key to look up
239
+ * @param category - Optional category for key isolation (defaults to `"default"`)
240
+ * @returns The stored string value, or `null` if the key doesn't exist or Redis is unavailable
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * const value = await provider.GetItem('entity-metadata', 'Metadata');
245
+ * if (value) {
246
+ * const metadata = JSON.parse(value);
247
+ * }
248
+ * ```
249
+ */
250
+ GetItem(key: string, category?: string): Promise<string | null>;
251
+ /**
252
+ * Stores a value in Redis under the given key and optional category.
253
+ *
254
+ * If a `ttlSeconds` is provided, the key will automatically expire after that
255
+ * duration. Otherwise, the configured `defaultTTLSeconds` is used. If neither
256
+ * is set, the key persists indefinitely.
257
+ *
258
+ * The key is also added to a Redis Set that tracks all keys in the category,
259
+ * enabling efficient `ClearCategory()` and `GetCategoryKeys()` operations.
260
+ *
261
+ * @param key - The key to store under
262
+ * @param value - The string value to store
263
+ * @param category - Optional category for key isolation (defaults to `"default"`)
264
+ * @param ttlSeconds - Optional time-to-live in seconds. Overrides `defaultTTLSeconds` from config.
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * // Store with default TTL
269
+ * await provider.SetItem('view:users', JSON.stringify(results), 'RunViewCache');
270
+ *
271
+ * // Store with explicit 10-minute TTL
272
+ * await provider.SetItem('view:users', JSON.stringify(results), 'RunViewCache', 600);
273
+ * ```
274
+ */
275
+ SetItem(key: string, value: string, category?: string, ttlSeconds?: number): Promise<void>;
276
+ /**
277
+ * Removes a key from Redis and from its category tracking set.
278
+ *
279
+ * @param key - The key to remove
280
+ * @param category - Optional category for key isolation (defaults to `"default"`)
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * await provider.Remove('view:users', 'RunViewCache');
285
+ * ```
286
+ */
287
+ Remove(key: string, category?: string): Promise<void>;
288
+ /**
289
+ * Clears all keys belonging to a specific category.
290
+ *
291
+ * Uses the category tracking Set to find all member keys, deletes them
292
+ * in a single pipeline call, then removes the tracking Set itself.
293
+ *
294
+ * @param category - The category to clear. If empty, clears the `"default"` category.
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * // Clear all cached RunView results
299
+ * await provider.ClearCategory('RunViewCache');
300
+ * ```
301
+ */
302
+ ClearCategory(category: string): Promise<void>;
303
+ /**
304
+ * Returns all keys belonging to a specific category.
305
+ *
306
+ * Reads from the category tracking Set, so the result reflects keys
307
+ * that were added via `SetItem` (some may have expired via TTL but
308
+ * will still appear in the set until cleaned up).
309
+ *
310
+ * @param category - The category to list keys from
311
+ * @returns Array of original key names (without the Redis prefix/category prefix)
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * const keys = await provider.GetCategoryKeys('RunViewCache');
316
+ * console.log(`${keys.length} cached views`);
317
+ * ```
318
+ */
319
+ GetCategoryKeys(category: string): Promise<string[]>;
320
+ /**
321
+ * Whether the Redis client currently has an active connection.
322
+ *
323
+ * Note: `ioredis` automatically reconnects on failure, so a `false` value
324
+ * here is usually transient. The provider continues to accept commands
325
+ * (they queue until reconnection succeeds or retry limit is hit).
326
+ */
327
+ get IsConnected(): boolean;
328
+ /**
329
+ * Returns the underlying `ioredis` client instance for advanced operations.
330
+ *
331
+ * Use with caution — direct client access bypasses key prefixing and
332
+ * category tracking. Prefer the `ILocalStorageProvider` methods for
333
+ * standard operations.
334
+ *
335
+ * @example
336
+ * ```typescript
337
+ * // Use for Redis-specific commands like pub/sub, streams, etc.
338
+ * const client = provider.Client;
339
+ * await client.publish('cache-invalidation', 'entity:Users');
340
+ * ```
341
+ */
342
+ get Client(): Redis;
343
+ /**
344
+ * Gracefully disconnects from Redis.
345
+ *
346
+ * Sends a `QUIT` command and waits for pending replies. After calling this
347
+ * method, the provider should not be used for further operations.
348
+ *
349
+ * Call this during application shutdown to ensure clean disconnection.
350
+ *
351
+ * @example
352
+ * ```typescript
353
+ * // During application shutdown
354
+ * process.on('SIGTERM', async () => {
355
+ * await redisProvider.Disconnect();
356
+ * process.exit(0);
357
+ * });
358
+ * ```
359
+ */
360
+ Disconnect(): Promise<void>;
361
+ /**
362
+ * Checks if a key exists in the specified category.
363
+ *
364
+ * This is more efficient than `GetItem()` when you only need to check
365
+ * existence without retrieving the value (avoids transferring the value
366
+ * over the network).
367
+ *
368
+ * @param key - The key to check
369
+ * @param category - Optional category for key isolation (defaults to `"default"`)
370
+ * @returns `true` if the key exists and has not expired
371
+ *
372
+ * @example
373
+ * ```typescript
374
+ * if (await provider.Exists('view:users', 'RunViewCache')) {
375
+ * // Use cached value
376
+ * }
377
+ * ```
378
+ */
379
+ Exists(key: string, category?: string): Promise<boolean>;
380
+ /**
381
+ * Returns the remaining time-to-live (in seconds) for a key.
382
+ *
383
+ * @param key - The key to check
384
+ * @param category - Optional category for key isolation (defaults to `"default"`)
385
+ * @returns TTL in seconds, `-1` if no expiration is set, `-2` if the key doesn't exist,
386
+ * or `null` if Redis is unavailable
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * const ttl = await provider.GetTTL('view:users', 'RunViewCache');
391
+ * if (ttl !== null && ttl > 0) {
392
+ * console.log(`Key expires in ${ttl} seconds`);
393
+ * }
394
+ * ```
395
+ */
396
+ GetTTL(key: string, category?: string): Promise<number | null>;
397
+ /**
398
+ * Pings the Redis server to verify connectivity.
399
+ *
400
+ * Useful for health checks and connection validation.
401
+ *
402
+ * @returns `true` if the server responds with `PONG`, `false` otherwise
403
+ *
404
+ * @example
405
+ * ```typescript
406
+ * const healthy = await provider.Ping();
407
+ * if (!healthy) {
408
+ * console.error('Redis is unreachable');
409
+ * }
410
+ * ```
411
+ */
412
+ Ping(): Promise<boolean>;
413
+ /**
414
+ * Starts listening for cache change events from other server instances.
415
+ * Creates a dedicated Redis connection for pub/sub (required by Redis protocol —
416
+ * a client in subscribe mode cannot execute other commands).
417
+ *
418
+ * Must be called explicitly after construction. No-op if `enablePubSub` is `false`
419
+ * in the config, or if already listening.
420
+ *
421
+ * @example
422
+ * ```typescript
423
+ * const provider = new RedisLocalStorageProvider({
424
+ * url: 'redis://localhost:6379',
425
+ * enablePubSub: true
426
+ * });
427
+ * await provider.StartListening();
428
+ *
429
+ * // Register for change events
430
+ * provider.OnCacheChanged((event) => {
431
+ * console.log(`Key "${event.CacheKey}" changed by server ${event.SourceServerId}`);
432
+ * });
433
+ * ```
434
+ */
435
+ StartListening(): Promise<void>;
436
+ /**
437
+ * Creates the subscriber Redis client, mirroring the main client's connection config.
438
+ * @internal
439
+ */
440
+ private createSubscriberClient;
441
+ /**
442
+ * Sets up event handlers on the subscriber client for logging.
443
+ * @internal
444
+ */
445
+ private setupSubscriberEventHandlers;
446
+ /**
447
+ * Handles an incoming pub/sub message. Parses the {@link CacheChangedEvent},
448
+ * filters out self-originated events, and emits to local listeners.
449
+ * @internal
450
+ */
451
+ private handlePubSubMessage;
452
+ /**
453
+ * Publishes a cache change event to Redis pub/sub. Called internally by
454
+ * `SetItem`, `Remove`, and `ClearCategory`. No-op if pub/sub is disabled.
455
+ *
456
+ * @param cacheKey - The cache key that changed
457
+ * @param category - The storage category
458
+ * @param action - What happened ('set', 'removed', 'category_cleared')
459
+ * @param data - The new value (only for 'set' actions)
460
+ * @internal
461
+ */
462
+ private publishChange;
463
+ /**
464
+ * Registers a callback for cache change events from other servers.
465
+ * The callback fires whenever another server instance modifies a cached entry
466
+ * (via `SetItem`, `Remove`, or `ClearCategory`).
467
+ *
468
+ * Events from this server instance (identified by {@link MJGlobal.ProcessUUID})
469
+ * are automatically filtered out.
470
+ *
471
+ * @param callback - Function invoked with the {@link CacheChangedEvent}
472
+ * @returns A function that, when called, removes this callback registration
473
+ *
474
+ * @example
475
+ * ```typescript
476
+ * const unsubscribe = provider.OnCacheChanged((event) => {
477
+ * // Dispatch to LocalCacheManager for callback routing
478
+ * LocalCacheManager.Instance.DispatchCacheChange(event);
479
+ * });
480
+ *
481
+ * // Later, on shutdown:
482
+ * unsubscribe();
483
+ * ```
484
+ */
485
+ OnCacheChanged(callback: (event: CacheChangedEvent) => void): () => void;
486
+ /**
487
+ * Whether the pub/sub subscriber connection is currently active.
488
+ */
489
+ get IsSubscriberConnected(): boolean;
490
+ }
491
+ //# sourceMappingURL=RedisLocalStorageProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RedisLocalStorageProvider.d.ts","sourceRoot":"","sources":["../src/RedisLocalStorageProvider.ts"],"names":[],"mappings":"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"}