layercache 1.2.1 → 1.2.3
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 +66 -11
- package/dist/{chunk-ZMDB5KOK.js → chunk-7V7XAB74.js} +24 -1
- package/dist/{chunk-46UH7LNM.js → chunk-KOYGHLVP.js} +142 -18
- package/dist/{chunk-GF47Y3XR.js → chunk-QHWG7QS5.js} +56 -25
- package/dist/cli.cjs +92 -27
- package/dist/cli.js +15 -4
- package/dist/{edge-C1sBhTfv.d.ts → edge-B_rUqDy6.d.cts} +39 -1
- package/dist/{edge-C1sBhTfv.d.cts → edge-B_rUqDy6.d.ts} +39 -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 +798 -127
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +582 -90
- package/package.json +5 -5
- package/packages/nestjs/dist/index.cjs +582 -61
- package/packages/nestjs/dist/index.d.cts +30 -0
- package/packages/nestjs/dist/index.d.ts +30 -0
- package/packages/nestjs/dist/index.js +582 -61
package/dist/cli.cjs
CHANGED
|
@@ -38,7 +38,30 @@ var import_ioredis = __toESM(require("ioredis"), 1);
|
|
|
38
38
|
|
|
39
39
|
// src/internal/StoredValue.ts
|
|
40
40
|
function isStoredValueEnvelope(value) {
|
|
41
|
-
|
|
41
|
+
if (typeof value !== "object" || value === null) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const v = value;
|
|
45
|
+
if (v.__layercache !== 1) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
61
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
42
65
|
}
|
|
43
66
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
44
67
|
if (!isStoredValueEnvelope(stored)) {
|
|
@@ -118,19 +141,21 @@ var RedisTagIndex = class {
|
|
|
118
141
|
client;
|
|
119
142
|
prefix;
|
|
120
143
|
scanCount;
|
|
144
|
+
knownKeysShards;
|
|
121
145
|
constructor(options) {
|
|
122
146
|
this.client = options.client;
|
|
123
147
|
this.prefix = options.prefix ?? "layercache:tag-index";
|
|
124
148
|
this.scanCount = options.scanCount ?? 100;
|
|
149
|
+
this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
|
|
125
150
|
}
|
|
126
151
|
async touch(key) {
|
|
127
|
-
await this.client.sadd(this.
|
|
152
|
+
await this.client.sadd(this.knownKeysKeyFor(key), key);
|
|
128
153
|
}
|
|
129
154
|
async track(key, tags) {
|
|
130
155
|
const keyTagsKey = this.keyTagsKey(key);
|
|
131
156
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
132
157
|
const pipeline = this.client.pipeline();
|
|
133
|
-
pipeline.sadd(this.
|
|
158
|
+
pipeline.sadd(this.knownKeysKeyFor(key), key);
|
|
134
159
|
for (const tag of existingTags) {
|
|
135
160
|
pipeline.srem(this.tagKeysKey(tag), key);
|
|
136
161
|
}
|
|
@@ -147,7 +172,7 @@ var RedisTagIndex = class {
|
|
|
147
172
|
const keyTagsKey = this.keyTagsKey(key);
|
|
148
173
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
149
174
|
const pipeline = this.client.pipeline();
|
|
150
|
-
pipeline.srem(this.
|
|
175
|
+
pipeline.srem(this.knownKeysKeyFor(key), key);
|
|
151
176
|
pipeline.del(keyTagsKey);
|
|
152
177
|
for (const tag of existingTags) {
|
|
153
178
|
pipeline.srem(this.tagKeysKey(tag), key);
|
|
@@ -159,12 +184,14 @@ var RedisTagIndex = class {
|
|
|
159
184
|
}
|
|
160
185
|
async keysForPrefix(prefix) {
|
|
161
186
|
const matches = [];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
187
|
+
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
188
|
+
let cursor = "0";
|
|
189
|
+
do {
|
|
190
|
+
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
191
|
+
cursor = nextCursor;
|
|
192
|
+
matches.push(...keys.filter((key) => key.startsWith(prefix)));
|
|
193
|
+
} while (cursor !== "0");
|
|
194
|
+
}
|
|
168
195
|
return matches;
|
|
169
196
|
}
|
|
170
197
|
async tagsForKey(key) {
|
|
@@ -172,19 +199,21 @@ var RedisTagIndex = class {
|
|
|
172
199
|
}
|
|
173
200
|
async matchPattern(pattern) {
|
|
174
201
|
const matches = [];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
203
|
+
let cursor = "0";
|
|
204
|
+
do {
|
|
205
|
+
const [nextCursor, keys] = await this.client.sscan(
|
|
206
|
+
knownKeysKey,
|
|
207
|
+
cursor,
|
|
208
|
+
"MATCH",
|
|
209
|
+
pattern,
|
|
210
|
+
"COUNT",
|
|
211
|
+
this.scanCount
|
|
212
|
+
);
|
|
213
|
+
cursor = nextCursor;
|
|
214
|
+
matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
|
|
215
|
+
} while (cursor !== "0");
|
|
216
|
+
}
|
|
188
217
|
return matches;
|
|
189
218
|
}
|
|
190
219
|
async clear() {
|
|
@@ -205,8 +234,17 @@ var RedisTagIndex = class {
|
|
|
205
234
|
} while (cursor !== "0");
|
|
206
235
|
return matches;
|
|
207
236
|
}
|
|
208
|
-
|
|
209
|
-
|
|
237
|
+
knownKeysKeyFor(key) {
|
|
238
|
+
if (this.knownKeysShards === 1) {
|
|
239
|
+
return `${this.prefix}:keys`;
|
|
240
|
+
}
|
|
241
|
+
return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
|
|
242
|
+
}
|
|
243
|
+
knownKeysKeys() {
|
|
244
|
+
if (this.knownKeysShards === 1) {
|
|
245
|
+
return [`${this.prefix}:keys`];
|
|
246
|
+
}
|
|
247
|
+
return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
|
|
210
248
|
}
|
|
211
249
|
keyTagsKey(key) {
|
|
212
250
|
return `${this.prefix}:key:${encodeURIComponent(key)}`;
|
|
@@ -215,6 +253,22 @@ var RedisTagIndex = class {
|
|
|
215
253
|
return `${this.prefix}:tag:${encodeURIComponent(tag)}`;
|
|
216
254
|
}
|
|
217
255
|
};
|
|
256
|
+
function normalizeKnownKeysShards(value) {
|
|
257
|
+
if (value === void 0) {
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
261
|
+
throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
|
|
262
|
+
}
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
function simpleHash(value) {
|
|
266
|
+
let hash = 0;
|
|
267
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
268
|
+
hash = hash * 31 + value.charCodeAt(index) >>> 0;
|
|
269
|
+
}
|
|
270
|
+
return hash;
|
|
271
|
+
}
|
|
218
272
|
|
|
219
273
|
// src/cli.ts
|
|
220
274
|
var CONNECT_TIMEOUT_MS = 5e3;
|
|
@@ -228,7 +282,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
228
282
|
const redisUrl = validateRedisUrl(args.redisUrl);
|
|
229
283
|
if (!redisUrl) {
|
|
230
284
|
process.stderr.write(
|
|
231
|
-
`Error: invalid Redis URL "${args.redisUrl}". Expected format: redis://[user:password@]host[:port][/db]
|
|
285
|
+
`Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
|
|
232
286
|
`
|
|
233
287
|
);
|
|
234
288
|
process.exitCode = 1;
|
|
@@ -242,7 +296,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
242
296
|
try {
|
|
243
297
|
await redis.connect().catch((error) => {
|
|
244
298
|
const message = error instanceof Error ? error.message : String(error);
|
|
245
|
-
throw new Error(`Failed to connect to Redis at "${redisUrl}": ${message}`);
|
|
299
|
+
throw new Error(`Failed to connect to Redis at "${maskRedisUrl(redisUrl)}": ${message}`);
|
|
246
300
|
});
|
|
247
301
|
if (args.command === "stats") {
|
|
248
302
|
const keys = await scanKeys(redis, args.pattern ?? "*");
|
|
@@ -397,6 +451,17 @@ function summarizeInspectableValue(value) {
|
|
|
397
451
|
}
|
|
398
452
|
return value;
|
|
399
453
|
}
|
|
454
|
+
function maskRedisUrl(url) {
|
|
455
|
+
try {
|
|
456
|
+
const parsed = new URL(url);
|
|
457
|
+
if (parsed.password) {
|
|
458
|
+
parsed.password = "***";
|
|
459
|
+
}
|
|
460
|
+
return parsed.toString();
|
|
461
|
+
} catch {
|
|
462
|
+
return url.replace(/:([^@/]+)@/, ":***@");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
400
465
|
if (process.argv[1]?.includes("cli.")) {
|
|
401
466
|
void main();
|
|
402
467
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
RedisTagIndex
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QHWG7QS5.js";
|
|
5
5
|
import {
|
|
6
6
|
isStoredValueEnvelope,
|
|
7
7
|
resolveStoredValue
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-7V7XAB74.js";
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
11
|
import Redis from "ioredis";
|
|
@@ -20,7 +20,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
20
20
|
const redisUrl = validateRedisUrl(args.redisUrl);
|
|
21
21
|
if (!redisUrl) {
|
|
22
22
|
process.stderr.write(
|
|
23
|
-
`Error: invalid Redis URL "${args.redisUrl}". Expected format: redis://[user:password@]host[:port][/db]
|
|
23
|
+
`Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
|
|
24
24
|
`
|
|
25
25
|
);
|
|
26
26
|
process.exitCode = 1;
|
|
@@ -34,7 +34,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
34
34
|
try {
|
|
35
35
|
await redis.connect().catch((error) => {
|
|
36
36
|
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
-
throw new Error(`Failed to connect to Redis at "${redisUrl}": ${message}`);
|
|
37
|
+
throw new Error(`Failed to connect to Redis at "${maskRedisUrl(redisUrl)}": ${message}`);
|
|
38
38
|
});
|
|
39
39
|
if (args.command === "stats") {
|
|
40
40
|
const keys = await scanKeys(redis, args.pattern ?? "*");
|
|
@@ -189,6 +189,17 @@ function summarizeInspectableValue(value) {
|
|
|
189
189
|
}
|
|
190
190
|
return value;
|
|
191
191
|
}
|
|
192
|
+
function maskRedisUrl(url) {
|
|
193
|
+
try {
|
|
194
|
+
const parsed = new URL(url);
|
|
195
|
+
if (parsed.password) {
|
|
196
|
+
parsed.password = "***";
|
|
197
|
+
}
|
|
198
|
+
return parsed.toString();
|
|
199
|
+
} catch {
|
|
200
|
+
return url.replace(/:([^@/]+)@/, ":***@");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
192
203
|
if (process.argv[1]?.includes("cli.")) {
|
|
193
204
|
void main();
|
|
194
205
|
}
|
|
@@ -165,6 +165,7 @@ interface CacheSingleFlightExecutionOptions {
|
|
|
165
165
|
leaseMs: number;
|
|
166
166
|
waitTimeoutMs: number;
|
|
167
167
|
pollIntervalMs: number;
|
|
168
|
+
renewIntervalMs?: number;
|
|
168
169
|
}
|
|
169
170
|
interface CacheSingleFlightCoordinator {
|
|
170
171
|
execute<T>(key: string, options: CacheSingleFlightExecutionOptions, worker: () => Promise<T>, waiter: () => Promise<T>): Promise<T>;
|
|
@@ -176,6 +177,7 @@ interface CacheStackOptions {
|
|
|
176
177
|
invalidationBus?: InvalidationBus;
|
|
177
178
|
tagIndex?: CacheTagIndex;
|
|
178
179
|
generation?: number;
|
|
180
|
+
generationCleanup?: boolean | CacheGenerationCleanupOptions;
|
|
179
181
|
broadcastL1Invalidation?: boolean;
|
|
180
182
|
/**
|
|
181
183
|
* @deprecated Use `broadcastL1Invalidation` instead.
|
|
@@ -194,10 +196,13 @@ interface CacheStackOptions {
|
|
|
194
196
|
writeStrategy?: 'write-through' | 'write-behind';
|
|
195
197
|
writeBehind?: CacheWriteBehindOptions;
|
|
196
198
|
fetcherRateLimit?: CacheRateLimitOptions;
|
|
199
|
+
backgroundRefreshTimeoutMs?: number;
|
|
197
200
|
singleFlightCoordinator?: CacheSingleFlightCoordinator;
|
|
198
201
|
singleFlightLeaseMs?: number;
|
|
199
202
|
singleFlightTimeoutMs?: number;
|
|
200
203
|
singleFlightPollMs?: number;
|
|
204
|
+
singleFlightRenewIntervalMs?: number;
|
|
205
|
+
snapshotBaseDir?: string | false;
|
|
201
206
|
/**
|
|
202
207
|
* Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
|
|
203
208
|
* before the oldest entries are pruned. Prevents unbounded memory growth.
|
|
@@ -210,6 +215,9 @@ interface CacheAdaptiveTtlOptions {
|
|
|
210
215
|
step?: number | LayerTtlMap;
|
|
211
216
|
maxTtl?: number | LayerTtlMap;
|
|
212
217
|
}
|
|
218
|
+
interface CacheGenerationCleanupOptions {
|
|
219
|
+
batchSize?: number;
|
|
220
|
+
}
|
|
213
221
|
type CacheTtlPolicy = 'until-midnight' | 'next-hour' | {
|
|
214
222
|
alignTo: number;
|
|
215
223
|
} | ((context: CacheTtlPolicyContext) => number | undefined);
|
|
@@ -228,6 +236,8 @@ interface CacheRateLimitOptions {
|
|
|
228
236
|
maxConcurrent?: number;
|
|
229
237
|
intervalMs?: number;
|
|
230
238
|
maxPerInterval?: number;
|
|
239
|
+
scope?: 'global' | 'key' | 'fetcher';
|
|
240
|
+
bucketKey?: string;
|
|
231
241
|
}
|
|
232
242
|
interface CacheWriteBehindOptions {
|
|
233
243
|
flushIntervalMs?: number;
|
|
@@ -411,7 +421,7 @@ interface TagIndexOptions {
|
|
|
411
421
|
/**
|
|
412
422
|
* Maximum number of keys tracked in `knownKeys`. When exceeded, the oldest
|
|
413
423
|
* 10 % of keys are pruned to keep memory bounded.
|
|
414
|
-
* Defaults to
|
|
424
|
+
* Defaults to 100,000.
|
|
415
425
|
*/
|
|
416
426
|
maxKnownKeys?: number;
|
|
417
427
|
}
|
|
@@ -420,6 +430,8 @@ declare class TagIndex implements CacheTagIndex {
|
|
|
420
430
|
private readonly keyToTags;
|
|
421
431
|
private readonly knownKeys;
|
|
422
432
|
private readonly maxKnownKeys;
|
|
433
|
+
private nextNodeId;
|
|
434
|
+
private readonly root;
|
|
423
435
|
constructor(options?: TagIndexOptions);
|
|
424
436
|
touch(key: string): Promise<void>;
|
|
425
437
|
track(key: string, tags: string[]): Promise<void>;
|
|
@@ -429,8 +441,14 @@ declare class TagIndex implements CacheTagIndex {
|
|
|
429
441
|
tagsForKey(key: string): Promise<string[]>;
|
|
430
442
|
matchPattern(pattern: string): Promise<string[]>;
|
|
431
443
|
clear(): Promise<void>;
|
|
444
|
+
private createTrieNode;
|
|
445
|
+
private insertKnownKey;
|
|
446
|
+
private findNode;
|
|
447
|
+
private collectFromNode;
|
|
448
|
+
private collectPatternMatches;
|
|
432
449
|
private pruneKnownKeysIfNeeded;
|
|
433
450
|
private removeKey;
|
|
451
|
+
private removeKnownKey;
|
|
434
452
|
}
|
|
435
453
|
|
|
436
454
|
declare class CacheNamespace {
|
|
@@ -501,6 +519,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
501
519
|
private readonly logger;
|
|
502
520
|
private readonly tagIndex;
|
|
503
521
|
private readonly fetchRateLimiter;
|
|
522
|
+
private readonly snapshotSerializer;
|
|
504
523
|
private readonly backgroundRefreshes;
|
|
505
524
|
private readonly layerDegradedUntil;
|
|
506
525
|
private readonly ttlResolver;
|
|
@@ -509,6 +528,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
509
528
|
private readonly writeBehindQueue;
|
|
510
529
|
private writeBehindTimer?;
|
|
511
530
|
private writeBehindFlushPromise?;
|
|
531
|
+
private generationCleanupPromise?;
|
|
512
532
|
private isDisconnecting;
|
|
513
533
|
private disconnectPromise?;
|
|
514
534
|
constructor(layers: CacheLayer[], options?: CacheStackOptions);
|
|
@@ -519,6 +539,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
519
539
|
* and no `fetcher` is provided.
|
|
520
540
|
*/
|
|
521
541
|
get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
542
|
+
private getPrepared;
|
|
522
543
|
/**
|
|
523
544
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
524
545
|
* Fetches and caches the value if not already present.
|
|
@@ -577,6 +598,11 @@ declare class CacheStack extends EventEmitter {
|
|
|
577
598
|
*/
|
|
578
599
|
getHitRate(): CacheHitRateSnapshot;
|
|
579
600
|
healthCheck(): Promise<CacheHealthCheckResult[]>;
|
|
601
|
+
/**
|
|
602
|
+
* Rotates the active generation prefix used for all future cache keys.
|
|
603
|
+
* Previous-generation keys remain in the underlying layers until they expire,
|
|
604
|
+
* unless `generationCleanup` is enabled to prune them in the background.
|
|
605
|
+
*/
|
|
580
606
|
bumpGeneration(nextGeneration?: number): number;
|
|
581
607
|
/**
|
|
582
608
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
@@ -604,6 +630,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
604
630
|
private resolveLayerSeconds;
|
|
605
631
|
private shouldNegativeCache;
|
|
606
632
|
private scheduleBackgroundRefresh;
|
|
633
|
+
private runBackgroundRefresh;
|
|
607
634
|
private resolveSingleFlightOptions;
|
|
608
635
|
private deleteKeys;
|
|
609
636
|
private publishInvalidation;
|
|
@@ -611,7 +638,14 @@ declare class CacheStack extends EventEmitter {
|
|
|
611
638
|
private getTagsForKey;
|
|
612
639
|
private formatError;
|
|
613
640
|
private sleep;
|
|
641
|
+
private withTimeout;
|
|
614
642
|
private shouldBroadcastL1Invalidation;
|
|
643
|
+
private collectKeysWithPrefix;
|
|
644
|
+
private collectKeysMatchingPattern;
|
|
645
|
+
private shouldCleanupGenerations;
|
|
646
|
+
private generationCleanupBatchSize;
|
|
647
|
+
private scheduleGenerationCleanup;
|
|
648
|
+
private cleanupGeneration;
|
|
615
649
|
private initializeWriteBehind;
|
|
616
650
|
private shouldWriteBehind;
|
|
617
651
|
private enqueueWriteBehind;
|
|
@@ -627,6 +661,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
627
661
|
private validateWriteOptions;
|
|
628
662
|
private validateLayerNumberOption;
|
|
629
663
|
private validatePositiveNumber;
|
|
664
|
+
private validateRateLimitOptions;
|
|
630
665
|
private validateNonNegativeNumber;
|
|
631
666
|
private validateCacheKey;
|
|
632
667
|
private validateTtlPolicy;
|
|
@@ -638,12 +673,15 @@ declare class CacheStack extends EventEmitter {
|
|
|
638
673
|
private applyFreshReadPolicies;
|
|
639
674
|
private shouldSkipLayer;
|
|
640
675
|
private handleLayerFailure;
|
|
676
|
+
private reportRecoverableLayerFailure;
|
|
641
677
|
private isGracefulDegradationEnabled;
|
|
642
678
|
private recordCircuitFailure;
|
|
643
679
|
private isNegativeStoredValue;
|
|
644
680
|
private emitError;
|
|
645
681
|
private serializeKeyPart;
|
|
646
682
|
private isCacheSnapshotEntries;
|
|
683
|
+
private sanitizeSnapshotValue;
|
|
684
|
+
private validateSnapshotFilePath;
|
|
647
685
|
private normalizeForSerialization;
|
|
648
686
|
}
|
|
649
687
|
|
|
@@ -165,6 +165,7 @@ interface CacheSingleFlightExecutionOptions {
|
|
|
165
165
|
leaseMs: number;
|
|
166
166
|
waitTimeoutMs: number;
|
|
167
167
|
pollIntervalMs: number;
|
|
168
|
+
renewIntervalMs?: number;
|
|
168
169
|
}
|
|
169
170
|
interface CacheSingleFlightCoordinator {
|
|
170
171
|
execute<T>(key: string, options: CacheSingleFlightExecutionOptions, worker: () => Promise<T>, waiter: () => Promise<T>): Promise<T>;
|
|
@@ -176,6 +177,7 @@ interface CacheStackOptions {
|
|
|
176
177
|
invalidationBus?: InvalidationBus;
|
|
177
178
|
tagIndex?: CacheTagIndex;
|
|
178
179
|
generation?: number;
|
|
180
|
+
generationCleanup?: boolean | CacheGenerationCleanupOptions;
|
|
179
181
|
broadcastL1Invalidation?: boolean;
|
|
180
182
|
/**
|
|
181
183
|
* @deprecated Use `broadcastL1Invalidation` instead.
|
|
@@ -194,10 +196,13 @@ interface CacheStackOptions {
|
|
|
194
196
|
writeStrategy?: 'write-through' | 'write-behind';
|
|
195
197
|
writeBehind?: CacheWriteBehindOptions;
|
|
196
198
|
fetcherRateLimit?: CacheRateLimitOptions;
|
|
199
|
+
backgroundRefreshTimeoutMs?: number;
|
|
197
200
|
singleFlightCoordinator?: CacheSingleFlightCoordinator;
|
|
198
201
|
singleFlightLeaseMs?: number;
|
|
199
202
|
singleFlightTimeoutMs?: number;
|
|
200
203
|
singleFlightPollMs?: number;
|
|
204
|
+
singleFlightRenewIntervalMs?: number;
|
|
205
|
+
snapshotBaseDir?: string | false;
|
|
201
206
|
/**
|
|
202
207
|
* Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
|
|
203
208
|
* before the oldest entries are pruned. Prevents unbounded memory growth.
|
|
@@ -210,6 +215,9 @@ interface CacheAdaptiveTtlOptions {
|
|
|
210
215
|
step?: number | LayerTtlMap;
|
|
211
216
|
maxTtl?: number | LayerTtlMap;
|
|
212
217
|
}
|
|
218
|
+
interface CacheGenerationCleanupOptions {
|
|
219
|
+
batchSize?: number;
|
|
220
|
+
}
|
|
213
221
|
type CacheTtlPolicy = 'until-midnight' | 'next-hour' | {
|
|
214
222
|
alignTo: number;
|
|
215
223
|
} | ((context: CacheTtlPolicyContext) => number | undefined);
|
|
@@ -228,6 +236,8 @@ interface CacheRateLimitOptions {
|
|
|
228
236
|
maxConcurrent?: number;
|
|
229
237
|
intervalMs?: number;
|
|
230
238
|
maxPerInterval?: number;
|
|
239
|
+
scope?: 'global' | 'key' | 'fetcher';
|
|
240
|
+
bucketKey?: string;
|
|
231
241
|
}
|
|
232
242
|
interface CacheWriteBehindOptions {
|
|
233
243
|
flushIntervalMs?: number;
|
|
@@ -411,7 +421,7 @@ interface TagIndexOptions {
|
|
|
411
421
|
/**
|
|
412
422
|
* Maximum number of keys tracked in `knownKeys`. When exceeded, the oldest
|
|
413
423
|
* 10 % of keys are pruned to keep memory bounded.
|
|
414
|
-
* Defaults to
|
|
424
|
+
* Defaults to 100,000.
|
|
415
425
|
*/
|
|
416
426
|
maxKnownKeys?: number;
|
|
417
427
|
}
|
|
@@ -420,6 +430,8 @@ declare class TagIndex implements CacheTagIndex {
|
|
|
420
430
|
private readonly keyToTags;
|
|
421
431
|
private readonly knownKeys;
|
|
422
432
|
private readonly maxKnownKeys;
|
|
433
|
+
private nextNodeId;
|
|
434
|
+
private readonly root;
|
|
423
435
|
constructor(options?: TagIndexOptions);
|
|
424
436
|
touch(key: string): Promise<void>;
|
|
425
437
|
track(key: string, tags: string[]): Promise<void>;
|
|
@@ -429,8 +441,14 @@ declare class TagIndex implements CacheTagIndex {
|
|
|
429
441
|
tagsForKey(key: string): Promise<string[]>;
|
|
430
442
|
matchPattern(pattern: string): Promise<string[]>;
|
|
431
443
|
clear(): Promise<void>;
|
|
444
|
+
private createTrieNode;
|
|
445
|
+
private insertKnownKey;
|
|
446
|
+
private findNode;
|
|
447
|
+
private collectFromNode;
|
|
448
|
+
private collectPatternMatches;
|
|
432
449
|
private pruneKnownKeysIfNeeded;
|
|
433
450
|
private removeKey;
|
|
451
|
+
private removeKnownKey;
|
|
434
452
|
}
|
|
435
453
|
|
|
436
454
|
declare class CacheNamespace {
|
|
@@ -501,6 +519,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
501
519
|
private readonly logger;
|
|
502
520
|
private readonly tagIndex;
|
|
503
521
|
private readonly fetchRateLimiter;
|
|
522
|
+
private readonly snapshotSerializer;
|
|
504
523
|
private readonly backgroundRefreshes;
|
|
505
524
|
private readonly layerDegradedUntil;
|
|
506
525
|
private readonly ttlResolver;
|
|
@@ -509,6 +528,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
509
528
|
private readonly writeBehindQueue;
|
|
510
529
|
private writeBehindTimer?;
|
|
511
530
|
private writeBehindFlushPromise?;
|
|
531
|
+
private generationCleanupPromise?;
|
|
512
532
|
private isDisconnecting;
|
|
513
533
|
private disconnectPromise?;
|
|
514
534
|
constructor(layers: CacheLayer[], options?: CacheStackOptions);
|
|
@@ -519,6 +539,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
519
539
|
* and no `fetcher` is provided.
|
|
520
540
|
*/
|
|
521
541
|
get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
542
|
+
private getPrepared;
|
|
522
543
|
/**
|
|
523
544
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
524
545
|
* Fetches and caches the value if not already present.
|
|
@@ -577,6 +598,11 @@ declare class CacheStack extends EventEmitter {
|
|
|
577
598
|
*/
|
|
578
599
|
getHitRate(): CacheHitRateSnapshot;
|
|
579
600
|
healthCheck(): Promise<CacheHealthCheckResult[]>;
|
|
601
|
+
/**
|
|
602
|
+
* Rotates the active generation prefix used for all future cache keys.
|
|
603
|
+
* Previous-generation keys remain in the underlying layers until they expire,
|
|
604
|
+
* unless `generationCleanup` is enabled to prune them in the background.
|
|
605
|
+
*/
|
|
580
606
|
bumpGeneration(nextGeneration?: number): number;
|
|
581
607
|
/**
|
|
582
608
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
@@ -604,6 +630,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
604
630
|
private resolveLayerSeconds;
|
|
605
631
|
private shouldNegativeCache;
|
|
606
632
|
private scheduleBackgroundRefresh;
|
|
633
|
+
private runBackgroundRefresh;
|
|
607
634
|
private resolveSingleFlightOptions;
|
|
608
635
|
private deleteKeys;
|
|
609
636
|
private publishInvalidation;
|
|
@@ -611,7 +638,14 @@ declare class CacheStack extends EventEmitter {
|
|
|
611
638
|
private getTagsForKey;
|
|
612
639
|
private formatError;
|
|
613
640
|
private sleep;
|
|
641
|
+
private withTimeout;
|
|
614
642
|
private shouldBroadcastL1Invalidation;
|
|
643
|
+
private collectKeysWithPrefix;
|
|
644
|
+
private collectKeysMatchingPattern;
|
|
645
|
+
private shouldCleanupGenerations;
|
|
646
|
+
private generationCleanupBatchSize;
|
|
647
|
+
private scheduleGenerationCleanup;
|
|
648
|
+
private cleanupGeneration;
|
|
615
649
|
private initializeWriteBehind;
|
|
616
650
|
private shouldWriteBehind;
|
|
617
651
|
private enqueueWriteBehind;
|
|
@@ -627,6 +661,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
627
661
|
private validateWriteOptions;
|
|
628
662
|
private validateLayerNumberOption;
|
|
629
663
|
private validatePositiveNumber;
|
|
664
|
+
private validateRateLimitOptions;
|
|
630
665
|
private validateNonNegativeNumber;
|
|
631
666
|
private validateCacheKey;
|
|
632
667
|
private validateTtlPolicy;
|
|
@@ -638,12 +673,15 @@ declare class CacheStack extends EventEmitter {
|
|
|
638
673
|
private applyFreshReadPolicies;
|
|
639
674
|
private shouldSkipLayer;
|
|
640
675
|
private handleLayerFailure;
|
|
676
|
+
private reportRecoverableLayerFailure;
|
|
641
677
|
private isGracefulDegradationEnabled;
|
|
642
678
|
private recordCircuitFailure;
|
|
643
679
|
private isNegativeStoredValue;
|
|
644
680
|
private emitError;
|
|
645
681
|
private serializeKeyPart;
|
|
646
682
|
private isCacheSnapshotEntries;
|
|
683
|
+
private sanitizeSnapshotValue;
|
|
684
|
+
private validateSnapshotFilePath;
|
|
647
685
|
private normalizeForSerialization;
|
|
648
686
|
}
|
|
649
687
|
|