@trieb.work/nextjs-turbo-redis-cache 1.14.1 → 1.15.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/pages.yml +38 -0
- package/CHANGELOG.md +12 -0
- package/README.md +150 -3
- package/dist/index.d.mts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +31 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +28 -9
- package/dist/index.mjs.map +1 -1
- package/docs/index.html +139 -0
- package/docs/robots.txt +4 -0
- package/docs/sitemap.xml +8 -0
- package/docs/styles.css +141 -0
- package/package.json +3 -3
- package/src/RedisStringsHandler.ts +48 -11
- package/src/index.ts +4 -0
- package/src/serializer.test.ts +178 -0
- package/src/serializer.ts +59 -0
- package/test/cache-components/cache-components.integration.spec.ts +39 -0
- package/test/integration/next-app-16-2-3-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
- package/test/integration/nextjs-cache-handler.integration.test.ts +2 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Deploy GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pages: write
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: pages
|
|
16
|
+
cancel-in-progress: true
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
deploy:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
environment:
|
|
22
|
+
name: github-pages
|
|
23
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout code
|
|
26
|
+
uses: actions/checkout@v6
|
|
27
|
+
|
|
28
|
+
- name: Setup Pages
|
|
29
|
+
uses: actions/configure-pages@v5
|
|
30
|
+
|
|
31
|
+
- name: Upload static site artifact
|
|
32
|
+
uses: actions/upload-pages-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
path: docs
|
|
35
|
+
|
|
36
|
+
- name: Deploy to GitHub Pages
|
|
37
|
+
id: deployment
|
|
38
|
+
uses: actions/deploy-pages@v4
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# [1.15.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.14.1...v1.15.0) (2026-05-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **serializer:** treat deserialize failures as cache misses ([a33b2c8](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/a33b2c8dd5d94686a451ed004b05a7883131f623))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **serializer:** add pluggable valueSerializer for compression and custom encoding ([f55d0a3](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/f55d0a3ccb9f007d231826e27bca04785bcd2c50))
|
|
12
|
+
|
|
1
13
|
## [1.14.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.14.0...v1.14.1) (2026-05-15)
|
|
2
14
|
|
|
3
15
|
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# nextjs-turbo-redis-cache
|
|
1
|
+
# nextjs-turbo-redis-cache - Next.js Cache Handler
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@trieb.work/nextjs-turbo-redis-cache)
|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
The ultimate Redis
|
|
6
|
+
The ultimate Redis Cache Handler for Next.js 15 / 16 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
|
|
|
8
8
|
Key Features:
|
|
9
9
|
|
|
@@ -30,7 +30,7 @@ Tested versions are:
|
|
|
30
30
|
- Nextjs 16.2.3 + redis client 4.7.0 (cacheComponents: false)
|
|
31
31
|
- Nextjs 16.2.3 + redis client 4.7.0 (cacheComponents: true)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
_Cache Components_ (Next.js 16+) are fully supported. Automated test coverage includes `'use cache'`, `cacheTag`, and `cacheLife` flows in the Cache Components integration suite.
|
|
34
34
|
|
|
35
35
|
For Cache Components, see the "Cache Components handler (Next.js 16+)" section below.
|
|
36
36
|
|
|
@@ -153,6 +153,153 @@ A working example of above can be found in the `test/integration/next-app-custom
|
|
|
153
153
|
| socketOptions | Redis client socket options for TLS/SSL configuration (e.g., `{ tls: true, rejectUnauthorized: false }`) | `{ connectTimeout: timeoutMs }` |
|
|
154
154
|
| clientOptions | Additional Redis client options (e.g., username, password) | `undefined` |
|
|
155
155
|
| killContainerOnErrorThreshold | Number of consecutive errors before the container is killed. Set to 0 to disable. | `Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0` |
|
|
156
|
+
| valueSerializer | Pluggable wire-format codec for Redis string values (compression, encryption, custom encoding). See [Custom value serializer](#custom-value-serializer-compression-encryption). | `jsonCacheValueSerializer` (`JSON.stringify` with built-in `Buffer` and `Map` encoding) |
|
|
157
|
+
|
|
158
|
+
## Custom value serializer (compression, encryption)
|
|
159
|
+
|
|
160
|
+
By default cache entries are stored as `JSON.stringify(...)` with built-in
|
|
161
|
+
`Buffer` and `Map` encoding. For workloads where the encoded payload is large
|
|
162
|
+
(big RSC trees, large fetch responses) or sensitive (PII), you can plug in a
|
|
163
|
+
custom codec - gzip, brotli, AES, anything - via the `valueSerializer` option,
|
|
164
|
+
without forking this package or losing the existing dedup / batch / keyspace
|
|
165
|
+
features.
|
|
166
|
+
|
|
167
|
+
### Contract
|
|
168
|
+
|
|
169
|
+
- `serialize(entry)` is called on every `set()` with the in-memory `CacheEntry`. It must return the string written to Redis, or a `Promise<string>` for async codecs.
|
|
170
|
+
- `deserialize(stored)` is called on every cache hit with the exact string read from Redis. It must return a `CacheEntry`, `null`, or a `Promise` of either. Returning `null` is treated as a cache miss - the handler returns `null` from `get()` without surfacing an error.
|
|
171
|
+
- Both methods may be async, enabling non-blocking codecs such as `zlib.brotliCompress` or `crypto.subtle` that don't block the Node.js event loop. Synchronous implementations continue to work unchanged.
|
|
172
|
+
- Only the main cache-entry storage path is routed through the serializer. Internal structures (`__sharedTags__`, `__revalidated_tags__`, `inMemoryDeduplicationCache`) are not affected.
|
|
173
|
+
|
|
174
|
+
### Default export for reuse
|
|
175
|
+
|
|
176
|
+
The default serializer is exported so you can wrap it (e.g. compress + JSON
|
|
177
|
+
fallback) or compare against it by reference to detect that no custom
|
|
178
|
+
serializer was configured. The underlying `Buffer` / `Map` JSON helpers used by
|
|
179
|
+
the default are also exported for use inside custom codecs:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import {
|
|
183
|
+
jsonCacheValueSerializer,
|
|
184
|
+
bufferAndMapReplacer,
|
|
185
|
+
bufferAndMapReviver,
|
|
186
|
+
} from '@trieb.work/nextjs-turbo-redis-cache';
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
> **Important:** a plain `JSON.stringify(value)` does not preserve native
|
|
190
|
+
> `Buffer` or `Map` values inside a cache entry. RSC payloads contain
|
|
191
|
+
> `Buffer`s. If you write a custom codec that doesn't use the exported
|
|
192
|
+
> `bufferAndMapReplacer` / `bufferAndMapReviver` (or doesn't wrap
|
|
193
|
+
> `jsonCacheValueSerializer`), expect those to come back as plain objects.
|
|
194
|
+
|
|
195
|
+
### Example: gzip (sync)
|
|
196
|
+
|
|
197
|
+
Wraps `bufferAndMapReplacer` / `bufferAndMapReviver` so native `Buffer` and
|
|
198
|
+
`Map` values inside the cache entry round-trip unchanged. `gzipSync` /
|
|
199
|
+
`gunzipSync` block the event loop - prefer the async brotli example below for
|
|
200
|
+
hot workloads.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
204
|
+
import {
|
|
205
|
+
RedisStringsHandler,
|
|
206
|
+
bufferAndMapReplacer,
|
|
207
|
+
bufferAndMapReviver,
|
|
208
|
+
} from '@trieb.work/nextjs-turbo-redis-cache';
|
|
209
|
+
|
|
210
|
+
const gzipSerializer = {
|
|
211
|
+
serialize(value) {
|
|
212
|
+
const json = JSON.stringify(value, bufferAndMapReplacer);
|
|
213
|
+
return gzipSync(json).toString('base64');
|
|
214
|
+
},
|
|
215
|
+
deserialize(stored) {
|
|
216
|
+
const buf = Buffer.from(stored, 'base64');
|
|
217
|
+
return JSON.parse(gunzipSync(buf).toString('utf8'), bufferAndMapReviver);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export default class CustomizedCacheHandler {
|
|
222
|
+
constructor() {
|
|
223
|
+
this.handler = new RedisStringsHandler({
|
|
224
|
+
valueSerializer: gzipSerializer,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// ... delegate get/set/revalidateTag/resetRequestCache to this.handler
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Example: brotli (async, non-blocking)
|
|
232
|
+
|
|
233
|
+
Uses `promisify(brotliCompress)` and `promisify(brotliDecompress)` so
|
|
234
|
+
compression runs on a worker thread and doesn't block the event loop.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { promisify } from 'node:util';
|
|
238
|
+
import { brotliCompress, brotliDecompress } from 'node:zlib';
|
|
239
|
+
import {
|
|
240
|
+
RedisStringsHandler,
|
|
241
|
+
bufferAndMapReplacer,
|
|
242
|
+
bufferAndMapReviver,
|
|
243
|
+
} from '@trieb.work/nextjs-turbo-redis-cache';
|
|
244
|
+
|
|
245
|
+
const brotliCompressAsync = promisify(brotliCompress);
|
|
246
|
+
const brotliDecompressAsync = promisify(brotliDecompress);
|
|
247
|
+
|
|
248
|
+
const brotliSerializer = {
|
|
249
|
+
async serialize(value) {
|
|
250
|
+
const json = JSON.stringify(value, bufferAndMapReplacer);
|
|
251
|
+
const compressed = await brotliCompressAsync(Buffer.from(json, 'utf8'));
|
|
252
|
+
return compressed.toString('base64');
|
|
253
|
+
},
|
|
254
|
+
async deserialize(stored) {
|
|
255
|
+
const buf = Buffer.from(stored, 'base64');
|
|
256
|
+
const decompressed = await brotliDecompressAsync(buf);
|
|
257
|
+
return JSON.parse(decompressed.toString('utf8'), bufferAndMapReviver);
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export default class CustomizedCacheHandler {
|
|
262
|
+
constructor() {
|
|
263
|
+
this.handler = new RedisStringsHandler({
|
|
264
|
+
valueSerializer: brotliSerializer,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// ... delegate get/set/revalidateTag/resetRequestCache to this.handler
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Operational notes
|
|
272
|
+
|
|
273
|
+
- **Changing the serializer makes existing Redis keys unreadable.** Any change
|
|
274
|
+
to the codec - swapping JSON for gzip, bumping a compression level, rotating
|
|
275
|
+
an encryption key - means previously written entries can no longer be
|
|
276
|
+
decoded. Either flush the affected keys (`FLUSHDB`, or scoped `UNLINK` of
|
|
277
|
+
`keyPrefix*`) or bump `keyPrefix` before deploying so old and new entries
|
|
278
|
+
live in disjoint keyspaces.
|
|
279
|
+
- **`Buffer` and `Map` encoding is built into the default.** The default
|
|
280
|
+
`jsonCacheValueSerializer` uses this package's `bufferAndMapReplacer` /
|
|
281
|
+
`bufferAndMapReviver` so native `Buffer` and `Map` values inside a
|
|
282
|
+
`CacheEntry` round-trip transparently. If you write a custom serializer that
|
|
283
|
+
doesn't reuse those (e.g. plain `JSON.stringify` over a binary payload),
|
|
284
|
+
expect RSC payload `Buffer`s to come back as plain `{ type: 'Buffer', data:
|
|
285
|
+
[...] }` objects. Reuse the exported default inside your codec, or use the
|
|
286
|
+
exported `bufferAndMapReplacer` / `bufferAndMapReviver`, to keep that
|
|
287
|
+
behavior.
|
|
288
|
+
- **The in-memory deduplication cache stores the wire-format string verbatim.**
|
|
289
|
+
When `redisGetDeduplication` is enabled (default), the value seeded after
|
|
290
|
+
`set()` and returned to subsequent `get()` calls is the exact string
|
|
291
|
+
produced by `serialize()`. With a compressing or encrypting codec that
|
|
292
|
+
means every dedup hit re-runs `deserialize()` (i.e. re-decompresses or
|
|
293
|
+
re-decrypts). For very hot keys, evaluate whether the per-hit codec cost
|
|
294
|
+
outweighs the Redis round-trip the dedup is saving.
|
|
295
|
+
- **Other internal trieb structures are not affected by `valueSerializer`.**
|
|
296
|
+
Only the main cache entries written by `set()` and read by `get()` go
|
|
297
|
+
through the codec. The shared-tags map and the revalidated-tags map are
|
|
298
|
+
untouched.
|
|
299
|
+
- **Cache Components handler is out of scope for now.** This option only
|
|
300
|
+
affects `RedisStringsHandler`. The Next.js 16+ `CacheComponentsHandler` does
|
|
301
|
+
not currently route through `valueSerializer`; that's a candidate follow-up.
|
|
302
|
+
|
|
156
303
|
|
|
157
304
|
## TLS Configuration
|
|
158
305
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
import { RedisClientOptions } from 'redis';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Pluggable wire-format codec for Redis string values used by `RedisStringsHandler`.
|
|
5
|
+
*
|
|
6
|
+
* The serializer is the single point at which an in-memory `CacheEntry` becomes the
|
|
7
|
+
* string written to Redis (and back). Plugging in a custom serializer lets you add
|
|
8
|
+
* compression (gzip/brotli), encryption, or any other custom encoding without forking
|
|
9
|
+
* this package or losing the existing dedup / batch / keyspace features.
|
|
10
|
+
*
|
|
11
|
+
* Both `serialize` and `deserialize` may return either a value directly or a `Promise`,
|
|
12
|
+
* which enables non-blocking async codecs such as stream-based compression
|
|
13
|
+
* (`zlib.brotliCompress`) or encryption (`crypto.subtle`). Synchronous implementations
|
|
14
|
+
* continue to work unchanged - awaiting a plain value is a no-op.
|
|
15
|
+
*
|
|
16
|
+
* The default export {@link jsonCacheValueSerializer} is `JSON.stringify` /
|
|
17
|
+
* `JSON.parse` paired with {@link bufferAndMapReplacer} / {@link bufferAndMapReviver},
|
|
18
|
+
* so native `Buffer` and `Map` values inside a `CacheEntry` round-trip transparently
|
|
19
|
+
* (this is required for Next.js RSC payloads).
|
|
20
|
+
*
|
|
21
|
+
* Operational note: changing the serializer (or any of its parameters such as a
|
|
22
|
+
* compression level or encryption key) makes existing Redis keys unreadable, because
|
|
23
|
+
* the deserializer will fail or return `null` for entries written by the previous
|
|
24
|
+
* format. Either flush the affected keys, bump `keyPrefix`, or migrate values
|
|
25
|
+
* out-of-band before deploying a new serializer.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type CacheValueSerializer = {
|
|
29
|
+
/**
|
|
30
|
+
* Encode an in-memory `CacheEntry` into the string written to Redis.
|
|
31
|
+
* May return a `Promise` for async codecs (e.g. compression, encryption).
|
|
32
|
+
*/
|
|
33
|
+
serialize(value: CacheEntry): string | Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Decode a string read from Redis back into a `CacheEntry`.
|
|
36
|
+
* Returning `null` (or a `Promise<null>`) signals "treat as cache miss" -
|
|
37
|
+
* the handler will return `null` from `get()` without surfacing an error.
|
|
38
|
+
* May return a `Promise` for async codecs.
|
|
39
|
+
*/
|
|
40
|
+
deserialize(stored: string): CacheEntry | null | Promise<CacheEntry | null>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Default serializer used by `RedisStringsHandler` when no `valueSerializer` is
|
|
44
|
+
* configured. Wraps `JSON.stringify` / `JSON.parse` with this package's
|
|
45
|
+
* {@link bufferAndMapReplacer} / {@link bufferAndMapReviver} so native `Buffer`
|
|
46
|
+
* and `Map` values inside a `CacheEntry` survive the round-trip.
|
|
47
|
+
*
|
|
48
|
+
* Exported as a singleton so consumers can compare against the default by
|
|
49
|
+
* reference (e.g. to detect that no custom serializer was configured).
|
|
50
|
+
*/
|
|
51
|
+
declare const jsonCacheValueSerializer: CacheValueSerializer;
|
|
52
|
+
|
|
3
53
|
type CacheEntry = {
|
|
4
54
|
value: unknown;
|
|
5
55
|
lastModified: number;
|
|
@@ -66,6 +116,27 @@ type CreateRedisStringsHandlerOptions = {
|
|
|
66
116
|
* @example { username: 'user', password: 'pass' }
|
|
67
117
|
*/
|
|
68
118
|
clientOptions?: Omit<RedisClientOptions, 'url' | 'database' | 'socket'>;
|
|
119
|
+
/** Pluggable wire-format codec for Redis string values. Lets you plug in
|
|
120
|
+
* compression (gzip/brotli), encryption, or any other custom encoding without
|
|
121
|
+
* forking this package or losing the existing dedup / batch / keyspace features.
|
|
122
|
+
*
|
|
123
|
+
* Both `serialize` and `deserialize` may return a `Promise`, enabling
|
|
124
|
+
* non-blocking async codecs (e.g. `zlib.brotliCompress`) that don't block the
|
|
125
|
+
* Node.js event loop. Synchronous implementations continue to work unchanged.
|
|
126
|
+
*
|
|
127
|
+
* Only the main cache-entry storage path is routed through the serializer.
|
|
128
|
+
* The shared-tags map and the revalidated-tags map are not. The in-memory
|
|
129
|
+
* deduplication cache stores the wire-format string verbatim - its contents
|
|
130
|
+
* change with the serializer, but the cache itself is not re-encoded.
|
|
131
|
+
*
|
|
132
|
+
* Operational note: changing the serializer (or any of its parameters such as
|
|
133
|
+
* a compression level or encryption key) makes existing Redis keys unreadable.
|
|
134
|
+
* Either flush the affected keys or bump `keyPrefix` before deploying.
|
|
135
|
+
*
|
|
136
|
+
* @default jsonCacheValueSerializer (JSON.stringify with bufferAndMapReplacer
|
|
137
|
+
* so native Buffer and Map values inside a CacheEntry round-trip transparently)
|
|
138
|
+
*/
|
|
139
|
+
valueSerializer?: CacheValueSerializer;
|
|
69
140
|
};
|
|
70
141
|
declare class RedisStringsHandler {
|
|
71
142
|
private client;
|
|
@@ -82,7 +153,8 @@ declare class RedisStringsHandler {
|
|
|
82
153
|
private defaultStaleAge;
|
|
83
154
|
private estimateExpireAge;
|
|
84
155
|
private killContainerOnErrorThreshold;
|
|
85
|
-
|
|
156
|
+
private valueSerializer;
|
|
157
|
+
constructor({ redisUrl, database, keyPrefix, sharedTagsKey, getTimeoutMs, revalidateTagQuerySize, avgResyncIntervalMs, redisGetDeduplication, inMemoryCachingTime, defaultStaleAge, estimateExpireAge, killContainerOnErrorThreshold, socketOptions, clientOptions, valueSerializer, }: CreateRedisStringsHandlerOptions);
|
|
86
158
|
resetRequestCache(): void;
|
|
87
159
|
private clientReadyCalls;
|
|
88
160
|
private assertClientIsReady;
|
|
@@ -152,6 +224,9 @@ declare class CachedHandler {
|
|
|
152
224
|
resetRequestCache(...args: Parameters<RedisStringsHandler['resetRequestCache']>): ReturnType<RedisStringsHandler['resetRequestCache']>;
|
|
153
225
|
}
|
|
154
226
|
|
|
227
|
+
declare function bufferAndMapReviver(_: string, value: any): any;
|
|
228
|
+
declare function bufferAndMapReplacer(_: string, value: any): any;
|
|
229
|
+
|
|
155
230
|
interface CacheComponentsEntry {
|
|
156
231
|
value: ReadableStream<Uint8Array>;
|
|
157
232
|
tags: string[];
|
|
@@ -175,4 +250,4 @@ type CreateCacheComponentsHandlerOptions = CreateRedisStringsHandlerOptions & {
|
|
|
175
250
|
declare function getRedisCacheComponentsHandler(options?: CreateCacheComponentsHandlerOptions): CacheComponentsHandler;
|
|
176
251
|
declare const redisCacheHandler: CacheComponentsHandler;
|
|
177
252
|
|
|
178
|
-
export { type CreateRedisStringsHandlerOptions, RedisStringsHandler, CachedHandler as default, getRedisCacheComponentsHandler, redisCacheHandler };
|
|
253
|
+
export { type CacheValueSerializer, type CreateRedisStringsHandlerOptions, RedisStringsHandler, bufferAndMapReplacer, bufferAndMapReviver, CachedHandler as default, getRedisCacheComponentsHandler, jsonCacheValueSerializer, redisCacheHandler };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
import { RedisClientOptions } from 'redis';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Pluggable wire-format codec for Redis string values used by `RedisStringsHandler`.
|
|
5
|
+
*
|
|
6
|
+
* The serializer is the single point at which an in-memory `CacheEntry` becomes the
|
|
7
|
+
* string written to Redis (and back). Plugging in a custom serializer lets you add
|
|
8
|
+
* compression (gzip/brotli), encryption, or any other custom encoding without forking
|
|
9
|
+
* this package or losing the existing dedup / batch / keyspace features.
|
|
10
|
+
*
|
|
11
|
+
* Both `serialize` and `deserialize` may return either a value directly or a `Promise`,
|
|
12
|
+
* which enables non-blocking async codecs such as stream-based compression
|
|
13
|
+
* (`zlib.brotliCompress`) or encryption (`crypto.subtle`). Synchronous implementations
|
|
14
|
+
* continue to work unchanged - awaiting a plain value is a no-op.
|
|
15
|
+
*
|
|
16
|
+
* The default export {@link jsonCacheValueSerializer} is `JSON.stringify` /
|
|
17
|
+
* `JSON.parse` paired with {@link bufferAndMapReplacer} / {@link bufferAndMapReviver},
|
|
18
|
+
* so native `Buffer` and `Map` values inside a `CacheEntry` round-trip transparently
|
|
19
|
+
* (this is required for Next.js RSC payloads).
|
|
20
|
+
*
|
|
21
|
+
* Operational note: changing the serializer (or any of its parameters such as a
|
|
22
|
+
* compression level or encryption key) makes existing Redis keys unreadable, because
|
|
23
|
+
* the deserializer will fail or return `null` for entries written by the previous
|
|
24
|
+
* format. Either flush the affected keys, bump `keyPrefix`, or migrate values
|
|
25
|
+
* out-of-band before deploying a new serializer.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type CacheValueSerializer = {
|
|
29
|
+
/**
|
|
30
|
+
* Encode an in-memory `CacheEntry` into the string written to Redis.
|
|
31
|
+
* May return a `Promise` for async codecs (e.g. compression, encryption).
|
|
32
|
+
*/
|
|
33
|
+
serialize(value: CacheEntry): string | Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Decode a string read from Redis back into a `CacheEntry`.
|
|
36
|
+
* Returning `null` (or a `Promise<null>`) signals "treat as cache miss" -
|
|
37
|
+
* the handler will return `null` from `get()` without surfacing an error.
|
|
38
|
+
* May return a `Promise` for async codecs.
|
|
39
|
+
*/
|
|
40
|
+
deserialize(stored: string): CacheEntry | null | Promise<CacheEntry | null>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Default serializer used by `RedisStringsHandler` when no `valueSerializer` is
|
|
44
|
+
* configured. Wraps `JSON.stringify` / `JSON.parse` with this package's
|
|
45
|
+
* {@link bufferAndMapReplacer} / {@link bufferAndMapReviver} so native `Buffer`
|
|
46
|
+
* and `Map` values inside a `CacheEntry` survive the round-trip.
|
|
47
|
+
*
|
|
48
|
+
* Exported as a singleton so consumers can compare against the default by
|
|
49
|
+
* reference (e.g. to detect that no custom serializer was configured).
|
|
50
|
+
*/
|
|
51
|
+
declare const jsonCacheValueSerializer: CacheValueSerializer;
|
|
52
|
+
|
|
3
53
|
type CacheEntry = {
|
|
4
54
|
value: unknown;
|
|
5
55
|
lastModified: number;
|
|
@@ -66,6 +116,27 @@ type CreateRedisStringsHandlerOptions = {
|
|
|
66
116
|
* @example { username: 'user', password: 'pass' }
|
|
67
117
|
*/
|
|
68
118
|
clientOptions?: Omit<RedisClientOptions, 'url' | 'database' | 'socket'>;
|
|
119
|
+
/** Pluggable wire-format codec for Redis string values. Lets you plug in
|
|
120
|
+
* compression (gzip/brotli), encryption, or any other custom encoding without
|
|
121
|
+
* forking this package or losing the existing dedup / batch / keyspace features.
|
|
122
|
+
*
|
|
123
|
+
* Both `serialize` and `deserialize` may return a `Promise`, enabling
|
|
124
|
+
* non-blocking async codecs (e.g. `zlib.brotliCompress`) that don't block the
|
|
125
|
+
* Node.js event loop. Synchronous implementations continue to work unchanged.
|
|
126
|
+
*
|
|
127
|
+
* Only the main cache-entry storage path is routed through the serializer.
|
|
128
|
+
* The shared-tags map and the revalidated-tags map are not. The in-memory
|
|
129
|
+
* deduplication cache stores the wire-format string verbatim - its contents
|
|
130
|
+
* change with the serializer, but the cache itself is not re-encoded.
|
|
131
|
+
*
|
|
132
|
+
* Operational note: changing the serializer (or any of its parameters such as
|
|
133
|
+
* a compression level or encryption key) makes existing Redis keys unreadable.
|
|
134
|
+
* Either flush the affected keys or bump `keyPrefix` before deploying.
|
|
135
|
+
*
|
|
136
|
+
* @default jsonCacheValueSerializer (JSON.stringify with bufferAndMapReplacer
|
|
137
|
+
* so native Buffer and Map values inside a CacheEntry round-trip transparently)
|
|
138
|
+
*/
|
|
139
|
+
valueSerializer?: CacheValueSerializer;
|
|
69
140
|
};
|
|
70
141
|
declare class RedisStringsHandler {
|
|
71
142
|
private client;
|
|
@@ -82,7 +153,8 @@ declare class RedisStringsHandler {
|
|
|
82
153
|
private defaultStaleAge;
|
|
83
154
|
private estimateExpireAge;
|
|
84
155
|
private killContainerOnErrorThreshold;
|
|
85
|
-
|
|
156
|
+
private valueSerializer;
|
|
157
|
+
constructor({ redisUrl, database, keyPrefix, sharedTagsKey, getTimeoutMs, revalidateTagQuerySize, avgResyncIntervalMs, redisGetDeduplication, inMemoryCachingTime, defaultStaleAge, estimateExpireAge, killContainerOnErrorThreshold, socketOptions, clientOptions, valueSerializer, }: CreateRedisStringsHandlerOptions);
|
|
86
158
|
resetRequestCache(): void;
|
|
87
159
|
private clientReadyCalls;
|
|
88
160
|
private assertClientIsReady;
|
|
@@ -152,6 +224,9 @@ declare class CachedHandler {
|
|
|
152
224
|
resetRequestCache(...args: Parameters<RedisStringsHandler['resetRequestCache']>): ReturnType<RedisStringsHandler['resetRequestCache']>;
|
|
153
225
|
}
|
|
154
226
|
|
|
227
|
+
declare function bufferAndMapReviver(_: string, value: any): any;
|
|
228
|
+
declare function bufferAndMapReplacer(_: string, value: any): any;
|
|
229
|
+
|
|
155
230
|
interface CacheComponentsEntry {
|
|
156
231
|
value: ReadableStream<Uint8Array>;
|
|
157
232
|
tags: string[];
|
|
@@ -175,4 +250,4 @@ type CreateCacheComponentsHandlerOptions = CreateRedisStringsHandlerOptions & {
|
|
|
175
250
|
declare function getRedisCacheComponentsHandler(options?: CreateCacheComponentsHandlerOptions): CacheComponentsHandler;
|
|
176
251
|
declare const redisCacheHandler: CacheComponentsHandler;
|
|
177
252
|
|
|
178
|
-
export { type CreateRedisStringsHandlerOptions, RedisStringsHandler, CachedHandler as default, getRedisCacheComponentsHandler, redisCacheHandler };
|
|
253
|
+
export { type CacheValueSerializer, type CreateRedisStringsHandlerOptions, RedisStringsHandler, bufferAndMapReplacer, bufferAndMapReviver, CachedHandler as default, getRedisCacheComponentsHandler, jsonCacheValueSerializer, redisCacheHandler };
|
package/dist/index.js
CHANGED
|
@@ -31,8 +31,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
RedisStringsHandler: () => RedisStringsHandler,
|
|
34
|
+
bufferAndMapReplacer: () => bufferAndMapReplacer,
|
|
35
|
+
bufferAndMapReviver: () => bufferAndMapReviver,
|
|
34
36
|
default: () => index_default,
|
|
35
37
|
getRedisCacheComponentsHandler: () => getRedisCacheComponentsHandler,
|
|
38
|
+
jsonCacheValueSerializer: () => jsonCacheValueSerializer,
|
|
36
39
|
redisCacheHandler: () => redisCacheHandler
|
|
37
40
|
});
|
|
38
41
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -487,6 +490,16 @@ function bufferAndMapReplacer(_, value) {
|
|
|
487
490
|
return value;
|
|
488
491
|
}
|
|
489
492
|
|
|
493
|
+
// src/serializer.ts
|
|
494
|
+
var jsonCacheValueSerializer = {
|
|
495
|
+
serialize(value) {
|
|
496
|
+
return JSON.stringify(value, bufferAndMapReplacer);
|
|
497
|
+
},
|
|
498
|
+
deserialize(stored) {
|
|
499
|
+
return JSON.parse(stored, bufferAndMapReviver);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
490
503
|
// src/RedisStringsHandler.ts
|
|
491
504
|
function redisErrorHandler(debugInfo, redisCommandResult) {
|
|
492
505
|
const beforeTimestamp = performance.now();
|
|
@@ -538,7 +551,8 @@ var RedisStringsHandler = class {
|
|
|
538
551
|
estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "production" ? staleAge * 2 : staleAge * 1.2,
|
|
539
552
|
killContainerOnErrorThreshold = process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD ? Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0,
|
|
540
553
|
socketOptions,
|
|
541
|
-
clientOptions
|
|
554
|
+
clientOptions,
|
|
555
|
+
valueSerializer = jsonCacheValueSerializer
|
|
542
556
|
}) {
|
|
543
557
|
this.clientReadyCalls = 0;
|
|
544
558
|
try {
|
|
@@ -549,6 +563,7 @@ var RedisStringsHandler = class {
|
|
|
549
563
|
this.estimateExpireAge = estimateExpireAge;
|
|
550
564
|
this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
|
|
551
565
|
this.getTimeoutMs = getTimeoutMs;
|
|
566
|
+
this.valueSerializer = valueSerializer;
|
|
552
567
|
try {
|
|
553
568
|
this.client = (0, import_redis.createClient)({
|
|
554
569
|
url: redisUrl,
|
|
@@ -718,10 +733,17 @@ var RedisStringsHandler = class {
|
|
|
718
733
|
if (!serializedCacheEntry) {
|
|
719
734
|
return null;
|
|
720
735
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
)
|
|
736
|
+
let cacheEntry;
|
|
737
|
+
try {
|
|
738
|
+
cacheEntry = await this.valueSerializer.deserialize(serializedCacheEntry);
|
|
739
|
+
} catch (err) {
|
|
740
|
+
console.warn(
|
|
741
|
+
"RedisStringsHandler.get() valueSerializer.deserialize failed, treating as cache miss",
|
|
742
|
+
this.keyPrefix + key,
|
|
743
|
+
err
|
|
744
|
+
);
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
725
747
|
debug(
|
|
726
748
|
"green",
|
|
727
749
|
"RedisStringsHandler.get() finished with result (cacheEntry)",
|
|
@@ -831,10 +853,7 @@ var RedisStringsHandler = class {
|
|
|
831
853
|
tags: ctx?.tags || [],
|
|
832
854
|
value: data
|
|
833
855
|
};
|
|
834
|
-
const serializedCacheEntry =
|
|
835
|
-
cacheEntry,
|
|
836
|
-
bufferAndMapReplacer
|
|
837
|
-
);
|
|
856
|
+
const serializedCacheEntry = await this.valueSerializer.serialize(cacheEntry);
|
|
838
857
|
if (this.redisGetDeduplication) {
|
|
839
858
|
this.redisDeduplicationHandler.seedRequestReturn(
|
|
840
859
|
key,
|
|
@@ -1370,7 +1389,10 @@ var index_default = CachedHandler;
|
|
|
1370
1389
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1371
1390
|
0 && (module.exports = {
|
|
1372
1391
|
RedisStringsHandler,
|
|
1392
|
+
bufferAndMapReplacer,
|
|
1393
|
+
bufferAndMapReviver,
|
|
1373
1394
|
getRedisCacheComponentsHandler,
|
|
1395
|
+
jsonCacheValueSerializer,
|
|
1374
1396
|
redisCacheHandler
|
|
1375
1397
|
});
|
|
1376
1398
|
//# sourceMappingURL=index.js.map
|