Skip to content

Repository files navigation

ocache

npm version npm downloads

Composable caching primitives with TTL, stale-while-revalidate, and HTTP response caching. Zero framework dependencies — works with any runtime that has standard Request/Response.

Tip

📖 Head to the documentation to learn more.

Features

  • 🗃️ Function caching — wrap any function with TTL, stale-while-revalidate, and request deduplication.
  • 🌐 HTTP response caching — automatic etag, last-modified, and 304 Not Modified support.
  • 🔑 Smart cache keys — derived from arguments or request URL, with per-header and per-query variance.
  • 🔌 Pluggable storage — bring your own backend via a minimal get/set interface.
  • ♻️ Invalidation & expiration — remove or mark entries stale on demand, with SWR background refresh.

Usage

Caching Functions

Wrap any function with defineCachedFunction to add caching with TTL, stale-while-revalidate, and request deduplication:

import { defineCachedFunction } from "ocache";

const cachedFetch = defineCachedFunction(
  async (url: string) => {
    const res = await fetch(url);
    return res.json();
  },
  {
    maxAge: 60, // Cache for 60 seconds
    name: "api-fetch",
  },
);

// First call hits the function, subsequent calls return cached result
const data = await cachedFetch("https://api.example.com/data");

Note

Learn more in the Caching Functions guide, and see Invalidation & Expiration and Storage.

Caching HTTP Handlers

Wrap HTTP handlers with defineCachedHandler for automatic response caching with etag, last-modified, and 304 Not Modified support:

import { defineCachedHandler } from "ocache";

const handler = defineCachedHandler(
  async (event) => {
    // event.req is a standard Request object
    const url = event.url ?? new URL(event.req.url);
    const data = await getExpensiveData(url.pathname);
    return new Response(JSON.stringify(data), {
      headers: { "content-type": "application/json" },
    });
  },
  {
    maxAge: 300, // Cache for 5 minutes
    swr: true,
    staleMaxAge: 600,
    varies: ["accept-language"], // Vary cache key by these headers (also emitted as `Vary`)
    allowQuery: ["color"], // Vary cache by these query params only
  },
);

API

CachedEventHandler

type CachedEventHandler<E extends HTTPEvent = HTTPEvent> = EventHandler<E> &

Cached event handler returned by defineCachedHandler.

An EventHandler augmented with on-demand revalidation methods. Each accepts the HTTPEvent directly and derives the exact storage keys the handler caches under, so no manual key reconstruction is needed.

They target the resource rather than one method variant of it: GET and HEAD responses are cached under separate keys, but all of a resource's variants are covered whichever method the passed event carries.


cachedFunction

const cachedFunction = defineCachedFunction;

Alias for defineCachedFunction.


CacheStatus

type CacheStatus = "hit" | "stale" | "revalidated" | "miss";

How a cached value was served on a given call.

  • "hit" — a fresh cached value was returned without re-resolving.
  • "stale" — a stale value was served while a background SWR refresh runs.
  • "revalidated" — a prior value existed but was expired/invalid, so it was re-resolved in the foreground (no stale value served) before returning.
  • "miss" — the value was resolved fresh on this call (nothing was cached).

createMemoryStorage

function createMemoryStorage(opts: MemoryStorageOptions =

Creates an in-memory storage backed by a Map with optional TTL support (in seconds) and LRU eviction.


defineCachedFunction

function defineCachedFunction<T, ArgsT extends unknown[] = any[]>(
  fn: (...args: ArgsT) => T | Promise<T>,
  opts: CacheOptions<T, ArgsT> =

Wraps a function with caching support including TTL, SWR, integrity checks, and request deduplication.

Parameters:

  • fn — The function to cache.
  • opts — Cache configuration options.

Returns: — A cached function with a .resolveKey(...args) method for cache key resolution.


defineCachedHandler

function defineCachedHandler<E extends HTTPEvent = HTTPEvent>(
  handler: EventHandler<E>,
  opts: CachedEventHandlerOptions<E> =

Wraps an HTTP event handler with response caching.

Automatically generates cache keys from the request origin (scheme, host and port, as resolved by the adapter — so one handler instance serving several hostnames keeps them apart), the URL path, variable headers and the request method (GET and HEAD are cached separately), sets cache-control, etag, and last-modified headers, and handles 304 Not Modified responses via conditional request headers.

Only GET/HEAD requests without a Range header are cacheable; everything else reaches the handler untouched and its response passes straight through. Of the responses, only 200, 203, 301 and 308 are stored — and a status that isn't stored is never advertised with a synthesized Cache-Control either.

A response that opts itself out is returned to the caller but never stored: Cache-Control: no-store, private, no-cache, a zero shared lifetime (s-maxage if present, else max-age), or Vary: *. must-revalidate is not an opt-out — such a response is stored and served fresh, but never served stale (it revalidates in the foreground once expired).

Parameters:

  • handler — The event handler to cache.
  • opts — Cache and HTTP-specific configuration options.

Returns: — A new event handler that serves cached responses when available. The handler also exposes .resolveKeys(event), .invalidate(event), and .expire(event) for on-demand revalidation, keyed exactly as the handler caches (no key reconstruction); they cover every method variant of the event's resource.


EventHandler

type EventHandler<E extends HTTPEvent = HTTPEvent> = (

Handler function that receives an HTTPEvent and returns a response value.


expireCache

async function expireCache<ArgsT extends unknown[] = any[]>(
  input:

Expires cached entries for given arguments and cache options across all base prefixes, without removing them.

Unlike invalidateCache (which removes entries entirely), expired entries keep serving the stale value with SWR — still bounded by the originally configured staleMaxAge window — while the next access triggers a background refresh. Without SWR, the next call re-resolves before returning.

Uses the same key derivation as defineCachedFunction / resolveCacheKeys. Pass the same maxAge / swr / staleMaxAge options you cache with so the remaining storage TTL is preserved.

Targets options.storage with the same rule as invalidateCache: throws if storage is unset, since there is no global store to fall back on.

Parameters:

  • input — Object with options (cache options) and optional args (function arguments).

Example:

// Mark a cached entry for background refresh on next access
await expireCache({
  options: { name: "fetchUser", getKey: (id: string) => id, maxAge: 60, staleMaxAge: 300, storage },
  args: ["user-123"],
});

invalidateCache

async function invalidateCache<ArgsT extends unknown[] = any[]>(
  input:

Invalidates (removes) cached entries for given arguments and cache options across all base prefixes.

Uses the same key derivation as defineCachedFunction / resolveCacheKeys.

Targets options.storage — pass the same backend (or, better, the very same options object you cached with, whose resolved storage is memoized on it) the entries were written to. Throws if storage is unset: there is no global store to fall back on, so the call could only purge a fresh empty one while the stale entry kept being served. A mismatched name/getKey still purges nothing silently. When the cached function is at hand, prefer its own .invalidate(...args).

Parameters:

  • input — Object with options (cache options) and optional args (function arguments).

Example:

// Invalidate a specific cached entry
await invalidateCache({
  options: { name: "fetchUser", getKey: (id: string) => id, storage },
  args: ["user-123"],
});

resolveCacheKeys

async function resolveCacheKeys<ArgsT extends unknown[] = any[]>(
  input:

Resolves all cache storage keys (one per base prefix) for given arguments and cache options.

Uses the same key derivation as defineCachedFunction internally:

  • When opts.getKey is provided, it is called with args to produce the key segment.
  • Otherwise, args are hashed with ohash (same default as defineCachedFunction).

Pass the same getKey, name, group, and base options you use in defineCachedFunction / defineCachedHandler to get the exact storage keys.

Parameters:

  • input — Object with options (cache options) and optional args (function arguments).

Returns: — An array of storage key strings (one per base prefix).

Example:

const storage = createMemoryStorage();
const fn = cachedFunction(fetchUser, { name: "fetchUser", getKey: (id: string) => id, storage });

const keys = await resolveCacheKeys({
  options: { name: "fetchUser", getKey: (id: string) => id },
  args: ["user-123"],
});
for (const key of keys) {
  await storage.set(key, null); // invalidate all tiers
}

StorageOption

type StorageOption = StorageInterface | (() => StorageInterface);

Where a cached function/handler persists its entries: a ready StorageInterface, or a factory returning one.

The factory form exists for late binding — handlers are typically defined at module load while the real backend (Redis, KV, ...) only exists once the server has started. It is called on the first actual cache read/write, never at definition time, and at most once per cached function/handler.

Development

local development
  • Clone this repository
  • Install latest LTS version of Node.js
  • Enable Corepack using corepack enable
  • Install dependencies using pnpm install
  • Run interactive tests using pnpm dev

License

Published under the MIT license 💛.

About

Standalone caching utilities with TTL, SWR, and HTTP response caching

Resources

Stars

86 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages