@@ -136,7 +136,7 @@ organization, or session IDs.
136136
137137| Option | Type | Default | Description |
138138| --------| ------| ---------| -------------|
139- | ` cacheHub ` | ` ResponseCacheHubOptions ` | ` {} ` | Options passed to the internal ` cache-hub ` ` MemoryCache ` . ` defaultTtl ` is not exposed because response TTL is controlled by ` ttl ` . |
139+ | ` cacheHub ` | ` ResponseCacheHubOptions ` | ` {} ` | Options for the internal ` cache-hub ` store. Omit ` mode ` for Memory, or set ` mode: "redis" ` / ` "multi-level" ` explicitly . ` defaultTtl ` is not exposed because response TTL is controlled by ` ttl ` . |
140140| ` ttl ` | ` number ` | ` 60000 ` | Cache TTL in milliseconds. ` ttl <= 0 ` bypasses caching. |
141141| ` namespace ` | ` string ` | ` "response-cache" ` | Prefix used when building cache keys. Useful when sharing one store across modules. |
142142| ` vary ` | ` readonly string[] \| "*" ` | ` [] ` | Header names that separate cache entries when those headers change the response. Use ` "*" ` only when every request header intentionally participates in the key. |
@@ -167,7 +167,7 @@ const cache = createResponseCache({
167167});
168168```
169169
170- ` cacheHub ` fields:
170+ Memory ` cacheHub ` fields:
171171
172172| Field | Type | Default from ` cache-hub ` | Description |
173173| -------| ------| --------------------------| -------------|
@@ -181,6 +181,100 @@ const cache = createResponseCache({
181181to ` response-cache-kit ` ; tag indexes are enabled internally so response tags and
182182` invalidateTag() ` work without user store wiring.
183183
184+ Redis and multi-level modes are opt-in. They still use ` cache-hub ` ; there is no
185+ custom external store injection.
186+
187+ ``` typescript
188+ const redisCache = createResponseCache ({
189+ ttl: 10_000 ,
190+ namespace: " api" ,
191+ cacheHub: {
192+ mode: " redis" ,
193+ url: " redis://localhost:6379" ,
194+ metaKeyPrefix: " api:response-cache" ,
195+ scanCount: 200 ,
196+ deleteCommand: " unlink" ,
197+ lease: {
198+ ttl: 1_000 ,
199+ waitForOwner: 1_200 ,
200+ pollInterval: 10 ,
201+ onTimeout: " fetch" ,
202+ },
203+ },
204+ });
205+
206+ const multiLevelCache = createResponseCache ({
207+ ttl: 10_000 ,
208+ namespace: " api" ,
209+ cacheHub: {
210+ mode: " multi-level" ,
211+ memory: { maxEntries: 5000 },
212+ redis: {
213+ url: " redis://localhost:6379" ,
214+ metaKeyPrefix: " api:response-cache" ,
215+ },
216+ writePolicy: " both" ,
217+ backfillOnRemoteHit: true ,
218+ remoteTimeout: 50 ,
219+ lease: true ,
220+ },
221+ });
222+ ```
223+
224+ Redis mode fields:
225+
226+ | Field | Type | Default | Description |
227+ | -------| ------| ---------| -------------|
228+ | ` mode ` | ` "redis" ` | required | Enables the ` cache-hub ` Redis adapter. |
229+ | ` url ` | ` string ` | ` "redis://localhost:6379" ` | Redis URL used when no ` client ` is provided. URL mode uses cache-hub's optional ` ioredis ` peer. |
230+ | ` client ` | ` object ` | none | Existing Redis-like client. Lifecycle remains owned by the caller. |
231+ | ` metaKeyPrefix ` | ` string ` | cache-hub default | Prefix for tag metadata keys. |
232+ | ` scanCount ` | ` number ` | cache-hub default | SCAN batch size for pattern/tag operations. |
233+ | ` deleteCommand ` | ` "del" \| "unlink" ` | ` "del" ` | Redis delete command used by cache-hub. |
234+ | ` lease ` | ` boolean \| ResponseCacheHubLeaseOptions ` | ` false ` | Enables Redis-backed cross-process refresh coordination. |
235+ | ` distributed ` | ` boolean \| ResponseCacheHubDistributedOptions ` | ` false ` | Enables cache-hub distributed tag invalidation. |
236+
237+ Multi-level mode fields:
238+
239+ | Field | Type | Default | Description |
240+ | -------| ------| ---------| -------------|
241+ | ` mode ` | ` "multi-level" ` | required | Uses local Memory as L1 and Redis as L2. |
242+ | ` memory ` | ` ResponseCacheHubMemoryOptions ` | ` {} ` | L1 Memory options. |
243+ | ` redis ` | ` ResponseCacheHubRedisTargetOptions ` | ` {} ` | L2 Redis target and metadata options. |
244+ | ` writePolicy ` | ` "both" \| "local-first-async-remote" ` | cache-hub default | Write-through behavior. |
245+ | ` backfillOnRemoteHit ` | ` boolean ` | cache-hub default | Whether L2 hits should refill L1. |
246+ | ` remoteTimeout ` | ` number ` | cache-hub default | L2 read timeout in milliseconds. |
247+ | ` remoteInvalidationErrors ` | ` "ignore" \| "throw" ` | cache-hub default | Whether L2 tag invalidation failures throw. |
248+ | ` lease ` | ` boolean \| ResponseCacheHubLeaseOptions ` | ` false ` | Uses the Redis layer for cross-process lease coordination. |
249+ | ` distributed ` | ` boolean \| ResponseCacheHubDistributedOptions ` | ` false ` | Broadcasts tag invalidation across instances. |
250+
251+ Lease fields:
252+
253+ | Field | Type | Default | Description |
254+ | -------| ------| ---------| -------------|
255+ | ` enabled ` | ` boolean ` | ` true ` when object/` true ` is provided | Set ` false ` to disable a prepared lease config. |
256+ | ` ttl ` | ` number ` | derived from response TTL, capped for refresh windows | Lease TTL in milliseconds. |
257+ | ` waitForOwner ` | ` number ` | ` leaseTtl + 25 ` | How long a non-owner waits for the owner to fill cache. |
258+ | ` pollInterval ` | ` number ` | ` 10 ` | Cache polling interval while waiting. |
259+ | ` onTimeout ` | ` "fetch" \| "throw" ` | ` "fetch" ` | Whether to run origin or throw when no owner fills cache in time. |
260+ | ` keyPrefix ` | ` string ` | cache-hub default | Redis lease key prefix. |
261+ | ` ownerId ` | ` string ` | generated by cache-hub | Stable owner prefix for lease tokens. |
262+
263+ Distributed fields:
264+
265+ | Field | Type | Default | Description |
266+ | -------| ------| ---------| -------------|
267+ | ` enabled ` | ` boolean ` | ` true ` when object/` true ` is provided | Set ` false ` to disable a prepared distributed config. |
268+ | ` redisUrl ` | ` string ` | cache-hub default | Redis URL for pub/sub when no ` redis ` object is provided. |
269+ | ` redis ` | ` object ` | none | Existing Redis-like pub connection. |
270+ | ` channel ` | ` string ` | cache-hub default | Pub/sub invalidation channel. |
271+ | ` instanceId ` | ` string ` | generated by cache-hub | Instance identifier used to ignore self messages. |
272+
273+ When Redis URL mode or distributed invalidation is enabled, the consuming
274+ application must satisfy cache-hub's optional Redis peer. Default Memory mode
275+ does not load or require Redis. Call ` await cache.close?.() ` during application
276+ shutdown when using Redis, MultiLevel, or distributed invalidation.
277+
184278### Cache Lifetime: ` ttl `
185279
186280` ttl ` controls how long a stored response stays fresh. Use shorter values for
@@ -279,8 +373,8 @@ designed.
279373
280374### Internal cache-hub Store: ` cacheHub `
281375
282- ` cacheHub ` configures the internal ` cache-hub ` memory store. It does not accept
283- an external store instance.
376+ ` cacheHub ` configures the internal ` cache-hub ` store. It does not accept an
377+ external store instance.
284378
285379- ` maxEntries ` : cap the number of cached responses.
286380- ` maxMemory ` : approximate memory limit in bytes; ` 0 ` means no explicit limit.
@@ -290,6 +384,12 @@ an external store instance.
290384 underlying store is disabled, so requests still run through ` cache.handle() `
291385 but cannot build useful hits. Use it for local debugging or temporary cache
292386 shutdowns, not as a long-term production setting.
387+ - ` mode: "redis" ` : stores response snapshots in cache-hub's Redis adapter.
388+ - ` mode: "multi-level" ` : uses cache-hub's L1 Memory + L2 Redis cache.
389+ - ` lease ` : optional Redis-backed cross-process refresh coordination. Same-process
390+ single-flight remains enabled in every mode.
391+ - ` distributed ` : optional cache-hub Pub/Sub invalidation for tags across
392+ instances.
293393
294394### Advanced Key Builder: ` keyBuilder `
295395
@@ -398,7 +498,10 @@ Builds the cache key without reading or writing the store.
398498
399499### ` cache.clear() `
400500
401- Clears the underlying ` cache-hub ` store.
501+ Clears entries written by the current response cache namespace. Stored snapshots
502+ always include an internal namespace tag, so Redis and MultiLevel modes use tag
503+ invalidation instead of ` flushdb ` . If a future store has no tag invalidation
504+ helper, ` clear() ` falls back to the store's own ` clear() ` .
402505
403506### ` cache.delete(key) `
404507
@@ -420,11 +523,21 @@ Returns `cache-hub` statistics such as `entries`, `hits`, `misses`, `hitRate`,
420523Returns the remaining TTL in milliseconds, ` null ` for a non-expiring key, or
421524` undefined ` when the key is missing or TTL lookup is unavailable.
422525
526+ ### ` cache.close?() `
527+
528+ Optional lifecycle hook. Use it during application shutdown when Redis,
529+ MultiLevel, or distributed invalidation is enabled. Memory mode also exposes it,
530+ but existing mocks do not need to implement it because the public contract keeps
531+ ` close ` optional.
532+
423533### ` createResponseCacheHeaders(result, options?) `
424534
425535Creates response cache headers from metadata. By default it emits `X-Cache:
426536HIT` for hits and ` X-Cache: MISS` for miss-like states. Set
427- ` cacheControl: true ` to also emit ` Cache-Control: public,max-age=N ` .
537+ ` cacheControl: true ` to also emit ` Cache-Control: public,max-age=N ` only when
538+ the result was actually stored in response cache. Responses skipped because of
539+ ` Set-Cookie ` , ` private ` , ` no-store ` , TTL bypass, or cache write failure do not
540+ receive a new public ` Cache-Control ` header from this helper.
428541
429542### Adapter helpers
430543
@@ -649,6 +762,8 @@ async function runVextRoute(req, res, route) {
649762- Skips ` Authorization ` requests unless a ` partitionKey ` is provided.
650763- Filters hop-by-hop headers from cached snapshots.
651764- Uses same-key single-flight protection for concurrent refreshes.
765+ - Can use Redis lease coordination for cross-process same-key refreshes when
766+ ` cacheHub.mode ` is ` "redis" ` or ` "multi-level" ` .
652767- Enables cache-hub tag indexes internally.
653768
654769## Concurrent Expiry Protection
@@ -657,6 +772,20 @@ If a cached response has `ttl: 2_000` and 10000 identical requests arrive after
657772it expires, only one request refreshes the origin. The other requests wait for
658773the same in-flight refresh and return the same updated response snapshot.
659774
775+ That default protection is per cache instance. In multi-process deployments,
776+ enable Redis lease coordination to reduce cross-process refresh storms:
777+
778+ ``` typescript
779+ createResponseCache ({
780+ ttl: 2_000 ,
781+ cacheHub: {
782+ mode: " redis" ,
783+ url: " redis://localhost:6379" ,
784+ lease: { waitForOwner: 1_000 , onTimeout: " fetch" },
785+ },
786+ });
787+ ```
788+
660789Different keys are independent. Failed origin refreshes are not cached, and a
661790later request can retry.
662791
@@ -750,9 +879,18 @@ temporary cache shutdowns, not as a long-term production setting.
750879
751880### Can I use Redis or multi-level cache?
752881
753- ` cache-hub ` is the only runtime caching dependency. Redis and multi-level cache
754- support should be designed as a separate cache-hub integration batch, not as an
755- external store option in ` response-cache-kit ` .
882+ Yes. Use ` cacheHub.mode: "redis" ` or ` cacheHub.mode: "multi-level" ` . The
883+ underlying implementation is still cache-hub; ` response-cache-kit ` does not
884+ accept a custom external store. Redis URL mode and distributed invalidation rely
885+ on cache-hub's optional Redis peer, while default Memory mode does not require
886+ Redis.
887+
888+ ### Why did Redis mode say ioredis is missing?
889+
890+ ` response-cache-kit ` only has ` cache-hub ` as a runtime dependency. Redis support
891+ is provided by cache-hub's Redis adapter, whose Redis client is optional. Install
892+ the Redis peer in the consuming application when you use URL mode or distributed
893+ invalidation, or pass an existing Redis-like ` client ` .
756894
757895### Why is ` npm run benchmark ` heavier than unit tests?
758896
0 commit comments