# Writing

> createMutation: run, busy, errors; the three ways to change cached values: invalidate, patch and optimistic; queues and removal.

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

createMutation sends a write and tells the screen what it is doing. Three functions change cached values: invalidate, patch and a mutation's optimistic.

## Mutations

`edit-member.svelte`

```svelte
<script lang="ts">
	import { createMutation } from '@svelte-lean/query';
	import { updateMember } from '$api/members';
	import type { Member } from '$api/types';

	let { member, onsaved }: { member: Member; onsaved: () => void } = $props();
	const save = createMutation(updateMember, { onSuccess: () => onsaved() });
</script>

<form
	onsubmit={(event) => {
		event.preventDefault();
		const form = new FormData(event.currentTarget);
		// The input is one object: path params and body fields together.
		void save.run({
			id: member.id,
			name: String(form.get('name')),
			team: String(form.get('team'))
		});
	}}
>
	<label>Name <input name="name" value={member.name} required /></label>
	<label>Team <input name="team" value={member.team} required /></label>
	<!-- busy covers queued and running; a second submit while busy returns the same promise. -->
	<button disabled={save.busy || !save.allowed()}>Save</button>
	{#if save.error}<p role="alert">{save.error.message || 'The member was not saved.'}</p>{/if}
</form>
```

| Field | Meaning |
| --- | --- |
| `run(input, options?)` | Sends the write; resolves the result, rejects a QueryError. |
| `status` | 'idle', 'queued' (behind a write with the same queue key), 'running', 'success', 'error'. |
| `busy` | queued or running: disable the button on it. |
| `error` | A QueryError; its field errors are typed by the body. |
| `data` | The last result. |
| `allowed(input?)` | The access verdict: without input for the button, with input for one row. |
| `progress` | Upload progress when the endpoint asks for it. |
| `slow` | The write has run longer than slowAfter. |
| `reset()` | Back to idle. |

- A second `run()` while one is busy returns the same promise (`exclusive`), whatever the body (JSON, files, a `FormData`); `exclusive: false` runs them side by side, `queue` one after the other.
- With `settle: 'refetched'`, the run stays `busy` and `onSuccess` waits until the lists it invalidated show the change.
- A new run clears `error`, so field marks leave while saving.
- `onSuccess` belongs to its run: a newer run or leaving the page does not drop it.
- Writes are not retried unless the definition says so.

## Changing cached values

| Call | What it does |
| --- | --- |
| `invalidate(targets)` | Refetch: observed entries ask the server again, the others go stale. |
| `patch(target, update)` | Change the cached value now, without a request. |
| `optimistic: (input) => patch(…)` | A patch shown from run() and undone if the write fails. |

## Invalidation

`src/lib/api/members.ts`

```ts
export const updateMember = defineMutation({
	patch: 'api/members/{id}',
	body: type<Partial<MemberInput>>(),
	result: type<Member>(),
	invalidates: (member) => [tag('Member', member.id), 'Members'],
	access: 'members.edit'
});
```

After success, the observed entries that carry `Member:<id>` or `Members` refetch once, even when a poll or a live event lands at the same time; the others are only marked stale. To wait for those refetches:

`settle`

```ts
// run() resolves once the invalidated lists on screen have refetched, not on the answer.
const save = createMutation(updateMember, { settle: 'refetched' });
```

### From code

`invalidate()`

```ts
// Anything the definition cannot know: an import, a websocket message, a button.
invalidate('Members');
invalidate([tag('Member', 7), 'Teams'], { reason: 'bulk-import' });
listMembers.invalidate({ team: 'Design' });
```

A server may name what changed in its answer; `laravel({ hints })` reads it, and the client refetches without a second rule in the application.

## Patch

`patch the saved row`

```ts
// The server answers with the saved row: replace it in every cached page and detail.
const save = createMutation(updateMember, {
	onSuccess: (member) => patch(tag('Member', member.id), (): Member => member)
});
```

On an entity tag, `patch` reaches every cached copy of the row: each page that holds it and its detail, without a request. Endpoints that list members declare `entity: 'Member'`. An endpoint, `[endpoint, params]` or a tag name patches whole values; `undefined` from the update changes nothing.

### Removing an entity

`after a delete`

```ts
const remove = createMutation(deleteMember, {
	// null removes the entity: unpaged arrays lose the row at once; pages and the detail refetch.
	onSuccess: (_, { id }) => patch(tag('Member', id), () => null)
});
```

## Optimistic updates

Pin a member, then refuse the next write and pin another: only that row goes back.

```svelte
<script lang="ts">
	import { createMutation, createQuery, patch, tag } from '@svelte-lean/query';
	import { listMembers, pinMember } from '$api/members';
	import type { Member } from '$api/types';

	const members = createQuery(listMembers, () => ({}));
	const pin = createMutation(pinMember, {
		// Written like a permanent patch; shown from run() until the server confirms, and a
		// failure removes only this layer. The tag reaches every cached page and the detail.
		optimistic: ({ id, pinned }) => patch(tag('Member', id), (m: Member) => ({ ...m, pinned }))
	});
</script>

<ul>
	{#each members.data?.rows ?? [] as member (member.id)}
		<li>
			{member.name}
			<button
				aria-pressed={member.pinned}
				onclick={() => pin.run({ id: member.id, pinned: !member.pinned }).catch(() => {})}
			>
				Pinned
			</button>
		</li>
	{/each}
</ul>
{#if pin.error}<p role="alert">Not saved: {pin.error.message}</p>{/if}
```

The layer is a function over the server value, not a snapshot: a refetch underneath does not lose it.

| Event | What happens |
| --- | --- |
| run() is called | The patch is applied to every entry of the target, also while queued. |
| A poll or a refetch lands meanwhile | The patch is applied over the new server value. |
| The write fails or is aborted | Its own layer is removed; nothing else is touched. |
| The write succeeds | The layer stays until the invalidated entry commits a newer answer, then it is removed. |
| patch(…, { until }) is given | After success it stays until the server value satisfies until. |

## Queues

`src/lib/api/members.ts`

```ts
export const pinMember = defineMutation({
	put: 'api/members/{id}/pin',
	body: type<{ pinned: boolean }>(),
	result: type<Member>(),
	invalidates: (member) => [tag('Member', member.id)],
	queue: (input) => `member:${input.id}` // one write per member at a time, in order
});
```

Writes with the same `queue` key run in order; the later ones read `queued` and their optimistic patches are already on screen. Rapid clicks on one switch end in the state of the last click.

## Errors

A failed write stores its error and rejects the promise; a component that renders `save.error` needs no `try`. A write the access rule refuses rejects with kind `forbidden` and sends nothing. Field errors into a form: [Forms](https://svelte-lean.vercel.app/query/forms.md).
