# Reactive queries in the browser

The dream API — a typed schema, a fluent query builder, and a live materialized view, all running on the IVM engine compiled to wasm, in-process in the browser.

`@rindle/wasm` is the IVM engine compiled to WebAssembly and wrapped in a typed
TypeScript client. You define a schema, build a query with a fluent builder, and
`materialize()` it into a **live view** — a reference-stable array that stays
equal to a fresh query as you write. The engine runs **in-process**, in the
tab: no server round-trip, no polling, ~200 kB gzipped.

This is the engine on its own, with no server — the **primitive underneath** the
[synced browser client](/docs/client). The schema, the builder, and the view you
learn here are **identical** once you add a server: the synced client runs this very
engine, fed by a daemon. See [backends & the homes](/docs/backends) for the seam
that makes one view serve every tier.

## Install

`@rindle/wasm` re-exports everything in `@rindle/client`, so a local app imports it all
from one package. (Working from a checkout of the repo instead? Depend on it through
the pnpm workspace — same imports.)

```bash
pnpm add @rindle/wasm
```

## Define a schema

A schema is a set of typed tables. Each column has a type (`string`, `number`,
`boolean`, `json<T>()`); the type drives the comparator and JSON parsing. Declare
a primary key per table.

> **Standalone vs. generated.** Because this engine runs with no database, you author the
> schema here in TypeScript — the rows come from `tx.add`, not a file. The moment a
> [daemon](/docs/daemon) backs it, **SQL becomes the source of truth** and this same schema is
> *generated* from your DDL with `rindle schema gen` — same `table(…)` shape, now derived from
> the database. See [schema & migrations](/docs/schema).

```ts
import { table, string, number, boolean, createSchema } from "@rindle/wasm";

const issue = table("issue")
  .columns({ id: number(), title: string(), priority: number(), closed: boolean() })
  .primaryKey("id");

const comment = table("comment")
  .columns({ id: number(), issueID: number(), body: string() })
  .primaryKey("id");

const schema = createSchema({ tables: [issue, comment] });
```

## Open a store

`createWasmStore` initializes the wasm module (once) and returns a `Store` wired
to the in-process backend. It is `async` because the first call boots the engine.

```ts
import { createWasmStore } from "@rindle/wasm";

const store = await createWasmStore(schema);
```

## Build a query, materialize a live view

`store.query.<table>` is a typed builder. Chain `where` / `orderBy` / `limit` and
finish with `materialize()` — you get back an `ArrayView` whose `.data` is the
current result and that you `subscribe` to for updates.

```ts
import { eq, gt } from "@rindle/wasm";

const view = store.query.issue
  .where.closed(false)          // field proxy: `closed = false`
  .where.priority(gt(3))        // operators: eq / ne / gt / ge / lt / le / like / ilike / inList …
  .orderBy("priority", "desc")
  .limit(20)
  .materialize();

const unsubscribe = view.subscribe((rows) => {
  // rows: { id: number; title: string; priority: number; closed: boolean }[]
  render(rows);
});
```

`subscribe` fires **immediately** with the current data, then again after every
write that affects the query — each time with the new materialized array. The
contract is **view-after-write == fresh-query**: fold nothing yourself; the view
is always exactly what a from-scratch query would return.

`where` is both callable and a field proxy, and there is camelCase sugar:

```ts
store.query.issue.where(or(issue.priority(gt(8)), issue.closed(true)));  // condition form
store.query.issue.where.closed(false);                                  // field proxy
store.query.issue.whereClosed(false);                                   // sugar — same thing
```

## Write

`store.write` takes a function that receives a transaction. Rows are plain
objects keyed by column; the store positionalizes them (and stringifies `json`
columns) before the engine sees them.

```ts
await store.write((tx) => {
  tx.add("issue", { id: 1, title: "first", priority: 5, closed: false });
  tx.edit(
    "issue",
    { id: 1, title: "first", priority: 5, closed: false }, // old
    { id: 1, title: "first", priority: 9, closed: false }, // new
  );
  tx.remove("issue", { id: 1, title: "first", priority: 9, closed: false });
});
```

In the wasm backend the write is applied synchronously and the affected views
update before `write` resolves. (In the [synced client](/docs/client) the same call
goes through your API server and the resulting deltas stream back — same API, async
timing.)

## Nesting: `sub`

Nest a child relationship by **explicit correlation** — there are no
schema-declared relationships; you state the join inline. The result type grows
the alias as a typed array.

```ts
const view = store.query.issue
  .where.closed(false)
  .sub("comments", comment, { parent: ["id"], child: ["issueID"] }, (c) =>
    c.orderBy("id", "asc"),
  )
  .materialize();

view.subscribe((rows) => {
  // rows: ( …Issue & { comments: Comment[] } )[]
});
```

## A single row: `one()`

A top-level `.one()` flips `materialize()` to a `SingularArrayView` whose `.data`
is the single row or `null` (the engine caps the query to `limit 1`). The unwrap
is type-level too.

```ts
const view = store.query.issue.where.id(eq(42)).one().materialize();
view.subscribe((row) => render(row)); // row: Issue | null, not Issue[]
```

## Paging: `start`

`start(cursor, { exclusive })` pages from a partial cursor row over the sort
columns (it lowers to a `Skip` in the engine).

```ts
const page2 = store.query.issue
  .orderBy("id", "asc")
  .start({ id: lastSeenId }, { exclusive: true })
  .limit(20)
  .materialize();
```

## Projection, aggregates, scalar subqueries

Three more builder methods — each covered in depth on
[supported queries](/docs/supported-queries-ts):

```ts
import { exists } from "@rindle/wasm";

// projection — sync only the columns you select
store.query.issue.select("id", "title");

// aggregate — a live count of a correlated child, as a scalar `number`
store.query.issue.countAs("commentCount", comment, { parent: ["id"], child: ["issueID"] });

// scalar subquery — fold a unique lookup to a literal at build time (a snapshot)
store.query.issue.where(
  exists(comment, { parent: ["id"], child: ["issueID"] }, (c) => c.where.id(42), { scalar: true }),
);
```

`select` drives **what syncs** — over the wire and into the engine — and hides a row
that lacks a selected column; `countAs` adds the alias to each row as a `number`; a
`scalar` exists is resolved **once at build time** and does not react to later child
changes.

## Reads are reference-stable

`view.data` is structurally shared: an update re-projects only the subtrees that
changed, so untouched rows keep their object identity. Frameworks that memoize on
identity (React, Solid) re-render only what moved. `json` columns are parsed to
objects once, on read.

## Initialization details

`createWasmStore` calls `initWasm()` for you. If you construct a backend by hand,
`await initWasm()` first; pass `initWasm(moduleOrPath)` to supply the wasm
yourself (a `WebAssembly.Module`, URL, or bytes — useful for custom asset
pipelines).

```ts
import { initWasm, WasmBackend, Store } from "@rindle/wasm";

await initWasm();                       // browser: fetches the wasm; Node: reads it from the package
const store = new Store(schema, new WasmBackend(schema));
```

> The front-page playground is exactly this code path — the real `@rindle/wasm`
> engine maintaining a real top-N in your browser tab.

## Next steps

- [The browser client](/docs/client) — add a server: the same engine, now synced
  with optimistic writes and instant local reads.
- [The three-tier architecture](/docs/architecture) — how this engine becomes a
  full synced app.
- [Backends & the homes](/docs/backends) — the `Backend` seam: the same store and
  view over wasm, native SQLite, or a server.
- [The change model](/docs/change-model) — the delta vocabulary the view folds.
- [Supported queries](/docs/supported-queries-ts) — what the builder can lower today.

---

[View this page on Rindle](https://rindle.sh/docs/wasm-client)
