Skip to content

Services Cache

Eugene Lazutkin edited this page Jul 1, 2026 · 2 revisions

Cache

io.cache is an opt-in, application-governed response cache for GET requests. It is not an HTTP cache: it does not read Cache-Control or max-age. TTLs come from your call, the app decides what to store, and the app decides when to sweep.

import io from 'double-meh';

io.cache.attach();
const a = await io.get('https://example.com/users/42', null, {cache: true});
const b = await io.get('https://example.com/users/42', null, {cache: true});
// second call is served from the cache, no network request

Opting in

Caching only happens when a request opts in with cache, and only for GET. Use cache: true for the default TTL, or cache: {ttl} (milliseconds) to override it. stream: true requests are never cached.

await io.get('https://example.com/c', null, {cache: true});
await io.get('https://example.com/c', null, {cache: {ttl: 30_000}});

App-governed TTL

io.cache.defaultTtl is 5 minutes. Each entry stores an absolute expiresAt; a fresh entry is served directly, an expired one is refetched. Pass ttl: Infinity to keep an entry until you evict it, or ttl: 0 to force a refetch every time.

io.cache.defaultTtl = 60_000;
await io.get('https://example.com/t', null, {cache: {ttl: 0}}); // always refetched

304 revalidation

When a stale entry carries an ETag or Last-Modified, the next read attaches If-None-Match / If-Modified-Since. A 304 Not Modified reuses the stored body and refreshes the entry's expiresAt — no re-download.

await io.get('https://example.com/r', null, {cache: {ttl: 0}});
// stale entry with ETag "v1" → conditional request → 304 → cached body reused
const body = await io.get('https://example.com/r', null, {cache: {ttl: 0}});

Only ok (2xx) responses are ever stored; error responses pass straight through.

Invalidation with remove

io.cache.remove(pattern) evicts by:

  • an exact URL string,
  • a trailing-* prefix string,
  • a RegExp tested against the stored key,
  • a function (key) => boolean.
await io.cache.remove('https://example.com/users/42');  // exact
await io.cache.remove('https://example.com/users/*');   // prefix
await io.cache.remove(/\/users\//);                     // RegExp on the key
await io.cache.remove(key => key.includes('draft'));    // predicate

Clearing and sweeping

io.cache.clear() drops everything. io.cache.sweep() deletes only expired entries — it is app-called, nothing runs it for you, so call it on your own schedule (e.g. an interval or idle callback).

await io.cache.clear();
await io.cache.sweep(); // purge entries past their expiresAt

Swappable storage

The backend lives at io.cache.storage and defaults to an in-memory Map. Any object implementing get / set / delete / clear / keys (sync or async) can replace it — for example a persistent or shared store.

io.cache.storage = {
  get: key => myStore.read(key),
  set: (key, entry) => myStore.write(key, entry),
  delete: key => myStore.drop(key),
  clear: () => myStore.reset(),
  keys: () => myStore.list()
};

Pre-populating with save

io.cache.save(target, response, ttl?) stores a response directly. io.adopt uses it under the hood, so an adopted GET is durable past its in-flight window and later cache: true reads hit without a network call.

await io.adopt('https://example.com/cf', json({from: 'prefetch'}));
const data = await io.get('https://example.com/cf', null, {cache: true}); // no network

Attaching

io.cache.attach() installs the service (and sets io.cache.isActive); io.cache.detach() removes it. Both return io for chaining.

io.cache.attach();
io.cache.detach();

See also

Clone this wiki locally