@bymax-one/nest-cache 1.2.3 → 1.2.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 +88 -1
- package/README.md +77 -24
- package/dist/admin/index.d.cts +37 -0
- package/dist/admin/index.d.ts +37 -0
- package/dist/server/index.cjs +44 -7
- package/dist/server/index.d.cts +18 -5
- package/dist/server/index.d.ts +18 -5
- package/dist/server/index.mjs +44 -7
- package/dist/shared/index.cjs +2 -1
- package/dist/shared/index.d.cts +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.mjs +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,91 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.5] - 2026-10-31
|
|
10
|
+
|
|
11
|
+
A documentation and test release. Nothing you execute changes: `dist/` differs from 1.2.4 only in one comment, the
|
|
12
|
+
JSDoc of `ConnectionManager.createSubscriberClient`, which no longer calls cluster Pub/Sub experimental.
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
- The README states that Pub/Sub works in cluster mode: channel and pattern subscribers both receive the
|
|
17
|
+
message. The same sentence in the `createSubscriberClient` JSDoc described it as an experimental
|
|
18
|
+
passthrough.
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
|
|
22
|
+
- The cluster end-to-end suite runs three masters on `redis:8-alpine`, formed with `redis-cli --cluster
|
|
23
|
+
create`, and measures what the cluster guards claim: `keys`, `getClient`, `scan`, `flushNamespace` and the
|
|
24
|
+
administration calls that read keys throw `UNSUPPORTED_IN_CLUSTER`, a rejected `ttlSeconds` writes nothing,
|
|
25
|
+
and Pub/Sub delivers to channel and pattern subscribers. Without the `keys` guard a real cluster returned
|
|
26
|
+
2 of 6 seeded keys.
|
|
27
|
+
|
|
28
|
+
## [1.2.4] - 2026-10-31
|
|
29
|
+
|
|
30
|
+
Two calls now refuse input they could not honor, one error code is added, and the documentation catches up with the code.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`CACHE_ERROR_CODES.INVALID_TTL` (`cache.invalid_ttl`, HTTP 400)**, thrown for a TTL the library refuses. See Changed.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **`CacheService.keys()` is refused in cluster mode.** `KEYS` carries no key, so the cluster client sends
|
|
39
|
+
it to one sampled node and the reply listed only that node's share of the keyspace, with no sign that it
|
|
40
|
+
was partial. It now throws `UNSUPPORTED_IN_CLUSTER` with `details.operation: 'keys'`, like `scan`,
|
|
41
|
+
`getClient` and `flushNamespace`. **Apply to a consumer.** A cluster deployment that called `keys()`
|
|
42
|
+
must iterate with `scan` on each node through its own client, or avoid listing keys; standalone and
|
|
43
|
+
sentinel are unchanged.
|
|
44
|
+
- **`ttlSeconds` is validated, with a new error code, `cache.invalid_ttl` (HTTP 400).** `set`, `setRaw`
|
|
45
|
+
and `setNx` need a safe integer of at least 1, or none for no expiry; `expire` needs a safe integer,
|
|
46
|
+
and zero or a negative one still deletes the key, as in Redis. Anything else throws `CacheException`
|
|
47
|
+
with `INVALID_TTL` and `details.reason` set to `not_a_safe_integer` or `below_minimum`, before a command
|
|
48
|
+
is sent. **Apply to a consumer.** What now throws `INVALID_TTL`: `set`, `setRaw`, `setNx` with `0`, a negative number, a fraction, `NaN`, `Infinity` or an integer above 2^53; `expire` with a fraction, `NaN`, `Infinity` or an integer above 2^53. Redis documents `SET ... EX` as a positive integer and `EXPIRE` as an
|
|
49
|
+
integer, and answers anything else with a raw reply error that carries no `cache.*` code, so the values
|
|
50
|
+
refused here already failed. The one exception is an integer above 2^53 seconds, a TTL of about 285
|
|
51
|
+
million years, which this refuses and Redis may accept. Code that caught the raw error by its message
|
|
52
|
+
should switch to `error.code === 'cache.invalid_ttl'`.
|
|
53
|
+
|
|
54
|
+
### Deprecated
|
|
55
|
+
|
|
56
|
+
- **Implementation helpers exported from `@bymax-one/nest-cache/admin`** are marked `@deprecated` and will
|
|
57
|
+
leave the public surface in 2.0.0: `validateScopes`, `findScope`, `isKeyInScope`, `readScopePattern`,
|
|
58
|
+
`resolveAdminOptions`, `ResolvedAdminOptions`, `parseInfoFields`, `readRedisStats`, `readKeyTtl`,
|
|
59
|
+
`readKeyType`, `readSizeBytes`, `IRedisReader`, `IRedisReaderSource`, `IRedisPipeline`,
|
|
60
|
+
`PipelineReply` and `ICacheProbe`. They keep working in 1.x. Do not depend on them.
|
|
61
|
+
|
|
62
|
+
### Documentation
|
|
63
|
+
|
|
64
|
+
- The README error-code table described `cache.shutdown_timeout` as thrown when `quit()` exceeds
|
|
65
|
+
`shutdownTimeoutMs`. It never reaches the caller: the library forces `disconnect()` and emits an
|
|
66
|
+
`error` event with `reason: 'forced_disconnect'`. The row now says so.
|
|
67
|
+
- The README states that `app.enableShutdownHooks()` is required for the graceful shutdown to run, and
|
|
68
|
+
carries a warning that `/admin` has no authentication of its own. It documents the `/admin` options
|
|
69
|
+
and their defaults and the exported types, constants and tokens it left out, lists `keys` among the
|
|
70
|
+
cluster-refused calls, and marks the three codes the library never throws as reserved.
|
|
71
|
+
- The sentinel example passed `process.env.REDIS_PASSWORD` as `password`, which does not compile
|
|
72
|
+
for a consumer that enables `exactOptionalPropertyTypes`. It now falls back to an empty string,
|
|
73
|
+
which ioredis treats like no password.
|
|
74
|
+
|
|
75
|
+
### Tests
|
|
76
|
+
|
|
77
|
+
- `src/readme.spec.ts` parses the README's option-defaults table and error-code table and asserts them
|
|
78
|
+
against the resolved defaults, the minimum timeouts, `CACHE_ERROR_CODES` and the status each
|
|
79
|
+
`CacheException` carries, so a README that drifts from the code fails CI.
|
|
80
|
+
|
|
81
|
+
### Internal
|
|
82
|
+
|
|
83
|
+
- `check:published` compiles every README snippet, not only the ones that import the package, through the
|
|
84
|
+
TypeScript API so a syntax error in one snippet no longer hides the type errors in the others. A
|
|
85
|
+
snippet that omits its import is given the package's public names, and the names a snippet takes from
|
|
86
|
+
the surrounding prose are typed in `scripts/readme-prelude.d.ts`, so a wrong method, option or field is
|
|
87
|
+
an error instead of a call on `any`.
|
|
88
|
+
- The same gate no longer excuses a wrong method on an imported type or on a union-typed value as a
|
|
89
|
+
class the README abbreviates, and a relative import in a snippet that resolves to nothing fails it.
|
|
90
|
+
- The same gate now checks the typed names the snippets rely on, so a name in that declaration file that
|
|
91
|
+
stops resolving is an error instead of silently becoming `any`, and it tolerates a parse error only
|
|
92
|
+
where the parse tree shows a `...` elision, so a genuine syntax error in a snippet fails it.
|
|
93
|
+
|
|
9
94
|
## [1.2.3] - 2026-10-30
|
|
10
95
|
|
|
11
96
|
### Changed
|
|
@@ -394,7 +479,9 @@ type or export moved.
|
|
|
394
479
|
- Published with npm OIDC provenance — no long-lived tokens
|
|
395
480
|
- Zero direct runtime dependencies (`dependencies: {}`) — `ioredis` and NestJS via peer deps
|
|
396
481
|
|
|
397
|
-
[Unreleased]: https://github.com/bymaxone/nest-cache/compare/v1.2.
|
|
482
|
+
[Unreleased]: https://github.com/bymaxone/nest-cache/compare/v1.2.5...HEAD
|
|
483
|
+
[1.2.5]: https://github.com/bymaxone/nest-cache/compare/v1.2.4...v1.2.5
|
|
484
|
+
[1.2.4]: https://github.com/bymaxone/nest-cache/compare/v1.2.3...v1.2.4
|
|
398
485
|
[1.2.3]: https://github.com/bymaxone/nest-cache/compare/v1.2.2...v1.2.3
|
|
399
486
|
[1.2.2]: https://github.com/bymaxone/nest-cache/compare/v1.2.1...v1.2.2
|
|
400
487
|
[1.2.1]: https://github.com/bymaxone/nest-cache/compare/v1.2.0...v1.2.1
|
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ BymaxCacheModule.forRoot({
|
|
|
171
171
|
{ host: 'sentinel2.example.com', port: 26379 }
|
|
172
172
|
],
|
|
173
173
|
name: 'mymaster',
|
|
174
|
-
password: process.env.REDIS_PASSWORD
|
|
174
|
+
password: process.env.REDIS_PASSWORD ?? ''
|
|
175
175
|
},
|
|
176
176
|
namespace: 'app'
|
|
177
177
|
})
|
|
@@ -219,6 +219,15 @@ import { BymaxCacheModule } from '@bymax-one/nest-cache'
|
|
|
219
219
|
export class AppModule {}
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
> [!IMPORTANT]
|
|
223
|
+
> **Call `app.enableShutdownHooks()` in `main.ts`.** Nest runs `OnModuleDestroy` on `SIGTERM` / `SIGINT` only when shutdown hooks are enabled. Without it the process exits on a signal without quitting the Redis connection gracefully, and `shutdownTimeoutMs` never comes into play.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const app = await NestFactory.create(AppModule)
|
|
227
|
+
app.enableShutdownHooks()
|
|
228
|
+
await app.listen(3000)
|
|
229
|
+
```
|
|
230
|
+
|
|
222
231
|
### 3. Inject `CacheService`
|
|
223
232
|
|
|
224
233
|
The module is global by default, so no re-import is needed in feature modules:
|
|
@@ -315,6 +324,9 @@ Channels are namespaced like keys. The subscriber connection is created lazily o
|
|
|
315
324
|
subscription. Redis Pub/Sub is fire-and-forget — messages published while a subscriber is offline
|
|
316
325
|
are not replayed.
|
|
317
326
|
|
|
327
|
+
Pub/Sub also works in cluster mode: channel and pattern subscribers both receive the message, which
|
|
328
|
+
the end-to-end suite verifies against a three-master cluster.
|
|
329
|
+
|
|
318
330
|
---
|
|
319
331
|
|
|
320
332
|
## 📜 Lua Scripts
|
|
@@ -420,6 +432,9 @@ export class HealthController {
|
|
|
420
432
|
|
|
421
433
|
A read-only surface for an operator-facing console: is the cache answering, what is it doing, what is in it.
|
|
422
434
|
|
|
435
|
+
> [!WARNING]
|
|
436
|
+
> **`/admin` has no authentication or authorization of its own.** The library registers services, not routes: `CacheStatusService` and `CacheAdminService` list keys, report `INFO` statistics and, for a readable scope, return stored values to whoever calls them. Put every route you build on them behind your own guard (an authenticated operator role), and never on a public route.
|
|
437
|
+
|
|
423
438
|
```ts
|
|
424
439
|
import { BymaxCacheModule } from '@bymax-one/nest-cache'
|
|
425
440
|
import { BymaxCacheAdminModule } from '@bymax-one/nest-cache/admin'
|
|
@@ -471,6 +486,21 @@ await this.admin.listKeys('cache', { includeSize: true })
|
|
|
471
486
|
await this.admin.revealValue('auth', 'auth:sess:1') // { status: 'withheld', origin }
|
|
472
487
|
```
|
|
473
488
|
|
|
489
|
+
### Options
|
|
490
|
+
|
|
491
|
+
| Option | Type | Default | Description |
|
|
492
|
+
| ------------------- | ----------------------- | ------- | ------------------------------------------------------------------------ |
|
|
493
|
+
| `scopes` | `readonly CacheScope[]` | — | The keyspaces this deployment exposes; required |
|
|
494
|
+
| `degradedAboveMs` | `number` | `250` | Probe latency above which `health()` reports `degraded` |
|
|
495
|
+
| `probeTimeoutMs` | `number` | `2000` | How long `health()` waits for `PING` before reporting `down` |
|
|
496
|
+
| `scanLimit` | `number` | `500` | Entries one `listKeys` page may carry (a page can slightly exceed it) |
|
|
497
|
+
| `commandBatchLimit` | `number` | `100` | Commands in one pipeline batch |
|
|
498
|
+
| `revealLimit` | `number` | `200` | Members or fields `revealValue` returns before `isComplete` is `false` |
|
|
499
|
+
| `revealStringLimit` | `number` | `4096` | Characters of a string value `revealValue` returns |
|
|
500
|
+
| `isGlobal` | `boolean` | `false` | Register the admin module globally. The cache module's default is `true` |
|
|
501
|
+
|
|
502
|
+
Every threshold must be a positive integer; one that is not fails the boot with `cache.invalid_scope`. `CacheAdminService` also has `listScopes()` and `describeKey(scope, key)`, which returns a key's type, TTL and whether its value is readable, without the value.
|
|
503
|
+
|
|
474
504
|
### What the library owns, and what you own
|
|
475
505
|
|
|
476
506
|
The library owns the **mechanism**: validating the allowlist, scanning against it, describing keys, withholding values. The application owns **which keyspaces exist**, the `origin` prose that explains them, the routes, and the guards. A cache library cannot know that another library writes at Redis root through `getClient()`, and making it depend on that library to find out would invert two packages to save an application from stating one thing about itself.
|
|
@@ -646,7 +676,7 @@ Scripts are declared up front — through `options.scripts` or `ScriptManagerSer
|
|
|
646
676
|
|
|
647
677
|
### Cluster mode refuses commands it cannot honor safely
|
|
648
678
|
|
|
649
|
-
`scan()`, `flushNamespace()`, and `getClient()` throw `UNSUPPORTED_IN_CLUSTER` under `mode: 'cluster'` rather than silently operating on one node. A partial flush that reports success is worse than an error.
|
|
679
|
+
`scan()`, `keys()`, `flushNamespace()`, and `getClient()` throw `UNSUPPORTED_IN_CLUSTER` under `mode: 'cluster'` rather than silently operating on one node. `KEYS` carries no key, so the cluster client sends it to a single node and the reply would list only that node's share of the keyspace. A partial flush that reports success is worse than an error.
|
|
650
680
|
|
|
651
681
|
### Security Checklist
|
|
652
682
|
|
|
@@ -680,7 +710,7 @@ When integrating `@bymax-one/nest-cache` in production, verify each of the follo
|
|
|
680
710
|
| Error Payloads | Connection URLs omitted (may embed credentials); a stored value is never echoed (a fixed text and its length only) |
|
|
681
711
|
| Destructive Ops | `flushNamespace()` blocked under `NODE_ENV=production` unless `allowFlushInProduction`; `SCAN` + `UNLINK`, namespace-scoped |
|
|
682
712
|
| Lua Execution | Scripts registered by name; call sites pass keys/args only — request input never reaches a script body |
|
|
683
|
-
| Cluster Safety | `scan` / `flushNamespace` / `getClient` throw `UNSUPPORTED_IN_CLUSTER` instead of acting on a single node
|
|
713
|
+
| Cluster Safety | `scan` / `keys` / `flushNamespace` / `getClient` throw `UNSUPPORTED_IN_CLUSTER` instead of acting on a single node |
|
|
684
714
|
| Transport | `rediss://` sets TLS on the client; explicit `connection.tls` supported for custom CA / mTLS |
|
|
685
715
|
| Connection Resilience | Bounded `maxRetriesPerRequest`, `READONLY`-failover reconnect, graceful `quit()` with a shutdown timeout |
|
|
686
716
|
| Supply Chain | `"dependencies": {}` — no transitive runtime packages of the library's own choosing; published with npm provenance |
|
|
@@ -746,6 +776,8 @@ pnpm lint # ESLint
|
|
|
746
776
|
| Namespace | `flushNamespace` (prod-guarded) |
|
|
747
777
|
| Health | `isHealthy` · `ping` · `info` |
|
|
748
778
|
|
|
779
|
+
**TTLs.** `ttlSeconds` on `set`, `setRaw` and `setNx` is a safe integer of at least `1`; omit it for no expiry. `expire` takes a safe integer, and as in Redis a zero or negative one deletes the key. Anything else throws `INVALID_TTL` before a command is sent.
|
|
780
|
+
|
|
749
781
|
### `PubSubService`
|
|
750
782
|
|
|
751
783
|
`publish<T>(channel, message)` · `subscribe<T>(channel, handler)` · `psubscribe<T>(pattern, handler)`
|
|
@@ -758,33 +790,54 @@ pnpm lint # ESLint
|
|
|
758
790
|
|
|
759
791
|
`CacheException` (extends `HttpException`) + `CACHE_ERROR_CODES` (namespaced `cache.*`).
|
|
760
792
|
|
|
793
|
+
### Types, constants and tokens
|
|
794
|
+
|
|
795
|
+
From `@bymax-one/nest-cache`:
|
|
796
|
+
|
|
797
|
+
| Export | What it is |
|
|
798
|
+
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
799
|
+
| `BymaxCacheModuleOptions`, `BymaxCacheModuleAsyncOptions` | The options of `forRoot()` and `forRootAsync()`; the Configuration table lists their fields |
|
|
800
|
+
| `BymaxCacheStandaloneConnection`, `BymaxCacheSentinelConnection`, `BymaxCacheClusterConnection` | The `connection`, `sentinel` and `cluster` blocks |
|
|
801
|
+
| `RedisOptions`, `ClusterOptions`, `ClusterNode`, `SentinelAddress` | The ioredis option types the blocks above refer to, re-exported |
|
|
802
|
+
| `ResolvedOptions` | The shape injected under `BYMAX_CACHE_OPTIONS`, with every default applied |
|
|
803
|
+
| `IPubSubHandler`, `IPubSubPatternHandler`, `Unsubscribe` | The handler signatures and the function `subscribe` / `psubscribe` return |
|
|
804
|
+
| `CACHE_ERROR_MESSAGES` | The default message of every code, keyed by code |
|
|
805
|
+
| `DEFAULT_NAMESPACE`, `DEFAULT_KEY_SEPARATOR`, `DEFAULT_REDIS_PORT` | `'app'`, `':'` and `6379` |
|
|
806
|
+
|
|
807
|
+
From `@bymax-one/nest-cache/shared` (also re-exported from the main entry): `CACHE_EVENT_NAMES`, `CacheEventName`, `CacheConnectionStatus`, `CacheErrorCode`, `CacheNamespace`, `CacheKeyPrefix` and `SerializableValue`.
|
|
808
|
+
|
|
809
|
+
From `@bymax-one/nest-cache/admin`: `BymaxCacheAdminModuleOptions` and `BymaxCacheAdminModuleAsyncOptions` (the Options table above), `CacheScope`, `ListKeysOptions`, the result types `CacheHealth`, `CacheConfig`, `CacheMode`, `RedisStats`, `KeyspacePage`, `KeyEntry`, `KeyDetail`, `KeyTtl`, `RevealResult` and `RevealedValue`, the token `BYMAX_CACHE_ADMIN_OPTIONS`, and the defaults `DEFAULT_DEGRADED_ABOVE_MS`, `DEFAULT_PROBE_TIMEOUT_MS`, `DEFAULT_SCAN_LIMIT`, `DEFAULT_COMMAND_BATCH_LIMIT`, `DEFAULT_REVEAL_LIMIT` and `DEFAULT_REVEAL_STRING_LIMIT`.
|
|
810
|
+
|
|
811
|
+
The other names `/admin` exports (the scope and `INFO` helpers such as `validateScopes` and `readRedisStats`, `resolveAdminOptions`, and the reader contracts) are implementation helpers, marked `@deprecated`: they will leave the public surface in 2.0.0, so do not depend on them.
|
|
812
|
+
|
|
761
813
|
---
|
|
762
814
|
|
|
763
815
|
## 🪪 Default Error Codes
|
|
764
816
|
|
|
765
817
|
All errors are instances of `CacheException` and carry a stable `code` string from `CACHE_ERROR_CODES`:
|
|
766
818
|
|
|
767
|
-
| Code | HTTP | When thrown
|
|
768
|
-
| ------------------------------------ | ---- |
|
|
769
|
-
| `cache.connection_failed` | 500 | Cannot connect after retries; also bootstrap validation of `connection` and timeouts
|
|
770
|
-
| `cache.command_timeout` | 504 | Reserved
|
|
771
|
-
| `cache.connection_lost` | 503 | Reserved
|
|
772
|
-
| `cache.deserialization_failed` | 500 | Malformed payload in `get<T>`
|
|
773
|
-
| `cache.serialization_failed` | 500 | Unserializable value in `set<T>`
|
|
774
|
-
| `cache.invalid_key` | 400 | Empty prefix or id (`build`), or empty key (`applyNamespace`)
|
|
775
|
-
| `cache.
|
|
776
|
-
| `cache.
|
|
777
|
-
| `cache.
|
|
778
|
-
| `cache.
|
|
779
|
-
| `cache.
|
|
780
|
-
| `cache.
|
|
781
|
-
| `cache.
|
|
782
|
-
| `cache.
|
|
783
|
-
| `cache.
|
|
784
|
-
| `cache.
|
|
785
|
-
| `cache.
|
|
786
|
-
| `cache.
|
|
787
|
-
| `cache.
|
|
819
|
+
| Code | HTTP | When thrown |
|
|
820
|
+
| ------------------------------------ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
821
|
+
| `cache.connection_failed` | 500 | Cannot connect after retries; also bootstrap validation of `connection` and timeouts |
|
|
822
|
+
| `cache.command_timeout` | 504 | Reserved, never thrown today |
|
|
823
|
+
| `cache.connection_lost` | 503 | Reserved, never thrown today |
|
|
824
|
+
| `cache.deserialization_failed` | 500 | Malformed payload in `get<T>` |
|
|
825
|
+
| `cache.serialization_failed` | 500 | Unserializable value in `set<T>` |
|
|
826
|
+
| `cache.invalid_key` | 400 | Empty prefix or id (`build`), or empty key (`applyNamespace`) |
|
|
827
|
+
| `cache.invalid_ttl` | 400 | `ttlSeconds` is not a safe integer, or is below 1 for `set` / `setRaw` / `setNx` |
|
|
828
|
+
| `cache.invalid_namespace` | 500 | Empty or whitespace-only namespace, empty `keySeparator`, namespace containing the separator or a glob metacharacter (`*`, `?`, `[`, `\`) |
|
|
829
|
+
| `cache.script_not_registered` | 500 | `eval(name)` before `register(name)` |
|
|
830
|
+
| `cache.script_execution_failed` | 500 | Lua runtime error or NOSCRIPT retry failure |
|
|
831
|
+
| `cache.script_registry_missing` | 500 | `eval` called when no `ScriptManagerService` is wired |
|
|
832
|
+
| `cache.flush_disabled_in_production` | 403 | `flushNamespace()` in prod without `allowFlushInProduction` |
|
|
833
|
+
| `cache.unsupported_in_cluster` | 500 | `scan`, `keys`, `flushNamespace`, or `getClient` called in cluster mode |
|
|
834
|
+
| `cache.cluster_misconfigured` | 500 | `mode: 'cluster'` without `cluster.nodes` |
|
|
835
|
+
| `cache.sentinel_misconfigured` | 500 | `mode: 'sentinel'` without `sentinel.sentinels`/`name` |
|
|
836
|
+
| `cache.shutdown_timeout` | 500 | Internal, never thrown to the caller: `quit()` exceeded `shutdownTimeoutMs`, so the library forces `disconnect()` and emits `error` with `reason: 'forced_disconnect'` |
|
|
837
|
+
| `cache.invalid_scope` | 500 | `/admin` scope allowlist invalid at wiring |
|
|
838
|
+
| `cache.scope_not_found` | 500 | `/admin` call named an undeclared scope id |
|
|
839
|
+
| `cache.scope_not_readable` | 500 | Reserved, never thrown today (unreadable scopes return `withheld`) |
|
|
840
|
+
| `cache.key_not_in_scope` | 500 | `/admin` call named a key outside the named scope |
|
|
788
841
|
|
|
789
842
|
Full catalog and HTTP status mapping: [`docs/technical_specification.md §12`](./docs/technical_specification.md).
|
|
790
843
|
|
package/dist/admin/index.d.cts
CHANGED
|
@@ -132,6 +132,9 @@ declare class BymaxCacheAdminModule {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/** Administration options after defaults are merged and validated. */
|
|
135
|
+
/**
|
|
136
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
137
|
+
*/
|
|
135
138
|
interface ResolvedAdminOptions {
|
|
136
139
|
readonly scopes: readonly CacheScope[];
|
|
137
140
|
readonly degradedAboveMs: number;
|
|
@@ -150,6 +153,8 @@ interface ResolvedAdminOptions {
|
|
|
150
153
|
* @returns The frozen resolved options.
|
|
151
154
|
* @throws {CacheException} `INVALID_SCOPE` when a scope is malformed or a
|
|
152
155
|
* threshold is not a positive integer.
|
|
156
|
+
*
|
|
157
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
153
158
|
*/
|
|
154
159
|
declare function resolveAdminOptions(options: BymaxCacheAdminModuleOptions): Readonly<ResolvedAdminOptions>;
|
|
155
160
|
|
|
@@ -370,6 +375,8 @@ interface RedisStats {
|
|
|
370
375
|
* the concrete `CacheService`, while a test supplies a plain object with two
|
|
371
376
|
* methods and no cast. A cast would be a blocking finding under this
|
|
372
377
|
* repository's suppression policy, so the narrow type is not a convenience.
|
|
378
|
+
*
|
|
379
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
373
380
|
*/
|
|
374
381
|
type ICacheProbe = Pick<CacheService, 'ping' | 'info'>;
|
|
375
382
|
/** Reports whether the cache is answering, what it is doing, and how it is wired. */
|
|
@@ -432,8 +439,14 @@ declare class CacheStatusService {
|
|
|
432
439
|
* this subpath.
|
|
433
440
|
*/
|
|
434
441
|
/** One entry of a pipeline's result: an error, or a value. */
|
|
442
|
+
/**
|
|
443
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
444
|
+
*/
|
|
435
445
|
type PipelineReply = [Error | null, unknown];
|
|
436
446
|
/** A pipeline being composed, chainable the way ioredis's commander is. */
|
|
447
|
+
/**
|
|
448
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
449
|
+
*/
|
|
437
450
|
interface IRedisPipeline {
|
|
438
451
|
/** Queues `TYPE`. */
|
|
439
452
|
type(key: string): IRedisPipeline;
|
|
@@ -445,6 +458,9 @@ interface IRedisPipeline {
|
|
|
445
458
|
exec(): Promise<PipelineReply[] | null>;
|
|
446
459
|
}
|
|
447
460
|
/** The subset of the Redis client this surface reads through. */
|
|
461
|
+
/**
|
|
462
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
463
|
+
*/
|
|
448
464
|
interface IRedisReader {
|
|
449
465
|
/** Cursor-based key iteration. */
|
|
450
466
|
scan(cursor: string | number, matchToken: 'MATCH', pattern: string, countToken: 'COUNT', count: number | string): Promise<[string, string[]]>;
|
|
@@ -478,6 +494,9 @@ interface IRedisReader {
|
|
|
478
494
|
call(command: string, ...args: (string | number)[]): Promise<unknown>;
|
|
479
495
|
}
|
|
480
496
|
/** How the administration surface obtains its reader. */
|
|
497
|
+
/**
|
|
498
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
499
|
+
*/
|
|
481
500
|
interface IRedisReaderSource {
|
|
482
501
|
/**
|
|
483
502
|
* Returns the client.
|
|
@@ -770,6 +789,8 @@ declare class CacheAdminService {
|
|
|
770
789
|
*
|
|
771
790
|
* @param pattern - A pattern that already satisfies {@link SCOPE_PATTERN_SHAPE}.
|
|
772
791
|
* @returns The literal prefix, and whether a trailing `*` allows a suffix.
|
|
792
|
+
*
|
|
793
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
773
794
|
*/
|
|
774
795
|
declare function readScopePattern(pattern: string): {
|
|
775
796
|
readonly prefix: string;
|
|
@@ -784,6 +805,8 @@ declare function readScopePattern(pattern: string): {
|
|
|
784
805
|
* @param pattern - The scope's validated match pattern.
|
|
785
806
|
* @param key - The key a caller named.
|
|
786
807
|
* @returns `true` when the key belongs to the scope.
|
|
808
|
+
*
|
|
809
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
787
810
|
*/
|
|
788
811
|
declare function isKeyInScope(pattern: string, key: string): boolean;
|
|
789
812
|
/**
|
|
@@ -799,6 +822,8 @@ declare function isKeyInScope(pattern: string, key: string): boolean;
|
|
|
799
822
|
* @returns A frozen list of frozen scopes, in declaration order.
|
|
800
823
|
* @throws {CacheException} `INVALID_SCOPE` when the list is empty, a required
|
|
801
824
|
* field is empty, an id repeats, or a pattern has no literal prefix.
|
|
825
|
+
*
|
|
826
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
802
827
|
*/
|
|
803
828
|
declare function validateScopes(scopes: readonly CacheScope[]): readonly CacheScope[];
|
|
804
829
|
/**
|
|
@@ -811,6 +836,8 @@ declare function validateScopes(scopes: readonly CacheScope[]): readonly CacheSc
|
|
|
811
836
|
* @param id - The identifier from the request.
|
|
812
837
|
* @returns The scope.
|
|
813
838
|
* @throws {CacheException} `SCOPE_NOT_FOUND` when no declaration has that id.
|
|
839
|
+
*
|
|
840
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
814
841
|
*/
|
|
815
842
|
declare function findScope(scopes: readonly CacheScope[], id: string): CacheScope;
|
|
816
843
|
|
|
@@ -842,6 +869,8 @@ declare function findScope(scopes: readonly CacheScope[], id: string): CacheScop
|
|
|
842
869
|
*
|
|
843
870
|
* @param text - The raw `INFO` output.
|
|
844
871
|
* @returns Field name to raw text value.
|
|
872
|
+
*
|
|
873
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
845
874
|
*/
|
|
846
875
|
declare function parseInfoFields(text: string): ReadonlyMap<string, string>;
|
|
847
876
|
/**
|
|
@@ -850,6 +879,8 @@ declare function parseInfoFields(text: string): ReadonlyMap<string, string>;
|
|
|
850
879
|
* @param infoText - The raw `INFO` output.
|
|
851
880
|
* @param readAt - When the reading was taken, by the reading host's clock.
|
|
852
881
|
* @returns The parsed statistics.
|
|
882
|
+
*
|
|
883
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
853
884
|
*/
|
|
854
885
|
declare function readRedisStats(infoText: string, readAt: Date): RedisStats;
|
|
855
886
|
|
|
@@ -867,6 +898,8 @@ declare function readRedisStats(infoText: string, readAt: Date): RedisStats;
|
|
|
867
898
|
*
|
|
868
899
|
* @param reply - Whatever the server answered.
|
|
869
900
|
* @returns The recognised type, or `other`.
|
|
901
|
+
*
|
|
902
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
870
903
|
*/
|
|
871
904
|
declare function readKeyType(reply: unknown): RedisKeyType;
|
|
872
905
|
/**
|
|
@@ -878,6 +911,8 @@ declare function readKeyType(reply: unknown): RedisKeyType;
|
|
|
878
911
|
*
|
|
879
912
|
* @param reply - Whatever the server answered.
|
|
880
913
|
* @returns Whether the key expires, is persistent, or is gone.
|
|
914
|
+
*
|
|
915
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
881
916
|
*/
|
|
882
917
|
declare function readKeyTtl(reply: unknown): KeyTtl;
|
|
883
918
|
/**
|
|
@@ -889,6 +924,8 @@ declare function readKeyTtl(reply: unknown): KeyTtl;
|
|
|
889
924
|
*
|
|
890
925
|
* @param reply - Whatever the server answered.
|
|
891
926
|
* @returns The size in bytes, or `null` when no size was reported.
|
|
927
|
+
*
|
|
928
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
892
929
|
*/
|
|
893
930
|
declare function readSizeBytes(reply: unknown): number | null;
|
|
894
931
|
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -132,6 +132,9 @@ declare class BymaxCacheAdminModule {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/** Administration options after defaults are merged and validated. */
|
|
135
|
+
/**
|
|
136
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
137
|
+
*/
|
|
135
138
|
interface ResolvedAdminOptions {
|
|
136
139
|
readonly scopes: readonly CacheScope[];
|
|
137
140
|
readonly degradedAboveMs: number;
|
|
@@ -150,6 +153,8 @@ interface ResolvedAdminOptions {
|
|
|
150
153
|
* @returns The frozen resolved options.
|
|
151
154
|
* @throws {CacheException} `INVALID_SCOPE` when a scope is malformed or a
|
|
152
155
|
* threshold is not a positive integer.
|
|
156
|
+
*
|
|
157
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
153
158
|
*/
|
|
154
159
|
declare function resolveAdminOptions(options: BymaxCacheAdminModuleOptions): Readonly<ResolvedAdminOptions>;
|
|
155
160
|
|
|
@@ -370,6 +375,8 @@ interface RedisStats {
|
|
|
370
375
|
* the concrete `CacheService`, while a test supplies a plain object with two
|
|
371
376
|
* methods and no cast. A cast would be a blocking finding under this
|
|
372
377
|
* repository's suppression policy, so the narrow type is not a convenience.
|
|
378
|
+
*
|
|
379
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
373
380
|
*/
|
|
374
381
|
type ICacheProbe = Pick<CacheService, 'ping' | 'info'>;
|
|
375
382
|
/** Reports whether the cache is answering, what it is doing, and how it is wired. */
|
|
@@ -432,8 +439,14 @@ declare class CacheStatusService {
|
|
|
432
439
|
* this subpath.
|
|
433
440
|
*/
|
|
434
441
|
/** One entry of a pipeline's result: an error, or a value. */
|
|
442
|
+
/**
|
|
443
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
444
|
+
*/
|
|
435
445
|
type PipelineReply = [Error | null, unknown];
|
|
436
446
|
/** A pipeline being composed, chainable the way ioredis's commander is. */
|
|
447
|
+
/**
|
|
448
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
449
|
+
*/
|
|
437
450
|
interface IRedisPipeline {
|
|
438
451
|
/** Queues `TYPE`. */
|
|
439
452
|
type(key: string): IRedisPipeline;
|
|
@@ -445,6 +458,9 @@ interface IRedisPipeline {
|
|
|
445
458
|
exec(): Promise<PipelineReply[] | null>;
|
|
446
459
|
}
|
|
447
460
|
/** The subset of the Redis client this surface reads through. */
|
|
461
|
+
/**
|
|
462
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
463
|
+
*/
|
|
448
464
|
interface IRedisReader {
|
|
449
465
|
/** Cursor-based key iteration. */
|
|
450
466
|
scan(cursor: string | number, matchToken: 'MATCH', pattern: string, countToken: 'COUNT', count: number | string): Promise<[string, string[]]>;
|
|
@@ -478,6 +494,9 @@ interface IRedisReader {
|
|
|
478
494
|
call(command: string, ...args: (string | number)[]): Promise<unknown>;
|
|
479
495
|
}
|
|
480
496
|
/** How the administration surface obtains its reader. */
|
|
497
|
+
/**
|
|
498
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
499
|
+
*/
|
|
481
500
|
interface IRedisReaderSource {
|
|
482
501
|
/**
|
|
483
502
|
* Returns the client.
|
|
@@ -770,6 +789,8 @@ declare class CacheAdminService {
|
|
|
770
789
|
*
|
|
771
790
|
* @param pattern - A pattern that already satisfies {@link SCOPE_PATTERN_SHAPE}.
|
|
772
791
|
* @returns The literal prefix, and whether a trailing `*` allows a suffix.
|
|
792
|
+
*
|
|
793
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
773
794
|
*/
|
|
774
795
|
declare function readScopePattern(pattern: string): {
|
|
775
796
|
readonly prefix: string;
|
|
@@ -784,6 +805,8 @@ declare function readScopePattern(pattern: string): {
|
|
|
784
805
|
* @param pattern - The scope's validated match pattern.
|
|
785
806
|
* @param key - The key a caller named.
|
|
786
807
|
* @returns `true` when the key belongs to the scope.
|
|
808
|
+
*
|
|
809
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
787
810
|
*/
|
|
788
811
|
declare function isKeyInScope(pattern: string, key: string): boolean;
|
|
789
812
|
/**
|
|
@@ -799,6 +822,8 @@ declare function isKeyInScope(pattern: string, key: string): boolean;
|
|
|
799
822
|
* @returns A frozen list of frozen scopes, in declaration order.
|
|
800
823
|
* @throws {CacheException} `INVALID_SCOPE` when the list is empty, a required
|
|
801
824
|
* field is empty, an id repeats, or a pattern has no literal prefix.
|
|
825
|
+
*
|
|
826
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
802
827
|
*/
|
|
803
828
|
declare function validateScopes(scopes: readonly CacheScope[]): readonly CacheScope[];
|
|
804
829
|
/**
|
|
@@ -811,6 +836,8 @@ declare function validateScopes(scopes: readonly CacheScope[]): readonly CacheSc
|
|
|
811
836
|
* @param id - The identifier from the request.
|
|
812
837
|
* @returns The scope.
|
|
813
838
|
* @throws {CacheException} `SCOPE_NOT_FOUND` when no declaration has that id.
|
|
839
|
+
*
|
|
840
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
814
841
|
*/
|
|
815
842
|
declare function findScope(scopes: readonly CacheScope[], id: string): CacheScope;
|
|
816
843
|
|
|
@@ -842,6 +869,8 @@ declare function findScope(scopes: readonly CacheScope[], id: string): CacheScop
|
|
|
842
869
|
*
|
|
843
870
|
* @param text - The raw `INFO` output.
|
|
844
871
|
* @returns Field name to raw text value.
|
|
872
|
+
*
|
|
873
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
845
874
|
*/
|
|
846
875
|
declare function parseInfoFields(text: string): ReadonlyMap<string, string>;
|
|
847
876
|
/**
|
|
@@ -850,6 +879,8 @@ declare function parseInfoFields(text: string): ReadonlyMap<string, string>;
|
|
|
850
879
|
* @param infoText - The raw `INFO` output.
|
|
851
880
|
* @param readAt - When the reading was taken, by the reading host's clock.
|
|
852
881
|
* @returns The parsed statistics.
|
|
882
|
+
*
|
|
883
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
853
884
|
*/
|
|
854
885
|
declare function readRedisStats(infoText: string, readAt: Date): RedisStats;
|
|
855
886
|
|
|
@@ -867,6 +898,8 @@ declare function readRedisStats(infoText: string, readAt: Date): RedisStats;
|
|
|
867
898
|
*
|
|
868
899
|
* @param reply - Whatever the server answered.
|
|
869
900
|
* @returns The recognised type, or `other`.
|
|
901
|
+
*
|
|
902
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
870
903
|
*/
|
|
871
904
|
declare function readKeyType(reply: unknown): RedisKeyType;
|
|
872
905
|
/**
|
|
@@ -878,6 +911,8 @@ declare function readKeyType(reply: unknown): RedisKeyType;
|
|
|
878
911
|
*
|
|
879
912
|
* @param reply - Whatever the server answered.
|
|
880
913
|
* @returns Whether the key expires, is persistent, or is gone.
|
|
914
|
+
*
|
|
915
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
881
916
|
*/
|
|
882
917
|
declare function readKeyTtl(reply: unknown): KeyTtl;
|
|
883
918
|
/**
|
|
@@ -889,6 +924,8 @@ declare function readKeyTtl(reply: unknown): KeyTtl;
|
|
|
889
924
|
*
|
|
890
925
|
* @param reply - Whatever the server answered.
|
|
891
926
|
* @returns The size in bytes, or `null` when no size was reported.
|
|
927
|
+
*
|
|
928
|
+
* @deprecated Implementation helper; will leave the public surface in 2.0.0. Do not depend on it.
|
|
892
929
|
*/
|
|
893
930
|
declare function readSizeBytes(reply: unknown): number | null;
|
|
894
931
|
|
package/dist/server/index.cjs
CHANGED
|
@@ -67,7 +67,8 @@ var CACHE_ERROR_CODES = {
|
|
|
67
67
|
INVALID_SCOPE: "cache.invalid_scope",
|
|
68
68
|
SCOPE_NOT_FOUND: "cache.scope_not_found",
|
|
69
69
|
SCOPE_NOT_READABLE: "cache.scope_not_readable",
|
|
70
|
-
KEY_NOT_IN_SCOPE: "cache.key_not_in_scope"
|
|
70
|
+
KEY_NOT_IN_SCOPE: "cache.key_not_in_scope",
|
|
71
|
+
INVALID_TTL: "cache.invalid_ttl"
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
// src/server/errors/cache-error-codes.ts
|
|
@@ -108,12 +109,17 @@ var CACHE_ERROR_MESSAGES = /* @__PURE__ */ new Map([
|
|
|
108
109
|
[
|
|
109
110
|
CACHE_ERROR_CODES.KEY_NOT_IN_SCOPE,
|
|
110
111
|
"That key does not belong to the named administration scope."
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
CACHE_ERROR_CODES.INVALID_TTL,
|
|
115
|
+
"A TTL in seconds must be a safe integer, and at least 1 for a write that sets one."
|
|
111
116
|
]
|
|
112
117
|
]);
|
|
113
118
|
var CACHE_ERROR_STATUS = /* @__PURE__ */ new Map([
|
|
114
119
|
[CACHE_ERROR_CODES.COMMAND_TIMEOUT, common.HttpStatus.GATEWAY_TIMEOUT],
|
|
115
120
|
[CACHE_ERROR_CODES.CONNECTION_LOST, common.HttpStatus.SERVICE_UNAVAILABLE],
|
|
116
121
|
[CACHE_ERROR_CODES.INVALID_KEY, common.HttpStatus.BAD_REQUEST],
|
|
122
|
+
[CACHE_ERROR_CODES.INVALID_TTL, common.HttpStatus.BAD_REQUEST],
|
|
117
123
|
[CACHE_ERROR_CODES.FLUSH_DISABLED_IN_PRODUCTION, common.HttpStatus.FORBIDDEN]
|
|
118
124
|
]);
|
|
119
125
|
var CacheException = class extends common.HttpException {
|
|
@@ -348,7 +354,8 @@ exports.ConnectionManager = class ConnectionManager {
|
|
|
348
354
|
* ready buffers until connected instead of failing fast like the data-plane
|
|
349
355
|
* main client (whose offline queue stays disabled to avoid silent buffering).
|
|
350
356
|
* This override applies to standalone/sentinel modes only; in cluster mode
|
|
351
|
-
* {@link createClient} ignores it
|
|
357
|
+
* {@link createClient} ignores it and the cluster client manages its own subscriber
|
|
358
|
+
* connection.
|
|
352
359
|
*
|
|
353
360
|
* @returns A fresh client wired with `subscriber`-role event listeners.
|
|
354
361
|
*/
|
|
@@ -779,6 +786,17 @@ var resolveSerializer = (options, injected) => (
|
|
|
779
786
|
options.serializer ?? injected ?? new exports.JsonSerializer()
|
|
780
787
|
);
|
|
781
788
|
|
|
789
|
+
// src/server/utils/ttl.ts
|
|
790
|
+
var MIN_WRITE_TTL_SECONDS = 1;
|
|
791
|
+
function assertTtlSeconds(ttlSeconds, minimum) {
|
|
792
|
+
if (!Number.isSafeInteger(ttlSeconds)) {
|
|
793
|
+
throw new CacheException(CACHE_ERROR_CODES.INVALID_TTL, { reason: "not_a_safe_integer" });
|
|
794
|
+
}
|
|
795
|
+
if (minimum !== void 0 && ttlSeconds < minimum) {
|
|
796
|
+
throw new CacheException(CACHE_ERROR_CODES.INVALID_TTL, { reason: "below_minimum", minimum });
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
782
800
|
// src/server/services/cache.service.ts
|
|
783
801
|
var PRODUCTION_ENV = "production";
|
|
784
802
|
var SCAN_COUNT = 100;
|
|
@@ -839,14 +857,17 @@ exports.CacheService = class CacheService {
|
|
|
839
857
|
* @param prefix - Entity-group prefix.
|
|
840
858
|
* @param id - Entity id.
|
|
841
859
|
* @param value - The value to store (passed through the serializer).
|
|
842
|
-
* @param ttlSeconds - Optional expiry in seconds; omit for
|
|
860
|
+
* @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
|
|
861
|
+
* no expiration.
|
|
843
862
|
* @returns Resolves once the write completes.
|
|
844
863
|
* @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
|
|
864
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
845
865
|
*/
|
|
846
866
|
async set(prefix, id, value, ttlSeconds) {
|
|
847
867
|
const key = this.keyBuilder.build(prefix, id);
|
|
848
868
|
const raw = this.serializer.serialize(value);
|
|
849
869
|
if (ttlSeconds !== void 0) {
|
|
870
|
+
assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
|
|
850
871
|
await this.connection.getClient().set(key, raw, "EX", ttlSeconds);
|
|
851
872
|
} else {
|
|
852
873
|
await this.connection.getClient().set(key, raw);
|
|
@@ -858,12 +879,15 @@ exports.CacheService = class CacheService {
|
|
|
858
879
|
* @param prefix - Entity-group prefix.
|
|
859
880
|
* @param id - Entity id.
|
|
860
881
|
* @param value - The raw string to store as-is.
|
|
861
|
-
* @param ttlSeconds - Optional expiry in seconds; omit for
|
|
882
|
+
* @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
|
|
883
|
+
* no expiration.
|
|
862
884
|
* @returns Resolves once the write completes.
|
|
885
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
863
886
|
*/
|
|
864
887
|
async setRaw(prefix, id, value, ttlSeconds) {
|
|
865
888
|
const key = this.keyBuilder.build(prefix, id);
|
|
866
889
|
if (ttlSeconds !== void 0) {
|
|
890
|
+
assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
|
|
867
891
|
await this.connection.getClient().set(key, value, "EX", ttlSeconds);
|
|
868
892
|
} else {
|
|
869
893
|
await this.connection.getClient().set(key, value);
|
|
@@ -876,13 +900,16 @@ exports.CacheService = class CacheService {
|
|
|
876
900
|
* @param prefix - Entity-group prefix.
|
|
877
901
|
* @param id - Entity id.
|
|
878
902
|
* @param value - The value to store (passed through the serializer).
|
|
879
|
-
* @param ttlSeconds - Optional expiry in seconds applied on the same atomic write
|
|
903
|
+
* @param ttlSeconds - Optional expiry in seconds applied on the same atomic write, a safe
|
|
904
|
+
* integer of at least 1.
|
|
880
905
|
* @returns `true` when the value was stored, `false` when the key already existed.
|
|
881
906
|
* @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
|
|
907
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
882
908
|
*/
|
|
883
909
|
async setNx(prefix, id, value, ttlSeconds) {
|
|
884
910
|
const key = this.keyBuilder.build(prefix, id);
|
|
885
911
|
const raw = this.serializer.serialize(value);
|
|
912
|
+
if (ttlSeconds !== void 0) assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
|
|
886
913
|
const result = ttlSeconds !== void 0 ? await this.connection.getClient().set(key, raw, "EX", ttlSeconds, "NX") : await this.connection.getClient().set(key, raw, "NX");
|
|
887
914
|
return result === "OK";
|
|
888
915
|
}
|
|
@@ -954,10 +981,13 @@ exports.CacheService = class CacheService {
|
|
|
954
981
|
*
|
|
955
982
|
* @param prefix - Entity-group prefix.
|
|
956
983
|
* @param id - Entity id.
|
|
957
|
-
* @param ttlSeconds - Expiry in seconds.
|
|
984
|
+
* @param ttlSeconds - Expiry in seconds, a safe integer. As in Redis, zero or a negative
|
|
985
|
+
* value deletes the key.
|
|
958
986
|
* @returns `true` when the timeout was set, `false` when the key does not exist.
|
|
987
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer.
|
|
959
988
|
*/
|
|
960
989
|
async expire(prefix, id, ttlSeconds) {
|
|
990
|
+
assertTtlSeconds(ttlSeconds, void 0);
|
|
961
991
|
const result = await this.connection.getClient().expire(this.keyBuilder.build(prefix, id), ttlSeconds);
|
|
962
992
|
return result === 1;
|
|
963
993
|
}
|
|
@@ -1165,6 +1195,9 @@ exports.CacheService = class CacheService {
|
|
|
1165
1195
|
* @param prefix - Entity-group prefix.
|
|
1166
1196
|
* @param pattern - Glob pattern for the id segment, e.g. `'*'`.
|
|
1167
1197
|
* @returns The matching fully-namespaced keys.
|
|
1198
|
+
* @throws {CacheException} `UNSUPPORTED_IN_CLUSTER` when called in cluster mode:
|
|
1199
|
+
* `KEYS` carries no key, so the cluster client sends it to one sampled node and the
|
|
1200
|
+
* reply would cover only that node's share of the keyspace.
|
|
1168
1201
|
* @example
|
|
1169
1202
|
* ```ts
|
|
1170
1203
|
* await cache.keys('users', '*') // ['app:users:u_1', 'app:users:u_2']
|
|
@@ -1172,7 +1205,11 @@ exports.CacheService = class CacheService {
|
|
|
1172
1205
|
*/
|
|
1173
1206
|
async keys(prefix, pattern) {
|
|
1174
1207
|
const fullPattern = this.keyBuilder.build(prefix, pattern);
|
|
1175
|
-
|
|
1208
|
+
const client = this.connection.getClient();
|
|
1209
|
+
if (!isScannableClient(client)) {
|
|
1210
|
+
throw new CacheException(CACHE_ERROR_CODES.UNSUPPORTED_IN_CLUSTER, { operation: "keys" });
|
|
1211
|
+
}
|
|
1212
|
+
return client.keys(fullPattern);
|
|
1176
1213
|
}
|
|
1177
1214
|
/**
|
|
1178
1215
|
* Iterates keys matching a pattern under a prefix using a non-blocking cursor.
|
package/dist/server/index.d.cts
CHANGED
|
@@ -399,7 +399,8 @@ declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
|
|
|
399
399
|
* ready buffers until connected instead of failing fast like the data-plane
|
|
400
400
|
* main client (whose offline queue stays disabled to avoid silent buffering).
|
|
401
401
|
* This override applies to standalone/sentinel modes only; in cluster mode
|
|
402
|
-
* {@link createClient} ignores it
|
|
402
|
+
* {@link createClient} ignores it and the cluster client manages its own subscriber
|
|
403
|
+
* connection.
|
|
403
404
|
*
|
|
404
405
|
* @returns A fresh client wired with `subscriber`-role event listeners.
|
|
405
406
|
*/
|
|
@@ -591,9 +592,11 @@ declare class CacheService {
|
|
|
591
592
|
* @param prefix - Entity-group prefix.
|
|
592
593
|
* @param id - Entity id.
|
|
593
594
|
* @param value - The value to store (passed through the serializer).
|
|
594
|
-
* @param ttlSeconds - Optional expiry in seconds; omit for
|
|
595
|
+
* @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
|
|
596
|
+
* no expiration.
|
|
595
597
|
* @returns Resolves once the write completes.
|
|
596
598
|
* @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
|
|
599
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
597
600
|
*/
|
|
598
601
|
set<T>(prefix: string, id: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
599
602
|
/**
|
|
@@ -602,8 +605,10 @@ declare class CacheService {
|
|
|
602
605
|
* @param prefix - Entity-group prefix.
|
|
603
606
|
* @param id - Entity id.
|
|
604
607
|
* @param value - The raw string to store as-is.
|
|
605
|
-
* @param ttlSeconds - Optional expiry in seconds; omit for
|
|
608
|
+
* @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
|
|
609
|
+
* no expiration.
|
|
606
610
|
* @returns Resolves once the write completes.
|
|
611
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
607
612
|
*/
|
|
608
613
|
setRaw(prefix: string, id: string, value: string, ttlSeconds?: number): Promise<void>;
|
|
609
614
|
/**
|
|
@@ -613,9 +618,11 @@ declare class CacheService {
|
|
|
613
618
|
* @param prefix - Entity-group prefix.
|
|
614
619
|
* @param id - Entity id.
|
|
615
620
|
* @param value - The value to store (passed through the serializer).
|
|
616
|
-
* @param ttlSeconds - Optional expiry in seconds applied on the same atomic write
|
|
621
|
+
* @param ttlSeconds - Optional expiry in seconds applied on the same atomic write, a safe
|
|
622
|
+
* integer of at least 1.
|
|
617
623
|
* @returns `true` when the value was stored, `false` when the key already existed.
|
|
618
624
|
* @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
|
|
625
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
619
626
|
*/
|
|
620
627
|
setNx<T>(prefix: string, id: string, value: T, ttlSeconds?: number): Promise<boolean>;
|
|
621
628
|
/**
|
|
@@ -667,8 +674,10 @@ declare class CacheService {
|
|
|
667
674
|
*
|
|
668
675
|
* @param prefix - Entity-group prefix.
|
|
669
676
|
* @param id - Entity id.
|
|
670
|
-
* @param ttlSeconds - Expiry in seconds.
|
|
677
|
+
* @param ttlSeconds - Expiry in seconds, a safe integer. As in Redis, zero or a negative
|
|
678
|
+
* value deletes the key.
|
|
671
679
|
* @returns `true` when the timeout was set, `false` when the key does not exist.
|
|
680
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer.
|
|
672
681
|
*/
|
|
673
682
|
expire(prefix: string, id: string, ttlSeconds: number): Promise<boolean>;
|
|
674
683
|
/**
|
|
@@ -809,6 +818,9 @@ declare class CacheService {
|
|
|
809
818
|
* @param prefix - Entity-group prefix.
|
|
810
819
|
* @param pattern - Glob pattern for the id segment, e.g. `'*'`.
|
|
811
820
|
* @returns The matching fully-namespaced keys.
|
|
821
|
+
* @throws {CacheException} `UNSUPPORTED_IN_CLUSTER` when called in cluster mode:
|
|
822
|
+
* `KEYS` carries no key, so the cluster client sends it to one sampled node and the
|
|
823
|
+
* reply would cover only that node's share of the keyspace.
|
|
812
824
|
* @example
|
|
813
825
|
* ```ts
|
|
814
826
|
* await cache.keys('users', '*') // ['app:users:u_1', 'app:users:u_2']
|
|
@@ -1199,6 +1211,7 @@ declare const CACHE_ERROR_CODES: {
|
|
|
1199
1211
|
readonly SCOPE_NOT_FOUND: "cache.scope_not_found";
|
|
1200
1212
|
readonly SCOPE_NOT_READABLE: "cache.scope_not_readable";
|
|
1201
1213
|
readonly KEY_NOT_IN_SCOPE: "cache.key_not_in_scope";
|
|
1214
|
+
readonly INVALID_TTL: "cache.invalid_ttl";
|
|
1202
1215
|
};
|
|
1203
1216
|
/** Union of every cache error code string value. */
|
|
1204
1217
|
type CacheErrorCode = (typeof CACHE_ERROR_CODES)[keyof typeof CACHE_ERROR_CODES];
|
package/dist/server/index.d.ts
CHANGED
|
@@ -399,7 +399,8 @@ declare class ConnectionManager implements OnModuleInit, OnModuleDestroy {
|
|
|
399
399
|
* ready buffers until connected instead of failing fast like the data-plane
|
|
400
400
|
* main client (whose offline queue stays disabled to avoid silent buffering).
|
|
401
401
|
* This override applies to standalone/sentinel modes only; in cluster mode
|
|
402
|
-
* {@link createClient} ignores it
|
|
402
|
+
* {@link createClient} ignores it and the cluster client manages its own subscriber
|
|
403
|
+
* connection.
|
|
403
404
|
*
|
|
404
405
|
* @returns A fresh client wired with `subscriber`-role event listeners.
|
|
405
406
|
*/
|
|
@@ -591,9 +592,11 @@ declare class CacheService {
|
|
|
591
592
|
* @param prefix - Entity-group prefix.
|
|
592
593
|
* @param id - Entity id.
|
|
593
594
|
* @param value - The value to store (passed through the serializer).
|
|
594
|
-
* @param ttlSeconds - Optional expiry in seconds; omit for
|
|
595
|
+
* @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
|
|
596
|
+
* no expiration.
|
|
595
597
|
* @returns Resolves once the write completes.
|
|
596
598
|
* @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
|
|
599
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
597
600
|
*/
|
|
598
601
|
set<T>(prefix: string, id: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
599
602
|
/**
|
|
@@ -602,8 +605,10 @@ declare class CacheService {
|
|
|
602
605
|
* @param prefix - Entity-group prefix.
|
|
603
606
|
* @param id - Entity id.
|
|
604
607
|
* @param value - The raw string to store as-is.
|
|
605
|
-
* @param ttlSeconds - Optional expiry in seconds; omit for
|
|
608
|
+
* @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
|
|
609
|
+
* no expiration.
|
|
606
610
|
* @returns Resolves once the write completes.
|
|
611
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
607
612
|
*/
|
|
608
613
|
setRaw(prefix: string, id: string, value: string, ttlSeconds?: number): Promise<void>;
|
|
609
614
|
/**
|
|
@@ -613,9 +618,11 @@ declare class CacheService {
|
|
|
613
618
|
* @param prefix - Entity-group prefix.
|
|
614
619
|
* @param id - Entity id.
|
|
615
620
|
* @param value - The value to store (passed through the serializer).
|
|
616
|
-
* @param ttlSeconds - Optional expiry in seconds applied on the same atomic write
|
|
621
|
+
* @param ttlSeconds - Optional expiry in seconds applied on the same atomic write, a safe
|
|
622
|
+
* integer of at least 1.
|
|
617
623
|
* @returns `true` when the value was stored, `false` when the key already existed.
|
|
618
624
|
* @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
|
|
625
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
619
626
|
*/
|
|
620
627
|
setNx<T>(prefix: string, id: string, value: T, ttlSeconds?: number): Promise<boolean>;
|
|
621
628
|
/**
|
|
@@ -667,8 +674,10 @@ declare class CacheService {
|
|
|
667
674
|
*
|
|
668
675
|
* @param prefix - Entity-group prefix.
|
|
669
676
|
* @param id - Entity id.
|
|
670
|
-
* @param ttlSeconds - Expiry in seconds.
|
|
677
|
+
* @param ttlSeconds - Expiry in seconds, a safe integer. As in Redis, zero or a negative
|
|
678
|
+
* value deletes the key.
|
|
671
679
|
* @returns `true` when the timeout was set, `false` when the key does not exist.
|
|
680
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer.
|
|
672
681
|
*/
|
|
673
682
|
expire(prefix: string, id: string, ttlSeconds: number): Promise<boolean>;
|
|
674
683
|
/**
|
|
@@ -809,6 +818,9 @@ declare class CacheService {
|
|
|
809
818
|
* @param prefix - Entity-group prefix.
|
|
810
819
|
* @param pattern - Glob pattern for the id segment, e.g. `'*'`.
|
|
811
820
|
* @returns The matching fully-namespaced keys.
|
|
821
|
+
* @throws {CacheException} `UNSUPPORTED_IN_CLUSTER` when called in cluster mode:
|
|
822
|
+
* `KEYS` carries no key, so the cluster client sends it to one sampled node and the
|
|
823
|
+
* reply would cover only that node's share of the keyspace.
|
|
812
824
|
* @example
|
|
813
825
|
* ```ts
|
|
814
826
|
* await cache.keys('users', '*') // ['app:users:u_1', 'app:users:u_2']
|
|
@@ -1199,6 +1211,7 @@ declare const CACHE_ERROR_CODES: {
|
|
|
1199
1211
|
readonly SCOPE_NOT_FOUND: "cache.scope_not_found";
|
|
1200
1212
|
readonly SCOPE_NOT_READABLE: "cache.scope_not_readable";
|
|
1201
1213
|
readonly KEY_NOT_IN_SCOPE: "cache.key_not_in_scope";
|
|
1214
|
+
readonly INVALID_TTL: "cache.invalid_ttl";
|
|
1202
1215
|
};
|
|
1203
1216
|
/** Union of every cache error code string value. */
|
|
1204
1217
|
type CacheErrorCode = (typeof CACHE_ERROR_CODES)[keyof typeof CACHE_ERROR_CODES];
|
package/dist/server/index.mjs
CHANGED
|
@@ -65,7 +65,8 @@ var CACHE_ERROR_CODES = {
|
|
|
65
65
|
INVALID_SCOPE: "cache.invalid_scope",
|
|
66
66
|
SCOPE_NOT_FOUND: "cache.scope_not_found",
|
|
67
67
|
SCOPE_NOT_READABLE: "cache.scope_not_readable",
|
|
68
|
-
KEY_NOT_IN_SCOPE: "cache.key_not_in_scope"
|
|
68
|
+
KEY_NOT_IN_SCOPE: "cache.key_not_in_scope",
|
|
69
|
+
INVALID_TTL: "cache.invalid_ttl"
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
// src/server/errors/cache-error-codes.ts
|
|
@@ -106,12 +107,17 @@ var CACHE_ERROR_MESSAGES = /* @__PURE__ */ new Map([
|
|
|
106
107
|
[
|
|
107
108
|
CACHE_ERROR_CODES.KEY_NOT_IN_SCOPE,
|
|
108
109
|
"That key does not belong to the named administration scope."
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
CACHE_ERROR_CODES.INVALID_TTL,
|
|
113
|
+
"A TTL in seconds must be a safe integer, and at least 1 for a write that sets one."
|
|
109
114
|
]
|
|
110
115
|
]);
|
|
111
116
|
var CACHE_ERROR_STATUS = /* @__PURE__ */ new Map([
|
|
112
117
|
[CACHE_ERROR_CODES.COMMAND_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT],
|
|
113
118
|
[CACHE_ERROR_CODES.CONNECTION_LOST, HttpStatus.SERVICE_UNAVAILABLE],
|
|
114
119
|
[CACHE_ERROR_CODES.INVALID_KEY, HttpStatus.BAD_REQUEST],
|
|
120
|
+
[CACHE_ERROR_CODES.INVALID_TTL, HttpStatus.BAD_REQUEST],
|
|
115
121
|
[CACHE_ERROR_CODES.FLUSH_DISABLED_IN_PRODUCTION, HttpStatus.FORBIDDEN]
|
|
116
122
|
]);
|
|
117
123
|
var CacheException = class extends HttpException {
|
|
@@ -346,7 +352,8 @@ var ConnectionManager = class {
|
|
|
346
352
|
* ready buffers until connected instead of failing fast like the data-plane
|
|
347
353
|
* main client (whose offline queue stays disabled to avoid silent buffering).
|
|
348
354
|
* This override applies to standalone/sentinel modes only; in cluster mode
|
|
349
|
-
* {@link createClient} ignores it
|
|
355
|
+
* {@link createClient} ignores it and the cluster client manages its own subscriber
|
|
356
|
+
* connection.
|
|
350
357
|
*
|
|
351
358
|
* @returns A fresh client wired with `subscriber`-role event listeners.
|
|
352
359
|
*/
|
|
@@ -777,6 +784,17 @@ var resolveSerializer = (options, injected) => (
|
|
|
777
784
|
options.serializer ?? injected ?? new JsonSerializer()
|
|
778
785
|
);
|
|
779
786
|
|
|
787
|
+
// src/server/utils/ttl.ts
|
|
788
|
+
var MIN_WRITE_TTL_SECONDS = 1;
|
|
789
|
+
function assertTtlSeconds(ttlSeconds, minimum) {
|
|
790
|
+
if (!Number.isSafeInteger(ttlSeconds)) {
|
|
791
|
+
throw new CacheException(CACHE_ERROR_CODES.INVALID_TTL, { reason: "not_a_safe_integer" });
|
|
792
|
+
}
|
|
793
|
+
if (minimum !== void 0 && ttlSeconds < minimum) {
|
|
794
|
+
throw new CacheException(CACHE_ERROR_CODES.INVALID_TTL, { reason: "below_minimum", minimum });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
780
798
|
// src/server/services/cache.service.ts
|
|
781
799
|
var PRODUCTION_ENV = "production";
|
|
782
800
|
var SCAN_COUNT = 100;
|
|
@@ -837,14 +855,17 @@ var CacheService = class {
|
|
|
837
855
|
* @param prefix - Entity-group prefix.
|
|
838
856
|
* @param id - Entity id.
|
|
839
857
|
* @param value - The value to store (passed through the serializer).
|
|
840
|
-
* @param ttlSeconds - Optional expiry in seconds; omit for
|
|
858
|
+
* @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
|
|
859
|
+
* no expiration.
|
|
841
860
|
* @returns Resolves once the write completes.
|
|
842
861
|
* @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
|
|
862
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
843
863
|
*/
|
|
844
864
|
async set(prefix, id, value, ttlSeconds) {
|
|
845
865
|
const key = this.keyBuilder.build(prefix, id);
|
|
846
866
|
const raw = this.serializer.serialize(value);
|
|
847
867
|
if (ttlSeconds !== void 0) {
|
|
868
|
+
assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
|
|
848
869
|
await this.connection.getClient().set(key, raw, "EX", ttlSeconds);
|
|
849
870
|
} else {
|
|
850
871
|
await this.connection.getClient().set(key, raw);
|
|
@@ -856,12 +877,15 @@ var CacheService = class {
|
|
|
856
877
|
* @param prefix - Entity-group prefix.
|
|
857
878
|
* @param id - Entity id.
|
|
858
879
|
* @param value - The raw string to store as-is.
|
|
859
|
-
* @param ttlSeconds - Optional expiry in seconds; omit for
|
|
880
|
+
* @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
|
|
881
|
+
* no expiration.
|
|
860
882
|
* @returns Resolves once the write completes.
|
|
883
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
861
884
|
*/
|
|
862
885
|
async setRaw(prefix, id, value, ttlSeconds) {
|
|
863
886
|
const key = this.keyBuilder.build(prefix, id);
|
|
864
887
|
if (ttlSeconds !== void 0) {
|
|
888
|
+
assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
|
|
865
889
|
await this.connection.getClient().set(key, value, "EX", ttlSeconds);
|
|
866
890
|
} else {
|
|
867
891
|
await this.connection.getClient().set(key, value);
|
|
@@ -874,13 +898,16 @@ var CacheService = class {
|
|
|
874
898
|
* @param prefix - Entity-group prefix.
|
|
875
899
|
* @param id - Entity id.
|
|
876
900
|
* @param value - The value to store (passed through the serializer).
|
|
877
|
-
* @param ttlSeconds - Optional expiry in seconds applied on the same atomic write
|
|
901
|
+
* @param ttlSeconds - Optional expiry in seconds applied on the same atomic write, a safe
|
|
902
|
+
* integer of at least 1.
|
|
878
903
|
* @returns `true` when the value was stored, `false` when the key already existed.
|
|
879
904
|
* @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
|
|
905
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
|
|
880
906
|
*/
|
|
881
907
|
async setNx(prefix, id, value, ttlSeconds) {
|
|
882
908
|
const key = this.keyBuilder.build(prefix, id);
|
|
883
909
|
const raw = this.serializer.serialize(value);
|
|
910
|
+
if (ttlSeconds !== void 0) assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
|
|
884
911
|
const result = ttlSeconds !== void 0 ? await this.connection.getClient().set(key, raw, "EX", ttlSeconds, "NX") : await this.connection.getClient().set(key, raw, "NX");
|
|
885
912
|
return result === "OK";
|
|
886
913
|
}
|
|
@@ -952,10 +979,13 @@ var CacheService = class {
|
|
|
952
979
|
*
|
|
953
980
|
* @param prefix - Entity-group prefix.
|
|
954
981
|
* @param id - Entity id.
|
|
955
|
-
* @param ttlSeconds - Expiry in seconds.
|
|
982
|
+
* @param ttlSeconds - Expiry in seconds, a safe integer. As in Redis, zero or a negative
|
|
983
|
+
* value deletes the key.
|
|
956
984
|
* @returns `true` when the timeout was set, `false` when the key does not exist.
|
|
985
|
+
* @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer.
|
|
957
986
|
*/
|
|
958
987
|
async expire(prefix, id, ttlSeconds) {
|
|
988
|
+
assertTtlSeconds(ttlSeconds, void 0);
|
|
959
989
|
const result = await this.connection.getClient().expire(this.keyBuilder.build(prefix, id), ttlSeconds);
|
|
960
990
|
return result === 1;
|
|
961
991
|
}
|
|
@@ -1163,6 +1193,9 @@ var CacheService = class {
|
|
|
1163
1193
|
* @param prefix - Entity-group prefix.
|
|
1164
1194
|
* @param pattern - Glob pattern for the id segment, e.g. `'*'`.
|
|
1165
1195
|
* @returns The matching fully-namespaced keys.
|
|
1196
|
+
* @throws {CacheException} `UNSUPPORTED_IN_CLUSTER` when called in cluster mode:
|
|
1197
|
+
* `KEYS` carries no key, so the cluster client sends it to one sampled node and the
|
|
1198
|
+
* reply would cover only that node's share of the keyspace.
|
|
1166
1199
|
* @example
|
|
1167
1200
|
* ```ts
|
|
1168
1201
|
* await cache.keys('users', '*') // ['app:users:u_1', 'app:users:u_2']
|
|
@@ -1170,7 +1203,11 @@ var CacheService = class {
|
|
|
1170
1203
|
*/
|
|
1171
1204
|
async keys(prefix, pattern) {
|
|
1172
1205
|
const fullPattern = this.keyBuilder.build(prefix, pattern);
|
|
1173
|
-
|
|
1206
|
+
const client = this.connection.getClient();
|
|
1207
|
+
if (!isScannableClient(client)) {
|
|
1208
|
+
throw new CacheException(CACHE_ERROR_CODES.UNSUPPORTED_IN_CLUSTER, { operation: "keys" });
|
|
1209
|
+
}
|
|
1210
|
+
return client.keys(fullPattern);
|
|
1174
1211
|
}
|
|
1175
1212
|
/**
|
|
1176
1213
|
* Iterates keys matching a pattern under a prefix using a non-blocking cursor.
|
package/dist/shared/index.cjs
CHANGED
|
@@ -20,7 +20,8 @@ var CACHE_ERROR_CODES = {
|
|
|
20
20
|
INVALID_SCOPE: "cache.invalid_scope",
|
|
21
21
|
SCOPE_NOT_FOUND: "cache.scope_not_found",
|
|
22
22
|
SCOPE_NOT_READABLE: "cache.scope_not_readable",
|
|
23
|
-
KEY_NOT_IN_SCOPE: "cache.key_not_in_scope"
|
|
23
|
+
KEY_NOT_IN_SCOPE: "cache.key_not_in_scope",
|
|
24
|
+
INVALID_TTL: "cache.invalid_ttl"
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
// src/shared/constants/event-names.ts
|
package/dist/shared/index.d.cts
CHANGED
|
@@ -97,6 +97,7 @@ declare const CACHE_ERROR_CODES: {
|
|
|
97
97
|
readonly SCOPE_NOT_FOUND: "cache.scope_not_found";
|
|
98
98
|
readonly SCOPE_NOT_READABLE: "cache.scope_not_readable";
|
|
99
99
|
readonly KEY_NOT_IN_SCOPE: "cache.key_not_in_scope";
|
|
100
|
+
readonly INVALID_TTL: "cache.invalid_ttl";
|
|
100
101
|
};
|
|
101
102
|
/** Union of every cache error code string value. */
|
|
102
103
|
type CacheErrorCode = (typeof CACHE_ERROR_CODES)[keyof typeof CACHE_ERROR_CODES];
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -97,6 +97,7 @@ declare const CACHE_ERROR_CODES: {
|
|
|
97
97
|
readonly SCOPE_NOT_FOUND: "cache.scope_not_found";
|
|
98
98
|
readonly SCOPE_NOT_READABLE: "cache.scope_not_readable";
|
|
99
99
|
readonly KEY_NOT_IN_SCOPE: "cache.key_not_in_scope";
|
|
100
|
+
readonly INVALID_TTL: "cache.invalid_ttl";
|
|
100
101
|
};
|
|
101
102
|
/** Union of every cache error code string value. */
|
|
102
103
|
type CacheErrorCode = (typeof CACHE_ERROR_CODES)[keyof typeof CACHE_ERROR_CODES];
|
package/dist/shared/index.mjs
CHANGED
|
@@ -18,7 +18,8 @@ var CACHE_ERROR_CODES = {
|
|
|
18
18
|
INVALID_SCOPE: "cache.invalid_scope",
|
|
19
19
|
SCOPE_NOT_FOUND: "cache.scope_not_found",
|
|
20
20
|
SCOPE_NOT_READABLE: "cache.scope_not_readable",
|
|
21
|
-
KEY_NOT_IN_SCOPE: "cache.key_not_in_scope"
|
|
21
|
+
KEY_NOT_IN_SCOPE: "cache.key_not_in_scope",
|
|
22
|
+
INVALID_TTL: "cache.invalid_ttl"
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
// src/shared/constants/event-names.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-cache",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "Typed Redis cache for NestJS based on ioredis 6, with namespace strategy, Pub/Sub and Lua script management.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|