# Persistence

> Opt-in IndexedDB persistence per endpoint and per session with ./persist: lazy restore, write-behind, maxAge and buster.

Page: https://svelte-lean.vercel.app/query/persistence · Markdown: https://svelte-lean.vercel.app/query/persistence.md · Index: https://svelte-lean.vercel.app/llms.txt

Reference data an application reads on every visit can paint from IndexedDB before the network answers. Only endpoints that ask for it are stored, per signed-in user.

> **Note** `./persist` is experimental: the API can still change in a minor release. Stability per entry point: [Entry points](https://svelte-lean.vercel.app/query.md#entry-points).

## Setup

`src/lib/query.ts`

```ts
export const persistence = persist({
	store: indexedDbStore(),
	// One partition per signed-in user; null (signed out) reads and writes nothing.
	session: () => (session.userId === null ? null : String(session.userId)),
	buster: BUILD_ID, // a new build drops every record
	maxAge: '24h',
	eager: [listMarkets] // restored before `ready` resolves
});

export const client = createClient({
	transport: http({ base: '/' }),
	modules: [persistence]
});
```

`src/lib/api/markets.ts`

```ts
export const listMarkets = defineQuery({
	get: 'api/markets',
	result: type<Market[]>(),
	entity: 'Market',
	tags: ['Markets'],
	fresh: '10m',
	persist: true // stored by ./persist, restored on the next visit
});
```

`persist: true` on the definition is the only opt-in; every other endpoint is never written. `persist: { maxAge: '1h' }` sets its own age.

## Restore

- Records are restored lazily: an entry reads its record when it is first used, unless its endpoint is listed in `eager`.
- While an entry's record is read, its request waits (no longer than the eager restore may hold `ready`; the binding shows `loading`). A record inside its `fresh` window is shown and no request is sent; an older one is shown and refetched in the background, as `refreshing`. `refetch()`, `load()` and `ep.fetch()` never wait.
- A record never replaces a newer value.
- `persistence.ready` resolves when the eager restore is done; it never rejects.
- When IndexedDB is unavailable (a private window), persistence is off for the session and queries use the network.

Measured (benchmark P7, a persisted list of 250 rows, reload): first data paint 115.2 ms after navigation start from IndexedDB, against 870 ms from the network without the module, where the test server answers after a fixed delay.

## Write-behind

Commits mark entries dirty; they are written in batches at background priority, one transaction per flush, and only the changed records. Longest task while flushing a thousand dirty entries among ten thousand persisted: 4.104 ms (engine benchmark B12).

## Sessions and sign-out

`sign out`

```ts
export async function signOut() {
	await persistence.forget(); // delete this user's records
	session.userId = null;
	client.clear();
}
```

Each session value has its own partition. When `session()` becomes `null`, nothing is read or written; `forget()` deletes the partition of the current session.

## Options

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `store` | `PersistStore` | required | Where records live: indexedDbStore(), or your own store. |
| `session` | `() => string \| null` | required | Reactive. The partition; only a hash of it is stored. null: read and write nothing. |
| `buster` | `string` | – | A build or schema version; a different value drops every record. |
| `maxAge` | `Duration` | '24h' | Older records are not restored; an endpoint’s persist.maxAge wins. |
| `maxBytes` | `number` | 8 MiB of text | Stored text per partition; the oldest records are evicted first. |
| `eager` | `QueryEndpoint[]` | – | Restored before ready resolves, at setup and on every session change. |
| `compress` | `number \| false` | 262144 | gzip values longer than this many characters. |

Persist JSON data only: a `Date` or a class instance in a value is reported in development and restores as JSON.

## Own store

A `PersistStore` has four methods: `get`, `put` (keeps the newer `updatedAt`), `delete` and `list`. `persistStoreSuite()` from [./testing](https://svelte-lean.vercel.app/query/testing.md) runs the contract's cases against your store, and `memoryStore()` is the in-memory store for tests.

## Reference

- [@svelte-lean/query/persist](https://svelte-lean.vercel.app/query/api/persist.md)
- [Server rendering](https://svelte-lean.vercel.app/query/ssr.md): the other way to start with data
