Skip to content

Repository files navigation

flightcache

All Contributors

Tiny async cache: TTL + LRU, single-flight dedup, and stale-while-revalidate. Zero dependencies.

CInpm versionbundle sizetypeslicense

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";constgetUser=memoize((id: string)=>db.users.find(id),{ttl: 30_000,// fresh for 30sstaleWhileRevalidate: 60_000,// then serve stale up to 60s more while refreshingmax: 1000,// LRU cap});// 50 concurrent calls for the same id → the DB is hit exactly once.awaitPromise.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 flightcache

API

memoize(fn, options?) → memoized

Wrap an async (or sync) function. The returned function caches by key and exposes .cache, .invalidate(...args), and .clear().

OptionTypeDefaultDescription
ttlnumber (ms)60000How long a value stays fresh.
staleWhileRevalidatenumber (ms)0Extra window to serve stale while refreshing.
maxnumberunboundedLRU cap on cached keys.
key(...args) => stringJSON.stringify(args)Derive the cache key from arguments.
singleFlightbooleantrueCoalesce concurrent same-key calls.
clock() => numberDate.nowInjectable clock for deterministic tests.
constm=memoize(fetchThing,{ttl: 5_000});awaitm("a");m.invalidate("a");// drop one keym.clear();// drop everythingm.cache.size;// inspect the underlying cache

TTLCache<K, V>

The storage layer on its own — a TTL + LRU map.

import{TTLCache}from"flightcache";constcache=newTTLCache<string,Buffer>({max: 500,ttl: 10_000});cache.set("k",buf);cache.get("k");// bumps recency; undefined if expiredcache.peek("k");// no recency changecache.has("k");cache.delete("k");cache.keys();// live keys, oldest → newest

Notes

  • 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 memoize function body.
  • Keys default to JSON.stringify(args); pass key for 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:

Tung Tran
Tung Tran

💻🚧

License

MIT © Tung Tran

About

Tiny async cache: TTL + LRU, single-flight dedup, and stale-while-revalidate. Zero dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages