layercache 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/README.md +101 -84
- package/dist/{chunk-KOYGHLVP.js → chunk-JC26W3KK.js} +27 -7
- package/dist/cli.cjs +2 -16
- package/dist/cli.js +2 -16
- package/dist/{edge-B_rUqDy6.d.cts → edge-P07GCO2Y.d.cts} +2 -2
- package/dist/{edge-B_rUqDy6.d.ts → edge-P07GCO2Y.d.ts} +2 -2
- package/dist/edge.cjs +27 -7
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +235 -125
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +161 -71
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +209 -118
- package/packages/nestjs/dist/index.d.cts +2 -2
- package/packages/nestjs/dist/index.d.ts +2 -2
- package/packages/nestjs/dist/index.js +209 -118
package/README.md
CHANGED
|
@@ -1,92 +1,99 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./logo.png" width="520" alt="layercache logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">layercache</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Production-ready multi-layer caching for Node.js</strong><br>
|
|
9
|
+
<em>Memory, Redis, persistence, invalidation, observability, and resilience in one API.</em>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/layercache"><img src="https://img.shields.io/npm/v/layercache" alt="npm version"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/layercache"><img src="https://img.shields.io/npm/dw/layercache" alt="npm downloads"></a>
|
|
15
|
+
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/layercache" alt="license"></a>
|
|
16
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-first-blue" alt="TypeScript"></a>
|
|
17
|
+
<a href="https://github.com/flyingsquirrel0419/layercache"><img src="https://img.shields.io/badge/tests-180%20passing-brightgreen" alt="tests"></a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="#installation">Installation</a> ·
|
|
22
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
23
|
+
<a href="#core-api">Core API</a> ·
|
|
24
|
+
<a href="#invalidation--freshness">Invalidation</a> ·
|
|
25
|
+
<a href="#integrations--tooling">Integrations</a> ·
|
|
26
|
+
<a href="#contributing">Contributing</a>
|
|
27
|
+
</p>
|
|
2
28
|
|
|
3
|
-
|
|
29
|
+
---
|
|
4
30
|
|
|
5
|
-
|
|
6
|
-
[](https://www.npmjs.com/package/layercache)
|
|
7
|
-
[](LICENSE)
|
|
8
|
-
[](https://www.typescriptlang.org/)
|
|
9
|
-
[](https://github.com/flyingsquirrel0419/layercache)
|
|
31
|
+
## At a glance
|
|
10
32
|
|
|
11
|
-
|
|
33
|
+
- **Fast read path** — combine memory L1, Redis L2, disk, or custom layers behind one API with automatic backfill.
|
|
34
|
+
- **Stampede control** — prevent duplicate miss storms with in-process dedupe and optional distributed single-flight.
|
|
35
|
+
- **Strong invalidation model** — support tags, batched tags, wildcards, prefixes, and generation-based cache rotation.
|
|
36
|
+
- **Built for production failure modes** — serve stale safely, refresh ahead, degrade gracefully, trip circuit breakers, and throttle fetchers.
|
|
37
|
+
- **Operational visibility included** — expose metrics, stats, health checks, OpenTelemetry spans, and an admin CLI.
|
|
38
|
+
- **Fits real Node stacks** — integrate directly with Express, Fastify, Hono, GraphQL, tRPC, and NestJS.
|
|
39
|
+
|
|
40
|
+
Most Node.js services hit the same wall:
|
|
41
|
+
|
|
42
|
+
| Approach | Tradeoff |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Memory-only cache | Fast, but every instance has a different view of data |
|
|
45
|
+
| Redis-only cache | Shared, but every request pays a network round-trip |
|
|
46
|
+
| Hand-rolled hybrid cache | Possible, but you rebuild stampede prevention, invalidation, TTL policy, and observability yourself |
|
|
47
|
+
|
|
48
|
+
`layercache` gives you a single API for layered caching and handles the hard parts for you: read-through fetches, backfill, stale serving, distributed invalidation, rate limiting, persistence, and operational introspection.
|
|
49
|
+
|
|
50
|
+
On a hit, `layercache` serves the fastest available layer and backfills anything above it. On a miss, the fetcher runs once, even under heavy concurrency.
|
|
51
|
+
|
|
52
|
+
## Performance profile
|
|
53
|
+
|
|
54
|
+
```text
|
|
12
55
|
L1 hit ~0.01 ms ← served from memory, zero network
|
|
13
56
|
L2 hit ~0.5 ms ← served from Redis, backfilled to memory
|
|
14
57
|
miss ~20 ms ← fetcher runs once, all layers filled
|
|
15
58
|
```
|
|
16
59
|
|
|
17
|
-
|
|
60
|
+
## Why teams use it
|
|
18
61
|
|
|
19
|
-
|
|
62
|
+
- **Predictable cache behavior** — layered reads, automatic backfill, negative caching, refresh-ahead, and stale serving.
|
|
63
|
+
- **Reliable invalidation** — tags, batched tags, wildcards, prefixes, and generation-based rotation.
|
|
64
|
+
- **Production safeguards** — rate limiting, circuit breakers, graceful degradation, compression limits, serializer hardening, and snapshot path validation.
|
|
65
|
+
- **Operational visibility** — metrics, health checks, OpenTelemetry spans, stats endpoints, and an admin CLI.
|
|
66
|
+
- **Works with your stack** — Express, Fastify, Hono, GraphQL, tRPC, NestJS, CLI, and custom cache layers.
|
|
20
67
|
|
|
21
|
-
|
|
68
|
+
## Feature map
|
|
22
69
|
|
|
23
|
-
|
|
24
|
-
- **Redis-only** → shared, but every read pays a network round-trip
|
|
25
|
-
- **Hand-rolled layers** → works, but you rewrite stampede prevention, backfill logic, and tag invalidation in every project
|
|
70
|
+
### Core caching
|
|
26
71
|
|
|
27
|
-
|
|
72
|
+
- Layered reads with automatic backfill
|
|
73
|
+
- Stampede prevention and optional distributed single-flight
|
|
74
|
+
- Bulk reads and writes with layer-level `getMany()` / `setMany()` fast paths
|
|
75
|
+
- `wrap()`, namespaces, cache warming, `getOrThrow()`, and `inspect()`
|
|
28
76
|
|
|
29
|
-
|
|
77
|
+
### Invalidation and freshness
|
|
30
78
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
79
|
+
- Tag invalidation, batched tag invalidation, wildcard invalidation, and prefix invalidation
|
|
80
|
+
- Generation-based invalidation with optional stale-generation cleanup
|
|
81
|
+
- Sliding TTL, adaptive TTL, refresh-ahead, stale-while-revalidate, and stale-if-error
|
|
82
|
+
- Per-layer TTL overrides, TTL policies, negative caching, and TTL jitter
|
|
35
83
|
|
|
36
|
-
|
|
84
|
+
### Operations and resilience
|
|
37
85
|
|
|
38
|
-
|
|
86
|
+
- Graceful degradation, circuit breakers, scoped fetcher rate limiting, and write-behind
|
|
87
|
+
- Persistence to memory snapshots or disk snapshots
|
|
88
|
+
- Compression, serializer fallback chains, and MessagePack support
|
|
89
|
+
- Health checks, per-layer metrics, latency tracking, and event hooks
|
|
39
90
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- **Layered reads & automatic backfill** — hits in slower layers propagate up
|
|
43
|
-
- **Cache stampede prevention** — mutex-based deduplication per key
|
|
44
|
-
- **Tag-based invalidation** — `set('user:123:posts', posts, { tags: ['user:123'] })` then `invalidateByTag('user:123')`
|
|
45
|
-
- **Batch tag invalidation** — `invalidateByTags(['tenant:a', 'users'], 'all')` for OR/AND invalidation in one call
|
|
46
|
-
- **Pattern invalidation** — `invalidateByPattern('user:*')`
|
|
47
|
-
- **Prefix invalidation** — efficient `invalidateByPrefix('user:123:')` for hierarchical keys
|
|
48
|
-
- **Generation-based invalidation** — `generation` prefixes keys with `vN:` and `bumpGeneration()` rotates the namespace instantly, with optional stale-generation cleanup
|
|
49
|
-
- **Per-layer TTL overrides** — different TTLs for memory vs. Redis in one call
|
|
50
|
-
- **TTL policies** — align TTLs to time boundaries (`until-midnight`, `next-hour`, `{ alignTo }`, or a function)
|
|
51
|
-
- **Negative caching** — cache known misses for a short TTL to protect the database
|
|
52
|
-
- **Stale strategies** — `staleWhileRevalidate` and `staleIfError` as opt-in read behavior
|
|
53
|
-
- **TTL jitter** — spread expirations to avoid synchronized stampedes
|
|
54
|
-
- **Sliding & adaptive TTL** — extend TTL on every read or ramp it up for hot keys
|
|
55
|
-
- **Refresh-ahead** — trigger background refresh when TTL drops below a threshold
|
|
56
|
-
- **Fetcher rate limiting** — cap concurrent fetchers or requests per interval
|
|
57
|
-
- **Best-effort writes** — tolerate partial layer write failures when desired
|
|
58
|
-
- **Write-behind mode** — write local layers immediately and flush slower remote layers asynchronously
|
|
59
|
-
- **Bulk reads** — `mget` uses layer-level `getMany()` when available
|
|
60
|
-
- **Bulk writes** — `mset` uses layer-level `setMany()` when available
|
|
61
|
-
- **Distributed tag index** — `RedisTagIndex` keeps tag state consistent across multiple servers
|
|
62
|
-
- **Optional distributed single-flight** — plug in a coordinator to dedupe misses across instances
|
|
63
|
-
- **Cross-server L1 invalidation** — Redis pub/sub bus flushes stale memory on other instances when you write or delete
|
|
64
|
-
- **`wrap()` decorator API** — turn any async function into a cached version with auto-generated keys
|
|
65
|
-
- **Cache warming** — pre-populate layers with a prioritised list of entries at startup
|
|
66
|
-
- **Namespaces** — scope a `CacheStack` to a key prefix for multi-tenant or module isolation
|
|
67
|
-
- **Event hooks** — `EventEmitter`-based events for hits, misses, stale serves, errors, and more
|
|
68
|
-
- **Graceful degradation** — skip a failing layer for a configurable retry window
|
|
69
|
-
- **Circuit breaker** — per-key or global; opens after N failures, recovers after cooldown
|
|
70
|
-
- **Compression** — transparent async gzip/brotli in `RedisLayer` (non-blocking) with a byte threshold
|
|
71
|
-
- **Serializer fallback chains** — transparently read legacy payloads (for example JSON) and rewrite them with the primary serializer
|
|
72
|
-
- **Metrics & stats** — per-layer hit/miss counters, **per-layer latency tracking**, circuit-breaker trips, degraded operations; HTTP stats handler
|
|
73
|
-
- **Health checks** — `cache.healthCheck()` returns per-layer health and latency
|
|
74
|
-
- **Persistence** — `exportState` / `importState` for in-process snapshots; `persistToFile` / `restoreFromFile` for disk
|
|
75
|
-
- **Admin CLI** — `layercache stats | keys | inspect | invalidate` against any Redis URL
|
|
76
|
-
- **Framework integrations** — Express middleware, Fastify plugin, Hono middleware, tRPC middleware, GraphQL resolver wrapper
|
|
77
|
-
- **OpenTelemetry plugin** — instrument `get` / `set` / invalidation flows with spans
|
|
78
|
-
- **MessagePack serializer** — drop-in replacement for lower Redis memory usage
|
|
79
|
-
- **NestJS module** — `CacheStackModule.forRoot(...)` and `forRootAsync(...)` with `@InjectCacheStack()`
|
|
80
|
-
- **`getOrThrow()`** — throws `CacheMissError` instead of returning `null`, for strict use cases
|
|
81
|
-
- **`inspect()`** — debug a key: see which layers hold it, remaining TTLs, tags, and staleness state
|
|
82
|
-
- **MemoryLayer cleanup hooks** — periodic TTL cleanup and `onEvict` callbacks
|
|
83
|
-
- **Conditional caching** — `shouldCache` predicate to skip caching specific fetcher results
|
|
84
|
-
- **Nested namespaces** — `namespace('a').namespace('b')` for composable key prefixes with namespace-scoped metrics
|
|
85
|
-
- **Custom layers** — implement the 5-method `CacheLayer` interface to plug in Memcached, DynamoDB, or anything else
|
|
86
|
-
- **Edge-safe entry point** — `layercache/edge` exports the non-Node helpers for Worker-style runtimes
|
|
87
|
-
- **ESM + CJS** — works with both module systems, Node.js ≥ 20
|
|
91
|
+
### Integrations and tooling
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
- Express, Fastify, Hono, GraphQL, tRPC, and NestJS integrations
|
|
94
|
+
- Redis-backed distributed tag index and invalidation bus support
|
|
95
|
+
- Admin CLI for stats, key inspection, and invalidation
|
|
96
|
+
- Edge-safe entry point for Worker-style runtimes
|
|
90
97
|
|
|
91
98
|
## Installation
|
|
92
99
|
|
|
@@ -105,16 +112,11 @@ import { CacheStack, MemoryLayer, RedisLayer } from 'layercache'
|
|
|
105
112
|
import Redis from 'ioredis'
|
|
106
113
|
|
|
107
114
|
const cache = new CacheStack([
|
|
108
|
-
new MemoryLayer({ ttl: 60,
|
|
109
|
-
new RedisLayer({ client: new Redis(), ttl: 3600 })
|
|
115
|
+
new MemoryLayer({ ttl: 60, maxSize: 1_000 }),
|
|
116
|
+
new RedisLayer({ client: new Redis(), ttl: 3600 })
|
|
110
117
|
])
|
|
111
118
|
|
|
112
|
-
// Fetch pattern — cache miss runs the fetcher, hit skips it entirely
|
|
113
119
|
const user = await cache.get<User>('user:123', () => db.findUser(123))
|
|
114
|
-
|
|
115
|
-
// Manual set / delete
|
|
116
|
-
await cache.set('user:123', user)
|
|
117
|
-
await cache.delete('user:123')
|
|
118
120
|
```
|
|
119
121
|
|
|
120
122
|
Memory-only setup (no Redis required):
|
|
@@ -171,6 +173,8 @@ await cache.set('user:123', user, {
|
|
|
171
173
|
})
|
|
172
174
|
```
|
|
173
175
|
|
|
176
|
+
## Invalidation & freshness
|
|
177
|
+
|
|
174
178
|
### `cache.invalidateByTag(tag): Promise<void>`
|
|
175
179
|
|
|
176
180
|
Deletes every key that was stored with this tag across all layers. In multi-instance deployments, this is only complete when every instance shares the same tag index implementation (for example `RedisTagIndex`).
|
|
@@ -199,6 +203,8 @@ Glob-style deletion against the tracked key set, plus any layer that can enumera
|
|
|
199
203
|
await cache.invalidateByPattern('user:*') // deletes user:1, user:2, …
|
|
200
204
|
```
|
|
201
205
|
|
|
206
|
+
Patterns must be non-empty, at most 1024 characters long, and free of control characters.
|
|
207
|
+
|
|
202
208
|
For multi-instance deployments, prefer a shared `RedisTagIndex`. Without it, pattern invalidation still scans real layer keys when available, but that fallback only helps on layers that implement `keys()`, and tag tracking itself remains process-local.
|
|
203
209
|
|
|
204
210
|
### `cache.invalidateByPrefix(prefix): Promise<void>`
|
|
@@ -316,6 +322,8 @@ await cache.warm(
|
|
|
316
322
|
|
|
317
323
|
Returns a scoped view with the same full API (`get`, `set`, `delete`, `clear`, `mget`, `wrap`, `warm`, `invalidateByTag`, `invalidateByPattern`, `getMetrics`). `clear()` only touches `prefix:*` keys, and namespace metrics are serialized per `CacheStack` instance so unrelated caches do not block each other while metrics are collected.
|
|
318
324
|
|
|
325
|
+
Namespace prefixes must be non-empty, at most 256 characters long, and free of control characters.
|
|
326
|
+
|
|
319
327
|
```ts
|
|
320
328
|
const users = cache.namespace('users')
|
|
321
329
|
const posts = cache.namespace('posts')
|
|
@@ -708,6 +716,8 @@ http.createServer(statsHandler).listen(9090)
|
|
|
708
716
|
// GET / → JSON stats
|
|
709
717
|
```
|
|
710
718
|
|
|
719
|
+
The built-in handler returns JSON with `Cache-Control: no-store` and `X-Content-Type-Options: nosniff` headers.
|
|
720
|
+
|
|
711
721
|
Or use the Fastify plugin:
|
|
712
722
|
|
|
713
723
|
```ts
|
|
@@ -763,9 +773,11 @@ cache.on('error', ({ event, context }) => logger.error(event, context))
|
|
|
763
773
|
|
|
764
774
|
---
|
|
765
775
|
|
|
766
|
-
##
|
|
776
|
+
## Integrations & tooling
|
|
767
777
|
|
|
768
|
-
###
|
|
778
|
+
### Framework integrations
|
|
779
|
+
|
|
780
|
+
#### Express
|
|
769
781
|
|
|
770
782
|
```ts
|
|
771
783
|
import { CacheStack, MemoryLayer, createExpressCacheMiddleware } from 'layercache'
|
|
@@ -785,7 +797,7 @@ app.get('/api/user/:id', createExpressCacheMiddleware(cache, {
|
|
|
785
797
|
}), handler)
|
|
786
798
|
```
|
|
787
799
|
|
|
788
|
-
|
|
800
|
+
#### tRPC
|
|
789
801
|
|
|
790
802
|
```ts
|
|
791
803
|
import { createTrpcCacheMiddleware } from 'layercache/integrations/trpc'
|
|
@@ -795,7 +807,7 @@ const cacheMiddleware = createTrpcCacheMiddleware(cache, 'trpc', { ttl: 60 })
|
|
|
795
807
|
export const cachedProcedure = t.procedure.use(cacheMiddleware)
|
|
796
808
|
```
|
|
797
809
|
|
|
798
|
-
|
|
810
|
+
#### GraphQL
|
|
799
811
|
|
|
800
812
|
```ts
|
|
801
813
|
import { cacheGraphqlResolver } from 'layercache/integrations/graphql'
|
|
@@ -812,7 +824,7 @@ const resolvers = {
|
|
|
812
824
|
|
|
813
825
|
---
|
|
814
826
|
|
|
815
|
-
|
|
827
|
+
### Admin CLI
|
|
816
828
|
|
|
817
829
|
Inspect and manage a Redis-backed cache without writing code.
|
|
818
830
|
|
|
@@ -1046,15 +1058,20 @@ new CacheStack([...], {
|
|
|
1046
1058
|
|
|
1047
1059
|
## Contributing
|
|
1048
1060
|
|
|
1061
|
+
Contributions are welcome, whether that means bug fixes, documentation improvements, performance work, new adapters, or issue reports.
|
|
1062
|
+
|
|
1049
1063
|
```bash
|
|
1050
1064
|
git clone https://github.com/flyingsquirrel0419/layercache
|
|
1051
1065
|
cd layercache
|
|
1052
1066
|
npm install
|
|
1067
|
+
npm run lint
|
|
1053
1068
|
npm test # vitest
|
|
1054
1069
|
npm run build:all # esm + cjs + nestjs package
|
|
1055
1070
|
```
|
|
1056
1071
|
|
|
1057
|
-
|
|
1072
|
+
- Read the [contribution guide](./CONTRIBUTING.md) before opening a PR.
|
|
1073
|
+
- Participation in the project is covered by the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
|
1074
|
+
- If you are filing an issue, include reproduction steps, expected behavior, and runtime details when relevant.
|
|
1058
1075
|
|
|
1059
1076
|
---
|
|
1060
1077
|
|
|
@@ -185,6 +185,7 @@ var MemoryLayer = class {
|
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
// src/invalidation/TagIndex.ts
|
|
188
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
188
189
|
var TagIndex = class {
|
|
189
190
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
190
191
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -239,7 +240,7 @@ var TagIndex = class {
|
|
|
239
240
|
}
|
|
240
241
|
async matchPattern(pattern) {
|
|
241
242
|
const matches = /* @__PURE__ */ new Set();
|
|
242
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
243
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
243
244
|
return [...matches];
|
|
244
245
|
}
|
|
245
246
|
async clear() {
|
|
@@ -291,7 +292,10 @@ var TagIndex = class {
|
|
|
291
292
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
|
-
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
295
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
296
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
295
299
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
296
300
|
if (visited.has(stateKey)) {
|
|
297
301
|
return;
|
|
@@ -308,21 +312,37 @@ var TagIndex = class {
|
|
|
308
312
|
return;
|
|
309
313
|
}
|
|
310
314
|
if (patternChar === "*") {
|
|
311
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
315
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
312
316
|
for (const [character, child2] of node.children) {
|
|
313
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
317
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
314
318
|
}
|
|
315
319
|
return;
|
|
316
320
|
}
|
|
317
321
|
if (patternChar === "?") {
|
|
318
322
|
for (const [character, child2] of node.children) {
|
|
319
|
-
this.collectPatternMatches(
|
|
323
|
+
this.collectPatternMatches(
|
|
324
|
+
child2,
|
|
325
|
+
`${prefix}${character}`,
|
|
326
|
+
pattern,
|
|
327
|
+
patternIndex + 1,
|
|
328
|
+
matches,
|
|
329
|
+
visited,
|
|
330
|
+
depth + 1
|
|
331
|
+
);
|
|
320
332
|
}
|
|
321
333
|
return;
|
|
322
334
|
}
|
|
323
335
|
const child = node.children.get(patternChar);
|
|
324
336
|
if (child) {
|
|
325
|
-
this.collectPatternMatches(
|
|
337
|
+
this.collectPatternMatches(
|
|
338
|
+
child,
|
|
339
|
+
`${prefix}${patternChar}`,
|
|
340
|
+
pattern,
|
|
341
|
+
patternIndex + 1,
|
|
342
|
+
matches,
|
|
343
|
+
visited,
|
|
344
|
+
depth + 1
|
|
345
|
+
);
|
|
326
346
|
}
|
|
327
347
|
}
|
|
328
348
|
pruneKnownKeysIfNeeded() {
|
|
@@ -423,7 +443,7 @@ function normalizeUrl(url) {
|
|
|
423
443
|
try {
|
|
424
444
|
const parsed = new URL(url, "http://localhost");
|
|
425
445
|
parsed.searchParams.sort();
|
|
426
|
-
return
|
|
446
|
+
return parsed.pathname + parsed.search;
|
|
427
447
|
} catch {
|
|
428
448
|
return url;
|
|
429
449
|
}
|
package/dist/cli.cjs
CHANGED
|
@@ -281,10 +281,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
281
281
|
}
|
|
282
282
|
const redisUrl = validateRedisUrl(args.redisUrl);
|
|
283
283
|
if (!redisUrl) {
|
|
284
|
-
process.stderr.write(
|
|
285
|
-
`Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
|
|
286
|
-
`
|
|
287
|
-
);
|
|
284
|
+
process.stderr.write("Error: invalid Redis URL. Expected format: redis://[user:password@]host[:port][/db]\n");
|
|
288
285
|
process.exitCode = 1;
|
|
289
286
|
return;
|
|
290
287
|
}
|
|
@@ -296,7 +293,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
296
293
|
try {
|
|
297
294
|
await redis.connect().catch((error) => {
|
|
298
295
|
const message = error instanceof Error ? error.message : String(error);
|
|
299
|
-
throw new Error(`Failed to connect to Redis
|
|
296
|
+
throw new Error(`Failed to connect to Redis: ${message}`);
|
|
300
297
|
});
|
|
301
298
|
if (args.command === "stats") {
|
|
302
299
|
const keys = await scanKeys(redis, args.pattern ?? "*");
|
|
@@ -451,17 +448,6 @@ function summarizeInspectableValue(value) {
|
|
|
451
448
|
}
|
|
452
449
|
return value;
|
|
453
450
|
}
|
|
454
|
-
function maskRedisUrl(url) {
|
|
455
|
-
try {
|
|
456
|
-
const parsed = new URL(url);
|
|
457
|
-
if (parsed.password) {
|
|
458
|
-
parsed.password = "***";
|
|
459
|
-
}
|
|
460
|
-
return parsed.toString();
|
|
461
|
-
} catch {
|
|
462
|
-
return url.replace(/:([^@/]+)@/, ":***@");
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
451
|
if (process.argv[1]?.includes("cli.")) {
|
|
466
452
|
void main();
|
|
467
453
|
}
|
package/dist/cli.js
CHANGED
|
@@ -19,10 +19,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
19
19
|
}
|
|
20
20
|
const redisUrl = validateRedisUrl(args.redisUrl);
|
|
21
21
|
if (!redisUrl) {
|
|
22
|
-
process.stderr.write(
|
|
23
|
-
`Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
|
|
24
|
-
`
|
|
25
|
-
);
|
|
22
|
+
process.stderr.write("Error: invalid Redis URL. Expected format: redis://[user:password@]host[:port][/db]\n");
|
|
26
23
|
process.exitCode = 1;
|
|
27
24
|
return;
|
|
28
25
|
}
|
|
@@ -34,7 +31,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
34
31
|
try {
|
|
35
32
|
await redis.connect().catch((error) => {
|
|
36
33
|
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
-
throw new Error(`Failed to connect to Redis
|
|
34
|
+
throw new Error(`Failed to connect to Redis: ${message}`);
|
|
38
35
|
});
|
|
39
36
|
if (args.command === "stats") {
|
|
40
37
|
const keys = await scanKeys(redis, args.pattern ?? "*");
|
|
@@ -189,17 +186,6 @@ function summarizeInspectableValue(value) {
|
|
|
189
186
|
}
|
|
190
187
|
return value;
|
|
191
188
|
}
|
|
192
|
-
function maskRedisUrl(url) {
|
|
193
|
-
try {
|
|
194
|
-
const parsed = new URL(url);
|
|
195
|
-
if (parsed.password) {
|
|
196
|
-
parsed.password = "***";
|
|
197
|
-
}
|
|
198
|
-
return parsed.toString();
|
|
199
|
-
} catch {
|
|
200
|
-
return url.replace(/:([^@/]+)@/, ":***@");
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
189
|
if (process.argv[1]?.includes("cli.")) {
|
|
204
190
|
void main();
|
|
205
191
|
}
|
|
@@ -518,6 +518,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
518
518
|
private unsubscribeInvalidation?;
|
|
519
519
|
private readonly logger;
|
|
520
520
|
private readonly tagIndex;
|
|
521
|
+
private readonly keyDiscovery;
|
|
521
522
|
private readonly fetchRateLimiter;
|
|
522
523
|
private readonly snapshotSerializer;
|
|
523
524
|
private readonly backgroundRefreshes;
|
|
@@ -640,8 +641,6 @@ declare class CacheStack extends EventEmitter {
|
|
|
640
641
|
private sleep;
|
|
641
642
|
private withTimeout;
|
|
642
643
|
private shouldBroadcastL1Invalidation;
|
|
643
|
-
private collectKeysWithPrefix;
|
|
644
|
-
private collectKeysMatchingPattern;
|
|
645
644
|
private shouldCleanupGenerations;
|
|
646
645
|
private generationCleanupBatchSize;
|
|
647
646
|
private scheduleGenerationCleanup;
|
|
@@ -664,6 +663,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
664
663
|
private validateRateLimitOptions;
|
|
665
664
|
private validateNonNegativeNumber;
|
|
666
665
|
private validateCacheKey;
|
|
666
|
+
private validatePattern;
|
|
667
667
|
private validateTtlPolicy;
|
|
668
668
|
private assertActive;
|
|
669
669
|
private awaitStartup;
|
|
@@ -518,6 +518,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
518
518
|
private unsubscribeInvalidation?;
|
|
519
519
|
private readonly logger;
|
|
520
520
|
private readonly tagIndex;
|
|
521
|
+
private readonly keyDiscovery;
|
|
521
522
|
private readonly fetchRateLimiter;
|
|
522
523
|
private readonly snapshotSerializer;
|
|
523
524
|
private readonly backgroundRefreshes;
|
|
@@ -640,8 +641,6 @@ declare class CacheStack extends EventEmitter {
|
|
|
640
641
|
private sleep;
|
|
641
642
|
private withTimeout;
|
|
642
643
|
private shouldBroadcastL1Invalidation;
|
|
643
|
-
private collectKeysWithPrefix;
|
|
644
|
-
private collectKeysMatchingPattern;
|
|
645
644
|
private shouldCleanupGenerations;
|
|
646
645
|
private generationCleanupBatchSize;
|
|
647
646
|
private scheduleGenerationCleanup;
|
|
@@ -664,6 +663,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
664
663
|
private validateRateLimitOptions;
|
|
665
664
|
private validateNonNegativeNumber;
|
|
666
665
|
private validateCacheKey;
|
|
666
|
+
private validatePattern;
|
|
667
667
|
private validateTtlPolicy;
|
|
668
668
|
private assertActive;
|
|
669
669
|
private awaitStartup;
|
package/dist/edge.cjs
CHANGED
|
@@ -295,6 +295,7 @@ var PatternMatcher = class _PatternMatcher {
|
|
|
295
295
|
};
|
|
296
296
|
|
|
297
297
|
// src/invalidation/TagIndex.ts
|
|
298
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
298
299
|
var TagIndex = class {
|
|
299
300
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
300
301
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -349,7 +350,7 @@ var TagIndex = class {
|
|
|
349
350
|
}
|
|
350
351
|
async matchPattern(pattern) {
|
|
351
352
|
const matches = /* @__PURE__ */ new Set();
|
|
352
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
353
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
353
354
|
return [...matches];
|
|
354
355
|
}
|
|
355
356
|
async clear() {
|
|
@@ -401,7 +402,10 @@ var TagIndex = class {
|
|
|
401
402
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
402
403
|
}
|
|
403
404
|
}
|
|
404
|
-
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
405
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
406
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
405
409
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
406
410
|
if (visited.has(stateKey)) {
|
|
407
411
|
return;
|
|
@@ -418,21 +422,37 @@ var TagIndex = class {
|
|
|
418
422
|
return;
|
|
419
423
|
}
|
|
420
424
|
if (patternChar === "*") {
|
|
421
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
425
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
422
426
|
for (const [character, child2] of node.children) {
|
|
423
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
427
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
424
428
|
}
|
|
425
429
|
return;
|
|
426
430
|
}
|
|
427
431
|
if (patternChar === "?") {
|
|
428
432
|
for (const [character, child2] of node.children) {
|
|
429
|
-
this.collectPatternMatches(
|
|
433
|
+
this.collectPatternMatches(
|
|
434
|
+
child2,
|
|
435
|
+
`${prefix}${character}`,
|
|
436
|
+
pattern,
|
|
437
|
+
patternIndex + 1,
|
|
438
|
+
matches,
|
|
439
|
+
visited,
|
|
440
|
+
depth + 1
|
|
441
|
+
);
|
|
430
442
|
}
|
|
431
443
|
return;
|
|
432
444
|
}
|
|
433
445
|
const child = node.children.get(patternChar);
|
|
434
446
|
if (child) {
|
|
435
|
-
this.collectPatternMatches(
|
|
447
|
+
this.collectPatternMatches(
|
|
448
|
+
child,
|
|
449
|
+
`${prefix}${patternChar}`,
|
|
450
|
+
pattern,
|
|
451
|
+
patternIndex + 1,
|
|
452
|
+
matches,
|
|
453
|
+
visited,
|
|
454
|
+
depth + 1
|
|
455
|
+
);
|
|
436
456
|
}
|
|
437
457
|
}
|
|
438
458
|
pruneKnownKeysIfNeeded() {
|
|
@@ -533,7 +553,7 @@ function normalizeUrl(url) {
|
|
|
533
553
|
try {
|
|
534
554
|
const parsed = new URL(url, "http://localhost");
|
|
535
555
|
parsed.searchParams.sort();
|
|
536
|
-
return
|
|
556
|
+
return parsed.pathname + parsed.search;
|
|
537
557
|
} catch {
|
|
538
558
|
return url;
|
|
539
559
|
}
|
package/dist/edge.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.cjs';
|
|
2
2
|
import 'node:events';
|
package/dist/edge.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.js';
|
|
2
2
|
import 'node:events';
|