layercache 1.2.3 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -84
- package/dist/{edge-B_rUqDy6.d.cts → edge-Dw97n89L.d.cts} +1 -2
- package/dist/{edge-B_rUqDy6.d.ts → edge-Dw97n89L.d.ts} +1 -2
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +113 -95
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +65 -47
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +113 -95
- package/packages/nestjs/dist/index.d.cts +1 -2
- package/packages/nestjs/dist/index.d.ts +1 -2
- package/packages/nestjs/dist/index.js +113 -95
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`).
|
|
@@ -763,9 +767,11 @@ cache.on('error', ({ event, context }) => logger.error(event, context))
|
|
|
763
767
|
|
|
764
768
|
---
|
|
765
769
|
|
|
766
|
-
##
|
|
770
|
+
## Integrations & tooling
|
|
767
771
|
|
|
768
|
-
###
|
|
772
|
+
### Framework integrations
|
|
773
|
+
|
|
774
|
+
#### Express
|
|
769
775
|
|
|
770
776
|
```ts
|
|
771
777
|
import { CacheStack, MemoryLayer, createExpressCacheMiddleware } from 'layercache'
|
|
@@ -785,7 +791,7 @@ app.get('/api/user/:id', createExpressCacheMiddleware(cache, {
|
|
|
785
791
|
}), handler)
|
|
786
792
|
```
|
|
787
793
|
|
|
788
|
-
|
|
794
|
+
#### tRPC
|
|
789
795
|
|
|
790
796
|
```ts
|
|
791
797
|
import { createTrpcCacheMiddleware } from 'layercache/integrations/trpc'
|
|
@@ -795,7 +801,7 @@ const cacheMiddleware = createTrpcCacheMiddleware(cache, 'trpc', { ttl: 60 })
|
|
|
795
801
|
export const cachedProcedure = t.procedure.use(cacheMiddleware)
|
|
796
802
|
```
|
|
797
803
|
|
|
798
|
-
|
|
804
|
+
#### GraphQL
|
|
799
805
|
|
|
800
806
|
```ts
|
|
801
807
|
import { cacheGraphqlResolver } from 'layercache/integrations/graphql'
|
|
@@ -812,7 +818,7 @@ const resolvers = {
|
|
|
812
818
|
|
|
813
819
|
---
|
|
814
820
|
|
|
815
|
-
|
|
821
|
+
### Admin CLI
|
|
816
822
|
|
|
817
823
|
Inspect and manage a Redis-backed cache without writing code.
|
|
818
824
|
|
|
@@ -1046,15 +1052,20 @@ new CacheStack([...], {
|
|
|
1046
1052
|
|
|
1047
1053
|
## Contributing
|
|
1048
1054
|
|
|
1055
|
+
Contributions are welcome, whether that means bug fixes, documentation improvements, performance work, new adapters, or issue reports.
|
|
1056
|
+
|
|
1049
1057
|
```bash
|
|
1050
1058
|
git clone https://github.com/flyingsquirrel0419/layercache
|
|
1051
1059
|
cd layercache
|
|
1052
1060
|
npm install
|
|
1061
|
+
npm run lint
|
|
1053
1062
|
npm test # vitest
|
|
1054
1063
|
npm run build:all # esm + cjs + nestjs package
|
|
1055
1064
|
```
|
|
1056
1065
|
|
|
1057
|
-
|
|
1066
|
+
- Read the [contribution guide](./CONTRIBUTING.md) before opening a PR.
|
|
1067
|
+
- Participation in the project is covered by the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
|
1068
|
+
- If you are filing an issue, include reproduction steps, expected behavior, and runtime details when relevant.
|
|
1058
1069
|
|
|
1059
1070
|
---
|
|
1060
1071
|
|
|
@@ -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;
|
|
@@ -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;
|
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-Dw97n89L.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-Dw97n89L.js';
|
|
2
2
|
import 'node:events';
|
package/dist/index.cjs
CHANGED
|
@@ -306,6 +306,107 @@ function addMap(base, delta) {
|
|
|
306
306
|
return result;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// src/invalidation/PatternMatcher.ts
|
|
310
|
+
var PatternMatcher = class _PatternMatcher {
|
|
311
|
+
/**
|
|
312
|
+
* Tests whether a glob-style pattern matches a value.
|
|
313
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
314
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
315
|
+
* quadratic memory usage on long patterns/keys.
|
|
316
|
+
*/
|
|
317
|
+
static matches(pattern, value) {
|
|
318
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
322
|
+
*/
|
|
323
|
+
static matchLinear(pattern, value) {
|
|
324
|
+
let patternIndex = 0;
|
|
325
|
+
let valueIndex = 0;
|
|
326
|
+
let starIndex = -1;
|
|
327
|
+
let backtrackValueIndex = 0;
|
|
328
|
+
while (valueIndex < value.length) {
|
|
329
|
+
const patternChar = pattern[patternIndex];
|
|
330
|
+
const valueChar = value[valueIndex];
|
|
331
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
332
|
+
starIndex = patternIndex;
|
|
333
|
+
patternIndex += 1;
|
|
334
|
+
backtrackValueIndex = valueIndex;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
338
|
+
patternIndex += 1;
|
|
339
|
+
valueIndex += 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (starIndex !== -1) {
|
|
343
|
+
patternIndex = starIndex + 1;
|
|
344
|
+
backtrackValueIndex += 1;
|
|
345
|
+
valueIndex = backtrackValueIndex;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
351
|
+
patternIndex += 1;
|
|
352
|
+
}
|
|
353
|
+
return patternIndex === pattern.length;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/internal/CacheKeyDiscovery.ts
|
|
358
|
+
var CacheKeyDiscovery = class {
|
|
359
|
+
constructor(options) {
|
|
360
|
+
this.options = options;
|
|
361
|
+
}
|
|
362
|
+
options;
|
|
363
|
+
async collectKeysWithPrefix(prefix) {
|
|
364
|
+
const { tagIndex } = this.options;
|
|
365
|
+
const matches = new Set(
|
|
366
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
367
|
+
);
|
|
368
|
+
await Promise.all(
|
|
369
|
+
this.options.layers.map(async (layer) => {
|
|
370
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const keys = await layer.keys();
|
|
375
|
+
for (const key of keys) {
|
|
376
|
+
if (key.startsWith(prefix)) {
|
|
377
|
+
matches.add(key);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
return [...matches];
|
|
386
|
+
}
|
|
387
|
+
async collectKeysMatchingPattern(pattern) {
|
|
388
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
389
|
+
await Promise.all(
|
|
390
|
+
this.options.layers.map(async (layer) => {
|
|
391
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const keys = await layer.keys();
|
|
396
|
+
for (const key of keys) {
|
|
397
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
398
|
+
matches.add(key);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
return [...matches];
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
309
410
|
// src/internal/CircuitBreakerManager.ts
|
|
310
411
|
var CircuitBreakerManager = class {
|
|
311
412
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -905,54 +1006,6 @@ var TtlResolver = class {
|
|
|
905
1006
|
}
|
|
906
1007
|
};
|
|
907
1008
|
|
|
908
|
-
// src/invalidation/PatternMatcher.ts
|
|
909
|
-
var PatternMatcher = class _PatternMatcher {
|
|
910
|
-
/**
|
|
911
|
-
* Tests whether a glob-style pattern matches a value.
|
|
912
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
913
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
914
|
-
* quadratic memory usage on long patterns/keys.
|
|
915
|
-
*/
|
|
916
|
-
static matches(pattern, value) {
|
|
917
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Linear-time glob matching with O(1) extra memory.
|
|
921
|
-
*/
|
|
922
|
-
static matchLinear(pattern, value) {
|
|
923
|
-
let patternIndex = 0;
|
|
924
|
-
let valueIndex = 0;
|
|
925
|
-
let starIndex = -1;
|
|
926
|
-
let backtrackValueIndex = 0;
|
|
927
|
-
while (valueIndex < value.length) {
|
|
928
|
-
const patternChar = pattern[patternIndex];
|
|
929
|
-
const valueChar = value[valueIndex];
|
|
930
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
931
|
-
starIndex = patternIndex;
|
|
932
|
-
patternIndex += 1;
|
|
933
|
-
backtrackValueIndex = valueIndex;
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
937
|
-
patternIndex += 1;
|
|
938
|
-
valueIndex += 1;
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
if (starIndex !== -1) {
|
|
942
|
-
patternIndex = starIndex + 1;
|
|
943
|
-
backtrackValueIndex += 1;
|
|
944
|
-
valueIndex = backtrackValueIndex;
|
|
945
|
-
continue;
|
|
946
|
-
}
|
|
947
|
-
return false;
|
|
948
|
-
}
|
|
949
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
950
|
-
patternIndex += 1;
|
|
951
|
-
}
|
|
952
|
-
return patternIndex === pattern.length;
|
|
953
|
-
}
|
|
954
|
-
};
|
|
955
|
-
|
|
956
1009
|
// src/invalidation/TagIndex.ts
|
|
957
1010
|
var TagIndex = class {
|
|
958
1011
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
@@ -1282,6 +1335,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1282
1335
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1283
1336
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1284
1337
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1338
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
1339
|
+
layers: this.layers,
|
|
1340
|
+
tagIndex: this.tagIndex,
|
|
1341
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1342
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1343
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1285
1346
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1286
1347
|
this.logger.warn?.(
|
|
1287
1348
|
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
@@ -1309,6 +1370,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1309
1370
|
unsubscribeInvalidation;
|
|
1310
1371
|
logger;
|
|
1311
1372
|
tagIndex;
|
|
1373
|
+
keyDiscovery;
|
|
1312
1374
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1313
1375
|
snapshotSerializer = new JsonSerializer();
|
|
1314
1376
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
@@ -1659,14 +1721,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1659
1721
|
}
|
|
1660
1722
|
async invalidateByPattern(pattern) {
|
|
1661
1723
|
await this.awaitStartup("invalidateByPattern");
|
|
1662
|
-
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1724
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1663
1725
|
await this.deleteKeys(keys);
|
|
1664
1726
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1665
1727
|
}
|
|
1666
1728
|
async invalidateByPrefix(prefix) {
|
|
1667
1729
|
await this.awaitStartup("invalidateByPrefix");
|
|
1668
1730
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1669
|
-
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1731
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1670
1732
|
await this.deleteKeys(keys);
|
|
1671
1733
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1672
1734
|
}
|
|
@@ -2279,50 +2341,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2279
2341
|
shouldBroadcastL1Invalidation() {
|
|
2280
2342
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2281
2343
|
}
|
|
2282
|
-
async collectKeysWithPrefix(prefix) {
|
|
2283
|
-
const matches = new Set(
|
|
2284
|
-
this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
|
|
2285
|
-
);
|
|
2286
|
-
await Promise.all(
|
|
2287
|
-
this.layers.map(async (layer) => {
|
|
2288
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
try {
|
|
2292
|
-
const keys = await layer.keys();
|
|
2293
|
-
for (const key of keys) {
|
|
2294
|
-
if (key.startsWith(prefix)) {
|
|
2295
|
-
matches.add(key);
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
} catch (error) {
|
|
2299
|
-
await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
2300
|
-
}
|
|
2301
|
-
})
|
|
2302
|
-
);
|
|
2303
|
-
return [...matches];
|
|
2304
|
-
}
|
|
2305
|
-
async collectKeysMatchingPattern(pattern) {
|
|
2306
|
-
const matches = new Set(await this.tagIndex.matchPattern(pattern));
|
|
2307
|
-
await Promise.all(
|
|
2308
|
-
this.layers.map(async (layer) => {
|
|
2309
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2310
|
-
return;
|
|
2311
|
-
}
|
|
2312
|
-
try {
|
|
2313
|
-
const keys = await layer.keys();
|
|
2314
|
-
for (const key of keys) {
|
|
2315
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
2316
|
-
matches.add(key);
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
} catch (error) {
|
|
2320
|
-
await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
2321
|
-
}
|
|
2322
|
-
})
|
|
2323
|
-
);
|
|
2324
|
-
return [...matches];
|
|
2325
|
-
}
|
|
2326
2344
|
shouldCleanupGenerations() {
|
|
2327
2345
|
return Boolean(this.options.generationCleanup);
|
|
2328
2346
|
}
|
|
@@ -2345,7 +2363,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2345
2363
|
}
|
|
2346
2364
|
async cleanupGeneration(generation) {
|
|
2347
2365
|
const prefix = `v${generation}:`;
|
|
2348
|
-
const keys = await this.collectKeysWithPrefix(prefix);
|
|
2366
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2349
2367
|
if (keys.length === 0) {
|
|
2350
2368
|
return;
|
|
2351
2369
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-
|
|
2
|
-
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-Dw97n89L.cjs';
|
|
2
|
+
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-Dw97n89L.cjs';
|
|
3
3
|
import Redis from 'ioredis';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-
|
|
2
|
-
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-Dw97n89L.js';
|
|
2
|
+
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-Dw97n89L.js';
|
|
3
3
|
import Redis from 'ioredis';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
|
package/dist/index.js
CHANGED
|
@@ -266,6 +266,59 @@ function addMap(base, delta) {
|
|
|
266
266
|
return result;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// src/internal/CacheKeyDiscovery.ts
|
|
270
|
+
var CacheKeyDiscovery = class {
|
|
271
|
+
constructor(options) {
|
|
272
|
+
this.options = options;
|
|
273
|
+
}
|
|
274
|
+
options;
|
|
275
|
+
async collectKeysWithPrefix(prefix) {
|
|
276
|
+
const { tagIndex } = this.options;
|
|
277
|
+
const matches = new Set(
|
|
278
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
279
|
+
);
|
|
280
|
+
await Promise.all(
|
|
281
|
+
this.options.layers.map(async (layer) => {
|
|
282
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const keys = await layer.keys();
|
|
287
|
+
for (const key of keys) {
|
|
288
|
+
if (key.startsWith(prefix)) {
|
|
289
|
+
matches.add(key);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
return [...matches];
|
|
298
|
+
}
|
|
299
|
+
async collectKeysMatchingPattern(pattern) {
|
|
300
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
301
|
+
await Promise.all(
|
|
302
|
+
this.options.layers.map(async (layer) => {
|
|
303
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const keys = await layer.keys();
|
|
308
|
+
for (const key of keys) {
|
|
309
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
310
|
+
matches.add(key);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
return [...matches];
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
269
322
|
// src/internal/CircuitBreakerManager.ts
|
|
270
323
|
var CircuitBreakerManager = class {
|
|
271
324
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -867,6 +920,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
867
920
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
868
921
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
869
922
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
923
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
924
|
+
layers: this.layers,
|
|
925
|
+
tagIndex: this.tagIndex,
|
|
926
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
927
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
928
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
870
931
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
871
932
|
this.logger.warn?.(
|
|
872
933
|
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
@@ -894,6 +955,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
894
955
|
unsubscribeInvalidation;
|
|
895
956
|
logger;
|
|
896
957
|
tagIndex;
|
|
958
|
+
keyDiscovery;
|
|
897
959
|
fetchRateLimiter = new FetchRateLimiter();
|
|
898
960
|
snapshotSerializer = new JsonSerializer();
|
|
899
961
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
@@ -1244,14 +1306,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1244
1306
|
}
|
|
1245
1307
|
async invalidateByPattern(pattern) {
|
|
1246
1308
|
await this.awaitStartup("invalidateByPattern");
|
|
1247
|
-
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1309
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1248
1310
|
await this.deleteKeys(keys);
|
|
1249
1311
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1250
1312
|
}
|
|
1251
1313
|
async invalidateByPrefix(prefix) {
|
|
1252
1314
|
await this.awaitStartup("invalidateByPrefix");
|
|
1253
1315
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1254
|
-
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1316
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1255
1317
|
await this.deleteKeys(keys);
|
|
1256
1318
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1257
1319
|
}
|
|
@@ -1864,50 +1926,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
1864
1926
|
shouldBroadcastL1Invalidation() {
|
|
1865
1927
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
1866
1928
|
}
|
|
1867
|
-
async collectKeysWithPrefix(prefix) {
|
|
1868
|
-
const matches = new Set(
|
|
1869
|
-
this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
|
|
1870
|
-
);
|
|
1871
|
-
await Promise.all(
|
|
1872
|
-
this.layers.map(async (layer) => {
|
|
1873
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
try {
|
|
1877
|
-
const keys = await layer.keys();
|
|
1878
|
-
for (const key of keys) {
|
|
1879
|
-
if (key.startsWith(prefix)) {
|
|
1880
|
-
matches.add(key);
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
} catch (error) {
|
|
1884
|
-
await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
1885
|
-
}
|
|
1886
|
-
})
|
|
1887
|
-
);
|
|
1888
|
-
return [...matches];
|
|
1889
|
-
}
|
|
1890
|
-
async collectKeysMatchingPattern(pattern) {
|
|
1891
|
-
const matches = new Set(await this.tagIndex.matchPattern(pattern));
|
|
1892
|
-
await Promise.all(
|
|
1893
|
-
this.layers.map(async (layer) => {
|
|
1894
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
try {
|
|
1898
|
-
const keys = await layer.keys();
|
|
1899
|
-
for (const key of keys) {
|
|
1900
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
1901
|
-
matches.add(key);
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
} catch (error) {
|
|
1905
|
-
await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
1906
|
-
}
|
|
1907
|
-
})
|
|
1908
|
-
);
|
|
1909
|
-
return [...matches];
|
|
1910
|
-
}
|
|
1911
1929
|
shouldCleanupGenerations() {
|
|
1912
1930
|
return Boolean(this.options.generationCleanup);
|
|
1913
1931
|
}
|
|
@@ -1930,7 +1948,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1930
1948
|
}
|
|
1931
1949
|
async cleanupGeneration(generation) {
|
|
1932
1950
|
const prefix = `v${generation}:`;
|
|
1933
|
-
const keys = await this.collectKeysWithPrefix(prefix);
|
|
1951
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
1934
1952
|
if (keys.length === 0) {
|
|
1935
1953
|
return;
|
|
1936
1954
|
}
|
package/package.json
CHANGED
|
@@ -504,6 +504,107 @@ function addMap(base, delta) {
|
|
|
504
504
|
return result;
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
508
|
+
var PatternMatcher = class _PatternMatcher {
|
|
509
|
+
/**
|
|
510
|
+
* Tests whether a glob-style pattern matches a value.
|
|
511
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
512
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
513
|
+
* quadratic memory usage on long patterns/keys.
|
|
514
|
+
*/
|
|
515
|
+
static matches(pattern, value) {
|
|
516
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
520
|
+
*/
|
|
521
|
+
static matchLinear(pattern, value) {
|
|
522
|
+
let patternIndex = 0;
|
|
523
|
+
let valueIndex = 0;
|
|
524
|
+
let starIndex = -1;
|
|
525
|
+
let backtrackValueIndex = 0;
|
|
526
|
+
while (valueIndex < value.length) {
|
|
527
|
+
const patternChar = pattern[patternIndex];
|
|
528
|
+
const valueChar = value[valueIndex];
|
|
529
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
530
|
+
starIndex = patternIndex;
|
|
531
|
+
patternIndex += 1;
|
|
532
|
+
backtrackValueIndex = valueIndex;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
536
|
+
patternIndex += 1;
|
|
537
|
+
valueIndex += 1;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (starIndex !== -1) {
|
|
541
|
+
patternIndex = starIndex + 1;
|
|
542
|
+
backtrackValueIndex += 1;
|
|
543
|
+
valueIndex = backtrackValueIndex;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
549
|
+
patternIndex += 1;
|
|
550
|
+
}
|
|
551
|
+
return patternIndex === pattern.length;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// ../../src/internal/CacheKeyDiscovery.ts
|
|
556
|
+
var CacheKeyDiscovery = class {
|
|
557
|
+
constructor(options) {
|
|
558
|
+
this.options = options;
|
|
559
|
+
}
|
|
560
|
+
options;
|
|
561
|
+
async collectKeysWithPrefix(prefix) {
|
|
562
|
+
const { tagIndex } = this.options;
|
|
563
|
+
const matches = new Set(
|
|
564
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
565
|
+
);
|
|
566
|
+
await Promise.all(
|
|
567
|
+
this.options.layers.map(async (layer) => {
|
|
568
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const keys = await layer.keys();
|
|
573
|
+
for (const key of keys) {
|
|
574
|
+
if (key.startsWith(prefix)) {
|
|
575
|
+
matches.add(key);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
);
|
|
583
|
+
return [...matches];
|
|
584
|
+
}
|
|
585
|
+
async collectKeysMatchingPattern(pattern) {
|
|
586
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
587
|
+
await Promise.all(
|
|
588
|
+
this.options.layers.map(async (layer) => {
|
|
589
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
const keys = await layer.keys();
|
|
594
|
+
for (const key of keys) {
|
|
595
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
596
|
+
matches.add(key);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} catch (error) {
|
|
600
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
return [...matches];
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
507
608
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
508
609
|
var CircuitBreakerManager = class {
|
|
509
610
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -1103,54 +1204,6 @@ var TtlResolver = class {
|
|
|
1103
1204
|
}
|
|
1104
1205
|
};
|
|
1105
1206
|
|
|
1106
|
-
// ../../src/invalidation/PatternMatcher.ts
|
|
1107
|
-
var PatternMatcher = class _PatternMatcher {
|
|
1108
|
-
/**
|
|
1109
|
-
* Tests whether a glob-style pattern matches a value.
|
|
1110
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
1111
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
1112
|
-
* quadratic memory usage on long patterns/keys.
|
|
1113
|
-
*/
|
|
1114
|
-
static matches(pattern, value) {
|
|
1115
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Linear-time glob matching with O(1) extra memory.
|
|
1119
|
-
*/
|
|
1120
|
-
static matchLinear(pattern, value) {
|
|
1121
|
-
let patternIndex = 0;
|
|
1122
|
-
let valueIndex = 0;
|
|
1123
|
-
let starIndex = -1;
|
|
1124
|
-
let backtrackValueIndex = 0;
|
|
1125
|
-
while (valueIndex < value.length) {
|
|
1126
|
-
const patternChar = pattern[patternIndex];
|
|
1127
|
-
const valueChar = value[valueIndex];
|
|
1128
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
1129
|
-
starIndex = patternIndex;
|
|
1130
|
-
patternIndex += 1;
|
|
1131
|
-
backtrackValueIndex = valueIndex;
|
|
1132
|
-
continue;
|
|
1133
|
-
}
|
|
1134
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
1135
|
-
patternIndex += 1;
|
|
1136
|
-
valueIndex += 1;
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
if (starIndex !== -1) {
|
|
1140
|
-
patternIndex = starIndex + 1;
|
|
1141
|
-
backtrackValueIndex += 1;
|
|
1142
|
-
valueIndex = backtrackValueIndex;
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
return false;
|
|
1146
|
-
}
|
|
1147
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
1148
|
-
patternIndex += 1;
|
|
1149
|
-
}
|
|
1150
|
-
return patternIndex === pattern.length;
|
|
1151
|
-
}
|
|
1152
|
-
};
|
|
1153
|
-
|
|
1154
1207
|
// ../../src/invalidation/TagIndex.ts
|
|
1155
1208
|
var TagIndex = class {
|
|
1156
1209
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
@@ -1479,6 +1532,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1479
1532
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1480
1533
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1481
1534
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1535
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
1536
|
+
layers: this.layers,
|
|
1537
|
+
tagIndex: this.tagIndex,
|
|
1538
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1539
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1540
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1482
1543
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1483
1544
|
this.logger.warn?.(
|
|
1484
1545
|
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
@@ -1506,6 +1567,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1506
1567
|
unsubscribeInvalidation;
|
|
1507
1568
|
logger;
|
|
1508
1569
|
tagIndex;
|
|
1570
|
+
keyDiscovery;
|
|
1509
1571
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1510
1572
|
snapshotSerializer = new JsonSerializer();
|
|
1511
1573
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
@@ -1856,14 +1918,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1856
1918
|
}
|
|
1857
1919
|
async invalidateByPattern(pattern) {
|
|
1858
1920
|
await this.awaitStartup("invalidateByPattern");
|
|
1859
|
-
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1921
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1860
1922
|
await this.deleteKeys(keys);
|
|
1861
1923
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1862
1924
|
}
|
|
1863
1925
|
async invalidateByPrefix(prefix) {
|
|
1864
1926
|
await this.awaitStartup("invalidateByPrefix");
|
|
1865
1927
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1866
|
-
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1928
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1867
1929
|
await this.deleteKeys(keys);
|
|
1868
1930
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1869
1931
|
}
|
|
@@ -2476,50 +2538,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2476
2538
|
shouldBroadcastL1Invalidation() {
|
|
2477
2539
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2478
2540
|
}
|
|
2479
|
-
async collectKeysWithPrefix(prefix) {
|
|
2480
|
-
const matches = new Set(
|
|
2481
|
-
this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
|
|
2482
|
-
);
|
|
2483
|
-
await Promise.all(
|
|
2484
|
-
this.layers.map(async (layer) => {
|
|
2485
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2486
|
-
return;
|
|
2487
|
-
}
|
|
2488
|
-
try {
|
|
2489
|
-
const keys = await layer.keys();
|
|
2490
|
-
for (const key of keys) {
|
|
2491
|
-
if (key.startsWith(prefix)) {
|
|
2492
|
-
matches.add(key);
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
} catch (error) {
|
|
2496
|
-
await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
2497
|
-
}
|
|
2498
|
-
})
|
|
2499
|
-
);
|
|
2500
|
-
return [...matches];
|
|
2501
|
-
}
|
|
2502
|
-
async collectKeysMatchingPattern(pattern) {
|
|
2503
|
-
const matches = new Set(await this.tagIndex.matchPattern(pattern));
|
|
2504
|
-
await Promise.all(
|
|
2505
|
-
this.layers.map(async (layer) => {
|
|
2506
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2507
|
-
return;
|
|
2508
|
-
}
|
|
2509
|
-
try {
|
|
2510
|
-
const keys = await layer.keys();
|
|
2511
|
-
for (const key of keys) {
|
|
2512
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
2513
|
-
matches.add(key);
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
} catch (error) {
|
|
2517
|
-
await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
2518
|
-
}
|
|
2519
|
-
})
|
|
2520
|
-
);
|
|
2521
|
-
return [...matches];
|
|
2522
|
-
}
|
|
2523
2541
|
shouldCleanupGenerations() {
|
|
2524
2542
|
return Boolean(this.options.generationCleanup);
|
|
2525
2543
|
}
|
|
@@ -2542,7 +2560,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2542
2560
|
}
|
|
2543
2561
|
async cleanupGeneration(generation) {
|
|
2544
2562
|
const prefix = `v${generation}:`;
|
|
2545
|
-
const keys = await this.collectKeysWithPrefix(prefix);
|
|
2563
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2546
2564
|
if (keys.length === 0) {
|
|
2547
2565
|
return;
|
|
2548
2566
|
}
|
|
@@ -410,6 +410,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
410
410
|
private unsubscribeInvalidation?;
|
|
411
411
|
private readonly logger;
|
|
412
412
|
private readonly tagIndex;
|
|
413
|
+
private readonly keyDiscovery;
|
|
413
414
|
private readonly fetchRateLimiter;
|
|
414
415
|
private readonly snapshotSerializer;
|
|
415
416
|
private readonly backgroundRefreshes;
|
|
@@ -532,8 +533,6 @@ declare class CacheStack extends EventEmitter {
|
|
|
532
533
|
private sleep;
|
|
533
534
|
private withTimeout;
|
|
534
535
|
private shouldBroadcastL1Invalidation;
|
|
535
|
-
private collectKeysWithPrefix;
|
|
536
|
-
private collectKeysMatchingPattern;
|
|
537
536
|
private shouldCleanupGenerations;
|
|
538
537
|
private generationCleanupBatchSize;
|
|
539
538
|
private scheduleGenerationCleanup;
|
|
@@ -410,6 +410,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
410
410
|
private unsubscribeInvalidation?;
|
|
411
411
|
private readonly logger;
|
|
412
412
|
private readonly tagIndex;
|
|
413
|
+
private readonly keyDiscovery;
|
|
413
414
|
private readonly fetchRateLimiter;
|
|
414
415
|
private readonly snapshotSerializer;
|
|
415
416
|
private readonly backgroundRefreshes;
|
|
@@ -532,8 +533,6 @@ declare class CacheStack extends EventEmitter {
|
|
|
532
533
|
private sleep;
|
|
533
534
|
private withTimeout;
|
|
534
535
|
private shouldBroadcastL1Invalidation;
|
|
535
|
-
private collectKeysWithPrefix;
|
|
536
|
-
private collectKeysMatchingPattern;
|
|
537
536
|
private shouldCleanupGenerations;
|
|
538
537
|
private generationCleanupBatchSize;
|
|
539
538
|
private scheduleGenerationCleanup;
|
|
@@ -468,6 +468,107 @@ function addMap(base, delta) {
|
|
|
468
468
|
return result;
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
472
|
+
var PatternMatcher = class _PatternMatcher {
|
|
473
|
+
/**
|
|
474
|
+
* Tests whether a glob-style pattern matches a value.
|
|
475
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
476
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
477
|
+
* quadratic memory usage on long patterns/keys.
|
|
478
|
+
*/
|
|
479
|
+
static matches(pattern, value) {
|
|
480
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
484
|
+
*/
|
|
485
|
+
static matchLinear(pattern, value) {
|
|
486
|
+
let patternIndex = 0;
|
|
487
|
+
let valueIndex = 0;
|
|
488
|
+
let starIndex = -1;
|
|
489
|
+
let backtrackValueIndex = 0;
|
|
490
|
+
while (valueIndex < value.length) {
|
|
491
|
+
const patternChar = pattern[patternIndex];
|
|
492
|
+
const valueChar = value[valueIndex];
|
|
493
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
494
|
+
starIndex = patternIndex;
|
|
495
|
+
patternIndex += 1;
|
|
496
|
+
backtrackValueIndex = valueIndex;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
500
|
+
patternIndex += 1;
|
|
501
|
+
valueIndex += 1;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (starIndex !== -1) {
|
|
505
|
+
patternIndex = starIndex + 1;
|
|
506
|
+
backtrackValueIndex += 1;
|
|
507
|
+
valueIndex = backtrackValueIndex;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
513
|
+
patternIndex += 1;
|
|
514
|
+
}
|
|
515
|
+
return patternIndex === pattern.length;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// ../../src/internal/CacheKeyDiscovery.ts
|
|
520
|
+
var CacheKeyDiscovery = class {
|
|
521
|
+
constructor(options) {
|
|
522
|
+
this.options = options;
|
|
523
|
+
}
|
|
524
|
+
options;
|
|
525
|
+
async collectKeysWithPrefix(prefix) {
|
|
526
|
+
const { tagIndex } = this.options;
|
|
527
|
+
const matches = new Set(
|
|
528
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
529
|
+
);
|
|
530
|
+
await Promise.all(
|
|
531
|
+
this.options.layers.map(async (layer) => {
|
|
532
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const keys = await layer.keys();
|
|
537
|
+
for (const key of keys) {
|
|
538
|
+
if (key.startsWith(prefix)) {
|
|
539
|
+
matches.add(key);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
);
|
|
547
|
+
return [...matches];
|
|
548
|
+
}
|
|
549
|
+
async collectKeysMatchingPattern(pattern) {
|
|
550
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
551
|
+
await Promise.all(
|
|
552
|
+
this.options.layers.map(async (layer) => {
|
|
553
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const keys = await layer.keys();
|
|
558
|
+
for (const key of keys) {
|
|
559
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
560
|
+
matches.add(key);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
);
|
|
568
|
+
return [...matches];
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
471
572
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
472
573
|
var CircuitBreakerManager = class {
|
|
473
574
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -1067,54 +1168,6 @@ var TtlResolver = class {
|
|
|
1067
1168
|
}
|
|
1068
1169
|
};
|
|
1069
1170
|
|
|
1070
|
-
// ../../src/invalidation/PatternMatcher.ts
|
|
1071
|
-
var PatternMatcher = class _PatternMatcher {
|
|
1072
|
-
/**
|
|
1073
|
-
* Tests whether a glob-style pattern matches a value.
|
|
1074
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
1075
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
1076
|
-
* quadratic memory usage on long patterns/keys.
|
|
1077
|
-
*/
|
|
1078
|
-
static matches(pattern, value) {
|
|
1079
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Linear-time glob matching with O(1) extra memory.
|
|
1083
|
-
*/
|
|
1084
|
-
static matchLinear(pattern, value) {
|
|
1085
|
-
let patternIndex = 0;
|
|
1086
|
-
let valueIndex = 0;
|
|
1087
|
-
let starIndex = -1;
|
|
1088
|
-
let backtrackValueIndex = 0;
|
|
1089
|
-
while (valueIndex < value.length) {
|
|
1090
|
-
const patternChar = pattern[patternIndex];
|
|
1091
|
-
const valueChar = value[valueIndex];
|
|
1092
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
1093
|
-
starIndex = patternIndex;
|
|
1094
|
-
patternIndex += 1;
|
|
1095
|
-
backtrackValueIndex = valueIndex;
|
|
1096
|
-
continue;
|
|
1097
|
-
}
|
|
1098
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
1099
|
-
patternIndex += 1;
|
|
1100
|
-
valueIndex += 1;
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
if (starIndex !== -1) {
|
|
1104
|
-
patternIndex = starIndex + 1;
|
|
1105
|
-
backtrackValueIndex += 1;
|
|
1106
|
-
valueIndex = backtrackValueIndex;
|
|
1107
|
-
continue;
|
|
1108
|
-
}
|
|
1109
|
-
return false;
|
|
1110
|
-
}
|
|
1111
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
1112
|
-
patternIndex += 1;
|
|
1113
|
-
}
|
|
1114
|
-
return patternIndex === pattern.length;
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
|
|
1118
1171
|
// ../../src/invalidation/TagIndex.ts
|
|
1119
1172
|
var TagIndex = class {
|
|
1120
1173
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
@@ -1443,6 +1496,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1443
1496
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1444
1497
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1445
1498
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1499
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
1500
|
+
layers: this.layers,
|
|
1501
|
+
tagIndex: this.tagIndex,
|
|
1502
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1503
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1504
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1446
1507
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1447
1508
|
this.logger.warn?.(
|
|
1448
1509
|
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
@@ -1470,6 +1531,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1470
1531
|
unsubscribeInvalidation;
|
|
1471
1532
|
logger;
|
|
1472
1533
|
tagIndex;
|
|
1534
|
+
keyDiscovery;
|
|
1473
1535
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1474
1536
|
snapshotSerializer = new JsonSerializer();
|
|
1475
1537
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
@@ -1820,14 +1882,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1820
1882
|
}
|
|
1821
1883
|
async invalidateByPattern(pattern) {
|
|
1822
1884
|
await this.awaitStartup("invalidateByPattern");
|
|
1823
|
-
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1885
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1824
1886
|
await this.deleteKeys(keys);
|
|
1825
1887
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1826
1888
|
}
|
|
1827
1889
|
async invalidateByPrefix(prefix) {
|
|
1828
1890
|
await this.awaitStartup("invalidateByPrefix");
|
|
1829
1891
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1830
|
-
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1892
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1831
1893
|
await this.deleteKeys(keys);
|
|
1832
1894
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1833
1895
|
}
|
|
@@ -2440,50 +2502,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2440
2502
|
shouldBroadcastL1Invalidation() {
|
|
2441
2503
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2442
2504
|
}
|
|
2443
|
-
async collectKeysWithPrefix(prefix) {
|
|
2444
|
-
const matches = new Set(
|
|
2445
|
-
this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
|
|
2446
|
-
);
|
|
2447
|
-
await Promise.all(
|
|
2448
|
-
this.layers.map(async (layer) => {
|
|
2449
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2450
|
-
return;
|
|
2451
|
-
}
|
|
2452
|
-
try {
|
|
2453
|
-
const keys = await layer.keys();
|
|
2454
|
-
for (const key of keys) {
|
|
2455
|
-
if (key.startsWith(prefix)) {
|
|
2456
|
-
matches.add(key);
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
} catch (error) {
|
|
2460
|
-
await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
2461
|
-
}
|
|
2462
|
-
})
|
|
2463
|
-
);
|
|
2464
|
-
return [...matches];
|
|
2465
|
-
}
|
|
2466
|
-
async collectKeysMatchingPattern(pattern) {
|
|
2467
|
-
const matches = new Set(await this.tagIndex.matchPattern(pattern));
|
|
2468
|
-
await Promise.all(
|
|
2469
|
-
this.layers.map(async (layer) => {
|
|
2470
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2471
|
-
return;
|
|
2472
|
-
}
|
|
2473
|
-
try {
|
|
2474
|
-
const keys = await layer.keys();
|
|
2475
|
-
for (const key of keys) {
|
|
2476
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
2477
|
-
matches.add(key);
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
} catch (error) {
|
|
2481
|
-
await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
2482
|
-
}
|
|
2483
|
-
})
|
|
2484
|
-
);
|
|
2485
|
-
return [...matches];
|
|
2486
|
-
}
|
|
2487
2505
|
shouldCleanupGenerations() {
|
|
2488
2506
|
return Boolean(this.options.generationCleanup);
|
|
2489
2507
|
}
|
|
@@ -2506,7 +2524,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2506
2524
|
}
|
|
2507
2525
|
async cleanupGeneration(generation) {
|
|
2508
2526
|
const prefix = `v${generation}:`;
|
|
2509
|
-
const keys = await this.collectKeysWithPrefix(prefix);
|
|
2527
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2510
2528
|
if (keys.length === 0) {
|
|
2511
2529
|
return;
|
|
2512
2530
|
}
|