Skip to content

storage

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

Storage backends

io.cache reads and writes entries through a swappable storage interface — get / set / delete / clear / keys, sync or async. The TTL, revalidation, and invalidation logic is backend-agnostic; the backend only stores bytes. Four backends ship under double-meh/storage/:

Backend Import Platform Persistence
memory double-meh/storage/memory.js everywhere none — per instance (the default)
filesystem double-meh/storage/fs.js Node, Bun, Deno survives runs; shared across processes
SQLite double-meh/storage/sqlite.js Node ≥ 22.5, Bun survives runs; many small entries, concurrent writers
Cache API double-meh/storage/cache-api.js browsers, Deno survives reloads

Swap one in by assigning io.cache.storage:

import io from 'double-meh';
import {fsStorage} from 'double-meh/storage/fs.js';

io.cache.storage = fsStorage({name: 'my-app'});

memory

The default: a plain Map behind the interface. Zero setup, zero persistence — the right fit for a long-running service and intra-run dedup.

import {memoryStorage} from 'double-meh/storage/memory.js';

io.cache.storage = memoryStorage(); // a fresh, empty store

filesystem

One file per entry in the OS cache directory$XDG_CACHE_HOME / ~/.cache on Linux, ~/Library/Caches on macOS, %LOCALAPPDATA% on Windows — namespaced by name. Filenames are SHA-256 hashes of the cache key; each file is a JSON metadata line followed by the raw body bytes. Writes go to a temp file and are renamed into place, so concurrent processes never observe a torn entry. Corrupt or foreign files degrade to a cache miss.

import {fsStorage} from 'double-meh/storage/fs.js';

io.cache.storage = fsStorage({name: 'my-app'}); // <OS cache dir>/my-app/
io.cache.storage = fsStorage({directory: '/tmp/scratch-cache'}); // explicit location

Options: name (app namespace inside the OS cache dir, default 'double-meh') or directory (explicit path, overrides name). The resolved path is exposed as storage.directory.

SQLite

An optional backend for heavy use: many small entries, concurrent writers, transactional atomicity. It is feature-detectedbun:sqlite on Bun, node:sqlite on Node (≥ 22.5) — and deliberately not supported on Deno, which ships no built-in driver: a dependency would leave the zero-dependency path. The factory is async because the driver import is.

import {sqliteStorage} from 'double-meh/storage/sqlite.js';

io.cache.storage = await sqliteStorage({name: 'my-app'}); // <OS cache dir>/my-app/cache.sqlite
io.cache.storage = await sqliteStorage({database: ':memory:'}); // explicit database

Options: name (as above) or database (a file path or ':memory:'). The storage object adds close() and exposes the resolved path as storage.database.

Cache API

The browser backend: entries persist in caches.open(name) and survive reloads, so a returning user starts warm. Entry metadata rides as synthetic x-io-* headers on the stored Response (stripped on read). Requires a secure context (HTTPS or localhost).

import {cacheApiStorage} from 'double-meh/storage/cache-api.js';

io.cache.storage = cacheApiStorage({name: 'my-app'});

Deno also ships the Cache API, but without cache.keys()keys, sweep, and prefix eviction need a browser there; exact get/set/remove work everywhere.

Freshness on reload is a recipe, not built-in behavior: to let a full reload clear the app cache, call caches.delete('my-app') (optionally gated on a real reload via the Navigation Timing API), or scope it with prefix eviction.

Writing your own

Any object with the five methods works; each may return a value or a promise:

io.cache.storage = {
  get: key => myStore.read(key), // CacheEntry | undefined
  set: (key, entry) => myStore.write(key, entry),
  delete: key => myStore.drop(key),
  clear: () => myStore.reset(),
  keys: () => myStore.list() // string[]
};

An entry is {status, statusText, headers, body, etag?, lastModified?, expiresAt, vary?}headers as an array of pairs, body an ArrayBuffer, expiresAt an absolute timestamp (Infinity = never expires), and vary an optional selecting-header snapshot (see Cache § Representations and Vary) that must round-trip as stored. Keep etag / lastModified mirrored in headers (the built-in entries always are — both fields are derived from them).

See also

Clone this wiki locally