@bymax-one/nest-cache 1.0.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,1296 @@
1
+ import * as _nestjs_common from '@nestjs/common';
2
+ import { Type, DynamicModule, ForwardReference, InjectionToken, OptionalFactoryDependency, OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, HttpException, HttpStatus } from '@nestjs/common';
3
+ import { ConnectionOptions } from 'node:tls';
4
+ import { ClusterNode, ClusterOptions, SentinelAddress, NatMap, Redis, Cluster, ChainableCommander } from 'ioredis';
5
+ export { ClusterNode, ClusterOptions, Redis, RedisKey, RedisOptions, SentinelAddress } from 'ioredis';
6
+
7
+ /**
8
+ * Cache connection event types.
9
+ *
10
+ * Layer: shared — zero-dependency. Emitted by the connection manager and
11
+ * surfaced to consumers through the optional `events.onEvent` callback.
12
+ */
13
+ /** Connection lifecycle events propagated from the underlying Redis client. */
14
+ type CacheEventName = 'connect' | 'ready' | 'error' | 'close' | 'reconnecting' | 'end';
15
+ /** Coarse connection status derived from the lifecycle events. */
16
+ type CacheConnectionStatus = 'connecting' | 'ready' | 'reconnecting' | 'closed';
17
+
18
+ /**
19
+ * Connection lifecycle observation contract.
20
+ *
21
+ * Layer: server. A consumer-supplied bag of callbacks the connection manager
22
+ * invokes on Redis lifecycle transitions. Used to bridge connection signals to
23
+ * a logger, metrics backend, or alerting system without coupling the library to
24
+ * any of them.
25
+ */
26
+
27
+ /**
28
+ * Plug-in for connection lifecycle observation.
29
+ *
30
+ * Callbacks MUST be fast and non-blocking — any exception thrown is caught and
31
+ * swallowed by the connection manager (best-effort observability must never
32
+ * crash the connection lifecycle). Payloads carry no secret values.
33
+ */
34
+ interface ICacheEvents {
35
+ /**
36
+ * Invoked on every connection lifecycle event.
37
+ *
38
+ * @remarks A forced disconnect during graceful shutdown surfaces here as an
39
+ * `'error'` event carrying `{ role: 'main', reason: 'forced_disconnect' }`
40
+ * (no `error` message field). The `CacheEventName` union has no dedicated
41
+ * shutdown member, so branch on `data.reason` to tell a forced teardown apart
42
+ * from an ioredis socket error (which carries an `error` string instead).
43
+ * @param event - The lifecycle event name.
44
+ * @param data - Secret-free structured context (e.g. `{ role: 'main' }`).
45
+ */
46
+ onEvent?: (event: CacheEventName, data: Record<string, unknown>) => void;
47
+ }
48
+
49
+ /**
50
+ * Lua script definition contract.
51
+ *
52
+ * Layer: server. Describes a named Lua script pre-registered with the script
53
+ * manager. The manager runs the register → `SCRIPT LOAD` → `EVALSHA` cycle,
54
+ * falling back to a full `EVAL` on `NOSCRIPT` (script evicted from the Redis
55
+ * cache).
56
+ */
57
+ /**
58
+ * A named Lua script for atomic server-side operations.
59
+ *
60
+ * The `name` is the lookup key consumers pass to `eval`; the `lua` body is
61
+ * loaded once via `SCRIPT LOAD` and thereafter invoked by SHA via `EVALSHA`,
62
+ * with a transparent `NOSCRIPT` fallback.
63
+ */
64
+ interface IScriptDefinition {
65
+ /** Unique lookup name used to invoke the script by reference. */
66
+ name: string;
67
+ /** The Lua source. Must never be built from untrusted input (CLAUDE.md §4). */
68
+ lua: string;
69
+ }
70
+
71
+ /**
72
+ * Value serialization strategy contract.
73
+ *
74
+ * Layer: server. Lets a consumer swap the default JSON serializer for a custom
75
+ * codec (e.g. MessagePack, a schema-aware encoder) without changing call sites.
76
+ */
77
+ /**
78
+ * Strategy for serializing/deserializing cache values.
79
+ *
80
+ * Implementations MUST be deterministic and MUST throw on malformed input
81
+ * during `deserialize` — never return `undefined` or a partial value. The cache
82
+ * service catches that throw and wraps it as a `CacheException` so deserialization
83
+ * fails closed (security invariant — see CLAUDE.md §4).
84
+ */
85
+ interface ISerializer {
86
+ /**
87
+ * Encodes a value to its string representation.
88
+ *
89
+ * @param value - The value to encode.
90
+ * @returns The serialized string.
91
+ * @throws If the value cannot be serialized.
92
+ */
93
+ serialize<T>(value: T): string;
94
+ /**
95
+ * Decodes a string back into a value.
96
+ *
97
+ * @param raw - The serialized string.
98
+ * @returns The decoded value, typed as `T`.
99
+ * @throws If `raw` is malformed (must fail closed, never return partial data).
100
+ */
101
+ deserialize<T>(raw: string): T;
102
+ }
103
+
104
+ /**
105
+ * Public configuration contracts for `BymaxCacheModule`.
106
+ *
107
+ * Layer: server. Describes every option a consumer passes to `forRoot` /
108
+ * `forRootAsync`, plus the mode-specific connection blocks. ioredis types are
109
+ * re-exported for consumer convenience. See `docs/technical_specification.md`
110
+ * §4 for full semantics.
111
+ */
112
+
113
+ /**
114
+ * Standalone (single-node) connection settings.
115
+ *
116
+ * Either `url` OR `host` is required. When both are present, `url` wins —
117
+ * discrete fields act as fallback (spec §11.3).
118
+ */
119
+ interface BymaxCacheStandaloneConnection {
120
+ /** `redis://` or `rediss://` (TLS) URL — overrides discrete fields. */
121
+ url?: string;
122
+ /** Hostname. Default: `localhost` when neither `url` nor `host` is set is rejected at validation. */
123
+ host?: string;
124
+ /** TCP port. Default: 6379. */
125
+ port?: number;
126
+ /** Auth password. Never logged or echoed in events. */
127
+ password?: string;
128
+ /** Logical database index. */
129
+ db?: number;
130
+ /** Auth username (Redis 6 ACLs). */
131
+ username?: string;
132
+ /** TLS options for `rediss://` or explicit TLS. */
133
+ tls?: ConnectionOptions;
134
+ /** Default: false (connects on `OnModuleInit`). */
135
+ lazyConnect?: boolean;
136
+ /** Default: 10_000 ms. */
137
+ connectTimeout?: number;
138
+ /** Default: 5_000 ms. */
139
+ commandTimeout?: number;
140
+ /** Default: 3. Do NOT pass `null` here — that value is BullMQ-specific. */
141
+ maxRetriesPerRequest?: number;
142
+ /** Default: true. */
143
+ enableReadyCheck?: boolean;
144
+ /** Default: false — fail fast instead of queueing. */
145
+ enableOfflineQueue?: boolean;
146
+ /** Default: `(times) => Math.min(times * 50, 2000)`. */
147
+ retryStrategy?: (times: number) => number | null | void;
148
+ /** Default: reconnects only on `READONLY` (replica failover). */
149
+ reconnectOnError?: (err: Error) => boolean | 1 | 2;
150
+ /** TCP keep-alive in ms. */
151
+ keepAlive?: number;
152
+ /** Disable Nagle's algorithm. */
153
+ noDelay?: boolean;
154
+ /** IP stack family. */
155
+ family?: 4 | 6;
156
+ }
157
+ /** Sentinel-mode connection settings. Required when `mode === 'sentinel'`. */
158
+ interface BymaxCacheSentinelConnection {
159
+ /** Sentinel addresses to query for the current master. */
160
+ sentinels: SentinelAddress[];
161
+ /** Master group name configured in the sentinels. */
162
+ name: string;
163
+ /** Password for the sentinel nodes themselves. */
164
+ sentinelPassword?: string;
165
+ /** Password for the data nodes. */
166
+ password?: string;
167
+ /**
168
+ * Connect to the master or a replica.
169
+ * @remarks `'replica'` is the current Redis 7 terminology; `'slave'` is still
170
+ * accepted by ioredis 5 at the wire level for backwards compatibility but
171
+ * should be avoided in new code. Both values are accepted here and normalised
172
+ * to the ioredis wire value internally.
173
+ */
174
+ role?: 'master' | 'replica' | 'slave';
175
+ /**
176
+ * Rewrites the master/replica addresses the sentinels announce. Needed when the
177
+ * sentinels report addresses that are not reachable as-is from the client —
178
+ * e.g. a NAT'd Docker/Kubernetes network where the announced internal IP must
179
+ * be translated to a published host. Maps `'<announced-host>:<port>'` to the
180
+ * reachable `{ host, port }`.
181
+ */
182
+ natMap?: NatMap;
183
+ }
184
+ /** Cluster-mode connection settings. Required when `mode === 'cluster'`. */
185
+ interface BymaxCacheClusterConnection {
186
+ /** Seed nodes for cluster discovery. */
187
+ nodes: ClusterNode[];
188
+ /** ioredis cluster options. */
189
+ options?: ClusterOptions;
190
+ }
191
+ /**
192
+ * Synchronous configuration for `BymaxCacheModule.forRoot()`.
193
+ *
194
+ * @see `docs/technical_specification.md` §4 for full semantics.
195
+ */
196
+ interface BymaxCacheModuleOptions {
197
+ /** Connection mode. Default: `'standalone'`. */
198
+ mode?: 'standalone' | 'sentinel' | 'cluster';
199
+ /** Standalone connection block (used when `mode === 'standalone'`). */
200
+ connection?: BymaxCacheStandaloneConnection;
201
+ /** Sentinel connection block (used when `mode === 'sentinel'`). */
202
+ sentinel?: BymaxCacheSentinelConnection;
203
+ /** Cluster connection block (used when `mode === 'cluster'`). */
204
+ cluster?: BymaxCacheClusterConnection;
205
+ /** Global namespace prefix applied to every key. Default: `'app'`. */
206
+ namespace?: string;
207
+ /** Separator between namespace/prefix/id segments. Default: `':'`. */
208
+ keySeparator?: string;
209
+ /** Custom serializer. Default: `JsonSerializer`. */
210
+ serializer?: ISerializer;
211
+ /** Connection lifecycle event hooks — plug a logger or metrics sink here. */
212
+ events?: ICacheEvents;
213
+ /** Graceful shutdown timeout in ms. Default: 5000. */
214
+ shutdownTimeoutMs?: number;
215
+ /**
216
+ * Allows `flushNamespace()` to run even when `NODE_ENV === 'production'`.
217
+ * Default: false. SAFETY: leave false in real production environments.
218
+ */
219
+ allowFlushInProduction?: boolean;
220
+ /**
221
+ * Register the module as global. Default: true.
222
+ *
223
+ * @remarks Read by {@link BymaxCacheModule.forRoot}. For `forRootAsync` the
224
+ * `global` flag is decided synchronously — before the async factory resolves —
225
+ * so pass `isGlobal` at the `forRootAsync({ isGlobal, ... })` call site; an
226
+ * `isGlobal` returned from inside the async `useFactory` has no effect.
227
+ */
228
+ isGlobal?: boolean;
229
+ /** Lua scripts pre-registered at module init. */
230
+ scripts?: readonly IScriptDefinition[];
231
+ }
232
+ /**
233
+ * Async configuration for `BymaxCacheModule.forRootAsync()`.
234
+ * Standard NestJS dynamic-module async options shape.
235
+ *
236
+ * @remarks The recommended async-registration contract: a `useFactory` returning
237
+ * the module options. `forRootAsync`'s parameter is the builder-generated
238
+ * async-options type — a superset that also accepts NestJS's `useClass` /
239
+ * `useExisting` provider strategies — so this curated interface documents the
240
+ * common `useFactory` path most consumers use rather than redefining it.
241
+ */
242
+ interface BymaxCacheModuleAsyncOptions {
243
+ /** Modules to import so the factory can inject their providers. */
244
+ imports?: Array<Type | DynamicModule | ForwardReference>;
245
+ /** Providers to inject into `useFactory`. */
246
+ inject?: Array<InjectionToken | OptionalFactoryDependency>;
247
+ /** Factory returning the resolved module options. */
248
+ useFactory: (...args: unknown[]) => Promise<BymaxCacheModuleOptions> | BymaxCacheModuleOptions;
249
+ /** Register the module as global. Default: true. */
250
+ isGlobal?: boolean;
251
+ }
252
+
253
+ /**
254
+ * Extra (non-option) registration flags folded into the generated dynamic
255
+ * module by the builder. Kept as a builder *extra* — rather than read from the
256
+ * resolved `BymaxCacheModuleOptions` — so the generated `forRootAsync` can decide
257
+ * the module's `global` flag synchronously, before the async options factory
258
+ * resolves.
259
+ */
260
+ interface BymaxCacheModuleExtras {
261
+ /**
262
+ * Register the module globally. Required in the resolved extras (the builder
263
+ * fills it from the `{ isGlobal: true }` default) but optional for consumers,
264
+ * who receive it as `Partial<BymaxCacheModuleExtras>` on the registration types.
265
+ */
266
+ isGlobal: boolean;
267
+ }
268
+ declare const ConfigurableModuleClass: _nestjs_common.ConfigurableModuleCls<BymaxCacheModuleOptions, "forRoot", "create", BymaxCacheModuleExtras>;
269
+ declare const OPTIONS_TYPE: BymaxCacheModuleOptions & Partial<BymaxCacheModuleExtras>;
270
+ declare const ASYNC_OPTIONS_TYPE: _nestjs_common.ConfigurableModuleAsyncOptions<BymaxCacheModuleOptions, "create"> & Partial<BymaxCacheModuleExtras>;
271
+
272
+ declare class BymaxCacheModule extends ConfigurableModuleClass {
273
+ /**
274
+ * Registers the cache module synchronously.
275
+ *
276
+ * @param options - Consumer options; validated and defaulted at registration.
277
+ * @returns The configured {@link DynamicModule}.
278
+ * @throws {import('./errors/cache.exception').CacheException} When the options
279
+ * fail bootstrap validation (e.g. missing connection, misconfigured mode).
280
+ */
281
+ static forRoot(options: typeof OPTIONS_TYPE): DynamicModule;
282
+ /**
283
+ * Registers the cache module asynchronously, resolving its options through a
284
+ * consumer-supplied factory (e.g. reading from `ConfigService`).
285
+ *
286
+ * @remarks
287
+ * Delegates the async-options plumbing (the `MODULE_OPTIONS_TOKEN` factory,
288
+ * `inject`, `imports`, and the `global` flag) to the {@link ConfigurableModuleClass}
289
+ * base, then augments the produced module with the cache providers. Options are
290
+ * validated and defaulted INSIDE the `BYMAX_CACHE_OPTIONS` factory — which
291
+ * injects the base `MODULE_OPTIONS_TOKEN` — so a misconfiguration surfaces during
292
+ * bootstrap exactly as it does for {@link BymaxCacheModule.forRoot}. The events
293
+ * and serializer providers are factories deriving from the resolved options,
294
+ * since those values are not known until the async factory runs.
295
+ * @param options - Async registration options: a `useFactory` returning the
296
+ * module options, plus optional `inject`, `imports`, and `isGlobal` (the
297
+ * builder defaults `isGlobal` to `true`).
298
+ * @returns The configured {@link DynamicModule}.
299
+ * @throws {import('./errors/cache.exception').CacheException} During bootstrap,
300
+ * when the factory-resolved options fail validation.
301
+ */
302
+ static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule;
303
+ /**
304
+ * The cache providers layered onto the base async module: the resolved-options
305
+ * provider (which validates + defaults the raw factory result read from
306
+ * `MODULE_OPTIONS_TOKEN`), the derived events and serializer providers, and the
307
+ * topology-independent {@link BymaxCacheModule.buildCommonProviders}. Mirrors the
308
+ * options/events/serializer wiring `forRoot` performs synchronously.
309
+ *
310
+ * @returns The async-only provider list.
311
+ */
312
+ private static buildAsyncProviders;
313
+ /**
314
+ * Topology-independent providers shared by `forRoot` and `forRootAsync` — the
315
+ * connection manager, key builder, cache services, and the `useExisting`
316
+ * aliases for the key-builder and script-registry tokens. The options, events,
317
+ * and serializer providers are NOT here: they differ between the sync
318
+ * (`useValue`) and async (`useFactory`) entry points.
319
+ *
320
+ * @returns The common provider list.
321
+ */
322
+ private static buildCommonProviders;
323
+ /**
324
+ * Tokens and services exported by both `forRoot` and `forRootAsync`.
325
+ *
326
+ * @returns The common export list.
327
+ */
328
+ private static buildCommonExports;
329
+ }
330
+
331
+ /**
332
+ * Fully-resolved module options type.
333
+ *
334
+ * Layer: server. The shape produced by `applyDefaults` and stored under the
335
+ * `BYMAX_CACHE_OPTIONS` token. Defaulted fields (including `mode`) are always
336
+ * present; mode-specific blocks are kept as required-but-nullable so the
337
+ * resolver can assign them unconditionally under `exactOptionalPropertyTypes`,
338
+ * while consumers still narrow on `undefined`.
339
+ */
340
+
341
+ /**
342
+ * Module options after defaults are merged. The defaulted fields
343
+ * (`mode`, `namespace`, `keySeparator`, `shutdownTimeoutMs`,
344
+ * `allowFlushInProduction`, `isGlobal`) are guaranteed present; the
345
+ * mode/connection blocks are present keys whose value may be `undefined`.
346
+ */
347
+ type ResolvedOptions = Required<Pick<BymaxCacheModuleOptions, 'mode' | 'namespace' | 'keySeparator' | 'shutdownTimeoutMs' | 'allowFlushInProduction' | 'isGlobal'>> & {
348
+ connection: BymaxCacheModuleOptions['connection'];
349
+ sentinel: BymaxCacheModuleOptions['sentinel'];
350
+ cluster: BymaxCacheModuleOptions['cluster'];
351
+ serializer: BymaxCacheModuleOptions['serializer'];
352
+ events: BymaxCacheModuleOptions['events'];
353
+ scripts: BymaxCacheModuleOptions['scripts'];
354
+ };
355
+
356
+ /** The two client flavors this manager can produce. */
357
+ type AnyRedis = Redis | Cluster;
358
+ declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
359
+ private readonly options;
360
+ private readonly events?;
361
+ private client;
362
+ private readonly redisOptionsResolved;
363
+ /** Default backoff: grow 50 ms per attempt, capped at 2 s. */
364
+ private readonly defaultRetryStrategy;
365
+ /** Default reconnect policy: reconnect only on a `READONLY` replica failover. */
366
+ private readonly defaultReconnectOnError;
367
+ /**
368
+ * @param options - Resolved module options (frozen).
369
+ * @param events - Optional consumer event callbacks; `@Optional()` so the
370
+ * module can provide `null` when the consumer omits `events`.
371
+ */
372
+ constructor(options: ResolvedOptions, events?: ICacheEvents | undefined);
373
+ /** Opens the main client and waits for readiness unless `lazyConnect`. */
374
+ onModuleInit(): Promise<void>;
375
+ /**
376
+ * Returns the singleton main client, creating it on first access if the
377
+ * module init has not run yet.
378
+ *
379
+ * @returns The shared main client.
380
+ */
381
+ getClient(): AnyRedis;
382
+ /**
383
+ * Creates a brand-new dedicated connection for subscriber mode (a subscriber
384
+ * connection cannot run normal commands), inheriting the main options.
385
+ *
386
+ * Ownership of the returned client transfers to the caller: `onModuleDestroy`
387
+ * quits only the main client, so a subscriber client must be quit/disconnected
388
+ * by its owner (the Pub/Sub service).
389
+ *
390
+ * The subscriber is a control-plane connection (only SUBSCRIBE / UNSUBSCRIBE),
391
+ * so its offline queue is enabled — a subscribe issued before the socket is
392
+ * ready buffers until connected instead of failing fast like the data-plane
393
+ * main client (whose offline queue stays disabled to avoid silent buffering).
394
+ * This override applies to standalone/sentinel modes only; in cluster mode
395
+ * {@link createClient} ignores it (cluster Pub/Sub is an experimental passthrough).
396
+ *
397
+ * @returns A fresh client wired with `subscriber`-role event listeners.
398
+ */
399
+ createSubscriberClient(): AnyRedis;
400
+ /** Quits the main client gracefully, forcing `disconnect()` on timeout. */
401
+ onModuleDestroy(): Promise<void>;
402
+ /**
403
+ * Instantiates the client matching the configured mode.
404
+ *
405
+ * @param overrides - Extra `RedisOptions` merged over the resolved defaults
406
+ * (used to enable the subscriber's offline queue). Ignored in cluster mode,
407
+ * where Pub/Sub has different semantics and is out of scope.
408
+ */
409
+ private createClient;
410
+ /** Merges connection options with defaults; URL fields take precedence. */
411
+ private buildRedisOptions;
412
+ /** Forwards a lifecycle event to `events.onEvent`, swallowing consumer throws. */
413
+ private emit;
414
+ /** Wires lifecycle listeners that forward to `events.onEvent`, swallowing throws. */
415
+ private registerListeners;
416
+ /** Resolves once the client is ready; rejects (wrapped) on connection error. */
417
+ private waitUntilReady;
418
+ }
419
+
420
+ /**
421
+ * Composes Redis keys following `{namespace}{separator}{prefix}{separator}{id}`.
422
+ *
423
+ * With the defaults (`namespace='app'`, `separator=':'`):
424
+ * - `build('users', 'u_1')` → `'app:users:u_1'`
425
+ * - `applyNamespace('rl:u_1')` → `'app:rl:u_1'`
426
+ *
427
+ * @see `docs/technical_specification.md` §7 — Namespace Strategy
428
+ */
429
+ declare class KeyBuilder {
430
+ private readonly namespace;
431
+ private readonly separator;
432
+ /**
433
+ * @param options - Resolved module options supplying the namespace + separator.
434
+ */
435
+ constructor(options: ResolvedOptions);
436
+ /**
437
+ * Builds the full namespaced key.
438
+ *
439
+ * @param prefix - The entity-group prefix (e.g. `'users'`).
440
+ * @param id - The entity id.
441
+ * @returns `{namespace}{sep}{prefix}{sep}{id}`.
442
+ * @throws {CacheException} `INVALID_KEY` when `prefix` or `id` is empty.
443
+ */
444
+ build(prefix: string, id: string): string;
445
+ /**
446
+ * Applies only the namespace to an already-composed key. Used by the Pub/Sub
447
+ * and script services for channel/key namespacing.
448
+ *
449
+ * @param keyWithoutNamespace - The bare key to namespace.
450
+ * @returns `{namespace}{sep}{keyWithoutNamespace}`.
451
+ * @throws {CacheException} `INVALID_KEY` when the key is empty.
452
+ */
453
+ applyNamespace(keyWithoutNamespace: string): string;
454
+ /**
455
+ * Returns the `{namespace}{separator}` prefix string used to build `SCAN`
456
+ * match patterns scoped to this namespace.
457
+ *
458
+ * @returns The namespace prefix, e.g. `'app:'`.
459
+ */
460
+ getNamespacePrefix(): string;
461
+ }
462
+
463
+ declare class ScriptManagerService implements OnApplicationBootstrap {
464
+ private readonly options;
465
+ private readonly connection;
466
+ /** Registry of script name → `{ lua, sha? }`. */
467
+ private readonly scripts;
468
+ /**
469
+ * @param options - Resolved module options; `options.scripts` seeds the registry.
470
+ * @param connection - Owns the client used for `SCRIPT LOAD` / `EVALSHA`.
471
+ * Explicit `@Inject` — the published bundle is built without
472
+ * emitDecoratorMetadata, so type-only DI cannot resolve a class provider
473
+ * (CLAUDE.md §5).
474
+ */
475
+ constructor(options: ResolvedOptions, connection: ConnectionManager);
476
+ /**
477
+ * Pre-loads every registered script once the application has bootstrapped,
478
+ * unless `lazyConnect` is set — in which case loading is deferred to the first
479
+ * {@link eval}.
480
+ *
481
+ * Runs in `onApplicationBootstrap` (not `onModuleInit`) deliberately: NestJS
482
+ * invokes `onModuleInit` hooks concurrently, so loading here would race the
483
+ * {@link ConnectionManager} connect and fail fast against a not-yet-writable
484
+ * socket (offline queue is disabled). `onApplicationBootstrap` is guaranteed to
485
+ * run after every `onModuleInit` resolved — i.e. once the connection is ready.
486
+ *
487
+ * @returns Resolves once all eager scripts are loaded.
488
+ */
489
+ onApplicationBootstrap(): Promise<void>;
490
+ /**
491
+ * Registers a script under `name`, or overrides an existing one. The new
492
+ * script is loaded lazily on its next {@link eval} / {@link load}.
493
+ *
494
+ * @param name - Lookup name used to invoke the script.
495
+ * @param lua - The Lua source. Never build this from untrusted input (CLAUDE.md §4).
496
+ */
497
+ register(name: string, lua: string): void;
498
+ /**
499
+ * Loads a registered script into Redis (if not already cached) and returns its
500
+ * SHA1. Idempotent — a cached SHA is reused without a second `SCRIPT LOAD`.
501
+ *
502
+ * @param name - The registered script name.
503
+ * @returns The script's SHA1.
504
+ * @throws {CacheException} `SCRIPT_NOT_REGISTERED` when `name` is unknown.
505
+ */
506
+ load(name: string): Promise<string>;
507
+ /**
508
+ * Executes a registered Lua script.
509
+ *
510
+ * Standalone / sentinel use `EVALSHA`; on `NOSCRIPT` the script is reloaded once
511
+ * and the call retried. CLUSTER uses `EVAL` (the full body): `EVALSHA` routes to
512
+ * the key's slot owner while `SCRIPT LOAD` is keyless (lands on an arbitrary
513
+ * node), so the owner could `NOSCRIPT` and a keyless reload would not fix it —
514
+ * `EVAL` ships the body and routes by key to the slot owner; a keyless `EVAL`
515
+ * would execute on an arbitrary node — this method throws
516
+ * `SCRIPT_EXECUTION_FAILED` when called in cluster mode with zero keys.
517
+ *
518
+ * Keys must already be namespaced — {@link CacheService.eval} handles that for
519
+ * consumer-facing usage. In cluster mode all keys of a single call must hash to
520
+ * the same slot (use a hash tag), per Redis cluster semantics.
521
+ *
522
+ * @param name - The registered script name.
523
+ * @param keys - `KEYS[]` for the script (already namespaced).
524
+ * @param args - `ARGV[]` for the script.
525
+ * @returns The script's return value, typed `unknown` (Redis Lua is dynamic).
526
+ * @throws {CacheException} `SCRIPT_NOT_REGISTERED` when `name` is unknown.
527
+ * @throws {CacheException} `SCRIPT_EXECUTION_FAILED` on a non-`NOSCRIPT` error,
528
+ * a failed reload-and-retry, or any cluster `EVAL` failure. The Lua source is
529
+ * never echoed in the error.
530
+ */
531
+ eval(name: string, keys: readonly string[], args: ReadonlyArray<string | number>): Promise<unknown>;
532
+ }
533
+
534
+ declare class CacheService {
535
+ private readonly options;
536
+ private readonly connection;
537
+ private readonly keyBuilder;
538
+ private readonly scriptRegistry?;
539
+ /** Resolved serializer: explicit option wins, then injected token, then JSON. */
540
+ private readonly serializer;
541
+ /**
542
+ * @param options - Resolved module options (frozen). Supplies `serializer`
543
+ * and `allowFlushInProduction`.
544
+ * @param connection - Owns the singleton ioredis client every command runs on.
545
+ * @param keyBuilder - Composes every namespaced key.
546
+ * @param injectedSerializer - Optional `BYMAX_CACHE_SERIALIZER` provider; used
547
+ * only when `options.serializer` is absent. `@Optional()` so the token may be
548
+ * unprovided in tests / minimal wirings.
549
+ * @param scriptRegistry - Optional `ScriptManagerService`; required only for
550
+ * {@link CacheService.eval}. `@Optional()` so the cache works without scripts.
551
+ */
552
+ constructor(options: ResolvedOptions, connection: ConnectionManager, keyBuilder: KeyBuilder, injectedSerializer?: ISerializer, scriptRegistry?: ScriptManagerService | undefined);
553
+ /**
554
+ * Reads a value and deserializes it through the configured serializer.
555
+ *
556
+ * @typeParam T - The expected decoded type.
557
+ * @param prefix - Entity-group prefix (e.g. `'users'`).
558
+ * @param id - Entity id.
559
+ * @returns The decoded value, or `null` when the key does not exist.
560
+ * @throws {CacheException} `DESERIALIZATION_FAILED` when the stored payload is
561
+ * not decodable as `T`.
562
+ */
563
+ get<T>(prefix: string, id: string): Promise<T | null>;
564
+ /**
565
+ * Reads the raw stored string without deserialization.
566
+ *
567
+ * @param prefix - Entity-group prefix.
568
+ * @param id - Entity id.
569
+ * @returns The raw string, or `null` when the key does not exist.
570
+ */
571
+ getRaw(prefix: string, id: string): Promise<string | null>;
572
+ /**
573
+ * Serializes and writes a value, optionally with a TTL.
574
+ *
575
+ * @typeParam T - The value's static type.
576
+ * @param prefix - Entity-group prefix.
577
+ * @param id - Entity id.
578
+ * @param value - The value to store (passed through the serializer).
579
+ * @param ttlSeconds - Optional expiry in seconds; omit for no expiration.
580
+ * @returns Resolves once the write completes.
581
+ * @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
582
+ */
583
+ set<T>(prefix: string, id: string, value: T, ttlSeconds?: number): Promise<void>;
584
+ /**
585
+ * Writes a raw string without serialization, optionally with a TTL.
586
+ *
587
+ * @param prefix - Entity-group prefix.
588
+ * @param id - Entity id.
589
+ * @param value - The raw string to store as-is.
590
+ * @param ttlSeconds - Optional expiry in seconds; omit for no expiration.
591
+ * @returns Resolves once the write completes.
592
+ */
593
+ setRaw(prefix: string, id: string, value: string, ttlSeconds?: number): Promise<void>;
594
+ /**
595
+ * Atomically writes a value only if the key does not already exist (`SET NX`).
596
+ *
597
+ * @typeParam T - The value's static type.
598
+ * @param prefix - Entity-group prefix.
599
+ * @param id - Entity id.
600
+ * @param value - The value to store (passed through the serializer).
601
+ * @param ttlSeconds - Optional expiry in seconds applied on the same atomic write.
602
+ * @returns `true` when the value was stored, `false` when the key already existed.
603
+ * @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
604
+ */
605
+ setNx<T>(prefix: string, id: string, value: T, ttlSeconds?: number): Promise<boolean>;
606
+ /**
607
+ * Deletes a single key.
608
+ *
609
+ * @param prefix - Entity-group prefix.
610
+ * @param id - Entity id.
611
+ * @returns The number of keys removed (`0` or `1`).
612
+ */
613
+ del(prefix: string, id: string): Promise<number>;
614
+ /**
615
+ * Deletes many keys under the same prefix in one round trip.
616
+ *
617
+ * @param prefix - Entity-group prefix shared by every id.
618
+ * @param ids - Entity ids to delete. An empty list is a no-op (no Redis call).
619
+ * @returns The number of keys actually removed.
620
+ */
621
+ delMany(prefix: string, ids: readonly string[]): Promise<number>;
622
+ /**
623
+ * Reports whether a key exists.
624
+ *
625
+ * @param prefix - Entity-group prefix.
626
+ * @param id - Entity id.
627
+ * @returns `true` when the key exists, otherwise `false`.
628
+ */
629
+ exists(prefix: string, id: string): Promise<boolean>;
630
+ /**
631
+ * Atomically increments a counter.
632
+ *
633
+ * @param prefix - Entity-group prefix.
634
+ * @param id - Entity id.
635
+ * @param by - Increment step. Defaults to `1` (uses `INCR`); any other value
636
+ * uses `INCRBY`.
637
+ * @returns The value after the increment.
638
+ */
639
+ incr(prefix: string, id: string, by?: number): Promise<number>;
640
+ /**
641
+ * Atomically decrements a counter.
642
+ *
643
+ * @param prefix - Entity-group prefix.
644
+ * @param id - Entity id.
645
+ * @param by - Decrement step. Defaults to `1` (uses `DECR`); any other value
646
+ * uses `DECRBY`.
647
+ * @returns The value after the decrement.
648
+ */
649
+ decr(prefix: string, id: string, by?: number): Promise<number>;
650
+ /**
651
+ * Sets a TTL on an existing key.
652
+ *
653
+ * @param prefix - Entity-group prefix.
654
+ * @param id - Entity id.
655
+ * @param ttlSeconds - Expiry in seconds.
656
+ * @returns `true` when the timeout was set, `false` when the key does not exist.
657
+ */
658
+ expire(prefix: string, id: string, ttlSeconds: number): Promise<boolean>;
659
+ /**
660
+ * Reads the remaining TTL of a key.
661
+ *
662
+ * @param prefix - Entity-group prefix.
663
+ * @param id - Entity id.
664
+ * @returns TTL in seconds; `-2` when the key does not exist, `-1` when it
665
+ * exists with no expiration.
666
+ */
667
+ ttl(prefix: string, id: string): Promise<number>;
668
+ /**
669
+ * Removes the TTL of a key, making it persistent.
670
+ *
671
+ * @param prefix - Entity-group prefix.
672
+ * @param id - Entity id.
673
+ * @returns `true` when a timeout was removed, `false` when the key has no TTL
674
+ * or does not exist.
675
+ */
676
+ persist(prefix: string, id: string): Promise<boolean>;
677
+ /**
678
+ * Reads many keys under the same prefix and deserializes each present value.
679
+ *
680
+ * @typeParam T - The expected decoded type of every value.
681
+ * @param prefix - Entity-group prefix shared by every id.
682
+ * @param ids - Entity ids to read. An empty list returns `[]` with no Redis call.
683
+ * @returns Values positionally aligned with `ids`; `null` for missing keys.
684
+ * @throws {CacheException} `DESERIALIZATION_FAILED` when any present payload is
685
+ * not decodable as `T`.
686
+ */
687
+ mget<T>(prefix: string, ids: readonly string[]): Promise<Array<T | null>>;
688
+ /**
689
+ * Writes many `[id, value]` pairs under the same prefix in one round trip.
690
+ *
691
+ * @typeParam T - The values' static type.
692
+ * @param prefix - Entity-group prefix shared by every entry.
693
+ * @param entries - `[id, value]` tuples. An empty list is a no-op (no Redis call).
694
+ * @returns Resolves once the write completes.
695
+ * @throws {CacheException} `SERIALIZATION_FAILED` when any value cannot be encoded.
696
+ */
697
+ mset<T>(prefix: string, entries: ReadonlyArray<readonly [string, T]>): Promise<void>;
698
+ /**
699
+ * Reads one hash field and deserializes its value.
700
+ *
701
+ * @typeParam T - The expected decoded type.
702
+ * @param prefix - Entity-group prefix.
703
+ * @param id - Entity id (the hash key).
704
+ * @param field - Hash field name (kept raw, never serialized).
705
+ * @returns The decoded field value, or `null` when the field does not exist.
706
+ * @throws {CacheException} `DESERIALIZATION_FAILED` when the field payload is
707
+ * not decodable as `T`.
708
+ */
709
+ hget<T>(prefix: string, id: string, field: string): Promise<T | null>;
710
+ /**
711
+ * Serializes and writes one hash field.
712
+ *
713
+ * @typeParam T - The value's static type.
714
+ * @param prefix - Entity-group prefix.
715
+ * @param id - Entity id (the hash key).
716
+ * @param field - Hash field name (kept raw, never serialized).
717
+ * @param value - The value to store (passed through the serializer).
718
+ * @returns `1` when the field is new, `0` when it overwrote an existing field.
719
+ * @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
720
+ */
721
+ hset<T>(prefix: string, id: string, field: string, value: T): Promise<number>;
722
+ /**
723
+ * Reads every field of a hash and deserializes each value.
724
+ *
725
+ * @typeParam T - The expected decoded type of every field value.
726
+ * @param prefix - Entity-group prefix.
727
+ * @param id - Entity id (the hash key).
728
+ * @returns A record of field → decoded value; `{}` when the hash does not exist.
729
+ * @throws {CacheException} `DESERIALIZATION_FAILED` when any field payload is
730
+ * not decodable as `T`.
731
+ */
732
+ hgetall<T>(prefix: string, id: string): Promise<Record<string, T>>;
733
+ /**
734
+ * Deletes one or more hash fields.
735
+ *
736
+ * @param prefix - Entity-group prefix.
737
+ * @param id - Entity id (the hash key).
738
+ * @param fields - Field names to delete. No fields is a no-op (no Redis call).
739
+ * @returns The number of fields actually removed.
740
+ */
741
+ hdel(prefix: string, id: string, ...fields: readonly string[]): Promise<number>;
742
+ /**
743
+ * Adds members to a set.
744
+ *
745
+ * Members are stored as raw strings — sets hold ids, not serialized objects,
746
+ * so the serializer is intentionally not applied here.
747
+ *
748
+ * @param prefix - Entity-group prefix.
749
+ * @param id - Entity id (the set key).
750
+ * @param members - String members to add. No members is a no-op (no Redis call).
751
+ * @returns The number of members newly added (excludes ones already present).
752
+ */
753
+ sadd(prefix: string, id: string, ...members: readonly string[]): Promise<number>;
754
+ /**
755
+ * Removes members from a set.
756
+ *
757
+ * @param prefix - Entity-group prefix.
758
+ * @param id - Entity id (the set key).
759
+ * @param members - String members to remove. No members is a no-op (no Redis call).
760
+ * @returns The number of members actually removed.
761
+ */
762
+ srem(prefix: string, id: string, ...members: readonly string[]): Promise<number>;
763
+ /**
764
+ * Reads every member of a set.
765
+ *
766
+ * @param prefix - Entity-group prefix.
767
+ * @param id - Entity id (the set key).
768
+ * @returns The raw string members; `[]` when the set does not exist.
769
+ */
770
+ smembers(prefix: string, id: string): Promise<string[]>;
771
+ /**
772
+ * Reports whether a member belongs to a set.
773
+ *
774
+ * @param prefix - Entity-group prefix.
775
+ * @param id - Entity id (the set key).
776
+ * @param member - The member to test.
777
+ * @returns `true` when the member is present, otherwise `false`.
778
+ */
779
+ sismember(prefix: string, id: string, member: string): Promise<boolean>;
780
+ /**
781
+ * Reads the cardinality of a set.
782
+ *
783
+ * @param prefix - Entity-group prefix.
784
+ * @param id - Entity id (the set key).
785
+ * @returns The member count; `0` when the set does not exist.
786
+ */
787
+ scard(prefix: string, id: string): Promise<number>;
788
+ /**
789
+ * Lists keys matching a pattern under a prefix.
790
+ *
791
+ * WARNING: `KEYS` is O(N) and BLOCKS the Redis server for the whole scan —
792
+ * prefer {@link CacheService.scan} in production.
793
+ *
794
+ * @param prefix - Entity-group prefix.
795
+ * @param pattern - Glob pattern for the id segment, e.g. `'*'`.
796
+ * @returns The matching fully-namespaced keys.
797
+ * @example
798
+ * ```ts
799
+ * await cache.keys('users', '*') // ['app:users:u_1', 'app:users:u_2']
800
+ * ```
801
+ */
802
+ keys(prefix: string, pattern: string): Promise<string[]>;
803
+ /**
804
+ * Iterates keys matching a pattern under a prefix using a non-blocking cursor.
805
+ *
806
+ * Safe for production: `SCAN` never blocks the server. Standalone / sentinel
807
+ * only — Cluster exposes different scan semantics and is rejected.
808
+ *
809
+ * @param prefix - Entity-group prefix.
810
+ * @param pattern - Glob pattern for the id segment, e.g. `'*'`.
811
+ * @param count - Per-batch hint passed to `SCAN` (not a hard limit).
812
+ * @returns An async iterable of fully-namespaced keys.
813
+ * @throws {CacheException} `UNSUPPORTED_IN_CLUSTER` when called in cluster mode
814
+ * (no usable top-level `scanStream`).
815
+ * @example
816
+ * ```ts
817
+ * for await (const key of cache.scan('users', '*')) {
818
+ * // key === 'app:users:u_1'
819
+ * }
820
+ * ```
821
+ */
822
+ scan(prefix: string, pattern: string, count?: number): AsyncIterable<string>;
823
+ /**
824
+ * Opens an ioredis pipeline for batching arbitrary commands.
825
+ *
826
+ * NOTE: keys passed to pipeline commands are NOT auto-namespaced — compose
827
+ * them through {@link KeyBuilder} yourself.
828
+ *
829
+ * @returns A chainable commander; call `.exec()` to flush.
830
+ * @example
831
+ * ```ts
832
+ * const pipe = cache.pipeline()
833
+ * pipe.set(keyBuilder.build('p', 'a'), '1')
834
+ * pipe.set(keyBuilder.build('p', 'b'), '2')
835
+ * await pipe.exec()
836
+ * ```
837
+ */
838
+ pipeline(): ChainableCommander;
839
+ /**
840
+ * Returns the raw ioredis client (escape hatch).
841
+ *
842
+ * Keys used through the returned client are NOT auto-namespaced. Reach for
843
+ * this only to run a command this facade does not expose.
844
+ *
845
+ * @returns The singleton ioredis client.
846
+ * @throws {CacheException} `UNSUPPORTED_IN_CLUSTER` when called in cluster
847
+ * mode — `Cluster` does not share the full `Redis` API surface.
848
+ */
849
+ getClient(): Redis;
850
+ /**
851
+ * Deletes EVERY key under the configured namespace via `SCAN` + `UNLINK`.
852
+ *
853
+ * Uses `UNLINK` (asynchronous reclaim) rather than `DEL` so a large keyset
854
+ * does not block the server. The `SCAN` pattern is scoped to
855
+ * `{namespace}{sep}*`, so keys of other namespaces are never touched.
856
+ *
857
+ * SAFETY: throws {@link CacheException} `FLUSH_DISABLED_IN_PRODUCTION` when
858
+ * `NODE_ENV === 'production'` unless `options.allowFlushInProduction` is `true`.
859
+ * Intended for tests and tooling — in production prefer
860
+ * {@link CacheService.del} / {@link CacheService.delMany}.
861
+ *
862
+ * @returns The total number of keys removed.
863
+ * @throws {CacheException} `FLUSH_DISABLED_IN_PRODUCTION` under the production guard.
864
+ * @throws {CacheException} `UNSUPPORTED_IN_CLUSTER` when called in cluster mode
865
+ * (no usable top-level `scanStream`).
866
+ */
867
+ flushNamespace(): Promise<number>;
868
+ /**
869
+ * Executes a Lua script registered with the {@link ScriptManagerService}. The
870
+ * `keys` are namespaced before reaching Redis (the same isolation guarantee as
871
+ * every other command); `args` are passed through untouched.
872
+ *
873
+ * @param scriptName - Name the script was registered under (via `options.scripts`
874
+ * or `ScriptManagerService.register`).
875
+ * @param keys - `KEYS[]` (bare; namespaced here before execution).
876
+ * @param args - `ARGV[]` for the script.
877
+ * @returns The script's return value, typed `unknown` (Redis Lua is dynamic).
878
+ * @throws {CacheException} `SCRIPT_REGISTRY_MISSING` when no script manager is
879
+ * wired (the module always wires one; this guards manual instantiations).
880
+ * @throws {CacheException} `SCRIPT_NOT_REGISTERED` / `SCRIPT_EXECUTION_FAILED`
881
+ * propagated from the script manager.
882
+ * @example
883
+ * ```ts
884
+ * const swapped = (await cache.eval('compareAndSet', ['session:abc'], ['v1', 'v2'])) as number
885
+ * ```
886
+ */
887
+ eval(scriptName: string, keys: readonly string[], args: ReadonlyArray<string | number>): Promise<unknown>;
888
+ /**
889
+ * Reports whether Redis answers `PING`. Never throws — a connection failure
890
+ * resolves to `false`, making it safe to wire directly into a health endpoint
891
+ * (e.g. `@nestjs/terminus`).
892
+ *
893
+ * @returns `true` when the server replies `PONG`, otherwise `false`.
894
+ */
895
+ isHealthy(): Promise<boolean>;
896
+ /**
897
+ * Sends a raw `PING`. Unlike {@link CacheService.isHealthy}, this propagates a
898
+ * connection failure — use it when the caller wants to handle the error.
899
+ *
900
+ * @returns `'PONG'` on a healthy connection.
901
+ * @throws The underlying ioredis error when the connection is down.
902
+ */
903
+ ping(): Promise<string>;
904
+ /**
905
+ * Returns the Redis `INFO` output, optionally scoped to a single section.
906
+ *
907
+ * @param section - Optional section name (e.g. `'memory'`, `'clients'`,
908
+ * `'replication'`); omit for the full report.
909
+ * @returns The `INFO` text.
910
+ */
911
+ info(section?: string): Promise<string>;
912
+ }
913
+
914
+ /**
915
+ * `ISerializer` backed by `JSON.stringify` / `JSON.parse`.
916
+ *
917
+ * JSON limitations the consumer must account for:
918
+ * - `Date` becomes an ISO string — rehydrate on read if a `Date` is needed.
919
+ * - `Map`, `Set`, `BigInt`, and `undefined` are NOT preserved.
920
+ * - `Buffer` is encoded as a verbose `{ type: 'Buffer', data: [...] }` object.
921
+ *
922
+ * Consumers needing a structure-preserving codec (MessagePack, CBOR, protobuf)
923
+ * implement {@link ISerializer} directly — see the spec §6.3 MessagePack example.
924
+ *
925
+ * @example
926
+ * ```ts
927
+ * const s = new JsonSerializer()
928
+ * s.serialize({ a: 1 }) // '{"a":1}'
929
+ * s.deserialize<{ a: number }>('{"a":1}') // { a: 1 }
930
+ * ```
931
+ */
932
+ declare class JsonSerializer implements ISerializer {
933
+ /**
934
+ * Encodes a value as a JSON string.
935
+ *
936
+ * @typeParam T - The value's static type.
937
+ * @param value - The value to encode.
938
+ * @returns The JSON string representation.
939
+ * @throws {CacheException} `SERIALIZATION_FAILED` when the value cannot be
940
+ * stringified. This covers `JSON.stringify` throwing (circular reference,
941
+ * `BigInt`) AND the silent cases where a top-level `undefined`, function, or
942
+ * `symbol` would make `JSON.stringify` return the JS value `undefined`
943
+ * instead of a string. The original message is attached under
944
+ * `details.error`; the value itself is never echoed, as it may carry secrets
945
+ * (CLAUDE.md §4).
946
+ */
947
+ serialize<T>(value: T): string;
948
+ /**
949
+ * Decodes a JSON string back into a value.
950
+ *
951
+ * Fails closed: a malformed payload throws instead of returning `undefined`
952
+ * or a partial value, so a corrupted cache entry can never masquerade as a
953
+ * valid `T` (security invariant — CLAUDE.md §4).
954
+ *
955
+ * @typeParam T - The expected decoded type.
956
+ * @param raw - The JSON string to decode.
957
+ * @returns The decoded value, typed as `T`.
958
+ * @throws {CacheException} `DESERIALIZATION_FAILED` when `raw` is not valid
959
+ * JSON. `details.preview` carries at most {@link MAX_PREVIEW_LENGTH}
960
+ * characters of `raw` (truncated with an ellipsis) to aid debugging without
961
+ * leaking a large payload that may contain PII.
962
+ */
963
+ deserialize<T>(raw: string): T;
964
+ }
965
+
966
+ /**
967
+ * Pub/Sub message handler contracts.
968
+ *
969
+ * Layer: server. Callback signatures the Pub/Sub service invokes when
970
+ * a message arrives on a subscribed channel or pattern. The message is already
971
+ * deserialized to `T` by the service.
972
+ */
973
+ /**
974
+ * Handler for messages on an exactly-named channel.
975
+ *
976
+ * @typeParam T - The deserialized message payload type.
977
+ * @param message - The deserialized payload.
978
+ * @param channel - The channel the message arrived on.
979
+ */
980
+ type IPubSubHandler<T> = (message: T, channel: string) => void | Promise<void>;
981
+ /**
982
+ * Handler for messages on a pattern subscription (`psubscribe`).
983
+ *
984
+ * @typeParam T - The deserialized message payload type.
985
+ * @param message - The deserialized payload.
986
+ * @param channel - The concrete channel the message arrived on.
987
+ * @param pattern - The pattern that matched the channel.
988
+ */
989
+ type IPubSubPatternHandler<T> = (message: T, channel: string, pattern: string) => void | Promise<void>;
990
+
991
+ /** Detaches a subscription's listener and unsubscribes its channel/pattern. */
992
+ type Unsubscribe = () => Promise<void>;
993
+ declare class PubSubService implements OnModuleDestroy {
994
+ private readonly connection;
995
+ private readonly keyBuilder;
996
+ private readonly events?;
997
+ /** Lazily-created dedicated subscriber connection (null until first subscribe). */
998
+ private subscriber;
999
+ /** Live-listener ref-count per namespaced channel; UNSUBSCRIBE fires on the last. */
1000
+ private readonly channelRefs;
1001
+ /** Live-listener ref-count per namespaced pattern; PUNSUBSCRIBE fires on the last. */
1002
+ private readonly patternRefs;
1003
+ /** Resolved serializer: explicit option wins, then injected token, then JSON. */
1004
+ private readonly serializer;
1005
+ /**
1006
+ * @param options - Resolved module options. Supplies the serializer.
1007
+ * @param connection - Owns the main client and mints subscriber connections.
1008
+ * Explicit `@Inject` — the published bundle is built without
1009
+ * emitDecoratorMetadata, so type-only DI cannot resolve a class provider
1010
+ * (CLAUDE.md §5).
1011
+ * @param keyBuilder - Namespaces every channel/pattern.
1012
+ * @param injectedSerializer - Optional `BYMAX_CACHE_SERIALIZER` provider.
1013
+ * @param events - Optional consumer observability callback bag; a swallowed
1014
+ * handler/deserialization failure is forwarded to it instead of vanishing.
1015
+ */
1016
+ constructor(options: ResolvedOptions, connection: ConnectionManager, keyBuilder: KeyBuilder, injectedSerializer?: ISerializer, events?: ICacheEvents | undefined);
1017
+ /**
1018
+ * Publishes a serialized message to a namespaced channel via the main client.
1019
+ *
1020
+ * @typeParam T - The message payload type.
1021
+ * @param channel - Bare channel name (namespaced before publishing).
1022
+ * @param message - Payload, encoded through the configured serializer.
1023
+ * @returns The number of subscribers that received the message.
1024
+ * @throws {CacheException} `SERIALIZATION_FAILED` when `message` cannot be encoded.
1025
+ */
1026
+ publish<T>(channel: string, message: T): Promise<number>;
1027
+ /**
1028
+ * Subscribes to a namespaced channel. Opens the subscriber connection lazily;
1029
+ * subsequent subscriptions reuse the same connection.
1030
+ *
1031
+ * The handler receives the deserialized message and the full namespaced
1032
+ * channel. A throw inside the handler (or a malformed payload) is swallowed so
1033
+ * it cannot tear down the shared subscriber.
1034
+ *
1035
+ * @typeParam T - The expected message payload type.
1036
+ * @param channel - Bare channel name (namespaced before subscribing).
1037
+ * @param handler - Invoked per message with `(message, channel)`.
1038
+ * @returns An {@link Unsubscribe} that detaches THIS listener; the channel is
1039
+ * only UNSUBSCRIBE'd once its last listener is removed, so unsubscribing one
1040
+ * handler never breaks others subscribed to the same channel.
1041
+ */
1042
+ subscribe<T>(channel: string, handler: IPubSubHandler<T>): Promise<Unsubscribe>;
1043
+ /**
1044
+ * Pattern-subscribes to a namespaced glob (e.g. `'users:*'`). Lazily opens the
1045
+ * subscriber connection, shared with {@link PubSubService.subscribe}.
1046
+ *
1047
+ * @typeParam T - The expected message payload type.
1048
+ * @param pattern - Bare glob pattern (namespaced before subscribing).
1049
+ * @param handler - Invoked per message with `(message, channel, pattern)`,
1050
+ * both in their full namespaced form.
1051
+ * @returns An {@link Unsubscribe} that detaches THIS listener; the pattern is
1052
+ * only PUNSUBSCRIBE'd once its last listener is removed.
1053
+ */
1054
+ psubscribe<T>(pattern: string, handler: IPubSubPatternHandler<T>): Promise<Unsubscribe>;
1055
+ /** Closes the subscriber connection gracefully, forcing disconnect on failure. */
1056
+ onModuleDestroy(): Promise<void>;
1057
+ /**
1058
+ * Returns the dedicated subscriber connection, creating it on first use.
1059
+ *
1060
+ * Reused for the lifetime of the module; ioredis transparently reconnects and
1061
+ * re-subscribes, so a single connection is kept rather than recreated. Typed as
1062
+ * the `Redis | Cluster` union the connection manager mints — `subscribe` /
1063
+ * `psubscribe` exist on both, so no cast is needed (cluster Pub/Sub is an
1064
+ * experimental passthrough per the spec).
1065
+ */
1066
+ private ensureSubscriber;
1067
+ /**
1068
+ * Subscribes the target (channel or pattern) only when it gains its FIRST
1069
+ * listener, then increments and returns its shared {@link SubscriptionRef} so a
1070
+ * later release knows when to issue the matching UNSUBSCRIBE / PUNSUBSCRIBE.
1071
+ *
1072
+ * @param refs - The channel or pattern ref-count map.
1073
+ * @param target - The full namespaced channel/pattern.
1074
+ * @param subscribe - Issues the SUBSCRIBE / PSUBSCRIBE for `target`.
1075
+ * @returns The shared ref for `target` (captured by the unsubscribe closure).
1076
+ */
1077
+ private retainSubscription;
1078
+ /**
1079
+ * Decrements the (closure-captured) ref and issues the UNSUBSCRIBE / PUNSUBSCRIBE
1080
+ * only when the LAST listener is removed — so unsubscribing one handler never
1081
+ * silently stops delivery to the others on the same channel/pattern.
1082
+ *
1083
+ * @param refs - The channel or pattern ref-count map.
1084
+ * @param target - The full namespaced channel/pattern.
1085
+ * @param ref - The shared ref returned by {@link retainSubscription}.
1086
+ * @param unsubscribe - Issues the UNSUBSCRIBE / PUNSUBSCRIBE for `target`.
1087
+ */
1088
+ private releaseSubscription;
1089
+ /**
1090
+ * Builds an idempotent {@link Unsubscribe}: the first call detaches the listener
1091
+ * and releases the subscription; later calls are no-ops, so a double unsubscribe
1092
+ * cannot over-decrement a shared channel/pattern.
1093
+ *
1094
+ * @param detach - Removes this subscription's event listener.
1095
+ * @param release - Decrements the reference count (see {@link releaseSubscription}).
1096
+ */
1097
+ private makeUnsubscribe;
1098
+ /**
1099
+ * Forwards a swallowed handler / deserialization failure to the optional
1100
+ * observability callback, itself swallowing any throw from `onEvent` — a
1101
+ * faulty consumer (handler OR callback) must never tear down the subscriber.
1102
+ * The error surfaces as an `'error'` event with `reason: 'handler_error'`.
1103
+ *
1104
+ * @param channel - The full namespaced channel the failed message arrived on.
1105
+ * @param error - The caught handler / deserialization failure.
1106
+ */
1107
+ private emitHandlerError;
1108
+ }
1109
+
1110
+ /**
1111
+ * NestJS injection tokens for `@bymax-one/nest-cache`.
1112
+ *
1113
+ * Layer: server. All tokens are `Symbol`-based to guarantee uniqueness across
1114
+ * the NestJS DI graph — two consumers cannot collide on the same token by
1115
+ * accident, and a string typo cannot resolve a foreign provider. Mirrors the
1116
+ * pattern established by `@bymax-one/nest-auth` and `@bymax-one/nest-logger`.
1117
+ */
1118
+ /** Resolved module options (connection, namespace, events, scripts). */
1119
+ declare const BYMAX_CACHE_OPTIONS: unique symbol;
1120
+ /**
1121
+ * The connection manager holding the singleton ioredis client. Consumers can
1122
+ * call `.getClient()` on the injected value to access the raw client. Wired as
1123
+ * `useExisting: ConnectionManager` so it resolves to the same instance.
1124
+ */
1125
+ declare const BYMAX_CACHE_CONNECTION: unique symbol;
1126
+ /** Registry of preloaded Lua scripts (name → SHA). */
1127
+ declare const BYMAX_CACHE_SCRIPT_REGISTRY: unique symbol;
1128
+ /** Optional consumer-supplied connection-event callback bag (`ICacheEvents`). */
1129
+ declare const BYMAX_CACHE_EVENTS: unique symbol;
1130
+ /** The value serializer (`ISerializer`; defaults to `JsonSerializer`). Injectable so consumers can override it. */
1131
+ declare const BYMAX_CACHE_SERIALIZER: unique symbol;
1132
+ /** The key builder that composes `{namespace}{sep}{prefix}{sep}{id}`. */
1133
+ declare const BYMAX_CACHE_KEY_BUILDER: unique symbol;
1134
+
1135
+ /**
1136
+ * Canonical cache error codes for `@bymax-one/nest-cache`.
1137
+ *
1138
+ * Layer: shared — zero-dependency, importable in any runtime (browser, edge,
1139
+ * Node). The server subpath's `CacheException` maps these codes to messages and
1140
+ * HTTP statuses.
1141
+ */
1142
+ /**
1143
+ * String error codes thrown by the cache library, namespaced under `cache.`.
1144
+ *
1145
+ * Append-only: new codes may be added, but existing values must never be
1146
+ * renamed or removed without a major version bump — consumers switch on them.
1147
+ */
1148
+ declare const CACHE_ERROR_CODES: {
1149
+ readonly CONNECTION_FAILED: "cache.connection_failed";
1150
+ readonly COMMAND_TIMEOUT: "cache.command_timeout";
1151
+ readonly CONNECTION_LOST: "cache.connection_lost";
1152
+ readonly SERIALIZATION_FAILED: "cache.serialization_failed";
1153
+ readonly DESERIALIZATION_FAILED: "cache.deserialization_failed";
1154
+ readonly INVALID_NAMESPACE: "cache.invalid_namespace";
1155
+ readonly INVALID_KEY: "cache.invalid_key";
1156
+ readonly SCRIPT_NOT_REGISTERED: "cache.script_not_registered";
1157
+ readonly SCRIPT_EXECUTION_FAILED: "cache.script_execution_failed";
1158
+ readonly SCRIPT_REGISTRY_MISSING: "cache.script_registry_missing";
1159
+ readonly FLUSH_DISABLED_IN_PRODUCTION: "cache.flush_disabled_in_production";
1160
+ readonly CLUSTER_MISCONFIGURED: "cache.cluster_misconfigured";
1161
+ readonly SENTINEL_MISCONFIGURED: "cache.sentinel_misconfigured";
1162
+ readonly SHUTDOWN_TIMEOUT: "cache.shutdown_timeout";
1163
+ readonly UNSUPPORTED_IN_CLUSTER: "cache.unsupported_in_cluster";
1164
+ };
1165
+ /** Union of every cache error code string value. */
1166
+ type CacheErrorCode = (typeof CACHE_ERROR_CODES)[keyof typeof CACHE_ERROR_CODES];
1167
+
1168
+ /**
1169
+ * Server-side error code re-exports and human-readable messages.
1170
+ *
1171
+ * Layer: server. The canonical codes live in the zero-dependency shared subpath;
1172
+ * this module re-exports them for server-side consumers and attaches the default
1173
+ * English message catalog. A `Map` (not an index signature) backs the lookup so
1174
+ * a runtime `code` value can never trigger object-injection.
1175
+ */
1176
+
1177
+ /**
1178
+ * Default end-user-facing messages per error code (English; consumers localize
1179
+ * upstream). Covers every code in {@link CACHE_ERROR_CODES}; unknown codes fall
1180
+ * back to a generic message at the `CacheException` throw site.
1181
+ *
1182
+ * Exposed as a {@link ReadonlyMap} so a consumer cannot mutate the shared
1183
+ * catalog — `.set`/`.delete`/`.clear` are compile errors. A `Map` (not an index
1184
+ * signature) still backs it so a runtime `code` can never trigger object-injection.
1185
+ */
1186
+ declare const CACHE_ERROR_MESSAGES: ReadonlyMap<CacheErrorCode, string>;
1187
+
1188
+ /**
1189
+ * Cache exception type and HTTP-status mapping.
1190
+ *
1191
+ * Layer: server — depends on `@nestjs/common` (`HttpException`). Error codes and
1192
+ * messages live in `cache-error-codes`; this module maps codes to HTTP statuses
1193
+ * and exposes the throwable. A `Map` (not an index signature) backs the status
1194
+ * lookup so a runtime `code` can never trigger object-injection.
1195
+ */
1196
+
1197
+ /**
1198
+ * Exception thrown by the cache library. Serializes to a structured
1199
+ * `{ error: { code, message, details } }` body and carries an HTTP status, so it
1200
+ * surfaces cleanly when thrown inside a NestJS request pipeline. The `code` and
1201
+ * `details` are exposed as readonly fields for `catch`-block branching without a
1202
+ * cast.
1203
+ *
1204
+ * SECURITY: `details` is serialized verbatim into the response body, so throw
1205
+ * sites MUST keep it small and free of secret values (CLAUDE.md §4). The library
1206
+ * does not truncate it here — a response-shaping exception filter is the right
1207
+ * place for that and lands in a later phase.
1208
+ *
1209
+ * @example
1210
+ * throw new CacheException(CACHE_ERROR_CODES.INVALID_KEY, { reason: 'empty_prefix' })
1211
+ */
1212
+ declare class CacheException extends HttpException {
1213
+ /** The canonical error code (one of {@link CACHE_ERROR_CODES}). */
1214
+ readonly code: CacheErrorCode;
1215
+ /** Structured, secret-free context attached at the throw site, or `null`. */
1216
+ readonly details: Record<string, unknown> | null;
1217
+ /**
1218
+ * @param code - One of {@link CACHE_ERROR_CODES}.
1219
+ * @param details - Optional structured context. Never include secret values.
1220
+ * @param statusCode - HTTP status override. When omitted, defaults to the
1221
+ * canonical status for `code` (§12.2), or 500 for codes whose canonical
1222
+ * status is 500.
1223
+ */
1224
+ constructor(code: CacheErrorCode, details?: Record<string, unknown>, statusCode?: HttpStatus);
1225
+ }
1226
+
1227
+ /**
1228
+ * Cache configuration value types.
1229
+ *
1230
+ * Layer: shared — zero-dependency. Semantic aliases used across the public API
1231
+ * to make key-building intent explicit at call sites (spec §11.2, §D11/D17).
1232
+ */
1233
+ /**
1234
+ * A logical key namespace. Every key in an application shares one namespace,
1235
+ * which guarantees tenant/app isolation — keys from one namespace never collide
1236
+ * with another. Must be non-empty and must not contain the key separator.
1237
+ *
1238
+ * @example
1239
+ * ```ts
1240
+ * const namespace: CacheNamespace = 'app' // single-app default
1241
+ * const tenant: CacheNamespace = 'tenant-42' // per-tenant isolation
1242
+ * ```
1243
+ */
1244
+ type CacheNamespace = string;
1245
+ /**
1246
+ * A logical key prefix that groups related entities under a namespace. Combined
1247
+ * by the key builder as `{namespace}{sep}{prefix}{sep}{id}`.
1248
+ *
1249
+ * @example
1250
+ * ```ts
1251
+ * const prefix: CacheKeyPrefix = 'user' // → app:user:1
1252
+ * const session: CacheKeyPrefix = 'session' // → app:session:abc
1253
+ * ```
1254
+ */
1255
+ type CacheKeyPrefix = string;
1256
+
1257
+ /**
1258
+ * JSON-serializable value type.
1259
+ *
1260
+ * Layer: shared — zero-dependency. Describes exactly what the default
1261
+ * `JsonSerializer` can round-trip through `JSON.stringify`/`JSON.parse`.
1262
+ */
1263
+ /**
1264
+ * A value that survives a JSON round-trip without loss.
1265
+ *
1266
+ * Deliberately excludes `Date`, `Map`, `Set`, `BigInt`, `undefined`, functions,
1267
+ * and class instances — these either throw, silently drop, or change type under
1268
+ * `JSON.stringify`. A consumer that needs them must supply a custom
1269
+ * `ISerializer`; the typed `get<T>`/`set<T>` API does not constrain `T` to this
1270
+ * type so a custom serializer stays unrestricted.
1271
+ *
1272
+ * @example
1273
+ * ```ts
1274
+ * const ok: SerializableValue = { id: 1, tags: ['a'], active: true, parent: null }
1275
+ * // const bad: SerializableValue = { when: new Date() } // WRONG: Date is not serializable
1276
+ * ```
1277
+ */
1278
+ type SerializableValue = string | number | boolean | null | SerializableValue[] | {
1279
+ [key: string]: SerializableValue;
1280
+ };
1281
+
1282
+ /**
1283
+ * Connection lifecycle event names, keyed by symbolic name. Each value is a
1284
+ * {@link CacheEventName}; the `satisfies` clause keeps the object and the union
1285
+ * in lock-step — adding an event to the union without a key here is a type error.
1286
+ */
1287
+ declare const CACHE_EVENT_NAMES: {
1288
+ readonly CONNECT: "connect";
1289
+ readonly READY: "ready";
1290
+ readonly ERROR: "error";
1291
+ readonly CLOSE: "close";
1292
+ readonly RECONNECTING: "reconnecting";
1293
+ readonly END: "end";
1294
+ };
1295
+
1296
+ export { BYMAX_CACHE_CONNECTION, BYMAX_CACHE_EVENTS, BYMAX_CACHE_KEY_BUILDER, BYMAX_CACHE_OPTIONS, BYMAX_CACHE_SCRIPT_REGISTRY, BYMAX_CACHE_SERIALIZER, type BymaxCacheClusterConnection, BymaxCacheModule, type BymaxCacheModuleAsyncOptions, type BymaxCacheModuleOptions, type BymaxCacheSentinelConnection, type BymaxCacheStandaloneConnection, CACHE_ERROR_CODES, CACHE_ERROR_MESSAGES, CACHE_EVENT_NAMES, type CacheConnectionStatus, type CacheErrorCode, type CacheEventName, CacheException, type CacheKeyPrefix, type CacheNamespace, CacheService, ConnectionManager, type ICacheEvents, type IPubSubHandler, type IPubSubPatternHandler, type IScriptDefinition, type ISerializer, JsonSerializer, KeyBuilder, PubSubService, ScriptManagerService, type SerializableValue, type Unsubscribe };