layercache 1.3.1 → 1.3.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 +156 -119
- package/dist/{chunk-GJBKCFE6.js → chunk-5RCAX2BQ.js} +9 -9
- package/dist/{chunk-BQLL6IM5.js → chunk-BORDQ3LA.js} +135 -0
- package/dist/cli.cjs +77 -5
- package/dist/cli.js +37 -7
- package/dist/edge.cjs +9 -9
- package/dist/edge.js +1 -1
- package/dist/index.cjs +96 -58
- package/dist/index.js +81 -156
- package/package.json +2 -12
- package/examples/nestjs-module/app.module.ts +0 -15
- package/packages/nestjs/dist/index.cjs +0 -3952
- package/packages/nestjs/dist/index.d.cts +0 -629
- package/packages/nestjs/dist/index.d.ts +0 -629
- package/packages/nestjs/dist/index.js +0 -3915
package/dist/index.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
-
RedisTagIndex
|
|
3
|
-
|
|
2
|
+
RedisTagIndex,
|
|
3
|
+
validateAdaptiveTtlOptions,
|
|
4
|
+
validateCacheKey,
|
|
5
|
+
validateCircuitBreakerOptions,
|
|
6
|
+
validateLayerNumberOption,
|
|
7
|
+
validateNonNegativeNumber,
|
|
8
|
+
validatePattern,
|
|
9
|
+
validatePositiveNumber,
|
|
10
|
+
validateRateLimitOptions,
|
|
11
|
+
validateTag,
|
|
12
|
+
validateTags,
|
|
13
|
+
validateTtlPolicy
|
|
14
|
+
} from "./chunk-BORDQ3LA.js";
|
|
4
15
|
import {
|
|
5
16
|
MemoryLayer,
|
|
6
17
|
TagIndex,
|
|
7
18
|
createHonoCacheMiddleware
|
|
8
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-5RCAX2BQ.js";
|
|
9
20
|
import {
|
|
10
21
|
PatternMatcher,
|
|
11
22
|
createStoredValueEnvelope,
|
|
@@ -715,6 +726,7 @@ var CacheStackLayerWriter = class {
|
|
|
715
726
|
};
|
|
716
727
|
|
|
717
728
|
// src/internal/CacheStackMaintenance.ts
|
|
729
|
+
var MAX_KEY_EPOCHS = 5e4;
|
|
718
730
|
var CacheStackMaintenance = class {
|
|
719
731
|
keyEpochs = /* @__PURE__ */ new Map();
|
|
720
732
|
writeBehindQueue = [];
|
|
@@ -758,6 +770,7 @@ var CacheStackMaintenance = class {
|
|
|
758
770
|
for (const key of keys) {
|
|
759
771
|
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
760
772
|
}
|
|
773
|
+
this.pruneKeyEpochsIfNeeded();
|
|
761
774
|
}
|
|
762
775
|
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
763
776
|
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
@@ -810,6 +823,16 @@ var CacheStackMaintenance = class {
|
|
|
810
823
|
async waitForGenerationCleanup() {
|
|
811
824
|
await this.generationCleanupPromise;
|
|
812
825
|
}
|
|
826
|
+
pruneKeyEpochsIfNeeded() {
|
|
827
|
+
if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
|
|
831
|
+
const toDelete = Math.ceil(sorted.length * 0.1);
|
|
832
|
+
for (let i = 0; i < toDelete; i++) {
|
|
833
|
+
this.keyEpochs.delete(sorted[i][0]);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
813
836
|
};
|
|
814
837
|
|
|
815
838
|
// src/internal/CacheStackRuntimePolicy.ts
|
|
@@ -849,16 +872,16 @@ function planFreshReadPolicies({
|
|
|
849
872
|
}
|
|
850
873
|
|
|
851
874
|
// src/internal/CacheStackSnapshotManager.ts
|
|
852
|
-
import { randomBytes } from "crypto";
|
|
853
875
|
import { constants, promises as fs } from "fs";
|
|
854
|
-
import path from "path";
|
|
855
876
|
|
|
856
877
|
// src/internal/CacheSnapshotFile.ts
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
878
|
+
import { randomBytes } from "crypto";
|
|
879
|
+
import { rename, unlink } from "fs/promises";
|
|
880
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
881
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
882
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
860
883
|
}
|
|
861
|
-
async function findExistingAncestor(directory, fs3,
|
|
884
|
+
async function findExistingAncestor(directory, fs3, path) {
|
|
862
885
|
let current = directory;
|
|
863
886
|
while (true) {
|
|
864
887
|
try {
|
|
@@ -869,7 +892,7 @@ async function findExistingAncestor(directory, fs3, path2) {
|
|
|
869
892
|
throw error;
|
|
870
893
|
}
|
|
871
894
|
}
|
|
872
|
-
const parent =
|
|
895
|
+
const parent = path.dirname(current);
|
|
873
896
|
if (parent === current) {
|
|
874
897
|
return current;
|
|
875
898
|
}
|
|
@@ -884,36 +907,36 @@ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = p
|
|
|
884
907
|
throw new Error("filePath must not contain null bytes.");
|
|
885
908
|
}
|
|
886
909
|
const { promises: fs3 } = await import("fs");
|
|
887
|
-
const
|
|
888
|
-
const resolved =
|
|
889
|
-
const baseDir = snapshotBaseDir === false ? false :
|
|
910
|
+
const path = await import("path");
|
|
911
|
+
const resolved = path.resolve(filePath);
|
|
912
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
890
913
|
if (baseDir === false) {
|
|
891
914
|
return resolved;
|
|
892
915
|
}
|
|
893
916
|
await fs3.mkdir(baseDir, { recursive: true });
|
|
894
917
|
const realBaseDir = await fs3.realpath(baseDir);
|
|
895
|
-
if (!isWithinSnapshotBase(realBaseDir, resolved,
|
|
918
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
896
919
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
897
920
|
}
|
|
898
921
|
if (mode === "read") {
|
|
899
922
|
const realTarget = await fs3.realpath(resolved);
|
|
900
|
-
if (!isWithinSnapshotBase(realBaseDir, realTarget,
|
|
923
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
901
924
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
902
925
|
}
|
|
903
926
|
return realTarget;
|
|
904
927
|
}
|
|
905
|
-
const parentDir =
|
|
906
|
-
const existingAncestor = await findExistingAncestor(parentDir, fs3,
|
|
928
|
+
const parentDir = path.dirname(resolved);
|
|
929
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs3, path);
|
|
907
930
|
const realExistingAncestor = await fs3.realpath(existingAncestor);
|
|
908
|
-
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor,
|
|
931
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
909
932
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
910
933
|
}
|
|
911
934
|
await fs3.mkdir(parentDir, { recursive: true });
|
|
912
935
|
const realParentDir = await fs3.realpath(parentDir);
|
|
913
|
-
if (!isWithinSnapshotBase(realBaseDir, realParentDir,
|
|
936
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
914
937
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
915
938
|
}
|
|
916
|
-
const targetPath =
|
|
939
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
917
940
|
try {
|
|
918
941
|
const existing = await fs3.lstat(targetPath);
|
|
919
942
|
if (existing.isSymbolicLink()) {
|
|
@@ -948,6 +971,17 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
948
971
|
}
|
|
949
972
|
return Buffer.concat(chunks).toString("utf8");
|
|
950
973
|
}
|
|
974
|
+
function atomicWriteTempPath(targetPath) {
|
|
975
|
+
return `${targetPath}.tmp-${randomBytes(8).toString("hex")}`;
|
|
976
|
+
}
|
|
977
|
+
async function commitAtomicWrite(tempPath, targetPath) {
|
|
978
|
+
try {
|
|
979
|
+
await rename(tempPath, targetPath);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
await unlink(tempPath).catch(() => void 0);
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
951
985
|
|
|
952
986
|
// src/internal/StructuredDataSanitizer.ts
|
|
953
987
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
@@ -1026,10 +1060,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1026
1060
|
}
|
|
1027
1061
|
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1028
1062
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1029
|
-
const tempPath =
|
|
1030
|
-
path.dirname(targetPath),
|
|
1031
|
-
`.layercache-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`
|
|
1032
|
-
);
|
|
1063
|
+
const tempPath = atomicWriteTempPath(targetPath);
|
|
1033
1064
|
let handle;
|
|
1034
1065
|
try {
|
|
1035
1066
|
handle = await fs.open(tempPath, "wx");
|
|
@@ -1044,7 +1075,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1044
1075
|
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1045
1076
|
await openedHandle.close();
|
|
1046
1077
|
handle = void 0;
|
|
1047
|
-
await
|
|
1078
|
+
await commitAtomicWrite(tempPath, targetPath);
|
|
1048
1079
|
} catch (error) {
|
|
1049
1080
|
await handle?.close().catch(() => void 0);
|
|
1050
1081
|
await fs.unlink(tempPath).catch(() => void 0);
|
|
@@ -1138,130 +1169,6 @@ var CacheStackSnapshotManager = class {
|
|
|
1138
1169
|
}
|
|
1139
1170
|
};
|
|
1140
1171
|
|
|
1141
|
-
// src/internal/CacheStackValidation.ts
|
|
1142
|
-
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1143
|
-
var MAX_PATTERN_LENGTH = 1024;
|
|
1144
|
-
var MAX_TAGS_PER_OPERATION = 128;
|
|
1145
|
-
function validatePositiveNumber(name, value) {
|
|
1146
|
-
if (value === void 0) {
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
1150
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
function validateNonNegativeNumber(name, value) {
|
|
1154
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
1155
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
function validateLayerNumberOption(name, value) {
|
|
1159
|
-
if (value === void 0) {
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
if (typeof value === "number") {
|
|
1163
|
-
validateNonNegativeNumber(name, value);
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1167
|
-
if (layerValue === void 0) {
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
function validateRateLimitOptions(name, options) {
|
|
1174
|
-
if (!options) {
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
1178
|
-
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
1179
|
-
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
1180
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
1181
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
1182
|
-
}
|
|
1183
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
1184
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
function validateCacheKey(key) {
|
|
1188
|
-
if (key.length === 0) {
|
|
1189
|
-
throw new Error("Cache key must not be empty.");
|
|
1190
|
-
}
|
|
1191
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1192
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1193
|
-
}
|
|
1194
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1195
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
1196
|
-
}
|
|
1197
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
1198
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
1199
|
-
}
|
|
1200
|
-
return key;
|
|
1201
|
-
}
|
|
1202
|
-
function validateTag(tag) {
|
|
1203
|
-
if (tag.length === 0) {
|
|
1204
|
-
throw new Error("Cache tag must not be empty.");
|
|
1205
|
-
}
|
|
1206
|
-
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
1207
|
-
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1208
|
-
}
|
|
1209
|
-
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
1210
|
-
throw new Error("Cache tag contains unsupported control characters.");
|
|
1211
|
-
}
|
|
1212
|
-
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
1213
|
-
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
1214
|
-
}
|
|
1215
|
-
return tag;
|
|
1216
|
-
}
|
|
1217
|
-
function validateTags(tags) {
|
|
1218
|
-
if (!tags) {
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
1222
|
-
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
1223
|
-
}
|
|
1224
|
-
for (const tag of tags) {
|
|
1225
|
-
validateTag(tag);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
function validatePattern(pattern) {
|
|
1229
|
-
if (pattern.length === 0) {
|
|
1230
|
-
throw new Error("Pattern must not be empty.");
|
|
1231
|
-
}
|
|
1232
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
1233
|
-
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
1234
|
-
}
|
|
1235
|
-
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
1236
|
-
throw new Error("Pattern contains unsupported control characters.");
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
function validateTtlPolicy(name, policy) {
|
|
1240
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
if ("alignTo" in policy) {
|
|
1244
|
-
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
throw new Error(`${name} is invalid.`);
|
|
1248
|
-
}
|
|
1249
|
-
function validateAdaptiveTtlOptions(options) {
|
|
1250
|
-
if (!options || options === true) {
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1254
|
-
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1255
|
-
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1256
|
-
}
|
|
1257
|
-
function validateCircuitBreakerOptions(options) {
|
|
1258
|
-
if (!options) {
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1262
|
-
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
1172
|
// src/internal/CircuitBreakerManager.ts
|
|
1266
1173
|
var CircuitBreakerManager = class {
|
|
1267
1174
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -1359,6 +1266,7 @@ var CircuitBreakerManager = class {
|
|
|
1359
1266
|
|
|
1360
1267
|
// src/internal/FetchRateLimiter.ts
|
|
1361
1268
|
var MAX_BUCKETS = 1e4;
|
|
1269
|
+
var MAX_QUEUE_PER_BUCKET = 1e4;
|
|
1362
1270
|
var FetchRateLimiter = class {
|
|
1363
1271
|
buckets = /* @__PURE__ */ new Map();
|
|
1364
1272
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -1367,6 +1275,7 @@ var FetchRateLimiter = class {
|
|
|
1367
1275
|
nextFetcherBucketId = 0;
|
|
1368
1276
|
drainTimer;
|
|
1369
1277
|
isDisposed = false;
|
|
1278
|
+
rateLimitBypasses = 0;
|
|
1370
1279
|
async schedule(options, context, task) {
|
|
1371
1280
|
if (this.isDisposed) {
|
|
1372
1281
|
throw new Error("FetchRateLimiter has been disposed.");
|
|
@@ -1381,6 +1290,11 @@ var FetchRateLimiter = class {
|
|
|
1381
1290
|
return new Promise((resolve2, reject) => {
|
|
1382
1291
|
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
1383
1292
|
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
1293
|
+
if (queue.length >= MAX_QUEUE_PER_BUCKET) {
|
|
1294
|
+
this.rateLimitBypasses += 1;
|
|
1295
|
+
task().then(resolve2, reject);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1384
1298
|
queue.push({
|
|
1385
1299
|
bucketKey,
|
|
1386
1300
|
options: normalized,
|
|
@@ -1685,7 +1599,13 @@ var MetricsCollector = class {
|
|
|
1685
1599
|
};
|
|
1686
1600
|
|
|
1687
1601
|
// src/internal/TtlResolver.ts
|
|
1602
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1688
1603
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
1604
|
+
var secureRandom = {
|
|
1605
|
+
value() {
|
|
1606
|
+
return randomBytes2(4).readUInt32BE(0) / 4294967296;
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1689
1609
|
var TtlResolver = class {
|
|
1690
1610
|
accessProfiles = /* @__PURE__ */ new Map();
|
|
1691
1611
|
maxProfileEntries;
|
|
@@ -1748,7 +1668,7 @@ var TtlResolver = class {
|
|
|
1748
1668
|
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
1749
1669
|
return ttl;
|
|
1750
1670
|
}
|
|
1751
|
-
const delta = (
|
|
1671
|
+
const delta = (secureRandom.value() * 2 - 1) * jitter;
|
|
1752
1672
|
return Math.max(1, Math.round(ttl + delta));
|
|
1753
1673
|
}
|
|
1754
1674
|
resolvePolicyTtl(key, value, policy) {
|
|
@@ -2864,7 +2784,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2864
2784
|
} catch (error) {
|
|
2865
2785
|
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2866
2786
|
this.metricsCollector.increment("refreshErrors");
|
|
2867
|
-
this.logger.
|
|
2787
|
+
this.logger.warn?.("background-refresh-error", { key, error: this.formatError(error) });
|
|
2868
2788
|
} finally {
|
|
2869
2789
|
this.backgroundRefreshes.delete(key);
|
|
2870
2790
|
this.backgroundRefreshAbort.delete(key);
|
|
@@ -3162,7 +3082,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
3162
3082
|
}
|
|
3163
3083
|
}
|
|
3164
3084
|
shouldSkipLayer(layer) {
|
|
3165
|
-
|
|
3085
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
3086
|
+
const skip = shouldSkipLayer(degradedUntil);
|
|
3087
|
+
if (!skip && degradedUntil !== void 0) {
|
|
3088
|
+
this.layerDegradedUntil.delete(layer.name);
|
|
3089
|
+
}
|
|
3090
|
+
return skip;
|
|
3166
3091
|
}
|
|
3167
3092
|
async handleLayerFailure(layer, operation, error) {
|
|
3168
3093
|
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
@@ -3964,12 +3889,12 @@ var RedisLayer = class {
|
|
|
3964
3889
|
};
|
|
3965
3890
|
|
|
3966
3891
|
// src/layers/DiskLayer.ts
|
|
3967
|
-
import { createHash as createHash2, randomBytes as
|
|
3892
|
+
import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
|
|
3968
3893
|
import { promises as fs2 } from "fs";
|
|
3969
3894
|
import { join, resolve } from "path";
|
|
3970
3895
|
|
|
3971
3896
|
// src/internal/PayloadProtection.ts
|
|
3972
|
-
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as
|
|
3897
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
3973
3898
|
var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
|
|
3974
3899
|
var MAGIC_SIGNED = Buffer.from("LCS1:");
|
|
3975
3900
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -4033,7 +3958,7 @@ var PayloadProtection = class {
|
|
|
4033
3958
|
}
|
|
4034
3959
|
// ── Encryption (AES-256-GCM) ──────────────────────────────────────────
|
|
4035
3960
|
encrypt(plaintext, key) {
|
|
4036
|
-
const iv =
|
|
3961
|
+
const iv = randomBytes3(IV_LENGTH);
|
|
4037
3962
|
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
4038
3963
|
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4039
3964
|
const authTag = cipher.getAuthTag();
|
|
@@ -4146,7 +4071,7 @@ var DiskLayer = class {
|
|
|
4146
4071
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
|
|
4147
4072
|
const protectedPayload = this.protection.protect(raw);
|
|
4148
4073
|
const targetPath = this.keyToPath(key);
|
|
4149
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${
|
|
4074
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${randomBytes4(8).toString("hex")}.tmp`;
|
|
4150
4075
|
try {
|
|
4151
4076
|
await fs2.writeFile(tempPath, protectedPayload);
|
|
4152
4077
|
await fs2.rename(tempPath, targetPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layercache",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Production-ready multi-layer caching for Node.js. Stack memory + Redis + disk behind one API with stampede prevention, tag invalidation, stale serving, and full observability.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"nodejs",
|
|
19
19
|
"express",
|
|
20
20
|
"fastify",
|
|
21
|
-
"nestjs",
|
|
22
21
|
"hono",
|
|
23
22
|
"trpc",
|
|
24
23
|
"graphql",
|
|
@@ -54,20 +53,15 @@
|
|
|
54
53
|
},
|
|
55
54
|
"files": [
|
|
56
55
|
"dist",
|
|
57
|
-
"packages/nestjs/dist",
|
|
58
56
|
"examples",
|
|
59
57
|
"benchmarks"
|
|
60
58
|
],
|
|
61
59
|
"engines": {
|
|
62
60
|
"node": ">=20"
|
|
63
61
|
},
|
|
64
|
-
"workspaces": [
|
|
65
|
-
"packages/nestjs"
|
|
66
|
-
],
|
|
67
62
|
"scripts": {
|
|
68
63
|
"build": "tsup src/index.ts src/cli.ts src/edge.ts --format esm,cjs --dts",
|
|
69
|
-
"build:
|
|
70
|
-
"build:all": "npm run build && npm run build:nestjs",
|
|
64
|
+
"build:all": "npm run build",
|
|
71
65
|
"test": "vitest run",
|
|
72
66
|
"test:coverage": "vitest run --coverage",
|
|
73
67
|
"test:watch": "vitest",
|
|
@@ -96,16 +90,12 @@
|
|
|
96
90
|
},
|
|
97
91
|
"devDependencies": {
|
|
98
92
|
"@biomejs/biome": "^1.9.4",
|
|
99
|
-
"@nestjs/common": "^11.1.0",
|
|
100
|
-
"@nestjs/core": "^11.1.0",
|
|
101
93
|
"@types/autocannon": "^7.12.7",
|
|
102
94
|
"@types/node": "^22.15.2",
|
|
103
95
|
"@vitest/coverage-v8": "^4.1.2",
|
|
104
96
|
"autocannon": "^8.0.0",
|
|
105
97
|
"ioredis": "^5.6.1",
|
|
106
98
|
"ioredis-mock": "^8.13.0",
|
|
107
|
-
"reflect-metadata": "^0.2.2",
|
|
108
|
-
"rxjs": "^7.8.1",
|
|
109
99
|
"tsup": "^8.5.0",
|
|
110
100
|
"tsx": "^4.19.3",
|
|
111
101
|
"typescript": "^5.8.3",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common'
|
|
2
|
-
import Redis from 'ioredis'
|
|
3
|
-
import { CacheStackModule } from '../../packages/nestjs/src'
|
|
4
|
-
import { MemoryLayer, RedisLayer } from '../../src'
|
|
5
|
-
|
|
6
|
-
const redis = new Redis(process.env.REDIS_URL)
|
|
7
|
-
|
|
8
|
-
@Module({
|
|
9
|
-
imports: [
|
|
10
|
-
CacheStackModule.forRoot({
|
|
11
|
-
layers: [new MemoryLayer({ ttl: 20 }), new RedisLayer({ client: redis, ttl: 300 })]
|
|
12
|
-
})
|
|
13
|
-
]
|
|
14
|
-
})
|
|
15
|
-
export class AppModule {}
|