{"_id":"@antihero/nano-cache","_rev":"3-6dd53acc5da8fe9076038b652c09b3e9","name":"@antihero/nano-cache","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@antihero/nano-cache","version":"1.0.0","keywords":["cache","caching","typescript","lru","adapter","redis","localstorage","indexeddb","memory-cache","zero-dependency","universal"],"author":{"name":"Lelianto Pradana","email":"lelianto.eko@gmail.com"},"license":"MIT","_id":"@antihero/nano-cache@1.0.0","maintainers":[{"name":"antihero","email":"lelianto.eko@gmail.com"}],"homepage":"https://github.com/Lelianto/nano-cache#readme","bugs":{"url":"https://github.com/Lelianto/nano-cache/issues"},"dist":{"shasum":"5685d77f347816a413427b618e8e60a1bb3f8713","tarball":"https://registry.npmjs.org/@antihero/nano-cache/-/nano-cache-1.0.0.tgz","fileCount":9,"integrity":"sha512-ORqwVanS81WQInClRMhIPGa0bv/Gm5nluGeHa/o6gcG/8c8wydSmvzrX4Lcc7sDBbgYewimXkbJUgWPw25e9lQ==","signatures":[{"sig":"MEUCICfr0Yt5RvLlA/5Q7f0QtkerW6gkviDcehIGyIUhC8SEAiEAyU3dZB6CVgDp0WFO10wF/AZc26sDfSbSozVQZs1K8xU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@antihero%2fnano-cache@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":252311},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=16.0.0"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}}},"gitHead":"f5a6a9ca63d8cc005914830b9605bc955d97a714","scripts":{"dev":"tsup --watch","lint":"eslint \"src/**/*.ts\" \"tests/**/*.ts\"","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"README.md\"","changeset":"changeset","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"antihero","email":"lelianto.eko@gmail.com"},"repository":{"url":"git+https://github.com/Lelianto/nano-cache.git","type":"git"},"_npmVersion":"10.8.2","description":"Universal, lightweight, zero-dependency TypeScript caching library with pluggable adapters.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.33.2","devDependencies":{"tsup":"^8.0.0","eslint":"^8.56.0","vitest":"^1.2.0","prettier":"^3.2.0","happy-dom":"^20.11.1","typescript":"^5.3.0","@types/node":"^20.11.0","@changesets/cli":"^2.27.1","@vitest/coverage-v8":"^1.2.0","@typescript-eslint/parser":"^7.0.0","@typescript-eslint/eslint-plugin":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/nano-cache_1.0.0_1785430001189_0.3962673426149035","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@antihero/nano-cache","version":"1.0.1","keywords":["cache","caching","typescript","lru","adapter","redis","localstorage","indexeddb","memory-cache","zero-dependency","universal"],"author":{"name":"Lelianto Pradana","email":"lelianto.eko@gmail.com"},"license":"MIT","_id":"@antihero/nano-cache@1.0.1","maintainers":[{"name":"antihero","email":"lelianto.eko@gmail.com"}],"homepage":"https://github.com/Lelianto/nano-cache#readme","bugs":{"url":"https://github.com/Lelianto/nano-cache/issues"},"dist":{"shasum":"3af0060681feb5905660c884f3a517db2c55f348","tarball":"https://registry.npmjs.org/@antihero/nano-cache/-/nano-cache-1.0.1.tgz","fileCount":9,"integrity":"sha512-YT9prY07cLGg1JEW2OT150QNRQt+hA2BNZheFjfPCDeRRwJSpr0FvflLSShp39Kq695wCxgOBWD2PvyYP7pnbw==","signatures":[{"sig":"MEQCIHaO7FKyetHTveYFPSA9B8JbrGARajPae1QinkT0mtf0AiBJdPLl1UReC+9zkbbFSDY/uJk/FPKO/Vl6TROyCOuwLw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@antihero%2fnano-cache@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":252351},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=16.0.0"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./package.json":"./package.json"},"gitHead":"7941ddc881bd9aa1c105f345c9c45c08ed4d8668","scripts":{"dev":"tsup --watch","lint":"eslint \"src/**/*.ts\" \"tests/**/*.ts\"","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"README.md\"","changeset":"changeset","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d3e00fd3-4f1b-4ab0-b7c6-52a2758ccc2e"}},"repository":{"url":"git+https://github.com/Lelianto/nano-cache.git","type":"git"},"_npmVersion":"11.16.0","description":"Universal, lightweight, zero-dependency TypeScript caching library with pluggable adapters.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.33.2","devDependencies":{"tsup":"^8.0.0","eslint":"^8.56.0","vitest":"^1.2.0","prettier":"^3.2.0","happy-dom":"^20.11.1","typescript":"^5.3.0","@types/node":"^20.11.0","@changesets/cli":"^2.27.1","@vitest/coverage-v8":"^1.2.0","@typescript-eslint/parser":"^7.0.0","@typescript-eslint/eslint-plugin":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/nano-cache_1.0.1_1785430912635_0.5757302815574663","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@antihero/nano-cache","version":"1.0.2","description":"Universal, lightweight, zero-dependency TypeScript caching library with pluggable adapters.","main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","sideEffects":false,"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:coverage":"vitest run --coverage","test:watch":"vitest","typecheck":"tsc --noEmit","lint":"eslint \"src/**/*.ts\" \"tests/**/*.ts\"","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"README.md\"","changeset":"changeset"},"keywords":["cache","caching","typescript","lru","adapter","redis","localstorage","indexeddb","memory-cache","zero-dependency","universal"],"author":{"name":"Lelianto Pradana","email":"lelianto.eko@gmail.com"},"license":"MIT","packageManager":"pnpm@10.33.2","repository":{"type":"git","url":"git+https://github.com/Lelianto/nano-cache.git"},"homepage":"https://github.com/Lelianto/nano-cache#readme","bugs":{"url":"https://github.com/Lelianto/nano-cache/issues"},"devDependencies":{"@changesets/cli":"^2.27.1","@types/node":"^20.11.0","@typescript-eslint/eslint-plugin":"^7.0.0","@typescript-eslint/parser":"^7.0.0","@vitest/coverage-v8":"^1.2.0","eslint":"^8.56.0","happy-dom":"^20.11.1","prettier":"^3.2.0","tsup":"^8.0.0","typescript":"^5.3.0","vitest":"^1.2.0"},"engines":{"node":">=16.0.0"},"gitHead":"01d9551ed721a07b1da5cc4ae4b88d5aa32a1e29","_id":"@antihero/nano-cache@1.0.2","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-Cj6JYgQ9JqxFelf/mTXKXeTQ7IQVaQrQw0MON3JJJ6YqKCE0C5+L9M/4jZ8jnD4SL4ANNN9D04ZNafjDEDDdcQ==","shasum":"297c285524e09836d3c5c05340827315f16409a1","tarball":"https://registry.npmjs.org/@antihero/nano-cache/-/nano-cache-1.0.2.tgz","fileCount":9,"unpackedSize":273763,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@antihero%2fnano-cache@1.0.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDNBwkbrgKzEVjPD7rooNCev7cNeYadg09CgaLPdcd7TwIhAO3ociKYfyvUULTwy5X+i0JJAziKtceEQ6OO06hc4xOl"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d3e00fd3-4f1b-4ab0-b7c6-52a2758ccc2e"}},"directories":{},"maintainers":[{"name":"antihero","email":"lelianto.eko@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nano-cache_1.0.2_1785588369804_0.32599900522022596"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-30T16:46:40.966Z","modified":"2026-08-01T12:46:10.273Z","1.0.0":"2026-07-30T16:46:41.347Z","1.0.1":"2026-07-30T17:01:52.764Z","1.0.2":"2026-08-01T12:46:09.961Z"},"bugs":{"url":"https://github.com/Lelianto/nano-cache/issues"},"author":{"name":"Lelianto Pradana","email":"lelianto.eko@gmail.com"},"license":"MIT","homepage":"https://github.com/Lelianto/nano-cache#readme","keywords":["cache","caching","typescript","lru","adapter","redis","localstorage","indexeddb","memory-cache","zero-dependency","universal"],"repository":{"type":"git","url":"git+https://github.com/Lelianto/nano-cache.git"},"description":"Universal, lightweight, zero-dependency TypeScript caching library with pluggable adapters.","maintainers":[{"name":"antihero","email":"lelianto.eko@gmail.com"}],"readme":"<p align=\"center\">\n  <h1 align=\"center\">nano-cache</h1>\n  <p align=\"center\">Universal, lightweight, zero-dependency TypeScript caching library with pluggable adapters.</p>\n  <p align=\"center\">\n    <a href=\"https://www.npmjs.com/package/@antihero/nano-cache\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/@antihero/nano-cache?style=flat-square&color=6366f1\" /></a>\n    <a href=\"https://github.com/Lelianto/nano-cache/actions\"><img alt=\"CI\" src=\"https://img.shields.io/github/actions/workflow/status/Lelianto/nano-cache/ci.yml?style=flat-square&label=CI\" /></a>\n    <img alt=\"coverage\" src=\"https://img.shields.io/badge/coverage-93%25-22c55e?style=flat-square\" />\n    <img alt=\"license\" src=\"https://img.shields.io/npm/l/@antihero/nano-cache?style=flat-square\" />\n    <img alt=\"size\" src=\"https://img.shields.io/bundlejs/size/@antihero/nano-cache?style=flat-square\" />\n  </p>\n</p>\n\n---\n\n**One cache API that works everywhere.** Write your caching logic once, then choose where the data\nlives — memory, `localStorage`, `sessionStorage`, IndexedDB, Redis, or your own backend — by swapping\na single line.\n\n```ts\nconst cache = createCache({ adapter: memoryAdapter() });   // Node, tests\nconst cache = createCache({ adapter: localStorageAdapter() }); // browser, survives reload\nconst cache = createCache({ adapter: redisAdapter(redis) }); // shared across servers\n```\n\nEverything else in your code stays identical.\n\n## Table of contents\n\n- [Install](#install)\n- [Quick start](#quick-start)\n- [Core concepts](#core-concepts)\n- [Real-world use cases](#real-world-use-cases)\n- [API reference](#api-reference)\n- [Adapters](#adapters)\n- [Events and stats](#events-and-stats)\n- [Serialization](#serialization)\n- [Gotchas](#gotchas)\n- [TypeScript](#typescript)\n\n## Install\n\n```bash\nnpm install @antihero/nano-cache\n```\n\n```bash\npnpm add @antihero/nano-cache\n```\n\n```bash\nyarn add @antihero/nano-cache\n```\n\nRequires Node 16+. Ships both ESM and CommonJS builds with TypeScript types. No runtime dependencies.\n\n## Quick start\n\n```ts\nimport { createCache, memoryAdapter } from '@antihero/nano-cache';\n\nconst cache = createCache({\n  adapter: memoryAdapter({ max: 1000 }), // evict least-recently-used past 1000 entries\n  ttl: '5m',                             // default expiry for every entry\n});\n\nawait cache.set('user:1', { name: 'Ada' });\nawait cache.get('user:1');   // { name: 'Ada' }\nawait cache.has('user:1');   // true\n\nawait cache.set('otp', '123456', { ttl: '30s' }); // override the default TTL\nawait cache.delete('otp');\n```\n\nEvery method returns a promise, even with synchronous adapters like memory. That is deliberate: it\nmeans switching to Redis or IndexedDB later requires no changes to your call sites.\n\n## Core concepts\n\n### TTL — how long an entry lives\n\nPass a number of milliseconds, or a duration string.\n\n```ts\nawait cache.set('a', 1, { ttl: 3000 });   // 3000 ms\nawait cache.set('b', 1, { ttl: '500ms' });\nawait cache.set('c', 1, { ttl: '30s' });\nawait cache.set('d', 1, { ttl: '10m' });\nawait cache.set('e', 1, { ttl: '2h' });\nawait cache.set('f', 1, { ttl: '7d' });\nawait cache.set('g', 1);                  // no TTL -> never expires\n```\n\nExpiry is **lazy**: nothing runs on a timer. An entry is removed when you try to read it after it\nexpired, or when `keys()` / `size()` sweeps the store. There is no background interval to leak.\n\n### Namespaces — keep unrelated caches apart\n\nA namespace prefixes every key, so two caches can share one backend without colliding.\n\n```ts\nconst users = createCache({ namespace: 'users', adapter: memoryAdapter() });\nconst posts = createCache({ namespace: 'posts', adapter: memoryAdapter() });\n\nawait users.set('1', { name: 'Ada' });\nawait posts.set('1', { title: 'Hello' });   // different entry, same key \"1\"\n\nawait users.keys();   // ['1'] — namespace is stripped from what you get back\n```\n\n### Tags — invalidate groups of entries\n\nTags let you drop many related entries at once, without knowing their keys.\n\n```ts\nawait cache.set('post:1', post1, { tags: ['posts', 'author:7'] });\nawait cache.set('post:2', post2, { tags: ['posts', 'author:9'] });\n\nawait cache.invalidateTag('author:7'); // drops post:1 only\nawait cache.invalidateTag('posts');    // drops everything tagged \"posts\"\n```\n\n## Real-world use cases\n\n### 1. Stop hammering a slow API\n\n`fetch()` is the method you will reach for most. It returns the cached value if present, otherwise\nruns your function, caches the result, and returns it.\n\n```ts\nimport { createCache, memoryAdapter } from '@antihero/nano-cache';\n\nconst cache = createCache({ adapter: memoryAdapter(), ttl: '10m' });\n\nasync function getExchangeRates() {\n  return cache.fetch('rates', async () => {\n    const res = await fetch('https://api.example.com/rates');\n    return res.json();\n  });\n}\n\nawait getExchangeRates(); // hits the network\nawait getExchangeRates(); // instant, from cache\n```\n\nForce a refresh when you need fresh data:\n\n```ts\nawait cache.fetch('rates', loadRates, { force: true });\n```\n\n### 2. Survive a traffic spike (stampede protection)\n\nIf 100 requests ask for the same missing key at once, a naive cache runs your loader 100 times.\n`fetch()` deduplicates concurrent calls: the first one runs, the other 99 wait for the same promise.\n\n```ts\nlet hits = 0;\n\nconst load = async () => {\n  hits++;\n  await new Promise((r) => setTimeout(r, 50));\n  return 'data';\n};\n\nawait Promise.all(Array.from({ length: 100 }, () => cache.fetch('key', load)));\n\nconsole.log(hits); // 1 — not 100\n```\n\nThis is the difference between a cold cache being a non-event and it taking down your database.\n\n### 3. Cache per user, and clear one user cleanly\n\n```ts\nconst cache = createCache({ adapter: memoryAdapter(), ttl: '15m' });\n\nasync function getDashboard(userId: string) {\n  return cache.fetch(\n    `dashboard:${userId}`,\n    () => db.buildDashboard(userId),\n    { tags: [`user:${userId}`] },\n  );\n}\n\n// User changed their settings — drop everything cached for them\nasync function onUserUpdated(userId: string) {\n  await cache.invalidateTag(`user:${userId}`);\n}\n```\n\n### 4. Remember a form draft in the browser\n\n`sessionStorage` keeps the draft through an accidental reload, but clears when the tab closes.\n\n```ts\nimport { createCache, sessionStorageAdapter } from '@antihero/nano-cache';\n\nconst drafts = createCache({\n  adapter: sessionStorageAdapter(),\n  namespace: 'draft',\n});\n\n// as the user types\nawait drafts.set('signup', { email, plan });\n\n// when the form mounts\nconst draft = await drafts.get('signup');\nif (draft) restore(draft);\n\n// on successful submit\nawait drafts.delete('signup');\n```\n\nUse `localStorageAdapter()` instead if the draft should survive closing the browser.\n\n### 5. Offline-first storage with IndexedDB\n\nIndexedDB holds far more data than `localStorage` and stores structured values, which makes it a good\nfit for caching lists of records for offline use.\n\n```ts\nimport { createCache, indexedDBAdapter } from '@antihero/nano-cache';\n\nconst offline = createCache({\n  adapter: indexedDBAdapter({ dbName: 'my-app', storeName: 'articles' }),\n  ttl: '7d',\n});\n\nasync function getArticles() {\n  return offline.fetch('articles', async () => {\n    const res = await fetch('/api/articles');\n    return res.json();\n  });\n}\n```\n\nIf IndexedDB is unavailable — private browsing, an old browser, server-side rendering — the adapter\nsilently falls back to in-memory storage instead of throwing.\n\n### 6. Share one cache across many servers with Redis\n\nMemory caches are per-process, so with several instances behind a load balancer each keeps its own\ncopy. Redis gives all of them one shared cache.\n\n```ts\nimport Redis from 'ioredis';\nimport { createCache, redisAdapter } from '@antihero/nano-cache';\n\nconst cache = createCache({\n  adapter: redisAdapter(new Redis(process.env.REDIS_URL), { prefix: 'myapp:' }),\n  ttl: '1h',\n});\n\nawait cache.set('session:abc', { userId: 7 }); // visible to every instance\n```\n\nTTLs are pushed down to Redis itself, so Redis expires the key for you.\n\nThe adapter calls `client.set(key, value, 'PX', ms)`, which is the **ioredis** signature. For\n`node-redis` v4, wrap the client:\n\n```ts\nimport { createClient } from 'redis';\n\nconst client = createClient();\nawait client.connect();\n\nconst shim = {\n  get: (k: string) => client.get(k),\n  set: (k: string, v: string, mode?: string, ttl?: number) =>\n    mode === 'PX' ? client.set(k, v, { PX: ttl! }) : client.set(k, v),\n  del: (...keys: string[]) => client.del(keys),\n  keys: (pattern: string) => client.keys(pattern),\n};\n\nconst cache = createCache({ adapter: redisAdapter(shim) });\n```\n\n### 7. Memoize an expensive computation\n\nCaching is not only for I/O. Anything slow and deterministic is a candidate.\n\n```ts\nconst cache = createCache({ adapter: memoryAdapter({ max: 500 }) });\n\nasync function renderMarkdown(doc: string) {\n  return cache.fetch(`md:${hash(doc)}`, () => heavyMarkdownToHtml(doc));\n}\n```\n\n`max: 500` caps memory: once full, the least recently used entry is evicted automatically.\n\n### 8. Bring your own storage backend\n\nAny store with get/set/delete can become an adapter. Here is one backed by a plain `Map`, which is\nalso the shape you would use for SQLite, a file, or a KV service.\n\n```ts\nimport { createCache, createAdapter } from '@antihero/nano-cache';\n\nconst store = new Map<string, any>();\n\nconst cache = createCache({\n  adapter: createAdapter({\n    get: (key) => store.get(key) ?? null,\n    set: (key, item) => void store.set(key, item),\n    delete: (key) => store.delete(key),\n    clear: () => store.clear(),\n    keys: () => Array.from(store.keys()),\n  }),\n});\n```\n\nOnly `get`, `set`, `delete`, and `clear` are required. Supply `keys` as well if you want `keys()`,\n`size()`, and tag invalidation to work. Expiry, tags, namespacing, stats, and events are handled for\nyou above the adapter.\n\n### 9. Warm a cache, then read it in bulk\n\n```ts\nawait cache.setMany([\n  { key: 'a', value: 1 },\n  { key: 'b', value: 2, options: { ttl: '1m' } },\n]);\n\nawait cache.getMany(['a', 'b', 'missing']);\n// { a: 1, b: 2, missing: null }\n\nawait cache.deleteMany(['a', 'b']); // 2\n```\n\n### 10. See whether your cache is actually helping\n\n```ts\nconst s = await cache.stats();\nconst total = s.hits + s.misses;\nconsole.log(`hit rate: ${((s.hits / total) * 100).toFixed(1)}%`);\n```\n\nA low hit rate usually means your TTL is too short or your keys are too specific.\n\n## API reference\n\nCreate a cache with `createCache(options)`, or `new NanoCache(options)`.\n\n| Option      | Type           | Default            | Description                                        |\n| ----------- | -------------- | ------------------ | -------------------------------------------------- |\n| `adapter`   | `CacheAdapter` | `memoryAdapter()`  | Where entries are stored                           |\n| `ttl`       | `TTL`          | none               | Default expiry for every entry                     |\n| `namespace` | `string`       | none               | Prefix applied to all keys                         |\n| `max`       | `number`       | unlimited          | LRU capacity — only when `adapter` is **not** given |\n| `onError`   | `function`     | none               | Observe failures while operations remain non-throwing |\n\n### Methods\n\n| Method                        | Returns                    | Notes                                                |\n| ----------------------------- | -------------------------- | ---------------------------------------------------- |\n| `set(key, value, options?)`   | `Promise<void>`            | `options`: `{ ttl, tags }`                           |\n| `get(key)`                    | `Promise<T \\| null>`       | `null` when missing or expired                       |\n| `has(key)`                    | `Promise<boolean>`         | Counts toward hit/miss stats                         |\n| `delete(key)`                 | `Promise<boolean>`         | `true` if an entry was removed                       |\n| `clear()`                     | `Promise<void>`            | Removes everything in the adapter                    |\n| `keys()`                      | `Promise<string[]>`        | Non-expired keys, namespace stripped                 |\n| `size()`                      | `Promise<number>`          | Count of non-expired entries                         |\n| `fetch(key, fn, options?)`    | `Promise<T>`               | Read-through + concurrency dedup; `{ ttl, tags, force }` |\n| `invalidateTag(tag)`          | `Promise<void>`            | Drops every entry carrying the tag                   |\n| `getMany(keys)`               | `Promise<Record<…>>`       | Missing keys map to `null`                           |\n| `setMany(entries)`            | `Promise<void>`            | `[{ key, value, options? }]`                         |\n| `deleteMany(keys)`            | `Promise<number>`          | Count actually deleted                               |\n| `on(event, listener)`         | `() => void`               | Returns an unsubscribe function                      |\n| `off(event, listener)`        | `void`                     |                                                      |\n| `stats()`                     | `Promise<CacheStats>`      | Counters plus entry count                            |\n\n## Adapters\n\n| Adapter                       | Where            | Persists     | Notes                                          |\n| ----------------------------- | ---------------- | ------------ | ---------------------------------------------- |\n| `memoryAdapter({ max })`      | anywhere         | no           | LRU eviction, fastest, per-process             |\n| `localStorageAdapter()`       | browser          | yes          | ~5 MB, strings only (serialized for you)       |\n| `sessionStorageAdapter()`     | browser          | per tab      | Cleared when the tab closes                    |\n| `indexedDBAdapter()`          | browser          | yes          | Large capacity, falls back to memory           |\n| `redisAdapter(client)`        | Node             | yes          | Shared across processes, native TTL            |\n| `createAdapter(config)`       | anywhere         | up to you    | Wrap any custom store                          |\n\nShared options: `localStorageAdapter`, `sessionStorageAdapter`, and `redisAdapter` accept\n`{ prefix, serializer }`. Redis also accepts `scanCount` and uses incremental `SCAN` when the client\nsupports it, with `KEYS` retained only as a compatibility fallback. `indexedDBAdapter` accepts\n`{ dbName, storeName, version }`. `memoryAdapter` accepts `{ max }`.\n\n## Events and stats\n\n```ts\nconst off = cache.on('expired', (key, value) => {\n  console.log('expired:', key, value);\n});\n\noff(); // unsubscribe\n```\n\n| Event     | Listener signature              |\n| --------- | ------------------------------- |\n| `set`     | `(key, value, options?) => void` |\n| `get`     | `(key, value) => void`          |\n| `delete`  | `(key) => void`                 |\n| `expired` | `(key, value) => void`          |\n| `clear`   | `() => void`                    |\n\n`get` fires only on a hit, and `expired` fires when a read discovers a stale entry. Exceptions thrown\ninside a listener are swallowed, so a bad listener cannot break a cache operation.\n\n`stats()` returns `hits`, `misses`, `sets`, `deletes`, `expired`, `entries`, and `memoryUsage`.\n\n## Serialization\n\nString-based adapters (`localStorage`, `sessionStorage`, Redis) need values converted to text. Plain\n`JSON.stringify` would turn a `Date` into a string and a `Map` into `{}`. The built-in serializer\nhandles `Date`, `Map`, `Set`, and `BigInt` and restores their real types on read.\n\n```ts\nawait cache.set('when', new Date());\n(await cache.get('when')) instanceof Date; // true\n\nawait cache.set('m', new Map([['a', 1]]));\n(await cache.get('m')).get('a'); // 1\n```\n\nRegister your own type with a transformer:\n\n```ts\nimport { defaultSerializer } from '@antihero/nano-cache';\n\nclass Money {\n  constructor(public cents: number) {}\n}\n\ndefaultSerializer.registerTransformer<Money>({\n  name: 'Money',\n  match: (v) => v instanceof Money,\n  serialize: (v) => v.cents,\n  deserialize: (cents) => new Money(cents),\n});\n```\n\n## Gotchas\n\nBehaviours that are easy to trip over. Each one is verified against the current release.\n\n**Operations fail silently.** A failed `set` — Redis down, `localStorage` quota exceeded — does not\nthrow; it is a no-op. A failed `get` returns `null`. This keeps a cache outage from taking down your\napp, but it also means you should never treat the cache as your source of truth. Use `onError` for\nlogging or monitoring without turning cache failures into application failures:\n\n```ts\nconst cache = createCache({\n  onError: (error, { operation, key }) => logger.warn({ error, operation, key }),\n});\n```\n\n**You cannot cache `null`.** `get()` returns `null` for both \"missing\" and \"stored `null`\", so\n`set(key, null)` is indistinguishable from a miss, and `has()` reports `false`. As a result `fetch()`\nre-runs your loader every time its result is `null` — there is no negative caching. Store a sentinel\nsuch as `{ empty: true }` if you need to remember \"this does not exist\".\n\n**An invalid TTL string is ignored, not rejected.** `{ ttl: '10min' }` or `{ ttl: 'abc' }` does not\nthrow — it is treated as \"no TTL\", so the entry never expires. Valid units are `ms`, `s`, `m`, `h`,\n`d`.\n\n**`max` only applies when you omit `adapter`.** `createCache({ max: 100 })` works, but\n`createCache({ adapter: memoryAdapter(), max: 100 })` silently ignores `max`. Pass it to the adapter\ninstead: `memoryAdapter({ max: 100 })`.\n\n**Serializers belong to string-based adapters.** Pass one to\n`localStorageAdapter({ serializer })`, `sessionStorageAdapter({ serializer })`, or\n`redisAdapter(client, { serializer })`.\n\n**`stats().memoryUsage` is process heap, not cache size.** It comes from `process.memoryUsage()` and\nreports your whole Node process; it is `0` in browsers. Use `size()` for the number of entries.\n\n**`has()` affects your stats.** It is implemented on top of `get()`, so it increments `hits` or\n`misses`.\n\n**Browser adapters are no-ops on the server.** With no `window`, `localStorageAdapter()` stores\nnothing and reads return `null`. That makes SSR safe by default, but a server-rendered pass will\nalways miss.\n\n## TypeScript\n\nTypes ship with the package; no `@types` install needed. Values are typed per call:\n\n```ts\nimport { createCache, memoryAdapter, type CacheStats } from '@antihero/nano-cache';\n\ninterface User {\n  id: string;\n  name: string;\n}\n\nconst cache = createCache({ adapter: memoryAdapter() });\n\nawait cache.set<User>('u1', { id: '1', name: 'Ada' });\n\nconst user = await cache.get<User>('u1'); // User | null\nif (user) console.log(user.name);         // narrow the null away first\n\nconst stats: CacheStats = await cache.stats();\n```\n\nExported types: `TTL`, `CacheItem`, `SetOptions`, `FetchOptions`, `CacheOptions`, `CacheStats`,\n`CacheEventMap`, `EventKey`, `CacheAdapter`, `CacheSerializer`, `TypeTransformer`, plus the option\ntypes for each adapter.\n\n## Contributing\n\n```bash\npnpm install\npnpm test          # run the suite\npnpm test:coverage # with coverage\npnpm lint\npnpm typecheck\npnpm build\n```\n\n## License\n\nMIT\n\n---\n\n## 🚀 More TypeScript Projects\n\nIf you find this package useful, you may also like these open-source projects.\n\n| Project | Description |\n|---------|-------------|\n| **💰 Monify** | Lightweight currency formatting library with multi-currency support. |\n| **🤖 AgentifAI** | Vendor-neutral AI agent event model and debugging toolkit. |\n| **⚡ Statelite** | Lightweight reactive state management for TypeScript. |\n| **🗄️ Nano Cache** | Universal cache abstraction for memory, Redis, IndexedDB, and more. |\n| **🔌 PlugnPlay** | Bootstrap cloud backends with minimal configuration. |\n| **🎨 Sagara UI** | Utility-first CSS framework optimized for AI-assisted development. |\n\n### Explore the ecosystem\n\n- 💰 Monify → https://github.com/Lelianto/monify\n- 🤖 AgentifAI → https://github.com/Lelianto/agentifai\n- ⚡ Statelite → https://github.com/Lelianto/statelite\n- 🗄️ Nano Cache → https://github.com/Lelianto/nano-cache\n- 🔌 PlugnPlay → https://github.com/Lelianto/plugnplay\n- 🎨 Sagara UI → https://github.com/Lelianto/sagaraui\n\n⭐ If you enjoy this project, consider giving it a star. It helps others discover the ecosystem.\n","readmeFilename":"README.md"}