---
title: '5. Invalidation & optimistic updates'
description: Declare what the write touches; derive what re-runs; predict what you can prove — and property-test the prediction.
order: 5
---

# Invalidation & optimistic updates

Chapter 4's mutation committed writes, but the response carried no fresh query data — you never
said what `cart/add` touches. In this chapter you close that loop: declare the touch set, derive
which queries re-run, and make the cart badge tick instantly with an exhaustiveness-checked,
property-tested optimistic transform. Step state: `site/tutorial/steps/05-optimistic/`.

## Declare the touches, derive the invalidation

There is no `invalidate()` call in the happy path. A write's touch set — which domains it writes,
keyed how — meets each query's read set from chapter 3, and the intersection is the invalidation
graph. The tutorial's plain store has no ASTs, so you declare the touches by hand — the floor every
adapter shares:

> **The production path.** With [`@kovojs/drizzle`](/guides/data-layer/), touch sites are extracted
> from the write ASTs and committed as a reviewable graph instead of declared by hand. See the
> [data-layer guide](/guides/data-layer/).

```tsx
// SPEC.md section 11.1: with the blessed @kovojs/drizzle adapter these touch
// sites are extracted from the write ASTs and committed as a reviewable
// graph. The tutorial's plain in-memory db has no ASTs to analyze, so it
// declares the touches — the SPEC.md section 14 v1 floor — and chapter 7
// runtime-verifies the declaration against observed writes.
export const addToCartTouches = [
  {
    domain: cart.key,
    keys: null,
    site: 'site/tutorial/steps/05-optimistic/src/app.tsx:addToCart',
    via: 'cart_items',
  },
  {
    domain: product.key,
    keys: 'arg:productId',
    predicate: 'eq',
    site: 'site/tutorial/steps/05-optimistic/src/app.tsx:addToCart',
    via: 'products',
  },
] as const;
```

The mutation registers its touches and the queries it may affect. The loaders now read the
per-request database, so post-commit reruns render what the transaction just committed — the
lifecycle orders `COMMIT` before query re-runs so responses can never show pre-commit data:

```ts
export const cartQuery = app.query({
  access: app.publicAccess('tutorial single-cart storefront read'),
  load: (_input, context) => loadCart(tutorialShopDb(context.request).query.snapshot()),
  reads: [cart],
});

export const productsQuery = app.query({
  access: app.publicAccess('tutorial public product catalog'),
  load: (_input, context) => loadProducts(tutorialShopDb(context.request).query.snapshot()),
  reads: [product],
});
```

Now the enhanced response carries server truth for every invalidated query alongside the
fragments — plus `Kovo-Changes`, the sanitized write summary (domains and keys, never input
values):

```ts
it('derives the queries to re-run and ships server truth on the wire', async () => {
  const request = shopRequest();
  const response = await submitAddToCart(
    formInput(request, { productId: 'p1', quantity: '2' }),
    request,
    {
      'Kovo-Fragment': 'true',
      'Kovo-Live-Targets':
        'cart-badge#components/cart-badge/cart-badge:{}; product-list#components/product-list/product-list:{}',
      'Kovo-Targets': `cart-badge=${cartQuery.key}; product-list=${productsQuery.key}`,
    },
  );
  expect(response.status).toBe(200);
  // Server truth for every invalidated query, as readable chunks: the
  // loader replaces each value and runs its update plan (SPEC.md §9.1).
  expect(response.body).toContain(`<kovo-query name="${cartQuery.key}">{"count":2}</kovo-query>`);
  expect(response.body).toContain(`<kovo-query name="${productsQuery.key}">`);
  expect(response.body).toContain('<kovo-fragment target="cart-badge">');
  expect(response.body).toContain('<kovo-fragment target="product-list">');
  // The sanitized write summary: domains and keys, never input values.
  expect(response.headers['Kovo-Changes']).toBe(
    `[{"domain":"${cart.key}"},{"domain":"${product.key}"}]`,
  );
});
```

The loader's side is mechanical: each `<kovo-query>` replaces the shared value and runs that
query's update plan — the bindings and stamps chapter 3 derived — across every dependent island.

## Key optimism to queries, not islands

A one-round-trip wait is fine for the product list, but the cart badge should tick instantly.
You declare optimism per **(mutation × invalidated query)** — never per island — so every island
consuming the query updates from one transform, including islands written months from now:

```tsx
// SPEC.md section 10.4: optimism is keyed to queries, never islands. The
// cart count is predictable from the input alone — a pure value transform.
// The product list depends on server truth (stock math lives in the handler),
// so it explicitly accepts the 1-RTT fragment: 'await-fragment' is a recorded
// decision, not an omission. Query handles bind both decisions to their
// exact result types; kovo check proves coverage against invalidation.
queue: 'cart',
optimistic: [
  cartQuery.optimistic(addToCartInput, predictCart),
  productsQuery.optimistic('await-fragment'),
],
```

Two deliberate choices are visible here:

- **`cart` is hand-written** because the prediction is closed over the input: count goes up by
  `quantity`. The same coverage slot can also be satisfied by a compiler-derived transform when
  the write dataflow is provable from the mutation input and query shape.
- **`products` is `'await-fragment'`** — a recorded decision, not an omission. The stock math
  lives in the handler; predicting it client-side would mean duplicating server logic, so you
  accept the round-trip latency in writing.

Exhaustiveness is the point. App source binds each decision through the exact query handle:
`cartQuery.optimistic(addToCartInput, predictCart)` carries the cart result and mutation-input
types, while `productsQuery.optimistic('await-fragment')` records the deliberate wait. The
compiler derives the invalidation registry from those source identities; the app does not augment
generated registry interfaces.

Delete the `productsQuery` binding and `kovo check optimistic` reports the uncovered invalidated
query — a forgotten optimistic update is never a silently stale badge.

```ts
it('predicts the cart count with the hand-written transform', () => {
  expect(predictCart({ count: 1 }, { productId: 'p1', quantity: 2 })).toEqual({ count: 3 });
});
```

## Property-test the prediction

A wrong prediction is worse than none: the runtime reconciles server truth over it — a right
guess is a near-no-op morph, a wrong guess a visible correction. So the step proves the transform
commutes with the real handler over generated states: predicting then observing equals applying
then shaping:

```ts
// The real write effect, restated over plain state: what the handler commits.
function applyAddToCart(state: ShopPropertyState, input: AddToCartInput): ShopPropertyState {
  const found = state.products[input.productId];
  if (!found || found.stock < input.quantity) {
    throw new Error(`invalid property case for ${input.productId}`);
  }

  return {
    cartItems: [...state.cartItems, { productId: input.productId, qty: input.quantity }],
    products: {
      ...state.products,
      [input.productId]: { stock: found.stock - input.quantity },
    },
  };
}

// What the cart query ships to the client for a given state.
function shapeCartQuery(state: ShopPropertyState): { count: number } {
  return { count: state.cartItems.reduce((total, item) => total + item.qty, 0) };
}
```

```ts
it('proves prediction ⊆ eventual truth over generated states', () => {
  expect(
    propertyTest<ShopPropertyState, AddToCartInput, { count: number }>({
      apply(state, input) {
        return applyAddToCart(state, input);
      },
      cases: propertyCases(),
      predict(state, input) {
        return predictCart(shapeCartQuery(state), input);
      },
      shape(state) {
        return shapeCartQuery(state);
      },
    }),
  ).toEqual({ cases: 18 });
});
```

Eighteen generated cases, zero browsers. The [optimistic updates guide](/guides/optimistic/)
covers queues, rebase-on-arrival, and multi-tab behavior.

The loop is closed: declared touches, derived invalidation, a proven instant badge. Next, first
render learns the same trick — streaming expensive fragments without blocking the shell.

<details>
<summary>Spec & diagnostics</summary>

No `invalidate()` in the happy path; transaction lifecycle orders `COMMIT` before re-runs: SPEC
§10.3. Touch extraction committed as a reviewable graph: SPEC §11.1. Declared touches as the
adapter floor: SPEC §14. `Kovo-Changes` sanitized write summary: SPEC §9.1. Derived update plan
across islands: SPEC §4.8. Optimism keyed per (mutation × query): SPEC §10.4. Derived
transforms and explicit punts: SPEC §10.5. Exhaustiveness requirement: SPEC §10.6; a missing
optimistic binding is **KV310** in `kovo check`; query-handle typing keeps each authored predictor
aligned with its query result and exact mutation schema. Generated registries: SPEC §6.1.
Transform/handler commutation proven over generated states: SPEC §11.4.

</details>
