@bymax-one/nest-cache 1.0.2 → 1.0.4
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 +46 -0
- package/dist/server/index.cjs +56 -20
- package/dist/server/index.d.cts +2 -4
- package/dist/server/index.d.ts +2 -4
- package/dist/server/index.mjs +56 -20
- package/package.json +41 -23
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.4] - 2026-08-04
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
- The Redis credentials are no longer disclosed when a service that holds the resolved
|
|
14
|
+
options is serialized. The three connection shapes — `connection`, `sentinel` and
|
|
15
|
+
`cluster` — move from plain fields on the resolved options to non-enumerable accessors,
|
|
16
|
+
and `ConnectionManager` keeps its client and the driver options built from them, as does
|
|
17
|
+
`PubSubService` with its subscriber, in ECMAScript private fields. A `url` carries the
|
|
18
|
+
password inline, the discrete forms carry it as a field, and an ioredis instance carries
|
|
19
|
+
`options.password` as a plain field, so `JSON.stringify`, object spread and
|
|
20
|
+
`util.inspect` on any holder emitted it in plaintext — which is what a structured logger
|
|
21
|
+
does when it renders a provider it was handed, and what an error reporter does when it
|
|
22
|
+
captures the scope of a throw.
|
|
23
|
+
|
|
24
|
+
Reading on purpose is unchanged: `options.connection` resolves as before, and no public
|
|
25
|
+
type or export moved.
|
|
26
|
+
|
|
27
|
+
## [1.0.3] - 2026-08-01
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
|
|
31
|
+
- **Peer floors raised to exclude known-vulnerable NestJS versions.** The declared
|
|
32
|
+
ranges were `@nestjs/common ^11.0.0` and `@nestjs/core ^11.0.0`, and both
|
|
33
|
+
admitted versions carrying published advisories:
|
|
34
|
+
|
|
35
|
+
| Peer | Advisory | Vulnerable | New floor |
|
|
36
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ---------- |
|
|
37
|
+
| `@nestjs/common` | [GHSA-cj7v-w2c7-cp7c](https://github.com/advisories/GHSA-cj7v-w2c7-cp7c) — remote code execution via the `Content-Type` header | `>= 11.0.0-next.1, < 11.0.16` | `^11.0.16` |
|
|
38
|
+
| `@nestjs/core` | [GHSA-36xv-jgw5-4q75](https://github.com/advisories/GHSA-36xv-jgw5-4q75) — improper neutralization of special elements in downstream output | `<= 11.1.17` | `^11.1.18` |
|
|
39
|
+
|
|
40
|
+
A peer range is a statement about which versions this library supports. Leaving
|
|
41
|
+
the floor below a published advisory told a consumer that a vulnerable install
|
|
42
|
+
was a supported one, and nothing in their tooling contradicted it — the install
|
|
43
|
+
resolved cleanly and silently.
|
|
44
|
+
|
|
45
|
+
Shipped as a patch, which is where a security fix belongs. A minor would have
|
|
46
|
+
bought nothing: `^1.0.2` accepts `1.1.0` just as readily as `1.0.3`, so the same
|
|
47
|
+
installs are affected either way. No runtime behaviour changed.
|
|
48
|
+
|
|
9
49
|
## [1.0.2] - 2026-07-29
|
|
10
50
|
|
|
11
51
|
### Fixed
|
|
@@ -87,3 +127,9 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
87
127
|
- E2E suite: `@nestjs/testing` with `ioredis-mock` + Testcontainers (standalone, sentinel, cluster, connection resilience)
|
|
88
128
|
- Published with npm OIDC provenance — no long-lived tokens
|
|
89
129
|
- Zero direct runtime dependencies (`dependencies: {}`) — `ioredis` and NestJS via peer deps
|
|
130
|
+
|
|
131
|
+
[1.0.4]: https://github.com/bymaxone/nest-cache/compare/v1.0.3...v1.0.4
|
|
132
|
+
[1.0.3]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.3
|
|
133
|
+
[1.0.2]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.2
|
|
134
|
+
[1.0.1]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.1
|
|
135
|
+
[1.0.0]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.0
|
package/dist/server/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
239
|
-
this.registerListeners(this
|
|
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
|
|
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
|
|
252
|
-
this
|
|
253
|
-
this.registerListeners(this
|
|
277
|
+
if (!__privateGet(this, _client)) {
|
|
278
|
+
__privateSet(this, _client, this.createClient());
|
|
279
|
+
this.registerListeners(__privateGet(this, _client), "main");
|
|
254
280
|
}
|
|
255
|
-
return this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1417
|
+
if (!__privateGet(this, _subscriber)) {
|
|
1383
1418
|
return;
|
|
1384
1419
|
}
|
|
1385
|
-
const subscriber = this
|
|
1386
|
-
this
|
|
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
|
|
1405
|
-
this
|
|
1439
|
+
if (!__privateGet(this, _subscriber)) {
|
|
1440
|
+
__privateSet(this, _subscriber, this.connection.createSubscriberClient());
|
|
1406
1441
|
}
|
|
1407
|
-
return this
|
|
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)),
|
package/dist/server/index.d.cts
CHANGED
|
@@ -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. */
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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. */
|
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
237
|
-
this.registerListeners(this
|
|
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
|
|
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
|
|
250
|
-
this
|
|
251
|
-
this.registerListeners(this
|
|
275
|
+
if (!__privateGet(this, _client)) {
|
|
276
|
+
__privateSet(this, _client, this.createClient());
|
|
277
|
+
this.registerListeners(__privateGet(this, _client), "main");
|
|
252
278
|
}
|
|
253
|
-
return this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1415
|
+
if (!__privateGet(this, _subscriber)) {
|
|
1381
1416
|
return;
|
|
1382
1417
|
}
|
|
1383
|
-
const subscriber = this
|
|
1384
|
-
this
|
|
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
|
|
1403
|
-
this
|
|
1437
|
+
if (!__privateGet(this, _subscriber)) {
|
|
1438
|
+
__privateSet(this, _subscriber, this.connection.createSubscriberClient());
|
|
1404
1439
|
}
|
|
1405
|
-
return this
|
|
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
|
+
"version": "1.0.4",
|
|
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",
|
|
@@ -64,8 +87,8 @@
|
|
|
64
87
|
},
|
|
65
88
|
"dependencies": {},
|
|
66
89
|
"peerDependencies": {
|
|
67
|
-
"@nestjs/common": "^11.0.
|
|
68
|
-
"@nestjs/core": "^11.
|
|
90
|
+
"@nestjs/common": "^11.0.16",
|
|
91
|
+
"@nestjs/core": "^11.1.18",
|
|
69
92
|
"ioredis": "^5.0.0",
|
|
70
93
|
"reflect-metadata": "^0.2.0"
|
|
71
94
|
},
|
|
@@ -116,6 +139,7 @@
|
|
|
116
139
|
"pubsub",
|
|
117
140
|
"lua"
|
|
118
141
|
],
|
|
142
|
+
"packageManager": "pnpm@10.8.1",
|
|
119
143
|
"engines": {
|
|
120
144
|
"node": ">=24.0.0"
|
|
121
145
|
},
|
|
@@ -123,24 +147,18 @@
|
|
|
123
147
|
"access": "public",
|
|
124
148
|
"registry": "https://registry.npmjs.org/"
|
|
125
149
|
},
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json",
|
|
140
|
-
"test:types": "tsc --noEmit -p tsconfig.typetest.json",
|
|
141
|
-
"size": "node scripts/check-size.mjs",
|
|
142
|
-
"check:exports": "attw --pack .",
|
|
143
|
-
"clean": "rm -rf dist coverage",
|
|
144
|
-
"release": "pnpm publish --provenance"
|
|
150
|
+
"pnpm": {
|
|
151
|
+
"overrides": {
|
|
152
|
+
"brace-expansion@1": "^1.1.18",
|
|
153
|
+
"brace-expansion@2": "^2.1.4",
|
|
154
|
+
"brace-expansion@5": "^5.0.9",
|
|
155
|
+
"esbuild": "^0.28.1",
|
|
156
|
+
"fast-uri": "^3.1.5",
|
|
157
|
+
"js-yaml@3": "^3.15.0",
|
|
158
|
+
"js-yaml@4": "^4.3.0",
|
|
159
|
+
"protobufjs": "^7.6.5",
|
|
160
|
+
"qs": "^6.15.2",
|
|
161
|
+
"undici": "^7.28.0"
|
|
162
|
+
}
|
|
145
163
|
}
|
|
146
|
-
}
|
|
164
|
+
}
|