Optimistic mutation (fold-based rollback, idempotency keys)
Generated from
pattern-library/registry.json— do not edit by hand; change the registry and runbun run patterns:build.
Category: Interaction Velocity · Tier: deep · Maturity: proven (verified 2026-09-09 @ 921e8266-dirty) · Risk: low — client-side state; the server contract is unchanged
The confirmed value and the pending intents are stored separately and the UI reads the fold of one over the other, so a failed mutation rolls back exactly — even with later mutations applied on top — and a repeated intent executes once.
When to use: Use for reversible, predictable actions where failure is uncommon: rename, toggle, reorder, move, favourite. Never for payments, irreversible deletion, or permission changes.
Docs
- docs/blueprint/velocity/interaction.md#optimistic-mutation — The pattern, and what must never go through it (GitHub · GitLab)
Code
src/lib/state/optimistic.svelte.ts— OptimisticValue: base + pending intents, folded on read (GitHub · GitLab)
Tests
Proof
Invariants
- Optimistic UI never silently diverges permanently from canonical state — the confirmed value is stored separately and is only ever replaced by the server's answer.
- Rollback is the removal of one intent from the pending list, never an undo applied in place; later intents stay applied because they are re-folded over the unchanged base.
applyis re-run on every read and must be pure.- A retried or double-submitted mutation with the same key executes once — the second caller joins the first.
- Security and authorization are never optimistic. The server stays the authority.
Emulation notes
- Derive the mutation key from what the intent MEANS (
rename:file-7), never from when it was issued — a timestamp makes every double-click a distinct intent, which is the thing the key prevents. - Pass the same key as the request's idempotency header and the duplicate protection extends across the network.
mutateresolves with an outcome object instead of throwing: a fire-and-forget caller that ignores the promise must not produce an unhandled rejection.$effectnever runs under vitest's node environment — unit-test the state machine and prove the reactive wiring in a browser.
Machine-readable record: optimistic-mutation in pattern-library/registry.json.