Skip to content

feat(query): default invalidations to the keys optimistic() declares#158

Merged
zzal merged 1 commit into
mainfrom
feat/default-invalidations
Jul 6, 2026
Merged

feat(query): default invalidations to the keys optimistic() declares#158
zzal merged 1 commit into
mainfrom
feat/default-invalidations

Conversation

@zzal

@zzal zzal commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Part II Wave 2 · #1 — delete the boilerplate, make the footgun inexpressible

Every optimistic CRUD mutation ended with the same required boilerplate restating keys the engine already knew:

func optimistic(_ i: Input) -> [OptimisticEdit] {
    [.update(TodoList()) { /* … */ }]
}
func invalidations(input: Input, output: Todo) -> [Invalidation] { [.exact(TodoList())] }  // ← restated, on every mutation

…and forgetting the method was the exact trap a DEBUG diagnostic papered over: the cache keeps the optimistic guess and never reconciles with the server.

Change

Mutation.invalidations now defaults to the keys optimistic(input) declares.exact per declared key, deduped in declaration order:

  • Derived from declarations, not the rollback stack (the audit's own caveat): a .noValue-skipped edit — nothing cached yet — still gets its refetch, so the server value lands either way. Deriving from rollback would silently lose exactly those (issue bug: optimistic mutation before its query loads traps in DEBUG (.noValue should be a benign skip) #96's territory). Test-pinned.
  • Dedup matters: duplicate invalidations of one key cancel-respawn its repair fetch. Test-pinned.
  • Not source-breaking: existing overrides win; an explicit [] override is the deliberate opt-out.
  • Diagnostic retired: the optimistic-without-invalidations DEBUG trap + its exit-test suite are deleted — the misuse is now inexpressible (the derived default can't be empty while optimistic edits exist). Making misuse inexpressible beats warning about it.
  • New documented contract: optimistic(_:) should be a pure declaration (the default re-reads it on success to learn the keys); run-once effects — like allocating an optimistic temp id — belong inside the edit's transform closure, which runs only when the edit applies.

Dogfood

All 4 invalidations overrides in the repo deleted (TodoCRUD ×3, QueryDemo ×1). AddTodo's temp-id allocation moved into its transform closure per the purity rule. EmbeddedTemplates.swift regenerated.

TDD

RED reproduced the footgun end-to-end before implementing — fetches → 1 (no repair refetch) and the cache stuck on the optimistic guess [100, 7]. 5 new tests: derived-default shape (dedup + order), skipped-edits-still-contribute (the caveat), override-wins, empty-optimistic → empty, and end-to-end reconciliation via the derived refetch.

Verification

  • Full host suite: 1383 tests, 277 suites green
  • TodoCRUD + QueryDemo wasm builds clean
  • Release CLI rebuilt, then TodoCRUD Playwright e2e 5/5 against the real Bun backend scaffolded from the new templates — "optimistic add reconciles to the server row" now exercises the derived invalidation in a real browser; forced-failure rollback unaffected

🤖 Generated with Claude Code

Every optimistic CRUD mutation ended with the same boilerplate invalidations
method restating keys the engine already knew from optimistic(_:) — and
FORGETTING the method was the exact footgun a DEBUG diagnostic papered over
(cache keeps the optimistic guess forever).

The default invalidations(input:output:) now derives from optimistic(input)'s
DECLARED edits: .exact per declared key, deduped in declaration order. Derived
from declarations, not the rollback stack — a .noValue-skipped edit (nothing
cached yet) still gets its refetch, so the server value lands either way
(deriving from rollback would silently lose exactly those; issue #96's
territory). Dedup matters: duplicate invalidations of one key cancel-respawn
its repair fetch. Existing overrides win — not source-breaking; an explicit []
override remains the deliberate opt-out.

The optimistic-without-invalidations diagnostic and its exit-test suite are
retired: the misuse it warned about is now inexpressible (the derived default
cannot be empty while optimistic edits exist) — making misuse inexpressible
beats warning about it.

New contract documented on the protocol: optimistic(_:) should be a PURE
declaration (the default re-reads it on success to learn the keys); effects
that must run once — like allocating an optimistic temp id — belong inside the
edit's transform closure, which runs only when the edit applies.

Dogfood: all 4 invalidations overrides in the repo deleted (TodoCRUD x3,
QueryDemo x1); AddTodo's temp-id allocation moved into its transform closure
per the purity rule; EmbeddedTemplates regenerated.

TDD: RED reproduced the footgun end-to-end (no repair refetch, cache stuck on
the guess) before the diagnostic deletion + implementation turned it GREEN.

Verified: host 1383/277, TodoCRUD + QueryDemo wasm builds clean, release CLI
rebuilt, TodoCRUD Playwright e2e 5/5 against the real Bun backend scaffolded
from the new templates (optimistic-add-reconcile now exercises the DERIVED
invalidation; rollback unaffected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zzal zzal merged commit 99d80da into main Jul 6, 2026
6 checks passed
@zzal zzal deleted the feat/default-invalidations branch July 6, 2026 23:57
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

📦 Bundle size

Artifact Baseline This PR Δ
App.wasm 5.06 MB 5.15 MB +1.90%
App.wasm (gzip) 1.80 MB 1.84 MB +2.17%
JS runtime 54.5 KB 54.5 KB +0.00%
JS runtime (gzip) 11.3 KB 11.3 KB +0.00%
Total (gzip) 1.81 MB 1.85 MB +2.16%

✅ Within budget (≤5% growth allowed).

Baseline: Swift 6.3, WASM SDK 6.3-RELEASE, measured 2026-06-18.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant