Data Invalidation
Use depends(), invalidate(), and invalidateAll() to control when server loaders re-run on client-side navigation.
By default, Bosia caches the result of every +page.server.ts and +layout.server.ts load() function in the browser after the first run. On the next client-side navigation, a loader only re-runs when something it actually read has changed — a route param, a search param, a tracked URL, or a key the loader declared via depends(). Layouts that haven't conceptually changed (e.g. a navbar that only reads locals.user) skip the server round-trip entirely.
This is the same model as SvelteKit: opt-in, predictable, and explicit.
Automatic dependency tracking
A loader automatically depends on:
- Every
params.Xit reads - Every
url.searchParams.get(X)or.has(X)it reads - Every
url.pathname/origin/hrefread - Every
cookies.get(X)it reads - Every URL it passes to the injected
fetch()
// +page.server.ts
import type { LoadEvent } from "bosia";
export async function load({ params, url, fetch }: LoadEvent) {
const q = url.searchParams.get("q") ?? "";
const res = await fetch(`/api/posts?q=${q}&slug=${params.slug}`);
return { posts: await res.json() };
}This loader re-runs when params.slug changes, when ?q= changes, or when something explicitly invalidates /api/posts?q=.... Navigating to a sibling route that shares the same params re-uses the cached result.
`depends()`
Declare a custom dependency key inside a loader:
export async function load({ depends, locals }: LoadEvent) {
depends("app:user");
return { user: locals.user };
}Call invalidate("app:user") from anywhere on the client to force this loader to re-run on the next navigation.
`invalidate()`
import { invalidate } from "bosia/client";
// Custom key (matches `depends("app:user")`)
await invalidate("app:user");
// URL (matches any loader that fetched this URL)
await invalidate("/api/posts");
// Predicate over the tracked URL
await invalidate((url) => url.pathname.startsWith("/api/"));Invalidation marks the matching cache entries dirty and re-runs only those loaders the next time the nav effect fires. Calling invalidate() outside of a navigation (e.g. after a WebSocket message) triggers a re-run on the current URL.
`invalidateAll()`
import { invalidateAll } from "bosia/client";
await invalidateAll();Wipes every cache entry. Every loader re-runs on the next nav.
Form actions
use:enhance invalidates the page loader only by default. Layouts stay cached. If a mutation should also re-run a layout (e.g. updating the user's display name in the navbar), call invalidate() from the submit handler:
<script lang="ts">
import { enhance } from "bosia/client";
import { invalidate } from "bosia/client";
</script>
<form
method="POST"
use:enhance={() => {
return async ({ update }) => {
await update();
await invalidate("app:user");
};
}}
>
<!-- ... -->
</form>Caveats
setHeaders: a cached loader does not re-apply response headers. If a loader setsCache-ControlorSet-CookieviasetHeaders, ensure those don't depend on data the loader skips.metadata(): page-levelmetadata()always runs on every navigation. Cache scope is loaders only.- Hard refresh: the cache lives in browser memory and is wiped on full reload. The next navigation behaves like a first-load.
- Private routes: cache lives per-browser, so
(private)routes are safe — there is no cross-user leakage. Server-side request dedup remains disabled for(private)routes as before.
Server-side `invalidate()` for the response cache
The browser-side invalidate() evicts the per-browser loader cache. Since v0.6 Bosia also keeps a server-side response cache that skips load() + render() + compression on cache hits. It's evicted with a parallel API exported from the server-only subpath:
import { invalidate, invalidateAll } from "bosia/server";
// Form action — re-render the next GET of any page that called depends("app:user")
export const actions = {
default: async ({ request }) => {
await updateUserName(request);
invalidate("app:user");
},
};invalidate("app:user")evicts every cached page whose loader calleddepends("app:user").invalidate("/api/posts")evicts every cached page whose loader fetched/api/posts, plus the cached/api/postsAPI response itself.invalidateAll("/products/")evicts every cached entry whose path starts with/products/.
Form actions are the most common invalidation point — after a mutation, call invalidate() so the next read serves fresh HTML. The function returns synchronously (no await needed).
API endpoints (+server.ts) are invalidated by URL/prefix only in v0.6. Tag-based invalidation for API handlers is on the roadmap.
Opt a route out of the server cache entirely with export const cache = false; in +page.ts, +page.server.ts, or +server.ts. See Response cache for the full design.