layercache 1.3.4 → 2.0.0
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 +42 -41
- package/dist/{chunk-BORDQ3LA.js → chunk-7KMKQ6QZ.js} +15 -1
- package/dist/{chunk-5RCAX2BQ.js → chunk-FFZCC7EQ.js} +3 -3
- package/dist/{chunk-4PPBOOXT.js → chunk-KJDFYE5T.js} +38 -26
- package/dist/cli.cjs +9 -9
- package/dist/cli.js +4 -4
- package/dist/{edge-DKkrQ_Ky.d.cts → edge-D2FpRlyS.d.cts} +71 -22
- package/dist/{edge-DKkrQ_Ky.d.ts → edge-D2FpRlyS.d.ts} +71 -22
- package/dist/edge.cjs +9 -9
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +399 -164
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +294 -81
- package/package.json +5 -5
- package/benchmarks/direct.ts +0 -221
- package/benchmarks/edge-utils.ts +0 -28
- package/benchmarks/edge.ts +0 -491
- package/benchmarks/http.ts +0 -99
- package/benchmarks/latency.ts +0 -45
- package/benchmarks/memory-pressure.ts +0 -144
- package/benchmarks/multi-process-fanout.ts +0 -231
- package/benchmarks/multi-process-worker.ts +0 -151
- package/benchmarks/paths.ts +0 -25
- package/benchmarks/queue-amplification-utils.ts +0 -48
- package/benchmarks/queue-amplification.ts +0 -230
- package/benchmarks/redis-latency-proxy.ts +0 -100
- package/benchmarks/redis.ts +0 -107
- package/benchmarks/scenario-utils.ts +0 -38
- package/benchmarks/server.ts +0 -157
- package/benchmarks/slow-redis-latency.ts +0 -309
- package/benchmarks/slow-redis-utils.ts +0 -29
- package/benchmarks/slow-redis.ts +0 -47
- package/benchmarks/stampede.ts +0 -26
- package/benchmarks/stats.ts +0 -46
- package/benchmarks/workload.ts +0 -77
- package/examples/express-api/index.ts +0 -31
- package/examples/nextjs-api-routes/route.ts +0 -16
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache_2.0-green" alt="license"></a>
|
|
20
20
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-first-3178C6?logo=typescript&logoColor=white" alt="TypeScript"></a>
|
|
21
21
|
<img src="https://img.shields.io/badge/Node.js-%E2%89%A5_20-339933?logo=nodedotjs&logoColor=white" alt="Node.js >= 20">
|
|
22
|
-
<img src="https://img.shields.io/badge/tests-
|
|
22
|
+
<img src="https://img.shields.io/badge/tests-549_passing-brightgreen" alt="tests">
|
|
23
23
|
<a href="https://coveralls.io/github/flyingsquirrel0419/layercache?branch=main"><img src="https://coveralls.io/repos/github/flyingsquirrel0419/layercache/badge.svg?branch=main&t=20260410" alt="Coveralls"></a>
|
|
24
24
|
</p>
|
|
25
25
|
|
|
@@ -64,8 +64,8 @@ import { CacheStack, MemoryLayer, RedisLayer } from 'layercache'
|
|
|
64
64
|
import Redis from 'ioredis'
|
|
65
65
|
|
|
66
66
|
const cache = new CacheStack([
|
|
67
|
-
new MemoryLayer({ ttl:
|
|
68
|
-
new RedisLayer({ client: new Redis(), ttl:
|
|
67
|
+
new MemoryLayer({ ttl: 60_000, maxSize: 1_000 }), // L1: in-process
|
|
68
|
+
new RedisLayer({ client: new Redis(), ttl: 3_600_000 }), // L2: shared
|
|
69
69
|
])
|
|
70
70
|
|
|
71
71
|
// Read-through: fetcher runs once, all layers filled
|
|
@@ -77,7 +77,7 @@ const user = await cache.get('user:123', () => db.findUser(123))
|
|
|
77
77
|
|
|
78
78
|
```ts
|
|
79
79
|
const cache = new CacheStack([
|
|
80
|
-
new MemoryLayer({ ttl:
|
|
80
|
+
new MemoryLayer({ ttl: 60_000 })
|
|
81
81
|
])
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -90,8 +90,8 @@ const cache = new CacheStack([
|
|
|
90
90
|
import { CacheStack, MemoryLayer, RedisLayer, DiskLayer } from 'layercache'
|
|
91
91
|
|
|
92
92
|
const cache = new CacheStack([
|
|
93
|
-
new MemoryLayer({ ttl:
|
|
94
|
-
new RedisLayer({ client: new Redis(), ttl:
|
|
93
|
+
new MemoryLayer({ ttl: 60_000, maxSize: 5_000 }),
|
|
94
|
+
new RedisLayer({ client: new Redis(), ttl: 3_600_000, compression: 'gzip' }),
|
|
95
95
|
new DiskLayer({ directory: './var/cache', maxFiles: 10_000 }),
|
|
96
96
|
])
|
|
97
97
|
```
|
|
@@ -172,10 +172,10 @@ import {
|
|
|
172
172
|
import Redis from 'ioredis'
|
|
173
173
|
|
|
174
174
|
const cache = new CacheStack([
|
|
175
|
-
new MemoryLayer({ ttl:
|
|
175
|
+
new MemoryLayer({ ttl: 60_000 }), // ms
|
|
176
176
|
new RedisLayer({
|
|
177
177
|
client: new Redis(),
|
|
178
|
-
ttl:
|
|
178
|
+
ttl: 300_000 // ms
|
|
179
179
|
})
|
|
180
180
|
])
|
|
181
181
|
|
|
@@ -194,38 +194,38 @@ const cache = new CacheStack([
|
|
|
194
194
|
|
|
195
195
|
## Comparison
|
|
196
196
|
|
|
197
|
-
| | node-cache-manager | keyv | cacheable | **layercache** |
|
|
198
|
-
|
|
199
|
-
| Multi-layer + auto backfill | Partial | Plugin | -- | **Yes** |
|
|
200
|
-
| Stampede prevention | -- | -- | -- | **Yes** |
|
|
201
|
-
| Tag invalidation | -- | Yes | Yes | **Yes** |
|
|
202
|
-
| TypeScript-first | Partial | Yes | Yes | **Yes** |
|
|
203
|
-
| Event hooks | Yes | Yes | Yes | **Yes** |
|
|
197
|
+
| | node-cache-manager | keyv | cacheable | BentoCache | **layercache** |
|
|
198
|
+
|---|:---:|:---:|:---:|:---:|:---:|
|
|
199
|
+
| Multi-layer + auto backfill | Partial | Plugin | -- | Partial | **Yes** |
|
|
200
|
+
| Stampede prevention | -- | -- | -- | Partial | **Yes** |
|
|
201
|
+
| Tag invalidation | -- | Yes | Yes | Yes | **Yes** |
|
|
202
|
+
| TypeScript-first | Partial | Yes | Yes | Yes | **Yes** |
|
|
203
|
+
| Event hooks | Yes | Yes | Yes | Yes | **Yes** |
|
|
204
204
|
|
|
205
205
|
<details>
|
|
206
206
|
<summary>Full comparison (19 features)</summary>
|
|
207
207
|
|
|
208
|
-
| | node-cache-manager | keyv | cacheable | **layercache** |
|
|
209
|
-
|
|
210
|
-
| Multi-layer with auto backfill | Partial | Plugin | -- | **Yes** |
|
|
211
|
-
| Stampede prevention | -- | -- | -- | **Yes** |
|
|
212
|
-
| Distributed single-flight | -- | -- | -- | **Yes** |
|
|
213
|
-
| Tag invalidation | -- | Yes | Yes | **Yes** |
|
|
214
|
-
| Distributed tags | -- | -- | -- | **Yes** |
|
|
215
|
-
| Cross-server L1 flush | -- | -- | -- | **Yes** |
|
|
216
|
-
| Stale-while-revalidate | -- | -- | -- | **Yes** |
|
|
217
|
-
| Circuit breaker | -- | -- | -- | **Yes** |
|
|
218
|
-
| Graceful degradation | -- | -- | -- | **Yes** |
|
|
219
|
-
| Sliding / adaptive TTL | -- | -- | -- | **Yes** |
|
|
220
|
-
| Cache warming | -- | -- | -- | **Yes** |
|
|
221
|
-
| Persistence / snapshots | -- | -- | -- | **Yes** |
|
|
222
|
-
| Compression | -- | -- | Yes | **Yes** |
|
|
223
|
-
| Admin CLI | -- | -- | -- | **Yes** |
|
|
224
|
-
| TypeScript-first | Partial | Yes | Yes | **Yes** |
|
|
225
|
-
| Wrap / decorator API | Yes | -- | -- | **Yes** |
|
|
226
|
-
| Namespaces | -- | Yes | Yes | **Yes** |
|
|
227
|
-
| Event hooks | Yes | Yes | Yes | **Yes** |
|
|
228
|
-
| Custom layers | Partial | -- | -- | **Yes** |
|
|
208
|
+
| | node-cache-manager | keyv | cacheable | BentoCache | **layercache** |
|
|
209
|
+
|---|:---:|:---:|:---:|:---:|:---:|
|
|
210
|
+
| Multi-layer with auto backfill | Partial | Plugin | -- | Partial | **Yes** |
|
|
211
|
+
| Stampede prevention | -- | -- | -- | Partial | **Yes** |
|
|
212
|
+
| Distributed single-flight | -- | -- | -- | -- | **Yes** |
|
|
213
|
+
| Tag invalidation | -- | Yes | Yes | Yes | **Yes** |
|
|
214
|
+
| Distributed tags | -- | -- | -- | -- | **Yes** |
|
|
215
|
+
| Cross-server L1 flush | -- | -- | -- | Yes | **Yes** |
|
|
216
|
+
| Stale-while-revalidate | -- | -- | -- | Yes | **Yes** |
|
|
217
|
+
| Circuit breaker | -- | -- | -- | Yes | **Yes** |
|
|
218
|
+
| Graceful degradation | -- | -- | -- | Yes | **Yes** |
|
|
219
|
+
| Sliding / adaptive TTL | -- | -- | -- | -- | **Yes** |
|
|
220
|
+
| Cache warming | -- | -- | -- | -- | **Yes** |
|
|
221
|
+
| Persistence / snapshots | -- | -- | -- | -- | **Yes** |
|
|
222
|
+
| Compression | -- | -- | Yes | -- | **Yes** |
|
|
223
|
+
| Admin CLI | -- | -- | -- | -- | **Yes** |
|
|
224
|
+
| TypeScript-first | Partial | Yes | Yes | Yes | **Yes** |
|
|
225
|
+
| Wrap / decorator API | Yes | -- | -- | Partial | **Yes** |
|
|
226
|
+
| Namespaces | -- | Yes | Yes | Yes | **Yes** |
|
|
227
|
+
| Event hooks | Yes | Yes | Yes | Yes | **Yes** |
|
|
228
|
+
| Custom layers | Partial | -- | -- | Yes | **Yes** |
|
|
229
229
|
|
|
230
230
|
</details>
|
|
231
231
|
|
|
@@ -265,6 +265,7 @@ const cache = new CacheStack([
|
|
|
265
265
|
| **Adaptive TTL** | Auto-ramp TTL for hot keys up to a ceiling |
|
|
266
266
|
| **Refresh-ahead** | Proactively refresh before expiry |
|
|
267
267
|
| **TTL policies** | Align expirations to calendar boundaries (`until-midnight`, `next-hour`, custom) |
|
|
268
|
+
| **Context-aware entry options** | Derive TTLs and tags from the cached value right before storage |
|
|
268
269
|
|
|
269
270
|
### Resilience & Operations
|
|
270
271
|
|
|
@@ -286,7 +287,7 @@ const cache = new CacheStack([
|
|
|
286
287
|
| **Metrics** | Hits, misses, fetches, stale hits, circuit breaker trips, and more |
|
|
287
288
|
| **Per-layer latency** | Avg, max, and sample count using Welford's algorithm |
|
|
288
289
|
| **Health checks** | Async health endpoint per layer with latency measurement |
|
|
289
|
-
| **Event hooks** | `hit`, `miss`, `set`, `delete`, `stale-serve`, `stampede-dedupe`, `backfill`, `warm`, `error` |
|
|
290
|
+
| **Event hooks** | `hit`, `miss`, `set`, `delete`, `expire`, `stale-serve`, `stampede-dedupe`, `backfill`, `warm`, `error` |
|
|
290
291
|
| **OpenTelemetry** | Hook-based distributed tracing support without method monkey-patching |
|
|
291
292
|
| **Prometheus exporter** | Metrics export including latency gauges |
|
|
292
293
|
| **HTTP stats handler** | JSON endpoint for dashboards |
|
|
@@ -316,10 +317,10 @@ layercache plugs into the frameworks you already use:
|
|
|
316
317
|
```ts
|
|
317
318
|
import { CacheStack, MemoryLayer, createExpressCacheMiddleware } from 'layercache'
|
|
318
319
|
|
|
319
|
-
const cache = new CacheStack([new MemoryLayer({ ttl:
|
|
320
|
+
const cache = new CacheStack([new MemoryLayer({ ttl: 60_000 })])
|
|
320
321
|
|
|
321
322
|
app.get('/api/users', createExpressCacheMiddleware(cache, {
|
|
322
|
-
ttl:
|
|
323
|
+
ttl: 30_000,
|
|
323
324
|
tags: ['users'],
|
|
324
325
|
keyResolver: (req) => `users:${req.url}`
|
|
325
326
|
}), async (req, res) => {
|
|
@@ -381,8 +382,8 @@ const coordinator = new RedisSingleFlightCoordinator({ client: redis })
|
|
|
381
382
|
|
|
382
383
|
const cache = new CacheStack(
|
|
383
384
|
[
|
|
384
|
-
new MemoryLayer({ ttl:
|
|
385
|
-
new RedisLayer({ client: redis, ttl:
|
|
385
|
+
new MemoryLayer({ ttl: 60_000, maxSize: 10_000 }),
|
|
386
|
+
new RedisLayer({ client: redis, ttl: 3_600_000, prefix: 'myapp:cache:' })
|
|
386
387
|
],
|
|
387
388
|
{
|
|
388
389
|
invalidationBus: bus,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PatternMatcher
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KJDFYE5T.js";
|
|
4
4
|
|
|
5
5
|
// src/internal/CacheStackValidation.ts
|
|
6
6
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
@@ -125,6 +125,19 @@ function validateCircuitBreakerOptions(options) {
|
|
|
125
125
|
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
126
126
|
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
127
127
|
}
|
|
128
|
+
function validateContextEntryOptions(name, options) {
|
|
129
|
+
if (!options) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
validateLayerNumberOption(`${name}.ttl`, options.ttl);
|
|
133
|
+
validateLayerNumberOption(`${name}.negativeTtl`, options.negativeTtl);
|
|
134
|
+
validateLayerNumberOption(`${name}.staleWhileRevalidate`, options.staleWhileRevalidate);
|
|
135
|
+
validateLayerNumberOption(`${name}.staleIfError`, options.staleIfError);
|
|
136
|
+
validateLayerNumberOption(`${name}.ttlJitter`, options.ttlJitter);
|
|
137
|
+
validateTtlPolicy(`${name}.ttlPolicy`, options.ttlPolicy);
|
|
138
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
139
|
+
validateTags(options.tags);
|
|
140
|
+
}
|
|
128
141
|
|
|
129
142
|
// src/invalidation/RedisTagIndex.ts
|
|
130
143
|
var RedisTagIndex = class {
|
|
@@ -318,5 +331,6 @@ export {
|
|
|
318
331
|
validateTtlPolicy,
|
|
319
332
|
validateAdaptiveTtlOptions,
|
|
320
333
|
validateCircuitBreakerOptions,
|
|
334
|
+
validateContextEntryOptions,
|
|
321
335
|
RedisTagIndex
|
|
322
336
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
unwrapStoredValue
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KJDFYE5T.js";
|
|
4
4
|
|
|
5
5
|
// src/layers/MemoryLayer.ts
|
|
6
6
|
var MemoryLayer = class {
|
|
@@ -57,7 +57,7 @@ var MemoryLayer = class {
|
|
|
57
57
|
this.entries.delete(key);
|
|
58
58
|
this.entries.set(key, {
|
|
59
59
|
value,
|
|
60
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl
|
|
60
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null,
|
|
61
61
|
accessCount: 0,
|
|
62
62
|
insertedAt: Date.now()
|
|
63
63
|
});
|
|
@@ -88,7 +88,7 @@ var MemoryLayer = class {
|
|
|
88
88
|
if (entry.expiresAt === null) {
|
|
89
89
|
return null;
|
|
90
90
|
}
|
|
91
|
-
return Math.max(0, Math.ceil(
|
|
91
|
+
return Math.max(0, Math.ceil(entry.expiresAt - Date.now()));
|
|
92
92
|
}
|
|
93
93
|
async size() {
|
|
94
94
|
this.pruneExpired();
|
|
@@ -38,29 +38,29 @@ function isStoredValueEnvelope(value) {
|
|
|
38
38
|
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
41
|
+
const maxTtlMs = 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
42
|
+
if (!isValidEnvelopeTtlMs(v.freshTtlMs, maxTtlMs)) {
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
|
-
if (!
|
|
45
|
+
if (!isValidEnvelopeTtlMs(v.staleWhileRevalidateMs, maxTtlMs)) {
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
|
-
if (!
|
|
48
|
+
if (!isValidEnvelopeTtlMs(v.staleIfErrorMs, maxTtlMs)) {
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
|
-
if (v.
|
|
51
|
+
if (v.freshTtlMs == null && (v.staleWhileRevalidateMs != null || v.staleIfErrorMs != null)) {
|
|
52
52
|
return false;
|
|
53
53
|
}
|
|
54
54
|
return true;
|
|
55
55
|
}
|
|
56
56
|
function createStoredValueEnvelope(options) {
|
|
57
57
|
const now = options.now ?? Date.now();
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const freshUntil =
|
|
62
|
-
const staleUntil = freshUntil &&
|
|
63
|
-
const errorUntil = freshUntil &&
|
|
58
|
+
const freshTtlMs = normalizePositiveMs(options.freshTtlMs);
|
|
59
|
+
const staleWhileRevalidateMs = normalizePositiveMs(options.staleWhileRevalidateMs);
|
|
60
|
+
const staleIfErrorMs = normalizePositiveMs(options.staleIfErrorMs);
|
|
61
|
+
const freshUntil = freshTtlMs ? now + freshTtlMs : null;
|
|
62
|
+
const staleUntil = freshUntil && staleWhileRevalidateMs ? freshUntil + staleWhileRevalidateMs : null;
|
|
63
|
+
const errorUntil = freshUntil && staleIfErrorMs ? freshUntil + staleIfErrorMs : null;
|
|
64
64
|
return {
|
|
65
65
|
__layercache: 1,
|
|
66
66
|
kind: options.kind,
|
|
@@ -68,9 +68,9 @@ function createStoredValueEnvelope(options) {
|
|
|
68
68
|
freshUntil,
|
|
69
69
|
staleUntil,
|
|
70
70
|
errorUntil,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
freshTtlMs: freshTtlMs ?? null,
|
|
72
|
+
staleWhileRevalidateMs: staleWhileRevalidateMs ?? null,
|
|
73
|
+
staleIfErrorMs: staleIfErrorMs ?? null
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -97,7 +97,7 @@ function unwrapStoredValue(stored) {
|
|
|
97
97
|
}
|
|
98
98
|
return stored.value ?? null;
|
|
99
99
|
}
|
|
100
|
-
function
|
|
100
|
+
function remainingStoredTtlMs(stored, now = Date.now()) {
|
|
101
101
|
if (!isStoredValueEnvelope(stored)) {
|
|
102
102
|
return void 0;
|
|
103
103
|
}
|
|
@@ -109,9 +109,9 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
109
109
|
if (remainingMs <= 0) {
|
|
110
110
|
return 1;
|
|
111
111
|
}
|
|
112
|
-
return Math.max(1, Math.ceil(remainingMs
|
|
112
|
+
return Math.max(1, Math.ceil(remainingMs));
|
|
113
113
|
}
|
|
114
|
-
function
|
|
114
|
+
function remainingFreshTtlMs(stored, now = Date.now()) {
|
|
115
115
|
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
116
116
|
return void 0;
|
|
117
117
|
}
|
|
@@ -119,7 +119,7 @@ function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
|
119
119
|
if (remainingMs <= 0) {
|
|
120
120
|
return 0;
|
|
121
121
|
}
|
|
122
|
-
return Math.max(1, Math.ceil(remainingMs
|
|
122
|
+
return Math.max(1, Math.ceil(remainingMs));
|
|
123
123
|
}
|
|
124
124
|
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
125
125
|
if (!isStoredValueEnvelope(stored)) {
|
|
@@ -128,12 +128,23 @@ function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
|
128
128
|
return createStoredValueEnvelope({
|
|
129
129
|
kind: stored.kind,
|
|
130
130
|
value: stored.value,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
freshTtlMs: stored.freshTtlMs ?? void 0,
|
|
132
|
+
staleWhileRevalidateMs: stored.staleWhileRevalidateMs ?? void 0,
|
|
133
|
+
staleIfErrorMs: stored.staleIfErrorMs ?? void 0,
|
|
134
134
|
now
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
+
function expireStoredEnvelope(stored, now = Date.now()) {
|
|
138
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
139
|
+
return stored;
|
|
140
|
+
}
|
|
141
|
+
const futureDeadlines = [stored.staleUntil, stored.errorUntil].filter((value) => value !== null);
|
|
142
|
+
const freshUntil = futureDeadlines.length > 0 ? Math.min(now, ...futureDeadlines) : now;
|
|
143
|
+
return {
|
|
144
|
+
...stored,
|
|
145
|
+
freshUntil
|
|
146
|
+
};
|
|
147
|
+
}
|
|
137
148
|
function maxExpiry(stored) {
|
|
138
149
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
139
150
|
(value) => value !== null
|
|
@@ -143,17 +154,17 @@ function maxExpiry(stored) {
|
|
|
143
154
|
}
|
|
144
155
|
return Math.max(...values);
|
|
145
156
|
}
|
|
146
|
-
function
|
|
157
|
+
function normalizePositiveMs(value) {
|
|
147
158
|
if (!value || value <= 0) {
|
|
148
159
|
return void 0;
|
|
149
160
|
}
|
|
150
161
|
return value;
|
|
151
162
|
}
|
|
152
|
-
function
|
|
163
|
+
function isValidEnvelopeTtlMs(value, maxTtlMs) {
|
|
153
164
|
if (value == null) {
|
|
154
165
|
return true;
|
|
155
166
|
}
|
|
156
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <=
|
|
167
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlMs;
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
// src/invalidation/PatternMatcher.ts
|
|
@@ -209,8 +220,9 @@ export {
|
|
|
209
220
|
createStoredValueEnvelope,
|
|
210
221
|
resolveStoredValue,
|
|
211
222
|
unwrapStoredValue,
|
|
212
|
-
|
|
213
|
-
|
|
223
|
+
remainingStoredTtlMs,
|
|
224
|
+
remainingFreshTtlMs,
|
|
214
225
|
refreshStoredEnvelope,
|
|
226
|
+
expireStoredEnvelope,
|
|
215
227
|
PatternMatcher
|
|
216
228
|
};
|
package/dist/cli.cjs
CHANGED
|
@@ -121,17 +121,17 @@ function isStoredValueEnvelope(value) {
|
|
|
121
121
|
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
122
122
|
return false;
|
|
123
123
|
}
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
124
|
+
const maxTtlMs = 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
125
|
+
if (!isValidEnvelopeTtlMs(v.freshTtlMs, maxTtlMs)) {
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
|
-
if (!
|
|
128
|
+
if (!isValidEnvelopeTtlMs(v.staleWhileRevalidateMs, maxTtlMs)) {
|
|
129
129
|
return false;
|
|
130
130
|
}
|
|
131
|
-
if (!
|
|
131
|
+
if (!isValidEnvelopeTtlMs(v.staleIfErrorMs, maxTtlMs)) {
|
|
132
132
|
return false;
|
|
133
133
|
}
|
|
134
|
-
if (v.
|
|
134
|
+
if (v.freshTtlMs == null && (v.staleWhileRevalidateMs != null || v.staleIfErrorMs != null)) {
|
|
135
135
|
return false;
|
|
136
136
|
}
|
|
137
137
|
return true;
|
|
@@ -160,11 +160,11 @@ function unwrapStoredValue(stored) {
|
|
|
160
160
|
}
|
|
161
161
|
return stored.value ?? null;
|
|
162
162
|
}
|
|
163
|
-
function
|
|
163
|
+
function isValidEnvelopeTtlMs(value, maxTtlMs) {
|
|
164
164
|
if (value == null) {
|
|
165
165
|
return true;
|
|
166
166
|
}
|
|
167
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <=
|
|
167
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlMs;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// src/invalidation/PatternMatcher.ts
|
|
@@ -483,14 +483,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
483
483
|
}
|
|
484
484
|
if (!validateCliInput(args.key, validateCacheKey)) return;
|
|
485
485
|
const payload = await redis.getBuffer(args.key);
|
|
486
|
-
const ttl = await redis.
|
|
486
|
+
const ttl = await redis.pttl(args.key);
|
|
487
487
|
const decoded = decodeInspectablePayload(payload);
|
|
488
488
|
process.stdout.write(
|
|
489
489
|
`${JSON.stringify(
|
|
490
490
|
{
|
|
491
491
|
key: args.key,
|
|
492
492
|
exists: payload !== null,
|
|
493
|
-
|
|
493
|
+
ttlMs: ttl >= 0 ? ttl : null,
|
|
494
494
|
sizeBytes: payload?.byteLength ?? 0,
|
|
495
495
|
isEnvelope: isStoredValueEnvelope(decoded),
|
|
496
496
|
state: payload === null ? null : resolveStoredValue(decoded).state,
|
package/dist/cli.js
CHANGED
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
validateCacheKey,
|
|
5
5
|
validatePattern,
|
|
6
6
|
validateTag
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7KMKQ6QZ.js";
|
|
8
8
|
import {
|
|
9
9
|
isStoredValueEnvelope,
|
|
10
10
|
resolveStoredValue
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-KJDFYE5T.js";
|
|
12
12
|
|
|
13
13
|
// src/cli.ts
|
|
14
14
|
import Redis from "ioredis";
|
|
@@ -99,14 +99,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
99
99
|
}
|
|
100
100
|
if (!validateCliInput(args.key, validateCacheKey)) return;
|
|
101
101
|
const payload = await redis.getBuffer(args.key);
|
|
102
|
-
const ttl = await redis.
|
|
102
|
+
const ttl = await redis.pttl(args.key);
|
|
103
103
|
const decoded = decodeInspectablePayload(payload);
|
|
104
104
|
process.stdout.write(
|
|
105
105
|
`${JSON.stringify(
|
|
106
106
|
{
|
|
107
107
|
key: args.key,
|
|
108
108
|
exists: payload !== null,
|
|
109
|
-
|
|
109
|
+
ttlMs: ttl >= 0 ? ttl : null,
|
|
110
110
|
sizeBytes: payload?.byteLength ?? 0,
|
|
111
111
|
isEnvelope: isStoredValueEnvelope(decoded),
|
|
112
112
|
state: payload === null ? null : resolveStoredValue(decoded).state,
|