@bymax-one/nest-cache 1.0.3 → 1.0.5

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/CHANGELOG.md CHANGED
@@ -6,6 +6,46 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.5] - 2026-08-06
10
+
11
+ **Documentation, tests and E2E only.** `dist/` is byte-identical to `1.0.4`.
12
+
13
+ ### Fixed
14
+
15
+ - The Redis Cluster E2E was flaky at roughly one run in three, failing with `CLUSTERDOWN` or
16
+ "Too many Cluster redirections". The container log line proves `redis-cli --cluster create`
17
+ printed its coverage message, but what the client needs is the state each node serves on the
18
+ port the test dials. The helper now polls `CLUSTER INFO` on every node until all six report
19
+ `cluster_state:ok` and full slot coverage. Six consecutive runs of the previously flaky spec
20
+ pass, where it failed three times in nine before.
21
+
22
+ ### Documentation
23
+
24
+ - The mutation badge said **100%**; the measured score is **99.78%**.
25
+
26
+ ### Tests
27
+
28
+ - `findProvider` gained a spec of its own; it had only ever been exercised through the module
29
+ suites, where either half of its disjunction covered for the other.
30
+
31
+ ## [1.0.4] - 2026-08-04
32
+
33
+ ### Security
34
+
35
+ - The Redis credentials are no longer disclosed when a service that holds the resolved
36
+ options is serialized. The three connection shapes — `connection`, `sentinel` and
37
+ `cluster` — move from plain fields on the resolved options to non-enumerable accessors,
38
+ and `ConnectionManager` keeps its client and the driver options built from them, as does
39
+ `PubSubService` with its subscriber, in ECMAScript private fields. A `url` carries the
40
+ password inline, the discrete forms carry it as a field, and an ioredis instance carries
41
+ `options.password` as a plain field, so `JSON.stringify`, object spread and
42
+ `util.inspect` on any holder emitted it in plaintext — which is what a structured logger
43
+ does when it renders a provider it was handed, and what an error reporter does when it
44
+ captures the scope of a throw.
45
+
46
+ Reading on purpose is unchanged: `options.connection` resolves as before, and no public
47
+ type or export moved.
48
+
9
49
  ## [1.0.3] - 2026-08-01
10
50
 
11
51
  ### Security
@@ -110,6 +150,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
110
150
  - Published with npm OIDC provenance — no long-lived tokens
111
151
  - Zero direct runtime dependencies (`dependencies: {}`) — `ioredis` and NestJS via peer deps
112
152
 
153
+ [1.0.5]: https://github.com/bymaxone/nest-cache/compare/v1.0.4...v1.0.5
154
+ [1.0.4]: https://github.com/bymaxone/nest-cache/compare/v1.0.3...v1.0.4
113
155
  [1.0.3]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.3
114
156
  [1.0.2]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.2
115
157
  [1.0.1]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.1
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  <a href="https://www.npmjs.com/package/@bymax-one/nest-cache"><img src="https://img.shields.io/npm/dm/@bymax-one/nest-cache?style=flat-square&colorA=000000&colorB=000000" alt="npm downloads" /></a>
15
15
  <a href="https://github.com/bymaxone/nest-cache/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/bymaxone/nest-cache/ci.yml?branch=main&style=flat-square&colorA=000000&label=CI" alt="CI status" /></a>
16
16
  <a href="https://github.com/bymaxone/nest-cache/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square&colorA=000000" alt="coverage" /></a>
17
- <a href="https://github.com/bymaxone/nest-cache/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-100%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
17
+ <a href="https://github.com/bymaxone/nest-cache/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-99.78%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
18
18
  <a href="https://scorecard.dev/viewer/?uri=github.com/bymaxone/nest-cache"><img src="https://api.scorecard.dev/projects/github.com/bymaxone/nest-cache/badge?style=flat-square" alt="OpenSSF Scorecard" /></a>
19
19
  <a href="https://github.com/bymaxone/nest-cache/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bymaxone/nest-cache?style=flat-square&colorA=000000&colorB=000000" alt="license" /></a>
20
20
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" /></a>
@@ -526,7 +526,7 @@ When integrating `@bymax-one/nest-cache` in production, verify each of the follo
526
526
  A cache is consulted on the hot path of every request that touches it, so the suite is held to a bar beyond "it runs" — every behavior is pinned so that a regression **fails a test**.
527
527
 
528
528
  - ✅ **100% line coverage** — statements, branches, functions, and lines, enforced by `jest.coverage.config.ts` as a pre-publish gate, not a target
529
- - ✅ **100% mutation score** — verified with [Stryker](https://stryker-mutator.io/) at `break: 95` and `ignoreStatic: false`: 433 seeded faults detected (427 killed, 6 timed out), **no survivors**
529
+ - ✅ **99.78% mutation score** — verified with [Stryker](https://stryker-mutator.io/) at `break: 95` and `ignoreStatic: false`; the single survivor is `configurable: false` on the withheld connection accessor, equivalent here because the resolved options are frozen on the way out, and [documented as such](./docs/mutation_testing_results.md)
530
530
  - ✅ **Zero suppressions** — the production source carries no coverage or mutation directives; the one would-be equivalent mutant was refactored away rather than silenced, so the score is an accounting rather than a number
531
531
  - ✅ **No real Redis in unit tests** — `ioredis-mock` throughout; e2e tests exercise the wired module through `@nestjs/testing` and Testcontainers against a real Redis for connection lifecycle, Pub/Sub, and Lua scripts
532
532
  - ✅ **Published-package smoke test** — `scripts/dogfood-smoke-test.mjs` validates exports, tarball shape, and a consumer install before tagging
@@ -4,6 +4,9 @@ var common = require('@nestjs/common');
4
4
  var ioredis = require('ioredis');
5
5
 
6
6
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __typeError = (msg) => {
8
+ throw TypeError(msg);
9
+ };
7
10
  var __decorateClass = (decorators, target, key, kind) => {
8
11
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
9
12
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
@@ -12,6 +15,10 @@ var __decorateClass = (decorators, target, key, kind) => {
12
15
  return result;
13
16
  };
14
17
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
18
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
19
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
20
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
21
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
15
22
 
16
23
  // src/server/bymax-cache.constants.ts
17
24
  var BYMAX_CACHE_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CACHE_OPTIONS");
@@ -215,8 +222,17 @@ function applyDefaults(options) {
215
222
  isGlobal: options.isGlobal ?? true,
216
223
  scripts: options.scripts
217
224
  };
225
+ for (const key of ["connection", "sentinel", "cluster"]) {
226
+ const value = Object.getOwnPropertyDescriptor(resolved, key)?.value;
227
+ Object.defineProperty(resolved, key, {
228
+ get: () => value,
229
+ enumerable: false,
230
+ configurable: false
231
+ });
232
+ }
218
233
  return Object.freeze(resolved);
219
234
  }
235
+ var _client, _redisOptionsResolved;
220
236
  exports.ConnectionManager = class ConnectionManager {
221
237
  /**
222
238
  * @param options - Resolved module options (frozen).
@@ -226,19 +242,29 @@ exports.ConnectionManager = class ConnectionManager {
226
242
  constructor(options, events) {
227
243
  this.options = options;
228
244
  this.events = events;
229
- this.client = null;
245
+ /**
246
+ * The live Redis client.
247
+ *
248
+ * An ECMAScript private field rather than a TypeScript `private` one, which is
249
+ * erased at runtime: an ioredis instance carries `options.password` as a plain
250
+ * field, so leaving it enumerable puts the credentials one `JSON.stringify`
251
+ * away from any service that holds this manager.
252
+ */
253
+ __privateAdd(this, _client, null);
254
+ /** The driver options built from the connection config; they carry the password. */
255
+ __privateAdd(this, _redisOptionsResolved);
230
256
  /** Default backoff: grow 50 ms per attempt, capped at 2 s. */
231
257
  this.defaultRetryStrategy = (times) => Math.min(times * DEFAULT_RETRY_BASE_MS, DEFAULT_RETRY_MAX_MS);
232
258
  /** Default reconnect policy: reconnect only on a `READONLY` replica failover. */
233
259
  this.defaultReconnectOnError = (err) => err.message.includes("READONLY");
234
- this.redisOptionsResolved = this.buildRedisOptions(options);
260
+ __privateSet(this, _redisOptionsResolved, this.buildRedisOptions(options));
235
261
  }
236
262
  /** Opens the main client and waits for readiness unless `lazyConnect`. */
237
263
  async onModuleInit() {
238
- this.client = this.createClient();
239
- this.registerListeners(this.client, "main");
264
+ __privateSet(this, _client, this.createClient());
265
+ this.registerListeners(__privateGet(this, _client), "main");
240
266
  if (!this.options.connection?.lazyConnect) {
241
- await this.waitUntilReady(this.client);
267
+ await this.waitUntilReady(__privateGet(this, _client));
242
268
  }
243
269
  }
244
270
  /**
@@ -248,11 +274,11 @@ exports.ConnectionManager = class ConnectionManager {
248
274
  * @returns The shared main client.
249
275
  */
250
276
  getClient() {
251
- if (!this.client) {
252
- this.client = this.createClient();
253
- this.registerListeners(this.client, "main");
277
+ if (!__privateGet(this, _client)) {
278
+ __privateSet(this, _client, this.createClient());
279
+ this.registerListeners(__privateGet(this, _client), "main");
254
280
  }
255
- return this.client;
281
+ return __privateGet(this, _client);
256
282
  }
257
283
  /**
258
284
  * Creates a brand-new dedicated connection for subscriber mode (a subscriber
@@ -278,7 +304,7 @@ exports.ConnectionManager = class ConnectionManager {
278
304
  }
279
305
  /** Quits the main client gracefully, forcing `disconnect()` on timeout. */
280
306
  async onModuleDestroy() {
281
- const client = this.client;
307
+ const client = __privateGet(this, _client);
282
308
  if (!client) {
283
309
  return;
284
310
  }
@@ -306,7 +332,7 @@ exports.ConnectionManager = class ConnectionManager {
306
332
  client.disconnect();
307
333
  } finally {
308
334
  clearTimeout(timer);
309
- this.client = null;
335
+ __privateSet(this, _client, null);
310
336
  }
311
337
  }
312
338
  // ─── Private ──────────────────────────────────────────────────────────────
@@ -324,7 +350,7 @@ exports.ConnectionManager = class ConnectionManager {
324
350
  throw new CacheException(CACHE_ERROR_CODES.SENTINEL_MISCONFIGURED, { mode: "sentinel" });
325
351
  }
326
352
  return new ioredis.Redis({
327
- ...this.redisOptionsResolved,
353
+ ...__privateGet(this, _redisOptionsResolved),
328
354
  ...overrides,
329
355
  sentinels: sentinel.sentinels,
330
356
  name: sentinel.name,
@@ -347,7 +373,7 @@ exports.ConnectionManager = class ConnectionManager {
347
373
  }
348
374
  return new ioredis.Cluster(cluster.nodes, cluster.options ?? {});
349
375
  }
350
- return new ioredis.Redis({ ...this.redisOptionsResolved, ...overrides });
376
+ return new ioredis.Redis({ ...__privateGet(this, _redisOptionsResolved), ...overrides });
351
377
  }
352
378
  /** Merges connection options with defaults; URL fields take precedence. */
353
379
  buildRedisOptions(opts) {
@@ -413,6 +439,8 @@ exports.ConnectionManager = class ConnectionManager {
413
439
  });
414
440
  }
415
441
  };
442
+ _client = new WeakMap();
443
+ _redisOptionsResolved = new WeakMap();
416
444
  exports.ConnectionManager = __decorateClass([
417
445
  common.Injectable(),
418
446
  __decorateParam(0, common.Inject(BYMAX_CACHE_OPTIONS)),
@@ -1259,6 +1287,7 @@ exports.CacheService = __decorateClass([
1259
1287
  __decorateParam(4, common.Optional()),
1260
1288
  __decorateParam(4, common.Inject(exports.ScriptManagerService))
1261
1289
  ], exports.CacheService);
1290
+ var _subscriber;
1262
1291
  exports.PubSubService = class PubSubService {
1263
1292
  /**
1264
1293
  * @param options - Resolved module options. Supplies the serializer.
@@ -1276,7 +1305,13 @@ exports.PubSubService = class PubSubService {
1276
1305
  this.keyBuilder = keyBuilder;
1277
1306
  this.events = events;
1278
1307
  /** Lazily-created dedicated subscriber connection (null until first subscribe). */
1279
- this.subscriber = null;
1308
+ /**
1309
+ * The dedicated subscriber connection.
1310
+ *
1311
+ * Private for the same reason as the manager's client: an ioredis instance
1312
+ * carries `options.password` as a plain field.
1313
+ */
1314
+ __privateAdd(this, _subscriber, null);
1280
1315
  /** Live-listener ref-count per namespaced channel; UNSUBSCRIBE fires on the last. */
1281
1316
  this.channelRefs = /* @__PURE__ */ new Map();
1282
1317
  /** Live-listener ref-count per namespaced pattern; PUNSUBSCRIBE fires on the last. */
@@ -1379,11 +1414,11 @@ exports.PubSubService = class PubSubService {
1379
1414
  }
1380
1415
  /** Closes the subscriber connection gracefully, forcing disconnect on failure. */
1381
1416
  async onModuleDestroy() {
1382
- if (!this.subscriber) {
1417
+ if (!__privateGet(this, _subscriber)) {
1383
1418
  return;
1384
1419
  }
1385
- const subscriber = this.subscriber;
1386
- this.subscriber = null;
1420
+ const subscriber = __privateGet(this, _subscriber);
1421
+ __privateSet(this, _subscriber, null);
1387
1422
  try {
1388
1423
  await subscriber.quit();
1389
1424
  } catch {
@@ -1401,10 +1436,10 @@ exports.PubSubService = class PubSubService {
1401
1436
  * experimental passthrough per the spec).
1402
1437
  */
1403
1438
  ensureSubscriber() {
1404
- if (!this.subscriber) {
1405
- this.subscriber = this.connection.createSubscriberClient();
1439
+ if (!__privateGet(this, _subscriber)) {
1440
+ __privateSet(this, _subscriber, this.connection.createSubscriberClient());
1406
1441
  }
1407
- return this.subscriber;
1442
+ return __privateGet(this, _subscriber);
1408
1443
  }
1409
1444
  /**
1410
1445
  * Subscribes the target (channel or pattern) only when it gains its FIRST
@@ -1483,6 +1518,7 @@ exports.PubSubService = class PubSubService {
1483
1518
  }
1484
1519
  }
1485
1520
  };
1521
+ _subscriber = new WeakMap();
1486
1522
  exports.PubSubService = __decorateClass([
1487
1523
  common.Injectable(),
1488
1524
  __decorateParam(0, common.Inject(BYMAX_CACHE_OPTIONS)),
@@ -356,10 +356,9 @@ type ResolvedOptions = Required<Pick<BymaxCacheModuleOptions, 'mode' | 'namespac
356
356
  /** The two client flavors this manager can produce. */
357
357
  type AnyRedis = Redis | Cluster;
358
358
  declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
359
+ #private;
359
360
  private readonly options;
360
361
  private readonly events?;
361
- private client;
362
- private readonly redisOptionsResolved;
363
362
  /** Default backoff: grow 50 ms per attempt, capped at 2 s. */
364
363
  private readonly defaultRetryStrategy;
365
364
  /** Default reconnect policy: reconnect only on a `READONLY` replica failover. */
@@ -991,11 +990,10 @@ type IPubSubPatternHandler<T> = (message: T, channel: string, pattern: string) =
991
990
  /** Detaches a subscription's listener and unsubscribes its channel/pattern. */
992
991
  type Unsubscribe = () => Promise<void>;
993
992
  declare class PubSubService implements OnModuleDestroy {
993
+ #private;
994
994
  private readonly connection;
995
995
  private readonly keyBuilder;
996
996
  private readonly events?;
997
- /** Lazily-created dedicated subscriber connection (null until first subscribe). */
998
- private subscriber;
999
997
  /** Live-listener ref-count per namespaced channel; UNSUBSCRIBE fires on the last. */
1000
998
  private readonly channelRefs;
1001
999
  /** Live-listener ref-count per namespaced pattern; PUNSUBSCRIBE fires on the last. */
@@ -356,10 +356,9 @@ type ResolvedOptions = Required<Pick<BymaxCacheModuleOptions, 'mode' | 'namespac
356
356
  /** The two client flavors this manager can produce. */
357
357
  type AnyRedis = Redis | Cluster;
358
358
  declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
359
+ #private;
359
360
  private readonly options;
360
361
  private readonly events?;
361
- private client;
362
- private readonly redisOptionsResolved;
363
362
  /** Default backoff: grow 50 ms per attempt, capped at 2 s. */
364
363
  private readonly defaultRetryStrategy;
365
364
  /** Default reconnect policy: reconnect only on a `READONLY` replica failover. */
@@ -991,11 +990,10 @@ type IPubSubPatternHandler<T> = (message: T, channel: string, pattern: string) =
991
990
  /** Detaches a subscription's listener and unsubscribes its channel/pattern. */
992
991
  type Unsubscribe = () => Promise<void>;
993
992
  declare class PubSubService implements OnModuleDestroy {
993
+ #private;
994
994
  private readonly connection;
995
995
  private readonly keyBuilder;
996
996
  private readonly events?;
997
- /** Lazily-created dedicated subscriber connection (null until first subscribe). */
998
- private subscriber;
999
997
  /** Live-listener ref-count per namespaced channel; UNSUBSCRIBE fires on the last. */
1000
998
  private readonly channelRefs;
1001
999
  /** Live-listener ref-count per namespaced pattern; PUNSUBSCRIBE fires on the last. */
@@ -2,6 +2,9 @@ import { ConfigurableModuleBuilder, HttpStatus, Injectable, Inject, Optional, Mo
2
2
  import { Redis, Cluster } from 'ioredis';
3
3
 
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __typeError = (msg) => {
6
+ throw TypeError(msg);
7
+ };
5
8
  var __decorateClass = (decorators, target, key, kind) => {
6
9
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
7
10
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
@@ -10,6 +13,10 @@ var __decorateClass = (decorators, target, key, kind) => {
10
13
  return result;
11
14
  };
12
15
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
16
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
17
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
18
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
19
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
13
20
 
14
21
  // src/server/bymax-cache.constants.ts
15
22
  var BYMAX_CACHE_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CACHE_OPTIONS");
@@ -213,8 +220,17 @@ function applyDefaults(options) {
213
220
  isGlobal: options.isGlobal ?? true,
214
221
  scripts: options.scripts
215
222
  };
223
+ for (const key of ["connection", "sentinel", "cluster"]) {
224
+ const value = Object.getOwnPropertyDescriptor(resolved, key)?.value;
225
+ Object.defineProperty(resolved, key, {
226
+ get: () => value,
227
+ enumerable: false,
228
+ configurable: false
229
+ });
230
+ }
216
231
  return Object.freeze(resolved);
217
232
  }
233
+ var _client, _redisOptionsResolved;
218
234
  var ConnectionManager = class {
219
235
  /**
220
236
  * @param options - Resolved module options (frozen).
@@ -224,19 +240,29 @@ var ConnectionManager = class {
224
240
  constructor(options, events) {
225
241
  this.options = options;
226
242
  this.events = events;
227
- this.client = null;
243
+ /**
244
+ * The live Redis client.
245
+ *
246
+ * An ECMAScript private field rather than a TypeScript `private` one, which is
247
+ * erased at runtime: an ioredis instance carries `options.password` as a plain
248
+ * field, so leaving it enumerable puts the credentials one `JSON.stringify`
249
+ * away from any service that holds this manager.
250
+ */
251
+ __privateAdd(this, _client, null);
252
+ /** The driver options built from the connection config; they carry the password. */
253
+ __privateAdd(this, _redisOptionsResolved);
228
254
  /** Default backoff: grow 50 ms per attempt, capped at 2 s. */
229
255
  this.defaultRetryStrategy = (times) => Math.min(times * DEFAULT_RETRY_BASE_MS, DEFAULT_RETRY_MAX_MS);
230
256
  /** Default reconnect policy: reconnect only on a `READONLY` replica failover. */
231
257
  this.defaultReconnectOnError = (err) => err.message.includes("READONLY");
232
- this.redisOptionsResolved = this.buildRedisOptions(options);
258
+ __privateSet(this, _redisOptionsResolved, this.buildRedisOptions(options));
233
259
  }
234
260
  /** Opens the main client and waits for readiness unless `lazyConnect`. */
235
261
  async onModuleInit() {
236
- this.client = this.createClient();
237
- this.registerListeners(this.client, "main");
262
+ __privateSet(this, _client, this.createClient());
263
+ this.registerListeners(__privateGet(this, _client), "main");
238
264
  if (!this.options.connection?.lazyConnect) {
239
- await this.waitUntilReady(this.client);
265
+ await this.waitUntilReady(__privateGet(this, _client));
240
266
  }
241
267
  }
242
268
  /**
@@ -246,11 +272,11 @@ var ConnectionManager = class {
246
272
  * @returns The shared main client.
247
273
  */
248
274
  getClient() {
249
- if (!this.client) {
250
- this.client = this.createClient();
251
- this.registerListeners(this.client, "main");
275
+ if (!__privateGet(this, _client)) {
276
+ __privateSet(this, _client, this.createClient());
277
+ this.registerListeners(__privateGet(this, _client), "main");
252
278
  }
253
- return this.client;
279
+ return __privateGet(this, _client);
254
280
  }
255
281
  /**
256
282
  * Creates a brand-new dedicated connection for subscriber mode (a subscriber
@@ -276,7 +302,7 @@ var ConnectionManager = class {
276
302
  }
277
303
  /** Quits the main client gracefully, forcing `disconnect()` on timeout. */
278
304
  async onModuleDestroy() {
279
- const client = this.client;
305
+ const client = __privateGet(this, _client);
280
306
  if (!client) {
281
307
  return;
282
308
  }
@@ -304,7 +330,7 @@ var ConnectionManager = class {
304
330
  client.disconnect();
305
331
  } finally {
306
332
  clearTimeout(timer);
307
- this.client = null;
333
+ __privateSet(this, _client, null);
308
334
  }
309
335
  }
310
336
  // ─── Private ──────────────────────────────────────────────────────────────
@@ -322,7 +348,7 @@ var ConnectionManager = class {
322
348
  throw new CacheException(CACHE_ERROR_CODES.SENTINEL_MISCONFIGURED, { mode: "sentinel" });
323
349
  }
324
350
  return new Redis({
325
- ...this.redisOptionsResolved,
351
+ ...__privateGet(this, _redisOptionsResolved),
326
352
  ...overrides,
327
353
  sentinels: sentinel.sentinels,
328
354
  name: sentinel.name,
@@ -345,7 +371,7 @@ var ConnectionManager = class {
345
371
  }
346
372
  return new Cluster(cluster.nodes, cluster.options ?? {});
347
373
  }
348
- return new Redis({ ...this.redisOptionsResolved, ...overrides });
374
+ return new Redis({ ...__privateGet(this, _redisOptionsResolved), ...overrides });
349
375
  }
350
376
  /** Merges connection options with defaults; URL fields take precedence. */
351
377
  buildRedisOptions(opts) {
@@ -411,6 +437,8 @@ var ConnectionManager = class {
411
437
  });
412
438
  }
413
439
  };
440
+ _client = new WeakMap();
441
+ _redisOptionsResolved = new WeakMap();
414
442
  ConnectionManager = __decorateClass([
415
443
  Injectable(),
416
444
  __decorateParam(0, Inject(BYMAX_CACHE_OPTIONS)),
@@ -1257,6 +1285,7 @@ CacheService = __decorateClass([
1257
1285
  __decorateParam(4, Optional()),
1258
1286
  __decorateParam(4, Inject(ScriptManagerService))
1259
1287
  ], CacheService);
1288
+ var _subscriber;
1260
1289
  var PubSubService = class {
1261
1290
  /**
1262
1291
  * @param options - Resolved module options. Supplies the serializer.
@@ -1274,7 +1303,13 @@ var PubSubService = class {
1274
1303
  this.keyBuilder = keyBuilder;
1275
1304
  this.events = events;
1276
1305
  /** Lazily-created dedicated subscriber connection (null until first subscribe). */
1277
- this.subscriber = null;
1306
+ /**
1307
+ * The dedicated subscriber connection.
1308
+ *
1309
+ * Private for the same reason as the manager's client: an ioredis instance
1310
+ * carries `options.password` as a plain field.
1311
+ */
1312
+ __privateAdd(this, _subscriber, null);
1278
1313
  /** Live-listener ref-count per namespaced channel; UNSUBSCRIBE fires on the last. */
1279
1314
  this.channelRefs = /* @__PURE__ */ new Map();
1280
1315
  /** Live-listener ref-count per namespaced pattern; PUNSUBSCRIBE fires on the last. */
@@ -1377,11 +1412,11 @@ var PubSubService = class {
1377
1412
  }
1378
1413
  /** Closes the subscriber connection gracefully, forcing disconnect on failure. */
1379
1414
  async onModuleDestroy() {
1380
- if (!this.subscriber) {
1415
+ if (!__privateGet(this, _subscriber)) {
1381
1416
  return;
1382
1417
  }
1383
- const subscriber = this.subscriber;
1384
- this.subscriber = null;
1418
+ const subscriber = __privateGet(this, _subscriber);
1419
+ __privateSet(this, _subscriber, null);
1385
1420
  try {
1386
1421
  await subscriber.quit();
1387
1422
  } catch {
@@ -1399,10 +1434,10 @@ var PubSubService = class {
1399
1434
  * experimental passthrough per the spec).
1400
1435
  */
1401
1436
  ensureSubscriber() {
1402
- if (!this.subscriber) {
1403
- this.subscriber = this.connection.createSubscriberClient();
1437
+ if (!__privateGet(this, _subscriber)) {
1438
+ __privateSet(this, _subscriber, this.connection.createSubscriberClient());
1404
1439
  }
1405
- return this.subscriber;
1440
+ return __privateGet(this, _subscriber);
1406
1441
  }
1407
1442
  /**
1408
1443
  * Subscribes the target (channel or pattern) only when it gains its FIRST
@@ -1481,6 +1516,7 @@ var PubSubService = class {
1481
1516
  }
1482
1517
  }
1483
1518
  };
1519
+ _subscriber = new WeakMap();
1484
1520
  PubSubService = __decorateClass([
1485
1521
  Injectable(),
1486
1522
  __decorateParam(0, Inject(BYMAX_CACHE_OPTIONS)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-cache",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Typed Redis cache for NestJS based on ioredis 5, with namespace strategy, Pub/Sub and Lua script management.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",
@@ -53,6 +53,29 @@
53
53
  ]
54
54
  }
55
55
  },
56
+ "scripts": {
57
+ "build": "pnpm clean && tsup",
58
+ "check:exports": "attw --pack .",
59
+ "check:published": "node scripts/check-published-surface.mjs",
60
+ "clean": "rm -rf dist coverage",
61
+ "lint": "eslint src scripts",
62
+ "lint:fix": "eslint src scripts --fix",
63
+ "mutation": "stryker run",
64
+ "mutation:dry-run": "stryker run --dryRunOnly",
65
+ "mutation:incremental": "stryker run --incremental",
66
+ "prepare": "husky",
67
+ "prepublishOnly": "pnpm clean && pnpm typecheck && pnpm test:types && pnpm lint && pnpm test:cov:all && pnpm build && pnpm check:published",
68
+ "release": "npm publish --provenance --access public",
69
+ "size": "node scripts/check-size.mjs",
70
+ "test": "jest",
71
+ "test:all": "pnpm test && pnpm test:e2e",
72
+ "test:cov": "jest --coverage",
73
+ "test:cov:all": "jest --config jest.coverage.config.ts --coverage",
74
+ "test:e2e": "jest --config jest.e2e.config.ts --runInBand",
75
+ "test:types": "tsc --noEmit -p tsconfig.typetest.json",
76
+ "test:watch": "jest --watch",
77
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json"
78
+ },
56
79
  "lint-staged": {
57
80
  "*.{ts,tsx,js,mjs,cjs}": [
58
81
  "eslint --fix",
@@ -116,32 +139,12 @@
116
139
  "pubsub",
117
140
  "lua"
118
141
  ],
142
+ "packageManager": "pnpm@11.20.0",
119
143
  "engines": {
120
144
  "node": ">=24.0.0"
121
145
  },
122
146
  "publishConfig": {
123
147
  "access": "public",
124
148
  "registry": "https://registry.npmjs.org/"
125
- },
126
- "scripts": {
127
- "build": "pnpm clean && tsup",
128
- "check:exports": "attw --pack .",
129
- "check:published": "node scripts/check-published-surface.mjs",
130
- "clean": "rm -rf dist coverage",
131
- "lint": "eslint src scripts",
132
- "lint:fix": "eslint src scripts --fix",
133
- "mutation": "stryker run",
134
- "mutation:dry-run": "stryker run --dryRunOnly",
135
- "mutation:incremental": "stryker run --incremental",
136
- "release": "pnpm publish --provenance",
137
- "size": "node scripts/check-size.mjs",
138
- "test": "jest",
139
- "test:all": "pnpm test && pnpm test:e2e",
140
- "test:cov": "jest --coverage",
141
- "test:cov:all": "jest --config jest.coverage.config.ts --coverage",
142
- "test:e2e": "jest --config jest.e2e.config.ts --runInBand",
143
- "test:types": "tsc --noEmit -p tsconfig.typetest.json",
144
- "test:watch": "jest --watch",
145
- "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json"
146
149
  }
147
- }
150
+ }