{"id":50400228,"url":"https://github.com/kareem411/tricache","last_synced_at":"2026-05-30T23:02:21.549Z","repository":{"id":359569133,"uuid":"1246673132","full_name":"Kareem411/TriCache","owner":"Kareem411","description":"The ultra-performance, unified caching engine for Node.js.⚡Zero-JSON binary cache for Node.js. 🛡️ Hard multi-tenant isolation, deterministic reservoir eviction, inlined WASM Bloom filters, and polymorphic hardware ciphers (GCM/CTR/XOR). 🚀","archived":false,"fork":false,"pushed_at":"2026-05-22T13:36:57.000Z","size":719,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-22T18:05:59.295Z","etag":null,"topics":["cache","disk-cache","encryption","lfu","lru","nodejs","performance","redis","typescript","valkey"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kareem411.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-22T12:37:55.000Z","updated_at":"2026-05-22T13:37:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Kareem411/TriCache","commit_stats":null,"previous_names":["kareem411/tricache"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Kareem411/TriCache","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kareem411%2FTriCache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kareem411%2FTriCache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kareem411%2FTriCache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kareem411%2FTriCache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kareem411","download_url":"https://codeload.github.com/Kareem411/TriCache/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kareem411%2FTriCache/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33712580,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","disk-cache","encryption","lfu","lru","nodejs","performance","redis","typescript","valkey"],"created_at":"2026-05-30T23:02:17.522Z","updated_at":"2026-05-30T23:02:21.540Z","avatar_url":"https://github.com/Kareem411.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TriCache\n\n[![CI](https://github.com/Kareem411/TriCache/actions/workflows/ci.yml/badge.svg)](https://github.com/Kareem411/TriCache/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/tricache.svg)](https://www.npmjs.com/package/tricache)\n[![npm downloads](https://img.shields.io/npm/dm/tricache.svg)](https://www.npmjs.com/package/tricache)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Node.js ≥ 22](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.x%20%7C%206.x-blue)](https://www.typescriptlang.org)\n[![L1 get](https://img.shields.io/badge/L1%20get-2.81%20M%2Fs-brightgreen)](BENCHMARKS.md)\n[![Thundering herd](https://img.shields.io/badge/thundering%20herd-100%25%20coalesced-brightgreen)](BENCHMARKS.md)\n\ntricache is an extremely fast three-tier Node.js cache library. It serves warm reads at **2.81 million operations per second** from a single thread — over 100× faster than a localhost Redis round-trip and below any network latency floor. When L1 fills, evicted entries spill to a local NVMe disk tier rather than being dropped, keeping hit rates high without unbounded RAM growth. Cache misses that reach L2 (Redis or Valkey) are automatically coalesced: no matter how many concurrent callers miss the same key, `fetchFn` fires exactly once. See the [performance section](#-performance) for full numbers. Stale-While-Revalidate, AES-256-GCM at-rest encryption, pub/sub fleet-wide invalidation, OOM guard, cold-start snapshots, and Prometheus metrics are also supported through optional configuration — with zero required fields to get started.\n\n\u003cimg src=\"https://raw.githubusercontent.com/Kareem411/TriCache/master/public/SmartMemoryCache_DiskTier.jpeg\" width=\"600\" alt=\"tricache architecture\" /\u003e\n\n---\n\n## ✨ Features\n\n| Feature | Detail |\n|---|---|\n| **Adaptive eviction** | LFU × LRU × priority score + Count-Min Sketch cross-eviction frequency; reservoir-sampled O(1) hot path; category limits prevent any prefix monopolising RAM |\n| **Count-Min Sketch** | 4 × 512 `Uint16Array` (4 KB) tracks historical access frequency across eviction boundaries — same-priority burst keys cannot displace long-resident entries; **78 % survival rate** in benchmark flood tests |\n| **WASM Bloom filter** | 562-byte binary inlined as Base64 — O(k=7) guaranteed-miss detection, no filesystem access, pure-JS fallback |\n| **msgpackr serialization** | All entries packed with msgpackr — uniform binary format, no JSON at any payload size |\n| **Stale-While-Revalidate** | Serve stale instantly, revalidate in background — zero added latency on cache hit |\n| **Stale-if-error** | Extend a stale entry's TTL when SWR revalidation fails — no errors served during upstream outages |\n| **Thundering-herd prevention** | Inflight `Promise` registry — only one `fetchFn` call per key regardless of concurrency |\n| **Pub/sub invalidation backplane** | Redis pub/sub channel propagates deletes across all instances in real time |\n| **Tag-based invalidation** | Tag entries on write; `invalidateTag('catalog')` evicts all matching entries from L1, disk, and Redis atomically |\n| **Batch read** | `mget()` collects L1 hits, calls `fetchFn` only for misses, preserves ordering |\n| **Batch write** | `mset()` / `mdel()` write or delete many keys in a single `Promise.all` call |\n| **TTL jitter** | `ttlJitterFactor` spreads expirations across a configurable ± window — prevents thundering-cliff mass-expiry |\n| **OpenTelemetry spans** | Structural `ICacheTracer` / `ICacheSpan` interfaces — pass any OTEL-compatible tracer; no peer dep required |\n| **L2 circuit breaker** | Suspends Redis after N consecutive failures; auto-probes after cooldown; state visible in `metrics()` |\n| **`warmFromL2(pattern)`** | Scan Redis and pre-populate L1 at startup; returns count loaded; no-op when Redis unavailable |\n| **OOM guard** | Polls `heapUsed/heapTotal` on a timer; emergency-evicts coldest L1 entries before the process crashes |\n| **Cold-start snapshot** | L1 serialised to disk on `SIGTERM`/`SIGINT`, reloaded on next startup — warm cache, cold process |\n| **AES-256-GCM encryption** | L2 (Redis) values, disk spill files, and snapshots encrypted at rest; zero-downtime key rotation via `previousEncryptionKey` |\n| **Prometheus metrics** | `cache.metrics()` + `CacheService.toPrometheusText()` — drop into any `/metrics` endpoint |\n| **Distributed counter** | `cache.increment()` backed by Redis `INCR` for distributed rate limiting; in-process fallback when Redis is disabled |\n| **Pluggable logger** | Bring your own `pino`, `winston`, etc. |\n| **L2 read-only mode** | `l2WriteMode: 'read-only'` reads from Redis but skips all writes — canary deploys, read replicas |\n| **Eviction callback** | `onEviction(key, reason)` fires on every L1 eviction with a typed reason string |\n| **Negative caching (`notFoundTtl`)** | Cache `null`/`undefined` fetchFn results for a configurable TTL — prevents hammering upstream on repeated misses |\n| **`setIfAbsent()`** | Atomic \"set if not cached\" — L1 `has()` check → Redis `SET NX EX` → L1 set on success; returns `true` if written, `false` if already present |\n| **Refresh-ahead** | Proactively recompute an entry in the background when remaining TTL falls below a configured fraction — zero-latency freshness |\n| **XFetch probabilistic early expiry** | Probabilistic background recompute keyed to last fetch duration and `xfetchBeta` — optimal protection against expiry spikes under load |\n| **Adaptive TTL** | Tracks per-key fetch latency in a rolling ring buffer; once ≥ 5 samples are collected, automatically sets TTL = `p95LatencyMs × multiplier`. Expensive keys get cached longer; cheap keys stay close to their base TTL — no manual TTL tuning required |\n| **`hotKeys(n)`** | Returns top N keys by Count-Min Sketch access frequency with size — no full Map scan |\n| **`dependsOn` cascade invalidation** | Tag entries with parent keys; deleting a parent automatically evicts all declared dependents from L1 |\n| **`onHit` / `onMiss` callbacks** | Per-operation hit/miss hooks with tier info (`'l1'` \\| `'disk'` \\| `'l2'`) — no wait for the metrics interval |\n| **`frozen` mode** | Dev-time mutation guard — `Object.freeze()` applied recursively to every L1 hit so accidental mutations throw immediately |\n| **`tags` in `get()` opts** | Attach tags at read time; when `fetchFn` populates the entry on a miss the tags are registered automatically |\n\n---\n\n## 📦 Install\n\n```bash\nnpm install tricache\n# or\npnpm add tricache\n```\n\n---\n\n## 🚀 Quick start\n\n```typescript\nimport { CacheService, CachePriority } from 'tricache';\n\n// Get (or create) the process-level singleton\nconst cache = CacheService.create({\n  redisHost: 'my-redis.example.com',   // omit or set NODE_ENV!=production to disable L2\n});\n\n// Get-or-fetch with a 5-minute TTL\nconst user = await cache.get(\n  `user:${userId}`,\n  () =\u003e db.users.findById(userId),\n  300,\n);\n\n// Explicit set\nawait cache.set(`user:${userId}`, user, 300);\n\n// Delete one key\nawait cache.delete(`user:${userId}`);\n\n// Delete by glob pattern\nawait cache.delete(`user:${userId}:*`);\n\n// Stale-While-Revalidate: serve stale for up to 30 s while refreshing in background\nconst dashboard = await cache.get(\n  `dashboard:${orgId}`,\n  () =\u003e analytics.buildDashboard(orgId),\n  300,\n  { swr: 30 },\n);\n\n// Distributed rate-limiting counter\nconst hits = await cache.increment(`ratelimit:${ip}`, 60 /* TTL seconds */);\n\n// Check if a key is cached (fast, no fetch)\nconst isCached = cache.has(`user:${userId}`);\n\n// Batch read\nconst [userA, userB] = await cache.mget(\n  [`user:${userIdA}`, `user:${userIdB}`],\n  (missKeys) =\u003e db.users.findByIds(missKeys).then(rowsToMap),\n  300,\n);\n\n// Batch write\nawait cache.mset({\n  [`user:${userIdA}`]: { value: userA, ttl: 300 },\n  [`user:${userIdB}`]: { value: userB, ttl: 300 },\n});\n\n// Batch delete\nawait cache.mdel([`user:${userIdA}`, `user:${userIdB}`]);\n\n// Warm L1 from Redis at startup\nconst loaded = await cache.warmFromL2('user:*');\nconsole.log(`Pre-warmed ${loaded} user entries`);\n\n// Or auto-warm at construction + gate traffic with ready()\nconst cache2 = CacheService.create({ warmKeys: 'user:*' });\nawait cache2.ready(); // resolves once warm-up completes — ideal for k8s readiness probes\n\n// Atomic set-if-absent — returns true if written, false if key already cached\nconst written = await cache.setIfAbsent(`session:${id}`, sessionData, 3600);\n\n// Dependency cascade: deleting 'org:42' automatically evicts 'org:42:config'\nawait cache.set('org:42:config', config, 300, undefined, { dependsOn: ['org:42'] });\nawait cache.delete('org:42'); // also evicts org:42:config\n\n// Top 10 hottest keys by Count-Min Sketch frequency\nconst hot = cache.hotKeys(10);\nconsole.log(hot); // [{ key: 'user:1', hits: 842, sizeBytes: 512 }, ...]\n\n// Tag entries for group invalidation\nawait cache.set(`product:${id}`, product, 300, undefined, { tags: ['catalog'] });\nawait cache.invalidateTag('catalog'); // evict all catalog entries\n\n// Health check with tier latencies\nconst { l1, disk, l2 } = await cache.ping();\n\n// Prometheus metrics\nconst snap = cache.metrics();\nconsole.log(CacheService.toPrometheusText(snap));\n```\n\n---\n\n## ⚙️ Configuration\n\nAll options are optional — sensible defaults apply.\n\n```typescript\nCacheService.create({\n  // ── Namespace ─────────────────────────────────────────────────────────\n  // Isolates keys, disk dir, snapshot file, and Redis backplane channel.\n  // Two instances with different namespaces are fully independent.\n  namespace: 'my-app',\n\n  // ── Logger ────────────────────────────────────────────────────────────\n  logger: pinoLogger,               // default: console warn/error only\n\n  // ── L1 (in-memory) ───────────────────────────────────────────────────\n  l1MaxBytes:   200 * 1024 * 1024,  // 200 MB total RAM cap (default)\n  l1MaxEntries: 2_000,              // max entries in L1 (default)\n  l1EvictionWatermark: 0.9,         // proactive eviction fires at 90 % of l1MaxEntries / l1MaxBytes (default)\n                                    // lower to 0.8 to reduce GC pressure on heap-bound workloads\n  categoryLimits: {\n    // per-prefix limits — keys are matched by startsWith()\n    'user:':      { maxEntries: 500,  maxSizeBytes: 50  * 1024 * 1024 },\n    'analytics:': { maxEntries: 100,  maxSizeBytes: 20  * 1024 * 1024 },\n    'default':    { maxEntries: 1000, maxSizeBytes: 100 * 1024 * 1024 },\n  },\n\n  // ── L1.5 (disk spill) ────────────────────────────────────────────────\n  diskCacheDir:      '/tmp/my-app-cache',  // default: os.tmpdir()/tricache-disk\n  diskMaxBytes:      500 * 1024 * 1024,   // 500 MB (default)\n  diskEntryMaxBytes: 10  * 1024 * 1024,   // 10 MB per entry (default)\n\n  // ── L2 (Redis / Valkey) ──────────────────────────────────────────────\n  redisHost:    'my-redis.example.com',   // or REDIS_HOST env var\n  redisPort:    6379,\n  redisTls:     true,                     // default: true when NODE_ENV=production\n  disableRedis: false,                    // default: true when NODE_ENV!=production\n\n  // ── Invalidation backplane ───────────────────────────────────────────\n  // Redis pub/sub channel that propagates deletes to all instances.\n  // Enabled by default when Redis is active.\n  invalidationBackplane: true,\n\n  // ── OOM guard ────────────────────────────────────────────────────────\n  oomProtection:      true,   // enabled by default\n  oomHeapThreshold:   0.85,   // evict when heapUsed/heapTotal \u003e 85 %\n  oomCheckIntervalMs: 10_000, // poll every 10 s\n  oomEvictPercent:    0.20,   // evict coldest 20 % of L1 per trigger\n\n  // ── Encryption ───────────────────────────────────────────────────────\n  // base64-encoded 32-byte key; or set CACHE_ENCRYPTION_KEY env var.\n  // node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"\n  encryptionKey: process.env.CACHE_ENCRYPTION_KEY,\n\n  // Zero-downtime key rotation — remove after all old entries have expired\n  previousEncryptionKey:  process.env.PREV_ENCRYPTION_KEY,\n  previousEncryptionMode: 'aes-256-gcm', // defaults to current encryptionMode\n\n  // ── L2 write mode ────────────────────────────────────────────────────\n  // 'read-write' (default) — reads and writes to Redis\n  // 'read-only'            — reads from Redis, skips all writes (canary / replica)\n  l2WriteMode: 'read-write',\n\n  // ── Stale-if-error ───────────────────────────────────────────────────\n  // Extra seconds to extend a stale L1 entry's expiry when a SWR fetchFn fails.\n  // Prevents serving errors while the upstream is temporarily down.\n  staleIfError: 300, // keep stale for 5 more minutes on revalidation error\n\n  // ── Eviction callback ────────────────────────────────────────────────\n  // Called synchronously whenever L1 evicts a key.\n  // reason: 'capacity' | 'category' | 'rebalance' | 'oom' | 'ttl' | 'manual'\n  onEviction: (key, reason) =\u003e metrics.increment(`cache.eviction.${reason}`),\n\n  // ── TTL jitter ────────────────────────────────────────────────────────\n  // Multiply each TTL by a random factor in [1-j, 1+j] to spread expiry.\n  // Prevents mass-expiry stampedes (\"thundering cliff\").\n  // Range [0, 1]; default 0 (no jitter).\n  ttlJitterFactor: 0.15,  // ± 15 % spread\n\n  // ── Adaptive TTL ──────────────────────────────────────────────────────\n  // When true, tricache tracks per-key fetch latency in a rolling ring\n  // buffer and derives an optimal TTL from the p95 fetch duration:\n  //   adaptedTtl = clamp(p95LatencyMs × multiplier, min, max)\n  // The caller-supplied ttlSeconds is used until ≥ 5 samples are collected,\n  // then the library takes over TTL management autonomously.\n  adaptiveTtl:            true,\n  adaptiveTtlMin:         10,      // floor: never assign TTL below 10 s (default)\n  adaptiveTtlMax:         86400,   // ceiling: never exceed 24 h (default)\n  adaptiveTtlMultiplier:  20,      // p95Ms × 20 = TTL in seconds (default)\n\n  // ── OpenTelemetry tracer ──────────────────────────────────────────────\n  // Pass any @opentelemetry/api-compatible tracer. No peer dependency.\n  // Spans: 'tricache.get' | 'tricache.set' | 'tricache.delete'\n  // Attributes: cache.key_prefix, cache.hit ('l1'|'disk'|'l2'|'miss')\n  tracer: trace.getTracer('my-app'),\n\n  // ── L2 circuit breaker ────────────────────────────────────────────────\n  // Opens after N consecutive Redis errors; probes after cooldown ms.\n  // State visible in cache.metrics().l2CircuitBreaker.state\n  l2CircuitBreakerThreshold:  5,      // default\n  l2CircuitBreakerCooldownMs: 30_000, // default\n\n  // ── Negative caching ──────────────────────────────────────────────────\n  // Cache null/undefined fetchFn results for this many seconds globally.\n  // Prevents repeated upstream calls for keys that genuinely don't exist.\n  // Can be overridden per-call via opts.notFoundTtl in cache.get().\n  notFoundTtl: 30, // seconds; 0 = disabled (default)\n\n  // ── Startup warm-up ───────────────────────────────────────────────────\n  // Auto-call warmFromL2(pattern) at construction time.\n  // cache.ready() resolves once warm-up finishes — use as a k8s readiness gate.\n  // No-op when Redis is disabled or unreachable.\n  warmKeys: 'user:*',\n\n  // ── Prometheus instance label ─────────────────────────────────────────\n  // Adds an `instance` label to every metric in toPrometheusText().\n  instanceName: 'api-us-east-1',\n\n  // ── Cold-start snapshot ──────────────────────────────────────────────\n  snapshotPath:              '/tmp/my-app-cache-snapshot.msgpack',\n  snapshotMaxAgeMs:          2 * 60 * 60 * 1000,  // 2 hours (default)\n  forbiddenSnapshotPrefixes: ['auth:', 'session:', 'mfa:', 'rate_limit:'],\n\n  // ── Metrics callback ─────────────────────────────────────────────────\n  metricsIntervalMs: 60_000,                       // emit every 60 s (default)\n  onMetrics: (m) =\u003e myMonitoring.record(m),        // optional push callback\n\n  // ── Per-operation hooks ───────────────────────────────────────────────\n  // onHit fires on every L1, disk, or L2 hit with the caller-facing key (no prefix)\n  // and the tier that served it. Lower latency than waiting for onMetrics.\n  onHit:  (key, tier) =\u003e cloudwatch.putMetricData({ key, tier }),\n\n  // onMiss fires when all three tiers are exhausted — before fetchFn is called.\n  onMiss: (key) =\u003e cloudwatch.putMetricData({ key }),\n\n  // ── Development mutation guard ────────────────────────────────────────\n  // When true, every L1 hit value is deep-frozen before being returned.\n  // Mutation attempts throw TypeError immediately in development.\n  // Do NOT enable in production — deep-freezing large objects has measurable overhead.\n  frozen: process.env.NODE_ENV !== 'production',\n});\n```\n\n### Environment variables\n\n| Variable | Purpose |\n|---|---|\n| `REDIS_HOST` | Redis/Valkey hostname (used when `redisHost` option is not set) |\n| `CACHE_ENCRYPTION_KEY` | Base64-encoded 32-byte AES-256-GCM key |\n| `NODE_ENV` | When `!== 'production'`, L2 Redis and TLS are disabled by default |\n\n---\n\n## 📖 API reference\n\n### `CacheService.create(options?)` → `CacheService`\n\nReturns the process-level singleton. Options are only applied on the **first** call per namespace — subsequent calls return the existing instance.\n\n### `CacheService.createAsync(optionsOrPromise)` → `Promise\u003cCacheService\u003e`\n\nAsync factory that resolves a `Promise\u003cCacheOptions\u003e` before constructing the singleton. Useful when config is fetched from a secret store at startup.\n\n```typescript\nconst cache = await CacheService.createAsync(fetchSecretsFromVault());\n```\n\n### `CacheService.reset(options?)` → `CacheService`\n\nDestroys the existing singleton and creates a fresh one. Useful in tests.\n\n### `cache.get\u003cT\u003e(key, fetchFn, ttlSeconds?, opts?)` → `Promise\u003cT\u003e`\n\nGet from cache or call `fetchFn` on a miss. The inflight map ensures `fetchFn` fires at most once per key regardless of concurrency.\n\n\u003e **Reference semantics:** on an L1 hit, the returned value is the live JS object stored in the entry — not a deep copy. Mutating it will corrupt the cached entry. Deep-clone at the call site if you need an independent copy.\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `key` | `string` | — | Cache key |\n| `fetchFn` | `() =\u003e Promise\u003cT\u003e` | — | Called on a miss; result is cached |\n| `ttlSeconds` | `number` | `300` | Hard TTL in seconds |\n| `opts.swr` | `number` | `0` | Stale-While-Revalidate grace seconds |\n| `opts.priority` | `CachePriority` | auto-inferred | Eviction priority override |\n| `opts.refreshAhead` | `number` | — | Fraction `(0, 1]` of TTL — triggers background recompute when `remaining ≤ ttl × (1 - refreshAhead)` |\n| `opts.xfetchBeta` | `number` | — | XFetch β ≥ 0 — scales probabilistic early recompute by last fetch duration; higher = recompute earlier |\n| `opts.notFoundTtl` | `number` | — | Per-call TTL in seconds for `null`/`undefined` results (overrides global `notFoundTtl`) |\n| `opts.tags` | `string[]` | — | Tags to register when `fetchFn` populates the entry on a miss; no-op on L1/L2 hits where tags are already registered |\n\n### `cache.set\u003cT\u003e(key, data, ttlSeconds?, priority?, opts?)` → `Promise\u003cvoid\u003e`\n\nWrites to L1 and (in production) L2. Publishes an invalidation to the backplane.\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `opts.tags` | `string[]` | `[]` | Associate tags with this entry for group invalidation |\n| `opts.dependsOn` | `string[]` | `[]` | Parent keys — when any parent is deleted, this entry is automatically evicted from L1 |\n\n```typescript\nawait cache.set('product:1', data, 60, undefined, { tags: ['catalog', 'featured'] });\n\n// Cascade invalidation: evicting 'org:42' also evicts 'org:42:members'\nawait cache.set('org:42:members', members, 300, undefined, { dependsOn: ['org:42'] });\nawait cache.delete('org:42'); // org:42:members is evicted too\n```\n\n### `cache.mget\u003cT\u003e(keys, fetchFn, ttl?, priority?)` → `Promise\u003c(T | undefined)[]\u003e`\n\nBatch read. Returns L1-cached values for hot keys; calls `fetchFn` only with the keys that missed. Preserves input ordering.\n\n`ttl` accepts a **plain number** (uniform TTL) or a **function `(key: string) =\u003e number`** (per-key TTL). The function is only called for miss keys — L1 hits are unaffected.\n\n```typescript\n// Uniform TTL\nconst [userA, userB] = await cache.mget(\n  ['user:1', 'user:2'],\n  (missKeys) =\u003e db.users.findByIds(missKeys).then(rowsToMap),\n  300,\n);\n\n// Per-key TTL — heterogeneous data in one batch call\nconst results = await cache.mget(\n  ['user:1', 'config:global', 'feature:flags'],\n  fetchFn,\n  (key) =\u003e key.startsWith('config:') ? 3600 : 300,\n);\n```\n\n### `cache.mset\u003cT\u003e(entries)` → `Promise\u003cvoid\u003e`\n\nWrite multiple entries in a single call. Each entry accepts `value`, `ttl`, `priority`, and `tags`.\n\n```typescript\nawait cache.mset({\n  'user:1': { value: alice, ttl: 300, priority: CachePriority.HIGH, tags: ['users'] },\n  'user:2': { value: bob,   ttl: 300 },\n});\n```\n\n### `cache.mdel(keys)` → `Promise\u003cvoid\u003e`\n\nDelete multiple keys in a single call. No-op for keys that do not exist.\n\n```typescript\nawait cache.mdel(['user:1', 'user:2', 'user:3']);\n```\n\n### `cache.warmFromL2(pattern)` → `Promise\u003cnumber\u003e`\n\nScan Redis for keys matching a glob pattern (e.g. `'user:*'`) and load their values into L1 with a 10-minute TTL. Returns the number of keys loaded. Returns `0` immediately when Redis is disabled or unreachable — safe to call unconditionally at startup.\n\n```typescript\n// In your application startup\nconst loaded = await cache.warmFromL2('user:*');\nconsole.log(`Pre-warmed ${loaded} user entries from Redis`);\n```\n\n### `cache.ready()` → `Promise\u003cvoid\u003e`\n\nReturns a Promise that resolves once the cache is fully initialised. Without `warmKeys`, resolves immediately. With `warmKeys`, resolves once the automatic `warmFromL2` call completes.\n\nDesigned for k8s readiness probes — await before accepting traffic, then never call again:\n\n```typescript\nconst cache = CacheService.create({ warmKeys: 'user:*' });\n\n// k8s readiness probe endpoint\napp.get('/ready', async (_req, res) =\u003e {\n  await cache.ready();\n  res.sendStatus(200);\n});\n```\n\n### `cache.has(key)` → `boolean`\n\nReturn `true` if the key exists in L1 and has not expired. Bloom-filter fast-path — no fetch, no disk or Redis round-trip.\n\n### `cache.ttl(key)` → `number | null`\n\nReturn the remaining TTL in **seconds** for a key currently held in L1. Returns `null` if the key is absent or expired. Does not fetch or consume the value.\n\n```typescript\nconst remaining = cache.ttl('user:123'); // e.g. 247 (seconds left)\nif (remaining !== null \u0026\u0026 remaining \u003c 30) await cache.touch('user:123', 300);\n```\n\n### `cache.touch(key, newTtlSeconds)` → `Promise\u003cboolean\u003e`\n\nExtend the TTL of a key in L1 (and fire-and-forget `EXPIRE` in Redis) without reading or re-fetching its value. Returns `false` if the key is absent or already expired.\n\n### `cache.getIfFresh\u003cT\u003e(key)` → `T | null`\n\nReturn the L1 value only if it is **fresh** (not yet in the SWR grace window). Returns `null` when absent, expired, or stale — without triggering a revalidation.\n\n```typescript\nconst fresh = cache.getIfFresh\u003cUser\u003e('user:123');\nif (fresh !== null) return fresh; // serve from L1, no network hop\n```\n\n### `cache.setIfAbsent\u003cT\u003e(key, value, ttlSeconds?)` → `Promise\u003cboolean\u003e`\n\nAtomically write `value` only if `key` is not already cached. Checks L1 first, then attempts a Redis `SET NX EX`. Returns `true` if the value was written, `false` if a live entry already existed.\n\nUseful for distributed lock-style writes, session initialisation, or any pattern where you must not overwrite an already-cached value.\n\n```typescript\nconst written = await cache.setIfAbsent(`session:${id}`, sessionData, 3600);\nif (!written) {\n  // session already exists — do not overwrite\n}\n```\n\n### `cache.hotKeys(n?)` → `Array\u003c{ key: string; hits: number; sizeBytes: number }\u003e`\n\nReturns the top `n` live L1 keys ranked by Count-Min Sketch access frequency. Namespace prefix is stripped from each key. Expired entries are excluded. Default `n = 10`.\n\n```typescript\nconst hot = cache.hotKeys(5);\n// [\n//   { key: 'user:1',    hits: 1024, sizeBytes: 512 },\n//   { key: 'product:7', hits:  893, sizeBytes: 256 },\n//   ...\n// ]\n```\n\n### `cache.invalidateTag(tag)` → `Promise\u003cvoid\u003e`\n\nEvict all entries associated with a tag from L1, disk, and Redis.\n\n```typescript\nawait cache.set('product:1', data, 60, undefined, { tags: ['catalog'] });\nawait cache.set('product:2', data, 60, undefined, { tags: ['catalog'] });\nawait cache.invalidateTag('catalog'); // evicts both entries\n```\n\n### `cache.ping()` → `Promise\u003cCachePingResult\u003e`\n\nMeasure L1 / disk / Redis latency in milliseconds. Returns `{ l1, disk, l2 }` — `l2` is `null` when Redis is disabled. Suitable for health-check endpoints.\n\n```typescript\napp.get('/health', async (_req, res) =\u003e {\n  const { l1, disk, l2 } = await cache.ping();\n  res.json({ status: 'ok', latencyMs: { l1, disk, l2 } });\n});\n```\n\n### `cache.drainToL2()` → `Promise\u003cnumber\u003e`\n\nPipeline all live L1 entries to Redis in a single round-trip. Returns the number of keys written. Useful for warming a new Redis node or zero-downtime failover.\n\n### `cache.delete(key)` → `Promise\u003cvoid\u003e`\n\nDeletes one exact key or a glob pattern (`user:abc:*`). Propagates to disk, Redis, and all backplane peers.\n\n### `cache.clear(prefix?)` → `Promise\u003cvoid\u003e`\n\nFlush all entries, or only those whose key starts with `prefix`. Propagates to disk and Redis.\n\n```typescript\nawait cache.clear();           // flush everything\nawait cache.clear('session:'); // flush only session keys\n```\n\n### `cache.rebalance()` → `void`\n\nEvict L1 entries that now violate the current category or global capacity limits. Useful when `categoryLimits` are tightened after startup — normally, existing entries are not re-evaluated until they expire naturally.\n\n```typescript\n// Tighten analytics limit at runtime, then immediately enforce it\ncache.options.categoryLimits['analytics:'].maxEntries = 50;\ncache.rebalance();\n```\n\n### `cache.increment(key, ttlSeconds?)` → `Promise\u003cnumber\u003e`\n\nRedis `INCR` — atomically increments a counter, setting TTL on first write. When Redis is disabled, maintains an in-process counter with the same TTL semantics so rate-limiting works in dev/test.\n\n### `cache.metrics()` → `CacheMetrics`\n\nReturns a full metrics snapshot including hit rates, bloom filter stats, backplane counters, OOM eviction history, and tier sizes.\n\n### `CacheService.toPrometheusText(metrics, prefix?, instanceName?)` → `string`\n\nConverts a `CacheMetrics` snapshot to Prometheus text exposition format. Pass `instanceName` to add an `instance` label alongside `namespace`.\n\n```typescript\napp.get('/metrics', (_req, res) =\u003e {\n  res.type('text/plain').send(\n    CacheService.toPrometheusText(cache.metrics(), 'tricache', 'api-us-east-1'),\n  );\n});\n```\n\n### `cache.stats()` → `{ l1, disk }`\n\nLightweight L1 and disk stats without the full metrics breakdown.\n\n### `cache.writeSnapshot(altPath?)` / `cache.loadSnapshot()`\n\nManual snapshot control. Called automatically on `SIGTERM`/`SIGINT` — only needed when you manage shutdown yourself. `writeSnapshot()` accepts an optional path to write to an alternate location without touching the configured default snapshot file.\n\n```typescript\n// Graceful-shutdown hook — write to a dated backup path\nprocess.on('SIGTERM', async () =\u003e {\n  await cache.writeSnapshot(`/backups/cache-${Date.now()}.snap`);\n  process.exit(0);\n});\n```\n\n### `cache.keys()` → `Generator\u003cstring\u003e`\n\nLazily yields the key for every live (non-expired) L1 entry. Namespace prefix is stripped automatically. Uses a dedicated generator that skips intermediate tuple allocation.\n\n```typescript\nfor (const key of cache.keys()) console.log(key);\n```\n\n### `cache.values\u003cT\u003e()` → `Generator\u003cT\u003e`\n\nLazily yields the cached value for every live L1 entry. Returns the live deserialized object (same reference semantics as `get()`). Uses `yield*` delegation — no intermediate generator frame.\n\n```typescript\nfor (const session of cache.values\u003cSession\u003e()) evict(session);\n```\n\n### `cache.entries\u003cT\u003e()` → `Generator\u003c[string, T]\u003e`\n\nLazily yields `[key, value]` pairs for every live L1 entry. Key has namespace prefix stripped.\n\n```typescript\nfor (const [key, user] of cache.entries\u003cUser\u003e()) sync(key, user);\n```\n\n\u003e **JIT note:** all three generators iterate `SmartMemoryCache.cache` (a single `Map`). V8 maintains per-call-site type feedback; having three generator functions share the same Map means no single one gets the full monomorphic specialization budget. In practice the throughput impact is ≤5 % relative to each running in isolation. See [BENCHMARKS.md](BENCHMARKS.md) for numbers.\n\n### `cache.destroy()` → `Promise\u003cvoid\u003e`\n\nCloses the Redis connection, unsubscribes the backplane, and stops all background timers.\n\n---\n\n## 🎯 Priority levels\n\n```typescript\nimport { CachePriority } from 'tricache';\n\nCachePriority.LOW      // 1 — analytics, reports — evicted first\nCachePriority.NORMAL   // 2 — general application data (default)\nCachePriority.HIGH     // 3 — user profiles, config — evicted last\nCachePriority.CRITICAL // 4 — never evicted while valid (auth tokens, sessions)\n```\n\nPriority is **auto-inferred** from the key when not specified:\n\n| Key contains | Inferred priority |\n|---|---|\n| `auth:` or `session:` | `CRITICAL` |\n| `user:`, `org:`, or `profile:` | `HIGH` |\n| `analytics:`, `report:`, or `stats:` | `LOW` |\n| anything else | `NORMAL` |\n\n---\n\n## 🧠 Eviction algorithm\n\nL1 eviction uses **reservoir sampling** — an O(n) single pass samples 16 candidates, then sorts only those 16 (O(1)). Each candidate is scored:\n\n```\nscore = priority × 1000 + min(hits, 100) × 10 + ttlRemaining/60s − age/60s\n```\n\n- Higher score = kept longer\n- `CRITICAL` entries are excluded from sampling while valid\n- When a category limit is breached, entries from that category receive a score penalty\n\n---\n\n## 🪵 Pluggable logger\n\nBring your own structured logger — tricache doesn't care if it's `pino`, `winston`, or `console`.\n\n```typescript\nimport pino from 'pino';\nconst logger = pino();\n\nCacheService.create({\n  logger: {\n    debug: (msg, meta) =\u003e logger.debug(meta ?? {}, msg),\n    info:  (msg, meta) =\u003e logger.info(meta  ?? {}, msg),\n    warn:  (msg, meta) =\u003e logger.warn(meta  ?? {}, msg),\n    error: (msg, meta, err) =\u003e logger.error({ ...(meta ?? {}), err }, msg),\n  },\n});\n```\n\n---\n\n## 🔐 Encryption\n\nAES-256-GCM for L2 (Redis) values, disk spill files, and cold-start snapshots. Three modes are available via `encryptionMode`:\n\n| Mode | Key length | Notes |\n|---|---|---|\n| `aes-256-gcm` | 32 bytes | **Default.** Authenticated encryption (AEAD). |\n| `aes-128-gcm` | 16 bytes | ~15% faster than AES-256. Same AEAD guarantees. |\n| `aes-128-ctr` | 16 bytes | Fastest cipher mode. AES-NI keystream, no auth tag. Use when integrity is guaranteed elsewhere (TLS, HMAC). |\n| `xor` | any (≥ 16 bytes recommended) | **NOT cryptographic.** XOR obfuscation only. Dev/non-sensitive data. |\n\n**Key generation:**\n\n```bash\n# AES-256 (32 bytes)\nnode -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"\n\n# AES-128 / AES-128-CTR (16 bytes)\nnode -e \"console.log(require('crypto').randomBytes(16).toString('base64'))\"\n\n# XOR — any length, minimum 16 bytes recommended\nnode -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"\n```\n\n```typescript\n// AES-256-GCM (default)\nCacheService.create({ encryptionKey: '\u003cbase64-32-bytes\u003e' });\n\n// AES-128-GCM\nCacheService.create({ encryptionKey: '\u003cbase64-16-bytes\u003e', encryptionMode: 'aes-128-gcm' });\n\n// AES-128-CTR (fastest cipher, no auth tag)\nCacheService.create({ encryptionKey: '\u003cbase64-16-bytes\u003e', encryptionMode: 'aes-128-ctr' });\n\n// XOR obfuscation (NOT cryptographic — dev/non-sensitive only)\nCacheService.create({ encryptionKey: '\u003cbase64-key\u003e', encryptionMode: 'xor' });\n\n// or use the env var: CACHE_ENCRYPTION_KEY=\u003cbase64-key\u003e\n```\n\n| Mode | Redis format | Disk / snapshot format |\n|---|---|---|\n| `aes-256-gcm` | `enc:v1:\u003cbase64(IV[12]\\|Tag[16]\\|CT)\u003e` | `TRIC1ENC\\|IV[12]\\|Tag[16]\\|CT[N]` |\n| `aes-128-gcm` | `a128:v1:\u003cbase64(IV[12]\\|Tag[16]\\|CT)\u003e` | `TRIC1128\\|IV[12]\\|Tag[16]\\|CT[N]` |\n| `aes-128-ctr` | `ctr:v1:\u003cbase64(IV[16]\\|CT)\u003e` | `TRIC1CTR\\|IV[16]\\|CT[N]` |\n| `xor` | `xor:v1:\u003cbase64(key⊕data)\u003e` | `TRIC1XOR\\|key⊕data[N]` |\n\nExisting plaintext values are read transparently during key rotation.\n\n### Zero-downtime key rotation\n\nSet `previousEncryptionKey` to your old key while rolling out a new one. The cache tries the current key first; if decryption fails it transparently retries with the previous key. Remove `previousEncryptionKey` once all old entries have expired.\n\n```typescript\nCacheService.create({\n  encryptionKey:         process.env.NEW_ENCRYPTION_KEY, // new AES-256 key\n  previousEncryptionKey: process.env.OLD_ENCRYPTION_KEY, // fallback for old entries\n  // previousEncryptionMode defaults to current encryptionMode\n});\n```\n\n---\n\n## 🧵 Worker thread crypto offload\n\nBy default, AES-GCM encryption/decryption runs synchronously on the V8 main thread. For payloads above a configurable size threshold this blocks the event loop for measurable durations. Enable worker thread offload to move encryption off the main thread:\n\n```typescript\nCacheService.create({\n  encryptionKey:        process.env.CACHE_ENCRYPTION_KEY,\n  workerThreads:        true,    // enable off-main-thread crypto\n  workerThresholdBytes: 131_072, // only offload payloads ≥ 128 KB (default)\n  workerPoolSize:       4,       // threads; 0 = auto (min(4, logical CPUs))\n});\n```\n\n**How it works:**\n- A fixed-size pool of Node.js `worker_threads` is created at startup; each worker holds an initialised `CacheEncryption` instance.\n- Dispatch is round-robin; inter-thread IPC carries only strings (structured-clone fast path).\n- Workers are `unref()`'d — they do not prevent the process from exiting cleanly.\n- If worker initialisation fails the pool is silently disabled and synchronous crypto is used as a fallback — zero configuration required in environments where workers are unavailable.\n\n**When to enable:**\n- Payloads routinely exceed a few hundred KB with encryption enabled.\n- You observe event-loop lag correlated with L2 read/write operations in APM.\n- Rule of thumb: the default 128 KB threshold keeps overhead negligible for typical API response payloads while protecting against multi-MB documents.\n\n---\n\n## 🔁 Backplane staleness fence\n\nRedis Pub/Sub has no delivery guarantees — a subscriber disconnect (network blip, Redis failover, container restart) can cause peer invalidation messages to be silently dropped. Entries written to L1 between the disconnect and reconnect may silently serve stale data.\n\nTriCache tracks when the subscriber disconnects and, on reconnect, compares the gap duration against `backplaneMaxStalenessMs`. If the gap exceeds the threshold, every L1 entry that was written before the disconnect is proactively evicted, forcing a controlled re-fetch from L2 on the next access:\n\n```typescript\nCacheService.create({\n  invalidationBackplane:   true,\n  backplaneMaxStalenessMs: 5_000, // default: evict stale L1 on gaps \u003e 5 s\n});\n```\n\nSetting `backplaneMaxStalenessMs: 0` disables the fence entirely. The eviction count and gap duration are logged at `warn` level on every triggered flush.\n\n---\n\n## ☁️ Serverless / ephemeral disk environments\n\nOn Lambda, Cloud Run, Fly.io, Railway, and similar platforms, the filesystem (`os.tmpdir()`) is container-scoped and is wiped on every cold start. Disk spill and cold-start snapshots are therefore useless and waste I/O budget.\n\nTriCache auto-detects serverless runtimes at construction time (zero I/O — environment variable checks only) and automatically disables the disk tier:\n\n| Runtime | Detection env var |\n|---|---|\n| AWS Lambda | `AWS_LAMBDA_FUNCTION_NAME` |\n| Google Cloud Run | `K_SERVICE` |\n| Google Cloud Functions | `FUNCTION_TARGET` |\n| Azure Functions | `WEBSITE_INSTANCE_ID` |\n| Fly.io | `FLY_APP_NAME` |\n| Railway | `RAILWAY_ENVIRONMENT` |\n| Vercel | `VERCEL` |\n\nYou can also control disk behaviour explicitly:\n\n```typescript\n// Force-disable disk tier (e.g., when running inside a Docker container with no writable tmpdir)\nCacheService.create({ disableDisk: true });\n\n// Force-enable disk tier even when a serverless env var is present (advanced override)\nCacheService.create({ disableDisk: false });\n```\n\nWhen disk is disabled:\n- The disk spill callback is a no-op.\n- `disk.load()` / `disk.delete()` / `disk.clear()` are never called.\n- `loadSnapshot()` and `writeSnapshot()` are skipped.\n- The background disk janitor timer is not started.\n- `metrics().disk.disabled` is `true`.\n\n---\n\n## 🔴 Redis Cluster and Sentinel\n\nTriCache supports Redis Cluster (slot-based sharding) and Redis Sentinel (automatic primary failover) via ioredis built-in support.\n\n### Redis Cluster\n\n```typescript\nCacheService.create({\n  redisClusterNodes: [\n    { host: 'redis-node-1.example.com', port: 6379 },\n    { host: 'redis-node-2.example.com', port: 6379 },\n    { host: 'redis-node-3.example.com', port: 6379 },\n  ],\n  redisTls: true,\n});\n```\n\nioredis handles slot routing, moved/ask redirects, and re-queuing commands during slot migrations transparently. You can list any subset of cluster nodes — ioredis discovers the full topology automatically.\n\n### Redis Sentinel\n\n```typescript\nCacheService.create({\n  redisSentinel: {\n    name: 'mymaster',\n    sentinels: [\n      { host: 'sentinel-1.example.com', port: 26379 },\n      { host: 'sentinel-2.example.com', port: 26379 },\n      { host: 'sentinel-3.example.com', port: 26379 },\n    ],\n  },\n  redisTls: true,\n});\n```\n\nioredis monitors the current master via the Sentinel topology and transparently reconnects to a new primary after failover. The backplane subscriber is also constructed in the appropriate cluster/sentinel mode.\n\n\u003e `redisHost` / `redisPort` are ignored when `redisClusterNodes` or `redisSentinel` is set. All three topology modes support `redisTls`.\n\n---\n\n## ⚡ WASM Bloom filter\n\nA 100,000-bit filter with k=7 hash probes:\n\n- At the default `l1MaxEntries: 2,000` — false-positive rate ≈ **0.01%**\n- At rated capacity (~18,000 entries) — false-positive rate ≈ **1%**\n- The filter rebuilds automatically when stale bits from deleted/expired entries accumulate\n\nMechanics:\n- `mightContain(key) === false` → **guaranteed miss** — the Map lookup is skipped entirely\n- `mightContain(key) === true` → probable hit — the Map is checked to confirm\n\nThe 562-byte WASM binary is inlined as Base64 — zero filesystem access at runtime. Falls back to a pure-JS implementation if `WebAssembly` is unavailable.\n\n---\n\n## 📊 Performance\n\nMeasured on a single Node.js thread (no `await` on synchronous paths):\n\n**L1 SmartMemoryCache**\n\n| Operation | Throughput | Latency | Notes |\n|---|---|---|---|\n| `get` — hot hit (8K entries) | **2.81 M/s** | 356 ns | bloom → Map lookup → return cached value |\n| `get` — cold miss | **7.14 M/s** | 140 ns | bloom gates → early return |\n| `set` — tiny payload | 899 K/s | 1.11 µs | pack() + Map.set + bloom.add |\n| `set` — small payload (≈ 512 B) | 554 K/s | 1.81 µs | pack() same unified path, larger payload |\n| `set` — large payload (≥ 512 B) | 205.3 K/s | 4.87 µs | pack() larger payload |\n| `set` — CRITICAL priority | 730.1 K/s | 1.37 µs | same set path; skipped in eviction sort |\n| `delete` — exact key | **5.36 M/s** | 186 ns | Map.delete |\n| `deletePattern` — glob wildcard | 7.2 K/s | 138 µs | O(n) Map scan |\n| Count-Min Sketch estimate | **3.37 M/s** | 297 ns | 4 row lookups — called on every `get()` hit and `set()` |\n\n**Iterator interface (L1 live entries, 500 entries)**\n\n| Method | Throughput | Latency | Notes |\n|---|---|---|---|\n| `cache.keys()` | 26.6 K/s | 37.53 µs | no `[key,entry]` tuple allocation |\n| `cache.values()` | 35.5 K/s | 28.19 µs | `yield*` delegation |\n| `cache.entries()` | 24.0 K/s | 41.73 µs | `[strippedKey, value]` pairs |\n| raw `Map` iteration (baseline) | 277.2 K/s | 3.61 µs | no expiry check, no generator overhead |\n\n**CacheService (end-to-end)**\n\n| Operation | Throughput | Latency | Notes |\n|---|---|---|---|\n| `get` — L1 warm hit | **2.03 M/s** | 491 ns | inflight check → l1.get → return cached value |\n| `get` — SWR stale serve | **1.78 M/s** | 562 ns | serves stale; revalidates async |\n| `get` — miss + fetchFn | 13.7 K/s | 73 µs | Promise microtask + l1.set |\n| `set` | 28.7 K/s | 34.86 µs | l1.set + disk.save (fire-and-forget) |\n| `delete` — exact key | 7.3 K/s | 137.82 µs | l1.delete + disk.delete + backplane |\n| `delete` — glob `*` | 687 K/s | 1.46 µs | l1.deletePattern O(n) + disk glob |\n\n**Encryption** (IV pool, pre-allocated output buffers)\n\n| Mode | Payload | Encrypt | Decrypt |\n|---|---|---|---|\n| AES-256-GCM | 64 B | 140.4 K/s / 7.12 µs | 155.5 K/s / 6.43 µs |\n| AES-256-GCM | 512 B | 103.1 K/s / 9.70 µs | 142.9 K/s / 6.99 µs |\n| AES-256-GCM | 4 KB | 58.4 K/s / 17.12 µs | 48.0 K/s / 20.84 µs |\n| AES-128-GCM | 64 B | 148.8 K/s / 6.72 µs | 173.0 K/s / 5.78 µs |\n| AES-128-GCM | 512 B | 135.7 K/s / 7.37 µs | 158.8 K/s / 6.30 µs |\n| AES-128-GCM | 4 KB | 70.2 K/s / 14.24 µs | 53.2 K/s / 18.79 µs |\n| AES-128-CTR | 64 B | 187.9 K/s / 5.32 µs | 196.9 K/s / 5.08 µs |\n| AES-128-CTR | 512 B | 183.5 K/s / 5.45 µs | 185.6 K/s / 5.39 µs |\n| AES-128-CTR | 4 KB | 78.4 K/s / 12.75 µs | 71.8 K/s / 13.93 µs |\n| XOR _(obfuscation only)_ | 64 B | 2.43 M/s / 412 ns | 2.10 M/s / 476 ns |\n| XOR _(obfuscation only)_ | 512 B | 665.5 K/s / 1.50 µs | 715.3 K/s / 1.40 µs |\n| XOR _(obfuscation only)_ | 4 KB | 114.5 K/s / 8.73 µs | 77.6 K/s / 12.89 µs |\n\n\u003e AES and XOR string-path numbers shown (Redis L2). Buffer path (disk/snapshot) is 5–20% faster — no base64 overhead.  \n\u003e AES-128-GCM is 5–50% faster than AES-256-GCM depending on payload (gap widens at mid-range sizes on AES-NI hardware).  \n\u003e AES-128-CTR removes the GHASH MAC step: ~50% faster than AES-128-GCM at small payloads; use only when integrity is guaranteed by transport.  \n\u003e XOR numbers are for the buffer path (32-bit word-level XOR, 4 bytes/iteration). XOR dominates at small payloads (no cipher setup) and remains ~2× faster than AES at 4 KB.\n\nSee [BENCHMARKS.md](BENCHMARKS.md) for the full breakdown: bloom filter cost, serialization by payload size, eviction pressure, concurrency analysis, multi-tenancy isolation, and a realistic 80/15/5 read/miss/write workload.\n\n---\n\n## 🤝 Contributing\n\nBug reports and pull requests are welcome!\n\n1. Fork the repo and create a feature branch\n2. Run `pnpm test` — all tests must pass\n3. Run `pnpm bench` if you touch a hot path and include before/after numbers in your PR\n4. Open your PR against `master`\n\n\u003e New to the codebase? Start with [src/cache-service.ts](src/cache-service.ts) for the public API and [src/smart-memory-cache.ts](src/smart-memory-cache.ts) for the L1 engine.\n\n---\n\n## 🛡️ Security\n\nFound a vulnerability? **Please don't open a public issue.** Report it privately via [GitHub Security Advisories](https://github.com/Kareem411/TriCache/security/advisories/new) so it can be patched before disclosure.\n\nFor encryption key generation and rotation best practices, see the [Encryption](#-encryption) section.\n\n---\n\n## 📄 License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkareem411%2Ftricache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkareem411%2Ftricache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkareem411%2Ftricache/lists"}