js/cache
js/cache.ts
fino:cache — small application cache abstraction with memory, SQLite, and HTTP helpers.
This module provides local cache primitives for application code that needs short-lived reuse without committing to a distributed cache contract. Cache entries are scoped by namespace, expire by millisecond TTL, and can be invalidated through tags. Values are serialized as JSON so the memory and SQLite backends share one observable value model.
Design
A Cache is a simple async key/value interface. memoryCache() is an LRU
cache intended for tests and single-process applications; sqliteCache()
persists entries through fino:database/sqlite and the configured
filesystem provider. responseCache() is a layer for fino:net/http/app
and caches safe GET/HEAD responses by method, URL, and configured vary
headers, stamping each response with an x-fino-cache diagnostic header.
Expiry is lazy: expired entries are removed by the read that observes them, not by a background sweeper.
This is deliberately not a distributed systems layer. There is no cross- process invalidation for memory caches, no cluster coherence, and no binary structured-clone value support in this baseline.
import { memoryCache, responseCache } from 'fino:cache';
import { App } from 'fino:net/http/app';
const cache = memoryCache({ maxEntries: 1_000 });
await cache.set('user:1', { name: 'Ada' }, { ttlMs: 60_000, tags: ['users'] });
const user = await cache.get('user:1');
const app = new App().layer(responseCache(cache, { ttlMs: 5_000 }));
Interfaces
interface CacheClock {
Clock used by cache backends to decide when entries expire.
Both memoryCache() and sqliteCache() read the clock on every operation
and compare it against each entry's stored expiry. Supplying a fake clock
makes TTL behavior deterministic in tests: advance the fake time instead of
sleeping.
import { memoryCache, type CacheClock } from 'fino:cache';
let now = 0;
const clock: CacheClock = { now: () => now };
const cache = memoryCache({ clock });
await cache.set('k', 'v', { ttlMs: 50 });
now += 51;
await cache.get('k'); // null — expired without waiting
Methods
now(): number
Return the current time in milliseconds.
interface CacheSetOptions {
Options applied when writing a cache entry with Cache.set().
Omitting ttlMs stores the entry without an expiry. A ttlMs of 0 (or a
negative value, which is clamped to 0) produces an entry that is already
expired on the next read. Tags are remembered per entry and matched later by
invalidateTags(); writing a key again replaces its previous tags entirely.
import { memoryCache } from 'fino:cache';
const cache = memoryCache();
await cache.set('user:1', { name: 'Ada' }, { ttlMs: 60_000, tags: ['users'] });
await cache.invalidateTags(['users']); // removes user:1
Properties
ttlMs?: number
Milliseconds until the entry expires. Omit for no expiry.
tags?: string[]
Tags used by invalidateTags() to remove related entries.
interface Cache {
Small async cache interface shared by all backends.
Values round-trip through JSON, so only JSON-serializable data survives a
set()/get() cycle, and reads return a fresh deserialized copy rather
than the object that was stored. get() returns null for missing or
expired entries; the read that observes an expired entry also deletes it.
Every operation is scoped to the cache's current namespace. namespace()
returns a view over the same backend with a different namespace, so keys do
not collide across namespaces and tag invalidation only affects the
namespace it is called on.
import { memoryCache, type Cache } from 'fino:cache';
const cache: Cache = memoryCache();
await cache.set('config', { theme: 'dark' }, { tags: ['settings'] });
const tenant = cache.namespace('tenant-42');
await tenant.set('config', { theme: 'light' });
await cache.get('config'); // { theme: 'dark' }
await tenant.get('config'); // { theme: 'light' }
await cache.invalidateTags(['settings']); // leaves tenant-42 untouched
Methods
get<T = unknown>(key: string): Promise<T | null>
Read key, returning null when the entry is missing or expired.
set<T = unknown>(key: string, value: T, opts?: CacheSetOptions): Promise<void>
Store value under key, replacing any previous value in this namespace.
delete(key: string): Promise<void>
Delete key from this namespace. Missing keys are ignored.
invalidateTags(tags: string[]): Promise<void>
Delete entries in this namespace that have any of the supplied tags.
namespace(name: string): Cache
Return a view over the same backend using name as its namespace.
interface MemoryCacheOptions {
Options for memoryCache().
import { memoryCache } from 'fino:cache';
const cache = memoryCache({
maxEntries: 10_000,
namespace: 'sessions'
});
Properties
maxEntries?: number
Maximum retained entries across all namespaces. Defaults to unlimited.
namespace?: string
Initial namespace. Defaults to "default".
clock?: CacheClock
Clock used for TTL checks. Defaults to Date.now().
interface SqliteCacheOptions {
Options for sqliteCache().
import { sqliteCache } from 'fino:cache';
import { DiskFileSystem } from 'fino:file';
const cache = await sqliteCache({
path: '/var/lib/myapp/cache.db',
namespace: 'render',
fs: new DiskFileSystem()
});
Properties
path: string
SQLite database path.
namespace?: string
Initial namespace. Defaults to "default".
clock?: CacheClock
Clock used for TTL checks. Defaults to Date.now().
fs?: FileSystem
Optional filesystem provider for the SQLite VFS.
interface SqliteCache extends Cache {
SQLite-backed cache handle returned by sqliteCache().
Adds close() on top of the Cache interface. Only the handle returned by
sqliteCache() owns the database connection; namespace views created with
namespace() share it without owning it, so closing the owning handle ends
access for every view derived from it.
import { sqliteCache } from 'fino:cache';
const cache = await sqliteCache({ path: '/tmp/app-cache.db' });
try {
await cache.set('greeting', 'hello', { ttlMs: 60_000 });
} finally {
await cache.close();
}
Methods
close(): Promise<void>
Close the underlying SQLite database.
interface ResponseCacheOptions {
Options for responseCache().
import { memoryCache, responseCache } from 'fino:cache';
const middleware = responseCache(memoryCache(), {
ttlMs: 5_000,
vary: ['accept-language'],
statuses: [200, 404],
header: 'x-cache'
});
Properties
ttlMs: number
TTL applied to every stored response.
methods?: string[]
HTTP methods to cache. Defaults to GET and HEAD.
statuses?: number[]
Response statuses to cache. Defaults to [200].
vary?: string[]
Request header names included in the cache key.
header?: false | string
Diagnostic header name, or false to disable it. Defaults to x-fino-cache.
Functions
function memoryCache(opts: MemoryCacheOptions = {}): Cache
Create an in-memory LRU cache.
Recency is tracked per key: reads refresh an entry's position, and when a
set() pushes the cache past maxEntries the least recently used entries
are evicted regardless of namespace. Entries are lost when the process exits
and are not shared across realms or processes. LRU size is counted in
entries, not bytes.
import { memoryCache } from 'fino:cache';
const cache = memoryCache({ maxEntries: 2 });
await cache.set('one', 1);
await cache.set('two', 2);
await cache.get('one'); // refreshes 'one'
await cache.set('three', 3); // evicts 'two', the least recently used
await cache.get('two'); // null
await cache.get('one'); // 1
function sqliteCache(opts: SqliteCacheOptions): Promise<SqliteCache>
Open a SQLite-backed cache.
Opens (creating if necessary) the database at opts.path and ensures the
fino_cache_entries and fino_cache_tags tables exist, so the same file
can also hold unrelated application tables. Entries persist across process
restarts; expired entries are removed lazily when a read observes them.
Call close() on the returned handle when the cache is no longer needed.
import { sqliteCache } from 'fino:cache';
const cache = await sqliteCache({ path: '/var/lib/myapp/cache.db' });
let report = await cache.get<string>('report:2026-07');
if (report === null) {
report = 'expensive result';
await cache.set('report:2026-07', report, { ttlMs: 3_600_000, tags: ['reports'] });
}
await cache.close();
function responseCache(cache: Cache, opts: ResponseCacheOptions): LayerMiddleware
Create HTTP response-cache middleware for fino:net/http/app.
Requests whose method is in methods (default GET/HEAD) are looked up
by method, full URL, and the values of the configured vary request
headers. On a hit the stored status, headers, and body are replayed without
invoking downstream handlers. On a miss the downstream response is stored
when it is safe to reuse: its status is in statuses (default [200]), it
carries no Set-Cookie header, and its Cache-Control does not include
no-store.
Unless disabled with header: false, every response gains a diagnostic
header (default x-fino-cache) valued HIT, MISS, or BYPASS, so cache
behavior is observable in tests and from clients. Stored entries expire
after ttlMs; pair the cache with invalidateTags() or delete() on a
namespaced view if routes need explicit invalidation.
import { memoryCache, responseCache } from 'fino:cache';
import { App } from 'fino:net/http/app';
const app = new App()
.layer(responseCache(memoryCache({ maxEntries: 500 }), {
ttlMs: 5_000,
vary: ['accept-language']
}));
app.get('/hello').handle(() => new Response('hello'));
// First request: x-fino-cache: MISS. Repeats within 5s: HIT.