Migration Guide

#Migrating to v0.3

ocache v0.3.0 is now available. This guide covers upgrading from v0.2. Most applications can upgrade by choosing a storage instance, clearing persistent entries, and auditing what cached HTTP handlers read and return. The v0.1 to v0.2 summary covers applications upgrading across both releases.

#Upgrade checklist

Replace setStorage() / useStorage() with the storage option.
Flush persistent v0.2 entries or move v0.3 to a new base prefix.
Declare every request header a cached handler reads in varies (or use the credential and cookie options), and every query parameter it reads in allowQuery.
Split cookie-setting responses out of cacheable routes.
Check that cached responses use a cacheable status and do not opt out through Cache-Control or Vary.
Set maxResolveTime and memory/body limits explicitly if the new defaults do not fit the workload.

#Storage is per cache instance

v0.2 used one process-global backend through setStorage() and useStorage(). v0.3 removes both exports. Every cached function and handler now owns a backend; its default is a fresh memory store that is not shared with any other instance.

Before:

import { defineCachedFunction, setStorage } from "ocache";

setStorage(storage);

const getUser = defineCachedFunction(fetchUser, { maxAge: 60 });
const getPosts = defineCachedFunction(fetchPosts, { maxAge: 60 });

After:

import { defineCachedFunction } from "ocache";

const getUser = defineCachedFunction(fetchUser, { maxAge: 60, storage });
const getPosts = defineCachedFunction(fetchPosts, { maxAge: 60, storage });

Pass the same instance when caches should share a connection, eviction budget, or persistent namespace. A factory can defer backend creation until the first operation:

const handler = defineCachedHandler(renderPage, {
  storage: () => storage,
});

The factory runs once per cached function or handler. See Storage.

#Standalone invalidation needs storage

invalidateCache() and expireCache() no longer fall back to a global backend. They throw when options.storage is absent. Prefer the methods attached to a cached function or handler:

await getUser.invalidate(id);
await handler.expire(event);

When a standalone helper is necessary, pass the original options object or reconstruct the options with the same backend:

const cacheOptions = {
  name: "getUser",
  getKey: (id: string) => id,
  maxAge: 60,
  storage,
};

const getUser = defineCachedFunction(fetchUser, cacheOptions);

await invalidateCache({ options: cacheOptions, args: [id] });

The instance methods also prevent an in-flight resolution or delayed write from undoing a purge. Standalone helpers cannot coordinate with an instance, so use them only when no instance reference is available. See Invalidation & Expiration.

#Start with an empty persistent namespace

Do not expect v0.2 and v0.3 to share persistent entries. Several corrections intentionally change cache identity:

  • handler keys include the request scheme, host, and port;
  • HEAD has a separate key from GET;
  • every name and group segment is escaped so it cannot alter key structure;
  • custom handler keys are escaped for the same reason;
  • argument, integrity, path, header, cookie, and ETag hashes use the standalone deterministic serializer and SHA-256 implementation;
  • malformed or unsafe entries are rejected more strictly on read.

Flush the old namespace during deployment, or change base while rolling out:

const options = {
  base: "/cache-v3",
  storage,
};

Using a new prefix is the safest rolling-upgrade strategy because old and new processes can otherwise read or overwrite entries under incompatible key and validation rules. Remove the v0.2 prefix after all old processes and entries are gone.

Important

Set explicit, stable name values for functions or handlers created in factories and loops. Equal-source closures receive the same generated name and therefore share entries when they use the same storage.

#Cached handlers see only keyed request data

A cached handler may now read exactly the inputs represented by its key. On cacheable requests, ocache removes every undeclared request header before calling the handler. Add each representation-changing header to varies:

const handler = defineCachedHandler(renderPage, {
  varies: ["accept-language", "user-agent"],
});

This is an allowlist. Audit reads of event.req.headers, including reads hidden in utilities called by the handler. There are no implicit exemptions for conditional, tracing, or request-ID headers. If output depends on one, declare it; if it is needed only for logging or tracing, capture it in middleware or elsewhere on the framework event before the cached handler runs.

Other request inputs follow the same rule:

  • allowQuery controls both the query names in the generated key and the query names visible to the handler. It is now opt-in: with the option unset, no query parameter varies the key and the handler receives a query-less URL. allowQuery: true restores the v0.2 behavior of keying and forwarding the full query string.
  • allowCookies controls both the cookie subset in the key and the filtered Cookie header visible to the handler.
  • varies: ["cookie"] opts in the complete raw Cookie header.
  • allowAuthorization: true opts in Authorization and Proxy-Authorization; by default both are removed.
  • a custom getKey replaces generated identity, but does not disable request filtering.

If a request must reach the handler unchanged, bypass caching instead:

const handler = defineCachedHandler(renderPrivatePage, {
  shouldBypassCache: (event) => event.req.headers.has("authorization"),
});

Non-cacheable methods and requests with Range also bypass without narrowing or response rewriting.

#Host and multi-host applications

Generated handler keys now include the resolved URL authority. The handler's Host header is rewritten to the host from that URL, ensuring the visible value is covered by the key. Applications that serve more than one origin no longer share entries for equal paths.

Check adapters that supply both event.url and event.req.url: they must describe the intended public authority. If output intentionally depends on a raw forwarded host header, list that header in varies rather than relying on an undeclared value.

A cacheable response can be replayed to later callers and shared by concurrent callers, so v0.3 always removes Set-Cookie. allowCookies controls request cookies only; it does not opt response cookies back in.

Move cookie creation to a non-cacheable request or bypass that request:

const handler = defineCachedHandler(route, {
  shouldBypassCache: (event) => event.req.url.endsWith("/session"),
});

allowCookies and varies: ["cookie"] now emit Vary: Cookie. This may reduce browser or CDN hit rates, but omitting it can serve one cookie-selected representation to callers with another cookie value. For server-only cookie variation, consider sendCacheControl: false; note that this suppresses only ocache's generated freshness header and is not an explicit downstream no-store policy.

See Cookies for safe patterns.

#HTTP cache eligibility is stricter

v0.2 broadly rejected errors and accepted many other responses. v0.3 stores only responses that pass all built-in checks.

#Status allowlist

Only these statuses are stored:

  • 200 OK
  • 203 Non-Authoritative Information
  • 301 Moved Permanently
  • 308 Permanent Redirect

Responses with null bodies, including 204, 205, and 304, are not stored. Use shouldCache to narrow this list further; it cannot widen it.

#Response opt-outs

A response is returned live but not stored when it has any of these properties:

  • Cache-Control: no-store, private, or no-cache;
  • an effective s-maxage=0 or max-age=0;
  • Vary: *;
  • a Vary name not covered by varies, allowCookies, or allowAuthorization;
  • a non-positive static or dynamic maxAge;
  • a body larger than maxBodySize.

A response rejected because of its status or Vary value receives no synthesized Cache-Control. Handler-provided headers remain intact except for Set-Cookie on cacheable routes. An oversized response bypasses serialization entirely, so it receives none of ocache's synthesized headers.

If a handler declares Vary, add every listed name to the cache configuration. Do not use Vary: * on a response intended for storage.

#Cache-Control follows the enforced lifetime

Generated Cache-Control now describes the entry's actual static or getMaxAge lifetime. In particular:

  • dynamic maxAge and staleMaxAge values are advertised;
  • s-maxage takes precedence over max-age when parsing handler directives;
  • must-revalidate prevents stale serving for that response;
  • unlimited SWR does not emit a valueless stale-while-revalidate directive;
  • sendCacheControl: false suppresses only the generated header.

Review CDN behavior if it depended on v0.2's static or valueless directives. See Cache-Control & Eligibility.

#Conditional requests and validators

v0.3 continues to synthesize an ETag for stored bodies, but no longer synthesizes Last-Modified. A response participates in date validation only when the handler provides that header.

ETag values for binary responses change because text and binary representations now use separate hash domains. Clear persistent entries as described above rather than mixing validator formats.

Conditional matching is also more complete:

  • If-None-Match takes precedence over If-Modified-Since;
  • weak tags, tag lists, and * are supported;
  • a 304 echoes the stored ETag, Last-Modified, Cache-Control, Expires, Vary, and cache-status policy where present;
  • headersOnly: true always runs the handler and compares the request against that live response's validators.

Custom handleCacheHeaders hooks should read the captured conditions.ifNoneMatch and conditions.ifModifiedSince. The narrowed event.req intentionally no longer contains those undeclared headers.

#New resource limits

#Resolution deadline

A shared resolution and its hooks now have a 30-second default deadline. On timeout, all waiters reject, the old entry is evicted, and the key becomes available for another resolution.

Disable the deadline or choose a workload-specific value explicitly:

const cached = defineCachedFunction(slowJob, {
  maxResolveTime: 120,
  // maxResolveTime: 0, // disable
});

For cached handlers, the leading request receives the deadline through event.req.signal. Pass it to abort-aware upstream work:

const handler = defineCachedHandler(async (event) => {
  return fetch(upstream, { signal: event.req.signal });
});

#Memory and response bodies

The built-in memory backend still defaults to 10,000 entries, but now also defaults to a 100 MB estimated byte budget. It evicts least-recently-used entries when either ceiling is exceeded. New options are available:

const storage = createMemoryStorage({
  maxSize: 20_000,
  maxBytes: 256 * 1024 * 1024,
  sizeOf: (value, key) => measure(value, key),
});

An entry larger than maxBytes, or one whose size cannot be measured, is refused. Set Infinity or 0 to disable a limit.

Cached handlers also stop buffering bodies larger than maxBodySize. Its default is derived from the backend's optional maxEntryBytes declaration; a backend without a declared ceiling has no derived body limit. An oversized response is served live and complete, but is not stored or decorated with generated cache headers.

Backends that preserve Uint8Array values may declare binary: true to store binary response bodies without base64. Do not set it on JSON-serializing backends. See the storage interface and Response size.

#Coming from v0.1

Upgrade v0.1 applications to the v0.2 behavior below first, then apply the v0.2-to-v0.3 changes above.

#SWR became opt-in

v0.1 enabled stale-while-revalidate by default. Since v0.2, swr defaults to false: an expired entry blocks while it is revalidated. Add swr: true anywhere that must continue serving stale data in the background.

staleMaxAge: 0 now means no stale serving. Use a positive number for a bounded stale window, or omit it with swr: true for a window bounded only by backend eviction.

#validate changed signature

The arguments moved into a context object and the hook may be asynchronous.

Before v0.2:

validate: (entry, id, locale) => entry.value.id === id,

Since v0.2:

validate: async (entry, { args: [id, locale] }) => entry.value.id === id,

Validation receives the stored shape when serialize is configured.

#Generated function names changed keys

When name is omitted, v0.2 uses fn.name, with a source hash fallback for anonymous functions, instead of placing every function under "_". This fixes collisions but changes keys. Flush old persistent entries or keep an explicit legacy name during a staged migration. Factory-created equal-source closures still need distinct explicit names when they share storage.

#Cookies became deny-by-default

v0.1 handlers could observe cookies without representing them safely in the key. v0.2 removes request cookies on cacheable routes unless they are explicitly selected:

const handler = defineCachedHandler(render, {
  allowCookies: ["theme", "locale"],
});

allowCookies filters the Cookie header and keys the allowed subset. varies: ["cookie"] selects the whole raw header. In v0.2, non-allowlisted Set-Cookie values were removed while allowed ones could survive; v0.3 tightens this further and removes every Set-Cookie from cacheable responses.

#Bypassed responses pass through unchanged

Since v0.2, non-cacheable methods and shouldBypassCache responses skip serialization and transformation. Streaming and binary bodies, request credentials, full query strings, and handler-provided headers pass through untouched. Do not rely on cache-generated ETag, Cache-Control, or status headers on a bypass path.

#APIs added across v0.1.x and v0.2

The v0.1.x releases added multi-tier base prefixes plus .resolveKeys(), .invalidate(), and .expire() on cached functions. v0.2 extended the same management methods to cached handlers and added APIs that replace common application-side workarounds:

  • getMaxAge for per-entry lifetimes;
  • serialize for converting values once before storage;
  • async validate and shouldCache for narrowing eligibility;
  • allowQuery for key-and-request query filtering;
  • cacheStatusHeader and CacheEntry.status for hit/miss reporting;
  • sendCacheControl: false for suppressing synthesized downstream freshness;
  • binary HTTP response preservation;
  • .resolveKeys(), .invalidate(), and .expire() on cached handlers;
  • createMemoryStorage({ maxSize }) with a 10,000-entry LRU default.

The v0.3 sections above supersede v0.2 where behavior changed again, especially storage ownership, cookie responses, byte limits, key construction, and HTTP eligibility.

#Smaller v0.3 behavior changes

  • Returning false, 0, or "" from transform now returns that value instead of falling back to the cached value.
  • Explicit undefined option values now fall back to defaults instead of overwriting them. null remains an explicit value where the option accepts it.
  • .invalidate() and .expire() cover all base tiers; handler methods cover both GET and HEAD variants of the resource.
  • Stale values are served only when their integrity matches the current function and options.
  • Non-positive maxAge values are not written as immortal, unusable entries.

After migrating, run representative requests twice and verify X-Cache: MISS followed by X-Cache: HIT, then test bypass, conditional, cookie, authorization, oversized-response, and invalidation paths relevant to the application.