layercache 2.0.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-FFZCC7EQ.js → chunk-5CIBABDH.js} +149 -19
- package/dist/{chunk-7KMKQ6QZ.js → chunk-NBMG7DHT.js} +118 -13
- package/dist/cli.cjs +186 -25
- package/dist/cli.js +69 -13
- package/dist/{edge-D2FpRlyS.d.cts → edge-BDyuPmIq.d.cts} +509 -0
- package/dist/{edge-D2FpRlyS.d.ts → edge-BDyuPmIq.d.ts} +509 -0
- package/dist/edge.cjs +148 -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 +797 -82
- package/dist/index.d.cts +289 -3
- package/dist/index.d.ts +289 -3
- package/dist/index.js +528 -47
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,12 +12,13 @@ import {
|
|
|
12
12
|
validateTag,
|
|
13
13
|
validateTags,
|
|
14
14
|
validateTtlPolicy
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-NBMG7DHT.js";
|
|
16
16
|
import {
|
|
17
17
|
MemoryLayer,
|
|
18
18
|
TagIndex,
|
|
19
|
-
createHonoCacheMiddleware
|
|
20
|
-
|
|
19
|
+
createHonoCacheMiddleware,
|
|
20
|
+
normalizeHttpCacheUrl
|
|
21
|
+
} from "./chunk-5CIBABDH.js";
|
|
21
22
|
import {
|
|
22
23
|
PatternMatcher,
|
|
23
24
|
createStoredValueEnvelope,
|
|
@@ -157,6 +158,9 @@ function addMetricMap(base, delta) {
|
|
|
157
158
|
|
|
158
159
|
// src/CacheNamespace.ts
|
|
159
160
|
var CacheNamespace = class _CacheNamespace {
|
|
161
|
+
/**
|
|
162
|
+
* Creates a namespace backed by an existing cache stack.
|
|
163
|
+
*/
|
|
160
164
|
constructor(cache, prefix) {
|
|
161
165
|
this.cache = cache;
|
|
162
166
|
this.prefix = prefix;
|
|
@@ -166,9 +170,16 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
166
170
|
prefix;
|
|
167
171
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
168
172
|
metrics = createEmptyNamespaceMetrics();
|
|
173
|
+
/**
|
|
174
|
+
* Reads a key inside this namespace and optionally runs a read-through fetcher
|
|
175
|
+
* on miss or refresh.
|
|
176
|
+
*/
|
|
169
177
|
async get(key, fetcher, options) {
|
|
170
178
|
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
171
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Alias for `get(key, fetcher, options)` that makes the get-or-set behavior explicit.
|
|
182
|
+
*/
|
|
172
183
|
async getOrSet(key, fetcher, options) {
|
|
173
184
|
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
174
185
|
}
|
|
@@ -178,24 +189,69 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
178
189
|
async getOrThrow(key, fetcher, options) {
|
|
179
190
|
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
180
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Returns true when the namespaced key exists and has not expired in any layer.
|
|
194
|
+
*/
|
|
181
195
|
async has(key) {
|
|
182
196
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
183
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Returns the remaining TTL in milliseconds for the namespaced key.
|
|
200
|
+
*/
|
|
184
201
|
async ttl(key) {
|
|
185
202
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
186
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Stores a value under a namespaced key.
|
|
206
|
+
*/
|
|
187
207
|
async set(key, value, options) {
|
|
188
208
|
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
189
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Deletes a namespaced key from all layers.
|
|
212
|
+
*/
|
|
190
213
|
async delete(key) {
|
|
191
214
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
192
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Deletes multiple namespaced keys from all layers.
|
|
218
|
+
*/
|
|
193
219
|
async mdelete(keys) {
|
|
194
220
|
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
195
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Alias for `delete(key)` scoped to this namespace.
|
|
224
|
+
*/
|
|
225
|
+
async invalidateByKey(key) {
|
|
226
|
+
await this.trackMetrics(() => this.cache.invalidateByKey(this.qualify(key)));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Alias for `mdelete(keys)` scoped to this namespace.
|
|
230
|
+
*/
|
|
231
|
+
async invalidateByKeys(keys) {
|
|
232
|
+
await this.trackMetrics(() => this.cache.invalidateByKeys(keys.map((k) => this.qualify(k))));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Marks one exact namespaced key expired without deleting its stale value.
|
|
236
|
+
*/
|
|
237
|
+
async expireByKey(key) {
|
|
238
|
+
await this.trackMetrics(() => this.cache.expireByKey(this.qualify(key)));
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Marks multiple exact namespaced keys expired without deleting their stale values.
|
|
242
|
+
*/
|
|
243
|
+
async expireByKeys(keys) {
|
|
244
|
+
await this.trackMetrics(() => this.cache.expireByKeys(keys.map((k) => this.qualify(k))));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Clears all keys in this namespace by invalidating the namespace prefix.
|
|
248
|
+
*/
|
|
196
249
|
async clear() {
|
|
197
250
|
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
198
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Reads many namespaced keys concurrently.
|
|
254
|
+
*/
|
|
199
255
|
async mget(entries) {
|
|
200
256
|
return this.trackMetrics(
|
|
201
257
|
() => this.cache.mget(
|
|
@@ -207,6 +263,9 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
207
263
|
)
|
|
208
264
|
);
|
|
209
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Writes many namespaced entries concurrently.
|
|
268
|
+
*/
|
|
210
269
|
async mset(entries) {
|
|
211
270
|
await this.trackMetrics(
|
|
212
271
|
() => this.cache.mset(
|
|
@@ -218,12 +277,21 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
218
277
|
)
|
|
219
278
|
);
|
|
220
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Deletes keys associated with a tag scoped to this namespace.
|
|
282
|
+
*/
|
|
221
283
|
async invalidateByTag(tag) {
|
|
222
284
|
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
223
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Expires keys associated with a tag scoped to this namespace while preserving stale windows.
|
|
288
|
+
*/
|
|
224
289
|
async expireByTag(tag) {
|
|
225
290
|
await this.trackMetrics(() => this.cache.expireByTag(this.qualifyTag(tag)));
|
|
226
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Deletes keys associated with any or all namespace-scoped tags.
|
|
294
|
+
*/
|
|
227
295
|
async invalidateByTags(tags, mode = "any") {
|
|
228
296
|
await this.trackMetrics(
|
|
229
297
|
() => this.cache.invalidateByTags(
|
|
@@ -232,6 +300,9 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
232
300
|
)
|
|
233
301
|
);
|
|
234
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Expires keys associated with any or all namespace-scoped tags while preserving stale windows.
|
|
305
|
+
*/
|
|
235
306
|
async expireByTags(tags, mode = "any") {
|
|
236
307
|
await this.trackMetrics(
|
|
237
308
|
() => this.cache.expireByTags(
|
|
@@ -240,15 +311,27 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
240
311
|
)
|
|
241
312
|
);
|
|
242
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Deletes namespaced keys matching a wildcard pattern.
|
|
316
|
+
*/
|
|
243
317
|
async invalidateByPattern(pattern) {
|
|
244
318
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
245
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Expires namespaced keys matching a wildcard pattern while preserving stale windows.
|
|
322
|
+
*/
|
|
246
323
|
async expireByPattern(pattern) {
|
|
247
324
|
await this.trackMetrics(() => this.cache.expireByPattern(this.qualify(pattern)));
|
|
248
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Deletes namespaced keys with the provided prefix.
|
|
328
|
+
*/
|
|
249
329
|
async invalidateByPrefix(prefix) {
|
|
250
330
|
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
251
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Expires namespaced keys with the provided prefix while preserving stale windows.
|
|
334
|
+
*/
|
|
252
335
|
async expireByPrefix(prefix) {
|
|
253
336
|
await this.trackMetrics(() => this.cache.expireByPrefix(this.qualify(prefix)));
|
|
254
337
|
}
|
|
@@ -265,9 +348,15 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
265
348
|
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
266
349
|
};
|
|
267
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Returns a cached wrapper whose generated keys are scoped to this namespace.
|
|
353
|
+
*/
|
|
268
354
|
wrap(keyPrefix, fetcher, options) {
|
|
269
355
|
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
270
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Warms entries after qualifying each key and tag with this namespace prefix.
|
|
359
|
+
*/
|
|
271
360
|
warm(entries, options) {
|
|
272
361
|
return this.cache.warm(
|
|
273
362
|
entries.map((entry) => ({
|
|
@@ -278,9 +367,15 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
278
367
|
options
|
|
279
368
|
);
|
|
280
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Returns metrics accumulated by operations performed through this namespace.
|
|
372
|
+
*/
|
|
281
373
|
getMetrics() {
|
|
282
374
|
return cloneNamespaceMetrics(this.metrics);
|
|
283
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Returns hit-rate statistics for operations performed through this namespace.
|
|
378
|
+
*/
|
|
284
379
|
getHitRate() {
|
|
285
380
|
return computeNamespaceHitRate(this.metrics);
|
|
286
381
|
}
|
|
@@ -297,6 +392,9 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
297
392
|
validateNamespaceKey(childPrefix);
|
|
298
393
|
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
299
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Qualifies a raw key with this namespace prefix.
|
|
397
|
+
*/
|
|
300
398
|
qualify(key) {
|
|
301
399
|
return `${this.prefix}:${key}`;
|
|
302
400
|
}
|
|
@@ -816,7 +914,9 @@ var CacheStackMaintenance = class {
|
|
|
816
914
|
}
|
|
817
915
|
bumpKeyEpochs(keys) {
|
|
818
916
|
for (const key of keys) {
|
|
819
|
-
|
|
917
|
+
const nextEpoch = this.currentKeyEpoch(key) + 1;
|
|
918
|
+
this.keyEpochs.delete(key);
|
|
919
|
+
this.keyEpochs.set(key, nextEpoch);
|
|
820
920
|
}
|
|
821
921
|
this.pruneKeyEpochsIfNeeded();
|
|
822
922
|
}
|
|
@@ -875,10 +975,13 @@ var CacheStackMaintenance = class {
|
|
|
875
975
|
if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
|
|
876
976
|
return;
|
|
877
977
|
}
|
|
878
|
-
const
|
|
879
|
-
const toDelete = Math.ceil(sorted.length * 0.1);
|
|
978
|
+
const toDelete = Math.ceil(this.keyEpochs.size * 0.1);
|
|
880
979
|
for (let i = 0; i < toDelete; i++) {
|
|
881
|
-
this.keyEpochs.
|
|
980
|
+
const oldestKey = this.keyEpochs.keys().next().value;
|
|
981
|
+
if (oldestKey === void 0) {
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
this.keyEpochs.delete(oldestKey);
|
|
882
985
|
}
|
|
883
986
|
}
|
|
884
987
|
};
|
|
@@ -1012,22 +1115,28 @@ var CacheStackReader = class {
|
|
|
1012
1115
|
if (upToIndex < 0) {
|
|
1013
1116
|
return;
|
|
1014
1117
|
}
|
|
1118
|
+
const operations = [];
|
|
1015
1119
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
1016
1120
|
const layer = this.options.layers[index];
|
|
1017
1121
|
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1018
1122
|
continue;
|
|
1019
1123
|
}
|
|
1020
1124
|
const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1125
|
+
operations.push(
|
|
1126
|
+
(async () => {
|
|
1127
|
+
try {
|
|
1128
|
+
await layer.set(key, stored, ttl);
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
await this.options.handleLayerFailure(layer, "backfill", error);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
this.options.metricsCollector.increment("backfills");
|
|
1134
|
+
this.options.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1135
|
+
this.options.emit("backfill", { key, layer: layer.name });
|
|
1136
|
+
})()
|
|
1137
|
+
);
|
|
1030
1138
|
}
|
|
1139
|
+
await Promise.all(operations);
|
|
1031
1140
|
}
|
|
1032
1141
|
abortAllRefreshes() {
|
|
1033
1142
|
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
@@ -1151,7 +1260,15 @@ var CacheStackReader = class {
|
|
|
1151
1260
|
}
|
|
1152
1261
|
await this.options.sleep(pollIntervalMs);
|
|
1153
1262
|
}
|
|
1154
|
-
|
|
1263
|
+
if (!this.options.singleFlightCoordinator) {
|
|
1264
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1265
|
+
}
|
|
1266
|
+
return this.options.singleFlightCoordinator.execute(
|
|
1267
|
+
key,
|
|
1268
|
+
this.resolveSingleFlightOptions(),
|
|
1269
|
+
() => this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext),
|
|
1270
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1271
|
+
);
|
|
1155
1272
|
}
|
|
1156
1273
|
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1157
1274
|
key,
|
|
@@ -2066,6 +2183,7 @@ var TtlResolver = class {
|
|
|
2066
2183
|
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
2067
2184
|
profile.hits += 1;
|
|
2068
2185
|
profile.lastAccessAt = Date.now();
|
|
2186
|
+
this.accessProfiles.delete(key);
|
|
2069
2187
|
this.accessProfiles.set(key, profile);
|
|
2070
2188
|
this.pruneIfNeeded();
|
|
2071
2189
|
}
|
|
@@ -2155,21 +2273,27 @@ var TtlResolver = class {
|
|
|
2155
2273
|
return;
|
|
2156
2274
|
}
|
|
2157
2275
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
this.accessProfiles.delete(entry[0]);
|
|
2276
|
+
for (let i = 0; i < toRemove; i++) {
|
|
2277
|
+
const oldestKey = this.accessProfiles.keys().next().value;
|
|
2278
|
+
if (oldestKey === void 0) {
|
|
2279
|
+
break;
|
|
2163
2280
|
}
|
|
2281
|
+
this.accessProfiles.delete(oldestKey);
|
|
2164
2282
|
}
|
|
2165
2283
|
}
|
|
2166
2284
|
};
|
|
2167
2285
|
|
|
2168
2286
|
// src/serialization/JsonSerializer.ts
|
|
2169
2287
|
var JsonSerializer = class {
|
|
2288
|
+
/**
|
|
2289
|
+
* Serializes a value to JSON.
|
|
2290
|
+
*/
|
|
2170
2291
|
serialize(value) {
|
|
2171
2292
|
return JSON.stringify(value);
|
|
2172
2293
|
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Parses JSON and sanitizes the result before returning it.
|
|
2296
|
+
*/
|
|
2173
2297
|
deserialize(payload) {
|
|
2174
2298
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2175
2299
|
let parsed;
|
|
@@ -2196,6 +2320,9 @@ var StampedeGuard = class {
|
|
|
2196
2320
|
this.maxInFlight = options.maxInFlight ?? 1e4;
|
|
2197
2321
|
this.entryTimeoutMs = options.entryTimeoutMs;
|
|
2198
2322
|
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Deduplicates concurrent work for the same key in this process.
|
|
2325
|
+
*/
|
|
2199
2326
|
async execute(key, task) {
|
|
2200
2327
|
const existing = this.inFlight.get(key);
|
|
2201
2328
|
if (existing) {
|
|
@@ -2295,6 +2422,9 @@ var DebugLogger = class {
|
|
|
2295
2422
|
}
|
|
2296
2423
|
};
|
|
2297
2424
|
var CacheStack = class extends EventEmitter {
|
|
2425
|
+
/**
|
|
2426
|
+
* Creates a cache stack from ordered layers and optional global behavior settings.
|
|
2427
|
+
*/
|
|
2298
2428
|
constructor(layers, options = {}) {
|
|
2299
2429
|
super();
|
|
2300
2430
|
this.layers = layers;
|
|
@@ -2560,6 +2690,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2560
2690
|
});
|
|
2561
2691
|
});
|
|
2562
2692
|
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Clears every configured layer, removes tag metadata, resets internal TTL
|
|
2695
|
+
* profiles, and broadcasts a clear invalidation message.
|
|
2696
|
+
*/
|
|
2563
2697
|
async clear() {
|
|
2564
2698
|
await this.awaitStartup("clear");
|
|
2565
2699
|
this.maintenance.beginClearEpoch();
|
|
@@ -2589,6 +2723,58 @@ var CacheStack = class extends EventEmitter {
|
|
|
2589
2723
|
operation: "delete"
|
|
2590
2724
|
});
|
|
2591
2725
|
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Alias for `delete(key)` that matches the `invalidateBy*` API family.
|
|
2728
|
+
*/
|
|
2729
|
+
async invalidateByKey(key) {
|
|
2730
|
+
await this.delete(key);
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Alias for `mdelete(keys)` that matches the `invalidateBy*` API family.
|
|
2734
|
+
*/
|
|
2735
|
+
async invalidateByKeys(keys) {
|
|
2736
|
+
await this.mdelete(keys);
|
|
2737
|
+
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Marks one exact key expired without deleting its stale value.
|
|
2740
|
+
*/
|
|
2741
|
+
async expireByKey(key) {
|
|
2742
|
+
await this.observeOperation("layercache.expire_by_key", { "layercache.key": String(key ?? "") }, async () => {
|
|
2743
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2744
|
+
await this.awaitStartup("expireByKey");
|
|
2745
|
+
await this.expireKeys([normalizedKey]);
|
|
2746
|
+
await this.publishInvalidation({
|
|
2747
|
+
scope: "key",
|
|
2748
|
+
keys: [normalizedKey],
|
|
2749
|
+
sourceId: this.instanceId,
|
|
2750
|
+
operation: "expire"
|
|
2751
|
+
});
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Marks multiple exact keys expired without deleting their stale values.
|
|
2756
|
+
*/
|
|
2757
|
+
async expireByKeys(keys) {
|
|
2758
|
+
await this.observeOperation("layercache.expire_by_keys", void 0, async () => {
|
|
2759
|
+
if (keys.length === 0) {
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
2763
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
2764
|
+
await this.awaitStartup("expireByKeys");
|
|
2765
|
+
await this.expireKeys(cacheKeys);
|
|
2766
|
+
await this.publishInvalidation({
|
|
2767
|
+
scope: "keys",
|
|
2768
|
+
keys: cacheKeys,
|
|
2769
|
+
sourceId: this.instanceId,
|
|
2770
|
+
operation: "expire"
|
|
2771
|
+
});
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Reads many keys concurrently. Simple reads use layer-level bulk fast paths;
|
|
2776
|
+
* entries with fetchers or options fall back to per-entry read-through logic.
|
|
2777
|
+
*/
|
|
2592
2778
|
async mget(entries) {
|
|
2593
2779
|
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2594
2780
|
this.assertActive("mget");
|
|
@@ -2676,6 +2862,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2676
2862
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2677
2863
|
});
|
|
2678
2864
|
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Writes many entries concurrently using each layer's bulk write fast path
|
|
2867
|
+
* when available.
|
|
2868
|
+
*/
|
|
2679
2869
|
async mset(entries) {
|
|
2680
2870
|
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2681
2871
|
this.assertActive("mset");
|
|
@@ -2688,6 +2878,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2688
2878
|
await this.writeBatch(normalizedEntries);
|
|
2689
2879
|
});
|
|
2690
2880
|
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Pre-populates cache entries by running their fetchers with bounded
|
|
2883
|
+
* concurrency. Higher-priority entries run first.
|
|
2884
|
+
*/
|
|
2691
2885
|
async warm(entries, options = {}) {
|
|
2692
2886
|
this.assertActive("warm");
|
|
2693
2887
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
@@ -2738,6 +2932,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2738
2932
|
validateNamespaceKey(prefix);
|
|
2739
2933
|
return new CacheNamespace(this, prefix);
|
|
2740
2934
|
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Deletes every key currently associated with `tag` and broadcasts an
|
|
2937
|
+
* invalidation message.
|
|
2938
|
+
*/
|
|
2741
2939
|
async invalidateByTag(tag) {
|
|
2742
2940
|
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2743
2941
|
validateTag(tag);
|
|
@@ -2747,6 +2945,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2747
2945
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2748
2946
|
});
|
|
2749
2947
|
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Marks every key associated with `tag` as expired while preserving stale
|
|
2950
|
+
* windows for stale serving.
|
|
2951
|
+
*/
|
|
2750
2952
|
async expireByTag(tag) {
|
|
2751
2953
|
await this.observeOperation("layercache.expire_by_tag", void 0, async () => {
|
|
2752
2954
|
validateTag(tag);
|
|
@@ -2756,6 +2958,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2756
2958
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2757
2959
|
});
|
|
2758
2960
|
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Deletes keys associated with any or all of the provided tags and broadcasts
|
|
2963
|
+
* an invalidation message.
|
|
2964
|
+
*/
|
|
2759
2965
|
async invalidateByTags(tags, mode = "any") {
|
|
2760
2966
|
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2761
2967
|
if (tags.length === 0) {
|
|
@@ -2772,6 +2978,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2772
2978
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2773
2979
|
});
|
|
2774
2980
|
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Marks keys associated with any or all of the provided tags as expired while
|
|
2983
|
+
* preserving stale windows for stale serving.
|
|
2984
|
+
*/
|
|
2775
2985
|
async expireByTags(tags, mode = "any") {
|
|
2776
2986
|
await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
|
|
2777
2987
|
if (tags.length === 0) {
|
|
@@ -2788,6 +2998,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2788
2998
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2789
2999
|
});
|
|
2790
3000
|
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Deletes keys matching a wildcard pattern such as `user:*`.
|
|
3003
|
+
*/
|
|
2791
3004
|
async invalidateByPattern(pattern) {
|
|
2792
3005
|
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
2793
3006
|
validatePattern(pattern);
|
|
@@ -2800,6 +3013,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2800
3013
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2801
3014
|
});
|
|
2802
3015
|
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Marks keys matching a wildcard pattern as expired while preserving stale
|
|
3018
|
+
* windows for stale serving.
|
|
3019
|
+
*/
|
|
2803
3020
|
async expireByPattern(pattern) {
|
|
2804
3021
|
await this.observeOperation("layercache.expire_by_pattern", void 0, async () => {
|
|
2805
3022
|
validatePattern(pattern);
|
|
@@ -2812,6 +3029,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2812
3029
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2813
3030
|
});
|
|
2814
3031
|
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Deletes keys that start with the provided prefix.
|
|
3034
|
+
*/
|
|
2815
3035
|
async invalidateByPrefix(prefix) {
|
|
2816
3036
|
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2817
3037
|
await this.awaitStartup("invalidateByPrefix");
|
|
@@ -2821,6 +3041,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2821
3041
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2822
3042
|
});
|
|
2823
3043
|
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Marks keys that start with the provided prefix as expired while preserving
|
|
3046
|
+
* stale windows for stale serving.
|
|
3047
|
+
*/
|
|
2824
3048
|
async expireByPrefix(prefix) {
|
|
2825
3049
|
await this.observeOperation("layercache.expire_by_prefix", void 0, async () => {
|
|
2826
3050
|
await this.awaitStartup("expireByPrefix");
|
|
@@ -2830,9 +3054,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
2830
3054
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2831
3055
|
});
|
|
2832
3056
|
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Returns cumulative cache metrics since startup or the last `resetMetrics()`.
|
|
3059
|
+
*/
|
|
2833
3060
|
getMetrics() {
|
|
2834
3061
|
return this.metricsCollector.snapshot;
|
|
2835
3062
|
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Returns metrics plus layer degradation state and active background refresh count.
|
|
3065
|
+
*/
|
|
2836
3066
|
getStats() {
|
|
2837
3067
|
return {
|
|
2838
3068
|
metrics: this.getMetrics(),
|
|
@@ -2844,6 +3074,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2844
3074
|
backgroundRefreshes: this.reader.activeRefreshCount
|
|
2845
3075
|
};
|
|
2846
3076
|
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Resets cumulative metrics counters.
|
|
3079
|
+
*/
|
|
2847
3080
|
resetMetrics() {
|
|
2848
3081
|
this.metricsCollector.reset();
|
|
2849
3082
|
}
|
|
@@ -2853,6 +3086,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2853
3086
|
getHitRate() {
|
|
2854
3087
|
return this.metricsCollector.hitRate();
|
|
2855
3088
|
}
|
|
3089
|
+
/**
|
|
3090
|
+
* Runs each layer's `ping()` hook when available and returns per-layer health
|
|
3091
|
+
* and latency information.
|
|
3092
|
+
*/
|
|
2856
3093
|
async healthCheck() {
|
|
2857
3094
|
await this.startup;
|
|
2858
3095
|
return Promise.all(
|
|
@@ -2896,6 +3133,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
2896
3133
|
}
|
|
2897
3134
|
return this.currentGeneration;
|
|
2898
3135
|
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Returns the active generation prefix number used for future cache keys.
|
|
3138
|
+
*/
|
|
3139
|
+
getGeneration() {
|
|
3140
|
+
return this.currentGeneration;
|
|
3141
|
+
}
|
|
2899
3142
|
/**
|
|
2900
3143
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
2901
3144
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
@@ -2937,22 +3180,38 @@ var CacheStack = class extends EventEmitter {
|
|
|
2937
3180
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
2938
3181
|
return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
|
|
2939
3182
|
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Exports cache entries from configured layers for process-local snapshots.
|
|
3185
|
+
*/
|
|
2940
3186
|
async exportState() {
|
|
2941
3187
|
await this.awaitStartup("exportState");
|
|
2942
3188
|
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2943
3189
|
}
|
|
3190
|
+
/**
|
|
3191
|
+
* Imports entries produced by `exportState()` into the configured layers.
|
|
3192
|
+
*/
|
|
2944
3193
|
async importState(entries) {
|
|
2945
3194
|
await this.awaitStartup("importState");
|
|
2946
3195
|
await this.snapshots.importState(entries);
|
|
2947
3196
|
}
|
|
3197
|
+
/**
|
|
3198
|
+
* Writes a snapshot file containing current cache entries.
|
|
3199
|
+
*/
|
|
2948
3200
|
async persistToFile(filePath) {
|
|
2949
3201
|
this.assertActive("persistToFile");
|
|
2950
3202
|
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2951
3203
|
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Restores cache entries from a snapshot file.
|
|
3206
|
+
*/
|
|
2952
3207
|
async restoreFromFile(filePath) {
|
|
2953
3208
|
this.assertActive("restoreFromFile");
|
|
2954
3209
|
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2955
3210
|
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Flushes background work, unsubscribes from buses, disposes timers, and then
|
|
3213
|
+
* disposes each layer that provides `dispose()`.
|
|
3214
|
+
*/
|
|
2956
3215
|
async disconnect() {
|
|
2957
3216
|
if (!this.disconnectPromise) {
|
|
2958
3217
|
this.isDisconnecting = true;
|
|
@@ -3437,12 +3696,65 @@ var CacheStack = class extends EventEmitter {
|
|
|
3437
3696
|
}
|
|
3438
3697
|
};
|
|
3439
3698
|
|
|
3699
|
+
// src/generation/RedisGenerationStore.ts
|
|
3700
|
+
var DEFAULT_GENERATION_KEY = "layercache:generation";
|
|
3701
|
+
var RedisGenerationStore = class {
|
|
3702
|
+
client;
|
|
3703
|
+
key;
|
|
3704
|
+
constructor(options) {
|
|
3705
|
+
this.client = options.client;
|
|
3706
|
+
this.key = options.key ?? DEFAULT_GENERATION_KEY;
|
|
3707
|
+
}
|
|
3708
|
+
async get() {
|
|
3709
|
+
const stored = await this.client.get(this.key);
|
|
3710
|
+
if (stored === null) {
|
|
3711
|
+
return void 0;
|
|
3712
|
+
}
|
|
3713
|
+
return this.parseGeneration(stored);
|
|
3714
|
+
}
|
|
3715
|
+
async getOrInitialize(initialGeneration = 0) {
|
|
3716
|
+
this.assertGeneration(initialGeneration);
|
|
3717
|
+
await this.client.set(this.key, String(initialGeneration), "NX");
|
|
3718
|
+
const generation = await this.get();
|
|
3719
|
+
if (generation === void 0) {
|
|
3720
|
+
throw new Error(`RedisGenerationStore failed to initialize generation key "${this.key}".`);
|
|
3721
|
+
}
|
|
3722
|
+
return generation;
|
|
3723
|
+
}
|
|
3724
|
+
async set(generation) {
|
|
3725
|
+
this.assertGeneration(generation);
|
|
3726
|
+
await this.client.set(this.key, String(generation));
|
|
3727
|
+
}
|
|
3728
|
+
async bump() {
|
|
3729
|
+
const generation = await this.client.incr(this.key);
|
|
3730
|
+
this.assertGeneration(generation);
|
|
3731
|
+
return generation;
|
|
3732
|
+
}
|
|
3733
|
+
parseGeneration(value) {
|
|
3734
|
+
const generation = Number.parseInt(value, 10);
|
|
3735
|
+
if (String(generation) !== value || !this.isGeneration(generation)) {
|
|
3736
|
+
throw new Error(`RedisGenerationStore found invalid persisted generation value for key "${this.key}".`);
|
|
3737
|
+
}
|
|
3738
|
+
return generation;
|
|
3739
|
+
}
|
|
3740
|
+
assertGeneration(value) {
|
|
3741
|
+
if (!this.isGeneration(value)) {
|
|
3742
|
+
throw new Error("RedisGenerationStore generation must be a non-negative safe integer.");
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
isGeneration(value) {
|
|
3746
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
3747
|
+
}
|
|
3748
|
+
};
|
|
3749
|
+
|
|
3440
3750
|
// src/invalidation/RedisInvalidationBus.ts
|
|
3751
|
+
import { createHash, createHmac, timingSafeEqual } from "crypto";
|
|
3441
3752
|
var RedisInvalidationBus = class {
|
|
3442
3753
|
channel;
|
|
3443
3754
|
publisher;
|
|
3444
3755
|
subscriber;
|
|
3445
3756
|
logger;
|
|
3757
|
+
signingKey;
|
|
3446
3758
|
handlers = /* @__PURE__ */ new Set();
|
|
3447
3759
|
sharedListener;
|
|
3448
3760
|
subscribePromise;
|
|
@@ -3451,7 +3763,11 @@ var RedisInvalidationBus = class {
|
|
|
3451
3763
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
3452
3764
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
3453
3765
|
this.logger = options.logger;
|
|
3766
|
+
this.signingKey = options.signingSecret ? normalizeSigningSecret(options.signingSecret) : void 0;
|
|
3454
3767
|
}
|
|
3768
|
+
/**
|
|
3769
|
+
* Subscribes to invalidation messages and returns an unsubscribe function.
|
|
3770
|
+
*/
|
|
3455
3771
|
async subscribe(handler) {
|
|
3456
3772
|
const previousPromise = this.subscribePromise;
|
|
3457
3773
|
let resolveThis;
|
|
@@ -3483,8 +3799,11 @@ var RedisInvalidationBus = class {
|
|
|
3483
3799
|
}
|
|
3484
3800
|
};
|
|
3485
3801
|
}
|
|
3802
|
+
/**
|
|
3803
|
+
* Publishes an invalidation message to other subscribers.
|
|
3804
|
+
*/
|
|
3486
3805
|
async publish(message) {
|
|
3487
|
-
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
3806
|
+
await this.publisher.publish(this.channel, JSON.stringify(this.signingKey ? this.signMessage(message) : message));
|
|
3488
3807
|
}
|
|
3489
3808
|
async dispatchToHandlers(payload) {
|
|
3490
3809
|
let message;
|
|
@@ -3495,10 +3814,11 @@ var RedisInvalidationBus = class {
|
|
|
3495
3814
|
maxNodes: 1e4,
|
|
3496
3815
|
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
3497
3816
|
});
|
|
3498
|
-
|
|
3817
|
+
const candidate = this.signingKey ? this.verifySignedEnvelope(parsed) : parsed;
|
|
3818
|
+
if (!this.isInvalidationMessage(candidate)) {
|
|
3499
3819
|
throw new Error("Invalid invalidation payload shape.");
|
|
3500
3820
|
}
|
|
3501
|
-
message =
|
|
3821
|
+
message = candidate;
|
|
3502
3822
|
} catch (error) {
|
|
3503
3823
|
this.reportError("invalid invalidation payload", error);
|
|
3504
3824
|
return;
|
|
@@ -3523,6 +3843,34 @@ var RedisInvalidationBus = class {
|
|
|
3523
3843
|
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
3524
3844
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
3525
3845
|
}
|
|
3846
|
+
signMessage(message) {
|
|
3847
|
+
const payload = JSON.stringify(message);
|
|
3848
|
+
return {
|
|
3849
|
+
payload: message,
|
|
3850
|
+
signature: this.createSignature(payload)
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
verifySignedEnvelope(value) {
|
|
3854
|
+
if (!value || typeof value !== "object") {
|
|
3855
|
+
throw new Error("Signed invalidation envelope must be an object.");
|
|
3856
|
+
}
|
|
3857
|
+
const envelope = value;
|
|
3858
|
+
if (!envelope.payload || typeof envelope.payload !== "object" || typeof envelope.signature !== "string") {
|
|
3859
|
+
throw new Error("Signed invalidation envelope is missing payload or signature.");
|
|
3860
|
+
}
|
|
3861
|
+
const payload = JSON.stringify(envelope.payload);
|
|
3862
|
+
const expected = this.createSignature(payload);
|
|
3863
|
+
if (!isEqualSignature(envelope.signature, expected)) {
|
|
3864
|
+
throw new Error("Invalid invalidation message signature.");
|
|
3865
|
+
}
|
|
3866
|
+
return envelope.payload;
|
|
3867
|
+
}
|
|
3868
|
+
createSignature(payload) {
|
|
3869
|
+
if (!this.signingKey) {
|
|
3870
|
+
throw new Error("RedisInvalidationBus signing key is not configured.");
|
|
3871
|
+
}
|
|
3872
|
+
return createHmac("sha256", this.signingKey).update(payload).digest("hex");
|
|
3873
|
+
}
|
|
3526
3874
|
reportError(message, error) {
|
|
3527
3875
|
if (this.logger?.error) {
|
|
3528
3876
|
this.logger.error(message, { error });
|
|
@@ -3531,6 +3879,15 @@ var RedisInvalidationBus = class {
|
|
|
3531
3879
|
console.error(`[layercache] ${message}`, error);
|
|
3532
3880
|
}
|
|
3533
3881
|
};
|
|
3882
|
+
function normalizeSigningSecret(secret) {
|
|
3883
|
+
const raw = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
|
|
3884
|
+
return createHash("sha256").update(raw).digest();
|
|
3885
|
+
}
|
|
3886
|
+
function isEqualSignature(actual, expected) {
|
|
3887
|
+
const actualBuffer = Buffer.from(actual, "hex");
|
|
3888
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
3889
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
3890
|
+
}
|
|
3534
3891
|
|
|
3535
3892
|
// src/http/createCacheStatsHandler.ts
|
|
3536
3893
|
function createCacheStatsHandler(cache, options = {}) {
|
|
@@ -3628,7 +3985,7 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3628
3985
|
return;
|
|
3629
3986
|
}
|
|
3630
3987
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
3631
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${
|
|
3988
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeHttpCacheUrl(rawUrl)}`;
|
|
3632
3989
|
const cached = await cache.get(key, void 0, options);
|
|
3633
3990
|
if (cached !== null) {
|
|
3634
3991
|
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
@@ -3644,12 +4001,14 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3644
4001
|
if (originalJson) {
|
|
3645
4002
|
res.json = (body) => {
|
|
3646
4003
|
res.setHeader?.("x-cache", "MISS");
|
|
3647
|
-
|
|
3648
|
-
cache.
|
|
3649
|
-
|
|
3650
|
-
|
|
4004
|
+
if (isSuccessfulStatus(res.statusCode)) {
|
|
4005
|
+
cache.set(key, body, options).catch((err) => {
|
|
4006
|
+
cache.emit("error", {
|
|
4007
|
+
operation: "set",
|
|
4008
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4009
|
+
});
|
|
3651
4010
|
});
|
|
3652
|
-
}
|
|
4011
|
+
}
|
|
3653
4012
|
return originalJson(body);
|
|
3654
4013
|
};
|
|
3655
4014
|
}
|
|
@@ -3659,14 +4018,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3659
4018
|
}
|
|
3660
4019
|
};
|
|
3661
4020
|
}
|
|
3662
|
-
function
|
|
3663
|
-
|
|
3664
|
-
const parsed = new URL(url, "http://localhost");
|
|
3665
|
-
parsed.searchParams.sort();
|
|
3666
|
-
return parsed.pathname + parsed.search;
|
|
3667
|
-
} catch {
|
|
3668
|
-
return url;
|
|
3669
|
-
}
|
|
4021
|
+
function isSuccessfulStatus(statusCode) {
|
|
4022
|
+
return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
|
|
3670
4023
|
}
|
|
3671
4024
|
|
|
3672
4025
|
// src/integrations/graphql.ts
|
|
@@ -3783,6 +4136,9 @@ var RedisLayer = class {
|
|
|
3783
4136
|
decompressionMaxBytes;
|
|
3784
4137
|
commandTimeoutMs;
|
|
3785
4138
|
disconnectOnDispose;
|
|
4139
|
+
/**
|
|
4140
|
+
* Creates a Redis cache layer using an existing ioredis client.
|
|
4141
|
+
*/
|
|
3786
4142
|
constructor(options) {
|
|
3787
4143
|
this.client = options.client;
|
|
3788
4144
|
this.defaultTtl = options.ttl;
|
|
@@ -3797,10 +4153,16 @@ var RedisLayer = class {
|
|
|
3797
4153
|
this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
|
|
3798
4154
|
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
3799
4155
|
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Reads and unwraps a fresh value from Redis.
|
|
4158
|
+
*/
|
|
3800
4159
|
async get(key) {
|
|
3801
4160
|
const payload = await this.getEntry(key);
|
|
3802
4161
|
return unwrapStoredValue(payload);
|
|
3803
4162
|
}
|
|
4163
|
+
/**
|
|
4164
|
+
* Reads the raw stored value or envelope from Redis.
|
|
4165
|
+
*/
|
|
3804
4166
|
async getEntry(key) {
|
|
3805
4167
|
this.validateKey(key);
|
|
3806
4168
|
const payload = await this.runCommand(
|
|
@@ -3812,6 +4174,9 @@ var RedisLayer = class {
|
|
|
3812
4174
|
}
|
|
3813
4175
|
return this.deserializeOrDelete(key, payload);
|
|
3814
4176
|
}
|
|
4177
|
+
/**
|
|
4178
|
+
* Reads many raw entries from Redis using a pipeline.
|
|
4179
|
+
*/
|
|
3815
4180
|
async getMany(keys) {
|
|
3816
4181
|
if (keys.length === 0) {
|
|
3817
4182
|
return [];
|
|
@@ -3837,6 +4202,9 @@ var RedisLayer = class {
|
|
|
3837
4202
|
})
|
|
3838
4203
|
);
|
|
3839
4204
|
}
|
|
4205
|
+
/**
|
|
4206
|
+
* Writes many entries to Redis using a pipeline.
|
|
4207
|
+
*/
|
|
3840
4208
|
async setMany(entries) {
|
|
3841
4209
|
if (entries.length === 0) {
|
|
3842
4210
|
return;
|
|
@@ -3857,6 +4225,9 @@ var RedisLayer = class {
|
|
|
3857
4225
|
}
|
|
3858
4226
|
await this.runCommand(`mset(${entries.length})`, () => pipeline.exec());
|
|
3859
4227
|
}
|
|
4228
|
+
/**
|
|
4229
|
+
* Stores a value in Redis using the provided TTL or layer default TTL.
|
|
4230
|
+
*/
|
|
3860
4231
|
async set(key, value, ttl = this.defaultTtl) {
|
|
3861
4232
|
this.validateKey(key);
|
|
3862
4233
|
const serialized = this.primarySerializer().serialize(value);
|
|
@@ -3871,10 +4242,16 @@ var RedisLayer = class {
|
|
|
3871
4242
|
}
|
|
3872
4243
|
await this.runCommand(`set(${this.displayKey(key)})`, () => this.client.set(normalizedKey, payload));
|
|
3873
4244
|
}
|
|
4245
|
+
/**
|
|
4246
|
+
* Deletes a key from Redis.
|
|
4247
|
+
*/
|
|
3874
4248
|
async delete(key) {
|
|
3875
4249
|
this.validateKey(key);
|
|
3876
4250
|
await this.runCommand(`delete(${this.displayKey(key)})`, () => this.client.del(this.withPrefix(key)));
|
|
3877
4251
|
}
|
|
4252
|
+
/**
|
|
4253
|
+
* Deletes multiple keys from Redis in batches.
|
|
4254
|
+
*/
|
|
3878
4255
|
async deleteMany(keys) {
|
|
3879
4256
|
if (keys.length === 0) {
|
|
3880
4257
|
return;
|
|
@@ -3887,11 +4264,17 @@ var RedisLayer = class {
|
|
|
3887
4264
|
() => this.client.del(...keys.map((key) => this.withPrefix(key)))
|
|
3888
4265
|
);
|
|
3889
4266
|
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Returns true when the key exists in Redis.
|
|
4269
|
+
*/
|
|
3890
4270
|
async has(key) {
|
|
3891
4271
|
this.validateKey(key);
|
|
3892
4272
|
const exists = await this.runCommand(`has(${this.displayKey(key)})`, () => this.client.exists(this.withPrefix(key)));
|
|
3893
4273
|
return exists > 0;
|
|
3894
4274
|
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Returns remaining Redis TTL in milliseconds, or null when absent or non-expiring.
|
|
4277
|
+
*/
|
|
3895
4278
|
async ttl(key) {
|
|
3896
4279
|
this.validateKey(key);
|
|
3897
4280
|
const remaining = await this.runCommand(
|
|
@@ -3903,6 +4286,9 @@ var RedisLayer = class {
|
|
|
3903
4286
|
}
|
|
3904
4287
|
return remaining;
|
|
3905
4288
|
}
|
|
4289
|
+
/**
|
|
4290
|
+
* Returns the number of keys under this layer's prefix.
|
|
4291
|
+
*/
|
|
3906
4292
|
async size() {
|
|
3907
4293
|
if (!this.prefix) {
|
|
3908
4294
|
return this.runCommand("dbsize()", () => this.client.dbsize());
|
|
@@ -3920,6 +4306,9 @@ var RedisLayer = class {
|
|
|
3920
4306
|
} while (cursor !== "0");
|
|
3921
4307
|
return count;
|
|
3922
4308
|
}
|
|
4309
|
+
/**
|
|
4310
|
+
* Runs a Redis ping command.
|
|
4311
|
+
*/
|
|
3923
4312
|
async ping() {
|
|
3924
4313
|
try {
|
|
3925
4314
|
return await this.runCommand("ping()", () => this.client.ping()) === "PONG";
|
|
@@ -3927,6 +4316,9 @@ var RedisLayer = class {
|
|
|
3927
4316
|
return false;
|
|
3928
4317
|
}
|
|
3929
4318
|
}
|
|
4319
|
+
/**
|
|
4320
|
+
* Disconnects the Redis client when `disconnectOnDispose` is enabled.
|
|
4321
|
+
*/
|
|
3930
4322
|
async dispose() {
|
|
3931
4323
|
if (this.disconnectOnDispose) {
|
|
3932
4324
|
this.client.disconnect();
|
|
@@ -3959,6 +4351,9 @@ var RedisLayer = class {
|
|
|
3959
4351
|
}
|
|
3960
4352
|
} while (cursor !== "0");
|
|
3961
4353
|
}
|
|
4354
|
+
/**
|
|
4355
|
+
* Returns keys under this layer's prefix without the prefix included.
|
|
4356
|
+
*/
|
|
3962
4357
|
async keys() {
|
|
3963
4358
|
const pattern = `${this.prefix}*`;
|
|
3964
4359
|
const keys = await this.scanKeys(pattern);
|
|
@@ -3967,6 +4362,9 @@ var RedisLayer = class {
|
|
|
3967
4362
|
}
|
|
3968
4363
|
return keys.map((key) => key.slice(this.prefix.length));
|
|
3969
4364
|
}
|
|
4365
|
+
/**
|
|
4366
|
+
* Visits keys under this layer's prefix without materializing all results.
|
|
4367
|
+
*/
|
|
3970
4368
|
async forEachKey(visitor) {
|
|
3971
4369
|
const pattern = `${this.prefix}*`;
|
|
3972
4370
|
let cursor = "0";
|
|
@@ -4188,12 +4586,12 @@ var RedisLayer = class {
|
|
|
4188
4586
|
};
|
|
4189
4587
|
|
|
4190
4588
|
// src/layers/DiskLayer.ts
|
|
4191
|
-
import { createHash as
|
|
4589
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
4192
4590
|
import { promises as fs2 } from "fs";
|
|
4193
4591
|
import { join, resolve } from "path";
|
|
4194
4592
|
|
|
4195
4593
|
// src/internal/PayloadProtection.ts
|
|
4196
|
-
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
4594
|
+
import { createCipheriv, createDecipheriv, createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4197
4595
|
var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
|
|
4198
4596
|
var MAGIC_SIGNED = Buffer.from("LCS1:");
|
|
4199
4597
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -4206,11 +4604,11 @@ var PayloadProtection = class {
|
|
|
4206
4604
|
constructor(options) {
|
|
4207
4605
|
if (options.encryptionKey) {
|
|
4208
4606
|
const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
|
|
4209
|
-
this.encryptionKey =
|
|
4607
|
+
this.encryptionKey = createHash2("sha256").update(raw).digest();
|
|
4210
4608
|
}
|
|
4211
4609
|
if (options.signingKey && !options.encryptionKey) {
|
|
4212
4610
|
const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
|
|
4213
|
-
this.signingKey =
|
|
4611
|
+
this.signingKey = createHash2("sha256").update(raw).digest();
|
|
4214
4612
|
}
|
|
4215
4613
|
}
|
|
4216
4614
|
/** Returns `true` when any protection (encryption or signing) is configured. */
|
|
@@ -4282,15 +4680,15 @@ var PayloadProtection = class {
|
|
|
4282
4680
|
}
|
|
4283
4681
|
// ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
|
|
4284
4682
|
sign(payload, key) {
|
|
4285
|
-
const hmac =
|
|
4683
|
+
const hmac = createHmac2("sha256", key).update(payload).digest();
|
|
4286
4684
|
return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
|
|
4287
4685
|
}
|
|
4288
4686
|
verify(payload, key) {
|
|
4289
4687
|
const headerEnd = MAGIC_SIGNED.length;
|
|
4290
4688
|
const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
|
|
4291
4689
|
const data = payload.subarray(headerEnd + HMAC_LENGTH);
|
|
4292
|
-
const expectedHmac =
|
|
4293
|
-
if (receivedHmac.length !== HMAC_LENGTH || !
|
|
4690
|
+
const expectedHmac = createHmac2("sha256", key).update(data).digest();
|
|
4691
|
+
if (receivedHmac.length !== HMAC_LENGTH || !timingSafeEqual2(receivedHmac, expectedHmac)) {
|
|
4294
4692
|
throw new PayloadProtectionError(
|
|
4295
4693
|
"HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
|
|
4296
4694
|
);
|
|
@@ -4324,6 +4722,9 @@ var DiskLayer = class {
|
|
|
4324
4722
|
maxEntryBytes;
|
|
4325
4723
|
protection;
|
|
4326
4724
|
writeQueue = Promise.resolve();
|
|
4725
|
+
/**
|
|
4726
|
+
* Creates a disk-backed cache layer.
|
|
4727
|
+
*/
|
|
4327
4728
|
constructor(options) {
|
|
4328
4729
|
this.directory = this.resolveDirectory(options.directory);
|
|
4329
4730
|
this.defaultTtl = options.ttl;
|
|
@@ -4336,9 +4737,15 @@ var DiskLayer = class {
|
|
|
4336
4737
|
signingKey: options.signingKey
|
|
4337
4738
|
});
|
|
4338
4739
|
}
|
|
4740
|
+
/**
|
|
4741
|
+
* Reads and unwraps a fresh value from disk.
|
|
4742
|
+
*/
|
|
4339
4743
|
async get(key) {
|
|
4340
4744
|
return unwrapStoredValue(await this.getEntry(key));
|
|
4341
4745
|
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Reads the raw stored value or envelope from disk.
|
|
4748
|
+
*/
|
|
4342
4749
|
async getEntry(key) {
|
|
4343
4750
|
const filePath = this.keyToPath(key);
|
|
4344
4751
|
const raw = await this.readEntryFile(filePath);
|
|
@@ -4358,6 +4765,9 @@ var DiskLayer = class {
|
|
|
4358
4765
|
}
|
|
4359
4766
|
return entry.value;
|
|
4360
4767
|
}
|
|
4768
|
+
/**
|
|
4769
|
+
* Stores a value on disk using the provided TTL or layer default TTL.
|
|
4770
|
+
*/
|
|
4361
4771
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4362
4772
|
await this.enqueueWrite(async () => {
|
|
4363
4773
|
await fs2.mkdir(this.directory, { recursive: true });
|
|
@@ -4383,16 +4793,28 @@ var DiskLayer = class {
|
|
|
4383
4793
|
}
|
|
4384
4794
|
});
|
|
4385
4795
|
}
|
|
4796
|
+
/**
|
|
4797
|
+
* Reads many raw entries from disk.
|
|
4798
|
+
*/
|
|
4386
4799
|
async getMany(keys) {
|
|
4387
4800
|
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
4388
4801
|
}
|
|
4802
|
+
/**
|
|
4803
|
+
* Writes many entries to disk.
|
|
4804
|
+
*/
|
|
4389
4805
|
async setMany(entries) {
|
|
4390
4806
|
await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
|
|
4391
4807
|
}
|
|
4808
|
+
/**
|
|
4809
|
+
* Returns true when the key exists and has not expired.
|
|
4810
|
+
*/
|
|
4392
4811
|
async has(key) {
|
|
4393
4812
|
const value = await this.getEntry(key);
|
|
4394
4813
|
return value !== null;
|
|
4395
4814
|
}
|
|
4815
|
+
/**
|
|
4816
|
+
* Returns remaining TTL in milliseconds, or null when absent or non-expiring.
|
|
4817
|
+
*/
|
|
4396
4818
|
async ttl(key) {
|
|
4397
4819
|
const filePath = this.keyToPath(key);
|
|
4398
4820
|
const raw = await this.readEntryFile(filePath);
|
|
@@ -4415,14 +4837,23 @@ var DiskLayer = class {
|
|
|
4415
4837
|
}
|
|
4416
4838
|
return remaining;
|
|
4417
4839
|
}
|
|
4840
|
+
/**
|
|
4841
|
+
* Deletes a key from disk.
|
|
4842
|
+
*/
|
|
4418
4843
|
async delete(key) {
|
|
4419
4844
|
await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
|
|
4420
4845
|
}
|
|
4846
|
+
/**
|
|
4847
|
+
* Deletes multiple keys from disk.
|
|
4848
|
+
*/
|
|
4421
4849
|
async deleteMany(keys) {
|
|
4422
4850
|
await this.enqueueWrite(async () => {
|
|
4423
4851
|
await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
|
|
4424
4852
|
});
|
|
4425
4853
|
}
|
|
4854
|
+
/**
|
|
4855
|
+
* Removes all cache entry files from this layer's directory.
|
|
4856
|
+
*/
|
|
4426
4857
|
async clear() {
|
|
4427
4858
|
await this.enqueueWrite(async () => {
|
|
4428
4859
|
let entries;
|
|
@@ -4447,11 +4878,17 @@ var DiskLayer = class {
|
|
|
4447
4878
|
});
|
|
4448
4879
|
return keys;
|
|
4449
4880
|
}
|
|
4881
|
+
/**
|
|
4882
|
+
* Visits all non-expired keys stored on disk.
|
|
4883
|
+
*/
|
|
4450
4884
|
async forEachKey(visitor) {
|
|
4451
4885
|
await this.scanEntries(async (entry) => {
|
|
4452
4886
|
await visitor(entry.key);
|
|
4453
4887
|
});
|
|
4454
4888
|
}
|
|
4889
|
+
/**
|
|
4890
|
+
* Returns the number of non-expired entries stored on disk.
|
|
4891
|
+
*/
|
|
4455
4892
|
async size() {
|
|
4456
4893
|
let count = 0;
|
|
4457
4894
|
await this.scanEntries(async () => {
|
|
@@ -4459,6 +4896,9 @@ var DiskLayer = class {
|
|
|
4459
4896
|
});
|
|
4460
4897
|
return count;
|
|
4461
4898
|
}
|
|
4899
|
+
/**
|
|
4900
|
+
* Verifies the cache directory can be created.
|
|
4901
|
+
*/
|
|
4462
4902
|
async ping() {
|
|
4463
4903
|
try {
|
|
4464
4904
|
await fs2.mkdir(this.directory, { recursive: true });
|
|
@@ -4467,10 +4907,13 @@ var DiskLayer = class {
|
|
|
4467
4907
|
return false;
|
|
4468
4908
|
}
|
|
4469
4909
|
}
|
|
4910
|
+
/**
|
|
4911
|
+
* Reserved for interface compatibility; DiskLayer does not hold persistent handles.
|
|
4912
|
+
*/
|
|
4470
4913
|
async dispose() {
|
|
4471
4914
|
}
|
|
4472
4915
|
keyToPath(key) {
|
|
4473
|
-
const hash =
|
|
4916
|
+
const hash = createHash3("sha256").update(key).digest("hex");
|
|
4474
4917
|
return join(this.directory, `${hash}.lc`);
|
|
4475
4918
|
}
|
|
4476
4919
|
resolveDirectory(directory) {
|
|
@@ -4670,6 +5113,9 @@ var MemcachedLayer = class {
|
|
|
4670
5113
|
client;
|
|
4671
5114
|
keyPrefix;
|
|
4672
5115
|
serializer;
|
|
5116
|
+
/**
|
|
5117
|
+
* Creates a Memcached cache layer using a compatible client.
|
|
5118
|
+
*/
|
|
4673
5119
|
constructor(options) {
|
|
4674
5120
|
this.client = options.client;
|
|
4675
5121
|
this.defaultTtl = options.ttl;
|
|
@@ -4677,9 +5123,15 @@ var MemcachedLayer = class {
|
|
|
4677
5123
|
this.keyPrefix = options.keyPrefix ?? "";
|
|
4678
5124
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
4679
5125
|
}
|
|
5126
|
+
/**
|
|
5127
|
+
* Reads and unwraps a fresh value from Memcached.
|
|
5128
|
+
*/
|
|
4680
5129
|
async get(key) {
|
|
4681
5130
|
return unwrapStoredValue(await this.getEntry(key));
|
|
4682
5131
|
}
|
|
5132
|
+
/**
|
|
5133
|
+
* Reads the raw stored value or envelope from Memcached.
|
|
5134
|
+
*/
|
|
4683
5135
|
async getEntry(key) {
|
|
4684
5136
|
this.validateKey(key);
|
|
4685
5137
|
const result = await this.client.get(this.withPrefix(key));
|
|
@@ -4692,9 +5144,15 @@ var MemcachedLayer = class {
|
|
|
4692
5144
|
return null;
|
|
4693
5145
|
}
|
|
4694
5146
|
}
|
|
5147
|
+
/**
|
|
5148
|
+
* Reads many raw entries from Memcached.
|
|
5149
|
+
*/
|
|
4695
5150
|
async getMany(keys) {
|
|
4696
5151
|
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
4697
5152
|
}
|
|
5153
|
+
/**
|
|
5154
|
+
* Stores a value in Memcached using the provided TTL or layer default TTL.
|
|
5155
|
+
*/
|
|
4698
5156
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4699
5157
|
this.validateKey(key);
|
|
4700
5158
|
const payload = this.serializer.serialize(value);
|
|
@@ -4702,18 +5160,30 @@ var MemcachedLayer = class {
|
|
|
4702
5160
|
expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
|
|
4703
5161
|
});
|
|
4704
5162
|
}
|
|
5163
|
+
/**
|
|
5164
|
+
* Returns true when the key exists in Memcached.
|
|
5165
|
+
*/
|
|
4705
5166
|
async has(key) {
|
|
4706
5167
|
this.validateKey(key);
|
|
4707
5168
|
const result = await this.client.get(this.withPrefix(key));
|
|
4708
5169
|
return result !== null && result.value !== null;
|
|
4709
5170
|
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Deletes a key from Memcached.
|
|
5173
|
+
*/
|
|
4710
5174
|
async delete(key) {
|
|
4711
5175
|
this.validateKey(key);
|
|
4712
5176
|
await this.client.delete(this.withPrefix(key));
|
|
4713
5177
|
}
|
|
5178
|
+
/**
|
|
5179
|
+
* Deletes multiple keys from Memcached.
|
|
5180
|
+
*/
|
|
4714
5181
|
async deleteMany(keys) {
|
|
4715
5182
|
await Promise.all(keys.map((key) => this.delete(key)));
|
|
4716
5183
|
}
|
|
5184
|
+
/**
|
|
5185
|
+
* Always throws because Memcached has no safe prefix clear primitive.
|
|
5186
|
+
*/
|
|
4717
5187
|
async clear() {
|
|
4718
5188
|
throw new Error(
|
|
4719
5189
|
"MemcachedLayer.clear() is not supported. Use a key prefix and rotate it to effectively invalidate all keys."
|
|
@@ -4741,9 +5211,15 @@ var MemcachedLayer = class {
|
|
|
4741
5211
|
// src/serialization/MsgpackSerializer.ts
|
|
4742
5212
|
import { decode, encode } from "@msgpack/msgpack";
|
|
4743
5213
|
var MsgpackSerializer = class {
|
|
5214
|
+
/**
|
|
5215
|
+
* Serializes a value to MessagePack bytes.
|
|
5216
|
+
*/
|
|
4744
5217
|
serialize(value) {
|
|
4745
5218
|
return Buffer.from(encode(value));
|
|
4746
5219
|
}
|
|
5220
|
+
/**
|
|
5221
|
+
* Decodes MessagePack bytes and sanitizes the result before returning it.
|
|
5222
|
+
*/
|
|
4747
5223
|
deserialize(payload) {
|
|
4748
5224
|
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
4749
5225
|
return sanitizeStructuredData(decode(normalized), {
|
|
@@ -4777,6 +5253,10 @@ var RedisSingleFlightCoordinator = class {
|
|
|
4777
5253
|
this.prefix = options.prefix ?? "layercache:singleflight";
|
|
4778
5254
|
this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
|
|
4779
5255
|
}
|
|
5256
|
+
/**
|
|
5257
|
+
* Executes `worker` when this process acquires the Redis lock; otherwise runs
|
|
5258
|
+
* `waiter` while another process owns the work.
|
|
5259
|
+
*/
|
|
4780
5260
|
async execute(key, options, worker, waiter) {
|
|
4781
5261
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
4782
5262
|
const token = randomUUID();
|
|
@@ -4932,6 +5412,7 @@ export {
|
|
|
4932
5412
|
MemoryLayer,
|
|
4933
5413
|
MsgpackSerializer,
|
|
4934
5414
|
PatternMatcher,
|
|
5415
|
+
RedisGenerationStore,
|
|
4935
5416
|
RedisInvalidationBus,
|
|
4936
5417
|
RedisLayer,
|
|
4937
5418
|
RedisSingleFlightCoordinator,
|