@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.
- 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 +54 -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 +12 -7
- 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/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 (
|
|
173
|
-
|
|
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
|
|
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
|
@@ -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;
|