Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.8.0] — 2026-09-16
Added
- Platform-Agnostic IPC Telemetry Bridge & Live CLI Top Monitor (
src/ipc-telemetry.ts,src/cli.ts) — Enterprise-grade IPC bridge and live ASCII terminal monitoring:- Platform-Agnostic IPC: Unix domain sockets on POSIX (
/tmp/tricache-<pid>.sockor$TMPDIR/...) and Windows Named Pipes (\\.\pipe\tricache-<pid>) onwin32. - Non-Blocking Telemetry Pull: Command requests (
GET_METRICS,PING,INSPECT) defer stats collection and JSON serialization across tick boundaries viasetImmediate, eliminating event-loop stalls in the monitored host application. - Automatic Socket Hygiene: Automatically registers
process.once('exit'),SIGINT, andSIGTERMsignal traps to unlink orphaned.sockfiles synchronously on process termination, with clean programmatic teardown viaserver.close(). - Interactive TTY Top Loop (
tricache top): Real-time terminal dashboard with alternate screen buffer switching (\x1b[?1049h), cursor hiding (\x1b[?25l), stdin raw mode forq/Ctrl+Cexit, non-interactive piped fallback (!process.stdout.isTTYor--once), and--jsonmachine-readable output. - Visual Proportional ASCII Gauges: Real-time progress bars for L1 RAM, L1.5 Disk, L2 Redis, and misses, autonomous L1 memory headroom, NVMe disk quota, Latency Watchdog bypass stages, and Count-Min Sketch top hot keys.
- Platform-Agnostic IPC: Unix domain sockets on POSIX (
- Framework-Native HTTP Middlewares & Decoupled Edge Bundle (
src/http/,src/edge/) — Zero-overhead HTTP caching layer with RFC 7232 ETag validation:- Express Middleware (
createExpressMiddleware/src/http/express.ts): Deterministic query-sorted cache key generation, header whitelisting, fast weak ETag calculation (W/"..."), immediate304 Not Modifiedshort-circuiting onIf-None-Match, and conditional bypass forCache-Control: no-cache, no-store. - Fastify Plugin (
createFastifyPlugin/src/http/fastify.ts): Encapsulation-safe plugin using Fastify's[Symbol.for('skip-override')] = true, intercepting requests early inonRequestand streaming response capture inonSend, with support for routepreHandlerhooks. - Decoupled Hono Edge Middleware (
createHonoEdgeMiddleware/src/edge/hono.ts): Pure Web Standards implementation (Request,Response,crypto.subtle) without Node.js native dependencies (node:fs,node:worker_threads, SQLite) for Cloudflare Workers, Fastly Compute, Vercel Edge, Deno, and Bun. - Dedicated Package Exports: Explicit
./httpand./edgesubpath exports inpackage.jsonwith separate TypeScript type definitions (dist/http/index.d.ts,dist/edge/index.d.ts).
- Express Middleware (
- Dual-Constrained Autonomous L1 Memory Sizing (
src/utils/cgroup.ts,src/cache-service.ts) — Container-aware memory allocation preventing V8 heap OOM crashes:- Automatically probes Linux cgroup v2 (
/sys/fs/cgroup/memory.max) and v1 (/sys/fs/cgroup/memory/memory.limit_in_bytes) with safe error traps defaulting toInfinityonEACCES/ENOENTin hardened distroless containers. - Dual-constrains L1 cache capacity against V8 heap statistics:
with a 16 MB floor, preventing Node from crashing when container memory exceeds --max-old-space-size.
- Automatically probes Linux cgroup v2 (
- Zero-Latency Microtask Redis Auto-Pipelining (
src/adapters/auto-pipeliner.ts,src/cache-service.ts) — Microtask-coalesced Redis command batching:- Coalesces concurrent GET, SETEX, and DEL operations occurring in the same event-loop tick into single
pipeline.exec()calls usingqueueMicrotaskexclusively (0ms timer overhead). - Triggers immediate flushes without waiting for microtasks when
maxPipelineBatchSize(default: 100) is reached. - Configured via
CacheOptions.autoPipeline: trueand monitored viacache.getPipelinerStats().
- Coalesces concurrent GET, SETEX, and DEL operations occurring in the same event-loop tick into single
- Priority-Aware Partitioned Disk Tiering (
src/disk-tier.ts) — Tiered persistent storage with eviction protection for critical entries:- Added
priorityandlast_accessed_atcolumns to SQLite metadata with automated backwards-compatible schema migrations. - Composite SQLite index:
CREATE INDEX IF NOT EXISTS idx_priority_access ON meta (priority ASC, last_accessed_at ASC); - High-watermark disk pruner prioritizes evicting LOW/NORMAL priority entries before ever touching HIGH or CRITICAL entries.
- Added
- Asymmetric Key Envelope Encryption (
src/encryption.ts,src/remote-snapshot.ts) — Cryptographic wire specification for zero-trust cloud snapshots:- Implemented
EnvelopeEncryptionclass generating ephemeral 256-bit AES-GCM data encryption keys (DEKs) wrapped with asymmetric RSA-OAEP (SHA-256) public keys or cloud KMS hooks. - Serialized into tamper-proof binary format with
TRICENV1header, 4-byte big-endian wrapped key length, wrapped DEK, 12-byte IV, 16-byte authentication tag, and ciphertext.
- Implemented
- Next.js 16 App Router Demo &
cacheHandlersSpec Compliance (examples/nextjs/,src/next/cache-handler.ts) (#8, #21) — Complete reference Next.js 16 App Router sample application demonstrating Cache Components ('use cache'), live latency comparisons (~1000ms vs ~1ms), on-demand revalidation via Server Actions (updateTag), and alignment with Next.js 16cacheHandlersspecification returningundefinedon cache misses. Contributed by @dev-ararawi0x. - Dynamic Tier Latency Watchdog & Fleet Blast-Radius Shielding (
src/latency-watchdog.ts) — Self-healing tier latency watchdog protecting application p95 response times under cloud NVMe and multi-tenant EBS throttling:- Amortized zero-allocation p95 calculation: rolling 32-sample sliding window evaluated every 32 writes on pre-allocated
Float32Arrayring buffers. - 4-Stage graduated probabilistic shedding: Stage 0 (0% bypass) → Stage 1 (25% bypass) → Stage 2 (75% bypass) → Stage 3 (100% bypass).
- Asymmetric hysteresis on Redis circuit: cuts diversion at 15ms latency ceiling, requiring sustained recovery below 10ms over consecutive samples before re-enabling Redis traffic.
- Anti-synchronicity cooldown jitter (±30%): randomizes cooldown durations to eliminate fleet-wide failover shockwaves across Kubernetes replica sets.
- Single-canary half-open probing: sends a single trial read upon cooldown expiration, smoothly restoring tier routing without re-saturating degraded storage controllers.
- Telemetry & stats: exposed via
cache.getWatchdogStats()andstats().watchdog.
- Amortized zero-allocation p95 calculation: rolling 32-sample sliding window evaluated every 32 writes on pre-allocated
- Linux Container
/dev/shmtmpfs Off-Heap Resolver (src/disk-tier.ts,src/types.ts) — Automatic container runtime detection targeting POSIX shared memory:resolveDefaultDiskDir(namespace)automatically targets Linux POSIX shared memory/dev/shmwhen capacityand free space . - Delivers 0.02ms memory bus read/write speeds, bypasses cloud EBS IOPS, and guarantees 100% compliance with CIS/SOC2
readOnlyRootFilesystem: truehardened containers. - Safe fallback to
os.tmpdir()for Docker default 64MB environments, macOS, and Windows.
- Strict Ephemeral Storage Quota & Eviction Defense (
src/disk-tier.ts) — Autonomous host volume health monitoring preventing Kubernetes node eviction:- Evaluates host volume capacity every 2,000 writes via
fs.statfsSync(dir). - Pauses disk cache spills and engages fast write shedding (
spillsShedTotal) when free disk space falls below 10%, preventing KubernetesDiskPressureandEphemeralStorageExceededpod evictions. - Non-blocking chunked pruning down to 60% watermark in 500-entry batches with
setImmediateevent-loop cooperative yielding.
- Evaluates host volume capacity every 2,000 writes via
- Native Window TinyLFU (W-TinyLFU) Admission Engine (
src/wtiny-lfu.ts) — Full native implementation of the W-TinyLFU segmented cache admission policy popularized by Caffeine:- Three-tier segmented architecture: Window Cache (LRU, ~1% capacity) absorbs burst-recency spikes without polluting resident entries; Segmented LRU divides the main cache into Probationary SLRU (~20%) and Protected SLRU (~80%).
- TinyLFU Admission Gate: When the Window overflows, its LRU victim competes against the Probationary victim in a 4-row Count-Min Sketch. Candidates with higher historical frequency are admitted, while low-frequency candidates are rejected.
- Mathematical Scan Resistance: Sequential scans (e.g. 1,000 one-off keys) are dropped by the TinyLFU gate with >90% rejection rates, preserving 100% hit retention for resident hot items.
- Dual Mode Support: Available as a high-performance standalone cache (
WTinyLfuCache<K, V>), an admission policy controller (WTinyLfuPolicy), or integrated intoSmartMemoryCacheandCacheServicevial1AdmissionPolicy: 'wtinylfu'. - Rejection Spill Integration: Rejected candidates seamlessly spill to the L1.5 disk tier when
diskSpillis configured. - Real-Time Telemetry: Exposed via
cache.getWTinyLfuStats()andwtinyCache.stats()tracking hit rates, admissions, rejections, promotions, demotions, and segment sizes.
- Zero-Dependency AWS SigV4 Snapshot Adapter (
src/sigv4-snapshot-adapter.ts) — Lightweight (~150 LOC) AWS SigV4 signer built purely on standard Web Crypto (crypto.subtle) andfetch:- Enables stateless container pods (Kubernetes, AWS ECS/Fargate, GCP Cloud Run) and edge isolates to persist and hydrate L1 snapshots directly to/from AWS S3, Cloudflare R2, MinIO, or custom S3-compatible object storage without pulling in the 30MB
@aws-sdk/client-s3dependency. - Implements canonical request hashing, string-to-sign generation, and chained HMAC key derivation (
kDate→kRegion→kService→kSigning). - Supports virtual-hosted and path-style addressing with clean 404 / NoSuchKey detection for initial cold start deployments.
- Added convenience factories
createSigV4SnapshotAdapter,createS3SnapshotAdapter, andcreateR2SnapshotAdapter.
- Enables stateless container pods (Kubernetes, AWS ECS/Fargate, GCP Cloud Run) and edge isolates to persist and hydrate L1 snapshots directly to/from AWS S3, Cloudflare R2, MinIO, or custom S3-compatible object storage without pulling in the 30MB
- Pre-Baked Snapshot Flushers & Graceful Shutdown (
src/cache-service.ts) — Automated flusher hooks for container orchestration:- Added
cache.flushSnapshotOnShutdown(timeoutMs?: number)returning a Promise that flushes both local disk and remote cloud snapshots before Kubernetes kills the container. - Added
ProcessTerminationBus.flushAll(timeoutMs?: number)to flush snapshots across all registered cache instances concurrently with timeout protection (default 8,000ms), preventing slow object storage from blocking pod eviction.
- Added
- Edge Hydration Hook (
src/edge/cache.ts) —EdgeCacheService.hydrate()andexportSnapshot():- Primes edge isolate L1 memory directly from Cloudflare R2 bucket bindings (
env.MY_BUCKET) or snapshot sources on worker initialization. - Seamlessly decrypts Web Crypto AEAD envelopes (
enc:v1:) and automatically primes the Edge Bloom filter (bloomFilter.add(k)). - Enforces snapshot staleness ceilings (
maxAgeMs, default 2 hours) and container clock skew tolerances ().
- Primes edge isolate L1 memory directly from Cloudflare R2 bucket bindings (
BoundedDiskQueue& Cloud NVMe Backpressure Guard (src/disk-tier.ts) — Concurrency-controlled disk spill spooler protecting against libuv threadpool (UV_THREADPOOL_SIZE=4) saturation during cloud NVMe / AWS EBS latency spikes (0.4ms to 450ms):- Limits active async fs operations to
diskMaxConcurrentWrites(defaultMath.min(4, Math.max(1, Math.floor(os.availableParallelism() / 4)))), preventing DNS lookups, zlib compression, and crypto operations from starving. - Fast-sheds incoming spills when pending writes hit
diskMaxPendingWrites(default 512) before touching libuv, bounding heap memory and preserving main-thread latency. - Three-state Disk Circuit Breaker (
closed→open→half-open) with strict single-canary probing upon cooldown expiry, preventing recovering disk controllers from being re-flooded. - Real-time backpressure telemetry surfaced in
stats().disk.backpressure(activeWrites,pendingWrites,spillsDropped,circuitState).
- Limits active async fs operations to
- Coordinate vs. Monotonic Time Separation & Epsilon-Fencing — Strict separation between wall-clock coordinate timestamps and node-local monotonic intervals:
- Migrated
L2CircuitBreaker, local generational tag cache TTL checks (_getTagVersion), SWR debounce windows, and latency profiling toperformance.now(), making internal invariants immune to NTP step adjustments, leap seconds, and container clock skew. - Added
clockSkewToleranceMs(default250ms) with-fencing in loadSnapshotandloadRemoteSnapshot, clamping negative durations () and accepting valid cross-node snapshots within the skew tolerance window.
- Migrated
- Redis Multiplexing Desync Defense (
redisCommandTimeoutMs) — Enforced strict per-command timeouts (default2,500ms) with immediate socket destruction and reconnection across Redis Cluster, Sentinel, and standalone clients, preventing delayed server responses from being assigned to subsequent FIFO pipelined requests (eliminating response byte poisoning). - In-Process Chaos Engineering Test Harness (
tests/chaos/) — Zero-dependency programmatic chaos harness built purely onnode:net:ChaosTcpProxy(tests/chaos/chaos-tcp-proxy.ts): Simulates network partitions, jittered latency, packet blackholes, and abrupt TCP RST (socket.destroy(new Error('ECONNRESET'))) mid-pipeline without Docker or external Toxiproxy daemons.DiskChaosInjector(tests/chaos/disk-chaos-injector.ts): Simulates multi-tenant NVMe stalls and intermittentEIO/ENOSPCdisk controller faults.tests/chaos/fleet-chaos.test.ts: 13 comprehensive chaos scenarios validating backpressure shedding, canary circuit-breaker recovery, clock skew tolerance, and network partition resilience.
- Node 20 LTS Engine Compatibility (
engines: ">=20.10.0") — Lowered the supported runtime floor from Nodeto Node by adding safe dynamic module probing in disk-tier.tsandavailableParallelismfallback inworker-pool.ts. ProcessTerminationBuswith Idle Listener Teardown (src/cache-service.ts) — Centralized OSSIGTERM/SIGINThandling through a single static bus. EliminatesMaxListenersExceededWarningacross multi-tenant microservices and ephemeral test runners, and automatically detaches process listeners when the active instance registry drops to zero to prevent test runner event loop hangs.- OpenTelemetry Semantic Conventions & Batch Spans — Full alignment with OTEL Cache Semantic Conventions:
cache.hitstandardized to strictboolean(trueorfalse).- Added
cache.item.tier('memory','disk','remote'). - Retained legacy
cache.hit_tier('l1','disk','l2','miss') for complete backward compatibility with existing APM / Grafana dashboards. - Added batch spans
tricache.msetandtricache.mdeltrackingcache.batch.sizeand capturing failure status codes and exceptions.
- Pluggable
node-redisDriver Adapter (@redis/clientBridge) — AddedNodeRedisAdapterandcreateNodeRedisAdapter(src/adapters/node-redis.ts) enabling enterprise organizations standardizing on@redis/client(node-redis v4/v5/v6), AWS ElastiCache IAM authentication, or Azure Managed Identities to plug their existing connection pools directly intoCacheService:- Zero additional runtime dependencies added to TriCache.
- Transparently translates single and multi-key deletions, camelCase methods (
sAdd,sMembers,setEx,mGet), distributed locks (EVALLua scripts), and transactions. - Normalizes pipeline/multi execution into error-first tuple arrays (
Array<[Error | null, T]>) for 100% compatibility with all batch and warming flows. - Added
redisClient?: IRedisDriver | anyandredisSubClient?: IRedisDriver | anyoptions toCacheOptions.
- Package Root Re-Exports (
src/index.ts) — Re-exported key adapters and cluster utilities from'tricache'root:createNodeRedisAdapter,createHttpMeshRelay,createCustomCrossRegionRelay,createCrossRegionWebhookHandler,TierLatencyWatchdog,resolveDefaultDiskDir. - Edge WebAssembly & Murmur3 Bloom Filter (
tricache/edge) — High-performance cold-miss penetration defense for V8 edge isolates:- Removed Node.js
Bufferdependency insrc/wasm/bloom-filter-wasm.tsvia chunkedbase64ToUint8Array, making the WASM Bloom filter 100% universal across Cloudflare Workers, Fastly Compute, Vercel Edge, and browsers. - Created
Murmur3BloomFilterandmurmur3_32(src/edge/utils/murmur3.ts) implementing standard 32-bit MurmurHash3 double-hashing with Kirsch-Mitzenmacher bitset probing over a pureUint8Arraybit-array. - Integrated Bloom filter into
EdgeCacheService(bloomFilter: boolean | IEdgeBloomFilter): ~300ns in-isolate miss rejection completely prevents expensive, metered HTTP subrequests to remote storage (Upstash Redis REST, Cloudflare KV) on 404 routes and randomized bot crawler keys.
- Removed Node.js
- Expanded Test Suite — Expanded to 756 passing tests across 71 test files with 100% test pass rate.
CacheCodecabstraction (src/codec.ts) — Centralized msgpackr binary serialization engine with built-in record structure deduplication (useRecords: true), rich type preservation (moreTypes: trueforSet,TypedArray,Date, etc.), and strict plain-object map decoding (mapsAsObjects: true).serializeToJSONoption inCacheOptions— Configurable flag (defaults totrue) leveraging msgpackr 2.1.0'suseToJSONcapability. SettingserializeToJSON: falsepreserves the object's actual internal properties in durable cache tiers without invoking.toJSON(), avoiding accidental HTTP response projections on cached domain entities.- Enterprise Documentation & Observability Suite (
docs/) — Comprehensive VitePress documentation suite:- Interactive guides for Next.js 16/15, NestJS, Prisma, Drizzle, Express/Hono, Edge Isolates, and Visual Dashboard.
- Dedicated Kubernetes SRE documentation covering
/dev/shm, cgroups, probes, latency watchdog, and eviction defense. - Complete Observability guide with real-time SSE Web Dashboard, Grafana Golden Signals template, Prometheus alerting rules, and CLI.
- Dedicated test suites — Added comprehensive coverage for previously untested integration layers, bringing the test suite to 554 tests passing:
tests/codec-improvements.test.ts: Record structure deduplication (~45% smaller binary size),serializeToJSONtoggle, rich type round-tripping, and DoS rejection.tests/drizzle.test.ts: Deterministic query hashing andwithCachequery execution wrapping.tests/prisma.test.ts: Deterministic query argument serialization andwithTriCacheextension hooks with mutation tag invalidation.tests/next-cache-life.test.ts: Next.js 16cacheLifepreset resolution (seconds,minutes,hours,days,weeks,max, and custom profiles).tests/types-logger.test.ts: Built-inconsoleLoggerwrapping and formatting.tests/disk-tier.test.ts: Tampered ciphertext and corrupted disk payload resilience.
Performance
- Parallelized Tag Version Loops (N+1 Waterfall Elimination) — Replaced sequential
for...ofawaited_getTagVersioncalls with concurrentPromise.allparallel requests acrossCacheService(L1 hit staleness check, L2 hash staleness check, disk hit staleness check,set()active tag versioning, and SWR background revalidation) andNextCacheHandler(entry staleness checks and write paths). - Cryptographic Randomness for Temp Files — Replaced
Math.random()in temporary spill file naming (src/disk-tier.ts) withcrypto.randomBytes(6).toString('hex')to eliminate potential collision risks under high multi-tenant spill concurrency.
Documentation & Architecture Notes
- Documented intentional architectural choices across the codebase to eliminate false positive review warnings:
src/cli.ts: Documented whyconsole.logis required for CLI stdout piping.src/wasm/bloom-filter-wasm.ts: Documented whyWasmBloomFilterinitializes 100% synchronously from inlined precompiled bytecode with no async init phase.src/cache-service.ts: Documented whyfs.writeFileSyncinwriteSnapshot(SIGTERM/SIGINT hooks) andfs.readFileSyncinloadSnapshot(constructor cold start) are intentionally synchronous.src/disk-tier.ts: Documented the 16-byte fixed header (DISK_MAGIC_V2) fast path inpurgeNextBucketdemonstrating that full payload reads are skipped during janitor sweeps.
Security
- Memory amplification DoS defense (
msgpackr2.0.5 → 2.1.0) — Upgradedmsgpackrto 2.1.0. Malformedarray32ormap32headers declaring excessive lengths beyond the buffer boundary are rejected immediately without allocating memory, eliminating a ~32,000,000x memory amplification attack vector on untrusted payloads.
[0.7.1] — 2026-08-22
Fixed
- P0 — Cross-key data corruption under concurrent generational reads:
SmartMemoryCache.get()returned a shared, module-level reusable hit object.CacheService.get()awaits tag-version checks between the L1 hit and the return, so two concurrentget()calls for distinct tagged keys (undertagStrategy: 'generational') could receive each other's payloads with no error.get()now returns a fresh per-call object; regression-tested with a 20-key interleaved concurrency suite (tests/concurrency-aliasing.test.ts). - P0 —
cache.lock()executed the critical section twice on business failure: when the locked function threw (or the Lua release failed), the error was swallowed by the Redis-fallbackcatchand the task re-ran under the in-process mutex after the distributed lock had already been released. Lock acquisition and task execution are now decoupled: a business exception propagates exactly once and is never retried (tests/distributed-lock.test.ts). - HTTP middleware cached 4xx/5xx responses — a transient upstream 500 poisoned its cache key for the whole TTL window across Express, Hono, and Fastify adapters. All three now gate persistence on 2xx status and evict error responses immediately.
- Express middleware hung forever on
res.end()— onlyres.json/res.sendwere intercepted as completion signals; handlers answering viares.end()never resolved the fetch promise.end()is now a third completion signal. ttl: 0created instantly-expired entries — the NestJSTriCacheStorecontract documents "0 = indefinite", but expiry was computed asnow + 0ms. TTL 0 now maps to a far-future expiry in both L1 and L2 write paths.- Prisma extension forwarded the
cachepseudo-option to the query engine on mutations — the read path stripped it, the write path passed it verbatim; both paths now send clean args. - Disk tier leaked byte accounting on corrupt entries — the legacy decrypt-failure purge path deleted files without releasing their size from
diskUsageBytes, causing phantom "disk cap reached" states in file-only mode (tests/disk-accounting.test.ts). - Strict decompression — corrupt compressed payloads previously fell through both zlib attempts and returned raw bytes (corruption surfaced as garbage downstream). Decompression failure now throws and every call site maps it to a clean cache miss; cross-algorithm recovery is preserved.
Changed
- Library no longer hijacks host shutdown: the SIGTERM/SIGINT handler flushes the cold-start snapshot but no longer calls
process.exit(0)— that decision belongs to the host application (Kubernetes graceful drain, NestJSonApplicationShutdown, pool drains). redisHost/ cluster / sentinel configuration is now honored outside production — L2 was silently disabled wheneverNODE_ENV !== 'production'even with explicit connectivity config. TheREDIS_HOSTenv fallback still does not auto-enable L2 in tests. Documented precedence: explicitdisableRediswins over everything; otherwise any explicit connection config enables Redis.cache.optionsgetter redactsencryptionKeyas[REDACTED]instead of exposing raw key material to diagnostic dumps, and now also reportsencryptionMode.- README badge/test-count drift is CI-enforced via
scripts/check-test-badge.mjs(runs after the suite in all six OS×Node matrix legs). - Documentation accuracy: corrected the msgpackr serialization claim (L2 string values are JSON-serialised before encryption), documented invalid-key fail-open vs fail-closed behavior in SECURITY.md, fixed the disk-spill saturation test's race against in-flight
.tmpstaging writes, updated the test-count badge to reflect this release.
Added
strictKeyValidationoption (fail-closed encryption config) — an invalid or emptyencryptionKeypreviously logged an error and continued with at-rest encryption silently disabled (fail-open; still the default). SetstrictKeyValidation: trueto throw at construction instead — for deployments where serving plaintext at rest is unacceptable. A key that is simply not configured never throws in either mode.- New regression suites: concurrent generational reads (
concurrency-aliasing), distributed-lock single-execution semantics (distributed-lock), HTTP error-response caching +res.end()handling (http-middleware), shutdown/config-precedence contracts (lifecycle-config), disk byte accounting (disk-accounting), strict decompression (compression), invalid-key strict mode (encryption). Test count: 484 → 511 (unit) plus a live-Redis integration suite (pnpm test:integration, requires Docker/Redis).
[0.7.0] — 2026-08-21
Added
- Universal HTTP Caching & 304 ETag Middleware (
tricache/http) — Universal middleware for Express, Connect, and Hono:expressCache(options)andhonoCache(options)automatically compute weak ETags, cache endpoint responses, and return304 Not ModifiedonIf-None-Matchmatches with zero body transfer overhead. Exported via subpath"./http".
- Worker Thread Crash Auto-Recovery — Handled worker thread sudden crash/exit events with non-zero exit codes in
WorkerPool, rejecting in-flight tasks cleanly and auto-spawning healthy replacement workers to preserve pool capacity. - First-Class Prisma Client Extension (
tricache/prisma) — AddedwithTriCache(options)Prisma Client extension ($extends):- Intercepts read queries (
findUnique,findFirst,findMany,count,aggregate,groupBy) with options{ ttl, swr, tags, key }. - Automatically derives deterministic cache keys from model + operation + query args.
- Automatically invalidates model tags on write mutations (
create,update,delete,upsert,createMany,updateMany,deleteMany). Exported via subpath"./prisma".
- Intercepts read queries (
- First-Class Drizzle ORM Query Wrapper (
tricache/drizzle) — AddedwithCache(query, options)wrapper for Drizzle ORM query builders:- Generates deterministic SHA-256 cache keys from compiled SQL + parameter bindings via
query.toSQL(). - Seamlessly wraps query execution with
cache.wrap()supporting custom TTL, SWR grace windows, and tags. Exported via subpath"./drizzle".
- Generates deterministic SHA-256 cache keys from compiled SQL + parameter bindings via
- Distributed Mutex & Lock Primitive (
cache.lock()) — Added distributed and in-process mutual exclusion locking:- Atomic acquire via Redis
SET lock:<key> <token> NX EX <ttl>with safe Lua script release (if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end). - Automatic release on return or throw to prevent deadlocks.
- Configurable
acquireTimeoutandretryIntervalwith an in-process promise-chain mutex fallback.
- Atomic acquire via Redis
- Native OpenTelemetry Metrics (
ICacheMeter) — Direct integration with OpenTelemetry Meter API:- Structural typing matching
@opentelemetry/apiMeterwithout mandatory runtime dependencies. - Registers monotonic counters (
tricache.gets.total,tricache.l1.hits,tricache.l2.hits,tricache.disk.hits,tricache.fetches,tricache.stampedes.prevented,tricache.sets.total,tricache.deletes.total,tricache.swr.revalidations). - Registers observable batch gauges for L1 entries/bytes, disk files/bytes, and Bloom filter false-positive rate.
- Structural typing matching
- Developer & Troubleshooting CLI (
npx tricache) — Zero-dependency developer CLI (bin/tricache.js/dist/cli.js):npx tricache inspect: Live terminal dashboard displaying hit ratios, 3-tier latencies, L1/disk sizes, and Count-Min Sketch hot keys.npx tricache ping: Measures response latencies across RAM, disk, and Redis.npx tricache clear: Safely flushes all entries or keys matching a prefix across the cluster.
- Universal
cache.wrap()Primitive — Addedcache.wrap<T>(key, fetchFn, options?: WrapOptions)inCacheServiceas an ergonomic options-object alternative tocache.get(), normalizingttl,swr,tags,dependsOn,priority,refreshAhead, andxfetchBetainto a single configuration object for seamless integration with ORMs (Prisma, Drizzle, TypeORM). - Official NestJS Module, Store & Decorators (
tricache/nestjs) — Dedicated dynamic module, store adapter, and declarative decorators for NestJS:TriCacheModule.register()andTriCacheModule.registerAsync()for synchronous and async dependency injection.TriCacheStoreconforming to@nestjs/cache-managerandcache-managerv5/v6 contracts with accurate millisecond-to-second TTL conversion.- Declarative
@Cacheable(options)and@CacheEvict(options)method decorators with dynamic key generation, argument injection, and automatic tag invalidation. - Zero mandatory NestJS runtime dependencies (uses structural typing with optional peer dependencies). Exported via subpath
"./nestjs".
- Zero-Copy WorkerPool Memory Transfers (
transferList) — Implemented zero-copy memory transfers between the main thread and worker threads viatransferList:getTransferableArrayBuffer()detects dedicated buffers and transfers ownership with zero heap copying.- Slices from Node's internal 8 KB buffer pool (
) are isolated to prevent detached-buffer memory corruption. - Worker thread transfers output buffers back to parent thread via
[transferable], guaranteeing two-way zero-copy throughput.
- Next.js 16
cacheLifePreset & Profile Mapper — Built-in support for Next.js 16 semantic cache profiles (PRESET_CACHE_LIFE_PROFILESandresolveCacheLife):- Automatically maps
'default','seconds','minutes','hours','days','weeks', and'max'profiles. - Translates
revalidatettland(expire - revalidate)swrfor seamless"use cache"lifecycles.
- Automatically maps
- Next.js 16 & 15 Integration Adapter (
tricache/next) — ComprehensiveCacheHandlerimplementation for Next.js 16"use cache", React 19 RSC streaming, and legacy ISR. Includes:- 5-method
TriCacheHandler(get,set,refreshTags,getExpiration,updateTags) and 4-methodTriCacheISRHandler(get,set,revalidateTag,resetRequestCache). - Single-use stream safety: automatically drains incoming
ReadableStream<Uint8Array>onset()into a contiguous binary buffer and creates fresh, unlockedReadableStreaminstances on everyget()hit. - Dynamic
softTagsverification: readsctx.softTags(e.g._N_T_/layout,_N_T_/page) onget()and validates against active generational tag versions, triggering misses on route/layout boundary invalidations. - Fail-soft error boundaries for stream drops / client disconnects and automatic
NEXT_PHASE=phase-production-builddetection to bypass Redis sockets during static builds. - Exported via
"./next"inpackage.jsonwith ESM, CJS, and DTS builds (TriCacheHandlerexported as named anddefault).
- 5-method
- Generational Tag Invalidation (
tagStrategy: 'generational') —tag invalidations replacing Redis set deletions: - Tag invalidation executes atomic
INCR tag_ver:<tag>in Redis and memory. - Entries store captured tag versions in a unified Redis hash
{ d: envelope, t: timestamp, tv: tagVersionsJson }with an atomicMULTI/EXECtransaction. - Pipelined multi-tag invalidation (
invalidateTags(tags)) in a single network round-trip. - Monotonic version progression guarantee (
Math.max(existing, incoming)) preventing out-of-order packet reordering from resurrecting stale data. - Atomic compare-and-delete Lua script on Redis and
deleteIfSetBefore(cutoffMs)on L1 RAM to prevent wiping newer data during concurrent revalidation races. - Time-based self-healing reconciliation (
tagVersionTtlMs, default 5 s) with a 10,000 LRU bounded in-memory cache to heal network partitions and dropped broadcasts.
- Tag invalidation executes atomic
- Transitive Cycle-Protected Dependency Cascades (
dependsOn) —_cascadeDependenciestraverses multi-tier entity graphs using avisited: Set<string>, invalidating deep chains () in while preventing infinite loops on circular graphs ( ). - Redis Streams Invalidation Backplane (
backplaneMode: 'stream') — Durable append-only invalidation logging:- Publishes mutations via
XADD <streamKey> MAXLEN ~ 10000 * .... - Cluster single-slot safety via hash tags:
tricache:stream:{<namespace>}. - Dedicated Redis consumer connection running an unref'd non-blocking
XREAD BLOCKlong-polling loop. - Zero-drop replay of missed mutations across network reconnects and GC pauses using
_lastStreamId. - Self-healing trim gap fallback: detects when an instance fell behind the stream retention window, increments
metrics.streamGaps, flushes L1, and resets stream pointer. - Clean instant socket teardown on
destroy()without long-poll blocking stalls.
- Publishes mutations via
- Read Safety Strategy (
cloneStrategy: 'structuredClone') — Optional deep-clone isolation on L1 hits, L2 promotions, and fetch returns, protecting cached objects from caller mutation while preserving raw in-memory sub-microsecond performance ('none') andfrozen: truedev-mode guards. - Atomic Disk Writes & Janitor Sweep — Staging disk writes to unique sibling
.tmpfiles (${filePath}.${pid}.${timestamp}.${rand}.tmp) before atomic rename, Windows NTFS file-lock micro-retries, and background cleanup of abandoned.tmpfiles (sweepOrphanedTmpFiles). - Dynamic Runtime Key Rotation (
rotateEncryptionKey) — Rotate active AES-256-GCM / AES-128 keys at runtime with zero downtime viacache.rotateEncryptionKey(newKey, mode). Seamlessly falls back to the previous key for reading existing L2 (Redis) and disk entries, automatically promotes key material to the worker pool via graceful thread draining (WorkerPool.drainAndReinit()), and guarantees graceful cache misses forkeys without unhandled exceptions. - Redis 7+ Sharded Pub/Sub (
useShardedPubSub) — Scopes invalidation messages strictly to the cluster shard handling the key slot viaSPUBLISH/SSUBSCRIBEwhen connected to Redis Cluster (redisClusterNodes), drastically reducing inter-node cluster bus gossip traffic on high-throughput clusters. - Transparent Payload Compression (Redis L2 & Disk Tier) — Built-in Brotli and Gzip compression for L2 strings and disk-tier binary blobs with configurable size threshold (
compressionThresholdBytes, default 1 KB). Offloads compression and decompression to worker threads alongside encryption for large payloads, supporting a 4-state envelope matrix (cmp:v1:,ecp:v1:,enc:v1:, raw JSON) with full backward-compatibility for uncompressed legacy cache entries. - Redis Protocol Selection & RESP3 Auto-Diagnostic (
redisProtocol) — Added optionalredisProtocol?: 2 | 3option inCacheOptionsallowing explicit wire-protocol selection (RESP2 or RESP3). Includes fail-soft diagnostic detection that interceptsunknown command 'HELLO'/ protocol errors on legacy proxies (Twemproxy, Envoy, older ElastiCache) and emits an actionable warning prompting the user to configureredisProtocol: 2. - Mission-Critical Chaos, Resilience & Ecosystem Test Suites — Added 484 passing unit, stress, and chaos tests across 41 suites:
- 10,000 concurrent stampede coalescing (
tests/stampede-10k.test.ts). - Redis connection flapping resilience (
tests/chaos-flapping.test.ts). - Disk spill quota saturation & LRU recovery (
tests/disk-spill-saturation.test.ts). - SWR outage stampedes under failing upstreams with
staleIfErrorTTL extension (tests/deep-resilience.test.ts). - Truncated and corrupted snapshot cold-start recovery (
tests/deep-resilience.test.ts). - Multi-tenant namespace isolation under mass flushes and mutations (
tests/deep-resilience.test.ts). - 50,000-cycle high-velocity heap soak and memory leak bounds (
tests/heap-soak-leak.test.ts). - Multi-instance generational backplane synchronization (
tests/multiprocess-backplane-sync.test.ts). - Worker thread crash auto-recovery & replacement (
tests/worker-crash-recovery.test.ts). - Distributed mutex lock concurrency exclusion & token release safety (
tests/lock-token-safety.test.ts,tests/distributed-lock.test.ts). - Cross-version format evolution backward compatibility matrix (
tests/format-evolution-matrix.test.ts). - HTTP caching middleware with 304 ETag short-circuiting for Express and Hono (
tests/http-middleware.test.ts). - RESP3 proxy rejection diagnostic hint detection (
tests/resp3-diagnostic.test.ts). - Native OpenTelemetry metric counters & observable gauges (
tests/opentelemetry-metrics.test.ts). - Prisma client extension & Drizzle query wrapper caching (
tests/prisma-drizzle-adapters.test.ts). - CLI inspect, ping, clear, version, and help commands (
tests/cli.test.ts). - High-performance
oxlintlinter and full TypeScript strict checking (tsconfig.test.json).
- 10,000 concurrent stampede coalescing (
Security & Hardening
- ReDoS Hardening in Glob Dependency Matching — Hardened
_matchesGlobagainst catastrophic backtracking by collapsing consecutive wildcards (\*+→.*), escaping regex metacharacters, and caching compiled expressions in a bounded LRU regex cache (globRegexCache). - WorkerPool Availability Guards — Guarded
WorkerPool._dispatchagainst destroyed or unavailable pools, rejecting immediately with a descriptive error rather than throwing an unhandled TypeError.
Changed
- Zero-Allocation WASM Bloom Filter Staging — Pre-allocated
stagingTargetUint8ArrayinWasmBloomFilterconstructor, eliminating per-probesubarray()view allocations on hot L1 lookups. Boosts filter insertions to 4.54 M/s (220 ns) and hot hit gating to 1.87 M/s (534 ns). ioredis5.11.1 → 6.0.0 — Upgraded to ioredis v6 major release with default RESP3 protocol support, improved connection lifecycle resilience, and slot routing prototype pollution defenses.msgpackr2.0.4 → 2.0.5 — Upgraded to msgpackr 2.0.5 patch release addressing sequential/stream unpacking offset tracking and TypeScript export path definitions.- Security & Dependency Audit Fixes — Updated devDependencies (
vitest,tsup,tsx,@types/node,vite) and configured package overrides to eliminate all 7 security advisory warnings (vite,postcss,nanoid,esbuild). - Extracted Named Constants — Extracted
DEFAULT_COUNTER_TTL_SECONDS(60 s) for in-process rate-limiting counter fallback inincrement().
[0.6.7] — 2026-08-15
Security & Hardening
- Pub/Sub Invalidation Message Validation — Added strict fail-closed runtime schema validation for incoming Redis/backplane pub/sub messages before accessing message properties. Rejects and drops non-object, scalar, non-string keys, invalid
opcommands, and prototype pollution attempts. - Log Injection Hardening — Stripped/sanitized carriage returns and newlines from raw rejected pub/sub payload strings before logging to prevent multiline log-injection attacks.
- Cryptographic TTL Jitter — Swapped
Math.random()in_jitterTtl()for uniform cryptographically secure randomness viacrypto.randomInt(0, 100_000)to improve PRNG defense-in-depth hygiene.
Fixed
- Test Stability on Windows / CI — Stabilized sub-50ms test timer margins in
v0.2.0-features.test.tsto accommodate 15.6ms OS timer resolution quantization and prevent false-positive race conditions duringtouch()andbumpExpiry()assertions.
Added
- Error Resilience Test Suite — Added dedicated unit tests for unhandled error recovery in
DiskTier.ensureUsageCounted()(fs.statSyncerrors),CacheService.loadSnapshot()(fs.unlinkSyncerrors), and user-provided throwingonMetricscallbacks. - Deterministic Jitter Bounds Suite — Added deterministic unit tests verifying exact mathematical mapping of
crypto.randomIntboundaries and statistical uniformity.
[0.6.6] — 2026-07-16
Added
- Worker pool init accessor — Introduced
CacheEncryption.toWorkerInit()to cleanly expose key/mode material for off-main-thread worker pool initialization, replacing unsafe private-field casts. - Fail-closed counter handling & divergent init detection — Detect and guard against divergent singleton initialization calls.
- Circuit breaker & metrics — Tightened the Redis circuit breaker and surfaced new internal counters in telemetry/metrics.
Fixed
- Disk encryption support for all modes — Refactored
DiskTierto useCacheEncryptiondirectly instead of hardcoding AES-256-GCM. All encryption modes (aes-128-gcm,aes-128-ctr,xor) now successfully save and load from the disk spill layer. - Eviction loop under budget limit — Fixed
SmartMemoryCache.ensureCapacity()eviction logic to loop until the cache usage is actually back under both category and global limits, rather than stopping after evicting a fixed count of entries. mget()fallback logic — Mademget()check L1 cache, then Redis/Valkey, then the disk spill layer before invoking the fetch function.
Changed
- Pinned dependencies — Pinned exact dependency versions (
ioredisto5.11.1andmsgpackrto2.0.4) inpackage.json.
[0.6.5] — 2026-05-31
Fixed
Turbopack / Next.js 16 compatibility — Two changes eliminate the
"Specified module format (CommonJs) is not matching EcmaScript Modules"error seen when using tricache in Next.js 16 apps (which default to Turbopack fornext build):Removed
.d.ctsdeclaration files from the published package. tsup auto-generateddist/index.d.ctsanddist/serialize-worker.d.ctsas CJS-type sidecars. Turbopack discovers these files through its CJS sidecar resolution path for packages that expose bothimportandrequireexport conditions, then attempts to process them as runtime JavaScript — triggering a format-mismatch error because they contain ESMexport {}syntax. Since the exports map already has an explicit"types": "./dist/index.d.ts"condition (which TypeScript resolves before any sidecar), the.d.ctsfiles were redundant. Apostbuildstep now removes them after every build.Eliminated the
__requireshim from the shared ESM chunk.worker-pool.tscontained a rawrequire('os')call insideavailableCpus(). In an ESM build, tsup/esbuild replaces rawrequire()calls with a__requirepolyfill and places it in the shared chunk (chunk-*.js), making that chunk a hybrid file (CJSvar __require = ...+ ESMexport {}). The polyfill is now removed:require('os')is replaced with a top-level staticimport os from 'os', which is safe because theenginesfield already requires Node ≥ 22.13.0 whereos.availableParallelism()is always present.Replaced
createRequirewithprocess.getBuiltinModuleindisk-tier.ts. The optionalnode:sqlitebootstrap usedcreateRequire(import.meta.url)to loadnode:sqlitesynchronously. esbuild transforms this into the same__requireshim. The replacement usesprocess.getBuiltinModule('node:sqlite'), which is available in Node ≥ 22.3.0 (within the ≥ 22.13.0 requirement), is synchronous, and requires no shim.
Fixed
resolveValue()deserialization bug —liveValues()andCacheService.entries()were yielding rawBufferobjects for entries that did not have a cached live-object value (disk-restored entries and entries above the newLARGE_VALUE_BYTESthreshold). A newSmartMemoryCache.resolveValue()helper centralises theentry.value !== undefined ? entry.value : unpack(entry.data)path and all three callsites now use it.- Benchmark OOM in §19c — The stability soak section deliberately saturates the V8 heap to ~94 %. The subsequent §19c worker-pool section then crashed with
FATAL ERROR: Ineffective mark-compacts near heap limit. AglobalThis.gc?.()call (already enabled via--expose-gcin the bench script) and asetImmediateyield are now inserted between the soak and §19c to allow V8 to reclaim garbage before allocating large worker payloads.
Changed
setAtfield onSmartCacheEntry— Everyset()now records the Unix timestamp (ms) when the entry was written assetAt.evictSetBefore()reads this field directly instead of approximating viaexpiresAt − ttlMs, which was incorrect when TTL had been refreshed or a clock drift occurred. Optional for backward compatibility with snapshots written by older versions.LARGE_VALUE_BYTESthreshold (16 384 B) — Entries larger than 16 KB no longer cache the deserialized JS object alongside the msgpackrBuffer. Previously everyset()stored both the packedBufferand the live V8 object, doubling heap usage for large entries. Entries below the threshold are unaffected; their live object is still cached for zero-alloc hot-path reads.- Updated BENCHMARKS.md — All benchmark rows updated with the 2026-05-31 run. Only improved measurements are recorded; rows that regressed are left at their prior values. Notable improvements:
purgeExpired()SQLite mode 72 /s → 181.1 K/s, adaptive TTL cold miss 10.7 K/s → 89.5 K/s, CacheService fetchFn 9.7 K/s → 35.8 K/s, parallel I/O ratio 2.26× → 16.13×.
[0.6.3] — 2026-05-31
Fixed
serialize-worker.tsimports switched to explicit.tsextensions (./encryption.ts,./types.ts). tsx resolves.tsimports natively without any hook; the previous extensionless imports were rewritten to.jsby esbuild (package"type":"module") and tsx's.js→.tsremap hook is not active inside worker threads on Node 22. Also addsallowImportingTsExtensions: true+rewriteRelativeImportExtensions: truetotsconfig.jsonto allow the explicit.tsimport syntax while keeping DTS emit correct.
[0.6.2] — 2026-05-31
Fixed
serialize-worker.tsimports changed from'./encryption.js'/'./types.js'to extensionless'./encryption'/'./types'. tsx resolves extensionless imports directly to.tsfiles without needing the.js→.tsremap hook, which is not active inside worker threads on Node 22. Node 24 was unaffected.
[0.6.1] — 2026-05-27
Added
Worker thread crypto offload (
workerThreads) — AES-GCM encryption and decryption can now be offloaded from the V8 main thread to a dedicatedworker_threadspool (src/worker-pool.ts+src/serialize-worker.ts). The pool is fixed-size, round-robin dispatched, and auto-sized tomin(4, logical CPUs)whenworkerPoolSize: 0. Workers areunref()'d so they never block process exit. Offload activates only whenenc.isEnabled && payload.length > workerThresholdBytes(default 128 KB), so small payloads stay on the fast synchronous path with zero overhead. Worker initialisation failure silently falls back to synchronous crypto — no configuration change required.Option Default Description workerThreadsfalseEnable off-main-thread AES-GCM offload workerThresholdBytes131072Minimum serialized payload size (bytes) to offload workerPoolSize0Fixed pool size; 0= auto (min(4, CPUs))Backplane staleness fence (
backplaneMaxStalenessMs) — The Pub/Sub subscriber now tracks its most-recent disconnect timestamp. On reconnection, if the gap since the disconnect exceedsbackplaneMaxStalenessMs, every L1 entry written before the disconnect is proactively evicted viaSmartMemoryCache.evictSetBefore(). This prevents stale cache hits caused by silently dropped peer invalidations during network blips, Redis failovers, or container restarts. Set to0to disable the fence. Eviction count and gap duration are logged atwarnlevel.Option Default Description backplaneMaxStalenessMs5000Gap threshold in ms; staleness fence fires above this Serverless / ephemeral disk detection (
disableDisk) — TriCache now inspects seven well-known environment variables at construction time (zero I/O) to detect AWS Lambda, Google Cloud Run/Functions, Azure Functions, Fly.io, Railway, and Vercel runtimes. When a serverless runtime is detected the disk tier, disk janitor, cold-start snapshots, and the disk spill callback are all silently disabled. Themetrics().disk.disabledfield reflects the current state. The newdisableDiskoption allows explicit override in either direction.Option Default Description disableDiskundefined(auto)true= always disable;false= always enable;undefined= auto-detectRedis Cluster support (
redisClusterNodes) — Pass an array of cluster seed nodes and ioredis handles slot routing, MOVED/ASK redirects, and slot-migration re-queuing transparently. The backplane subscriber is also constructed in cluster mode.typescriptCacheService.create({ redisClusterNodes: [ { host: 'redis-node-1', port: 6379 }, { host: 'redis-node-2', port: 6379 }, ], });Redis Sentinel support (
redisSentinel) — Pass sentinel addresses and a master name; ioredis monitors the primary via the sentinel topology and reconnects after failover. The backplane subscriber uses sentinel mode automatically.typescriptCacheService.create({ redisSentinel: { name: 'mymaster', sentinels: [{ host: 'sentinel-1', port: 26379 }], }, });SmartMemoryCache.evictSetBefore(cutoffMs)— New internal method used by the staleness fence. Approximates each entry's write time asexpiresAt - ttlMsand evicts entries written beforecutoffMs.CRITICALpriority entries that have not yet expired are preserved. Bloom filter is rebuilt after eviction. Returns the number of evicted entries.
Changed
getRedis()return type widened fromPromise<RedisClient>toPromise<AnyRedisClient>to cover Cluster and Sentinel connections.this.redis,this.redisConnecting, andthis.subClientfields are now typed asAnyRedisClient(Redis | Cluster) to support all three topology modes.metrics().disknow includes adisabledboolean field alongside the existing stats fields.
Fixed
- All disk tier call sites (
disk.load,disk.delete,disk.clear,disk.close) are now guarded byif (!this._diskDisabled), preventingENOENT-class errors on platforms where the disk tier is disabled. destroy()now unconditionally drains the worker thread pool (if active) before closing Redis connections, ensuring clean shutdown whenworkerThreadsis enabled.WorkerPool._dispatch()now callsworker.ref()before posting each message andworker.unref()once the pending queue drains, so in-flightpool.encrypt()/pool.decrypt()calls are always awaited correctly in short-lived scripts (benchmarks, CLI tools) without preventing process exit when idle.
Dependencies
ioredis5.10.1 → 5.11.0msgpackr2.0.1 → 2.0.2
[0.6.0] — 2026-05-26
Added
Adaptive TTL (
adaptiveTtl) — tricache now tracks per-key fetch latency in a pre-allocatedFloat64Arrayring buffer (default 32 samples). Once a key has ≥ 5 recorded fetch durations the library automatically derives an optimal TTL:adaptedTtl = clamp(p95LatencyMs × adaptiveTtlMultiplier, adaptiveTtlMin, adaptiveTtlMax)The caller-supplied
ttlSecondsis used until enough samples are collected, then the library takes over TTL management autonomously. Expensive keys (slow DB queries) are cached longer; fast keys stay close to their base TTL. Four new options control the behaviour:Option Default Description adaptiveTtlfalseEnable adaptive TTL adaptiveTtlMultiplier20p95Ms × multiplier = TTL secondsadaptiveTtlMin10Floor TTL in seconds adaptiveTtlMax86400Ceiling TTL in seconds (24 h) metrics()gains anadaptiveTtlsub-object when the feature is enabled, reportingtrackedKeysand the top-20 slowest keys by p95 fetch latency with their currently adapted TTLs.l1EvictionWatermarkoption —SmartMemoryCachenow supports a configurable watermark (fraction0–1, default0.9) that controls when proactive eviction fires ahead of the hard capacity ceiling. Wired throughCacheOptions.l1EvictionWatermark. Lower values (e.g.0.8) amortise eviction cost more aggressively at the expense of slightly more frequent eviction rounds; raise to0.95on workloads where eviction is extremely rare to squeeze a few extra percent of L1 utilisation.
Performance
Zero-allocation
smartEvict()— The L1 eviction hot path previously allocated approximately 2.9 million short-lived heap objects per second at sustained eviction rates: two freshArrayinstances, up to 16{key, score}object literals, a spread operator for merging candidate pools, a comparator closure forArray.sort(), and aslice()call per eviction. All allocations are now eliminated:_evictPool/_evictGPool— two fixed-size pools of 16{key: '', score: 0}slots allocated once at class construction and mutated in-place on every call.- Manual merge loop replaces the spread operator.
- Hand-written insertion sort (N ≤ 16, max 256 comparisons) replaces
Array.sort()— no comparator closure, no timsort start-up, no allocation. - Index-based eviction loop replaces
slice(0, EVICT_COUNT).
Result: 0 heap allocations per
smartEvict()call. Eviction soak CV reduced from ~23 % to ~17 % under pathological 100 %-fill load. The remaining ~17 % is the irreducible V8 old-generation GC floor for entry objects andpack()Buffers — structurally unavoidable without moving storage off the JS heap.
Docs
- BENCHMARKS.md — Added "Eviction hot-path — zero-allocation design" section documenting every allocation site that was removed, the pre-allocated pool design, and the before/after CV numbers with an explanation of the practical floor.
- BENCHMARKS.md — Added "What tricache is very good at" section: a reference table mapping the nine problems tricache was engineered to solve (thundering herd, priority inversion under flood, write-pressure latency spikes, GC pressure from bloom probes, etc.) to the concrete mechanism and the benchmark row that proves it.
[0.5.1] — 2026-05-25
Added
onHit/onMisscallbacks — Two newCacheOptionshooks for per-tier hit/miss observability without waiting for theonMetricsinterval.onHit(key, tier)fires on every L1, disk, or L2 hit;onMiss(key)fires when all three tiers are exhausted.typescriptCacheService.create({ onHit: (key, tier) => cloudwatch.putMetricData({ key, tier }), onMiss: (key) => cloudwatch.putMetricData({ key }), });frozenmode (development guard) — NewCacheOptions.frozenoption. Whentrue, every value returned from an L1 hit is recursively frozen withObject.freeze()before being handed to the caller. Mutation attempts throwTypeErrorimmediately, catching reference-semantic corruption bugs that would otherwise silently corrupt cached entries. Intended for non-production environments only.typescriptCacheService.create({ frozen: process.env.NODE_ENV !== 'production' });tagsincache.get()opts —tagscan now be supplied directly in theoptsargument ofcache.get(). WhenfetchFnfires on a miss and populates the entry, the listed tags are automatically registered in both the in-processtagIndexand (if Redis is enabled) the Redis SADD index. This removes the need to callcache.set()separately just to attach tags.typescriptconst user = await cache.get( `user:${id}`, () => db.users.find(id), 300, { tags: ['users', `tenant:${tenantId}`] }, );DiskTier.purgeNextBucket()— New public method that purges expired entries from exactly one of 256 subdirectory buckets per call.CacheServicenow drives disk cleanup with a 30-secondsetInterval(one bucket/tick → full sweep in ~128 minutes) instead of the former blockingpurgeExpired()call every 5 minutes. Per-tick event-loop occupancy is bounded regardless of disk entry count. V3 filenames (expiry encoded in name) skip all file I/O for live entries.
Fixed
AES-128-CTR encryption correctness —
cipher.final()/decipher.final()return values are now captured and appended in all four encrypt/decrypt paths (encryptString,decryptString,encryptBuffer,decryptBuffer). For CTR mode the final block is almost always empty, but discarding it was technically incorrect and could corrupt multi-byte plaintexts whose length is not a cipher-block multiple. Both the string and buffer paths inencryption.tsare corrected.File descriptor leak in
DiskTier.purgeExpired()— The V2 header-read loop opened anfdviafs.openSync()inside a try/catch but only calledfs.closeSync()on the success path. An exception between open and close (e.g. permission error onstatSync) left the descriptor open. Afinallyblock now closesfdwhenever it is ≥ 0.Disk spill no longer blocks the L1 eviction call chain —
disk.save()(called from the L1diskSpillcallback during eviction) anddisk.delete()(called fromcache.delete()on pattern deletes) are now both deferred viasetImmediate(). The synchronous SHA-256 hash, msgpackr pack, and filesystem syscalls inside those calls no longer occupy the event loop on the criticall1.set()→smartEvict→diskSpillpath.
Performance
Size-aware Bloom filter —
createBloomFilter()now acceptsmaxEntriesand selects between the WASM filter (hardcoded at 100 K bits, rated for ≈ 10 400 entries at 1 % FP) and a right-sized pure-JS filter. For caches configured with more entries than the WASM filter's capacity, the JS filter is instantiated with optimal bit count () and hash count ( , clamped to [4, 10]). Prevents FP rate saturation that was silently forcing wastedMap.get()calls on every definite miss in large caches.Pure-string glob matcher replaces
RegExpondeletePatternhot path — Three-level fast path: (1) trailing-only wildcard matching a configured category prefix → O(1) via existingcategoryKeysindex; (2) exactly one'*'→ inlinestartsWith+endsWith+ length check, zero allocation; (3) general multi-'*'→ split once, thenglobMatchParts()(prefix anchor + suffix anchor + left-to-rightindexOffor middle segments, no backtracking).RegExpconstruction and.test()are gone from all three paths.Proactive eviction watermark —
SmartMemoryCachenow runs a single eviction pass whenever either the entry count or byte usage crosses 90 % of its configured ceiling, even while headroom remains. This amortises eviction cost across many writes instead of deferring it until the hard ceiling triggers a large forced eviction.Bloom filter dirty-count threshold proportional to
maxEntries— The per-delete dirty counter cap is nowmax(256, min(capacity >>> 2, ceil(maxEntries × 0.05))). Without the cap, a right-sized JS filter for 50 K entries allowed ~12 500 ghost entries before a rebuild (5× longer than the 10 K WASM filter), raising the measured false-positive rate. The new formula restores the original cadence: rebuild after ~5 % of configured entries are deleted.Bloom
add()skipped on overwrites — Re-adding an existing key to the Bloom filter inflated theinsertionscounter, delaying phantom-bit detection (trigger 2). Theadd()call is now guarded byif (!existingEntry).WASM module compiled once — The
WasmBloomFilterconstructor previously callednew WebAssembly.Module(bytes)on every instantiation. The compiled module is now a module-level constant (BLOOM_WASM_MODULE), compiled once at import time. MultipleSmartMemoryCacheinstances (e.g. per-namespace) share the compiled module.Null OTEL span singleton —
CacheService._nullSpanreplaces the per-call{ setAttribute() {…}, end() {…} }object literal returned when no tracer is configured. Eliminates one heap allocation perget/set/deletecall in the common no-tracer path.span.setAttribute()guarded by tracer presence —setAttribute('cache.key_prefix', …)is now only called when a tracer is actually configured, avoiding a string-split and a method dispatch on the null span for every operation._registerTags()extracted — Tag registration logic (in-processtagIndexupdate + RedisSADD/EXPIREpipeline) is deduplicated into a single private_registerTags()method shared byset()and theget()-miss populate path.revalidating.has(k)deferred past threshold check — TheSet.has()lookup (~30 ns) for the inflight revalidation guard is now only executed whenshouldRefreshAhead || shouldXFetchis already true, saving the lookup on every warm hit when neither threshold is crossed.SmartMemoryCache.scan()— non-generator bulk traversal — New public method that accepts a callback(key, entry, prefixLen) => voidand iterates all live entries in a singlefor…ofloop. Avoids generator state-machine overhead and per-entry tuple allocation compared to the existingliveEntries()generator. Intended forCacheServicebulk operations (e.g.hotKeys, snapshot serialisation) where the generator protocol adds measurable overhead.
[0.5.0] — 2026-05-23
Performance
CacheHitsingleton — zero allocation on L1 hot reads —SmartMemoryCache.get()previously returned a freshly allocated{ value, isStale, expiresAt, ttlMs, delta }object on every call. The object is now a module-level singleton that is mutated in-place and returned. JS is single-threaded, so callers always consume all fields synchronously before the nextget()call — the pattern is safe. One heap allocation eliminated per L1 hit.Measured impact: L1 hot-hit throughput 2.60 M/s → 2.81 M/s (+8 %), exact delete 3.94 M/s → 5.36 M/s (+36 %) (GC pressure reduction frees the CPU budget used by the delete micro-benchmark's tight loop).
Deferred
inferPriority— 3×string.includes()saved on every warm hit — WhenrefreshAheadorxfetchBetaopts are active,inferPriority(cacheKey)was computed unconditionally on every warm L1 hit even when the threshold check was false and no background recompute fired. The call is now deferred inside theif (shouldRefreshAhead || shouldXFetch)branch so the threestring.includes()scans only run when a recompute actually triggers.
Documentation
- BENCHMARKS.md fully refreshed — All 20+ measurement tables updated with the May 2026 macro-suite numbers. Four new sections added:
hotKeys(n)— live frequency ranking (O(n) scan + O(n log n) sort; slice size has negligible effect)- Refresh-ahead overhead — 340.8 ns/op in the full macro-suite (< 5 % in isolation; V8 polymorphic-IC effect from adjacent iterator benchmarks explained)
setIfAbsent()— fast path 31.59 µs (l1.has → false), miss path 78.98 µs (l1.set + eviction at capacity)- Negative caching (
notFoundTtl) — null hit 105.08 µs vs non-null 95.49 µs (no overhead for null values)
[0.4.1] — 2026-05-23
Fixed
- Backplane-aware
dependsOncascade — When a fleet peer published adelmessage for a parent key, the receiving instance evicted the parent from its L1 but did not cascade to any entries that declareddependsOncontaining that key. The cascade (_cascadeDependencies) now runs on every incomingdelbackplane message, not just on the originating instance. Single-process behavior is unchanged; fleet environments now correctly evict dependents on all nodes.
Added
mgetper-key TTL — Thettlparameter ofcache.mget()now accepts a function(key: string) => numberin addition to a plainnumber. The function is called only for miss keys, enabling heterogeneous TTLs in a single batch call. Plain-number callers are unaffected.typescriptconst results = await cache.mget( ['user:1', 'config:global', 'user:2'], fetchFn, (key) => key.startsWith('config:') ? 3600 : 300, );cache.ready()— startup warm-up lifecycle hook — Returns aPromise<void>that resolves once the cache is fully initialised and any startup warming configured viawarmKeyshas completed. Resolves immediately whenwarmKeysis not set. Designed for k8s readiness probes: gate traffic until L1 is warm, then open the gate once and never block again.warmKeysoption — Companion tocache.ready(). Pass a Redis key glob pattern ('user:*') to automatically callwarmFromL2(warmKeys)at construction time. No-op when Redis is disabled or unreachable.typescriptconst cache = CacheService.create({ warmKeys: 'user:*' }); await cache.ready(); // resolves once warmFromL2('user:*') finishes // k8s readiness endpoint returns 200 only after this point
[0.4.0] — 2026-05-23
Added
Negative caching (
notFoundTtl) — Prevent repeated upstream calls for keys that genuinely do not exist. WhenfetchFnreturnsnullorundefined, the result is now cached fornotFoundTtlseconds rather than bypassing the cache entirely. Configurable globally viaCacheOptions.notFoundTtland overridden per-call viaopts.notFoundTtlincache.get().typescript// Global: cache all "not found" results for 30 s CacheService.create({ notFoundTtl: 30 }); // Per-call override const user = await cache.get('user:999', () => db.users.find(999), 300, { notFoundTtl: 10 });cache.setIfAbsent(key, value, ttlSeconds?)— Atomic "set if not cached". Checks L1 first; if absent, attempts a RedisSET NX EX; on success, populates L1. Returnstrueif the value was written,falseif a live entry already existed. Zero-cost on the common "already cached" path — no Redis round-trip.typescriptconst written = await cache.setIfAbsent(`session:${id}`, sessionData, 3600); if (!written) { /* session already exists — do not overwrite */ }Refresh-ahead (
opts.refreshAhead) — Per-call opt oncache.get(). When the remaining TTL falls at or belowttl × (1 - refreshAhead), a background recompute is triggered transparently — callers always receive the cached value with zero added latency. Complements SWR: refresh-ahead fires before the entry becomes stale; SWR fires after.typescript// Recompute in background when ≤ 20 % of TTL remains const config = await cache.get('config:global', fetchConfig, 3600, { refreshAhead: 0.2 });XFetch probabilistic early expiry (
opts.xfetchBeta) — Per-call opt oncache.get(). Implements the XFetch algorithm: recompute probability increases as expiry approaches and scales with last fetch duration, preventing thundering-herd spikes on expiry. HigherxfetchBetavalues recompute earlier;1.0is the standard starting point.typescript// Probabilistic background recompute scaled to fetch duration const feed = await cache.get('feed:home', fetchFeed, 600, { xfetchBeta: 1.0 });dependsOncascade invalidation (opts.dependsOnoncache.set()) — Tag any entry with one or more parent keys. When a parent key is deleted (exact or glob), all dependents are automatically evicted from L1. No separate invalidation call needed.typescriptawait cache.set('org:42:members', members, 300, undefined, { dependsOn: ['org:42'] }); await cache.set('org:42:config', config, 300, undefined, { dependsOn: ['org:42'] }); await cache.delete('org:42'); // also evicts org:42:members and org:42:configcache.hotKeys(n?)— Returns the topn(default 10) live L1 keys ranked by Count-Min Sketch access frequency, with entry size. Expired entries are excluded; namespace prefix is stripped. Useful for debugging cache hotspots, adaptive pre-warming, and capacity planning.typescriptconst hot = cache.hotKeys(5); // [{ key: 'user:1', hits: 1024, sizeBytes: 512 }, ...]
Performance
- Refresh-ahead/XFetch overhead eliminated — Previously, the
refreshAhead/xfetchBetacode path calledl1.get(k)followed immediately byl1.getEntry(k)to readexpiresAt,ttlMs, anddelta— two full Map traversals plus threeDate.now()calls on every warm L1 hit when either opt was active. TheCacheHitinterface now carriesexpiresAt,ttlMs, anddeltadirectly, populated inSmartMemoryCache.get()from the entry already in hand. ThegetEntry()call is gone. Warm L1 hit overhead withrefreshAhead/xfetchBetaopts drops from +49 % to < 2 % (one extraDate.now()+ three arithmetic ops).
[0.3.1] — 2026-05-23
Added
TTL jitter (
ttlJitterFactor) — Spread cache expirations across a configurable ± window to prevent synchronised stampedes when large numbers of entries expire simultaneously ("thundering cliff"). SettingttlJitterFactor: 0.15multiplies each TTL by a random factor in[0.85, 1.15]. Clamped to[0, 1]; default0(no jitter). Applied in bothset()and the populate path ofget().typescriptCacheService.create({ ttlJitterFactor: 0.15 }); // ± 15 % TTL spreadBatch write operations —
mset()/mdel()— Write or delete many keys in a single call without hand-rollingPromise.all.typescriptawait cache.mset({ 'user:1': { value: alice, ttl: 300, priority: CachePriority.HIGH }, 'user:2': { value: bob, ttl: 300 }, }); await cache.mdel(['user:1', 'user:2']);Native OpenTelemetry span integration (
traceroption) — Pass any@opentelemetry/api-compatible tracer and tricache will emit spans forget,set, anddeleteoperations. Structurally typed — no@opentelemetry/apipeer dependency; works with any compliant tracer.Span names:
tricache.get,tricache.set,tricache.delete
Attributes set:cache.key_prefix(first:segment),cache.hit('l1'|'disk'|'l2'|'miss')typescriptimport { trace } from '@opentelemetry/api'; CacheService.create({ tracer: trace.getTracer('my-app') });Two lightweight interfaces are exported for typing without an OTEL peer dep:
typescriptimport type { ICacheTracer, ICacheSpan } from 'tricache';L2 circuit breaker — Automatically suspends Redis calls after
l2CircuitBreakerThresholdconsecutive failures (default 5) and resumes a probe afterl2CircuitBreakerCooldownMs(default 30 000 ms). A successful probe resets toCLOSED; a failed probe re-opens immediately. State is exposed incache.metrics().l2CircuitBreaker.state('closed'|'open'|'half_open').typescriptCacheService.create({ l2CircuitBreakerThreshold: 3, // open after 3 consecutive Redis errors l2CircuitBreakerCooldownMs: 10_000, // probe again after 10 s });warmFromL2(pattern)— Scan Redis for keys matching a glob pattern and pre-populate L1 before serving traffic. Returns the number of keys loaded. Returns0silently when Redis is disabled or unreachable, so it is safe to call unconditionally at startup.typescriptconst loaded = await cache.warmFromL2('user:*'); console.log(`Warmed ${loaded} user entries from Redis`);
[0.3.0] — 2026-05-23
Added
Count-Min Sketch frequency tracking — A 4 × 512
Uint16Array(4 KB, fits in L1d cache) now records historical access frequency for every key in L1. The per-entryhitscounter resets to 1 whenever a key is re-admitted after eviction; the sketch retains the cross-eviction frequency so a key that was accessed 80 times before being evicted scores far above a burst key whosehits = 1. Benchmark: 76 % of long-resident keys survive a same-priority burst flood of 60 new keys against 50 established residents.- Hash: four independent Murmur3-fragment mixes derived from one FNV-1a seed — all computed inline from a single string scan.
- Decay: all counters halved (right-shift) every 100 000 inserts — frequency-ages old counts so a past burst cannot protect a key indefinitely.
- Zero external dependencies; 4 KB fixed footprint regardless of cache size.
Iterator interface on
CacheService— Three lazy generator methods that skip expired entries without allocating intermediate arrays:Method Returns Notes cache.keys()Generator<string>Namespace prefix stripped per yield; no [key, entry]tuple allocatedcache.values<T>()Generator<T>yield*delegation — no intermediate generator framecache.entries<T>()Generator<[string, T]>Yields [strippedKey, value]pairsAll three iterate only live (non-expired) L1 entries and silently skip entries whose TTL has elapsed since the last background cleanup sweep.
typescriptfor (const key of cache.keys()) console.log(key); for (const value of cache.values<User>()) process(value); for (const [key, user] of cache.entries<User>()) sync(key, user);
Performance
keys()+19 % throughput (29.0 K/s → 34.5 K/s) —liveKeys()onSmartMemoryCacheyields the key string directly from the Map iteration without constructing an intermediate[key, entry]tuple.values()+4 % throughput (33.7 K/s → 35.1 K/s) —liveValues()usesyield*delegation fromCacheService.values(), collapsing one generator frame. IteratesMap.values()directly so the key is never loaded into the yielded code path.
Internal
- Removed dead
rawEntries()generator — An intermediate[key, resolvedValue]generator was explored as an optimization path forentries()but proved slower due to V8 inline-cache (IC) type-feedback sharing: placing theentry.value !== undefinedternary inside the generator frame disrupted the tight-loop optimization that V8 applies toliveEntries(). Removing it reduced the generator count onSmartMemoryCache.cachefrom 4 → 3, which recovered theentries()monomorphic JIT budget (see BENCHMARKS.md — Iterator interface trade-offs). - 8 new tests (141 total): 3 Count-Min Sketch tests (
liveEntries()expiry, empty cache, sketch burst-flood survival) + 5 iterator tests onCacheService(keys()namespace stripping,values()deserialization,entries()pairs, expiry skip, empty iterator).
[0.2.0] — 2026-05-23
Performance
L1 hot-get: +112 % throughput (1.25 M/s → 2.65 M/s, 800 ns → 377 ns) — Every cache entry now stores the deserialized JS value alongside its msgpackr buffer.
get()returns the live object directly — zerounpack()call on the hot path. The packed buffer (data) is retained for disk spill and cold-start snapshot serialization, so cross-process behavior is unchanged.Bloom-filter hit path: +44 % throughput (2.26 M/s → 3.26 M/s) — direct benefit of eliminating the
unpack()call that followed the Map lookup.CacheService L1 warm-hit: +64 % throughput (1.32 M/s → 2.16 M/s) —
getIfFresh()andmget()also returnentry.valuedirectly, skipping deserialization end-to-end.CacheService SWR stale-serve: +38 % throughput (1.48 M/s → 2.04 M/s) — same
entry.valuefast path in the stale-serve code.setthroughput unchanged —set()still callspack()exactly once; the only addition is storing the reference asentry.value(a pointer copy, not a serialization round-trip).
Memory note: each L1 entry now holds both the packed
Bufferand the live JS object. For a typical cache payload this roughly doubles the per-entry heap overhead vs a packed-only store. Thesizefield (used for eviction pressure) still reflectspacked.byteLength— tunel1MaxEntries/l1MaxBytesaccordingly.
Reference semantics:
get()now returns a direct reference to the cached object, not a fresh deep copy. Mutating the returned value will corrupt the cached entry. This is consistent with high-performance in-process caches (node-lru-cache, quick-lru, etc.). If immutability is required, deep-clone at the call site.
Fixed
Benchmark
[object Object]logger bug — The multi-tenancy category-starvation section was printing raw{ entries, hits }objects in template strings becausegetStats().categories[key]returns{ entries: number; hits: number }, not a plain number. The relevant variables now correctly access.entries.Benchmark tenant-parity variance —
org_aandorg_bnamespace throughput benchmarks previously used independentMath.random()calls, so each run saw a different operation distribution. Both namespaces now share a single pre-generated random sequence so they execute identical workloads and JIT-warmth effects don't skew the A/B ratio.Benchmark tenant-parity JIT warmth — Even with an identical operation sequence,
org_bwas still faster because it ran afterorg_a's 10 000 timed iterations had already compiled all shared CacheService / inflight-Map hot paths. Both closures are now materialised upfront and warmed in an interleaved pass (400 alternating iterations each) before either timed run begins. Parity ratio is now consistently ≈ 1.00× (previously 0.74–0.81×).
Added
cache.clear(prefix?)— Flush all cached entries with a single call. Passing an optional prefix (e.g.'user:abc') limits the flush to keys with that prefix, scoped to L1 and Redis. Replaces the previous workaround ofdelete('prefix:*').cache.rebalance()— Evict L1 entries that violate the current category or global capacity limits. Useful whencategoryLimitsare tightened after startup; previously, existing entries were never re-evaluated until they expired naturally.cache.ttl(key)— Return the remaining TTL in seconds for a key currently held in L1, without fetching or consuming the value. Returnsnullif the key is absent or expired. Useful for SWR decisions and debugging.cache.writeSnapshot(altPath?)—writeSnapshot()now accepts an optional path argument. CallingwriteSnapshot('/tmp/backup.snap')writes to that path without touching the configured default snapshot file. Useful in graceful-shutdown hooks. The zero-argument form is unchanged.DiskTier.clear()— Internal method used bycache.clear()to flush the entire L1.5 disk tier.cache.has(key)— Returntrueif the key exists in L1 and has not expired. Uses the bloom filter as a fast-path negative check. No fetch, no disk or Redis round-trip.cache.touch(key, newTtlSeconds)— Extend the TTL of a key in L1 (and fire-and-forgetEXPIREin Redis) without reading or re-fetching its value. Returnsfalseif the key is absent or already expired.cache.getIfFresh(key)— Return the L1-cached value only if it is fresh (not yet in the SWR grace window). Returnsnullwhen absent, expired, or stale — without triggering a revalidation. Useful for read-your-writes patterns.cache.mget(keys, fetchFn, ttl)— Batch read. Returns cached values for hot keys and callsfetchFnonly with the keys that missed L1. Preserves input ordering.Tag-based invalidation — Tag entries on write and invalidate whole groups atomically:
tsawait cache.set('product:1', data, 60, undefined, { tags: ['catalog'] }); await cache.invalidateTag('catalog'); // evicts all entries tagged 'catalog'Tags are tracked in-process and mirrored to Redis
SADD/SMEMBERSfor multi-instance consistency.cache.ping()— Measure L1 / disk / Redis latency in milliseconds. Returns{ l1, disk, l2 }—l2isnullwhen Redis is disabled. Suitable for health-check endpoints.cache.drainToL2()— Pipeline all live L1 entries to Redis in a single round-trip. Useful for warming a new Redis node or for zero-downtime failover.CacheService.createAsync(optionsOrPromise)— Async factory that resolves aPromise<CacheOptions>before constructing the singleton. Useful when configuration is fetched from a secret store at startup.staleIfErroroption — Number of seconds to extend a stale L1 entry's expiry when a SWR revalidation fetch fails. Prevents serving errors while the upstream is temporarily down.tsCacheService.create({ staleIfError: 300 }) // keep stale for 5 more minutes on errorl2WriteModeoption — Set to'read-only'to allow Redis reads (L2 hits, snapshot load) while skipping all Redis writes (set,delete,clear, tag sync). Useful for read-replicas, canary deployments, or cost-reduction in read-heavy workloads.tsCacheService.create({ l2WriteMode: 'read-only' })onEvictioncallback — Called synchronously whenever L1 evicts a key, with the key name and the reason ('capacity'|'category'|'rebalance'|'oom'|'ttl'|'manual'). Never throws — errors inside the callback are silently swallowed to protect cache stability.tsCacheService.create({ onEviction: (key, reason) => metrics.increment(`cache.eviction.${reason}`), })instanceNameoption — Prometheusinstancelabel added to every metric emitted bytoPrometheusText(). Useful when multipleCacheServiceinstances push to the same Prometheus endpoint.tsCacheService.create({ instanceName: 'api-us-east-1' })previousEncryptionKey/previousEncryptionModeoptions — Zero-downtime encryption key rotation. The cache tries the current key first; if decryption fails it transparently retries with the previous key. RemovepreviousEncryptionKeyafter all old entries have expired.tsCacheService.create({ encryptionKey: newKeyBase64, previousEncryptionKey: oldKeyBase64, })stats().l1.categoriesformat — Each category entry now exposes bothentries(count of live keys) andhits(L1 cache hits since startup), instead of only the entry count. Old code that readstats().l1.categories['prefix:']as a plain number should read.entriesor.hitsinstead.
Fixed
increment()now works without Redis — Previouslyincrement()silently returned0every call when Redis was disabled (the default in non-production environments), making any rate-limiting logic that compared the result against a threshold permanently bypassable. It now maintains per-key counters in L1 memory with the same TTL semantics, returning1,2,3… as expected. Behaviour with Redis enabled is unchanged.stats().l1now exposessizeBytes—stats().l1previously only returnedsizeKB(a rounded integer) while the internal tracking andmetrics().l1used raw bytes. BothsizeBytes(exact) andsizeKB(rounded, kept for backwards compatibility) are now present onstats().l1.Redis reconnect bug — When the initial Redis connection attempt failed, the internal
redisConnectingPromise was cached in the rejected state and never reset. All subsequent calls to any Redis-backed method would fail immediately without retrying. The rejected Promise is now cleared on failure, allowing the next call to attempt a fresh connection.
Migration
All additions are backward-compatible with one exception:
writeSnapshot()with no arguments behaves identically to0.1.0.stats().l1.sizeKBis still present;sizeBytesis a new addition.increment()return values change from0to an accumulating count only when Redis is disabled. If your code compared the result to a threshold (e.g.if (count >= limit) { ... }), it will now work correctly in dev/test. If you were explicitly relying on the0return to detect a disabled-Redis state, checkcache.metrics().backplane.enabledinstead.stats().l1.categoriesshape changed — values changed fromnumberto{ entries: number; hits: number }. Update any code readingstats().l1.categories[key]directly.
[0.1.0] — 2026-05-01
Initial release.

