@trieb.work/nextjs-turbo-redis-cache 1.2.1 → 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.
Files changed (60) hide show
  1. package/.github/workflows/ci.yml +30 -6
  2. package/.github/workflows/release.yml +6 -3
  3. package/.next/trace +11 -0
  4. package/.vscode/settings.json +10 -0
  5. package/CHANGELOG.md +54 -0
  6. package/README.md +149 -34
  7. package/dist/index.d.mts +92 -20
  8. package/dist/index.d.ts +92 -20
  9. package/dist/index.js +319 -60
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +315 -60
  12. package/dist/index.mjs.map +1 -1
  13. package/package.json +12 -7
  14. package/scripts/vitest-run-staged.cjs +1 -1
  15. package/src/CachedHandler.ts +23 -9
  16. package/src/DeduplicatedRequestHandler.ts +50 -1
  17. package/src/RedisStringsHandler.ts +330 -89
  18. package/src/SyncedMap.ts +74 -4
  19. package/src/index.ts +4 -2
  20. package/src/utils/debug.ts +30 -0
  21. package/src/utils/json.ts +26 -0
  22. package/test/integration/next-app/README.md +36 -0
  23. package/test/integration/next-app/eslint.config.mjs +16 -0
  24. package/test/integration/next-app/next.config.js +6 -0
  25. package/test/integration/next-app/package-lock.json +5833 -0
  26. package/test/integration/next-app/package.json +29 -0
  27. package/test/integration/next-app/pnpm-lock.yaml +3679 -0
  28. package/test/integration/next-app/postcss.config.mjs +5 -0
  29. package/test/integration/next-app/public/file.svg +1 -0
  30. package/test/integration/next-app/public/globe.svg +1 -0
  31. package/test/integration/next-app/public/next.svg +1 -0
  32. package/test/integration/next-app/public/vercel.svg +1 -0
  33. package/test/integration/next-app/public/window.svg +1 -0
  34. package/test/integration/next-app/src/app/api/cached-static-fetch/route.ts +18 -0
  35. package/test/integration/next-app/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
  36. package/test/integration/next-app/src/app/api/revalidatePath/route.ts +15 -0
  37. package/test/integration/next-app/src/app/api/revalidateTag/route.ts +15 -0
  38. package/test/integration/next-app/src/app/api/revalidated-fetch/route.ts +17 -0
  39. package/test/integration/next-app/src/app/api/uncached-fetch/route.ts +15 -0
  40. package/test/integration/next-app/src/app/globals.css +26 -0
  41. package/test/integration/next-app/src/app/layout.tsx +59 -0
  42. package/test/integration/next-app/src/app/page.tsx +755 -0
  43. package/test/integration/next-app/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
  44. package/test/integration/next-app/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
  45. package/test/integration/next-app/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  46. package/test/integration/next-app/src/app/pages/no-fetch/default-page/page.tsx +55 -0
  47. package/test/integration/next-app/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
  48. package/test/integration/next-app/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
  49. package/test/integration/next-app/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  50. package/test/integration/next-app/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
  51. package/test/integration/next-app/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
  52. package/test/integration/next-app/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  53. package/test/integration/next-app/src/app/revalidation-interface.tsx +267 -0
  54. package/test/integration/next-app/tsconfig.json +27 -0
  55. package/test/integration/next-app-customized/README.md +36 -0
  56. package/test/integration/next-app-customized/customized-cache-handler.js +34 -0
  57. package/test/integration/next-app-customized/eslint.config.mjs +16 -0
  58. package/test/integration/next-app-customized/next.config.js +6 -0
  59. package/test/integration/nextjs-cache-handler.integration.test.ts +840 -0
  60. package/vite.config.ts +23 -8
package/src/SyncedMap.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // SyncedMap.ts
2
2
  import { Client, getTimeoutRedisCommandOptions } from './RedisStringsHandler';
3
+ import { debugVerbose, debug } from './utils/debug';
3
4
 
4
5
  type CustomizedSync = {
5
6
  withoutRedisHashmap?: boolean;
@@ -169,18 +170,60 @@ export class SyncedMap<V> {
169
170
  }
170
171
  };
171
172
 
172
- const keyEventHandler = async (_channel: string, message: string) => {
173
- const key = message;
173
+ const keyEventHandler = async (key: string, message: string) => {
174
+ debug(
175
+ 'yellow',
176
+ 'SyncedMap.keyEventHandler() called with message',
177
+ this.redisKey,
178
+ message,
179
+ key,
180
+ );
181
+ // const key = message;
174
182
  if (key.startsWith(this.keyPrefix)) {
175
183
  const keyInMap = key.substring(this.keyPrefix.length);
176
184
  if (this.filterKeys(keyInMap)) {
185
+ debugVerbose(
186
+ 'SyncedMap.keyEventHandler() key matches filter and will be deleted',
187
+ this.redisKey,
188
+ message,
189
+ key,
190
+ );
177
191
  await this.delete(keyInMap, true);
178
192
  }
193
+ } else {
194
+ debugVerbose(
195
+ 'SyncedMap.keyEventHandler() key does not have prefix',
196
+ this.redisKey,
197
+ message,
198
+ key,
199
+ );
179
200
  }
180
201
  };
181
202
 
182
203
  try {
183
- await this.subscriberClient.connect();
204
+ await this.subscriberClient.connect().catch(async () => {
205
+ await this.subscriberClient.connect();
206
+ });
207
+
208
+ // Check if keyspace event configuration is set correctly
209
+ const keyspaceEventConfig = (
210
+ await this.subscriberClient.configGet('notify-keyspace-events')
211
+ )?.['notify-keyspace-events'];
212
+ if (!keyspaceEventConfig.includes('E')) {
213
+ throw new Error(
214
+ "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`",
215
+ );
216
+ }
217
+ if (
218
+ !keyspaceEventConfig.includes('A') &&
219
+ !(
220
+ keyspaceEventConfig.includes('x') && keyspaceEventConfig.includes('e')
221
+ )
222
+ ) {
223
+ throw new Error(
224
+ "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`",
225
+ );
226
+ }
184
227
 
185
228
  await Promise.all([
186
229
  // We use a custom channel for insert/delete For the following reason:
@@ -188,7 +231,7 @@ export class SyncedMap<V> {
188
231
  // could get thousands of messages for one revalidateTag (For example revalidateTag("algolia") would send an enormous amount of network packages)
189
232
  // Also we can send the value in the message for insert
190
233
  this.subscriberClient.subscribe(this.syncChannel, syncHandler),
191
- // Subscribe to Redis keyspace notifications for evicted and expired keys
234
+ // Subscribe to Redis keyevent notifications for evicted and expired keys
192
235
  this.subscriberClient.subscribe(
193
236
  `__keyevent@${this.database}__:evicted`,
194
237
  keyEventHandler,
@@ -224,10 +267,20 @@ export class SyncedMap<V> {
224
267
  }
225
268
 
226
269
  public get(key: string): V | undefined {
270
+ debugVerbose(
271
+ 'SyncedMap.get() called with key',
272
+ key,
273
+ JSON.stringify(this.map.get(key))?.substring(0, 100),
274
+ );
227
275
  return this.map.get(key);
228
276
  }
229
277
 
230
278
  public async set(key: string, value: V): Promise<void> {
279
+ debugVerbose(
280
+ 'SyncedMap.set() called with key',
281
+ key,
282
+ JSON.stringify(value)?.substring(0, 100),
283
+ );
231
284
  this.map.set(key, value);
232
285
  const operations = [];
233
286
 
@@ -258,10 +311,20 @@ export class SyncedMap<V> {
258
311
  await Promise.all(operations);
259
312
  }
260
313
 
314
+ // /api/revalidated-fetch
315
+ // true
316
+
261
317
  public async delete(
262
318
  keys: string[] | string,
263
319
  withoutSyncMessage = false,
264
320
  ): Promise<void> {
321
+ debugVerbose(
322
+ 'SyncedMap.delete() called with keys',
323
+ this.redisKey,
324
+ keys,
325
+ withoutSyncMessage,
326
+ );
327
+
265
328
  const keysArray = Array.isArray(keys) ? keys : [keys];
266
329
  const operations = [];
267
330
 
@@ -285,7 +348,14 @@ export class SyncedMap<V> {
285
348
  this.client.publish(this.syncChannel, JSON.stringify(deletionMessage)),
286
349
  );
287
350
  }
351
+
288
352
  await Promise.all(operations);
353
+ debugVerbose(
354
+ 'SyncedMap.delete() finished operations',
355
+ this.redisKey,
356
+ keys,
357
+ operations.length,
358
+ );
289
359
  }
290
360
 
291
361
  public has(key: string): boolean {
package/src/index.ts CHANGED
@@ -1,2 +1,4 @@
1
- import CachedHandler from "./CachedHandler";
2
- export default CachedHandler;
1
+ import CachedHandler from './CachedHandler';
2
+ export default CachedHandler;
3
+ import RedisStringsHandler from './RedisStringsHandler';
4
+ export { RedisStringsHandler };
@@ -0,0 +1,30 @@
1
+ export function debug(
2
+ color:
3
+ | 'red'
4
+ | 'blue'
5
+ | 'green'
6
+ | 'yellow'
7
+ | 'cyan'
8
+ | 'white'
9
+ | 'none' = 'none',
10
+ ...args: unknown[]
11
+ ): void {
12
+ const colorCode = {
13
+ red: '\x1b[31m',
14
+ blue: '\x1b[34m',
15
+ green: '\x1b[32m',
16
+ yellow: '\x1b[33m',
17
+ cyan: '\x1b[36m',
18
+ white: '\x1b[37m',
19
+ none: '',
20
+ };
21
+ if (process.env.DEBUG_CACHE_HANDLER) {
22
+ console.log(colorCode[color], 'DEBUG CACHE HANDLER: ', ...args);
23
+ }
24
+ }
25
+
26
+ export function debugVerbose(color: string, ...args: unknown[]) {
27
+ if (process.env.DEBUG_CACHE_HANDLER_VERBOSE_VERBOSE) {
28
+ console.log('\x1b[35m', 'DEBUG SYNCED MAP: ', ...args);
29
+ }
30
+ }
@@ -0,0 +1,26 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export function bufferReviver(_: string, value: any): any {
3
+ if (value && typeof value === 'object' && typeof value.$binary === 'string') {
4
+ return Buffer.from(value.$binary, 'base64');
5
+ }
6
+ return value;
7
+ }
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ export function bufferReplacer(_: string, value: any): any {
10
+ if (Buffer.isBuffer(value)) {
11
+ return {
12
+ $binary: value.toString('base64'),
13
+ };
14
+ }
15
+ if (
16
+ value &&
17
+ typeof value === 'object' &&
18
+ value?.type === 'Buffer' &&
19
+ Array.isArray(value.data)
20
+ ) {
21
+ return {
22
+ $binary: Buffer.from(value.data).toString('base64'),
23
+ };
24
+ }
25
+ return value;
26
+ }
@@ -0,0 +1,36 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -0,0 +1,16 @@
1
+ import { dirname } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { FlatCompat } from "@eslint/eslintrc";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ const compat = new FlatCompat({
9
+ baseDirectory: __dirname,
10
+ });
11
+
12
+ const eslintConfig = [
13
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
14
+ ];
15
+
16
+ export default eslintConfig;
@@ -0,0 +1,6 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ cacheHandler: require.resolve('@trieb.work/nextjs-turbo-redis-cache'),
4
+ };
5
+
6
+ module.exports = nextConfig;