# Incremental Computation is Awesome

A living board, a solved puzzle, a sand avalanche, and four more reasons to keep a query running. Seven experiments with Rindle’s real in-browser WASM engine.

A query usually feels like a question you ask a database. You get an answer, use it, and ask again later.

But a query can also be something you **leave running**. Its answer stays in memory. When a row changes, the engine carries that change through the query and updates the result. The answer becomes a live part of your program.

That shift is useful for applications. It is also a lot of fun.

Everything below runs on Rindle’s Rust engine, compiled to WebAssembly, right here in your browser. `@rindle/client` supplies the typed schema and query builder. `@rindle/wasm` supplies the local store. There is no simulation server and no recorded animation.

**Scroll to start each experiment.** Panels pause when they leave view or the tab becomes hidden. Each has Play, Step, and Reset controls. If your device requests reduced motion, the experiments wait for you to press Play or Step.

## 01 · Life is three queries

A dead cell with three live neighbours comes alive. A live cell with two or three live neighbours survives. Every other cell is dead in the next generation.

Those rules describe three sets. So let us make three queries.

The board contains a row for each cell. Each living cell also contributes a `liveEdge` row aimed at each neighbour on the board, up to eight. Counting the edges aimed at a cell gives its live-neighbour count.

The starting pattern is a glider gun. It keeps producing little travelling patterns while you read. Cells outside this finite board stay dead.

<!-- demo:life -->

The queries now live inside the experiment. Select **Births**, change the count from `3` to `4`, and apply the edit. The query counts and board outlines change immediately. The board changes when the driver takes its next step.

**Pause, then Step through a generation.** The first step freezes the union of the three result sets. The next step writes the changed cells and edges. Rindle delivers its view changes during that commit. The last step paints the queued cell patches on the canvas. Click a stage to advance directly to it and pause. Clicking an earlier stage finishes the intervening work and enters the next generation; it does not rewind the board.

Hover a cell to inspect it and see the surrounding neighbours. Click a cell to flip it. Even with the generation loop paused, that write triggers the same rule and live-cell query subscriptions.

The execution stages count actual source writes, view callbacks, and canvas patches. Rendering has its own explicit query: `aliveCells` selects cells where `alive` is true. Its `onChanges` subscription delivers additions when cells become alive and removals when they die. An addition paints a cell; a removal erases it. Startup draws the empty grid and paints the initial query result. Later frames apply only the delivered changes. The query outlines and hover highlights have their own overlay canvas.

The editor accepts the local cell query chain shown in the panel, with optional ordering and a limit. Autocomplete helps with identifiers and values. Invalid text or an unsupported engine predicate leaves the last valid queries active.

The little JavaScript driver takes the union of the three rule results to get the next generation. It computes deaths in JavaScript: cells alive now but absent from that union. It writes only cells whose state changes, together with their outgoing edges. Rindle updates the three answers as those writes arrive.

There is one essential detail: the driver reads **all three results before it writes anything**. It then applies the generation in one batch. Otherwise, a cell sees part of the next generation mixed into the current one, and we get a different automaton.

This is the division of work throughout this article. The queries maintain the answers. JavaScript chooses when to act on them.

## 02 · A Sudoku solver that watches possibilities disappear

A blank square starts with a set of candidate digits. When another square receives a digit, that digit disappears from candidates in the same row, column, and box.

A square with one candidate has a forced answer. That condition is a live query too: count the candidates, then keep squares whose count equals one.

Watch the small candidate numbers. One placement removes possibilities elsewhere, which reveals another forced placement. A sequence of local deductions solves this puzzle.

<!-- demo:sudoku -->

```javascript
const forced = store.query.cell
  .where.value(0)
  .countAs("choices", candidate, {
    parent: ["id"], child: ["cellId"]
  })
  .having("choices", "=", 1)
  .orderBy("id", "asc")
  .materialize();
```

The driver takes the first forced square, places its only candidate, and removes conflicting candidate rows in one write. The maintained view reveals the next available moves.

This solver uses **naked singles**: a square with exactly one candidate. The supplied puzzle is solvable with that rule alone. A harder puzzle can stall with several candidates in every blank square. More deduction rules or a search procedure belong in the application. Rindle does not invent a guess or run a backtracking search for us.

The useful part is that “what is forced now?” remains available after every change.

## 03 · An avalanche from a count

Put 1,600 grains on one site. A site with at least four grains can topple: it gives one grain to each of its four neighbours.

That can make the neighbours topple. And their neighbours. A pile spreads into a patterned landscape through a chain of tiny local changes.

Each grain is a row here. `countAs` maintains the height at every site, and `having` keeps the sites ready to topple.

<!-- demo:sandpile -->

```javascript
const unstable = store.query.site
  .countAs("grains", grain, {
    parent: ["id"], child: ["siteId"]
  })
  .having("grains", ">=", 4)
  .materialize();
```

The driver snapshots this frontier and performs a wave of topples. A site with twelve grains can topple three times in that wave. Each topple removes four grain rows and inserts rows at its neighbours. Grains that cross the board’s outer edge leave the system.

The display shows the maintained counts. Violet means another topple is possible. Once every site has fewer than four grains, the query becomes empty and the animation stops.

Representing individual grains as rows makes this example easy to express with a count. It also has a cost: one physical quantity becomes many database rows. This is an experiment in modelling rules, not a claim that a row per grain is the fastest sandpile implementation.

## 04 · One ant, one changed square

Langton’s ant follows two rules. On a white square, turn right and flip it black. On a black square, turn left and flip it white. Then move forward.

Most of the board stays exactly as it was. Each step changes one cell and the ant’s position. That makes the small change visible: one square flips while the trail remains.

An existence query keeps the cell beneath the ant available as the ant moves.

<!-- demo:ant -->

```javascript
const underfoot = store.query.cell
  .where(exists(ant, {
    parent: ["id"], child: ["cellId"]
  }))
  .materialize();
```

The driver reads `underfoot.data[0]` to choose its turn. It edits the cell and the ant in one batch. The position change makes the query point to the next cell automatically.

This example uses a finite board with wrapped edges. The cell-edit counter measures source writes, not internal operator work or browser painting. Those are different costs, even when the input change is tiny.

## 05 · Keep the best prices on screen

Now something closer to an ordinary application. Eighty synthetic quotes change over time. The screen shows the five highest bids and five lowest asks.

A quote can change without entering either visible set. Another change can push a quote across the cutoff and evict the previous fifth row. The result includes order as well as membership.

<!-- demo:orderbook -->

```javascript
const bids = store.query.quote
  .where.side("bid")
  .orderBy("price", "desc")
  .orderBy("id", "asc")
  .limit(5)
  .materialize();

const asks = store.query.quote
  .where.side("ask")
  .orderBy("price", "asc")
  .orderBy("id", "asc")
  .limit(5)
  .materialize();
```

The second sort key makes ties deterministic. Prices are integer cents in the store and formatted for display.

The driver edits one quote per tick. It does not sort the quotes or select the first five for the UI. The two maintained views already contain those results. This is the same pattern as a live leaderboard, a priority queue, or the most relevant documents for an agent’s next step.

## 06 · A plan that knows what is unblocked

A task can run when it is incomplete and none of its dependencies points to an incomplete task.

That sentence translates directly into `notExists` around an `exists`: there is no dependency whose prerequisite is still unfinished. Completing a task can reveal several new choices at once.

<!-- demo:planner -->

```javascript
const ready = store.query.task
  .where.done(false)
  .where(notExists(dependency,
    { parent: ["id"], child: ["taskId"] },
    d => d.where(exists(task,
      { parent: ["needsId"], child: ["id"] },
      t => t.where.done(false)
    ))
  ))
  .orderBy("id", "asc")
  .materialize();
```

The demo driver completes the first ready task on each tick. The query maintains the ready set as completion rows change.

This query checks **direct prerequisites**. It does not calculate arbitrary reachability or detect dependency cycles. A cycle of unfinished tasks can leave the ready set empty. A real planner still needs an application policy for that case, missing prerequisites, failures, and retries.

Even with that boundary, this is a useful little building block: a scheduler’s next choices, expressed as data that stays current.

## 07 · Do not take the moving pictures on faith

An animation can look plausible while its state is wrong. The promise of incremental maintenance is stronger than plausibility:

**The maintained result after a write must equal a fresh query over the same rows.**

This last experiment makes that promise visible. It adds, removes, and edits rows. After every write it compares three answers: the long-lived WASM view, a newly materialized WASM query, and an independent JavaScript filter and sort.

<!-- demo:contract -->

```javascript
const query = store.query.item
  .where.active(true)
  .orderBy("score", "desc")
  .orderBy("id", "asc")
  .limit(8);

const maintained = query.materialize();
// After each write, compare maintained.data with:
// 1. query.materialize().data (destroy the temporary view afterward)
// 2. a JavaScript filter + sort + slice over the source rows
```

“Verified states” counts actual comparisons in your tab, including the initial state. A mismatch throws an error and stops the demo. The independent calculation matters: two paths through the same engine can share a bug.

This deliberately repeats work to check correctness. It is not a latency benchmark or a proof for every supported query shape. It is a small, live test of a precise claim.

## Keep an answer alive

A living board. A shrinking set of possibilities. A toppling frontier. A moving point of interest. A ranked window. A set of unblocked tasks.

Each starts as a question about rows. Each becomes something the program can keep using while those rows change.

Incremental computation still does work. Initial materialization reads data. A change with many dependents can cause substantial work. Dense changes, view maintenance, JavaScript, and rendering all contribute to the cost. These demos show the programming model, not a universal speedup over specialized algorithms.

But the programming model is the part I find awesome. Define an answer once, keep changing the facts, and the answer keeps up.

To build your own, start with [a local WASM store](https://rindle.sh/docs/wasm-client), explore the [TypeScript query shapes](https://rindle.sh/docs/supported-queries-ts), and connect a view to your UI. Keep the view while you need its answer. Call `view.destroy()` when you are done.

---

[View this page on Rindle](https://rindle.sh/blog/incremental-computation-is-awesome)
