SSR framework for Svelte 5 + Bun with islands-based selective hydration
Cache
MochiCache wraps stale-while-revalidate-cache for caching server-side data — typically slow upstream API calls. Construct once at module scope and share the instance across requests.
// src/lib/cache.ts
import { MochiCache } from 'mochi-framework';
export const pokemonCache = new MochiCache({
minTimeToStale: 10_000, // serve fresh for 10s
maxTimeToLive: 300_000, // hard expiry at 5min
});Use it from a page or API route:
<script>
import { params } from 'mochi-framework';
import { pokemonCache } from '../lib/cache';
const id = params.id ?? 'pikachu';
const pokemon = await pokemonCache.fetch(`pokemon:${id}`, async () => {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
return res.ok ? await res.json() : null;
});
</script>Behaviour
- Fresh (within
minTimeToStale): cached value returned, no fetch. - Stale (between
minTimeToStaleandmaxTimeToLive): cached value returned immediately, fetch runs in the background and updates the cache. - Expired (past
maxTimeToLive): fetch runs synchronously and the caller waits.
API
| Method | Returns |
|---|---|
fetch(key, fn) | Promise<T> |
fetchWithStatus(key, fn) | Promise<{ value, status }> |
delete(key) | Promise<void> |
status is 'fresh' \| 'stale' \| 'expired' \| 'miss'.
Options
MochiCacheOptions extends the upstream Config (so retry, retryDelay, serialize, deserialize and any future options pass through). Mochi-specific defaults:
| Option | Default |
|---|---|
minTimeToStale | 5_000 (5s) |
maxTimeToLive | 600_000 (10min) |
storage | in-memory Map |
For multi-process or persistent caching, pass a custom storage that implements getItem / setItem / removeItem (e.g. Redis, SQLite via bun:sqlite).
Subscribing to cache events
MochiCache emits two events on mochiEvents:
| Event | Payload | When |
|---|---|---|
cache:read | { key, status } | Every cache lookup, regardless of which method ran. |
cache:revalidate | { key } | A background refetch starts (stale read). |
status is 'fresh' \| 'stale' \| 'expired' \| 'miss'. Use mochiEvents.setHandler to attach a custom subscriber — it replaces a prior handler under the same name, so dev re-imports don’t pile up listeners:
import { mochiEvents } from 'mochi-framework';
mochiEvents.setHandler('metrics:cache-read', 'cache:read', ({ key, status }) => {
metrics.increment(`cache.${status}`, { key });
});logger() already prints cache:revalidate lines by default. Pass { cache: 'verbose' } to also print every read, or { cache: false } to silence cache logging:
import { logger } from 'mochi-framework';
logger({ cache: 'verbose' });See the Cache Events demo for a worked example that pipes events into an in-memory ring buffer and renders them on the page.
Server-only
MochiCache lives on the server. Importing it into a hydratable island throws — caches are shared per-process state and don’t make sense in the browser. Construct cache instances in .ts modules or page-route scripts, never inside a mochi:hydrate component.