Skip to content

DanMat/query-cache

Repository files navigation

@danmat/query-cache

CI npm version minified + gzip size License: MIT

RFC 10008-correct caching for the HTTP QUERY method. A QUERY's identity lives in its body, not its URL — so a correct cache key must incorporate the request content. This library derives such keys and ships a tiny in-memory cache built on them.

Zero dependencies. Fully typed. Isomorphic (Node 18+, Deno, Bun, browsers, edge — anywhere Web Crypto exists).

import { QueryCache } from "@danmat/query-cache";

const cache = new QueryCache({ maxEntries: 500 });

const res = await cache.wrap(
  { url: "https://api.example.com/search", body, headers },
  () => query(url, { body, headers }), // your fetcher, only called on a miss
);

The problem

RFC 10008 says QUERY responses are cacheable, but with a catch:

The cache key for a QUERY request MUST incorporate the request content and related metadata.

Every existing HTTP cache keys on the URL. For QUERY, two requests to the same URL with different bodies are different queries — key on the URL alone and you'll serve one query's results for another. This library does the body-aware keying the spec requires.

Install

npm install @danmat/query-cache

API

queryCacheKey(input, options?): Promise<string>

Derives a stable, collision-resistant SHA-256 key that incorporates the method, normalized URL (query params sorted), content type, any configured Vary headers, and the request body bytes. Accepts either a Request or a plain { url, method?, body?, headers? } descriptor.

const key = await queryCacheKey({
  url: "https://api.example.com/search",
  body: JSON.stringify({ filter: { status: "active" } }),
  headers: { "content-type": "application/json" },
});
// "9f2c…" — same bytes ⇒ same key; different body ⇒ different key
Option Type Description
varyHeaders string[] Extra request headers to fold into the key. content-type is always included.
normalizeBody (bytes, contentType) => Uint8Array Canonicalize the body before hashing (e.g. sorted-key JSON) so semantically-equal bodies collapse to one key.

class QueryCache

A minimal in-memory cache keyed by queryCacheKey. Honors Cache-Control: no-store and max-age, with optional LRU eviction and a default TTL. Responses are cloned on store and retrieval, so cached bodies stay readable.

const cache = new QueryCache({ maxEntries: 1000, ttl: 60_000 });

await cache.store(input, response);        // respects no-store / max-age
const hit = await cache.match(input);      // Response clone, or undefined
const res = await cache.wrap(input, fetch); // match-or-fetch-and-store
await cache.delete(input);
cache.clear();
cache.size; // number of live entries

Constructor options extend the key options above plus maxEntries, ttl (ms), and now (clock injection for testing).

Notes

  • Hashing uses the Web Crypto SubtleCrypto.digest global, with a node:crypto fallback for older Node — no dependencies either way.
  • Determinism: identical bytes hash identically. FormData bodies aren't recommended as cache inputs — their multipart boundary is randomized, so they won't produce stable keys. Prefer JSON/text/SQL payloads (which QUERY is designed for), or supply normalizeBody.

The @danmat QUERY suite

▶️ See them work together: query-suite-example — a runnable demo using all four, with a 🌐 live playground.

License

MIT © Dan Matthew

About

RFC 10008-correct caching for the HTTP QUERY method: cache keys derived from the request body, plus a tiny in-memory cache. Zero dependencies.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors