@trieb.work/nextjs-turbo-redis-cache 1.10.3 → 1.11.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 +26 -12
- package/CHANGELOG.md +7 -0
- package/README.md +101 -2
- package/dist/index.d.mts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +318 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +315 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/playwright.config.ts +8 -1
- package/src/CacheComponentsHandler.ts +471 -0
- package/src/index.ts +7 -0
- package/test/cache-components/cache-components.integration.spec.ts +188 -0
- package/test/integration/next-app-15-4-7/next.config.js +3 -0
- package/test/integration/next-app-15-4-7/pnpm-lock.yaml +1 -1
- package/test/integration/next-app-16-0-3/next.config.ts +3 -0
- package/test/integration/next-app-16-1-1-cache-components/README.md +36 -0
- package/test/integration/next-app-16-1-1-cache-components/cache-handler.js +3 -0
- package/test/integration/next-app-16-1-1-cache-components/eslint.config.mjs +18 -0
- package/test/integration/next-app-16-1-1-cache-components/next.config.ts +13 -0
- package/test/integration/next-app-16-1-1-cache-components/package.json +28 -0
- package/test/integration/next-app-16-1-1-cache-components/pnpm-lock.yaml +4128 -0
- package/test/integration/next-app-16-1-1-cache-components/postcss.config.mjs +7 -0
- package/test/integration/next-app-16-1-1-cache-components/public/file.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/globe.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/next.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/vercel.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/window.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/page.tsx +90 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/favicon.ico +0 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/globals.css +26 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/layout.tsx +57 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/page.tsx +755 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/revalidation-interface.tsx +267 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/update-tag-test/page.tsx +22 -0
- package/test/integration/next-app-16-1-1-cache-components/tsconfig.json +34 -0
- package/tests/cache-lab.spec.ts +157 -0
- package/vitest.cache-components.config.ts +16 -0
- package/.next/trace +0 -11
package/.github/workflows/ci.yml
CHANGED
|
@@ -25,12 +25,15 @@ jobs:
|
|
|
25
25
|
pull-requests: write # Grant write access to pull request comments
|
|
26
26
|
strategy:
|
|
27
27
|
matrix:
|
|
28
|
-
|
|
29
|
-
- next-app-15-0-3
|
|
30
|
-
|
|
31
|
-
- next-app-15-
|
|
32
|
-
|
|
33
|
-
- next-app-
|
|
28
|
+
include:
|
|
29
|
+
- next-test-app: next-app-15-0-3
|
|
30
|
+
run-cache-components: false
|
|
31
|
+
- next-test-app: next-app-15-3-2
|
|
32
|
+
run-cache-components: false
|
|
33
|
+
- next-test-app: next-app-15-4-7
|
|
34
|
+
run-cache-components: false
|
|
35
|
+
- next-test-app: next-app-16-0-3
|
|
36
|
+
run-cache-components: true
|
|
34
37
|
|
|
35
38
|
steps:
|
|
36
39
|
- name: Checkout code
|
|
@@ -78,18 +81,29 @@ jobs:
|
|
|
78
81
|
SKIP_BUILD: true
|
|
79
82
|
NEXT_TEST_APP: ${{ matrix.next-test-app }}
|
|
80
83
|
|
|
81
|
-
- name: Install
|
|
82
|
-
if:
|
|
84
|
+
- name: Install Cache Components Integration Test Project
|
|
85
|
+
if: matrix.run-cache-components
|
|
86
|
+
run: cd test/integration/next-app-16-1-1-cache-components && pnpm install
|
|
87
|
+
|
|
88
|
+
- name: Build Cache Components Integration Test Project
|
|
89
|
+
if: matrix.run-cache-components
|
|
90
|
+
run: cd test/integration/next-app-16-1-1-cache-components && pnpm build
|
|
91
|
+
|
|
92
|
+
- name: Run Cache Components integration tests
|
|
93
|
+
if: matrix.run-cache-components
|
|
94
|
+
run: pnpm test:cache-components
|
|
95
|
+
|
|
96
|
+
- name: Install Playwright browsers
|
|
97
|
+
if: matrix.run-cache-components
|
|
83
98
|
run: pnpm exec playwright install --with-deps
|
|
84
99
|
|
|
85
|
-
- name: Run Playwright E2E tests
|
|
86
|
-
if:
|
|
100
|
+
- name: Run Playwright E2E tests
|
|
101
|
+
if: matrix.run-cache-components
|
|
87
102
|
run: pnpm test:e2e
|
|
88
103
|
env:
|
|
89
|
-
PLAYWRIGHT_BASE_URL: http://localhost:
|
|
104
|
+
PLAYWRIGHT_BASE_URL: http://localhost:3101
|
|
90
105
|
|
|
91
106
|
- name: Code Coverage Comments
|
|
92
|
-
# only run for pull requests from this repository - so no problems for external PRs
|
|
93
107
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
|
94
108
|
uses: kcjpop/coverage-comments@v2.2
|
|
95
109
|
with:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.11.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.10.3...v1.11.0) (2026-02-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* release cache components ([#67](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/67)) ([815e66b](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/815e66bab21f58d77693cad2a9d73f95ff8e7d79))
|
|
7
|
+
|
|
1
8
|
## [1.10.3](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.10.2...v1.10.3) (2026-01-12)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# nextjs-turbo-redis-cache
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@trieb.work/nextjs-turbo-redis-cache)
|
|
4
|
-

|
|
5
5
|
|
|
6
6
|
The ultimate Redis caching solution for Next.js 15 and the app router. Built for production-ready, large-scale projects, it delivers unparalleled performance and efficiency with features tailored for high-traffic applications. This package has been created after extensibly testing the @neshca package and finding several major issues with it.
|
|
7
7
|
|
|
@@ -28,8 +28,11 @@ Tested versions are:
|
|
|
28
28
|
- Nextjs 15.4.7 + redis client 4.7.0
|
|
29
29
|
- Nextjs 16.0.3 + redis client 4.7.0 (cacheComponents: false)
|
|
30
30
|
- Nextjs 16.1.1 + redis client 4.7.0 (cacheComponents: false)
|
|
31
|
+
- Nextjs 16.1.1 + redis client 4.7.0 (cacheComponents: true)
|
|
31
32
|
|
|
32
|
-
Currently PPR, 'use cache', cacheLife and cacheTag are not tested. Use these operations with caution and your own risk. [Cache Components](https://nextjs.org/docs/app/getting-started/cache-components)
|
|
33
|
+
Currently PPR, 'use cache', cacheLife and cacheTag are not tested. Use these operations with caution and your own risk. [Cache Components](https://nextjs.org/docs/app/getting-started/cache-components) are supported experimentally (Next.js 16+).
|
|
34
|
+
|
|
35
|
+
For Cache Components, see the "Cache Components handler (Next.js 16+)" section below.
|
|
33
36
|
|
|
34
37
|
## Getting started
|
|
35
38
|
|
|
@@ -215,6 +218,12 @@ To run all tests you can use the following command:
|
|
|
215
218
|
pnpm build && pnpm test
|
|
216
219
|
```
|
|
217
220
|
|
|
221
|
+
Folder layout / runners:
|
|
222
|
+
|
|
223
|
+
- **Vitest** (unit + integration) lives in `src/**/*.test.ts(x)` and `test/**`.
|
|
224
|
+
- **Playwright** (E2E) lives in `tests/**` (see `playwright.config.ts`).
|
|
225
|
+
- `test/browser/**` contains Vitest tests that hit a running Next.js app over HTTP. Despite the folder name, this is not Playwright and does not use Vitest browser mode.
|
|
226
|
+
|
|
218
227
|
### Unit tests
|
|
219
228
|
|
|
220
229
|
To run unit tests you can use the following command:
|
|
@@ -231,6 +240,14 @@ To run integration tests you can use the following command:
|
|
|
231
240
|
pnpm build && pnpm test:integration
|
|
232
241
|
```
|
|
233
242
|
|
|
243
|
+
### E2E tests (Playwright)
|
|
244
|
+
|
|
245
|
+
To run Playwright tests (`tests/**`) you can use:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
pnpm test:e2e
|
|
249
|
+
```
|
|
250
|
+
|
|
234
251
|
The integration tests will start a Next.js server and test the caching handler. You can modify testing behavior by setting the following environment variables:
|
|
235
252
|
|
|
236
253
|
- SKIP_BUILD: If set to true, the integration tests will not build the Next.js app. Therefore the nextjs app needs to be built before running the tests. Or you execute the test once without skip build and the re-execute `pnpm test:integration` with skip build set to true.
|
|
@@ -239,6 +256,88 @@ The integration tests will start a Next.js server and test the caching handler.
|
|
|
239
256
|
|
|
240
257
|
Integration tests may have dependencies between test cases, so individual test failures should be evaluated in the context of the full test suite rather than in isolation.
|
|
241
258
|
|
|
259
|
+
## Cache Components handler (Next.js 16+)
|
|
260
|
+
|
|
261
|
+
This package can be used as a Cache Components handler (Node.js runtime) for Next.js apps that enable Cache Components.
|
|
262
|
+
|
|
263
|
+
This is experimental support and the Next.js Cache Components APIs may still change. We don't have a larger production project right now available to test this in real world conditions.
|
|
264
|
+
|
|
265
|
+
### Enable Cache Components + cache handler
|
|
266
|
+
|
|
267
|
+
Install the package in your Next.js app:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
pnpm add @trieb.work/nextjs-turbo-redis-cache redis
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
In your Next.js app, enable Cache Components and point `cacheHandlers.default` to a module that exports the handler instance:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// next.config.ts
|
|
277
|
+
import type { NextConfig } from 'next';
|
|
278
|
+
|
|
279
|
+
const nextConfig: NextConfig = {
|
|
280
|
+
cacheComponents: true,
|
|
281
|
+
cacheHandlers: {
|
|
282
|
+
default: require.resolve('./cache-handler.js'),
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export default nextConfig;
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```js
|
|
290
|
+
// cache-handler.js
|
|
291
|
+
const { redisCacheHandler } = require('@trieb.work/nextjs-turbo-redis-cache');
|
|
292
|
+
|
|
293
|
+
module.exports = redisCacheHandler;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
If you prefer ESM:
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
// cache-handler.js
|
|
300
|
+
import { redisCacheHandler } from '@trieb.work/nextjs-turbo-redis-cache';
|
|
301
|
+
|
|
302
|
+
export default redisCacheHandler;
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Required environment variables
|
|
306
|
+
|
|
307
|
+
- `REDIS_URL` (recommended): e.g. `redis://localhost:6379`
|
|
308
|
+
|
|
309
|
+
Optional:
|
|
310
|
+
|
|
311
|
+
- `VERCEL_URL`: used as a key prefix for multi-tenant isolation (also useful in tests). If unset, a default prefix is used.
|
|
312
|
+
- `REDIS_COMMAND_TIMEOUT_MS`: timeout (ms) for Redis commands used by the handler.
|
|
313
|
+
|
|
314
|
+
### Local manual testing (Cache Lab)
|
|
315
|
+
|
|
316
|
+
This repo includes a dedicated Next.js Cache Components integration app with real pages for manual testing.
|
|
317
|
+
|
|
318
|
+
1. Start Redis locally.
|
|
319
|
+
1. Install + start the Cache Components test app:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
pnpm -C test/integration/next-app-16-1-1-cache-components install
|
|
323
|
+
pnpm -C test/integration/next-app-16-1-1-cache-components dev
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Then open the Cache Lab pages:
|
|
327
|
+
|
|
328
|
+
- `/cache-lab`
|
|
329
|
+
- `/cache-lab/use-cache-nondeterministic`
|
|
330
|
+
- `/cache-lab/cachelife-short`
|
|
331
|
+
- `/cache-lab/tag-invalidation`
|
|
332
|
+
- `/cache-lab/stale-while-revalidate`
|
|
333
|
+
- `/cache-lab/runtime-data-suspense`
|
|
334
|
+
|
|
335
|
+
To run the Playwright E2E tests against a running dev server:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
PLAYWRIGHT_BASE_URL=http://localhost:3101 pnpm test:e2e
|
|
339
|
+
```
|
|
340
|
+
|
|
242
341
|
## Some words on nextjs caching internals
|
|
243
342
|
|
|
244
343
|
Nextjs will use different caching objects for different pages and api routes. Currently supported are kind: APP_ROUTE and APP_PAGE.
|
package/dist/index.d.mts
CHANGED
|
@@ -149,4 +149,25 @@ declare class CachedHandler {
|
|
|
149
149
|
resetRequestCache(...args: Parameters<RedisStringsHandler['resetRequestCache']>): ReturnType<RedisStringsHandler['resetRequestCache']>;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
interface CacheComponentsEntry {
|
|
153
|
+
value: ReadableStream<Uint8Array>;
|
|
154
|
+
tags: string[];
|
|
155
|
+
stale: number;
|
|
156
|
+
timestamp: number;
|
|
157
|
+
expire: number;
|
|
158
|
+
revalidate: number;
|
|
159
|
+
}
|
|
160
|
+
interface CacheComponentsHandler {
|
|
161
|
+
get(cacheKey: string, softTags: string[]): Promise<CacheComponentsEntry | undefined>;
|
|
162
|
+
set(cacheKey: string, pendingEntry: Promise<CacheComponentsEntry>): Promise<void>;
|
|
163
|
+
refreshTags(): Promise<void>;
|
|
164
|
+
getExpiration(tags: string[]): Promise<number>;
|
|
165
|
+
updateTags(tags: string[], durations?: {
|
|
166
|
+
expire?: number;
|
|
167
|
+
}): Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
type CreateCacheComponentsHandlerOptions = CreateRedisStringsHandlerOptions;
|
|
170
|
+
declare function getRedisCacheComponentsHandler(options?: CreateCacheComponentsHandlerOptions): CacheComponentsHandler;
|
|
171
|
+
declare const redisCacheHandler: CacheComponentsHandler;
|
|
172
|
+
|
|
173
|
+
export { type CreateRedisStringsHandlerOptions, RedisStringsHandler, CachedHandler as default, getRedisCacheComponentsHandler, redisCacheHandler };
|
package/dist/index.d.ts
CHANGED
|
@@ -149,4 +149,25 @@ declare class CachedHandler {
|
|
|
149
149
|
resetRequestCache(...args: Parameters<RedisStringsHandler['resetRequestCache']>): ReturnType<RedisStringsHandler['resetRequestCache']>;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
interface CacheComponentsEntry {
|
|
153
|
+
value: ReadableStream<Uint8Array>;
|
|
154
|
+
tags: string[];
|
|
155
|
+
stale: number;
|
|
156
|
+
timestamp: number;
|
|
157
|
+
expire: number;
|
|
158
|
+
revalidate: number;
|
|
159
|
+
}
|
|
160
|
+
interface CacheComponentsHandler {
|
|
161
|
+
get(cacheKey: string, softTags: string[]): Promise<CacheComponentsEntry | undefined>;
|
|
162
|
+
set(cacheKey: string, pendingEntry: Promise<CacheComponentsEntry>): Promise<void>;
|
|
163
|
+
refreshTags(): Promise<void>;
|
|
164
|
+
getExpiration(tags: string[]): Promise<number>;
|
|
165
|
+
updateTags(tags: string[], durations?: {
|
|
166
|
+
expire?: number;
|
|
167
|
+
}): Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
type CreateCacheComponentsHandlerOptions = CreateRedisStringsHandlerOptions;
|
|
170
|
+
declare function getRedisCacheComponentsHandler(options?: CreateCacheComponentsHandlerOptions): CacheComponentsHandler;
|
|
171
|
+
declare const redisCacheHandler: CacheComponentsHandler;
|
|
172
|
+
|
|
173
|
+
export { type CreateRedisStringsHandlerOptions, RedisStringsHandler, CachedHandler as default, getRedisCacheComponentsHandler, redisCacheHandler };
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
RedisStringsHandler: () => RedisStringsHandler,
|
|
24
|
-
default: () => index_default
|
|
24
|
+
default: () => index_default,
|
|
25
|
+
getRedisCacheComponentsHandler: () => getRedisCacheComponentsHandler,
|
|
26
|
+
redisCacheHandler: () => redisCacheHandler
|
|
25
27
|
});
|
|
26
28
|
module.exports = __toCommonJS(index_exports);
|
|
27
29
|
|
|
@@ -975,10 +977,324 @@ var CachedHandler = class {
|
|
|
975
977
|
}
|
|
976
978
|
};
|
|
977
979
|
|
|
980
|
+
// src/CacheComponentsHandler.ts
|
|
981
|
+
var import_redis2 = require("redis");
|
|
982
|
+
var REVALIDATED_TAGS_KEY2 = "__cacheComponents_revalidated_tags__";
|
|
983
|
+
var SHARED_TAGS_KEY = "__cacheComponents_sharedTags__";
|
|
984
|
+
var killContainerOnErrorCount2 = 0;
|
|
985
|
+
async function streamToBuffer(stream) {
|
|
986
|
+
const reader = stream.getReader();
|
|
987
|
+
const chunks = [];
|
|
988
|
+
while (true) {
|
|
989
|
+
const { value, done } = await reader.read();
|
|
990
|
+
if (done) break;
|
|
991
|
+
if (value) {
|
|
992
|
+
chunks.push(value);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (chunks.length === 1) {
|
|
996
|
+
return chunks[0];
|
|
997
|
+
}
|
|
998
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
999
|
+
const result = new Uint8Array(totalLength);
|
|
1000
|
+
let offset = 0;
|
|
1001
|
+
for (const chunk of chunks) {
|
|
1002
|
+
result.set(chunk, offset);
|
|
1003
|
+
offset += chunk.byteLength;
|
|
1004
|
+
}
|
|
1005
|
+
return result;
|
|
1006
|
+
}
|
|
1007
|
+
function bufferToReadableStream(buffer) {
|
|
1008
|
+
return new ReadableStream({
|
|
1009
|
+
start(controller) {
|
|
1010
|
+
controller.enqueue(buffer);
|
|
1011
|
+
controller.close();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
var RedisCacheComponentsHandler = class {
|
|
1016
|
+
constructor({
|
|
1017
|
+
redisUrl = process.env.REDIS_URL ? process.env.REDIS_URL : process.env.REDISHOST ? `redis://${process.env.REDISHOST}:${process.env.REDISPORT}` : "redis://localhost:6379",
|
|
1018
|
+
database = process.env.VERCEL_ENV === "production" ? 0 : 1,
|
|
1019
|
+
keyPrefix = process.env.VERCEL_URL || "UNDEFINED_URL_",
|
|
1020
|
+
getTimeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS ? Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500 : 500,
|
|
1021
|
+
revalidateTagQuerySize = 250,
|
|
1022
|
+
avgResyncIntervalMs = 60 * 60 * 1e3,
|
|
1023
|
+
socketOptions,
|
|
1024
|
+
clientOptions,
|
|
1025
|
+
killContainerOnErrorThreshold = process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD ? Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0
|
|
1026
|
+
}) {
|
|
1027
|
+
try {
|
|
1028
|
+
this.keyPrefix = keyPrefix;
|
|
1029
|
+
this.getTimeoutMs = getTimeoutMs;
|
|
1030
|
+
this.client = (0, import_redis2.createClient)({
|
|
1031
|
+
url: redisUrl,
|
|
1032
|
+
pingInterval: 1e4,
|
|
1033
|
+
...database !== 0 ? { database } : {},
|
|
1034
|
+
...socketOptions ? { socket: { ...socketOptions } } : {},
|
|
1035
|
+
...clientOptions || {}
|
|
1036
|
+
});
|
|
1037
|
+
this.client.on("error", (error) => {
|
|
1038
|
+
console.error(
|
|
1039
|
+
"RedisCacheComponentsHandler client error",
|
|
1040
|
+
error,
|
|
1041
|
+
killContainerOnErrorCount2++
|
|
1042
|
+
);
|
|
1043
|
+
setTimeout(
|
|
1044
|
+
() => this.client.connect().catch((err) => {
|
|
1045
|
+
console.error(
|
|
1046
|
+
"Failed to reconnect RedisCacheComponentsHandler client after connection loss:",
|
|
1047
|
+
err
|
|
1048
|
+
);
|
|
1049
|
+
}),
|
|
1050
|
+
1e3
|
|
1051
|
+
);
|
|
1052
|
+
if (killContainerOnErrorThreshold > 0 && killContainerOnErrorCount2 >= killContainerOnErrorThreshold) {
|
|
1053
|
+
console.error(
|
|
1054
|
+
"RedisCacheComponentsHandler client error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
|
|
1055
|
+
error,
|
|
1056
|
+
killContainerOnErrorCount2++
|
|
1057
|
+
);
|
|
1058
|
+
this.client.disconnect();
|
|
1059
|
+
this.client.quit();
|
|
1060
|
+
setTimeout(() => {
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}, 500);
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
this.client.connect().then(() => {
|
|
1066
|
+
debug("green", "RedisCacheComponentsHandler client connected.");
|
|
1067
|
+
}).catch(() => {
|
|
1068
|
+
this.client.connect().catch((error) => {
|
|
1069
|
+
console.error(
|
|
1070
|
+
"Failed to connect RedisCacheComponentsHandler client:",
|
|
1071
|
+
error
|
|
1072
|
+
);
|
|
1073
|
+
this.client.disconnect();
|
|
1074
|
+
throw error;
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY2 && key !== SHARED_TAGS_KEY;
|
|
1078
|
+
this.revalidatedTagsMap = new SyncedMap({
|
|
1079
|
+
client: this.client,
|
|
1080
|
+
keyPrefix,
|
|
1081
|
+
redisKey: REVALIDATED_TAGS_KEY2,
|
|
1082
|
+
database,
|
|
1083
|
+
querySize: revalidateTagQuerySize,
|
|
1084
|
+
filterKeys,
|
|
1085
|
+
resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
1086
|
+
});
|
|
1087
|
+
this.sharedTagsMap = new SyncedMap({
|
|
1088
|
+
client: this.client,
|
|
1089
|
+
keyPrefix,
|
|
1090
|
+
redisKey: SHARED_TAGS_KEY,
|
|
1091
|
+
database,
|
|
1092
|
+
querySize: revalidateTagQuerySize,
|
|
1093
|
+
filterKeys,
|
|
1094
|
+
resyncIntervalMs: avgResyncIntervalMs - avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
1095
|
+
});
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
console.error("RedisCacheComponentsHandler constructor error", error);
|
|
1098
|
+
throw error;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
async assertClientIsReady() {
|
|
1102
|
+
if (!this.client.isReady && !this.client.isOpen) {
|
|
1103
|
+
await this.client.connect().catch((error) => {
|
|
1104
|
+
console.error(
|
|
1105
|
+
"RedisCacheComponentsHandler assertClientIsReady reconnect error:",
|
|
1106
|
+
error
|
|
1107
|
+
);
|
|
1108
|
+
throw error;
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
await Promise.all([
|
|
1112
|
+
this.revalidatedTagsMap.waitUntilReady(),
|
|
1113
|
+
this.sharedTagsMap.waitUntilReady()
|
|
1114
|
+
]);
|
|
1115
|
+
}
|
|
1116
|
+
async computeMaxRevalidation(tags) {
|
|
1117
|
+
let max = 0;
|
|
1118
|
+
for (const tag of tags) {
|
|
1119
|
+
const ts = this.revalidatedTagsMap.get(tag);
|
|
1120
|
+
if (ts && ts > max) {
|
|
1121
|
+
max = ts;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return max;
|
|
1125
|
+
}
|
|
1126
|
+
async get(cacheKey, softTags) {
|
|
1127
|
+
const redisKey = `${this.keyPrefix}${cacheKey}`;
|
|
1128
|
+
try {
|
|
1129
|
+
await this.assertClientIsReady();
|
|
1130
|
+
const serialized = await redisErrorHandler(
|
|
1131
|
+
"RedisCacheComponentsHandler.get(), operation: get " + this.getTimeoutMs + "ms " + redisKey,
|
|
1132
|
+
this.client.get(
|
|
1133
|
+
(0, import_redis2.commandOptions)({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
|
|
1134
|
+
redisKey
|
|
1135
|
+
)
|
|
1136
|
+
);
|
|
1137
|
+
if (!serialized) {
|
|
1138
|
+
return void 0;
|
|
1139
|
+
}
|
|
1140
|
+
const stored = JSON.parse(serialized);
|
|
1141
|
+
const now = Date.now();
|
|
1142
|
+
const expiryTime = stored.timestamp + stored.expire * 1e3;
|
|
1143
|
+
if (Number.isFinite(stored.expire) && stored.expire > 0 && now > expiryTime) {
|
|
1144
|
+
await this.client.unlink(redisKey).catch(() => {
|
|
1145
|
+
});
|
|
1146
|
+
await this.sharedTagsMap.delete(cacheKey).catch(() => {
|
|
1147
|
+
});
|
|
1148
|
+
return void 0;
|
|
1149
|
+
}
|
|
1150
|
+
const maxRevalidation = await this.computeMaxRevalidation([
|
|
1151
|
+
...stored.tags || [],
|
|
1152
|
+
...softTags || []
|
|
1153
|
+
]);
|
|
1154
|
+
if (maxRevalidation > 0 && maxRevalidation > stored.timestamp) {
|
|
1155
|
+
await this.client.unlink(redisKey).catch(() => {
|
|
1156
|
+
});
|
|
1157
|
+
await this.sharedTagsMap.delete(cacheKey).catch(() => {
|
|
1158
|
+
});
|
|
1159
|
+
return void 0;
|
|
1160
|
+
}
|
|
1161
|
+
const valueBuffer = typeof stored.value === "string" ? new Uint8Array(Buffer.from(stored.value, "base64")) : stored.value;
|
|
1162
|
+
return {
|
|
1163
|
+
...stored,
|
|
1164
|
+
value: bufferToReadableStream(valueBuffer)
|
|
1165
|
+
};
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
console.error(
|
|
1168
|
+
"RedisCacheComponentsHandler.get() Error occurred while getting cache entry. Returning undefined so site can continue to serve content while cache is disabled. The original error was:",
|
|
1169
|
+
error,
|
|
1170
|
+
killContainerOnErrorCount2++
|
|
1171
|
+
);
|
|
1172
|
+
return void 0;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
async set(cacheKey, pendingEntry) {
|
|
1176
|
+
try {
|
|
1177
|
+
await this.assertClientIsReady();
|
|
1178
|
+
const entry = await pendingEntry;
|
|
1179
|
+
const [storeStream] = entry.value.tee();
|
|
1180
|
+
const buffer = await streamToBuffer(storeStream);
|
|
1181
|
+
const stored = {
|
|
1182
|
+
value: Buffer.from(buffer).toString("base64"),
|
|
1183
|
+
tags: entry.tags || [],
|
|
1184
|
+
stale: entry.stale,
|
|
1185
|
+
timestamp: entry.timestamp,
|
|
1186
|
+
expire: entry.expire,
|
|
1187
|
+
revalidate: entry.revalidate
|
|
1188
|
+
};
|
|
1189
|
+
let serialized;
|
|
1190
|
+
try {
|
|
1191
|
+
const cleanStored = {
|
|
1192
|
+
value: stored.value,
|
|
1193
|
+
tags: Array.isArray(stored.tags) ? [...stored.tags] : [],
|
|
1194
|
+
stale: Number(stored.stale),
|
|
1195
|
+
timestamp: Number(stored.timestamp),
|
|
1196
|
+
expire: Number(stored.expire),
|
|
1197
|
+
revalidate: Number(stored.revalidate)
|
|
1198
|
+
};
|
|
1199
|
+
serialized = JSON.stringify(cleanStored);
|
|
1200
|
+
} catch (jsonError) {
|
|
1201
|
+
console.error("JSON.stringify error:", jsonError);
|
|
1202
|
+
console.error("Stored object:", stored);
|
|
1203
|
+
throw jsonError;
|
|
1204
|
+
}
|
|
1205
|
+
const ttlSeconds = Number.isFinite(stored.expire) && stored.expire > 0 ? Math.floor(stored.expire) : void 0;
|
|
1206
|
+
const redisKey = `${this.keyPrefix}${cacheKey}`;
|
|
1207
|
+
const setOperation = redisErrorHandler(
|
|
1208
|
+
"RedisCacheComponentsHandler.set(), operation: set " + redisKey,
|
|
1209
|
+
this.client.set(redisKey, serialized, {
|
|
1210
|
+
...ttlSeconds ? { EX: ttlSeconds } : {}
|
|
1211
|
+
})
|
|
1212
|
+
);
|
|
1213
|
+
let tagsOperation;
|
|
1214
|
+
const tags = stored.tags || [];
|
|
1215
|
+
if (tags.length > 0) {
|
|
1216
|
+
const currentTags = this.sharedTagsMap.get(cacheKey);
|
|
1217
|
+
const currentIsSameAsNew = currentTags?.length === tags.length && currentTags.every((v) => tags.includes(v)) && tags.every((v) => currentTags.includes(v));
|
|
1218
|
+
if (!currentIsSameAsNew) {
|
|
1219
|
+
tagsOperation = this.sharedTagsMap.set(cacheKey, [...tags]);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
await Promise.all([setOperation, tagsOperation]);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
console.error(
|
|
1225
|
+
"RedisCacheComponentsHandler.set() Error occurred while setting cache entry. The original error was:",
|
|
1226
|
+
error,
|
|
1227
|
+
killContainerOnErrorCount2++
|
|
1228
|
+
);
|
|
1229
|
+
throw error;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
async refreshTags() {
|
|
1233
|
+
await this.assertClientIsReady();
|
|
1234
|
+
}
|
|
1235
|
+
async getExpiration(tags) {
|
|
1236
|
+
try {
|
|
1237
|
+
await this.assertClientIsReady();
|
|
1238
|
+
return this.computeMaxRevalidation(tags || []);
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
console.error(
|
|
1241
|
+
"RedisCacheComponentsHandler.getExpiration() Error occurred while getting expiration for tags. The original error was:",
|
|
1242
|
+
error
|
|
1243
|
+
);
|
|
1244
|
+
return 0;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
async updateTags(tags, _durations) {
|
|
1248
|
+
try {
|
|
1249
|
+
void _durations;
|
|
1250
|
+
await this.assertClientIsReady();
|
|
1251
|
+
const now = Date.now();
|
|
1252
|
+
const tagsSet = new Set(tags || []);
|
|
1253
|
+
for (const tag of tagsSet) {
|
|
1254
|
+
await this.revalidatedTagsMap.set(tag, now);
|
|
1255
|
+
}
|
|
1256
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
1257
|
+
for (const [key, storedTags] of this.sharedTagsMap.entries()) {
|
|
1258
|
+
if (storedTags.some((tag) => tagsSet.has(tag))) {
|
|
1259
|
+
keysToDelete.add(key);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (keysToDelete.size === 0) {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const cacheKeys = Array.from(keysToDelete);
|
|
1266
|
+
const fullRedisKeys = cacheKeys.map((key) => `${this.keyPrefix}${key}`);
|
|
1267
|
+
await redisErrorHandler(
|
|
1268
|
+
"RedisCacheComponentsHandler.updateTags(), operation: unlink",
|
|
1269
|
+
this.client.unlink(fullRedisKeys)
|
|
1270
|
+
);
|
|
1271
|
+
const deleteTagsOperation = this.sharedTagsMap.delete(cacheKeys);
|
|
1272
|
+
await deleteTagsOperation;
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
console.error(
|
|
1275
|
+
"RedisCacheComponentsHandler.updateTags() Error occurred while updating tags. The original error was:",
|
|
1276
|
+
error,
|
|
1277
|
+
killContainerOnErrorCount2++
|
|
1278
|
+
);
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
var singletonHandler;
|
|
1284
|
+
function getRedisCacheComponentsHandler(options = {}) {
|
|
1285
|
+
if (!singletonHandler) {
|
|
1286
|
+
singletonHandler = new RedisCacheComponentsHandler(options);
|
|
1287
|
+
}
|
|
1288
|
+
return singletonHandler;
|
|
1289
|
+
}
|
|
1290
|
+
var redisCacheHandler = getRedisCacheComponentsHandler();
|
|
1291
|
+
|
|
978
1292
|
// src/index.ts
|
|
979
1293
|
var index_default = CachedHandler;
|
|
980
1294
|
// Annotate the CommonJS export names for ESM import in node:
|
|
981
1295
|
0 && (module.exports = {
|
|
982
|
-
RedisStringsHandler
|
|
1296
|
+
RedisStringsHandler,
|
|
1297
|
+
getRedisCacheComponentsHandler,
|
|
1298
|
+
redisCacheHandler
|
|
983
1299
|
});
|
|
984
1300
|
//# sourceMappingURL=index.js.map
|