npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

best-before

v0.1.1

Published

RFC 9111 HTTP caching semantics layered on the Web Cache API

Readme

best-before

best-before is a minimal RFC 9111 HTTP caching semantics layer on top of the Web Cache API.

Implementation

The goal of best-before is to layer RFC 9111 semantics on top of any runtime implementation that can provide a Web Cache API store. Specifically, it accepts as store (caches.open(name)) and layers Cache-Control client and server semantics, non-GET request remapping, stale-while-revalidate, and stale-if-error behaviour on top. The goal is to be as accurate as possible, without adding all CDN-level request collapsing and strictness guarantees.

  • Runtime-agnostic across Web Cache API runtimes: Works against runtimes that support Request, Response, and Cache
  • Partial body hashing: Creates a max-1MB hash for request body caching
  • Eager read, lazy fetch: Splits cache response from execution, to encourage racing cache retrieval then execute fetches lazily

Standards referenced:

The underlying implementation assumes that Vary can only be supported when Cache#match honours Vary. If the Web Cache store does not support Vary, related functionality and invalidation won't work as expected.

Quick start

In a Cloudflare Worker, usage may involve invoking createHttpCache and passing it a Cache.

import { createHttpCache } from 'best-before';

export default {
  async fetch(request, env, ctx) {
    const cache = createHttpCache(await caches.open('http'));

    const outcome = await cache.handle(
      request,
      // passthrough function calling the origin fetch the cache wraps
      req => fetch(req),
      // accepts ctx.waitUntil for deferred revalidation
      ctx
    );

    // outcome.response may deliver a cached Response,
    // while outcome.resolve falls through to the passthrough
    return outcome.resolve();
  },
};

Behavior

Eager read, lazy fetch

handle() probes the cache and returns a CacheOutcome. The common path is to call resolve immediately ((await cache.handle(...).resolve()). resolve() handles resolution of uncached responses. When an ExecutionContext is passed, waitUntil is called when a passthrough is for background revalidation.

The response property is used when the cache is raced against another source, which may skip the origin. Read response first to see if the cache had an available response, without paying the cost of calling the origin via resolve(), then only call resolve() when the actual response is needed.

A stale-while-revalidate revalidation is deferred until either response is ready or resolve() is called.

// Probes the cache and may deliver a cached response early
const outcome = await cache.handle(request, callOrigin, ctx);
// `outcome.response` may be populated, but `tryAsset` takes precedence and the passthrough can be deferred
return (await tryAsset(request)) ?? (await outcome.resolve());

Shared vs. private cache

shared (default true) selects RFC 9111 shared-cache semantics. Set shared: false for a private / client-side cache (browser-like):

| | shared (default) | private | | -------------------------------------------------------- | ---------------------- | -------------------------- | | bare Cache-Control: max-age=… (no public/s-maxage) | not stored | cached & served | | Expires (no Cache-Control) | cached & served | cached & served | | s-maxage | governs freshness | ignored (shared-only) | | private responses | not stored | stored & served | | Authorization on request | not publicly cacheable | cached (single-user store) | | proxy-revalidate | forces revalidation | ignored (shared-only) |

By default a shared cache treats a plain Cache-Control: max-age directive (without proxy-specific public, s-max-age, cdn-cache-control, must-revalidate, or proxy-revalidate) as private and does not store it. A server-set expiration, that isn't Cache-Control, such as Expires, Access-Control-Max-Age for CORS, or a permanent 301/308 redirect, is always shared-cached.

Setting requireSharedDirective: false bypasses this and treats unauthenticated GET and HEAD as cacheable by default. Either way, s-maxage takes precedence, and Authorization or private responses are never shared-cached.

Isolating caches per user is your responsibility with shared: false, and user-specific responses will be cached. If a single shared store is used, one user's data may leak to another, if the store isn't partitioned. The CacheStore interface should be wrapped or partitioned before passing it to best-before with shared: false.

Conditional requests

On a fresh cache hit, if the client's If-None-Match or If-Modified-Since already match the stored entry, the library returns an empty 304 Not Modified (weak ETag comparison, * and comma-lists, If-None-Match taking precedence over If-Modified-Since; GET/HEAD only)

However, origin revalidation defaults to re-fetching a full 200 response by stripping conditional headers. Set conditionalRevalidation: true to instead sent the stored entry's validator headers when revalidating a stale entry. On a 304, the retained body is re-served and re-stored with refreshed metadata. This is an efficiency optimization, not a correctness change, and is opt-in.

Pure decision engine

The RFC 9111 decision logic is exported as standalone, pure-over-arguments functions, so you can build custom orchestration or use the library purely as a decision engine.

import {
  computeStoreDecision,
  computeCacheDecision,
  parseCacheControl,
} from 'best-before';

// Should this origin response be stored, and with what Cache-Control?
const store = computeStoreDecision(request, response, { shared: true });

// Given a stored entry, is it a hit / stale / miss / timeout?
const decision = computeCacheDecision(
  parseCacheControl(request.headers.get('cache-control')),
  store.input,
  age,
  { staleWhileRevalidate: true, shared: true }
);

API Reference

createHttpCache(store: CacheStore, options?: HttpCacheOptions) => HttpCache

  • Accepts a CacheStore (the native Cache from caches.open(name) works directly)
  • Parameters
    • options?: HttpCacheOptions: feature toggles (all default on-by-default; see below)

Returns an HttpCache that layers RFC 9111 freshness/staleness semantics over the store.

interface HttpCache

interface HttpCache {
  handle(
    request: Request,
    passthrough: Passthrough,
    ctx?: ExecutionCtx
  ): Promise<CacheOutcome>;
}
  • handle probes the store for request and returns a CacheOutcome. It never calls passthrough itself.
  • Parameters
    • passthrough: the origin fetch the cache wraps, run at most once and only when the origin is needed
    • ctx?: when given, background revalidations use ctx.waitUntil

interface CacheOutcome

interface CacheOutcome {
  readonly response: CacheResponse | undefined;
  resolve(): Promise<CacheResponse>;
}
  • response: the answer the cache has ready without the origin (a hit, an SWR-stale entry, a 304, or an only-if-cached 504), or undefined on a miss. Reading it never calls passthrough.
  • resolve(): the response to serve, always. Returns response when the cache can answer; otherwise runs passthrough and stores the result. Runs at most once.

class CacheResponse extends Response

A Response (return it directly) with two extra own properties:

  • Properties
    • decision: CacheDecision: the freshness/staleness verdict
    • cacheStatus: CacheStatus: an RFC 9211-shaped description of how the request was served

The library never emits a Cache-Status header, but once can be derived by formatting cacheStatus.

type Passthrough = (request: Request) => Promise<Response>

The origin fetch the cache wraps. Called by resolve() (or a deferred stale-while-revalidate revalidation) with a request that may be rewritten (conditional headers stripped, HEAD remapped to GET, the stored entry's validators added under conditionalRevalidation).

interface ExecutionCtx

interface ExecutionCtx {
  waitUntil(promise: Promise<unknown>): void;
}

interface CacheStore

interface CacheStore {
  match(
    request: Request,
    options?: { ignoreMethod?: boolean }
  ): Promise<Response | undefined>;
  put(request: Request, response: Response): Promise<void>;
  delete(
    request: Request,
    options?: { ignoreVary?: boolean }
  ): Promise<boolean>;
}

The minimal storage surface the library needs. The native Cache satisfies it structurally, so caches.open(name) can be passed directly. Namespacing should be done by cache name (caches.open('http-v2')), not a key prefix. Wrap the store to transform what's stored/read, instrument hits/puts, scope keys, attach Cache-Tag, or back it with an alternate store.

[!IMPORTANT] A from-scratch store must implement Vary matching. The library stores the Vary header and rejects Vary: *, but delegates secondary-key selection to match. It does not compare varied request headers itself. If your CacheStore does not support Vary, it won't function.

interface HttpCacheOptions

The options accepted by createHttpCache — all optional.

| Option | Default | Description | | ------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | staleWhileRevalidate | true | Honor stale-while-revalidate (serve stale, revalidate in the background) | | staleIfError | true | Honor stale-if-error (serve stale when the origin errors) | | onlyIfCached | true | Honor the client's only-if-cached (a miss becomes a 504) | | clientCacheBypass | true | Honor client cache-busting (no-cache, max-age=0, etc); set false to ignore it and protect the origin | | shared | true | Shared cache (true) vs private/client-side cache (false) | | requireSharedDirective | true | Require an explicit shared directive (public, s-maxage, etc) before shared-caching a bare max-age, treating it as private; Expires/CORS/301-308 stay shared-cached | | cacheNonGetMethods | false | Cache non-GET/HEAD methods (POST, OPTIONS, etc) via method+body-hashed synthetic GET keys | | conditionalRevalidation | false | Revalidate a stale entry with a conditional request instead of a full re-fetch |

The min-fresh and max-stale request directives are only partially honored (some clients, e.g. Chrome, don't send them)

computeStoreDecision(request: Request, response: ResponseLike, options: StoreDecisionOptions) => StoreDecision | null

Pure over (request, response). Accepts options:

  • shared (default true): shared vs private cache semantics
  • requireSharedDirective (default true): shared cache only — treat a bare max-age as private

Returns null when the response isn't storable, otherwise a StoreDecision of two Cache-Controls: input and output.

interface StoreDecision {
  readonly input: CacheControl;
  readonly output: CacheControl;
}
  • input: the Cache-Control that governs how the stored entry is later evaluated (feed it to computeCacheDecision as entry.cacheControl)
  • output: the transformed Cache-Control persisted onto the stored response

computeCacheDecision(client: CacheControl, cacheControl: CacheControl, age: number, options: CacheDecisionOptions) => CacheDecision

Pure over its arguments. client is the parsed request directives, cacheControl the stored entry's governing directive (computeStoreDecision(...).input), and age its age in seconds. Accepts options:

  • staleWhileRevalidate
  • staleIfError
  • onlyIfCached
  • clientCacheBypass
  • shared

Returns a CacheDecision verdict.

enum CacheDecision {
  /** only-if-cached with no cached response, return 504 */
  MISS_TIMEOUT,
  /** a client directive forces revalidation */
  MISS_REQUEST,
  /** no cached response, origin consulted */
  MISS,
  /** fresh cached response served */
  HIT,
  /** request is not cacheable */
  BYPASS,
  /** origin errored and a stale entry may be served */
  STALE_IF_ERROR,
  /** stale entry served while revalidating in the background */
  STALE_WHILE_REVALIDATE,
}

parseCacheControl(headerValue: string | null) => CacheControl

  • Accepts a raw Cache-Control header value (or null)

Returns a parsed CacheControl. Fault-tolerant: unknown directives are collected into unrecognized and malformed values are skipped rather than throwing.