layercache 1.2.2 → 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 +119 -89
- package/dist/{chunk-ZMDB5KOK.js → chunk-7V7XAB74.js} +24 -1
- package/dist/{chunk-46UH7LNM.js → chunk-KOYGHLVP.js} +142 -18
- package/dist/{chunk-IXCMHVHP.js → chunk-QHWG7QS5.js} +1 -1
- package/dist/cli.cjs +37 -3
- package/dist/cli.js +15 -4
- package/dist/{edge-DLpdQN0W.d.ts → edge-Dw97n89L.d.cts} +33 -1
- package/dist/{edge-DLpdQN0W.d.cts → edge-Dw97n89L.d.ts} +33 -1
- package/dist/edge.cjs +165 -17
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +657 -146
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +447 -84
- package/package.json +4 -4
- package/packages/nestjs/dist/index.cjs +558 -91
- package/packages/nestjs/dist/index.d.cts +24 -0
- package/packages/nestjs/dist/index.d.ts +24 -0
- package/packages/nestjs/dist/index.js +558 -91
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
|
|
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 ≥ 18
|
|
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,9 +173,11 @@ 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
|
-
Deletes every key that was stored with this tag across all layers.
|
|
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`).
|
|
177
181
|
|
|
178
182
|
```ts
|
|
179
183
|
await cache.set('user:123', user, { tags: ['user:123'] })
|
|
@@ -193,12 +197,14 @@ await cache.invalidateByTags(['users', 'posts'], 'any') // keys tagged with e
|
|
|
193
197
|
|
|
194
198
|
### `cache.invalidateByPattern(pattern): Promise<void>`
|
|
195
199
|
|
|
196
|
-
Glob-style deletion against the tracked key set.
|
|
200
|
+
Glob-style deletion against the tracked key set, plus any layer that can enumerate real keys (for example `MemoryLayer`, `RedisLayer`, or `DiskLayer`).
|
|
197
201
|
|
|
198
202
|
```ts
|
|
199
203
|
await cache.invalidateByPattern('user:*') // deletes user:1, user:2, …
|
|
200
204
|
```
|
|
201
205
|
|
|
206
|
+
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.
|
|
207
|
+
|
|
202
208
|
### `cache.invalidateByPrefix(prefix): Promise<void>`
|
|
203
209
|
|
|
204
210
|
Prefer this over glob invalidation when your keys are hierarchical.
|
|
@@ -280,6 +286,17 @@ await cache.set('user:123', user)
|
|
|
280
286
|
cache.bumpGeneration() // now reads use v2:user:123
|
|
281
287
|
```
|
|
282
288
|
|
|
289
|
+
If you also want old generation keys cleaned up automatically instead of waiting for TTL expiry:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const cache = new CacheStack([...], {
|
|
293
|
+
generation: 1,
|
|
294
|
+
generationCleanup: { batchSize: 500 }
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`bumpGeneration()` only rotates future reads and writes by default. Enable `generationCleanup` when you want previous generations to be pruned automatically instead of aging out by TTL.
|
|
299
|
+
|
|
283
300
|
### OpenTelemetry note
|
|
284
301
|
|
|
285
302
|
`createOpenTelemetryPlugin()` currently wraps a `CacheStack` instance's methods directly. Use one OpenTelemetry plugin per cache instance; if you need to compose multiple wrappers, install them in a fixed order and uninstall them in reverse order.
|
|
@@ -482,10 +499,10 @@ const cache = new CacheStack(
|
|
|
482
499
|
await cache.disconnect() // unsubscribes cleanly on shutdown
|
|
483
500
|
```
|
|
484
501
|
|
|
485
|
-
By default,
|
|
502
|
+
By default, write-triggered L1 invalidation is **off** even when an invalidation bus is configured. This avoids surprising Redis Pub/Sub traffic in write-heavy services. Enable it explicitly when you want every write to evict peer memory caches immediately:
|
|
486
503
|
|
|
487
504
|
```ts
|
|
488
|
-
new CacheStack([...], { invalidationBus: bus,
|
|
505
|
+
new CacheStack([...], { invalidationBus: bus, broadcastL1Invalidation: true })
|
|
489
506
|
```
|
|
490
507
|
|
|
491
508
|
### Distributed tag invalidation
|
|
@@ -510,6 +527,8 @@ const cache = new CacheStack(
|
|
|
510
527
|
|
|
511
528
|
Now `invalidateByTag('user:123')` on any server deletes every tagged key, regardless of which server originally wrote it.
|
|
512
529
|
|
|
530
|
+
The same recommendation applies to `invalidateByPattern()` and `invalidateByPrefix()` in distributed deployments: a shared tag index gives the most complete view of known keys, while layer key scans act as a fallback only when the shared layer exposes `keys()`.
|
|
531
|
+
|
|
513
532
|
### Safe Redis clearing
|
|
514
533
|
|
|
515
534
|
`RedisLayer.clear()` is intentionally conservative. Without a `prefix`, it throws instead of deleting the whole Redis database.
|
|
@@ -622,6 +641,8 @@ await cache.get('leaderboard', fetchLeaderboard, {
|
|
|
622
641
|
})
|
|
623
642
|
```
|
|
624
643
|
|
|
644
|
+
Background refreshes time out after 30 seconds by default so a hung upstream fetch cannot block future refresh attempts forever. Override that with `backgroundRefreshTimeoutMs`.
|
|
645
|
+
|
|
625
646
|
---
|
|
626
647
|
|
|
627
648
|
## Graceful degradation & circuit breaker
|
|
@@ -718,6 +739,8 @@ await cache.persistToFile('./cache-snapshot.json')
|
|
|
718
739
|
await cache.restoreFromFile('./cache-snapshot.json')
|
|
719
740
|
```
|
|
720
741
|
|
|
742
|
+
For safety, file snapshots are restricted to `process.cwd()` by default. Set `snapshotBaseDir` to an explicit directory for application-controlled snapshot storage, or `false` if you intentionally want to disable that restriction.
|
|
743
|
+
|
|
721
744
|
---
|
|
722
745
|
|
|
723
746
|
## Event hooks
|
|
@@ -744,9 +767,11 @@ cache.on('error', ({ event, context }) => logger.error(event, context))
|
|
|
744
767
|
|
|
745
768
|
---
|
|
746
769
|
|
|
747
|
-
##
|
|
770
|
+
## Integrations & tooling
|
|
771
|
+
|
|
772
|
+
### Framework integrations
|
|
748
773
|
|
|
749
|
-
|
|
774
|
+
#### Express
|
|
750
775
|
|
|
751
776
|
```ts
|
|
752
777
|
import { CacheStack, MemoryLayer, createExpressCacheMiddleware } from 'layercache'
|
|
@@ -766,7 +791,7 @@ app.get('/api/user/:id', createExpressCacheMiddleware(cache, {
|
|
|
766
791
|
}), handler)
|
|
767
792
|
```
|
|
768
793
|
|
|
769
|
-
|
|
794
|
+
#### tRPC
|
|
770
795
|
|
|
771
796
|
```ts
|
|
772
797
|
import { createTrpcCacheMiddleware } from 'layercache/integrations/trpc'
|
|
@@ -776,7 +801,7 @@ const cacheMiddleware = createTrpcCacheMiddleware(cache, 'trpc', { ttl: 60 })
|
|
|
776
801
|
export const cachedProcedure = t.procedure.use(cacheMiddleware)
|
|
777
802
|
```
|
|
778
803
|
|
|
779
|
-
|
|
804
|
+
#### GraphQL
|
|
780
805
|
|
|
781
806
|
```ts
|
|
782
807
|
import { cacheGraphqlResolver } from 'layercache/integrations/graphql'
|
|
@@ -793,7 +818,7 @@ const resolvers = {
|
|
|
793
818
|
|
|
794
819
|
---
|
|
795
820
|
|
|
796
|
-
|
|
821
|
+
### Admin CLI
|
|
797
822
|
|
|
798
823
|
Inspect and manage a Redis-backed cache without writing code.
|
|
799
824
|
|
|
@@ -1019,7 +1044,7 @@ new CacheStack([...], {
|
|
|
1019
1044
|
|
|
1020
1045
|
## Requirements
|
|
1021
1046
|
|
|
1022
|
-
- Node.js ≥
|
|
1047
|
+
- Node.js ≥ 20
|
|
1023
1048
|
- TypeScript ≥ 5.0 (optional — fully typed, ships `.d.ts`)
|
|
1024
1049
|
- ioredis ≥ 5 (optional peer dependency — only needed for `RedisLayer` / `RedisTagIndex`)
|
|
1025
1050
|
|
|
@@ -1027,15 +1052,20 @@ new CacheStack([...], {
|
|
|
1027
1052
|
|
|
1028
1053
|
## Contributing
|
|
1029
1054
|
|
|
1055
|
+
Contributions are welcome, whether that means bug fixes, documentation improvements, performance work, new adapters, or issue reports.
|
|
1056
|
+
|
|
1030
1057
|
```bash
|
|
1031
1058
|
git clone https://github.com/flyingsquirrel0419/layercache
|
|
1032
1059
|
cd layercache
|
|
1033
1060
|
npm install
|
|
1061
|
+
npm run lint
|
|
1034
1062
|
npm test # vitest
|
|
1035
1063
|
npm run build:all # esm + cjs + nestjs package
|
|
1036
1064
|
```
|
|
1037
1065
|
|
|
1038
|
-
|
|
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.
|
|
1039
1069
|
|
|
1040
1070
|
---
|
|
1041
1071
|
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
// src/internal/StoredValue.ts
|
|
2
2
|
function isStoredValueEnvelope(value) {
|
|
3
|
-
|
|
3
|
+
if (typeof value !== "object" || value === null) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const v = value;
|
|
7
|
+
if (v.__layercache !== 1) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
23
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
4
27
|
}
|
|
5
28
|
function createStoredValueEnvelope(options) {
|
|
6
29
|
const now = options.now ?? Date.now();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
PatternMatcher,
|
|
3
2
|
unwrapStoredValue
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7V7XAB74.js";
|
|
5
4
|
|
|
6
5
|
// src/layers/MemoryLayer.ts
|
|
7
6
|
var MemoryLayer = class {
|
|
@@ -49,16 +48,10 @@ var MemoryLayer = class {
|
|
|
49
48
|
return entry.value;
|
|
50
49
|
}
|
|
51
50
|
async getMany(keys) {
|
|
52
|
-
|
|
53
|
-
for (const key of keys) {
|
|
54
|
-
values.push(await this.getEntry(key));
|
|
55
|
-
}
|
|
56
|
-
return values;
|
|
51
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
57
52
|
}
|
|
58
53
|
async setMany(entries) {
|
|
59
|
-
|
|
60
|
-
await this.set(entry.key, entry.value, entry.ttl);
|
|
61
|
-
}
|
|
54
|
+
await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
|
|
62
55
|
}
|
|
63
56
|
async set(key, value, ttl = this.defaultTtl) {
|
|
64
57
|
this.entries.delete(key);
|
|
@@ -197,15 +190,17 @@ var TagIndex = class {
|
|
|
197
190
|
keyToTags = /* @__PURE__ */ new Map();
|
|
198
191
|
knownKeys = /* @__PURE__ */ new Set();
|
|
199
192
|
maxKnownKeys;
|
|
193
|
+
nextNodeId = 1;
|
|
194
|
+
root = this.createTrieNode();
|
|
200
195
|
constructor(options = {}) {
|
|
201
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
196
|
+
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
202
197
|
}
|
|
203
198
|
async touch(key) {
|
|
204
|
-
this.
|
|
199
|
+
this.insertKnownKey(key);
|
|
205
200
|
this.pruneKnownKeysIfNeeded();
|
|
206
201
|
}
|
|
207
202
|
async track(key, tags) {
|
|
208
|
-
this.
|
|
203
|
+
this.insertKnownKey(key);
|
|
209
204
|
this.pruneKnownKeysIfNeeded();
|
|
210
205
|
if (tags.length === 0) {
|
|
211
206
|
return;
|
|
@@ -231,18 +226,104 @@ var TagIndex = class {
|
|
|
231
226
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
232
227
|
}
|
|
233
228
|
async keysForPrefix(prefix) {
|
|
234
|
-
|
|
229
|
+
const node = this.findNode(prefix);
|
|
230
|
+
if (!node) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
const matches = [];
|
|
234
|
+
this.collectFromNode(node, prefix, matches);
|
|
235
|
+
return matches;
|
|
235
236
|
}
|
|
236
237
|
async tagsForKey(key) {
|
|
237
238
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
238
239
|
}
|
|
239
240
|
async matchPattern(pattern) {
|
|
240
|
-
|
|
241
|
+
const matches = /* @__PURE__ */ new Set();
|
|
242
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
243
|
+
return [...matches];
|
|
241
244
|
}
|
|
242
245
|
async clear() {
|
|
243
246
|
this.tagToKeys.clear();
|
|
244
247
|
this.keyToTags.clear();
|
|
245
248
|
this.knownKeys.clear();
|
|
249
|
+
this.root.children.clear();
|
|
250
|
+
this.root.terminal = false;
|
|
251
|
+
this.nextNodeId = this.root.id + 1;
|
|
252
|
+
}
|
|
253
|
+
createTrieNode() {
|
|
254
|
+
return {
|
|
255
|
+
id: this.nextNodeId++,
|
|
256
|
+
terminal: false,
|
|
257
|
+
children: /* @__PURE__ */ new Map()
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
insertKnownKey(key) {
|
|
261
|
+
if (this.knownKeys.has(key)) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.knownKeys.add(key);
|
|
265
|
+
let node = this.root;
|
|
266
|
+
for (const character of key) {
|
|
267
|
+
let child = node.children.get(character);
|
|
268
|
+
if (!child) {
|
|
269
|
+
child = this.createTrieNode();
|
|
270
|
+
node.children.set(character, child);
|
|
271
|
+
}
|
|
272
|
+
node = child;
|
|
273
|
+
}
|
|
274
|
+
node.terminal = true;
|
|
275
|
+
}
|
|
276
|
+
findNode(prefix) {
|
|
277
|
+
let node = this.root;
|
|
278
|
+
for (const character of prefix) {
|
|
279
|
+
node = node.children.get(character);
|
|
280
|
+
if (!node) {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return node;
|
|
285
|
+
}
|
|
286
|
+
collectFromNode(node, prefix, matches) {
|
|
287
|
+
if (node.terminal) {
|
|
288
|
+
matches.push(prefix);
|
|
289
|
+
}
|
|
290
|
+
for (const [character, child] of node.children) {
|
|
291
|
+
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
295
|
+
const stateKey = `${node.id}:${patternIndex}`;
|
|
296
|
+
if (visited.has(stateKey)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
visited.add(stateKey);
|
|
300
|
+
if (patternIndex === pattern.length) {
|
|
301
|
+
if (node.terminal) {
|
|
302
|
+
matches.add(prefix);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const patternChar = pattern[patternIndex];
|
|
307
|
+
if (patternChar === void 0) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (patternChar === "*") {
|
|
311
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
312
|
+
for (const [character, child2] of node.children) {
|
|
313
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (patternChar === "?") {
|
|
318
|
+
for (const [character, child2] of node.children) {
|
|
319
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const child = node.children.get(patternChar);
|
|
324
|
+
if (child) {
|
|
325
|
+
this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
|
|
326
|
+
}
|
|
246
327
|
}
|
|
247
328
|
pruneKnownKeysIfNeeded() {
|
|
248
329
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
@@ -259,7 +340,7 @@ var TagIndex = class {
|
|
|
259
340
|
}
|
|
260
341
|
}
|
|
261
342
|
removeKey(key) {
|
|
262
|
-
this.
|
|
343
|
+
this.removeKnownKey(key);
|
|
263
344
|
const tags = this.keyToTags.get(key);
|
|
264
345
|
if (!tags) {
|
|
265
346
|
return;
|
|
@@ -276,6 +357,34 @@ var TagIndex = class {
|
|
|
276
357
|
}
|
|
277
358
|
this.keyToTags.delete(key);
|
|
278
359
|
}
|
|
360
|
+
removeKnownKey(key) {
|
|
361
|
+
if (!this.knownKeys.delete(key)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const path = [];
|
|
365
|
+
let node = this.root;
|
|
366
|
+
for (const character of key) {
|
|
367
|
+
const child = node.children.get(character);
|
|
368
|
+
if (!child) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
path.push([node, character]);
|
|
372
|
+
node = child;
|
|
373
|
+
}
|
|
374
|
+
node.terminal = false;
|
|
375
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
376
|
+
const entry = path[index];
|
|
377
|
+
if (!entry) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const [parent, character] = entry;
|
|
381
|
+
const child = parent.children.get(character);
|
|
382
|
+
if (!child || child.terminal || child.children.size > 0) {
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
parent.children.delete(character);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
279
388
|
};
|
|
280
389
|
|
|
281
390
|
// src/integrations/hono.ts
|
|
@@ -287,7 +396,8 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
287
396
|
await next();
|
|
288
397
|
return;
|
|
289
398
|
}
|
|
290
|
-
const
|
|
399
|
+
const rawPath = context.req.path ?? context.req.url ?? "/";
|
|
400
|
+
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl(rawPath)}`;
|
|
291
401
|
const cached = await cache.get(key, void 0, options);
|
|
292
402
|
if (cached !== null) {
|
|
293
403
|
context.header?.("x-cache", "HIT");
|
|
@@ -298,12 +408,26 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
298
408
|
const originalJson = context.json.bind(context);
|
|
299
409
|
context.json = (body, status) => {
|
|
300
410
|
context.header?.("x-cache", "MISS");
|
|
301
|
-
|
|
411
|
+
cache.set(key, body, options).catch((err) => {
|
|
412
|
+
cache.emit("error", {
|
|
413
|
+
operation: "set",
|
|
414
|
+
error: err instanceof Error ? err.message : String(err)
|
|
415
|
+
});
|
|
416
|
+
});
|
|
302
417
|
return originalJson(body, status);
|
|
303
418
|
};
|
|
304
419
|
await next();
|
|
305
420
|
};
|
|
306
421
|
}
|
|
422
|
+
function normalizeUrl(url) {
|
|
423
|
+
try {
|
|
424
|
+
const parsed = new URL(url, "http://localhost");
|
|
425
|
+
parsed.searchParams.sort();
|
|
426
|
+
return decodeURIComponent(parsed.pathname) + parsed.search;
|
|
427
|
+
} catch {
|
|
428
|
+
return url;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
307
431
|
|
|
308
432
|
export {
|
|
309
433
|
MemoryLayer,
|