Query Start
Client setup
@svelte-lean/query stable target./http stable target./laravel stable target./parts stable targetOne client per application: how requests are sent, what partitions the cache, who may read what, which modules are on and which engine parts load up front.
Markdown version of this page, for assistants (Markdown for AI)
On this page
Install
npm install @svelte-lean/querypnpm add @svelte-lean/queryyarn add @svelte-lean/querybun add @svelte-lean/queryClient
Create it once in the root layout. Components, classes and modules reach the same client; on the server each request gets its own (Server rendering).
<!-- src/routes/+layout.svelte — one client for the application -->
<script lang="ts">
import { createClient, setClient } from '@svelte-lean/query';
import { http } from '@svelte-lean/query/http';
import { can, session } from '$api/session.svelte';
let { children } = $props();
setClient(
createClient({
transport: http({
base: '/',
headers: () => ({ authorization: `Bearer ${session.token}` })
}),
scope: () => [session.workspace], // part of every cache key
can // resolves the permission strings of `access` rules
})
);
</script>
{@render children()}Transport
The core never calls fetch itself; http() does.
// src/lib/api/transport.ts
import { http } from '@svelte-lean/query/http';
import { laravel } from '@svelte-lean/query/laravel';
import { refreshToken, session } from '$api/session.svelte';
export const transport = http({
base: '/',
headers: () => ({ authorization: `Bearer ${session.token}` }),
timeout: '30s',
// The platform's conventions: envelope, refusals, 422 field errors, paginators.
profile: laravel(),
// The last word on a response: refresh the token once on a 401 and send again.
intercept: async (response, _request, retry) => {
if (response.status !== 401) return response;
await refreshToken();
return retry();
}
});| Option | Default | Use |
|---|---|---|
base | '/' or a function of the request | Prefix of every path; a function picks an origin per request. |
headers | (request) => HeadersInit | Read per request: a token that changes is sent fresh. |
timeout | '30s' | Per attempt; a timeout is a typed error and is retried for queries. |
profile | none: JSON in, JSON out | The platform conventions (laravel(), or your own). |
intercept | none | The last word on a response: refresh and retry, sign out. |
fetch | globalThis.fetch | SvelteKit's fetch in a load, a fake in tests. |
Profile
A profile states the server's conventions once. laravel() unwraps the { success, data, message } envelope, turns success: false into a
typed refusal that is never retried, reads 422 field errors and paginators, and writes query
strings and multipart bodies the way Laravel reads them. Any server can have its own profile: the profile contract.
Scope
The scope prefixes every cache key, so the data of one workspace, tenant or region never answers for another.
// Part of every cache key: entries of one workspace never answer another.
scope: () => [session.workspace],
// On a change, the previous workspace's entries are dropped (the default) or kept.
onScopeChange: 'drop',Access
Endpoints name permissions; the client resolves them. Three answers: true sends, false sends nothing and removes cached data, undefined (rights still loading)
waits and keeps what is shown.
// Resolves the permission strings of `access` rules. `undefined` = not known yet.
can,
// One more rule for every endpoint, combined with each endpoint's own `access`.
access: () => (session.userId === null ? false : true),Defaults and limits
defaults: {
fresh: '10s', // default 0: stale at once, revalidated on the next use
keep: '10m', // default '5m': how long an unobserved entry stays
retry: 2, // queries only; writes are never retried unless they say so
slowAfter: '8s', // `info.slow` turns true after this
focus: true // revalidate stale observed entries when the tab is shown again
},
limits: {
concurrency: 'auto', // 6 per origin; 'auto' raises it once HTTP/2 or HTTP/3 is seen
inactiveEntries: 2000 // with inactiveBytes: the budget of unobserved entries
},| Option | Default | Meaning |
|---|---|---|
defaults.fresh | 0 | How long a value counts as fresh; stale values are revalidated on use. |
defaults.keep | '5m' | How long an entry nobody shows stays cached. |
defaults.retry | 2 | Retries of a failed read (network, timeout, 5xx, 408, 429). Writes: 0. |
defaults.slowAfter | '8s' | When the slow flag of a fetch or a write turns on. |
defaults.loadingDelay | 0 | With data on screen, a fetch reads as refreshing until it has run this long. |
defaults.focus | true | Revalidate stale entries on screen when the tab is shown again. |
limits.concurrency | 6 | Requests at once per origin; 'auto' raises it to 24 once an answer reports HTTP/2 or HTTP/3, which a cross-origin API shows only with Timing-Allow-Origin (otherwise set a number). |
limits.inactiveBytes | 33554432 | Byte budget of unobserved entries; the oldest leave first. |
limits.inactiveEntries | 2000 | Count budget of unobserved entries. |
An endpoint's own fresh, keep and retry win over these.
Whatever the cap, background work never takes an origin's last slot: invalidations, polls,
prefetches and endpoints declared priority: 'low' (a dashboard's figures) queue
behind the reads a page waits for. A use or a call can say priority too.
Modules
Optional behavior is a module, imported from its own entry point and set up with the client.
// Optional behavior, each its own import: nothing else is in the bundle.
modules: [devtools(), tabs({ session: () => String(session.userId) })]| Entry point | Module | Guide |
|---|---|---|
./live | live(source, rules) | Realtime |
./tabs | tabs({ session }) | Realtime |
./persist | persist({ store, session }) | Persistence |
./ssr | kitInvalidate({ invalidate }) | Server rendering |
./devtools | devtools({ panel: true }) | Devtools |
A module of your own
// Reports every failed answer; setup returns the teardown.
export const reportFailures = defineModule({
name: 'report-failures',
setup(_client, hooks) {
return hooks.onCommit((entry) => {
if (entry.status === 'error') console.warn(entry.endpoint.id, entry.error?.kind);
});
}
});
// createClient({ transport, modules: [reportFailures] })defineModule brings the hook set with the module, so a client without modules
carries none of it. Hooks: ModuleHooks.
Engine parts
Polling, connectivity, access rules, memory collection, the request limiter, identity reuse and the param index are optional parts of the engine.
- Small apps: nothing to do. Each part loads the first time the client needs it; an application that never polls never downloads the polling code.
- Large apps: include the parts statically. An admin panel that polls, checks rights and runs long sessions uses every part anyway; listing them puts them in the first load, with no extra chunk request and no loading step.
// Every engine part in the first load: no chunk request, no loading step.
createClient({ transport: http({ base: '/' }), parts: allParts });import { access, limiter, polling } from '@svelte-lean/query/parts';
// Only some; the others still load on first use.
createClient({ transport: http({ base: '/' }), parts: [access, limiter, polling] });Sizes per part and of allParts: Performance.
Reference: @svelte-lean/query/parts.
Outside components
// A class or a plain module reaches the same client: the browser has one per page.
import { getClient, invalidate, send } from '@svelte-lean/query';
export function signOut() {
getClient().clear(); // abort everything, drop every entry and optimistic layer
}
export function afterImport() {
invalidate(['Members', 'Teams']); // the free form of getClient().invalidate(…)
}In the browser the first client created is the realm client: components, classes, modules and
code after an await reach it. setClient(client) makes another one the
realm client (in a component it also sets the context; in a test's beforeEach it
only sets the realm), and disposing it hands the realm back to the client it replaced. onSuccess and optimistic always act on their mutation's client.
One request without the cache
For an endpoint the definitions cannot describe: the transport's headers, envelope and errors apply.
// One request through the transport (headers, envelope, typed errors) without the cache.
export async function ping(): Promise<{ ok: boolean }> {
return send<{ ok: boolean }>({ method: 'GET', path: 'api/health' });
}