@ghost_debugger/nanocache
v0.1.0
Published
Tiny isomorphic async memoize + cache: in-flight dedupe, stale-while-revalidate, TTL and LRU. Zero dependencies. Runs on the browser, Node, Deno, Bun and edge.
Maintainers
Readme
nanocache
Tiny isomorphic async memoize + cache. In-flight dedupe, stale-while-revalidate, TTL and LRU. Zero dependencies.
One cache primitive for your whole stack — the same code runs in the browser, Node, Deno, Bun, and edge/serverless. No window, no process, no build-target juggling.
- 🧬 Isomorphic — universal JS only (
Map,Promise,Date.now). Nothing environment-specific. - 🚦 In-flight dedupe — concurrent calls with the same key share a single execution (kills thundering-herd / duplicate requests).
- 🔄 Stale-while-revalidate — serve the cached value instantly, refresh in the background.
- ⏱️ TTL + LRU — bound entries by age (
maxAge) and count (maxSize) so long-running processes don't leak memory. - 🛡️ Never poisons the cache — rejected promises are not cached.
- 🎛️ Full control —
get / set / has / delete / clear / keyOfon the memoized function. - 📦 ~1 KB, zero deps, dual ESM + CJS, fully typed.
Install
npm install nanocacheQuick start
import { memoize } from "nanocache";
const getUser = memoize(
async (id: string) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
},
{
maxAge: 60_000, // cache for 60s
staleWhileRevalidate: true, // serve stale instantly, refresh in background
maxSize: 1000, // keep at most 1000 users in memory
},
);
await getUser("1"); // miss → fetches
await getUser("1"); // hit → cached
// 50 concurrent calls, ONE network request:
await Promise.all(Array.from({ length: 50 }, () => getUser("1")));Works with sync functions too — you just get TTL/LRU caching (the async-only features engage automatically when the wrapped function returns a promise):
const fib = memoize((n: number): number => (n < 2 ? n : fib(n - 1) + fib(n - 2)));API
memoize(fn, options?)
Returns a memoized version of fn with cache-control methods attached.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| maxAge | number | Infinity | Time-to-live in ms. |
| maxSize | number | Infinity | Max entries in the default LRU store. |
| staleWhileRevalidate | boolean | false | Return stale value, refresh in background. |
| key | (...args) => string | stable serialize | Derive the cache key from arguments. |
| store | CacheStore | in-memory LRU | Swap in a custom (synchronous) store. |
Cache-control methods
getUser.keyOf("1"); // "1" — the key these args map to
getUser.has("1"); // boolean — is a fresh entry cached?
getUser.get("1"); // peek at the cached value (no call)
getUser.set("1", user); // prime the cache
getUser.delete("1"); // evict one key
getUser.clear(); // evict everythingDefault cache key
A single primitive argument is used verbatim (getUser("1") → key "1").
Otherwise arguments are serialized deterministically with sorted object keys,
so { a: 1, b: 2 } and { b: 2, a: 1 } hit the same entry. For arguments that
aren't JSON-representable (Map, Set, Date, BigInt, functions, circular
refs), pass your own key function.
How it compares
| | nanocache | p-memoize | memoizee | lru-cache |
| --- | :---: | :---: | :---: | :---: |
| Isomorphic (browser + server) | ✅ | ✅ | ✅ | ✅ |
| In-flight dedupe (coalescing) | ✅ | ⚠️ partial | ❌ | ❌ (not a memoizer) |
| Stale-while-revalidate | ✅ | ❌ | ❌ | ❌ |
| TTL | ✅ | via add-on | ✅ | ✅ |
| LRU bound | ✅ | via add-on | ⚠️ | ✅ |
| Doesn't cache rejections | ✅ | ✅ | ⚠️ | n/a |
| Runtime dependencies | 0 | 0 | several | 0 |
Universality
The published code touches only APIs present in every JavaScript runtime, so it ships unmodified everywhere:
| Environment | Supported | | --- | :---: | | Browser (bundled) | ✅ | | Node.js (ESM + CJS) | ✅ | | Deno / Bun | ✅ | | Edge / serverless (Cloudflare, Vercel Edge) | ✅ | | React Native | ✅ |
The source is type-checked with no ambient Node or DOM globals, so any accidental use of an environment-specific API fails the build — the isomorphism guarantee is enforced, not just claimed.
Not in v1 (planned)
- Async / remote stores (e.g. Redis) — the store interface stays synchronous for now.
AbortSignalcancellation.
License
MIT
