@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.mjs
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
// src/RedisStringsHandler.ts
|
|
2
2
|
import { commandOptions, createClient } from "redis";
|
|
3
3
|
|
|
4
|
+
// src/utils/debug.ts
|
|
5
|
+
function debug(color = "none", ...args) {
|
|
6
|
+
const colorCode = {
|
|
7
|
+
red: "\x1B[31m",
|
|
8
|
+
blue: "\x1B[34m",
|
|
9
|
+
green: "\x1B[32m",
|
|
10
|
+
yellow: "\x1B[33m",
|
|
11
|
+
cyan: "\x1B[36m",
|
|
12
|
+
white: "\x1B[37m",
|
|
13
|
+
none: ""
|
|
14
|
+
};
|
|
15
|
+
if (process.env.DEBUG_CACHE_HANDLER) {
|
|
16
|
+
console.log(colorCode[color], "DEBUG CACHE HANDLER: ", ...args);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function debugVerbose(color, ...args) {
|
|
20
|
+
if (process.env.DEBUG_CACHE_HANDLER_VERBOSE_VERBOSE) {
|
|
21
|
+
console.log("\x1B[35m", "DEBUG SYNCED MAP: ", ...args);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
4
25
|
// src/SyncedMap.ts
|
|
5
26
|
var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
|
|
6
27
|
var SyncedMap = class {
|
|
@@ -116,24 +137,56 @@ var SyncedMap = class {
|
|
|
116
137
|
}
|
|
117
138
|
}
|
|
118
139
|
};
|
|
119
|
-
const keyEventHandler = async (
|
|
120
|
-
|
|
140
|
+
const keyEventHandler = async (key, message) => {
|
|
141
|
+
debug(
|
|
142
|
+
"yellow",
|
|
143
|
+
"SyncedMap.keyEventHandler() called with message",
|
|
144
|
+
this.redisKey,
|
|
145
|
+
message,
|
|
146
|
+
key
|
|
147
|
+
);
|
|
121
148
|
if (key.startsWith(this.keyPrefix)) {
|
|
122
149
|
const keyInMap = key.substring(this.keyPrefix.length);
|
|
123
150
|
if (this.filterKeys(keyInMap)) {
|
|
151
|
+
debugVerbose(
|
|
152
|
+
"SyncedMap.keyEventHandler() key matches filter and will be deleted",
|
|
153
|
+
this.redisKey,
|
|
154
|
+
message,
|
|
155
|
+
key
|
|
156
|
+
);
|
|
124
157
|
await this.delete(keyInMap, true);
|
|
125
158
|
}
|
|
159
|
+
} else {
|
|
160
|
+
debugVerbose(
|
|
161
|
+
"SyncedMap.keyEventHandler() key does not have prefix",
|
|
162
|
+
this.redisKey,
|
|
163
|
+
message,
|
|
164
|
+
key
|
|
165
|
+
);
|
|
126
166
|
}
|
|
127
167
|
};
|
|
128
168
|
try {
|
|
129
|
-
await this.subscriberClient.connect()
|
|
169
|
+
await this.subscriberClient.connect().catch(async () => {
|
|
170
|
+
await this.subscriberClient.connect();
|
|
171
|
+
});
|
|
172
|
+
const keyspaceEventConfig = (await this.subscriberClient.configGet("notify-keyspace-events"))?.["notify-keyspace-events"];
|
|
173
|
+
if (!keyspaceEventConfig.includes("E")) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
"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`"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (!keyspaceEventConfig.includes("A") && !(keyspaceEventConfig.includes("x") && keyspaceEventConfig.includes("e"))) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"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`"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
130
183
|
await Promise.all([
|
|
131
184
|
// We use a custom channel for insert/delete For the following reason:
|
|
132
185
|
// With custom channel we can delete multiple entries in one message. If we would listen to unlink / del we
|
|
133
186
|
// could get thousands of messages for one revalidateTag (For example revalidateTag("algolia") would send an enormous amount of network packages)
|
|
134
187
|
// Also we can send the value in the message for insert
|
|
135
188
|
this.subscriberClient.subscribe(this.syncChannel, syncHandler),
|
|
136
|
-
// Subscribe to Redis
|
|
189
|
+
// Subscribe to Redis keyevent notifications for evicted and expired keys
|
|
137
190
|
this.subscriberClient.subscribe(
|
|
138
191
|
`__keyevent@${this.database}__:evicted`,
|
|
139
192
|
keyEventHandler
|
|
@@ -165,9 +218,19 @@ var SyncedMap = class {
|
|
|
165
218
|
await this.setupLock;
|
|
166
219
|
}
|
|
167
220
|
get(key) {
|
|
221
|
+
debugVerbose(
|
|
222
|
+
"SyncedMap.get() called with key",
|
|
223
|
+
key,
|
|
224
|
+
JSON.stringify(this.map.get(key))?.substring(0, 100)
|
|
225
|
+
);
|
|
168
226
|
return this.map.get(key);
|
|
169
227
|
}
|
|
170
228
|
async set(key, value) {
|
|
229
|
+
debugVerbose(
|
|
230
|
+
"SyncedMap.set() called with key",
|
|
231
|
+
key,
|
|
232
|
+
JSON.stringify(value)?.substring(0, 100)
|
|
233
|
+
);
|
|
171
234
|
this.map.set(key, value);
|
|
172
235
|
const operations = [];
|
|
173
236
|
if (this.customizedSync?.withoutSetSync) {
|
|
@@ -194,7 +257,15 @@ var SyncedMap = class {
|
|
|
194
257
|
);
|
|
195
258
|
await Promise.all(operations);
|
|
196
259
|
}
|
|
260
|
+
// /api/revalidated-fetch
|
|
261
|
+
// true
|
|
197
262
|
async delete(keys, withoutSyncMessage = false) {
|
|
263
|
+
debugVerbose(
|
|
264
|
+
"SyncedMap.delete() called with keys",
|
|
265
|
+
this.redisKey,
|
|
266
|
+
keys,
|
|
267
|
+
withoutSyncMessage
|
|
268
|
+
);
|
|
198
269
|
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
199
270
|
const operations = [];
|
|
200
271
|
for (const key of keysArray) {
|
|
@@ -216,6 +287,12 @@ var SyncedMap = class {
|
|
|
216
287
|
);
|
|
217
288
|
}
|
|
218
289
|
await Promise.all(operations);
|
|
290
|
+
debugVerbose(
|
|
291
|
+
"SyncedMap.delete() finished operations",
|
|
292
|
+
this.redisKey,
|
|
293
|
+
keys,
|
|
294
|
+
operations.length
|
|
295
|
+
);
|
|
219
296
|
}
|
|
220
297
|
has(key) {
|
|
221
298
|
return this.map.has(key);
|
|
@@ -230,19 +307,59 @@ var DeduplicatedRequestHandler = class {
|
|
|
230
307
|
constructor(fn, cachingTimeMs, inMemoryDeduplicationCache) {
|
|
231
308
|
// Method to handle deduplicated requests
|
|
232
309
|
this.deduplicatedFunction = (key) => {
|
|
310
|
+
debugVerbose(
|
|
311
|
+
"DeduplicatedRequestHandler.deduplicatedFunction() called with",
|
|
312
|
+
key
|
|
313
|
+
);
|
|
233
314
|
const self = this;
|
|
234
315
|
const dedupedFn = async (...args) => {
|
|
316
|
+
debugVerbose(
|
|
317
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn called with",
|
|
318
|
+
key
|
|
319
|
+
);
|
|
235
320
|
if (self.inMemoryDeduplicationCache && self.inMemoryDeduplicationCache.has(key)) {
|
|
321
|
+
debugVerbose(
|
|
322
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
323
|
+
key,
|
|
324
|
+
"found key in inMemoryDeduplicationCache"
|
|
325
|
+
);
|
|
236
326
|
const res = await self.inMemoryDeduplicationCache.get(key).then((v) => structuredClone(v));
|
|
327
|
+
debugVerbose(
|
|
328
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
329
|
+
key,
|
|
330
|
+
"found key in inMemoryDeduplicationCache and served result from there",
|
|
331
|
+
JSON.stringify(res).substring(0, 200)
|
|
332
|
+
);
|
|
237
333
|
return res;
|
|
238
334
|
}
|
|
239
335
|
const promise = self.fn(...args);
|
|
240
336
|
self.inMemoryDeduplicationCache.set(key, promise);
|
|
337
|
+
debugVerbose(
|
|
338
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
339
|
+
key,
|
|
340
|
+
"did not found key in inMemoryDeduplicationCache. Setting it now and waiting for promise to resolve"
|
|
341
|
+
);
|
|
241
342
|
try {
|
|
343
|
+
const ts = performance.now();
|
|
242
344
|
const result = await promise;
|
|
345
|
+
debugVerbose(
|
|
346
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
347
|
+
key,
|
|
348
|
+
"promise resolved (in ",
|
|
349
|
+
performance.now() - ts,
|
|
350
|
+
"ms). Returning result",
|
|
351
|
+
JSON.stringify(result).substring(0, 200)
|
|
352
|
+
);
|
|
243
353
|
return structuredClone(result);
|
|
244
354
|
} finally {
|
|
245
355
|
setTimeout(() => {
|
|
356
|
+
debugVerbose(
|
|
357
|
+
"DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
|
|
358
|
+
key,
|
|
359
|
+
"deleting key from inMemoryDeduplicationCache after ",
|
|
360
|
+
self.cachingTimeMs,
|
|
361
|
+
"ms"
|
|
362
|
+
);
|
|
246
363
|
self.inMemoryDeduplicationCache.delete(key);
|
|
247
364
|
}, self.cachingTimeMs);
|
|
248
365
|
}
|
|
@@ -257,18 +374,41 @@ var DeduplicatedRequestHandler = class {
|
|
|
257
374
|
seedRequestReturn(key, value) {
|
|
258
375
|
const resultPromise = new Promise((res) => res(value));
|
|
259
376
|
this.inMemoryDeduplicationCache.set(key, resultPromise);
|
|
377
|
+
debugVerbose(
|
|
378
|
+
"DeduplicatedRequestHandler.seedRequestReturn() seeded result ",
|
|
379
|
+
key,
|
|
380
|
+
value.substring(0, 200)
|
|
381
|
+
);
|
|
260
382
|
setTimeout(() => {
|
|
261
383
|
this.inMemoryDeduplicationCache.delete(key);
|
|
262
384
|
}, this.cachingTimeMs);
|
|
263
385
|
}
|
|
264
386
|
};
|
|
265
387
|
|
|
388
|
+
// src/utils/json.ts
|
|
389
|
+
function bufferReviver(_, value) {
|
|
390
|
+
if (value && typeof value === "object" && typeof value.$binary === "string") {
|
|
391
|
+
return Buffer.from(value.$binary, "base64");
|
|
392
|
+
}
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
function bufferReplacer(_, value) {
|
|
396
|
+
if (Buffer.isBuffer(value)) {
|
|
397
|
+
return {
|
|
398
|
+
$binary: value.toString("base64")
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (value && typeof value === "object" && value?.type === "Buffer" && Array.isArray(value.data)) {
|
|
402
|
+
return {
|
|
403
|
+
$binary: Buffer.from(value.data).toString("base64")
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return value;
|
|
407
|
+
}
|
|
408
|
+
|
|
266
409
|
// src/RedisStringsHandler.ts
|
|
267
410
|
var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_";
|
|
268
411
|
var REVALIDATED_TAGS_KEY = "__revalidated_tags__";
|
|
269
|
-
function isImplicitTag(tag) {
|
|
270
|
-
return tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID);
|
|
271
|
-
}
|
|
272
412
|
function getTimeoutRedisCommandOptions(timeoutMs) {
|
|
273
413
|
return commandOptions({ signal: AbortSignal.timeout(timeoutMs) });
|
|
274
414
|
}
|
|
@@ -283,7 +423,7 @@ var RedisStringsHandler = class {
|
|
|
283
423
|
redisGetDeduplication = true,
|
|
284
424
|
inMemoryCachingTime = 1e4,
|
|
285
425
|
defaultStaleAge = 60 * 60 * 24 * 14,
|
|
286
|
-
estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "
|
|
426
|
+
estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "production" ? staleAge * 2 : staleAge * 1.2
|
|
287
427
|
}) {
|
|
288
428
|
this.keyPrefix = keyPrefix;
|
|
289
429
|
this.timeoutMs = timeoutMs;
|
|
@@ -301,9 +441,12 @@ var RedisStringsHandler = class {
|
|
|
301
441
|
});
|
|
302
442
|
this.client.connect().then(() => {
|
|
303
443
|
console.info("Redis client connected.");
|
|
304
|
-
}).catch((
|
|
305
|
-
|
|
306
|
-
|
|
444
|
+
}).catch(() => {
|
|
445
|
+
this.client.connect().catch((error) => {
|
|
446
|
+
console.error("Failed to connect Redis client:", error);
|
|
447
|
+
this.client.disconnect();
|
|
448
|
+
throw error;
|
|
449
|
+
});
|
|
307
450
|
});
|
|
308
451
|
} catch (error) {
|
|
309
452
|
console.error("Failed to initialize Redis client");
|
|
@@ -352,8 +495,7 @@ var RedisStringsHandler = class {
|
|
|
352
495
|
this.redisGet = redisGet;
|
|
353
496
|
this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
|
|
354
497
|
}
|
|
355
|
-
resetRequestCache(
|
|
356
|
-
console.warn("WARNING resetRequestCache() was called", args);
|
|
498
|
+
resetRequestCache() {
|
|
357
499
|
}
|
|
358
500
|
async assertClientIsReady() {
|
|
359
501
|
await Promise.all([
|
|
@@ -365,75 +507,153 @@ var RedisStringsHandler = class {
|
|
|
365
507
|
}
|
|
366
508
|
}
|
|
367
509
|
async get(key, ctx) {
|
|
510
|
+
if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") {
|
|
511
|
+
console.warn(
|
|
512
|
+
"RedisStringsHandler.get() called with",
|
|
513
|
+
key,
|
|
514
|
+
ctx,
|
|
515
|
+
" this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
|
|
516
|
+
ctx?.kind
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
debug("green", "RedisStringsHandler.get() called with", key, ctx);
|
|
368
520
|
await this.assertClientIsReady();
|
|
369
521
|
const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
|
|
370
|
-
const
|
|
522
|
+
const serializedCacheEntry = await clientGet(
|
|
371
523
|
getTimeoutRedisCommandOptions(this.timeoutMs),
|
|
372
524
|
this.keyPrefix + key
|
|
373
525
|
);
|
|
374
|
-
|
|
526
|
+
debug(
|
|
527
|
+
"green",
|
|
528
|
+
"RedisStringsHandler.get() finished with result (serializedCacheEntry)",
|
|
529
|
+
serializedCacheEntry?.substring(0, 200)
|
|
530
|
+
);
|
|
531
|
+
if (!serializedCacheEntry) {
|
|
375
532
|
return null;
|
|
376
533
|
}
|
|
377
|
-
const
|
|
378
|
-
|
|
534
|
+
const cacheEntry = JSON.parse(
|
|
535
|
+
serializedCacheEntry,
|
|
536
|
+
bufferReviver
|
|
537
|
+
);
|
|
538
|
+
debug(
|
|
539
|
+
"green",
|
|
540
|
+
"RedisStringsHandler.get() finished with result (cacheEntry)",
|
|
541
|
+
JSON.stringify(cacheEntry).substring(0, 200)
|
|
542
|
+
);
|
|
543
|
+
if (!cacheEntry) {
|
|
379
544
|
return null;
|
|
380
545
|
}
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
546
|
+
if (!cacheEntry?.tags) {
|
|
547
|
+
console.warn(
|
|
548
|
+
"RedisStringsHandler.get() called with",
|
|
549
|
+
key,
|
|
550
|
+
ctx,
|
|
551
|
+
"cacheEntry is mall formed (missing tags)"
|
|
552
|
+
);
|
|
385
553
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
554
|
+
if (!cacheEntry?.value) {
|
|
555
|
+
console.warn(
|
|
556
|
+
"RedisStringsHandler.get() called with",
|
|
557
|
+
key,
|
|
558
|
+
ctx,
|
|
559
|
+
"cacheEntry is mall formed (missing value)"
|
|
560
|
+
);
|
|
392
561
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
562
|
+
if (!cacheEntry?.lastModified) {
|
|
563
|
+
console.warn(
|
|
564
|
+
"RedisStringsHandler.get() called with",
|
|
565
|
+
key,
|
|
566
|
+
ctx,
|
|
567
|
+
"cacheEntry is mall formed (missing lastModified)"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (ctx.kind === "FETCH") {
|
|
571
|
+
const combinedTags = /* @__PURE__ */ new Set([
|
|
572
|
+
...ctx?.softTags || [],
|
|
573
|
+
...ctx?.tags || []
|
|
574
|
+
]);
|
|
575
|
+
if (combinedTags.size === 0) {
|
|
576
|
+
return cacheEntry;
|
|
577
|
+
}
|
|
578
|
+
for (const tag of combinedTags) {
|
|
579
|
+
const revalidationTime = this.revalidatedTagsMap.get(tag);
|
|
580
|
+
if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
|
|
581
|
+
const redisKey = this.keyPrefix + key;
|
|
582
|
+
this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
|
|
583
|
+
console.error(
|
|
584
|
+
"Error occurred while unlinking stale data. Retrying now. Error was:",
|
|
585
|
+
err
|
|
586
|
+
);
|
|
587
|
+
this.client.unlink(
|
|
588
|
+
getTimeoutRedisCommandOptions(this.timeoutMs),
|
|
589
|
+
redisKey
|
|
590
|
+
);
|
|
591
|
+
}).finally(async () => {
|
|
592
|
+
await this.sharedTagsMap.delete(key);
|
|
593
|
+
await this.revalidatedTagsMap.delete(tag);
|
|
594
|
+
});
|
|
595
|
+
debug(
|
|
596
|
+
"green",
|
|
597
|
+
'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
|
|
598
|
+
tag,
|
|
599
|
+
redisKey,
|
|
600
|
+
revalidationTime,
|
|
601
|
+
cacheEntry
|
|
405
602
|
);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
await this.revalidatedTagsMap.delete(tag);
|
|
409
|
-
});
|
|
410
|
-
return null;
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
411
605
|
}
|
|
412
606
|
}
|
|
413
|
-
return
|
|
607
|
+
return cacheEntry;
|
|
414
608
|
}
|
|
415
609
|
async set(key, data, ctx) {
|
|
416
|
-
if (data.kind
|
|
417
|
-
console.
|
|
418
|
-
|
|
419
|
-
|
|
610
|
+
if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
|
|
611
|
+
console.warn(
|
|
612
|
+
"RedisStringsHandler.set() called with",
|
|
613
|
+
key,
|
|
614
|
+
ctx,
|
|
615
|
+
data,
|
|
616
|
+
" this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
|
|
617
|
+
data?.kind
|
|
618
|
+
);
|
|
420
619
|
}
|
|
421
620
|
await this.assertClientIsReady();
|
|
422
|
-
data.
|
|
423
|
-
|
|
621
|
+
if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
|
|
622
|
+
const tags = data.headers["x-next-cache-tags"]?.split(",");
|
|
623
|
+
ctx.tags = [...ctx.tags || [], ...tags || []];
|
|
624
|
+
}
|
|
625
|
+
const cacheEntry = {
|
|
626
|
+
lastModified: Date.now(),
|
|
627
|
+
tags: ctx?.tags || [],
|
|
628
|
+
value: data
|
|
629
|
+
};
|
|
630
|
+
const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
|
|
424
631
|
if (this.redisGetDeduplication) {
|
|
425
|
-
this.redisDeduplicationHandler.seedRequestReturn(
|
|
632
|
+
this.redisDeduplicationHandler.seedRequestReturn(
|
|
633
|
+
key,
|
|
634
|
+
serializedCacheEntry
|
|
635
|
+
);
|
|
426
636
|
}
|
|
427
637
|
const expireAt = ctx.revalidate && Number.isSafeInteger(ctx.revalidate) && ctx.revalidate > 0 ? this.estimateExpireAge(ctx.revalidate) : this.estimateExpireAge(this.defaultStaleAge);
|
|
428
638
|
const options = getTimeoutRedisCommandOptions(this.timeoutMs);
|
|
429
639
|
const setOperation = this.client.set(
|
|
430
640
|
options,
|
|
431
641
|
this.keyPrefix + key,
|
|
432
|
-
|
|
642
|
+
serializedCacheEntry,
|
|
433
643
|
{
|
|
434
644
|
EX: expireAt
|
|
435
645
|
}
|
|
436
646
|
);
|
|
647
|
+
debug(
|
|
648
|
+
"blue",
|
|
649
|
+
"RedisStringsHandler.set() will set the following serializedCacheEntry",
|
|
650
|
+
this.keyPrefix,
|
|
651
|
+
key,
|
|
652
|
+
data,
|
|
653
|
+
ctx,
|
|
654
|
+
serializedCacheEntry?.substring(0, 200),
|
|
655
|
+
expireAt
|
|
656
|
+
);
|
|
437
657
|
let setTagsOperation;
|
|
438
658
|
if (ctx.tags && ctx.tags.length > 0) {
|
|
439
659
|
const currentTags = this.sharedTagsMap.get(key);
|
|
@@ -445,27 +665,54 @@ var RedisStringsHandler = class {
|
|
|
445
665
|
);
|
|
446
666
|
}
|
|
447
667
|
}
|
|
668
|
+
debug(
|
|
669
|
+
"blue",
|
|
670
|
+
"RedisStringsHandler.set() will set the following sharedTagsMap",
|
|
671
|
+
key,
|
|
672
|
+
ctx.tags
|
|
673
|
+
);
|
|
448
674
|
await Promise.all([setOperation, setTagsOperation]);
|
|
449
675
|
}
|
|
450
|
-
|
|
676
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
677
|
+
async revalidateTag(tagOrTags, ...rest) {
|
|
678
|
+
debug(
|
|
679
|
+
"red",
|
|
680
|
+
"RedisStringsHandler.revalidateTag() called with",
|
|
681
|
+
tagOrTags,
|
|
682
|
+
rest
|
|
683
|
+
);
|
|
451
684
|
const tags = new Set([tagOrTags || []].flat());
|
|
452
685
|
await this.assertClientIsReady();
|
|
686
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
453
687
|
for (const tag of tags) {
|
|
454
|
-
if (
|
|
688
|
+
if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
|
|
455
689
|
const now = Date.now();
|
|
690
|
+
debug(
|
|
691
|
+
"red",
|
|
692
|
+
"RedisStringsHandler.revalidateTag() set revalidation time for tag",
|
|
693
|
+
tag,
|
|
694
|
+
"to",
|
|
695
|
+
now
|
|
696
|
+
);
|
|
456
697
|
await this.revalidatedTagsMap.set(tag, now);
|
|
457
698
|
}
|
|
458
699
|
}
|
|
459
|
-
const keysToDelete = [];
|
|
460
700
|
for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
|
|
461
701
|
if (sharedTags.some((tag) => tags.has(tag))) {
|
|
462
|
-
keysToDelete.
|
|
702
|
+
keysToDelete.add(key);
|
|
463
703
|
}
|
|
464
704
|
}
|
|
465
|
-
|
|
705
|
+
debug(
|
|
706
|
+
"red",
|
|
707
|
+
"RedisStringsHandler.revalidateTag() found",
|
|
708
|
+
keysToDelete,
|
|
709
|
+
"keys to delete"
|
|
710
|
+
);
|
|
711
|
+
if (keysToDelete.size === 0) {
|
|
466
712
|
return;
|
|
467
713
|
}
|
|
468
|
-
const
|
|
714
|
+
const redisKeys = Array.from(keysToDelete);
|
|
715
|
+
const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
|
|
469
716
|
const options = getTimeoutRedisCommandOptions(this.timeoutMs);
|
|
470
717
|
const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
|
|
471
718
|
if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
|
|
@@ -473,8 +720,12 @@ var RedisStringsHandler = class {
|
|
|
473
720
|
this.inMemoryDeduplicationCache.delete(key);
|
|
474
721
|
}
|
|
475
722
|
}
|
|
476
|
-
const deleteTagsOperation = this.sharedTagsMap.delete(
|
|
723
|
+
const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
|
|
477
724
|
await Promise.all([deleteKeysOperation, deleteTagsOperation]);
|
|
725
|
+
debug(
|
|
726
|
+
"red",
|
|
727
|
+
"RedisStringsHandler.revalidateTag() finished delete operations"
|
|
728
|
+
);
|
|
478
729
|
}
|
|
479
730
|
};
|
|
480
731
|
|
|
@@ -488,12 +739,15 @@ var CachedHandler = class {
|
|
|
488
739
|
}
|
|
489
740
|
}
|
|
490
741
|
get(...args) {
|
|
742
|
+
debugVerbose("CachedHandler.get called with", args);
|
|
491
743
|
return cachedHandler.get(...args);
|
|
492
744
|
}
|
|
493
745
|
set(...args) {
|
|
746
|
+
debugVerbose("CachedHandler.set called with", args);
|
|
494
747
|
return cachedHandler.set(...args);
|
|
495
748
|
}
|
|
496
749
|
revalidateTag(...args) {
|
|
750
|
+
debugVerbose("CachedHandler.revalidateTag called with", args);
|
|
497
751
|
return cachedHandler.revalidateTag(...args);
|
|
498
752
|
}
|
|
499
753
|
resetRequestCache(...args) {
|
|
@@ -504,6 +758,7 @@ var CachedHandler = class {
|
|
|
504
758
|
// src/index.ts
|
|
505
759
|
var index_default = CachedHandler;
|
|
506
760
|
export {
|
|
761
|
+
RedisStringsHandler,
|
|
507
762
|
index_default as default
|
|
508
763
|
};
|
|
509
764
|
//# sourceMappingURL=index.mjs.map
|