sveltelean Query
Version v0.2.0

Example

Click "Pinned" several times in a row. Then press "Refuse the next write" and click once more.

Pinned members

The example runs in the browser.

Requests the server received

  1. None yet.
Code
<!-- src/lib/members/PinnedList.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, () => ({}), { poll: () => '10s' });

	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 })),
		// Every click runs; pinMember's `queue` sends the writes of one member in order.
		exclusive: false
	});
</script>

{#if pin.error}<p role="alert">{pin.error.message}</p>{/if}
<ul>
	{#each members.data?.rows ?? [] as member (member.id)}
		<li>
			{member.name}
			<button
				type="button"
				aria-pressed={member.pinned}
				onclick={() => pin.run({ id: member.id, pinned: !member.pinned }).catch(() => {})}
			>
				Pinned
			</button>
		</li>
	{/each}
</ul>
The component shown, against an in-memory server that answers after a fixed delay; the list polls meanwhile.
src/lib/api/members.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
});

How it works

MomentWhat happens
Clickrun() calls optimistic, whose patch() is a layer: data is the server value with the row changed, in every cached page and the detail
Clicks while the first write is outeach one is queued behind the previous (queue per member) and shown at once
A poll answers meanwhilethe layer is applied over the new server value: no flicker, no lost click
Successthe layer stays until the refetch the write triggered has answered, then goes
Failureonly this run's layer is removed; later clicks keep theirs; pin.error is set
  • No snapshot is taken and nothing is restored: a layer is a function over server data, so a rollback cannot overwrite newer data.
  • exclusive: false lets every click run; the default returns the running promise to a second click.
  • aria-pressed on a native button is the toggle state for assistive technology.

Variations

Hold until the server agrees

TypeScript
// Keep the change on screen until the server value agrees (a slow, eventually consistent API).
const pin = createMutation(pinMember, {
	optimistic: ({ id, pinned }) =>
		patch([getMember, { id }], (m) => ({ ...m, pinned }), { until: (m) => m.pinned === pinned })
});

Leave the value as it is

TypeScript
// One patch for every cached copy of the row: list pages and the detail. `undefined` from
// the update changes nothing.
const rename = createMutation(updateMember, {
	optimistic: ({ id, name }) =>
		patch(tag('Member', id), (m: Member) => (name ? { ...m, name } : undefined))
});