layercache 2.1.0 → 3.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 +22 -6
- package/dist/{chunk-IVX6ABFX.js → chunk-5CIBABDH.js} +62 -19
- package/dist/{chunk-6X7NV5BG.js → chunk-NBMG7DHT.js} +85 -13
- package/dist/cli.cjs +153 -25
- package/dist/cli.js +69 -13
- package/dist/{edge-BCU8D-Yd.d.cts → edge-BDyuPmIq.d.cts} +5 -0
- package/dist/{edge-BCU8D-Yd.d.ts → edge-BDyuPmIq.d.ts} +5 -0
- package/dist/edge.cjs +61 -19
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +312 -82
- package/dist/index.d.cts +47 -3
- package/dist/index.d.ts +47 -3
- package/dist/index.js +163 -47
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ __export(index_exports, {
|
|
|
39
39
|
MemoryLayer: () => MemoryLayer,
|
|
40
40
|
MsgpackSerializer: () => MsgpackSerializer,
|
|
41
41
|
PatternMatcher: () => PatternMatcher,
|
|
42
|
+
RedisGenerationStore: () => RedisGenerationStore,
|
|
42
43
|
RedisInvalidationBus: () => RedisInvalidationBus,
|
|
43
44
|
RedisLayer: () => RedisLayer,
|
|
44
45
|
RedisSingleFlightCoordinator: () => RedisSingleFlightCoordinator,
|
|
@@ -1157,7 +1158,9 @@ var CacheStackMaintenance = class {
|
|
|
1157
1158
|
}
|
|
1158
1159
|
bumpKeyEpochs(keys) {
|
|
1159
1160
|
for (const key of keys) {
|
|
1160
|
-
|
|
1161
|
+
const nextEpoch = this.currentKeyEpoch(key) + 1;
|
|
1162
|
+
this.keyEpochs.delete(key);
|
|
1163
|
+
this.keyEpochs.set(key, nextEpoch);
|
|
1161
1164
|
}
|
|
1162
1165
|
this.pruneKeyEpochsIfNeeded();
|
|
1163
1166
|
}
|
|
@@ -1216,10 +1219,13 @@ var CacheStackMaintenance = class {
|
|
|
1216
1219
|
if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
|
|
1217
1220
|
return;
|
|
1218
1221
|
}
|
|
1219
|
-
const
|
|
1220
|
-
const toDelete = Math.ceil(sorted.length * 0.1);
|
|
1222
|
+
const toDelete = Math.ceil(this.keyEpochs.size * 0.1);
|
|
1221
1223
|
for (let i = 0; i < toDelete; i++) {
|
|
1222
|
-
this.keyEpochs.
|
|
1224
|
+
const oldestKey = this.keyEpochs.keys().next().value;
|
|
1225
|
+
if (oldestKey === void 0) {
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
this.keyEpochs.delete(oldestKey);
|
|
1223
1229
|
}
|
|
1224
1230
|
}
|
|
1225
1231
|
};
|
|
@@ -1353,22 +1359,28 @@ var CacheStackReader = class {
|
|
|
1353
1359
|
if (upToIndex < 0) {
|
|
1354
1360
|
return;
|
|
1355
1361
|
}
|
|
1362
|
+
const operations = [];
|
|
1356
1363
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
1357
1364
|
const layer = this.options.layers[index];
|
|
1358
1365
|
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1359
1366
|
continue;
|
|
1360
1367
|
}
|
|
1361
1368
|
const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1369
|
+
operations.push(
|
|
1370
|
+
(async () => {
|
|
1371
|
+
try {
|
|
1372
|
+
await layer.set(key, stored, ttl);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
await this.options.handleLayerFailure(layer, "backfill", error);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
this.options.metricsCollector.increment("backfills");
|
|
1378
|
+
this.options.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1379
|
+
this.options.emit("backfill", { key, layer: layer.name });
|
|
1380
|
+
})()
|
|
1381
|
+
);
|
|
1371
1382
|
}
|
|
1383
|
+
await Promise.all(operations);
|
|
1372
1384
|
}
|
|
1373
1385
|
abortAllRefreshes() {
|
|
1374
1386
|
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
@@ -1492,7 +1504,15 @@ var CacheStackReader = class {
|
|
|
1492
1504
|
}
|
|
1493
1505
|
await this.options.sleep(pollIntervalMs);
|
|
1494
1506
|
}
|
|
1495
|
-
|
|
1507
|
+
if (!this.options.singleFlightCoordinator) {
|
|
1508
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1509
|
+
}
|
|
1510
|
+
return this.options.singleFlightCoordinator.execute(
|
|
1511
|
+
key,
|
|
1512
|
+
this.resolveSingleFlightOptions(),
|
|
1513
|
+
() => this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext),
|
|
1514
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1515
|
+
);
|
|
1496
1516
|
}
|
|
1497
1517
|
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1498
1518
|
key,
|
|
@@ -2544,6 +2564,7 @@ var TtlResolver = class {
|
|
|
2544
2564
|
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
2545
2565
|
profile.hits += 1;
|
|
2546
2566
|
profile.lastAccessAt = Date.now();
|
|
2567
|
+
this.accessProfiles.delete(key);
|
|
2547
2568
|
this.accessProfiles.set(key, profile);
|
|
2548
2569
|
this.pruneIfNeeded();
|
|
2549
2570
|
}
|
|
@@ -2633,12 +2654,12 @@ var TtlResolver = class {
|
|
|
2633
2654
|
return;
|
|
2634
2655
|
}
|
|
2635
2656
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
this.accessProfiles.delete(entry[0]);
|
|
2657
|
+
for (let i = 0; i < toRemove; i++) {
|
|
2658
|
+
const oldestKey = this.accessProfiles.keys().next().value;
|
|
2659
|
+
if (oldestKey === void 0) {
|
|
2660
|
+
break;
|
|
2641
2661
|
}
|
|
2662
|
+
this.accessProfiles.delete(oldestKey);
|
|
2642
2663
|
}
|
|
2643
2664
|
}
|
|
2644
2665
|
};
|
|
@@ -2770,6 +2791,9 @@ var TagIndex = class {
|
|
|
2770
2791
|
}
|
|
2771
2792
|
insertKnownKey(key) {
|
|
2772
2793
|
const isNew = !this.knownKeys.has(key);
|
|
2794
|
+
if (!isNew) {
|
|
2795
|
+
this.knownKeys.delete(key);
|
|
2796
|
+
}
|
|
2773
2797
|
this.knownKeys.set(key, Date.now());
|
|
2774
2798
|
if (!isNew) {
|
|
2775
2799
|
return;
|
|
@@ -2868,13 +2892,13 @@ var TagIndex = class {
|
|
|
2868
2892
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
2869
2893
|
return;
|
|
2870
2894
|
}
|
|
2871
|
-
const sorted = [...this.knownKeys.entries()].sort((a, b) => a[1] - b[1]);
|
|
2872
2895
|
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
2873
|
-
for (let i = 0; i < toRemove
|
|
2874
|
-
const
|
|
2875
|
-
if (
|
|
2876
|
-
|
|
2896
|
+
for (let i = 0; i < toRemove; i += 1) {
|
|
2897
|
+
const oldestKey = this.knownKeys.keys().next().value;
|
|
2898
|
+
if (oldestKey === void 0) {
|
|
2899
|
+
break;
|
|
2877
2900
|
}
|
|
2901
|
+
this.removeKnownKey(oldestKey);
|
|
2878
2902
|
}
|
|
2879
2903
|
}
|
|
2880
2904
|
removeKey(key) {
|
|
@@ -3775,6 +3799,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3775
3799
|
}
|
|
3776
3800
|
return this.currentGeneration;
|
|
3777
3801
|
}
|
|
3802
|
+
/**
|
|
3803
|
+
* Returns the active generation prefix number used for future cache keys.
|
|
3804
|
+
*/
|
|
3805
|
+
getGeneration() {
|
|
3806
|
+
return this.currentGeneration;
|
|
3807
|
+
}
|
|
3778
3808
|
/**
|
|
3779
3809
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
3780
3810
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
@@ -4332,12 +4362,65 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4332
4362
|
}
|
|
4333
4363
|
};
|
|
4334
4364
|
|
|
4365
|
+
// src/generation/RedisGenerationStore.ts
|
|
4366
|
+
var DEFAULT_GENERATION_KEY = "layercache:generation";
|
|
4367
|
+
var RedisGenerationStore = class {
|
|
4368
|
+
client;
|
|
4369
|
+
key;
|
|
4370
|
+
constructor(options) {
|
|
4371
|
+
this.client = options.client;
|
|
4372
|
+
this.key = options.key ?? DEFAULT_GENERATION_KEY;
|
|
4373
|
+
}
|
|
4374
|
+
async get() {
|
|
4375
|
+
const stored = await this.client.get(this.key);
|
|
4376
|
+
if (stored === null) {
|
|
4377
|
+
return void 0;
|
|
4378
|
+
}
|
|
4379
|
+
return this.parseGeneration(stored);
|
|
4380
|
+
}
|
|
4381
|
+
async getOrInitialize(initialGeneration = 0) {
|
|
4382
|
+
this.assertGeneration(initialGeneration);
|
|
4383
|
+
await this.client.set(this.key, String(initialGeneration), "NX");
|
|
4384
|
+
const generation = await this.get();
|
|
4385
|
+
if (generation === void 0) {
|
|
4386
|
+
throw new Error(`RedisGenerationStore failed to initialize generation key "${this.key}".`);
|
|
4387
|
+
}
|
|
4388
|
+
return generation;
|
|
4389
|
+
}
|
|
4390
|
+
async set(generation) {
|
|
4391
|
+
this.assertGeneration(generation);
|
|
4392
|
+
await this.client.set(this.key, String(generation));
|
|
4393
|
+
}
|
|
4394
|
+
async bump() {
|
|
4395
|
+
const generation = await this.client.incr(this.key);
|
|
4396
|
+
this.assertGeneration(generation);
|
|
4397
|
+
return generation;
|
|
4398
|
+
}
|
|
4399
|
+
parseGeneration(value) {
|
|
4400
|
+
const generation = Number.parseInt(value, 10);
|
|
4401
|
+
if (String(generation) !== value || !this.isGeneration(generation)) {
|
|
4402
|
+
throw new Error(`RedisGenerationStore found invalid persisted generation value for key "${this.key}".`);
|
|
4403
|
+
}
|
|
4404
|
+
return generation;
|
|
4405
|
+
}
|
|
4406
|
+
assertGeneration(value) {
|
|
4407
|
+
if (!this.isGeneration(value)) {
|
|
4408
|
+
throw new Error("RedisGenerationStore generation must be a non-negative safe integer.");
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
isGeneration(value) {
|
|
4412
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
4413
|
+
}
|
|
4414
|
+
};
|
|
4415
|
+
|
|
4335
4416
|
// src/invalidation/RedisInvalidationBus.ts
|
|
4417
|
+
var import_node_crypto3 = require("crypto");
|
|
4336
4418
|
var RedisInvalidationBus = class {
|
|
4337
4419
|
channel;
|
|
4338
4420
|
publisher;
|
|
4339
4421
|
subscriber;
|
|
4340
4422
|
logger;
|
|
4423
|
+
signingKey;
|
|
4341
4424
|
handlers = /* @__PURE__ */ new Set();
|
|
4342
4425
|
sharedListener;
|
|
4343
4426
|
subscribePromise;
|
|
@@ -4346,6 +4429,7 @@ var RedisInvalidationBus = class {
|
|
|
4346
4429
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
4347
4430
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
4348
4431
|
this.logger = options.logger;
|
|
4432
|
+
this.signingKey = options.signingSecret ? normalizeSigningSecret(options.signingSecret) : void 0;
|
|
4349
4433
|
}
|
|
4350
4434
|
/**
|
|
4351
4435
|
* Subscribes to invalidation messages and returns an unsubscribe function.
|
|
@@ -4385,7 +4469,7 @@ var RedisInvalidationBus = class {
|
|
|
4385
4469
|
* Publishes an invalidation message to other subscribers.
|
|
4386
4470
|
*/
|
|
4387
4471
|
async publish(message) {
|
|
4388
|
-
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
4472
|
+
await this.publisher.publish(this.channel, JSON.stringify(this.signingKey ? this.signMessage(message) : message));
|
|
4389
4473
|
}
|
|
4390
4474
|
async dispatchToHandlers(payload) {
|
|
4391
4475
|
let message;
|
|
@@ -4396,10 +4480,11 @@ var RedisInvalidationBus = class {
|
|
|
4396
4480
|
maxNodes: 1e4,
|
|
4397
4481
|
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
4398
4482
|
});
|
|
4399
|
-
|
|
4483
|
+
const candidate = this.signingKey ? this.verifySignedEnvelope(parsed) : parsed;
|
|
4484
|
+
if (!this.isInvalidationMessage(candidate)) {
|
|
4400
4485
|
throw new Error("Invalid invalidation payload shape.");
|
|
4401
4486
|
}
|
|
4402
|
-
message =
|
|
4487
|
+
message = candidate;
|
|
4403
4488
|
} catch (error) {
|
|
4404
4489
|
this.reportError("invalid invalidation payload", error);
|
|
4405
4490
|
return;
|
|
@@ -4424,6 +4509,34 @@ var RedisInvalidationBus = class {
|
|
|
4424
4509
|
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
4425
4510
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
4426
4511
|
}
|
|
4512
|
+
signMessage(message) {
|
|
4513
|
+
const payload = JSON.stringify(message);
|
|
4514
|
+
return {
|
|
4515
|
+
payload: message,
|
|
4516
|
+
signature: this.createSignature(payload)
|
|
4517
|
+
};
|
|
4518
|
+
}
|
|
4519
|
+
verifySignedEnvelope(value) {
|
|
4520
|
+
if (!value || typeof value !== "object") {
|
|
4521
|
+
throw new Error("Signed invalidation envelope must be an object.");
|
|
4522
|
+
}
|
|
4523
|
+
const envelope = value;
|
|
4524
|
+
if (!envelope.payload || typeof envelope.payload !== "object" || typeof envelope.signature !== "string") {
|
|
4525
|
+
throw new Error("Signed invalidation envelope is missing payload or signature.");
|
|
4526
|
+
}
|
|
4527
|
+
const payload = JSON.stringify(envelope.payload);
|
|
4528
|
+
const expected = this.createSignature(payload);
|
|
4529
|
+
if (!isEqualSignature(envelope.signature, expected)) {
|
|
4530
|
+
throw new Error("Invalid invalidation message signature.");
|
|
4531
|
+
}
|
|
4532
|
+
return envelope.payload;
|
|
4533
|
+
}
|
|
4534
|
+
createSignature(payload) {
|
|
4535
|
+
if (!this.signingKey) {
|
|
4536
|
+
throw new Error("RedisInvalidationBus signing key is not configured.");
|
|
4537
|
+
}
|
|
4538
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.signingKey).update(payload).digest("hex");
|
|
4539
|
+
}
|
|
4427
4540
|
reportError(message, error) {
|
|
4428
4541
|
if (this.logger?.error) {
|
|
4429
4542
|
this.logger.error(message, { error });
|
|
@@ -4432,18 +4545,31 @@ var RedisInvalidationBus = class {
|
|
|
4432
4545
|
console.error(`[layercache] ${message}`, error);
|
|
4433
4546
|
}
|
|
4434
4547
|
};
|
|
4548
|
+
function normalizeSigningSecret(secret) {
|
|
4549
|
+
const raw = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
|
|
4550
|
+
return (0, import_node_crypto3.createHash)("sha256").update(raw).digest();
|
|
4551
|
+
}
|
|
4552
|
+
function isEqualSignature(actual, expected) {
|
|
4553
|
+
const actualBuffer = Buffer.from(actual, "hex");
|
|
4554
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
4555
|
+
return actualBuffer.length === expectedBuffer.length && (0, import_node_crypto3.timingSafeEqual)(actualBuffer, expectedBuffer);
|
|
4556
|
+
}
|
|
4435
4557
|
|
|
4436
4558
|
// src/invalidation/RedisTagIndex.ts
|
|
4559
|
+
var DEFAULT_KNOWN_KEYS_SHARDS = 16;
|
|
4437
4560
|
var RedisTagIndex = class {
|
|
4438
4561
|
client;
|
|
4439
4562
|
prefix;
|
|
4440
4563
|
scanCount;
|
|
4441
4564
|
knownKeysShards;
|
|
4565
|
+
logger;
|
|
4566
|
+
warnedLegacyKnownKeys = false;
|
|
4442
4567
|
constructor(options) {
|
|
4443
4568
|
this.client = options.client;
|
|
4444
4569
|
this.prefix = options.prefix ?? "layercache:tag-index";
|
|
4445
4570
|
this.scanCount = options.scanCount ?? 100;
|
|
4446
4571
|
this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
|
|
4572
|
+
this.logger = options.logger;
|
|
4447
4573
|
}
|
|
4448
4574
|
/**
|
|
4449
4575
|
* Records a key as known without changing tag assignments.
|
|
@@ -4479,6 +4605,9 @@ var RedisTagIndex = class {
|
|
|
4479
4605
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
4480
4606
|
const pipeline = this.client.pipeline();
|
|
4481
4607
|
pipeline.srem(this.knownKeysKeyFor(key), key);
|
|
4608
|
+
if (this.knownKeysShards > 1) {
|
|
4609
|
+
pipeline.srem(this.legacyKnownKeysKey(), key);
|
|
4610
|
+
}
|
|
4482
4611
|
pipeline.del(keyTagsKey);
|
|
4483
4612
|
for (const tag of existingTags) {
|
|
4484
4613
|
pipeline.srem(this.tagKeysKey(tag), key);
|
|
@@ -4509,28 +4638,34 @@ var RedisTagIndex = class {
|
|
|
4509
4638
|
* Returns known keys that start with a prefix.
|
|
4510
4639
|
*/
|
|
4511
4640
|
async keysForPrefix(prefix) {
|
|
4512
|
-
const matches =
|
|
4513
|
-
for (const knownKeysKey of this.
|
|
4641
|
+
const matches = /* @__PURE__ */ new Set();
|
|
4642
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
4514
4643
|
let cursor = "0";
|
|
4515
4644
|
do {
|
|
4516
4645
|
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
4517
4646
|
cursor = nextCursor;
|
|
4518
|
-
|
|
4647
|
+
for (const key of keys) {
|
|
4648
|
+
if (key.startsWith(prefix)) {
|
|
4649
|
+
matches.add(key);
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4519
4652
|
} while (cursor !== "0");
|
|
4520
4653
|
}
|
|
4521
|
-
return matches;
|
|
4654
|
+
return [...matches];
|
|
4522
4655
|
}
|
|
4523
4656
|
/**
|
|
4524
4657
|
* Visits known keys that start with a prefix.
|
|
4525
4658
|
*/
|
|
4526
4659
|
async forEachKeyForPrefix(prefix, visitor) {
|
|
4527
|
-
|
|
4660
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4661
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
4528
4662
|
let cursor = "0";
|
|
4529
4663
|
do {
|
|
4530
4664
|
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
4531
4665
|
cursor = nextCursor;
|
|
4532
4666
|
for (const key of keys) {
|
|
4533
|
-
if (key.startsWith(prefix)) {
|
|
4667
|
+
if (key.startsWith(prefix) && !visited.has(key)) {
|
|
4668
|
+
visited.add(key);
|
|
4534
4669
|
await visitor(key);
|
|
4535
4670
|
}
|
|
4536
4671
|
}
|
|
@@ -4547,8 +4682,8 @@ var RedisTagIndex = class {
|
|
|
4547
4682
|
* Returns known keys matching a wildcard pattern.
|
|
4548
4683
|
*/
|
|
4549
4684
|
async matchPattern(pattern) {
|
|
4550
|
-
const matches =
|
|
4551
|
-
for (const knownKeysKey of this.
|
|
4685
|
+
const matches = /* @__PURE__ */ new Set();
|
|
4686
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
4552
4687
|
let cursor = "0";
|
|
4553
4688
|
do {
|
|
4554
4689
|
const [nextCursor, keys] = await this.client.sscan(
|
|
@@ -4560,16 +4695,21 @@ var RedisTagIndex = class {
|
|
|
4560
4695
|
this.scanCount
|
|
4561
4696
|
);
|
|
4562
4697
|
cursor = nextCursor;
|
|
4563
|
-
|
|
4698
|
+
for (const key of keys) {
|
|
4699
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
4700
|
+
matches.add(key);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4564
4703
|
} while (cursor !== "0");
|
|
4565
4704
|
}
|
|
4566
|
-
return matches;
|
|
4705
|
+
return [...matches];
|
|
4567
4706
|
}
|
|
4568
4707
|
/**
|
|
4569
4708
|
* Visits known keys matching a wildcard pattern.
|
|
4570
4709
|
*/
|
|
4571
4710
|
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
4572
|
-
|
|
4711
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4712
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
4573
4713
|
let cursor = "0";
|
|
4574
4714
|
do {
|
|
4575
4715
|
const [nextCursor, keys] = await this.client.sscan(
|
|
@@ -4582,7 +4722,8 @@ var RedisTagIndex = class {
|
|
|
4582
4722
|
);
|
|
4583
4723
|
cursor = nextCursor;
|
|
4584
4724
|
for (const key of keys) {
|
|
4585
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
4725
|
+
if (PatternMatcher.matches(pattern, key) && !visited.has(key)) {
|
|
4726
|
+
visited.add(key);
|
|
4586
4727
|
await visitor(key);
|
|
4587
4728
|
}
|
|
4588
4729
|
}
|
|
@@ -4599,6 +4740,31 @@ var RedisTagIndex = class {
|
|
|
4599
4740
|
}
|
|
4600
4741
|
await this.client.del(...indexKeys);
|
|
4601
4742
|
}
|
|
4743
|
+
async migrateLegacyKnownKeys() {
|
|
4744
|
+
if (this.knownKeysShards === 1) {
|
|
4745
|
+
return { migratedKeys: 0 };
|
|
4746
|
+
}
|
|
4747
|
+
const legacyKey = this.legacyKnownKeysKey();
|
|
4748
|
+
let cursor = "0";
|
|
4749
|
+
let migratedKeys = 0;
|
|
4750
|
+
do {
|
|
4751
|
+
const [nextCursor, keys] = await this.client.sscan(legacyKey, cursor, "COUNT", this.scanCount);
|
|
4752
|
+
cursor = nextCursor;
|
|
4753
|
+
if (keys.length === 0) {
|
|
4754
|
+
continue;
|
|
4755
|
+
}
|
|
4756
|
+
const pipeline = this.client.pipeline();
|
|
4757
|
+
for (const key of keys) {
|
|
4758
|
+
pipeline.sadd(this.knownKeysKeyFor(key), key);
|
|
4759
|
+
}
|
|
4760
|
+
await pipeline.exec();
|
|
4761
|
+
migratedKeys += keys.length;
|
|
4762
|
+
} while (cursor !== "0");
|
|
4763
|
+
if (migratedKeys > 0) {
|
|
4764
|
+
await this.client.del(legacyKey);
|
|
4765
|
+
}
|
|
4766
|
+
return { migratedKeys };
|
|
4767
|
+
}
|
|
4602
4768
|
async scanIndexKeys() {
|
|
4603
4769
|
const matches = [];
|
|
4604
4770
|
let cursor = "0";
|
|
@@ -4616,12 +4782,40 @@ var RedisTagIndex = class {
|
|
|
4616
4782
|
}
|
|
4617
4783
|
return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
|
|
4618
4784
|
}
|
|
4785
|
+
async knownKeysKeysForRead() {
|
|
4786
|
+
if (this.knownKeysShards === 1) {
|
|
4787
|
+
return [this.legacyKnownKeysKey()];
|
|
4788
|
+
}
|
|
4789
|
+
const shardedKeys = this.knownKeysKeys();
|
|
4790
|
+
const legacyKey = this.legacyKnownKeysKey();
|
|
4791
|
+
const legacyExists = await this.client.exists(legacyKey) > 0;
|
|
4792
|
+
if (!legacyExists) {
|
|
4793
|
+
return shardedKeys;
|
|
4794
|
+
}
|
|
4795
|
+
this.warnLegacyKnownKeys(legacyKey);
|
|
4796
|
+
return [legacyKey, ...shardedKeys];
|
|
4797
|
+
}
|
|
4619
4798
|
knownKeysKeys() {
|
|
4620
4799
|
if (this.knownKeysShards === 1) {
|
|
4621
4800
|
return [`${this.prefix}:keys`];
|
|
4622
4801
|
}
|
|
4623
4802
|
return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
|
|
4624
4803
|
}
|
|
4804
|
+
legacyKnownKeysKey() {
|
|
4805
|
+
return `${this.prefix}:keys`;
|
|
4806
|
+
}
|
|
4807
|
+
warnLegacyKnownKeys(legacyKey) {
|
|
4808
|
+
if (this.warnedLegacyKnownKeys) {
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
this.warnedLegacyKnownKeys = true;
|
|
4812
|
+
const message = "RedisTagIndex detected a legacy RedisTagIndex known-key set. Run `layercache migrate-tag-index` to migrate keys into the sharded layout.";
|
|
4813
|
+
if (this.logger?.warn) {
|
|
4814
|
+
this.logger.warn(message, { legacyKey, knownKeysShards: this.knownKeysShards });
|
|
4815
|
+
return;
|
|
4816
|
+
}
|
|
4817
|
+
console.warn(`[layercache] ${message}`, { legacyKey, knownKeysShards: this.knownKeysShards });
|
|
4818
|
+
}
|
|
4625
4819
|
keyTagsKey(key) {
|
|
4626
4820
|
return `${this.prefix}:key:${encodeURIComponent(key)}`;
|
|
4627
4821
|
}
|
|
@@ -4631,7 +4825,7 @@ var RedisTagIndex = class {
|
|
|
4631
4825
|
};
|
|
4632
4826
|
function normalizeKnownKeysShards(value) {
|
|
4633
4827
|
if (value === void 0) {
|
|
4634
|
-
return
|
|
4828
|
+
return DEFAULT_KNOWN_KEYS_SHARDS;
|
|
4635
4829
|
}
|
|
4636
4830
|
if (!Number.isInteger(value) || value <= 0) {
|
|
4637
4831
|
throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
|
|
@@ -4727,6 +4921,41 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
4727
4921
|
};
|
|
4728
4922
|
}
|
|
4729
4923
|
|
|
4924
|
+
// src/integrations/httpCacheKeys.ts
|
|
4925
|
+
var SENSITIVE_QUERY_PARAMETERS = /* @__PURE__ */ new Set([
|
|
4926
|
+
"access_token",
|
|
4927
|
+
"api_key",
|
|
4928
|
+
"apikey",
|
|
4929
|
+
"auth",
|
|
4930
|
+
"authorization",
|
|
4931
|
+
"code",
|
|
4932
|
+
"credentials",
|
|
4933
|
+
"id_token",
|
|
4934
|
+
"jwt",
|
|
4935
|
+
"password",
|
|
4936
|
+
"private_key",
|
|
4937
|
+
"refresh_token",
|
|
4938
|
+
"secret",
|
|
4939
|
+
"session",
|
|
4940
|
+
"sessionid",
|
|
4941
|
+
"session_id",
|
|
4942
|
+
"token"
|
|
4943
|
+
]);
|
|
4944
|
+
function normalizeHttpCacheUrl(url) {
|
|
4945
|
+
try {
|
|
4946
|
+
const parsed = new URL(url, "http://localhost");
|
|
4947
|
+
for (const name of [...parsed.searchParams.keys()]) {
|
|
4948
|
+
if (SENSITIVE_QUERY_PARAMETERS.has(name.toLowerCase())) {
|
|
4949
|
+
parsed.searchParams.delete(name);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
parsed.searchParams.sort();
|
|
4953
|
+
return parsed.pathname + parsed.search;
|
|
4954
|
+
} catch {
|
|
4955
|
+
return url;
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4730
4959
|
// src/integrations/express.ts
|
|
4731
4960
|
function createExpressCacheMiddleware(cache, options = {}) {
|
|
4732
4961
|
const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
|
|
@@ -4742,7 +4971,7 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
4742
4971
|
return;
|
|
4743
4972
|
}
|
|
4744
4973
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
4745
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${
|
|
4974
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeHttpCacheUrl(rawUrl)}`;
|
|
4746
4975
|
const cached = await cache.get(key, void 0, options);
|
|
4747
4976
|
if (cached !== null) {
|
|
4748
4977
|
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
@@ -4758,12 +4987,14 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
4758
4987
|
if (originalJson) {
|
|
4759
4988
|
res.json = (body) => {
|
|
4760
4989
|
res.setHeader?.("x-cache", "MISS");
|
|
4761
|
-
|
|
4762
|
-
cache.
|
|
4763
|
-
|
|
4764
|
-
|
|
4990
|
+
if (isSuccessfulStatus(res.statusCode)) {
|
|
4991
|
+
cache.set(key, body, options).catch((err) => {
|
|
4992
|
+
cache.emit("error", {
|
|
4993
|
+
operation: "set",
|
|
4994
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4995
|
+
});
|
|
4765
4996
|
});
|
|
4766
|
-
}
|
|
4997
|
+
}
|
|
4767
4998
|
return originalJson(body);
|
|
4768
4999
|
};
|
|
4769
5000
|
}
|
|
@@ -4773,14 +5004,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
4773
5004
|
}
|
|
4774
5005
|
};
|
|
4775
5006
|
}
|
|
4776
|
-
function
|
|
4777
|
-
|
|
4778
|
-
const parsed = new URL(url, "http://localhost");
|
|
4779
|
-
parsed.searchParams.sort();
|
|
4780
|
-
return parsed.pathname + parsed.search;
|
|
4781
|
-
} catch {
|
|
4782
|
-
return url;
|
|
4783
|
-
}
|
|
5007
|
+
function isSuccessfulStatus(statusCode) {
|
|
5008
|
+
return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
|
|
4784
5009
|
}
|
|
4785
5010
|
|
|
4786
5011
|
// src/integrations/graphql.ts
|
|
@@ -4811,35 +5036,39 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
4811
5036
|
return;
|
|
4812
5037
|
}
|
|
4813
5038
|
const rawPath = context.req.path ?? context.req.url ?? "/";
|
|
4814
|
-
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${
|
|
5039
|
+
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeHttpCacheUrl(rawPath)}`;
|
|
4815
5040
|
const cached = await cache.get(key, void 0, options);
|
|
4816
5041
|
if (cached !== null) {
|
|
4817
5042
|
context.header?.("x-cache", "HIT");
|
|
4818
5043
|
context.header?.("content-type", "application/json; charset=utf-8");
|
|
4819
5044
|
return context.json(cached);
|
|
4820
5045
|
}
|
|
5046
|
+
let currentStatus;
|
|
5047
|
+
const originalStatus = context.status?.bind(context);
|
|
5048
|
+
if (originalStatus) {
|
|
5049
|
+
context.status = (status) => {
|
|
5050
|
+
currentStatus = status;
|
|
5051
|
+
return originalStatus(status);
|
|
5052
|
+
};
|
|
5053
|
+
}
|
|
4821
5054
|
const originalJson = context.json.bind(context);
|
|
4822
5055
|
context.json = (body, status) => {
|
|
4823
5056
|
context.header?.("x-cache", "MISS");
|
|
4824
|
-
|
|
4825
|
-
cache.
|
|
4826
|
-
|
|
4827
|
-
|
|
5057
|
+
if (isSuccessfulStatus2(status ?? currentStatus)) {
|
|
5058
|
+
cache.set(key, body, options).catch((err) => {
|
|
5059
|
+
cache.emit("error", {
|
|
5060
|
+
operation: "set",
|
|
5061
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5062
|
+
});
|
|
4828
5063
|
});
|
|
4829
|
-
}
|
|
5064
|
+
}
|
|
4830
5065
|
return originalJson(body, status);
|
|
4831
5066
|
};
|
|
4832
5067
|
await next();
|
|
4833
5068
|
};
|
|
4834
5069
|
}
|
|
4835
|
-
function
|
|
4836
|
-
|
|
4837
|
-
const parsed = new URL(url, "http://localhost");
|
|
4838
|
-
parsed.searchParams.sort();
|
|
4839
|
-
return parsed.pathname + parsed.search;
|
|
4840
|
-
} catch {
|
|
4841
|
-
return url;
|
|
4842
|
-
}
|
|
5070
|
+
function isSuccessfulStatus2(statusCode) {
|
|
5071
|
+
return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
|
|
4843
5072
|
}
|
|
4844
5073
|
|
|
4845
5074
|
// src/integrations/opentelemetry.ts
|
|
@@ -5634,12 +5863,12 @@ var RedisLayer = class {
|
|
|
5634
5863
|
};
|
|
5635
5864
|
|
|
5636
5865
|
// src/layers/DiskLayer.ts
|
|
5637
|
-
var
|
|
5866
|
+
var import_node_crypto5 = require("crypto");
|
|
5638
5867
|
var import_node_fs2 = require("fs");
|
|
5639
5868
|
var import_node_path = require("path");
|
|
5640
5869
|
|
|
5641
5870
|
// src/internal/PayloadProtection.ts
|
|
5642
|
-
var
|
|
5871
|
+
var import_node_crypto4 = require("crypto");
|
|
5643
5872
|
var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
|
|
5644
5873
|
var MAGIC_SIGNED = Buffer.from("LCS1:");
|
|
5645
5874
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -5652,11 +5881,11 @@ var PayloadProtection = class {
|
|
|
5652
5881
|
constructor(options) {
|
|
5653
5882
|
if (options.encryptionKey) {
|
|
5654
5883
|
const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
|
|
5655
|
-
this.encryptionKey = (0,
|
|
5884
|
+
this.encryptionKey = (0, import_node_crypto4.createHash)("sha256").update(raw).digest();
|
|
5656
5885
|
}
|
|
5657
5886
|
if (options.signingKey && !options.encryptionKey) {
|
|
5658
5887
|
const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
|
|
5659
|
-
this.signingKey = (0,
|
|
5888
|
+
this.signingKey = (0, import_node_crypto4.createHash)("sha256").update(raw).digest();
|
|
5660
5889
|
}
|
|
5661
5890
|
}
|
|
5662
5891
|
/** Returns `true` when any protection (encryption or signing) is configured. */
|
|
@@ -5703,8 +5932,8 @@ var PayloadProtection = class {
|
|
|
5703
5932
|
}
|
|
5704
5933
|
// ── Encryption (AES-256-GCM) ──────────────────────────────────────────
|
|
5705
5934
|
encrypt(plaintext, key) {
|
|
5706
|
-
const iv = (0,
|
|
5707
|
-
const cipher = (0,
|
|
5935
|
+
const iv = (0, import_node_crypto4.randomBytes)(IV_LENGTH);
|
|
5936
|
+
const cipher = (0, import_node_crypto4.createCipheriv)(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
5708
5937
|
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
5709
5938
|
const authTag = cipher.getAuthTag();
|
|
5710
5939
|
return Buffer.concat([MAGIC_ENCRYPTED, iv, authTag, encrypted]);
|
|
@@ -5715,7 +5944,7 @@ var PayloadProtection = class {
|
|
|
5715
5944
|
const authTag = payload.subarray(headerEnd + IV_LENGTH, headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
|
|
5716
5945
|
const ciphertext = payload.subarray(headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
|
|
5717
5946
|
try {
|
|
5718
|
-
const decipher = (0,
|
|
5947
|
+
const decipher = (0, import_node_crypto4.createDecipheriv)(ALGORITHM, key, iv, {
|
|
5719
5948
|
authTagLength: AUTH_TAG_LENGTH
|
|
5720
5949
|
});
|
|
5721
5950
|
decipher.setAuthTag(authTag);
|
|
@@ -5728,15 +5957,15 @@ var PayloadProtection = class {
|
|
|
5728
5957
|
}
|
|
5729
5958
|
// ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
|
|
5730
5959
|
sign(payload, key) {
|
|
5731
|
-
const hmac = (0,
|
|
5960
|
+
const hmac = (0, import_node_crypto4.createHmac)("sha256", key).update(payload).digest();
|
|
5732
5961
|
return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
|
|
5733
5962
|
}
|
|
5734
5963
|
verify(payload, key) {
|
|
5735
5964
|
const headerEnd = MAGIC_SIGNED.length;
|
|
5736
5965
|
const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
|
|
5737
5966
|
const data = payload.subarray(headerEnd + HMAC_LENGTH);
|
|
5738
|
-
const expectedHmac = (0,
|
|
5739
|
-
if (receivedHmac.length !== HMAC_LENGTH || !(0,
|
|
5967
|
+
const expectedHmac = (0, import_node_crypto4.createHmac)("sha256", key).update(data).digest();
|
|
5968
|
+
if (receivedHmac.length !== HMAC_LENGTH || !(0, import_node_crypto4.timingSafeEqual)(receivedHmac, expectedHmac)) {
|
|
5740
5969
|
throw new PayloadProtectionError(
|
|
5741
5970
|
"HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
|
|
5742
5971
|
);
|
|
@@ -5828,7 +6057,7 @@ var DiskLayer = class {
|
|
|
5828
6057
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
|
|
5829
6058
|
const protectedPayload = this.protection.protect(raw);
|
|
5830
6059
|
const targetPath = this.keyToPath(key);
|
|
5831
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0,
|
|
6060
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto5.randomBytes)(8).toString("hex")}.tmp`;
|
|
5832
6061
|
try {
|
|
5833
6062
|
await import_node_fs2.promises.writeFile(tempPath, protectedPayload);
|
|
5834
6063
|
await import_node_fs2.promises.rename(tempPath, targetPath);
|
|
@@ -5961,7 +6190,7 @@ var DiskLayer = class {
|
|
|
5961
6190
|
async dispose() {
|
|
5962
6191
|
}
|
|
5963
6192
|
keyToPath(key) {
|
|
5964
|
-
const hash = (0,
|
|
6193
|
+
const hash = (0, import_node_crypto5.createHash)("sha256").update(key).digest("hex");
|
|
5965
6194
|
return (0, import_node_path.join)(this.directory, `${hash}.lc`);
|
|
5966
6195
|
}
|
|
5967
6196
|
resolveDirectory(directory) {
|
|
@@ -6279,7 +6508,7 @@ var MsgpackSerializer = class {
|
|
|
6279
6508
|
};
|
|
6280
6509
|
|
|
6281
6510
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
6282
|
-
var
|
|
6511
|
+
var import_node_crypto6 = require("crypto");
|
|
6283
6512
|
var RELEASE_SCRIPT = `
|
|
6284
6513
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
6285
6514
|
return redis.call("del", KEYS[1])
|
|
@@ -6307,7 +6536,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
6307
6536
|
*/
|
|
6308
6537
|
async execute(key, options, worker, waiter) {
|
|
6309
6538
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
6310
|
-
const token = (0,
|
|
6539
|
+
const token = (0, import_node_crypto6.randomUUID)();
|
|
6311
6540
|
const acquired = await this.runCommand(
|
|
6312
6541
|
`acquire("${key}")`,
|
|
6313
6542
|
() => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")
|
|
@@ -6461,6 +6690,7 @@ function sanitizeLabel(value) {
|
|
|
6461
6690
|
MemoryLayer,
|
|
6462
6691
|
MsgpackSerializer,
|
|
6463
6692
|
PatternMatcher,
|
|
6693
|
+
RedisGenerationStore,
|
|
6464
6694
|
RedisInvalidationBus,
|
|
6465
6695
|
RedisLayer,
|
|
6466
6696
|
RedisSingleFlightCoordinator,
|