@trieb.work/nextjs-turbo-redis-cache 1.2.0 → 1.3.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/.github/workflows/ci.yml +30 -6
- package/.github/workflows/release.yml +6 -3
- package/.next/trace +11 -0
- package/.vscode/settings.json +10 -0
- package/CHANGELOG.md +61 -0
- package/README.md +149 -34
- package/dist/index.d.mts +92 -20
- package/dist/index.d.ts +92 -20
- package/dist/index.js +319 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +315 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -9
- package/scripts/vitest-run-staged.cjs +1 -1
- package/src/CachedHandler.ts +23 -9
- package/src/DeduplicatedRequestHandler.ts +50 -1
- package/src/RedisStringsHandler.ts +330 -89
- package/src/SyncedMap.ts +74 -4
- package/src/index.ts +4 -2
- package/src/utils/debug.ts +30 -0
- package/src/utils/json.ts +26 -0
- package/test/integration/next-app/README.md +36 -0
- package/test/integration/next-app/eslint.config.mjs +16 -0
- package/test/integration/next-app/next.config.js +6 -0
- package/test/integration/next-app/package-lock.json +5833 -0
- package/test/integration/next-app/package.json +29 -0
- package/test/integration/next-app/pnpm-lock.yaml +3679 -0
- package/test/integration/next-app/postcss.config.mjs +5 -0
- package/test/integration/next-app/public/file.svg +1 -0
- package/test/integration/next-app/public/globe.svg +1 -0
- package/test/integration/next-app/public/next.svg +1 -0
- package/test/integration/next-app/public/vercel.svg +1 -0
- package/test/integration/next-app/public/window.svg +1 -0
- package/test/integration/next-app/src/app/api/cached-static-fetch/route.ts +18 -0
- package/test/integration/next-app/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
- package/test/integration/next-app/src/app/api/revalidatePath/route.ts +15 -0
- package/test/integration/next-app/src/app/api/revalidateTag/route.ts +15 -0
- package/test/integration/next-app/src/app/api/revalidated-fetch/route.ts +17 -0
- package/test/integration/next-app/src/app/api/uncached-fetch/route.ts +15 -0
- package/test/integration/next-app/src/app/globals.css +26 -0
- package/test/integration/next-app/src/app/layout.tsx +59 -0
- package/test/integration/next-app/src/app/page.tsx +755 -0
- package/test/integration/next-app/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
- package/test/integration/next-app/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app/src/app/pages/no-fetch/default-page/page.tsx +55 -0
- package/test/integration/next-app/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
- package/test/integration/next-app/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
- package/test/integration/next-app/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app/src/app/revalidation-interface.tsx +267 -0
- package/test/integration/next-app/tsconfig.json +27 -0
- package/test/integration/next-app-customized/README.md +36 -0
- package/test/integration/next-app-customized/customized-cache-handler.js +34 -0
- package/test/integration/next-app-customized/eslint.config.mjs +16 -0
- package/test/integration/next-app-customized/next.config.js +6 -0
- package/test/integration/nextjs-cache-handler.integration.test.ts +840 -0
- package/vite.config.ts +23 -8
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
RedisStringsHandler: () => RedisStringsHandler,
|
|
23
24
|
default: () => index_default
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -27,6 +28,27 @@ module.exports = __toCommonJS(index_exports);
|
|
|
27
28
|
// src/RedisStringsHandler.ts
|
|
28
29
|
var import_redis = require("redis");
|
|
29
30
|
|
|
31
|
+
// src/utils/debug.ts
|
|
32
|
+
function debug(color = "none", ...args) {
|
|
33
|
+
const colorCode = {
|
|
34
|
+
red: "\x1B[31m",
|
|
35
|
+
blue: "\x1B[34m",
|
|
36
|
+
green: "\x1B[32m",
|
|
37
|
+
yellow: "\x1B[33m",
|
|
38
|
+
cyan: "\x1B[36m",
|
|
39
|
+
white: "\x1B[37m",
|
|
40
|
+
none: ""
|
|
41
|
+
};
|
|
42
|
+
if (process.env.DEBUG_CACHE_HANDLER) {
|
|
43
|
+
console.log(colorCode[color], "DEBUG CACHE HANDLER: ", ...args);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function debugVerbose(color, ...args) {
|
|
47
|
+
if (process.env.DEBUG_CACHE_HANDLER_VERBOSE_VERBOSE) {
|
|
48
|
+
console.log("\x1B[35m", "DEBUG SYNCED MAP: ", ...args);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
// src/SyncedMap.ts
|
|
31
53
|
var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
|
|
32
54
|
var SyncedMap = class {
|
|
@@ -142,24 +164,56 @@ var SyncedMap = class {
|
|
|
142
164
|
}
|
|
143
165
|
}
|
|
144
166
|
};
|
|
145
|
-
const keyEventHandler = async (
|
|
146
|
-
|
|
167
|
+
const keyEventHandler = async (key, message) => {
|
|
168
|
+
debug(
|
|
169
|
+
"yellow",
|
|
170
|
+
"SyncedMap.keyEventHandler() called with message",
|
|
171
|
+
this.redisKey,
|
|
172
|
+
message,
|
|
173
|
+
key
|
|
174
|
+
);
|
|
147
175
|
if (key.startsWith(this.keyPrefix)) {
|
|
148
176
|
const keyInMap = key.substring(this.keyPrefix.length);
|
|
149
177
|
if (this.filterKeys(keyInMap)) {
|
|
178
|
+
debugVerbose(
|
|
179
|
+
"SyncedMap.keyEventHandler() key matches filter and will be deleted",
|
|
180
|
+
this.redisKey,
|
|
181
|
+
message,
|
|
182
|
+
key
|
|
183
|
+
);
|
|
150
184
|
await this.delete(keyInMap, true);
|
|
151
185
|
}
|
|
186
|
+
} else {
|
|
187
|
+
debugVerbose(
|
|
188
|
+
"SyncedMap.keyEventHandler() key does not have prefix",
|
|
189
|
+
this.redisKey,
|
|
190
|
+
message,
|
|
191
|
+
key
|
|
192
|
+
);
|
|
152
193
|
}
|
|
153
194
|
};
|
|
154
195
|
try {
|
|
155
|
-
await this.subscriberClient.connect()
|
|
196
|
+
await this.subscriberClient.connect().catch(async () => {
|
|
197
|
+
await this.subscriberClient.connect();
|
|
198
|
+
});
|
|
199
|
+
const keyspaceEventConfig = (await this.subscriberClient.configGet("notify-keyspace-events"))?.["notify-keyspace-events"];
|
|
200
|
+
if (!keyspaceEventConfig.includes("E")) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"Keyspace event configuration has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (!keyspaceEventConfig.includes("A") && !(keyspaceEventConfig.includes("x") && keyspaceEventConfig.includes("e"))) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"Keyspace event configuration has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
156
210
|
await Promise.all([
|
|
157
211
|
// We use a custom channel for insert/delete For the following reason:
|
|
158
212
|
// With custom channel we can delete multiple entries in one message. If we would listen to unlink / del we
|
|
159
213
|
// could get thousands of messages for one revalidateTag (For example revalidateTag("algolia") would send an enormous amount of network packages)
|
|
160
214
|
// Also we can send the value in the message for insert
|
|
161
215
|
this.subscriberClient.subscribe(this.syncChannel, syncHandler),
|
|
162
|
-
// Subscribe to Redis
|
|
216
|
+
// Subscribe to Redis keyevent notifications for evicted and expired keys
|
|
163
217
|
this.subscriberClient.subscribe(
|
|
164
218
|
`__keyevent@${this.database}__:evicted`,
|
|
165
219
|
keyEventHandler
|
|
@@ -191,9 +245,19 @@ var SyncedMap = class {
|
|
|
191
245
|
await this.setupLock;
|
|
192
246
|
}
|
|
193
247
|
get(key) {
|
|
248
|
+
debugVerbose(
|
|
249
|
+
"SyncedMap.get() called with key",
|
|
250
|
+
key,
|
|
251
|
+
JSON.stringify(this.map.get(key))?.substring(0, 100)
|
|
252
|
+
);
|
|
194
253
|
return this.map.get(key);
|
|
195
254
|
}
|
|
196
255
|
async set(key, value) {
|
|
256
|
+
debugVerbose(
|
|
257
|
+
"SyncedMap.set() called with key",
|
|
258
|
+
key,
|
|
259
|
+
JSON.stringify(value)?.substring(0, 100)
|
|
260
|
+
);
|
|
197
261
|
this.map.set(key, value);
|
|
198
262
|
const operations = [];
|
|
199
263
|
if (this.customizedSync?.withoutSetSync) {
|
|
@@ -220,7 +284,15 @@ var SyncedMap = class {
|
|
|
220
284
|
);
|
|
221
285
|
await Promise.all(operations);
|
|
222
286
|
}
|
|
287
|
+
// /api/revalidated-fetch
|
|
288
|
+
// true
|
|
223
289
|
async delete(keys, withoutSyncMessage = false) {
|
|
290
|
+
debugVerbose(
|
|
291
|
+
"SyncedMap.delete() called with keys",
|
|
292
|
+
this.redisKey,
|
|
293
|
+
keys,
|
|
294
|
+
withoutSyncMessage
|
|
295
|
+
);
|
|
224
296
|
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
225
297
|
const operations = [];
|
|
226
298
|
for (const key of keysArray) {
|
|
@@ -242,6 +314,12 @@ var SyncedMap = class {
|
|
|
242
314
|
);
|
|
243
315
|
}
|
|
244
316
|
await Promise.all(operations);
|
|
317
|
+
debugVerbose(
|
|
318
|
+
"SyncedMap.delete() finished operations",
|
|
319
|
+
this.redisKey,
|
|
320
|
+
keys,
|
|
321
|
+
operations.length
|
|
322
|
+
);
|
|
245
323
|
}
|
|
246
324
|
has(key) {
|
|
247
325
|
return this.map.has(key);
|
|
@@ -256,19 +334,59 @@ var DeduplicatedRequestHandler = class {
|
|
|
256
334
|
constructor(fn, cachingTimeMs, inMemoryDeduplicationCache) {
|
|
257
335
|
// Method to handle deduplicated requests
|
|
258
336
|
this.deduplicatedFunction = (key) => {
|
|
337
|
+
debugVerbose(
|
|
338
|
+
"DeduplicatedRequestHandler.deduplicatedFunction() called with",
|
|
339
|
+
key
|
|
340
|
+
);
|
|
259
341
|
const self = this;
|
|
260
342
|
const dedupedFn = async (...args) => {
|
|
343
|
+
debugVerbose(
|
|
344
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn called with",
|
|
345
|
+
key
|
|
346
|
+
);
|
|
261
347
|
if (self.inMemoryDeduplicationCache && self.inMemoryDeduplicationCache.has(key)) {
|
|
348
|
+
debugVerbose(
|
|
349
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
350
|
+
key,
|
|
351
|
+
"found key in inMemoryDeduplicationCache"
|
|
352
|
+
);
|
|
262
353
|
const res = await self.inMemoryDeduplicationCache.get(key).then((v) => structuredClone(v));
|
|
354
|
+
debugVerbose(
|
|
355
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
356
|
+
key,
|
|
357
|
+
"found key in inMemoryDeduplicationCache and served result from there",
|
|
358
|
+
JSON.stringify(res).substring(0, 200)
|
|
359
|
+
);
|
|
263
360
|
return res;
|
|
264
361
|
}
|
|
265
362
|
const promise = self.fn(...args);
|
|
266
363
|
self.inMemoryDeduplicationCache.set(key, promise);
|
|
364
|
+
debugVerbose(
|
|
365
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
366
|
+
key,
|
|
367
|
+
"did not found key in inMemoryDeduplicationCache. Setting it now and waiting for promise to resolve"
|
|
368
|
+
);
|
|
267
369
|
try {
|
|
370
|
+
const ts = performance.now();
|
|
268
371
|
const result = await promise;
|
|
372
|
+
debugVerbose(
|
|
373
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
374
|
+
key,
|
|
375
|
+
"promise resolved (in ",
|
|
376
|
+
performance.now() - ts,
|
|
377
|
+
"ms). Returning result",
|
|
378
|
+
JSON.stringify(result).substring(0, 200)
|
|
379
|
+
);
|
|
269
380
|
return structuredClone(result);
|
|
270
381
|
} finally {
|
|
271
382
|
setTimeout(() => {
|
|
383
|
+
debugVerbose(
|
|
384
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
385
|
+
key,
|
|
386
|
+
"deleting key from inMemoryDeduplicationCache after ",
|
|
387
|
+
self.cachingTimeMs,
|
|
388
|
+
"ms"
|
|
389
|
+
);
|
|
272
390
|
self.inMemoryDeduplicationCache.delete(key);
|
|
273
391
|
}, self.cachingTimeMs);
|
|
274
392
|
}
|
|
@@ -283,18 +401,41 @@ var DeduplicatedRequestHandler = class {
|
|
|
283
401
|
seedRequestReturn(key, value) {
|
|
284
402
|
const resultPromise = new Promise((res) => res(value));
|
|
285
403
|
this.inMemoryDeduplicationCache.set(key, resultPromise);
|
|
404
|
+
debugVerbose(
|
|
405
|
+
"DeduplicatedRequestHandler.seedRequestReturn() seeded result ",
|
|
406
|
+
key,
|
|
407
|
+
value.substring(0, 200)
|
|
408
|
+
);
|
|
286
409
|
setTimeout(() => {
|
|
287
410
|
this.inMemoryDeduplicationCache.delete(key);
|
|
288
411
|
}, this.cachingTimeMs);
|
|
289
412
|
}
|
|
290
413
|
};
|
|
291
414
|
|
|
415
|
+
// src/utils/json.ts
|
|
416
|
+
function bufferReviver(_, value) {
|
|
417
|
+
if (value && typeof value === "object" && typeof value.$binary === "string") {
|
|
418
|
+
return Buffer.from(value.$binary, "base64");
|
|
419
|
+
}
|
|
420
|
+
return value;
|
|
421
|
+
}
|
|
422
|
+
function bufferReplacer(_, value) {
|
|
423
|
+
if (Buffer.isBuffer(value)) {
|
|
424
|
+
return {
|
|
425
|
+
$binary: value.toString("base64")
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (value && typeof value === "object" && value?.type === "Buffer" && Array.isArray(value.data)) {
|
|
429
|
+
return {
|
|
430
|
+
$binary: Buffer.from(value.data).toString("base64")
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
435
|
+
|
|
292
436
|
// src/RedisStringsHandler.ts
|
|
293
437
|
var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_";
|
|
294
438
|
var REVALIDATED_TAGS_KEY = "__revalidated_tags__";
|
|
295
|
-
function isImplicitTag(tag) {
|
|
296
|
-
return tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID);
|
|
297
|
-
}
|
|
298
439
|
function getTimeoutRedisCommandOptions(timeoutMs) {
|
|
299
440
|
return (0, import_redis.commandOptions)({ signal: AbortSignal.timeout(timeoutMs) });
|
|
300
441
|
}
|
|
@@ -309,7 +450,7 @@ var RedisStringsHandler = class {
|
|
|
309
450
|
redisGetDeduplication = true,
|
|
310
451
|
inMemoryCachingTime = 1e4,
|
|
311
452
|
defaultStaleAge = 60 * 60 * 24 * 14,
|
|
312
|
-
estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "
|
|
453
|
+
estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "production" ? staleAge * 2 : staleAge * 1.2
|
|
313
454
|
}) {
|
|
314
455
|
this.keyPrefix = keyPrefix;
|
|
315
456
|
this.timeoutMs = timeoutMs;
|
|
@@ -327,9 +468,12 @@ var RedisStringsHandler = class {
|
|
|
327
468
|
});
|
|
328
469
|
this.client.connect().then(() => {
|
|
329
470
|
console.info("Redis client connected.");
|
|
330
|
-
}).catch((
|
|
331
|
-
|
|
332
|
-
|
|
471
|
+
}).catch(() => {
|
|
472
|
+
this.client.connect().catch((error) => {
|
|
473
|
+
console.error("Failed to connect Redis client:", error);
|
|
474
|
+
this.client.disconnect();
|
|
475
|
+
throw error;
|
|
476
|
+
});
|
|
333
477
|
});
|
|
334
478
|
} catch (error) {
|
|
335
479
|
console.error("Failed to initialize Redis client");
|
|
@@ -378,8 +522,7 @@ var RedisStringsHandler = class {
|
|
|
378
522
|
this.redisGet = redisGet;
|
|
379
523
|
this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
|
|
380
524
|
}
|
|
381
|
-
resetRequestCache(
|
|
382
|
-
console.warn("WARNING resetRequestCache() was called", args);
|
|
525
|
+
resetRequestCache() {
|
|
383
526
|
}
|
|
384
527
|
async assertClientIsReady() {
|
|
385
528
|
await Promise.all([
|
|
@@ -391,75 +534,153 @@ var RedisStringsHandler = class {
|
|
|
391
534
|
}
|
|
392
535
|
}
|
|
393
536
|
async get(key, ctx) {
|
|
537
|
+
if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") {
|
|
538
|
+
console.warn(
|
|
539
|
+
"RedisStringsHandler.get() called with",
|
|
540
|
+
key,
|
|
541
|
+
ctx,
|
|
542
|
+
" this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
|
|
543
|
+
ctx?.kind
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
debug("green", "RedisStringsHandler.get() called with", key, ctx);
|
|
394
547
|
await this.assertClientIsReady();
|
|
395
548
|
const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
|
|
396
|
-
const
|
|
549
|
+
const serializedCacheEntry = await clientGet(
|
|
397
550
|
getTimeoutRedisCommandOptions(this.timeoutMs),
|
|
398
551
|
this.keyPrefix + key
|
|
399
552
|
);
|
|
400
|
-
|
|
553
|
+
debug(
|
|
554
|
+
"green",
|
|
555
|
+
"RedisStringsHandler.get() finished with result (serializedCacheEntry)",
|
|
556
|
+
serializedCacheEntry?.substring(0, 200)
|
|
557
|
+
);
|
|
558
|
+
if (!serializedCacheEntry) {
|
|
401
559
|
return null;
|
|
402
560
|
}
|
|
403
|
-
const
|
|
404
|
-
|
|
561
|
+
const cacheEntry = JSON.parse(
|
|
562
|
+
serializedCacheEntry,
|
|
563
|
+
bufferReviver
|
|
564
|
+
);
|
|
565
|
+
debug(
|
|
566
|
+
"green",
|
|
567
|
+
"RedisStringsHandler.get() finished with result (cacheEntry)",
|
|
568
|
+
JSON.stringify(cacheEntry).substring(0, 200)
|
|
569
|
+
);
|
|
570
|
+
if (!cacheEntry) {
|
|
405
571
|
return null;
|
|
406
572
|
}
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
573
|
+
if (!cacheEntry?.tags) {
|
|
574
|
+
console.warn(
|
|
575
|
+
"RedisStringsHandler.get() called with",
|
|
576
|
+
key,
|
|
577
|
+
ctx,
|
|
578
|
+
"cacheEntry is mall formed (missing tags)"
|
|
579
|
+
);
|
|
411
580
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
581
|
+
if (!cacheEntry?.value) {
|
|
582
|
+
console.warn(
|
|
583
|
+
"RedisStringsHandler.get() called with",
|
|
584
|
+
key,
|
|
585
|
+
ctx,
|
|
586
|
+
"cacheEntry is mall formed (missing value)"
|
|
587
|
+
);
|
|
418
588
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
589
|
+
if (!cacheEntry?.lastModified) {
|
|
590
|
+
console.warn(
|
|
591
|
+
"RedisStringsHandler.get() called with",
|
|
592
|
+
key,
|
|
593
|
+
ctx,
|
|
594
|
+
"cacheEntry is mall formed (missing lastModified)"
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
if (ctx.kind === "FETCH") {
|
|
598
|
+
const combinedTags = /* @__PURE__ */ new Set([
|
|
599
|
+
...ctx?.softTags || [],
|
|
600
|
+
...ctx?.tags || []
|
|
601
|
+
]);
|
|
602
|
+
if (combinedTags.size === 0) {
|
|
603
|
+
return cacheEntry;
|
|
604
|
+
}
|
|
605
|
+
for (const tag of combinedTags) {
|
|
606
|
+
const revalidationTime = this.revalidatedTagsMap.get(tag);
|
|
607
|
+
if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
|
|
608
|
+
const redisKey = this.keyPrefix + key;
|
|
609
|
+
this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
|
|
610
|
+
console.error(
|
|
611
|
+
"Error occurred while unlinking stale data. Retrying now. Error was:",
|
|
612
|
+
err
|
|
613
|
+
);
|
|
614
|
+
this.client.unlink(
|
|
615
|
+
getTimeoutRedisCommandOptions(this.timeoutMs),
|
|
616
|
+
redisKey
|
|
617
|
+
);
|
|
618
|
+
}).finally(async () => {
|
|
619
|
+
await this.sharedTagsMap.delete(key);
|
|
620
|
+
await this.revalidatedTagsMap.delete(tag);
|
|
621
|
+
});
|
|
622
|
+
debug(
|
|
623
|
+
"green",
|
|
624
|
+
'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
|
|
625
|
+
tag,
|
|
626
|
+
redisKey,
|
|
627
|
+
revalidationTime,
|
|
628
|
+
cacheEntry
|
|
431
629
|
);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
await this.revalidatedTagsMap.delete(tag);
|
|
435
|
-
});
|
|
436
|
-
return null;
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
437
632
|
}
|
|
438
633
|
}
|
|
439
|
-
return
|
|
634
|
+
return cacheEntry;
|
|
440
635
|
}
|
|
441
636
|
async set(key, data, ctx) {
|
|
442
|
-
if (data.kind
|
|
443
|
-
console.
|
|
444
|
-
|
|
445
|
-
|
|
637
|
+
if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
|
|
638
|
+
console.warn(
|
|
639
|
+
"RedisStringsHandler.set() called with",
|
|
640
|
+
key,
|
|
641
|
+
ctx,
|
|
642
|
+
data,
|
|
643
|
+
" this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
|
|
644
|
+
data?.kind
|
|
645
|
+
);
|
|
446
646
|
}
|
|
447
647
|
await this.assertClientIsReady();
|
|
448
|
-
data.
|
|
449
|
-
|
|
648
|
+
if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
|
|
649
|
+
const tags = data.headers["x-next-cache-tags"]?.split(",");
|
|
650
|
+
ctx.tags = [...ctx.tags || [], ...tags || []];
|
|
651
|
+
}
|
|
652
|
+
const cacheEntry = {
|
|
653
|
+
lastModified: Date.now(),
|
|
654
|
+
tags: ctx?.tags || [],
|
|
655
|
+
value: data
|
|
656
|
+
};
|
|
657
|
+
const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
|
|
450
658
|
if (this.redisGetDeduplication) {
|
|
451
|
-
this.redisDeduplicationHandler.seedRequestReturn(
|
|
659
|
+
this.redisDeduplicationHandler.seedRequestReturn(
|
|
660
|
+
key,
|
|
661
|
+
serializedCacheEntry
|
|
662
|
+
);
|
|
452
663
|
}
|
|
453
664
|
const expireAt = ctx.revalidate && Number.isSafeInteger(ctx.revalidate) && ctx.revalidate > 0 ? this.estimateExpireAge(ctx.revalidate) : this.estimateExpireAge(this.defaultStaleAge);
|
|
454
665
|
const options = getTimeoutRedisCommandOptions(this.timeoutMs);
|
|
455
666
|
const setOperation = this.client.set(
|
|
456
667
|
options,
|
|
457
668
|
this.keyPrefix + key,
|
|
458
|
-
|
|
669
|
+
serializedCacheEntry,
|
|
459
670
|
{
|
|
460
671
|
EX: expireAt
|
|
461
672
|
}
|
|
462
673
|
);
|
|
674
|
+
debug(
|
|
675
|
+
"blue",
|
|
676
|
+
"RedisStringsHandler.set() will set the following serializedCacheEntry",
|
|
677
|
+
this.keyPrefix,
|
|
678
|
+
key,
|
|
679
|
+
data,
|
|
680
|
+
ctx,
|
|
681
|
+
serializedCacheEntry?.substring(0, 200),
|
|
682
|
+
expireAt
|
|
683
|
+
);
|
|
463
684
|
let setTagsOperation;
|
|
464
685
|
if (ctx.tags && ctx.tags.length > 0) {
|
|
465
686
|
const currentTags = this.sharedTagsMap.get(key);
|
|
@@ -471,27 +692,54 @@ var RedisStringsHandler = class {
|
|
|
471
692
|
);
|
|
472
693
|
}
|
|
473
694
|
}
|
|
695
|
+
debug(
|
|
696
|
+
"blue",
|
|
697
|
+
"RedisStringsHandler.set() will set the following sharedTagsMap",
|
|
698
|
+
key,
|
|
699
|
+
ctx.tags
|
|
700
|
+
);
|
|
474
701
|
await Promise.all([setOperation, setTagsOperation]);
|
|
475
702
|
}
|
|
476
|
-
|
|
703
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
704
|
+
async revalidateTag(tagOrTags, ...rest) {
|
|
705
|
+
debug(
|
|
706
|
+
"red",
|
|
707
|
+
"RedisStringsHandler.revalidateTag() called with",
|
|
708
|
+
tagOrTags,
|
|
709
|
+
rest
|
|
710
|
+
);
|
|
477
711
|
const tags = new Set([tagOrTags || []].flat());
|
|
478
712
|
await this.assertClientIsReady();
|
|
713
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
479
714
|
for (const tag of tags) {
|
|
480
|
-
if (
|
|
715
|
+
if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
|
|
481
716
|
const now = Date.now();
|
|
717
|
+
debug(
|
|
718
|
+
"red",
|
|
719
|
+
"RedisStringsHandler.revalidateTag() set revalidation time for tag",
|
|
720
|
+
tag,
|
|
721
|
+
"to",
|
|
722
|
+
now
|
|
723
|
+
);
|
|
482
724
|
await this.revalidatedTagsMap.set(tag, now);
|
|
483
725
|
}
|
|
484
726
|
}
|
|
485
|
-
const keysToDelete = [];
|
|
486
727
|
for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
|
|
487
728
|
if (sharedTags.some((tag) => tags.has(tag))) {
|
|
488
|
-
keysToDelete.
|
|
729
|
+
keysToDelete.add(key);
|
|
489
730
|
}
|
|
490
731
|
}
|
|
491
|
-
|
|
732
|
+
debug(
|
|
733
|
+
"red",
|
|
734
|
+
"RedisStringsHandler.revalidateTag() found",
|
|
735
|
+
keysToDelete,
|
|
736
|
+
"keys to delete"
|
|
737
|
+
);
|
|
738
|
+
if (keysToDelete.size === 0) {
|
|
492
739
|
return;
|
|
493
740
|
}
|
|
494
|
-
const
|
|
741
|
+
const redisKeys = Array.from(keysToDelete);
|
|
742
|
+
const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
|
|
495
743
|
const options = getTimeoutRedisCommandOptions(this.timeoutMs);
|
|
496
744
|
const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
|
|
497
745
|
if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
|
|
@@ -499,8 +747,12 @@ var RedisStringsHandler = class {
|
|
|
499
747
|
this.inMemoryDeduplicationCache.delete(key);
|
|
500
748
|
}
|
|
501
749
|
}
|
|
502
|
-
const deleteTagsOperation = this.sharedTagsMap.delete(
|
|
750
|
+
const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
|
|
503
751
|
await Promise.all([deleteKeysOperation, deleteTagsOperation]);
|
|
752
|
+
debug(
|
|
753
|
+
"red",
|
|
754
|
+
"RedisStringsHandler.revalidateTag() finished delete operations"
|
|
755
|
+
);
|
|
504
756
|
}
|
|
505
757
|
};
|
|
506
758
|
|
|
@@ -514,12 +766,15 @@ var CachedHandler = class {
|
|
|
514
766
|
}
|
|
515
767
|
}
|
|
516
768
|
get(...args) {
|
|
769
|
+
debugVerbose("CachedHandler.get called with", args);
|
|
517
770
|
return cachedHandler.get(...args);
|
|
518
771
|
}
|
|
519
772
|
set(...args) {
|
|
773
|
+
debugVerbose("CachedHandler.set called with", args);
|
|
520
774
|
return cachedHandler.set(...args);
|
|
521
775
|
}
|
|
522
776
|
revalidateTag(...args) {
|
|
777
|
+
debugVerbose("CachedHandler.revalidateTag called with", args);
|
|
523
778
|
return cachedHandler.revalidateTag(...args);
|
|
524
779
|
}
|
|
525
780
|
resetRequestCache(...args) {
|
|
@@ -529,4 +784,8 @@ var CachedHandler = class {
|
|
|
529
784
|
|
|
530
785
|
// src/index.ts
|
|
531
786
|
var index_default = CachedHandler;
|
|
787
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
788
|
+
0 && (module.exports = {
|
|
789
|
+
RedisStringsHandler
|
|
790
|
+
});
|
|
532
791
|
//# sourceMappingURL=index.js.map
|