flightcache
v0.1.1
Published
Tiny async cache: TTL + LRU, single-flight dedup, and stale-while-revalidate. Zero dependencies.
Maintainers
Readme
flightcache
Tiny async cache: TTL + LRU, single-flight dedup, and stale-while-revalidate. Zero dependencies.
Caching an expensive async call sounds simple — until ten requests for the same
cold key arrive at once and all ten hit your database (a cache stampede), or a
value expires mid-traffic and every caller suddenly blocks on a slow refresh.
flightcache solves both: it coalesces concurrent calls into one (single
flight) and can serve a slightly stale value instantly while refreshing in the
background (stale-while-revalidate).
import { memoize } from "flightcache";
const getUser = memoize((id: string) => db.users.find(id), {
ttl: 30_000, // fresh for 30s
staleWhileRevalidate: 60_000, // then serve stale up to 60s more while refreshing
max: 1000, // LRU cap
});
// 50 concurrent calls for the same id → the DB is hit exactly once.
await Promise.all(ids.map(getUser));Why flightcache?
- No stampede. Concurrent calls for the same key share a single in-flight promise instead of all recomputing.
- No latency cliff. With
staleWhileRevalidate, expiry returns the cached value immediately and refreshes in the background — readers never wait on a slow recompute. - TTL + LRU built in. Per-entry TTL and least-recently-used eviction.
- Failures aren't cached. A rejected computation propagates and is retried next call, not memoized.
- Zero dependencies, ESM + CJS + types, and an injectable clock for tests.
Install
npm install flightcache
# or: pnpm add flightcache / yarn add flightcache / bun add flightcacheAPI
memoize(fn, options?) → memoized
Wrap an async (or sync) function. The returned function caches by key and exposes
.cache, .invalidate(...args), and .clear().
| Option | Type | Default | Description |
| ---------------------- | ----------------------- | --------------------- | ---------------------------------------------------- |
| ttl | number (ms) | 60000 | How long a value stays fresh. |
| staleWhileRevalidate | number (ms) | 0 | Extra window to serve stale while refreshing. |
| max | number | unbounded | LRU cap on cached keys. |
| key | (...args) => string | JSON.stringify(args)| Derive the cache key from arguments. |
| singleFlight | boolean | true | Coalesce concurrent same-key calls. |
| clock | () => number | Date.now | Injectable clock for deterministic tests. |
const m = memoize(fetchThing, { ttl: 5_000 });
await m("a");
m.invalidate("a"); // drop one key
m.clear(); // drop everything
m.cache.size; // inspect the underlying cacheTTLCache<K, V>
The storage layer on its own — a TTL + LRU map.
import { TTLCache } from "flightcache";
const cache = new TTLCache<string, Buffer>({ max: 500, ttl: 10_000 });
cache.set("k", buf);
cache.get("k"); // bumps recency; undefined if expired
cache.peek("k"); // no recency change
cache.has("k");
cache.delete("k");
cache.keys(); // live keys, oldest → newestNotes
- This is an in-memory, single-process cache (per Node instance / browser tab).
For cross-process sharing, put a Redis/Memcached layer behind the same
memoizefunction body. - Keys default to
JSON.stringify(args); passkeyfor stable hashing of complex arguments or to ignore volatile fields.
Contributors ✨
This project follows the all-contributors specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the emoji key for how each contribution is recognized, and open a PR or issue to get involved.
Thanks goes to these wonderful people:
License
MIT © Tung Tran
