@bymax-one/nest-cache 1.0.4 → 1.0.6
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 +42 -0
- package/README.md +2 -2
- package/dist/server/index.cjs +27 -9
- package/dist/server/index.d.cts +17 -1
- package/dist/server/index.d.ts +17 -1
- package/dist/server/index.mjs +27 -9
- package/package.json +4 -17
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.6] - 2026-08-06
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `ConnectionManager.onModuleInit()` assigned a freshly created client over the field
|
|
14
|
+
unconditionally. When something had already opened the main client through
|
|
15
|
+
`getClient()`, that first socket was left connected with no reference left to close it —
|
|
16
|
+
`onModuleDestroy()` quits only the current client, so the abandoned one stayed in Redis's
|
|
17
|
+
`CLIENT LIST` until its own timeout. Init now adopts an existing client instead of
|
|
18
|
+
replacing it, and the assignment lives in one private accessor so no other path can
|
|
19
|
+
strand a live connection.
|
|
20
|
+
|
|
21
|
+
Reaching it takes touching the cache before the cache module's own init hook runs, which
|
|
22
|
+
is possible in two ordinary shapes: NestJS orders `onModuleInit` by module depth, so a
|
|
23
|
+
consumer module deeper in the graph runs first, and `app.get()` works between
|
|
24
|
+
`NestFactory.create()` and `app.init()`. The ordinary boot path opened exactly one client
|
|
25
|
+
before this change and still does.
|
|
26
|
+
|
|
27
|
+
## [1.0.5] - 2026-08-06
|
|
28
|
+
|
|
29
|
+
**Documentation, tests and E2E only.** `dist/` is byte-identical to `1.0.4`.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- The Redis Cluster E2E was flaky at roughly one run in three, failing with `CLUSTERDOWN` or
|
|
34
|
+
"Too many Cluster redirections". The container log line proves `redis-cli --cluster create`
|
|
35
|
+
printed its coverage message, but what the client needs is the state each node serves on the
|
|
36
|
+
port the test dials. The helper now polls `CLUSTER INFO` on every node until all six report
|
|
37
|
+
`cluster_state:ok` and full slot coverage. Six consecutive runs of the previously flaky spec
|
|
38
|
+
pass, where it failed three times in nine before.
|
|
39
|
+
|
|
40
|
+
### Documentation
|
|
41
|
+
|
|
42
|
+
- The mutation badge said **100%**; the measured score is **99.78%**.
|
|
43
|
+
|
|
44
|
+
### Tests
|
|
45
|
+
|
|
46
|
+
- `findProvider` gained a spec of its own; it had only ever been exercised through the module
|
|
47
|
+
suites, where either half of its disjunction covered for the other.
|
|
48
|
+
|
|
9
49
|
## [1.0.4] - 2026-08-04
|
|
10
50
|
|
|
11
51
|
### Security
|
|
@@ -128,6 +168,8 @@ type or export moved.
|
|
|
128
168
|
- Published with npm OIDC provenance — no long-lived tokens
|
|
129
169
|
- Zero direct runtime dependencies (`dependencies: {}`) — `ioredis` and NestJS via peer deps
|
|
130
170
|
|
|
171
|
+
[1.0.6]: https://github.com/bymaxone/nest-cache/compare/v1.0.5...v1.0.6
|
|
172
|
+
[1.0.5]: https://github.com/bymaxone/nest-cache/compare/v1.0.4...v1.0.5
|
|
131
173
|
[1.0.4]: https://github.com/bymaxone/nest-cache/compare/v1.0.3...v1.0.4
|
|
132
174
|
[1.0.3]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.3
|
|
133
175
|
[1.0.2]: https://github.com/bymaxone/nest-cache/releases/tag/v1.0.2
|
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-
|
|
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
|
-
- ✅ **
|
|
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
|
package/dist/server/index.cjs
CHANGED
|
@@ -227,6 +227,7 @@ function applyDefaults(options) {
|
|
|
227
227
|
Object.defineProperty(resolved, key, {
|
|
228
228
|
get: () => value,
|
|
229
229
|
enumerable: false,
|
|
230
|
+
// Stryker disable next-line BooleanLiteral: equivalent HERE — `resolved` is `Object.freeze`d on the way out and freezing makes every property non-configurable anyway, so flipping this flag changes nothing observable. It stays because it states the guarantee where the accessor is defined, and nest-storage withholds its credentials the same way WITHOUT freezing, where this flag is the only thing enforcing it.
|
|
230
231
|
configurable: false
|
|
231
232
|
});
|
|
232
233
|
}
|
|
@@ -259,12 +260,19 @@ exports.ConnectionManager = class ConnectionManager {
|
|
|
259
260
|
this.defaultReconnectOnError = (err) => err.message.includes("READONLY");
|
|
260
261
|
__privateSet(this, _redisOptionsResolved, this.buildRedisOptions(options));
|
|
261
262
|
}
|
|
262
|
-
/**
|
|
263
|
+
/**
|
|
264
|
+
* Opens the main client and waits for readiness unless `lazyConnect`.
|
|
265
|
+
*
|
|
266
|
+
* Adopts a client {@link getClient} already opened rather than replacing it:
|
|
267
|
+
* NestJS orders `onModuleInit` by module depth, and `app.get()` works before
|
|
268
|
+
* `app.init()`, so a consumer can reach the cache first. Overwriting here
|
|
269
|
+
* would strand that socket with no reference left to close it — teardown
|
|
270
|
+
* quits only the current client.
|
|
271
|
+
*/
|
|
263
272
|
async onModuleInit() {
|
|
264
|
-
|
|
265
|
-
this.registerListeners(__privateGet(this, _client), "main");
|
|
273
|
+
const client = this.ensureClient();
|
|
266
274
|
if (!this.options.connection?.lazyConnect) {
|
|
267
|
-
await this.waitUntilReady(
|
|
275
|
+
await this.waitUntilReady(client);
|
|
268
276
|
}
|
|
269
277
|
}
|
|
270
278
|
/**
|
|
@@ -274,11 +282,7 @@ exports.ConnectionManager = class ConnectionManager {
|
|
|
274
282
|
* @returns The shared main client.
|
|
275
283
|
*/
|
|
276
284
|
getClient() {
|
|
277
|
-
|
|
278
|
-
__privateSet(this, _client, this.createClient());
|
|
279
|
-
this.registerListeners(__privateGet(this, _client), "main");
|
|
280
|
-
}
|
|
281
|
-
return __privateGet(this, _client);
|
|
285
|
+
return this.ensureClient();
|
|
282
286
|
}
|
|
283
287
|
/**
|
|
284
288
|
* Creates a brand-new dedicated connection for subscriber mode (a subscriber
|
|
@@ -336,6 +340,20 @@ exports.ConnectionManager = class ConnectionManager {
|
|
|
336
340
|
}
|
|
337
341
|
}
|
|
338
342
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
343
|
+
/**
|
|
344
|
+
* Returns the main client, opening and wiring it on the first call. The only
|
|
345
|
+
* place a live client is assigned, so no caller can replace one; teardown
|
|
346
|
+
* only clears the field back to `null`.
|
|
347
|
+
*
|
|
348
|
+
* @returns The shared main client.
|
|
349
|
+
*/
|
|
350
|
+
ensureClient() {
|
|
351
|
+
if (!__privateGet(this, _client)) {
|
|
352
|
+
__privateSet(this, _client, this.createClient());
|
|
353
|
+
this.registerListeners(__privateGet(this, _client), "main");
|
|
354
|
+
}
|
|
355
|
+
return __privateGet(this, _client);
|
|
356
|
+
}
|
|
339
357
|
/**
|
|
340
358
|
* Instantiates the client matching the configured mode.
|
|
341
359
|
*
|
package/dist/server/index.d.cts
CHANGED
|
@@ -369,7 +369,15 @@ declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
|
|
|
369
369
|
* module can provide `null` when the consumer omits `events`.
|
|
370
370
|
*/
|
|
371
371
|
constructor(options: ResolvedOptions, events?: ICacheEvents | undefined);
|
|
372
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Opens the main client and waits for readiness unless `lazyConnect`.
|
|
374
|
+
*
|
|
375
|
+
* Adopts a client {@link getClient} already opened rather than replacing it:
|
|
376
|
+
* NestJS orders `onModuleInit` by module depth, and `app.get()` works before
|
|
377
|
+
* `app.init()`, so a consumer can reach the cache first. Overwriting here
|
|
378
|
+
* would strand that socket with no reference left to close it — teardown
|
|
379
|
+
* quits only the current client.
|
|
380
|
+
*/
|
|
373
381
|
onModuleInit(): Promise<void>;
|
|
374
382
|
/**
|
|
375
383
|
* Returns the singleton main client, creating it on first access if the
|
|
@@ -398,6 +406,14 @@ declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
|
|
|
398
406
|
createSubscriberClient(): AnyRedis;
|
|
399
407
|
/** Quits the main client gracefully, forcing `disconnect()` on timeout. */
|
|
400
408
|
onModuleDestroy(): Promise<void>;
|
|
409
|
+
/**
|
|
410
|
+
* Returns the main client, opening and wiring it on the first call. The only
|
|
411
|
+
* place a live client is assigned, so no caller can replace one; teardown
|
|
412
|
+
* only clears the field back to `null`.
|
|
413
|
+
*
|
|
414
|
+
* @returns The shared main client.
|
|
415
|
+
*/
|
|
416
|
+
private ensureClient;
|
|
401
417
|
/**
|
|
402
418
|
* Instantiates the client matching the configured mode.
|
|
403
419
|
*
|
package/dist/server/index.d.ts
CHANGED
|
@@ -369,7 +369,15 @@ declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
|
|
|
369
369
|
* module can provide `null` when the consumer omits `events`.
|
|
370
370
|
*/
|
|
371
371
|
constructor(options: ResolvedOptions, events?: ICacheEvents | undefined);
|
|
372
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Opens the main client and waits for readiness unless `lazyConnect`.
|
|
374
|
+
*
|
|
375
|
+
* Adopts a client {@link getClient} already opened rather than replacing it:
|
|
376
|
+
* NestJS orders `onModuleInit` by module depth, and `app.get()` works before
|
|
377
|
+
* `app.init()`, so a consumer can reach the cache first. Overwriting here
|
|
378
|
+
* would strand that socket with no reference left to close it — teardown
|
|
379
|
+
* quits only the current client.
|
|
380
|
+
*/
|
|
373
381
|
onModuleInit(): Promise<void>;
|
|
374
382
|
/**
|
|
375
383
|
* Returns the singleton main client, creating it on first access if the
|
|
@@ -398,6 +406,14 @@ declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
|
|
|
398
406
|
createSubscriberClient(): AnyRedis;
|
|
399
407
|
/** Quits the main client gracefully, forcing `disconnect()` on timeout. */
|
|
400
408
|
onModuleDestroy(): Promise<void>;
|
|
409
|
+
/**
|
|
410
|
+
* Returns the main client, opening and wiring it on the first call. The only
|
|
411
|
+
* place a live client is assigned, so no caller can replace one; teardown
|
|
412
|
+
* only clears the field back to `null`.
|
|
413
|
+
*
|
|
414
|
+
* @returns The shared main client.
|
|
415
|
+
*/
|
|
416
|
+
private ensureClient;
|
|
401
417
|
/**
|
|
402
418
|
* Instantiates the client matching the configured mode.
|
|
403
419
|
*
|
package/dist/server/index.mjs
CHANGED
|
@@ -225,6 +225,7 @@ function applyDefaults(options) {
|
|
|
225
225
|
Object.defineProperty(resolved, key, {
|
|
226
226
|
get: () => value,
|
|
227
227
|
enumerable: false,
|
|
228
|
+
// Stryker disable next-line BooleanLiteral: equivalent HERE — `resolved` is `Object.freeze`d on the way out and freezing makes every property non-configurable anyway, so flipping this flag changes nothing observable. It stays because it states the guarantee where the accessor is defined, and nest-storage withholds its credentials the same way WITHOUT freezing, where this flag is the only thing enforcing it.
|
|
228
229
|
configurable: false
|
|
229
230
|
});
|
|
230
231
|
}
|
|
@@ -257,12 +258,19 @@ var ConnectionManager = class {
|
|
|
257
258
|
this.defaultReconnectOnError = (err) => err.message.includes("READONLY");
|
|
258
259
|
__privateSet(this, _redisOptionsResolved, this.buildRedisOptions(options));
|
|
259
260
|
}
|
|
260
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* Opens the main client and waits for readiness unless `lazyConnect`.
|
|
263
|
+
*
|
|
264
|
+
* Adopts a client {@link getClient} already opened rather than replacing it:
|
|
265
|
+
* NestJS orders `onModuleInit` by module depth, and `app.get()` works before
|
|
266
|
+
* `app.init()`, so a consumer can reach the cache first. Overwriting here
|
|
267
|
+
* would strand that socket with no reference left to close it — teardown
|
|
268
|
+
* quits only the current client.
|
|
269
|
+
*/
|
|
261
270
|
async onModuleInit() {
|
|
262
|
-
|
|
263
|
-
this.registerListeners(__privateGet(this, _client), "main");
|
|
271
|
+
const client = this.ensureClient();
|
|
264
272
|
if (!this.options.connection?.lazyConnect) {
|
|
265
|
-
await this.waitUntilReady(
|
|
273
|
+
await this.waitUntilReady(client);
|
|
266
274
|
}
|
|
267
275
|
}
|
|
268
276
|
/**
|
|
@@ -272,11 +280,7 @@ var ConnectionManager = class {
|
|
|
272
280
|
* @returns The shared main client.
|
|
273
281
|
*/
|
|
274
282
|
getClient() {
|
|
275
|
-
|
|
276
|
-
__privateSet(this, _client, this.createClient());
|
|
277
|
-
this.registerListeners(__privateGet(this, _client), "main");
|
|
278
|
-
}
|
|
279
|
-
return __privateGet(this, _client);
|
|
283
|
+
return this.ensureClient();
|
|
280
284
|
}
|
|
281
285
|
/**
|
|
282
286
|
* Creates a brand-new dedicated connection for subscriber mode (a subscriber
|
|
@@ -334,6 +338,20 @@ var ConnectionManager = class {
|
|
|
334
338
|
}
|
|
335
339
|
}
|
|
336
340
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
341
|
+
/**
|
|
342
|
+
* Returns the main client, opening and wiring it on the first call. The only
|
|
343
|
+
* place a live client is assigned, so no caller can replace one; teardown
|
|
344
|
+
* only clears the field back to `null`.
|
|
345
|
+
*
|
|
346
|
+
* @returns The shared main client.
|
|
347
|
+
*/
|
|
348
|
+
ensureClient() {
|
|
349
|
+
if (!__privateGet(this, _client)) {
|
|
350
|
+
__privateSet(this, _client, this.createClient());
|
|
351
|
+
this.registerListeners(__privateGet(this, _client), "main");
|
|
352
|
+
}
|
|
353
|
+
return __privateGet(this, _client);
|
|
354
|
+
}
|
|
337
355
|
/**
|
|
338
356
|
* Instantiates the client matching the configured mode.
|
|
339
357
|
*
|
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.6",
|
|
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",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "pnpm clean && tsup",
|
|
58
58
|
"check:exports": "attw --pack .",
|
|
59
|
+
"check:mutants": "node scripts/check-mutation-directives.mjs",
|
|
59
60
|
"check:published": "node scripts/check-published-surface.mjs",
|
|
60
61
|
"clean": "rm -rf dist coverage",
|
|
61
62
|
"lint": "eslint src scripts",
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"mutation:dry-run": "stryker run --dryRunOnly",
|
|
65
66
|
"mutation:incremental": "stryker run --incremental",
|
|
66
67
|
"prepare": "husky",
|
|
67
|
-
"prepublishOnly": "pnpm clean && pnpm typecheck && pnpm test:types && pnpm lint && pnpm test:cov:all && pnpm build && pnpm check:published",
|
|
68
|
+
"prepublishOnly": "pnpm clean && pnpm typecheck && pnpm test:types && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm check:published",
|
|
68
69
|
"release": "npm publish --provenance --access public",
|
|
69
70
|
"size": "node scripts/check-size.mjs",
|
|
70
71
|
"test": "jest",
|
|
@@ -139,26 +140,12 @@
|
|
|
139
140
|
"pubsub",
|
|
140
141
|
"lua"
|
|
141
142
|
],
|
|
142
|
-
"packageManager": "pnpm@
|
|
143
|
+
"packageManager": "pnpm@11.20.0",
|
|
143
144
|
"engines": {
|
|
144
145
|
"node": ">=24.0.0"
|
|
145
146
|
},
|
|
146
147
|
"publishConfig": {
|
|
147
148
|
"access": "public",
|
|
148
149
|
"registry": "https://registry.npmjs.org/"
|
|
149
|
-
},
|
|
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
|
-
}
|
|
163
150
|
}
|
|
164
151
|
}
|