@0xdoublesharp/adaptive-cache
v0.1.1
Published
Adaptive caching module for Redis with Fastify and Express integration
Downloads
385
Maintainers
Readme
@0xdoublesharp/adaptive-cache
Server-side response caching for APIs that need fast reads without hand-tuning every TTL.
@0xdoublesharp/adaptive-cache caches Express, Fastify, and direct AdaptiveCache results. It hashes response bodies, tracks whether content changes, and automatically grows TTLs for stable data while resetting TTLs for volatile data.
Redis remains the default authoritative backend through Lua scripts. To keep hot payloads in memory, add a bounded clustered LRU cache in front of Redis. L1 hits still validate invalidation state against Redis. For single-server or one-host Node cluster deployments, the clustered LRU backend can run without Redis.
See loading, freshness, and cache policy for getOrSet, stale refresh, negative caching, typed/bulk APIs, write fencing, selective compression, events, and HTTP identity controls.
The feature guide and examples below describe version 0.1.1. See the 0.1.1 release notes for batching and dependency fixes, and 0.1.0 release notes for new APIs and behavior since 0.0.6; see release history and upgrade instructions before deploying them.
This package does not generate HTTP Cache-Control headers. It honors storage restrictions and replays eligible cached headers. It caches server responses and adds diagnostic X-Cache-* headers unless disabled.
Use It For
| Need | What the module does |
| ---------------------------- | ------------------------------------------------------------------------------------ |
| API responses that stabilize | Increases TTL when response hashes stay the same. |
| API responses that change | Resets TTL when content changes, so volatile data does not stay stale for long. |
| Cross-host correctness | Uses Redis Lua as the atomic authority for payloads, metadata, tags, and locks. |
| Hot payloads in memory | Keeps hot payloads in bounded L1 memory with a Redis validation round trip. |
| No-Redis deployments | Provides a volatile clustered-lru backend for single-server or one-host clusters. |
| Expensive refreshes | Coalesces getOrSet loaders and fences Redis writes after lease loss. |
| Targeted invalidation | Invalidates keys, tags, or namespaces and rejects obsolete Redis writes. |
| Memory safety | Sets L1 size budgets and skips entries that are too large for the configured budget. |
Use getOrSet for expensive work that needs coalescing, stale refresh, or negative caching. Use ordinary middleware when a route only needs response caching. Stale background refresh requires an explicit loader.
Architecture
Feature Matrix
| Capability | redis | l1-redis | clustered-lru |
| ------------------------------ | :-----: | :--------: | :-------------: |
| Adaptive TTL growth/reset | Yes | Yes | Yes |
| Redis Lua atomic metadata | Yes | Yes | No |
| Bounded L1 hot reads | No | Yes | Yes |
| Cross-host cache state | Yes | Yes | No |
| Tag invalidation | Yes | Yes | Yes |
| Refresh locks | Yes | Yes | Host-local |
| Works without Redis | No | No | Yes |
| Recommended production default | Yes | Hot APIs | Single host |
All built-in backends support typed/bulk APIs, stale windows, negative caching, selective compression, and events. Redis-backed modes provide atomic loader lease and write-token checks. Clustered LRU provides host-local coordination with weaker write/release guarantees.
Installation
Requires Node.js 22 or newer.
Redis-only usage:
pnpm add @0xdoublesharp/adaptive-cacheExpress or Fastify users should also install the framework they use:
pnpm add express
pnpm add fastifyLRU-backed modes require optional peer dependencies:
pnpm add @0xdoublesharp/lru-cache-clustered@^2.1.1 lru-cache@^11Redis-only users do not need the LRU packages at runtime.
Backend Modes
| Backend | Redis required | Best for | Behavior |
| --------------- | -------------- | --------------------------------------- | ------------------------------------------------------------------ |
| redis | Yes | Default production mode | Redis Lua stores data, metadata, tags, and locks. |
| l1-redis | Yes | Production hot paths | Clustered LRU serves hot reads; Redis Lua remains authoritative. |
| clustered-lru | No | Single-server or one-host Node clusters | Volatile bounded cache with adaptive TTL logic in process/cluster. |
| custom backend | Depends | Advanced integrations | Provide the AdaptiveCacheBackend interface. |
redis
This is the default. It uses Redis Lua scripts for:
- Fetching cached data and metadata.
- Updating cached data and adaptive metadata atomically.
- Refresh locks.
- Tag invalidation.
import { adaptiveExpressCache } from '@0xdoublesharp/adaptive-cache'
app.get('/api/summary', adaptiveExpressCache(), handler)l1-redis
Use this when Redis exists but hot reads should be served from a bounded local/clustered LRU cache.
import { adaptiveExpressCache } from '@0xdoublesharp/adaptive-cache'
app.get(
'/api/summary',
adaptiveExpressCache({
backend: 'l1-redis',
initialTTL: 5,
maxTTL: 900,
lru: {
namespace: 'api-cache',
maxSizeBytes: 64 * 1024 * 1024,
maxEntrySizeBytes: 6 * 1024 * 1024,
},
}),
handler,
)Read path:
- Read Redis invalidation barriers and check clustered LRU.
- On a valid L1 hit, return the payload without fetching or decoding it from Redis.
- On L1 miss, fetch from Redis Lua.
- On Redis hit, hydrate L1 with Redis remaining TTL and metadata.
- On Redis miss, continue to the handler.
Write path:
- Encode/compress once and hash the response.
- Store in L1 immediately with
initialTTL. - Run Redis Lua update asynchronously.
- When Redis returns, reconcile L1 with Redis' authoritative adaptive TTL and metadata.
- If Redis update fails, keep the short-lived L1 entry and log the failure.
For shutdown-sensitive workers, use l1Redis.writeMode: 'await-redis' to wait for Redis before set() resolves, or call cache.flush() before process shutdown to wait for pending async Redis writes.
Loader writes, middleware writes, and asynchronous compression wait for Redis to accept their write token. Ordinary synchronous set() calls retain the optimistic write path above. Every L1 hit validates its token against Redis, including when a Pub/Sub message was missed. This adds a Redis round trip to hot reads in exchange for stronger invalidation guarantees.
Invalidation:
- Explicit clears drain pending writes on the shared Redis client, delete Redis data, drain active L1 hydrations, remove L1 entries, then publish an invalidation message.
- Reads that overlap an invalidation cannot hydrate an older snapshot back into L1.
- Tag invalidation removes matching L1 entries and Redis entries.
- Redis update publishes L1 invalidation only when content changed.
- Other server-local L1 caches subscribe to Redis invalidation messages and delete stale keys.
- Namespace and write barriers prevent invalidated in-flight work from becoming a later cache hit.
Locks:
l1-redisuses Redis Lua locks as the distributed lock authority.
clustered-lru
Use this when Redis is not available and a volatile bounded backend is still useful.
import { adaptiveFastifyCache } from '@0xdoublesharp/adaptive-cache'
await fastify.register(
adaptiveFastifyCache({
backend: 'clustered-lru',
initialTTL: 5,
maxTTL: 300,
lru: {
namespace: 'api-cache',
maxSizeBytes: 64 * 1024 * 1024,
},
}),
)This mode keeps adaptive metadata in separate bounded LRU entries for metaTTL, so unchanged data can increase its TTL after the payload expires. It is fast and memory-bounded, but it is not cross-host durable. Treat it as process/host-local cache state.
Locks:
clustered-lruacquires locks atomically on the local Node cluster primary. Lock release is best-effort: the dependency has no atomic compare-and-delete operation, so release can race with an expired or force-replaced lock. Use a Redis-backed backend when strict refresh coordination is required.
LRU Memory Limits
l1-redis and clustered-lru store an envelope in clustered LRU:
- Encoded payload.
- Response hash.
- Adaptive metadata:
dataTTL,lastChanged,changeCount. - Created/updated timestamps.
- Expiration timestamp.
- Tags.
Defaults:
| Option | Default | Description |
| ----------------------- | --------------------- | ---------------------------------------------------- |
| lru.maxSizeBytes | 64 * 1024 * 1024 | Total LRU size budget. |
| lru.maxEntrySizeBytes | 10% of maxSizeBytes | Entries larger than this are skipped for L1 storage. |
| lru.timeout | package default | Clustered LRU operation timeout. |
| lru.failsafe | 'reject' | Clustered LRU failure behavior. |
| lru.namespace | 'adaptive-cache' | Shared cache namespace. |
Entry size is estimated as encoded payload bytes plus tag metadata and a small fixed metadata overhead. TTLs passed to clustered LRU are milliseconds; adaptive cache options use seconds.
LRU Cache Clustered v2.1 Local L1
The lru.localL1 option is passed through to @0xdoublesharp/lru-cache-clustered v2.1.1+ for per-worker local L1 caching.
adaptiveExpressCache({
backend: 'l1-redis',
lru: {
namespace: 'api-cache',
maxSizeBytes: 64 * 1024 * 1024,
localL1: {
enabled: true,
experimental: true,
ttl: 2_000,
maxSize: 4 * 1024 * 1024,
invalidation: 'broadcast',
methods: {
get: true,
has: true,
fetch: true,
memoize: true,
},
},
},
})You can also pass localL1: true when using @0xdoublesharp/lru-cache-clustered v2.1.1 or newer.
Express Usage
import express from 'express'
import { adaptiveExpressCache } from '@0xdoublesharp/adaptive-cache'
const app = express()
app.get(
'/api/items',
adaptiveExpressCache({
initialTTL: 10,
maxTTL: 3600,
ttlScaling: 1.5,
includeDebugHeaders: true,
}),
async (_req, res) => {
const items = await loadItems()
res.json(items)
},
)Dynamic maxTTL
maxTTL can be a function. Middleware resolves it after your handler returns.
app.get(
'/api/items/:id',
adaptiveExpressCache({
initialTTL: 60,
maxTTL: (data) => (data.status === 'ended' ? 86400 : 300),
}),
async (req, res) => {
res.json(await loadItem(req.params.id))
},
)Tags
Tags can be static or request-derived.
app.get(
'/api/users/:id',
adaptiveExpressCache({
tags: (req) => [`user:${req.params.id}`, 'users'],
}),
async (req, res) => {
res.json(await loadUser(req.params.id))
},
)Clear by tag:
import { getDefaultCache } from '@0xdoublesharp/adaptive-cache'
await getDefaultCache().invalidateTags(['users'])Clear one computed middleware key:
import { clearAdaptiveCache } from '@0xdoublesharp/adaptive-cache'
await clearAdaptiveCache('/api/users/123', {}, 'adaptive:')For non-default backends, pass backend options to clearAdaptiveCache:
await clearAdaptiveCache('/api/users/123', {}, 'adaptive:', {
backend: 'clustered-lru',
lru: { namespace: 'api-cache' },
})Fastify Usage
import Fastify from 'fastify'
import { adaptiveFastifyCache } from '@0xdoublesharp/adaptive-cache'
const fastify = Fastify()
await fastify.register(
adaptiveFastifyCache({
backend: 'l1-redis',
initialTTL: 10,
maxTTL: 3600,
ttlScaling: 1.5,
includeDebugHeaders: true,
lru: {
namespace: 'fastify-api',
maxSizeBytes: 64 * 1024 * 1024,
},
}),
)
fastify.get('/api/summary', async () => {
return await loadSummary()
})Direct API
Use AdaptiveCache outside Express/Fastify.
import { AdaptiveCache } from '@0xdoublesharp/adaptive-cache'
const cache = new AdaptiveCache({
backend: 'l1-redis',
includeDebugHeaders: true,
lru: { namespace: 'jobs' },
})
await cache.set('adaptive:job:summary', { total: 42 }, { tags: ['jobs'] })
const hit = await cache.get<{ total: number }>('adaptive:job:summary')
if (hit) {
console.log(hit.data, hit.ttl, hit.metadata)
}
await cache.flush()
await cache.close()Direct API keys are complete keys. keyPrefix selects coordination and invalidation state but does not prepend text to a direct key. Middleware constructs prefixed keys automatically.
Payloads are stored in a versioned envelope so direct API calls can round-trip JSON values, strings, buffers, and undefined. Middleware caches eligible successful GET responses with their status, selected headers, and original body. JSON-looking text and binary responses retain their representation on a hit. Other methods and Node/WHATWG streams bypass caching. HTTP 404 responses require an explicit negativeTTL. Existing body-only cache entries remain readable until they expire.
Coalesced loading and stale refresh
const cache = new AdaptiveCache({
keyPrefix: 'catalog:v2:',
initialTTL: 10,
maxTTL: 300,
staleWhileRevalidate: 30,
staleIfError: 120,
ttlJitter: 0.1,
compression: { minBytes: 1024, async: true, skipCompressed: true, onlyIfSmaller: true },
})
const product = await cache.getOrSet('catalog:v2:product:42', () => loadProduct('42'), {
tags: ['products'],
negativeTTL: 5,
})
const hits = await cache.getMany(['catalog:v2:product:42', 'catalog:v2:product:43'])
await cache.invalidateTags(['products'])
await cache.invalidateNamespace()
await cache.close()getMany() uses batched payload and primary invalidation checks on clustered LRU. Results and events remain per key; the call is not a transaction. See the batch-read report for measured gains and backend compatibility.
Stale windows are measured independently from freshness expiry. Plain get() returns fresh entries only; get(key, { allowStale: true }) exposes retained stale data. Negative entries have no stale window. See the feature guide for force-refresh behavior, events, fingerprints, bulk operations, and custom backend limits.
HTTP identity and response policy
Range and If-Range requests bypass caching, and partial 206 responses are not stored. Authorization and Cookie requests bypass caching unless explicitly enabled with a key or variation that isolates the caller. Responses with Cache-Control: private or no-store, Set-Cookie, Vary: *, or undeclared Vary headers are excluded. Declare varyBy: ['Accept-Encoding'] when compression middleware adds that response header.
const products = adaptiveExpressCache({
keyPrefix: 'catalog:v2:',
key: (req) => `product:${req.params?.id}`,
varyBy: ['Accept-Language'],
loader: (req) => loadProduct(req.params?.id),
staleWhileRevalidate: 30,
})
app.get('/products/:id', products)
// During shutdown, after accepting no further requests:
await products.close()Fastify accepts the same policy options and closes owned caches in its onClose hook. Both adapters accept cache to reuse a caller-owned instance. Keep that instance's namespace aligned with the middleware prefix. Use products.clear(request) to invalidate a response using the same custom key and header variations as the adapter. See HTTP identity and ownership.
Generic function caching
cacheResult is a simple Redis helper for non-adaptive function results. It uses the default Redis client directly and does not use the adaptive backend layer.
import { cacheResult } from '@0xdoublesharp/adaptive-cache'
const data = await cacheResult('expensive-report', 60, async () => {
return await buildReport()
})Refresh Locks
Use refresh locks to prevent many workers from refreshing the same expensive resource at once.
import { releaseCacheRefreshLock, shouldRefreshCache } from '@0xdoublesharp/adaptive-cache'
const result = await shouldRefreshCache('report:last-update', 60)
if (result[0] === 'UPDATE') {
const lockValue = result[1]
try {
await rebuildReport()
} finally {
await releaseCacheRefreshLock('report:last-update', lockValue)
}
}lockExpirationSeconds defaults to 60. You can set a process-wide default:
import { setDefaultLockExpirationSeconds } from '@0xdoublesharp/adaptive-cache'
setDefaultLockExpirationSeconds(120)Or pass a per-call value:
await shouldRefreshCache('report:last-update', 60, false, 120)Configuration
| Option | Type | Default | Description |
| ----------------------- | ------------------------------------------------------------------ | ------------- | ------------------------------------------------------------------------------------- |
| initialTTL | number | 5 | Starting cache duration in seconds. |
| maxTTL | number \| (data) => number \| undefined | 900 | Maximum data TTL in seconds. Middleware can resolve it from response data. |
| ttlScaling | number | 2 | Factor used to grow TTL when content is unchanged. Growth is damped by changeCount. |
| redisPrefix | string | 'adaptive:' | Prefix for Redis keys and invalidation channels. |
| keyPrefix | string | unset | Clearer alias for redisPrefix; applies to Redis and non-Redis backends. |
| includeHeaders | boolean | true | Add X-Cache and X-Cache-TTL. |
| includeDebugHeaders | boolean | false | Add adaptive metadata headers. |
| forceRefresh | boolean | false | Bypass existing cache reads and refresh after the handler response. |
| lockExpirationSeconds | number | 60 | Refresh lock expiration in seconds. |
| metaTTL | number | 604800 | Metadata/tag TTL in positive integer seconds. |
| compress | boolean | true | Gzip payloads and store them as base64. |
| logLevel | 'debug' \| 'info' \| 'warn' \| 'error' \| 'silent' | 'info' | Logger verbosity. |
| backend | 'redis' \| 'l1-redis' \| 'clustered-lru' \| AdaptiveCacheBackend | 'redis' | Cache backend. |
| lru | AdaptiveCacheLruOptions | unset | LRU options for l1-redis and clustered-lru. |
| l1Redis.writeMode | 'async' \| 'await-redis' | 'async' | Whether l1-redis returns after L1 write or waits for Redis reconciliation. |
| ignoreQueryParams | string[] | ['refresh'] | Query parameters excluded from middleware cache-key hashes. |
Additional options shared by the cache and adapters:
| Option | Default | Purpose |
| ------------------------------ | ------- | ---------------------------------------------------------------------------------------------- |
| staleWhileRevalidate | 0 | Seconds after freshness expiry during which a loader can refresh in the background. |
| staleIfError | 0 | Independent seconds after freshness expiry during which a failed loader may return stale data. |
| negativeTTL | 0 | Opt-in TTL for null/undefined loader results or normal-handler HTTP 404 responses. |
| lockWaitTimeoutMs | 5000 | Maximum loader lock wait in milliseconds. |
| ttlJitter | 0 | Fraction from 0 to 1 by which to randomly shorten freshness. |
| fingerprint / ignoreFields | unset | Custom content fingerprint or dotted fields excluded from the hash. |
| compression | unset | minBytes, async, skipCompressed, and onlyIfSmaller selective codec settings. |
| onEvent | unset | Observer for cache activity; also available through cache.onEvent(). |
HTTP-only options are cache, loader, key, varyBy, shouldCache, cacheAuthenticated, cacheCookies, and request-derived tags. Defaults bypass authenticated/cookie requests and use path/query keys. See the feature guide for examples and per-call overrides.
Headers
| Header | Meaning |
| ----------------------- | ---------------------------------------------------------------------- |
| X-Cache: HIT | Cached response returned. |
| X-Cache: MISS | No cache entry; handler ran. |
| X-Cache: BYPASS | Cache read skipped because forceRefresh or ?refresh=true was used. |
| X-Cache: RETRY | Cache read/decode failed; handler ran without failing the request. |
| X-Cache-TTL | Remaining data TTL in seconds. |
| X-Cache-Data-TTL | Debug: adaptive TTL assigned to the data version. |
| X-Cache-Last-Modified | Debug: Unix timestamp when content last changed, or unknown. |
| X-Cache-Refreshed | Debug: content change count. |
Redis Keys and Lua Metadata
Given an adaptive key, this package uses:
${key}data: versioned Redis envelope containing encoded payload, freshness, namespace generation, tags, and tag epochs.${key}data:state: auxiliary freshness and tag membership state.${key}meta: adaptive metadata hash.${redisPrefix}tag:${tag}: Redis set of keys for tag invalidation.${redisPrefix}l1:invalidate: Redis Pub/Sub channel for L1 invalidations.${lastUpdateKey}-lock: refresh lock key.${redisPrefix}__adaptive:namespace,${redisPrefix}__adaptive:barrier, and${redisPrefix}__adaptive:tag-epoch:${tag}: invalidation control keys.__adaptive:legacy-barrier: shared barrier for clears without namespace information.
Lua fetch returns:
[data, remainingTTL, dataTTL, lastChanged, changeCount, hash, staleFlag, staleAgeMs]Lua update returns:
['CACHED', dataTTL, lastChanged, changeCount, hash, changedFlag]Fetch preserves the original leading fields and appends stale state. The public hit reports staleAge in seconds. A rejected write returns ['INVALIDATED', 0]. These internal Lua results do not imply wire-format compatibility with older readers; see the upgrade notes.
TTL policies
The default policy preserves the existing TTL growth/reset behavior. Opt into additive increase/multiplicative decrease with ttlPolicy: { type: 'aimd' }, or time-aware smoothing with ttlPolicy: { type: 'ewma' }. Both are available on all built-in backends. See TTL policies for parameters, formulas, and observation limits.
Default adaptive TTL formula
On changed content:
changeCount += 1dataTTL = min(initialTTL, maxTTL)lastChanged = now
On unchanged content:
- If
dataTTL >= maxTTL, keepmaxTTL. - Otherwise compute a damped increase:
decayFactor = 1.0 - min(0.9, changeCount * 0.01)
increaseFactor = max(0, ttlScaling - 1)
increase = ceil(floor(dataTTL * increaseFactor) * decayFactor)
dataTTL = max(initialTTL, min(dataTTL + increase, maxTTL))After either path, clamp dataTTL to maxTTL. Optional jitter shortens freshness; stale retention extends physical storage without extending the fresh TTL.
Environment Variables
| Variable | Description |
| ------------------------------- | ----------------------------------------------------------------- |
| REDIS_TLS_URL | Preferred Redis TLS connection string. |
| REDIS_URL | Redis connection string. Used when REDIS_TLS_URL is not set. |
| REDIS_HOST | Redis host fallback. Defaults to localhost. |
| REDIS_PORT | Redis port fallback. Defaults to 6379. |
| REDIS_TLS_REJECT_UNAUTHORIZED | Set to false to disable Redis TLS certificate verification. |
| REDIS_INSECURE_TLS | Set to true to disable Redis TLS certificate verification. |
| CACHE_TIME | Default duration string for cache(), for example "5 seconds". |
TLS options are applied only for REDIS_TLS_URL or rediss:// URLs. Certificate verification is enabled by default.
Custom Backends
Advanced users can provide a backend object instead of a backend name.
import type { AdaptiveCacheBackend } from '@0xdoublesharp/adaptive-cache'
const backend: AdaptiveCacheBackend = {
name: 'custom',
async fetch(input) {
return null
},
async update(input) {
return ['CACHED', input.initialTTL]
},
async clear(key, dataKey, metaKey) {},
async invalidateTags(tags, redisPrefix) {
return []
},
async shouldRefresh(lastUpdateKey, refreshThreshold, currentTime, force, lockExpirationSeconds, lockValue) {
return ['UPDATE', lockValue]
},
async releaseLock(lastUpdateKey, currentTime, lockValue) {
return ['UPDATED']
},
async flush() {},
}Custom backends can implement captureWriteToken, invalidateNamespace, and onEvent. getOrSet requires write-token support, and supportsWriteFencing() reports whether that method exists. It does not certify distributed atomicity: the backend must enforce tokens and leases during writes. A backend without namespace invalidation fails explicitly when that operation is requested.
Operational Notes
- Redis is the cross-host authority in
redisandl1-redismodes. Persistence depends on the Redis deployment configuration. clustered-lruis volatile and local to the host/Node cluster.- L1 entries are intentionally short-lived until Redis Lua returns the authoritative adaptive TTL.
- Oversized entries are skipped for L1 storage rather than risking unbounded server memory growth.
l1-redisinvalidation uses Redis Pub/Sub, so processes must share the same Redis and prefix to invalidate each other.?refresh=truebypasses reads but is ignored when computing the cache key, so refresh requests update the normal key.- Explicit
clear(key)removes data, metadata, and auxiliary state and cleans recorded tag memberships. Namespace invalidation advances a generation without scanning payloads; old payloads expire normally. - Call
cache.flush()before shutdown if you use default asyncl1-rediswrites and need to wait for pending Redis reconciliation. redisPrefixstill works; preferkeyPrefixfor new code when you want a backend-neutral name.
Development
Dependency resolution uses a seven-day minimum release age, configured as minimumReleaseAge: 10080 in pnpm-workspace.yaml. Do not add release-age exclusions. Run pnpm update --latest to select eligible updates, then check peer compatibility and run the quality gate. The current toolchain keeps TypeScript at 6.0.3 because the installed ESLint parser declares support below 6.1.
Implementation files follow responsibility boundaries: AdaptiveCache.ts owns the public API and lifecycle; CacheLoader.ts handles coalescing, freshness windows, and loader locks; payload.ts handles fingerprints and codecs. TTL policy validation and calculations have dedicated modules. HTTP adapters use shared policy/response helpers, and backends/lruEnvelope.ts contains LRU TTL and envelope calculations. Tests are grouped by backend, loader behavior, response policy, and lifecycle, with fixtures under test/helpers.
Keep new behavior in the module that owns it. Split files when they accumulate distinct responsibilities, and preserve behavioral tests when moving code.
Every Vitest test must contain an assertion. Prefer stored state and real HTTP responses over log-only or mock-only checks.
pnpm install
pnpm run lint
pnpm run typecheck
pnpm run test
pnpm run test:coverage
pnpm run quality
pnpm run build
pnpm run bench:configspnpm run test runs the full suite, including Redis/Testcontainers integration tests.
pnpm run test:coverage runs the full suite, including Redis/Testcontainers integration tests, and currently verifies:
Statements : 100%
Branches : 100%
Functions : 100%
Lines : 100%pnpm run quality runs lint, typecheck, coverage, build, and built-declaration consumer checks.
pnpm run bench:lru measures primary-process and real worker-process clustered LRU operations, with and without local L1. It also checks retained worker invalidation and delivered IPC requests under backpressure. See the 2.1.1 dependency evaluation for comparison results.
pnpm run bench:configs compares redis, l1-redis, and clustered-lru behavior against a local Redis using unique benchmark key prefixes. It does not call FLUSHALL.
Releases and verification
CHANGELOG.md records changes through version 0.1.1. Release history and process records the source revisions, migration steps, and release checklist. Keep release entries separate when preparing a version; do not replace the preceding entry.
The 2026-09-10 verification report records the feature checks and performance sample. The earlier audit report documents the preceding bug/test/dependency review and is a historical snapshot.
License
MIT. See package.json for package metadata.
