🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

Request cache

The request cache memoizes work for the duration of one HTTP request. Entries die with the request, so a page that renders the same lookup in ten components pays for it once, and the next request sees fresh data.

import { requestCache } from 'mochi-framework';

const user = await requestCache(`user:${id}`, () => db.user(id));

Every value the callback reads must appear in the key. The cache never inspects the function, so a key that omits id collides silently.

requestMemo

Wrap a function once at module scope. Every call site is then memoized by its arguments:

import { requestMemo } from 'mochi-framework';

export const getUser = requestMemo((id: string) => db.user(id));
<script>
  import { getUser } from '../lib/users';

  // However many components call this, one query per request.
  const user = await getUser(params.id);
</script>

The wrapper is the shared identity. Two separate requestMemo() calls over the same function get separate entries. Export the wrapped function so every importer shares it, or pass { namespace } to share entries between wrappers.

Arguments are keyed by a type-tagged serialization (1 and '1' never collide, and objects go through JSON.stringify). For arguments that cannot serialize, pass your own key:

const getProfile = requestMemo((user: User) => db.profile(user.id), { key: (user) => user.id });

Async

Both forms store the in-flight promise on the first call, so concurrent callers share one execution:

// One fetch, three awaits.
const [a, b, c] = await Promise.all([getUser('42'), getUser('42'), getUser('42')]);

A rejected promise evicts its entry, so a failure is never cached. The next call retries.

The store

getRequestCache() returns the underlying store for imperative access:

import { getRequestCache } from 'mochi-framework';

const cache = getRequestCache();
cache.set('tenant', tenant);
cache.get('tenant');
cache.delete('tenant');
cache.stats(); // { hits, misses } — also shown in the debug bar's Cache panel

Outside a request

Called outside a request handler — a startup script, a background job, a detached email render — the callback runs uncached, with a one-time warning in development. Nothing throws, so helpers built on the request cache stay usable everywhere.

For a requestMemo wrapper expected to run outside a request, pass { quiet: true } to suppress that warning:

export const getUser = requestMemo((id: string) => db.user(id), { quiet: true });

On the client

These are server-only helpers. In the browser bundle they resolve to no-op stubs instead of throwing: requestCache(key, fn) runs fn() uncached, requestMemo(fn) returns fn unwrapped, and getRequestCache() hands back a fresh throwaway store per call.

In the debug bar

In development, the debug bar’s Cache panel has a Request cache section reporting hits, misses, hit rate, and surviving entries for the render that produced the page.

See it in action

Live demos showing key concepts from this page