layercache 1.2.8 → 1.2.9
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 +2 -2
- package/dist/cli.cjs +14 -1
- package/dist/cli.js +14 -1
- package/dist/{edge-DBs8Ko5W.d.cts → edge-BXWTKlI1.d.cts} +1 -0
- package/dist/{edge-DBs8Ko5W.d.ts → edge-BXWTKlI1.d.ts} +1 -0
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +146 -61
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +143 -58
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +99 -41
- package/packages/nestjs/dist/index.d.cts +1 -0
- package/packages/nestjs/dist/index.d.ts +1 -0
- package/packages/nestjs/dist/index.js +99 -41
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache_2.0-green" alt="license"></a>
|
|
16
16
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-first-3178C6?logo=typescript&logoColor=white" alt="TypeScript"></a>
|
|
17
17
|
<img src="https://img.shields.io/badge/Node.js-%E2%89%A5_20-339933?logo=nodedotjs&logoColor=white" alt="Node.js >= 20">
|
|
18
|
-
<img src="https://img.shields.io/badge/tests-
|
|
19
|
-
<a href="https://coveralls.io/github/flyingsquirrel0419/layercache?branch=main"><img src="https://coveralls.io/repos/github/flyingsquirrel0419/layercache/badge.svg?branch=main&t=
|
|
18
|
+
<img src="https://img.shields.io/badge/tests-411_passing-brightgreen" alt="tests">
|
|
19
|
+
<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>
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
package/dist/cli.cjs
CHANGED
|
@@ -465,6 +465,11 @@ function parseArgs(argv) {
|
|
|
465
465
|
const token = rest[index];
|
|
466
466
|
const value = rest[index + 1];
|
|
467
467
|
if (token === "--redis") {
|
|
468
|
+
if (!value || value.startsWith("--")) {
|
|
469
|
+
process.stderr.write("Error: --redis requires a value (e.g. redis://localhost:6379)\n");
|
|
470
|
+
process.exitCode = 1;
|
|
471
|
+
return parsed;
|
|
472
|
+
}
|
|
468
473
|
parsed.redisUrl = value;
|
|
469
474
|
index += 1;
|
|
470
475
|
} else if (token === "--pattern") {
|
|
@@ -484,6 +489,7 @@ function parseArgs(argv) {
|
|
|
484
489
|
return parsed;
|
|
485
490
|
}
|
|
486
491
|
var BATCH_DELETE_SIZE = 500;
|
|
492
|
+
var SCAN_MAX_KEYS = 1e6;
|
|
487
493
|
async function batchDelete(redis, keys) {
|
|
488
494
|
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
489
495
|
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
@@ -497,6 +503,13 @@ async function scanKeys(redis, pattern) {
|
|
|
497
503
|
const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
|
|
498
504
|
cursor = nextCursor;
|
|
499
505
|
keys.push(...batch);
|
|
506
|
+
if (keys.length >= SCAN_MAX_KEYS) {
|
|
507
|
+
process.stderr.write(
|
|
508
|
+
`Warning: stopped scanning after ${SCAN_MAX_KEYS} keys. Use a more specific --pattern to narrow results.
|
|
509
|
+
`
|
|
510
|
+
);
|
|
511
|
+
return keys;
|
|
512
|
+
}
|
|
500
513
|
} while (cursor !== "0");
|
|
501
514
|
return keys;
|
|
502
515
|
}
|
|
@@ -539,7 +552,7 @@ function maskRedisUrl(url) {
|
|
|
539
552
|
return url.replace(/:([^@/]+)@/, ":***@");
|
|
540
553
|
}
|
|
541
554
|
}
|
|
542
|
-
if (process.argv[1]?.
|
|
555
|
+
if (process.argv[1]?.endsWith("cli.cjs") || process.argv[1]?.endsWith("cli.js")) {
|
|
543
556
|
void main();
|
|
544
557
|
}
|
|
545
558
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/cli.js
CHANGED
|
@@ -123,6 +123,11 @@ function parseArgs(argv) {
|
|
|
123
123
|
const token = rest[index];
|
|
124
124
|
const value = rest[index + 1];
|
|
125
125
|
if (token === "--redis") {
|
|
126
|
+
if (!value || value.startsWith("--")) {
|
|
127
|
+
process.stderr.write("Error: --redis requires a value (e.g. redis://localhost:6379)\n");
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
126
131
|
parsed.redisUrl = value;
|
|
127
132
|
index += 1;
|
|
128
133
|
} else if (token === "--pattern") {
|
|
@@ -142,6 +147,7 @@ function parseArgs(argv) {
|
|
|
142
147
|
return parsed;
|
|
143
148
|
}
|
|
144
149
|
var BATCH_DELETE_SIZE = 500;
|
|
150
|
+
var SCAN_MAX_KEYS = 1e6;
|
|
145
151
|
async function batchDelete(redis, keys) {
|
|
146
152
|
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
147
153
|
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
@@ -155,6 +161,13 @@ async function scanKeys(redis, pattern) {
|
|
|
155
161
|
const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
|
|
156
162
|
cursor = nextCursor;
|
|
157
163
|
keys.push(...batch);
|
|
164
|
+
if (keys.length >= SCAN_MAX_KEYS) {
|
|
165
|
+
process.stderr.write(
|
|
166
|
+
`Warning: stopped scanning after ${SCAN_MAX_KEYS} keys. Use a more specific --pattern to narrow results.
|
|
167
|
+
`
|
|
168
|
+
);
|
|
169
|
+
return keys;
|
|
170
|
+
}
|
|
158
171
|
} while (cursor !== "0");
|
|
159
172
|
return keys;
|
|
160
173
|
}
|
|
@@ -197,7 +210,7 @@ function maskRedisUrl(url) {
|
|
|
197
210
|
return url.replace(/:([^@/]+)@/, ":***@");
|
|
198
211
|
}
|
|
199
212
|
}
|
|
200
|
-
if (process.argv[1]?.
|
|
213
|
+
if (process.argv[1]?.endsWith("cli.cjs") || process.argv[1]?.endsWith("cli.js")) {
|
|
201
214
|
void main();
|
|
202
215
|
}
|
|
203
216
|
export {
|
|
@@ -556,6 +556,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
556
556
|
private readonly layerWriter;
|
|
557
557
|
private readonly snapshots;
|
|
558
558
|
private readonly backgroundRefreshes;
|
|
559
|
+
private readonly backgroundRefreshAbort;
|
|
559
560
|
private readonly layerDegradedUntil;
|
|
560
561
|
private readonly maintenance;
|
|
561
562
|
private readonly ttlResolver;
|
|
@@ -556,6 +556,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
556
556
|
private readonly layerWriter;
|
|
557
557
|
private readonly snapshots;
|
|
558
558
|
private readonly backgroundRefreshes;
|
|
559
|
+
private readonly backgroundRefreshAbort;
|
|
559
560
|
private readonly layerDegradedUntil;
|
|
560
561
|
private readonly maintenance;
|
|
561
562
|
private readonly ttlResolver;
|
package/dist/edge.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-BXWTKlI1.cjs';
|
|
2
2
|
import 'node:events';
|
package/dist/edge.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-BXWTKlI1.js';
|
|
2
2
|
import 'node:events';
|
package/dist/index.cjs
CHANGED
|
@@ -528,7 +528,7 @@ function normalizeForSerialization(value) {
|
|
|
528
528
|
}
|
|
529
529
|
function serializeKeyPart(value) {
|
|
530
530
|
if (typeof value === "string") {
|
|
531
|
-
return `s:${value}`;
|
|
531
|
+
return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
|
|
532
532
|
}
|
|
533
533
|
if (typeof value === "number") {
|
|
534
534
|
return `n:${value}`;
|
|
@@ -917,6 +917,7 @@ var CacheStackLayerWriter = class {
|
|
|
917
917
|
}
|
|
918
918
|
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
919
919
|
const failures = results.filter((result) => result.status === "rejected");
|
|
920
|
+
const degraded = results.filter((result) => result.status === "fulfilled");
|
|
920
921
|
if (failures.length === 0) {
|
|
921
922
|
return;
|
|
922
923
|
}
|
|
@@ -1095,6 +1096,7 @@ function planFreshReadPolicies({
|
|
|
1095
1096
|
}
|
|
1096
1097
|
|
|
1097
1098
|
// src/internal/CacheStackSnapshotManager.ts
|
|
1099
|
+
var import_node_crypto = require("crypto");
|
|
1098
1100
|
var import_node_fs = require("fs");
|
|
1099
1101
|
var import_node_path = __toESM(require("path"), 1);
|
|
1100
1102
|
|
|
@@ -1194,6 +1196,42 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
1194
1196
|
return Buffer.concat(chunks).toString("utf8");
|
|
1195
1197
|
}
|
|
1196
1198
|
|
|
1199
|
+
// src/internal/StructuredDataSanitizer.ts
|
|
1200
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1201
|
+
function sanitizeStructuredData(value, options) {
|
|
1202
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1203
|
+
}
|
|
1204
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1205
|
+
state.count += 1;
|
|
1206
|
+
if (state.count > options.maxNodes) {
|
|
1207
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1208
|
+
}
|
|
1209
|
+
if (depth > options.maxDepth) {
|
|
1210
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1211
|
+
}
|
|
1212
|
+
if (Array.isArray(value)) {
|
|
1213
|
+
const sanitized2 = [];
|
|
1214
|
+
for (const entry of value) {
|
|
1215
|
+
sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
|
|
1216
|
+
}
|
|
1217
|
+
return sanitized2;
|
|
1218
|
+
}
|
|
1219
|
+
if (!isPlainObject(value)) {
|
|
1220
|
+
return value;
|
|
1221
|
+
}
|
|
1222
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1223
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1224
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1228
|
+
}
|
|
1229
|
+
return sanitized;
|
|
1230
|
+
}
|
|
1231
|
+
function isPlainObject(value) {
|
|
1232
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1197
1235
|
// src/internal/CacheStackSnapshotManager.ts
|
|
1198
1236
|
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1199
1237
|
var CacheStackSnapshotManager = class {
|
|
@@ -1218,7 +1256,16 @@ var CacheStackSnapshotManager = class {
|
|
|
1218
1256
|
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1219
1257
|
await Promise.all(
|
|
1220
1258
|
batch.map(async (entry) => {
|
|
1221
|
-
await Promise.all(
|
|
1259
|
+
await Promise.all(
|
|
1260
|
+
this.options.layers.map(async (layer) => {
|
|
1261
|
+
if (this.options.shouldSkipLayer(layer)) return;
|
|
1262
|
+
try {
|
|
1263
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1266
|
+
}
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1222
1269
|
await this.options.tagIndex.touch(entry.key);
|
|
1223
1270
|
})
|
|
1224
1271
|
);
|
|
@@ -1228,7 +1275,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1228
1275
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1229
1276
|
const tempPath = import_node_path.default.join(
|
|
1230
1277
|
import_node_path.default.dirname(targetPath),
|
|
1231
|
-
`.layercache-${process.pid}-${Date.now()}-${
|
|
1278
|
+
`.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
|
|
1232
1279
|
);
|
|
1233
1280
|
let handle;
|
|
1234
1281
|
try {
|
|
@@ -1328,7 +1375,13 @@ var CacheStackSnapshotManager = class {
|
|
|
1328
1375
|
});
|
|
1329
1376
|
}
|
|
1330
1377
|
sanitizeSnapshotValue(value) {
|
|
1331
|
-
|
|
1378
|
+
const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1379
|
+
return sanitizeStructuredData(roundTripped, {
|
|
1380
|
+
label: "Snapshot value",
|
|
1381
|
+
maxDepth: 64,
|
|
1382
|
+
maxNodes: 1e4,
|
|
1383
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
1384
|
+
});
|
|
1332
1385
|
}
|
|
1333
1386
|
};
|
|
1334
1387
|
|
|
@@ -1708,7 +1761,13 @@ var FetchRateLimiter = class {
|
|
|
1708
1761
|
this.pendingBuckets.add(next.bucketKey);
|
|
1709
1762
|
}
|
|
1710
1763
|
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
1711
|
-
this.
|
|
1764
|
+
if (!this.drainTimer) {
|
|
1765
|
+
this.drainTimer = setTimeout(() => {
|
|
1766
|
+
this.drainTimer = void 0;
|
|
1767
|
+
this.drain();
|
|
1768
|
+
}, 0);
|
|
1769
|
+
this.drainTimer.unref?.();
|
|
1770
|
+
}
|
|
1712
1771
|
});
|
|
1713
1772
|
}
|
|
1714
1773
|
}
|
|
@@ -1750,6 +1809,9 @@ var FetchRateLimiter = class {
|
|
|
1750
1809
|
}
|
|
1751
1810
|
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1752
1811
|
this.evictIdleBuckets();
|
|
1812
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1813
|
+
throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
|
|
1814
|
+
}
|
|
1753
1815
|
}
|
|
1754
1816
|
const bucket = { active: 0, startedAt: [] };
|
|
1755
1817
|
this.buckets.set(bucketKey, bucket);
|
|
@@ -2228,38 +2290,6 @@ var TagIndex = class {
|
|
|
2228
2290
|
}
|
|
2229
2291
|
};
|
|
2230
2292
|
|
|
2231
|
-
// src/internal/StructuredDataSanitizer.ts
|
|
2232
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2233
|
-
function sanitizeStructuredData(value, options) {
|
|
2234
|
-
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2235
|
-
}
|
|
2236
|
-
function sanitizeValue(value, depth, state, options) {
|
|
2237
|
-
state.count += 1;
|
|
2238
|
-
if (state.count > options.maxNodes) {
|
|
2239
|
-
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
2240
|
-
}
|
|
2241
|
-
if (depth > options.maxDepth) {
|
|
2242
|
-
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
2243
|
-
}
|
|
2244
|
-
if (Array.isArray(value)) {
|
|
2245
|
-
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
2246
|
-
}
|
|
2247
|
-
if (!isPlainObject(value)) {
|
|
2248
|
-
return value;
|
|
2249
|
-
}
|
|
2250
|
-
const sanitized = options.createObject?.() ?? {};
|
|
2251
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
2252
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
2253
|
-
continue;
|
|
2254
|
-
}
|
|
2255
|
-
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
2256
|
-
}
|
|
2257
|
-
return sanitized;
|
|
2258
|
-
}
|
|
2259
|
-
function isPlainObject(value) {
|
|
2260
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
2293
|
// src/serialization/JsonSerializer.ts
|
|
2264
2294
|
var JsonSerializer = class {
|
|
2265
2295
|
serialize(value) {
|
|
@@ -2424,6 +2454,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2424
2454
|
tagIndex: this.tagIndex,
|
|
2425
2455
|
snapshotSerializer: this.snapshotSerializer,
|
|
2426
2456
|
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2457
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2458
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2427
2459
|
qualifyKey: this.qualifyKey.bind(this),
|
|
2428
2460
|
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2429
2461
|
validateCacheKey,
|
|
@@ -2448,6 +2480,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2448
2480
|
layerWriter;
|
|
2449
2481
|
snapshots;
|
|
2450
2482
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2483
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2451
2484
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2452
2485
|
maintenance = new CacheStackMaintenance();
|
|
2453
2486
|
ttlResolver;
|
|
@@ -2692,7 +2725,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2692
2725
|
}
|
|
2693
2726
|
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2694
2727
|
const layer = this.layers[layerIndex];
|
|
2695
|
-
if (!layer) continue;
|
|
2728
|
+
if (!layer || this.shouldSkipLayer(layer)) continue;
|
|
2696
2729
|
const keys = [...pending];
|
|
2697
2730
|
if (keys.length === 0) {
|
|
2698
2731
|
break;
|
|
@@ -2709,6 +2742,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2709
2742
|
await layer.delete(key);
|
|
2710
2743
|
continue;
|
|
2711
2744
|
}
|
|
2745
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2746
|
+
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2747
|
+
}
|
|
2712
2748
|
await this.tagIndex.touch(key);
|
|
2713
2749
|
await this.backfill(key, stored, layerIndex - 1);
|
|
2714
2750
|
resultsByKey.set(key, resolved.value);
|
|
@@ -2964,7 +3000,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2964
3000
|
await this.unsubscribeInvalidation?.();
|
|
2965
3001
|
await this.flushWriteBehindQueue();
|
|
2966
3002
|
await this.maintenance.waitForGenerationCleanup();
|
|
2967
|
-
|
|
3003
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
3004
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
3005
|
+
}
|
|
3006
|
+
await Promise.allSettled(
|
|
3007
|
+
[...this.backgroundRefreshes.values()].map((promise) => {
|
|
3008
|
+
let timer;
|
|
3009
|
+
return Promise.race([
|
|
3010
|
+
promise,
|
|
3011
|
+
new Promise((resolve2) => {
|
|
3012
|
+
timer = setTimeout(resolve2, 5e3);
|
|
3013
|
+
timer.unref?.();
|
|
3014
|
+
})
|
|
3015
|
+
]).finally(() => {
|
|
3016
|
+
if (timer) clearTimeout(timer);
|
|
3017
|
+
});
|
|
3018
|
+
})
|
|
3019
|
+
);
|
|
3020
|
+
this.backgroundRefreshes.clear();
|
|
3021
|
+
this.backgroundRefreshAbort.clear();
|
|
2968
3022
|
this.maintenance.disposeWriteBehindTimer();
|
|
2969
3023
|
this.fetchRateLimiter.dispose();
|
|
2970
3024
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -3231,15 +3285,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3231
3285
|
}
|
|
3232
3286
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3233
3287
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3288
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
3234
3289
|
const refresh = (async () => {
|
|
3235
3290
|
this.metricsCollector.increment("refreshes");
|
|
3236
3291
|
try {
|
|
3292
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3237
3293
|
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
3238
3294
|
} catch (error) {
|
|
3295
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3239
3296
|
this.metricsCollector.increment("refreshErrors");
|
|
3240
3297
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
3241
3298
|
} finally {
|
|
3242
3299
|
this.backgroundRefreshes.delete(key);
|
|
3300
|
+
this.backgroundRefreshAbort.delete(key);
|
|
3243
3301
|
}
|
|
3244
3302
|
})();
|
|
3245
3303
|
this.backgroundRefreshes.set(key, refresh);
|
|
@@ -3342,7 +3400,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3342
3400
|
timer.unref?.();
|
|
3343
3401
|
})
|
|
3344
3402
|
]);
|
|
3345
|
-
if (result && typeof result === "object" && "kind" in result) {
|
|
3403
|
+
if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
|
|
3346
3404
|
if (result.kind === "error") {
|
|
3347
3405
|
throw result.error;
|
|
3348
3406
|
}
|
|
@@ -3360,7 +3418,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3360
3418
|
}
|
|
3361
3419
|
async observeOperation(name, attributes, execute) {
|
|
3362
3420
|
const id = this.nextOperationId;
|
|
3363
|
-
this.nextOperationId
|
|
3421
|
+
this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
|
|
3364
3422
|
this.emit("operation-start", { id, name, attributes });
|
|
3365
3423
|
try {
|
|
3366
3424
|
const result = await execute();
|
|
@@ -3596,6 +3654,7 @@ var RedisInvalidationBus = class {
|
|
|
3596
3654
|
logger;
|
|
3597
3655
|
handlers = /* @__PURE__ */ new Set();
|
|
3598
3656
|
sharedListener;
|
|
3657
|
+
subscribePromise;
|
|
3599
3658
|
constructor(options) {
|
|
3600
3659
|
this.publisher = options.publisher;
|
|
3601
3660
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
@@ -3603,15 +3662,27 @@ var RedisInvalidationBus = class {
|
|
|
3603
3662
|
this.logger = options.logger;
|
|
3604
3663
|
}
|
|
3605
3664
|
async subscribe(handler) {
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
await
|
|
3665
|
+
const previousPromise = this.subscribePromise;
|
|
3666
|
+
let resolveThis;
|
|
3667
|
+
this.subscribePromise = new Promise((resolve2) => {
|
|
3668
|
+
resolveThis = resolve2;
|
|
3669
|
+
});
|
|
3670
|
+
if (previousPromise) {
|
|
3671
|
+
await previousPromise;
|
|
3672
|
+
}
|
|
3673
|
+
try {
|
|
3674
|
+
if (this.handlers.size === 0) {
|
|
3675
|
+
const listener = (_channel, payload) => {
|
|
3676
|
+
void this.dispatchToHandlers(payload);
|
|
3677
|
+
};
|
|
3678
|
+
this.sharedListener = listener;
|
|
3679
|
+
this.subscriber.on("message", listener);
|
|
3680
|
+
await this.subscriber.subscribe(this.channel);
|
|
3681
|
+
}
|
|
3682
|
+
this.handlers.add(handler);
|
|
3683
|
+
} finally {
|
|
3684
|
+
resolveThis();
|
|
3613
3685
|
}
|
|
3614
|
-
this.handlers.add(handler);
|
|
3615
3686
|
return async () => {
|
|
3616
3687
|
this.handlers.delete(handler);
|
|
3617
3688
|
if (this.handlers.size === 0 && this.sharedListener) {
|
|
@@ -4037,10 +4108,21 @@ function normalizeUrl2(url) {
|
|
|
4037
4108
|
}
|
|
4038
4109
|
|
|
4039
4110
|
// src/integrations/opentelemetry.ts
|
|
4111
|
+
var MAX_SPANS = 1e4;
|
|
4040
4112
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
4041
4113
|
const spans = /* @__PURE__ */ new Map();
|
|
4042
4114
|
const onStart = (event) => {
|
|
4043
|
-
|
|
4115
|
+
try {
|
|
4116
|
+
if (spans.size >= MAX_SPANS) {
|
|
4117
|
+
const oldest = spans.keys().next().value;
|
|
4118
|
+
if (oldest !== void 0) {
|
|
4119
|
+
spans.get(oldest)?.end();
|
|
4120
|
+
spans.delete(oldest);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
|
|
4124
|
+
} catch {
|
|
4125
|
+
}
|
|
4044
4126
|
};
|
|
4045
4127
|
const onEnd = (event) => {
|
|
4046
4128
|
const span = spans.get(event.id);
|
|
@@ -4048,12 +4130,15 @@ function createOpenTelemetryPlugin(cache, tracer) {
|
|
|
4048
4130
|
return;
|
|
4049
4131
|
}
|
|
4050
4132
|
spans.delete(event.id);
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4133
|
+
try {
|
|
4134
|
+
span.setAttribute?.("layercache.success", event.success);
|
|
4135
|
+
if (event.result) {
|
|
4136
|
+
span.setAttribute?.("layercache.result", event.result);
|
|
4137
|
+
}
|
|
4138
|
+
if (event.error !== void 0) {
|
|
4139
|
+
span.recordException?.(event.error);
|
|
4140
|
+
}
|
|
4141
|
+
} catch {
|
|
4057
4142
|
}
|
|
4058
4143
|
span.end();
|
|
4059
4144
|
};
|
|
@@ -4619,7 +4704,7 @@ var RedisLayer = class {
|
|
|
4619
4704
|
};
|
|
4620
4705
|
|
|
4621
4706
|
// src/layers/DiskLayer.ts
|
|
4622
|
-
var
|
|
4707
|
+
var import_node_crypto2 = require("crypto");
|
|
4623
4708
|
var import_node_fs2 = require("fs");
|
|
4624
4709
|
var import_node_path2 = require("path");
|
|
4625
4710
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
@@ -4672,7 +4757,7 @@ var DiskLayer = class {
|
|
|
4672
4757
|
};
|
|
4673
4758
|
const payload = this.serializer.serialize(entry);
|
|
4674
4759
|
const targetPath = this.keyToPath(key);
|
|
4675
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${
|
|
4760
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
|
|
4676
4761
|
try {
|
|
4677
4762
|
await import_node_fs2.promises.writeFile(tempPath, payload);
|
|
4678
4763
|
await import_node_fs2.promises.rename(tempPath, targetPath);
|
|
@@ -4772,7 +4857,7 @@ var DiskLayer = class {
|
|
|
4772
4857
|
async dispose() {
|
|
4773
4858
|
}
|
|
4774
4859
|
keyToPath(key) {
|
|
4775
|
-
const hash = (0,
|
|
4860
|
+
const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
|
|
4776
4861
|
return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
|
|
4777
4862
|
}
|
|
4778
4863
|
resolveDirectory(directory) {
|
|
@@ -5050,7 +5135,7 @@ var MsgpackSerializer = class {
|
|
|
5050
5135
|
};
|
|
5051
5136
|
|
|
5052
5137
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
5053
|
-
var
|
|
5138
|
+
var import_node_crypto3 = require("crypto");
|
|
5054
5139
|
var RELEASE_SCRIPT = `
|
|
5055
5140
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
5056
5141
|
return redis.call("del", KEYS[1])
|
|
@@ -5072,7 +5157,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
5072
5157
|
}
|
|
5073
5158
|
async execute(key, options, worker, waiter) {
|
|
5074
5159
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
5075
|
-
const token = (0,
|
|
5160
|
+
const token = (0, import_node_crypto3.randomUUID)();
|
|
5076
5161
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
5077
5162
|
if (acquired === "OK") {
|
|
5078
5163
|
const renewTimer = this.startLeaseRenewal(lockKey, token, options);
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-
|
|
2
|
-
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-BXWTKlI1.cjs';
|
|
2
|
+
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-BXWTKlI1.cjs';
|
|
3
3
|
import Redis from 'ioredis';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
|
|
@@ -23,6 +23,7 @@ declare class RedisInvalidationBus implements InvalidationBus {
|
|
|
23
23
|
private readonly logger?;
|
|
24
24
|
private readonly handlers;
|
|
25
25
|
private sharedListener?;
|
|
26
|
+
private subscribePromise;
|
|
26
27
|
constructor(options: RedisInvalidationBusOptions);
|
|
27
28
|
subscribe(handler: (message: InvalidationMessage) => Promise<void> | void): Promise<() => Promise<void>>;
|
|
28
29
|
publish(message: InvalidationMessage): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-
|
|
2
|
-
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-BXWTKlI1.js';
|
|
2
|
+
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-BXWTKlI1.js';
|
|
3
3
|
import Redis from 'ioredis';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
|
|
@@ -23,6 +23,7 @@ declare class RedisInvalidationBus implements InvalidationBus {
|
|
|
23
23
|
private readonly logger?;
|
|
24
24
|
private readonly handlers;
|
|
25
25
|
private sharedListener?;
|
|
26
|
+
private subscribePromise;
|
|
26
27
|
constructor(options: RedisInvalidationBusOptions);
|
|
27
28
|
subscribe(handler: (message: InvalidationMessage) => Promise<void> | void): Promise<() => Promise<void>>;
|
|
28
29
|
publish(message: InvalidationMessage): Promise<void>;
|