# @aweftjs/core

Observables, the deltas they produce, commits, scopes and identity.

State lives in observables of three kinds: `createObject({ ... })` (string slots),
`createArray([ ... ])` (ordered, addressed by positions that survive edits elsewhere) and
`createMap([[key, value], ...])` (keyed by id), each taking its initial contents. Assignment
is the mutation, and an array's `push`, `splice` and index assignment are edits like any
other. Every property of an observable belongs to you; everything the library does is a
free function that takes the observable, so no field name is reserved.

One assignment is one commit. An `atomic` block is one commit no matter how much it writes,
and a block that throws rolls back and emits nothing. A commit applies whole or not at all,
and a watcher never sees a document between deltas.

## Quickstart

```ts
import {
	apply, atomic, createObject, idOf, observer, snapshot, type Commit,
} from '@aweftjs/core';

const doc = createObject({ title: 'plan', width: 1, height: 1 });

// Watch a scope. The watcher gets the commit; the tree is already updated when it runs.
const stop = observer(doc).path('title').watch((change) => {
	console.log(change.deltas.length, doc.title);
});

doc.title = 'plan b';   // one commit
atomic(() => {          // also one commit, and the title watcher never fires for it
	doc.width = 3;
	doc.height = 4;
});

// Undo: a watcher that asks for it gets the commit that undoes each change.
const undos: Commit[] = [];
const stopRecording = observer(doc).watch(
	(change) => undos.push(change.inverse()), { inverse: true });
doc.title = 'oops';
stopRecording();          // stop before undoing, or the undo records itself
apply(doc, undos.pop()!); // title is 'plan b' again

stop();
```

Registering a watcher returns the function that stops it, always.

A watcher cannot tell a commit landed with `apply` from a local mutation. An undo stack that stays
subscribed while it undoes will record its own undo; hold a flag for the duration of the call, the
way [`recipes/core`](/docs/recipes/core) does.

That flag works while `apply` is called from ordinary code. It does not work when `apply`
is called from **inside** a watcher, which is the shape a replication seam reaches for
first. Delivery is deferred, so the nested commit reaches the second document's watchers
after the outer watcher has already returned and cleared the flag. Queue the commit and
apply it once the delivery has finished:

```ts
const queued: Commit[] = [];
observer(source).watch((change) => queued.push({ deltas: [...change.deltas] }));

// later, outside any delivery
while (queued.length > 0) apply(mirror, queued.shift()!);
```

A real transport queues here anyway, because writing to a socket is not synchronous.

## A replica

Two documents stay in step when they share a root id and every commit crosses:

```ts
const source = createObject();
const mirror = createObject(undefined, idOf(source));

observer(source).watch((change) => apply(mirror, change));

source.title = 'shared';
// snapshot(mirror) now deep-equals snapshot(source), after every commit
```

A plain `createObject()` on the receiving side does not work: it has a different root id,
so the source's commits are refused as unreachable. Mint the copy with the source root's
id, as above.

A replica built from commits holds only what commits described, and one thing is never
described: the slots the **watched** observable was constructed with. Nothing can watch an
observable before it exists, so a watcher wired afterwards never hears about them. Start the
source empty and assign its slots after the watcher is wired, as above, or hand the receiving
side a starting point with `fromSnapshot(snapshot(source))` and replicate from there.

Everything below that is fine. Attaching an observable into a watched document emits the
slots it was constructed with, in the same commit as the attach, so a subtree built and
attached in one breath replicates whole:

```ts
atomic(() => { doc.settings = createObject({ theme: 'dark', limit: 5 }); });
// three deltas: the attach, and one for each slot the child was constructed with
```

Compare the two by deep equality, not by `JSON.stringify`. A snapshot's slots are a plain
object, so the order they were inserted in is part of the string and is not part of the
document: applying the same commits in two orders gives two strings for one document.
`canonicalJson` in the sibling testing package is the comparison that holds.

## Refusing a change before it lands

`intercept(doc, fn)` puts a rule on a document. It is called with the commit about to close,
after every delta has been applied and before any watcher is told, and it answers with the
reasons to refuse. Empty means the commit closes.

```ts
const stop = intercept(doc, (commit) =>
	doc.title === '' ? [{ code: 'invalid', message: 'a title needs a name', path: ['title'] }] : []);

doc.title = '';        // throws RefusedError; doc.title is what it was
apply(doc, arriving);  // refused the same way, and nothing was delivered
stop();
```

A refusal rolls the whole commit back and tells nobody, exactly as a throwing `atomic` block
does, then throws a `RefusedError` carrying the refusals at whoever made the commit: the
assignment, the block, or the `apply`. Catching it is a complete recovery, because there is
no half-applied state to repair.

One seam covers every way a commit is made, so an assignment, a block and an arriving commit
are all read by the same rule, and a block is read once with everything it wrote. A rule is
about a whole document, not the subtree under the observable it was registered on.

Inside a rule the document reads as the commit would leave it, which is what a rule across
two slots needs. Writing to it from in there throws `sealed`: a change made from inside the
answer would be a change to the commit being answered for. Several rules on one document all
run, refusals and all, so one commit reports every problem it has rather than one per retry.

`@aweftjs/schema` is this with the rule written for you from a description of the document.

## Scope where you read, not at the root

A scope registers its listener on the observable it was built from, and delivery walks each
delta up its attach path checking every listener it passes. So the cost of a write is the
number of listeners standing between it and the top.

```ts
observer(doc).path('tasks', 3, 'done').watch(fn);  // checked on every write anywhere
observer(task).path('done').watch(fn);             // checked only on writes under task
```

Both see the same changes. The first is checked on every write in the document, the second only on
writes under `task`. Measured with
[`bench/write.ts`](https://github.com/torrinworx/aweft/blob/9a5bb24770dc7555257f8a307d79917efc58df70/bench/write.ts), on one write
nobody matches: 1,000 listeners on the root cost 4.56 us and 10,000 cost 51.59 us, while the same
listeners registered on the observable they are about stay flat at 0.42 to 0.45 us. Start the
scope at the thing you are reading and the question does not arise.

A number in a path names a position, not an element. `path('tasks', 0)` follows whatever sits
at index 0 now, so removing the first task makes it the second task's scope. To follow one
element wherever it moves, start the scope at the element.

## Derive values from what you read

`map` turns a scope into a derived value, and derived values compose:

```ts
const caps = observer(doc).path('title').map((v) => String(v).toUpperCase());
const area = all([observer(doc).path('width'), observer(doc).path('height')])
	.map(([w, h]) => Number(w) * Number(h));

caps.get();                       // the current value
const stop = area.watch(render);  // the new value, after each change
area.effect(render);              // the value now, and after each change
```

`watch` means two things, and the types keep them apart: on a scope it delivers commits,
because a scope is about a place in a document; on a derived value it delivers the value,
because there is no commit. Derived delivery runs after the whole commit has been
delivered, so a value combining two branches never computes against half a commit, and an
`atomic` block is one recompute however much it writes.

A derived value is memoized while something watches it and recomputed on read while
nothing does, so an abandoned chain holds no subscription. A change that settles to an
equal value (`Object.is`) is not delivered: a container mutated in place reads as
unchanged, so derive the field you mean, not the container holding it. The transform must
be pure per input; anything else it reads is not tracked.

`bool(a, b)`, `def(fallback)`, `defined()` and `unwrap()` are shorthand over `map`.
Writing goes through a declared path only: `map` is read-only, `setter(fn)` declares the
write half, and `isImmutable()` answers before an input renders. `selector` is per-key
selection that scales: a change reaches the two keys it moved between and no others.

```ts
const select = observer(app).path('selectedId').selector();
select(id).effect((on) => row.classList.toggle('active', on)); // per row
select(id).set(true);                                          // select this row
select(id).set(false);                                         // clear, only if selected
```

`set(true)` writes the key to the source. `set(false)` clears the source only when this key
is the selected one, so deselecting a row that already lost the selection changes nothing.

## Interface state lives in cells

A cell is a reactive value outside the document: no delta, no replication, no place in the
undo history. Which tab is open is a cell; the document is the document.

```ts
const open = mutable(false);
open.set(true);
const label = open.bool('hide', 'show');

timer(1000).map(() => new Date().toLocaleTimeString()).effect(show);
fromEvent(window, 'resize').wait(100).effect(relayout);
```

`mutableArray(items)` is a list cell: an array edited in place (`push`, `splice`, index
assignment) whose `watch` delivers each edit as a list of changes, with no delta and no place
in a document. A list on the page that is not part of the document, such as open toasts.

`derive(fn)` is a value of the whole list, recomputed on every edit, for the questions a page
asks about a list rather than about one row:

```ts
const rows = mutableArray<Session>();
const empty = rows.derive((items) => items.length === 0);
const total = rows.derive((items) => items.reduce((sum, r) => sum + r.bytes, 0));
```

It is an ordinary derived value: watchers hear only the answers that changed, so a `fn` that
builds a fresh array or object is delivered on every edit, and reading it while nothing watches
computes it there and then. Unlike `watch`, it settles inside an `atomic` block where each edit
is made, the same as a plain cell does.

Inside `atomic`, the calls in the block deliver once at its close, as one list in the order
they were made, so a swap written as two index assignments arrives as one change list and a
binding over the list moves both rows:

```ts
atomic(() => { const t = rows[1]; rows[1] = rows[998]; rows[998] = t; });
```

A block that throws still delivers them, because nothing rolls the list back. A watcher hears
exactly the changes made after it subscribed and before it unsubscribed, so one that subscribes
part way through a block is told the rest of it and one that unsubscribes inside a block is
told nothing. A plain cell is different: `mutable(x).set(v)` notifies inside the block, since
holding it would make a derived value that is being watched read the value the block just
overwrote.

Writing a cell into a document slot is refused (`cell-in-document`), so whether state
replicates stays answerable from the type being written. `immutable(x)` wraps anything as
a read-only view or a constant.

`throttle(ms)` and `wait(ms)` exist only on this value surface. A commit stream cannot be
rate limited through this API, because a receiver that misses one commit of a burst holds
a different document forever after. Reads are never delayed, only delivery.

## Watch a shape, not only a place

A scope step can be a wildcard: `skip(count)` matches any run of keys, `tree(key)` matches
the named key at any depth. They are ordinary steps, so `path`, `ignore` and `shallow`
compose with them unchanged.

```ts
observer(board).skip().path('done').watch(fn);  // every column's done flag
observer(doc).tree('draft').watch(fn);          // any draft, anywhere
```

A wildcard scope names many places, so it has no single value: `get()` is undefined,
`set()` throws, and `isImmutable()` is true. Only scopes that use wildcards pay for the
backtracking matcher.

`skip()` matches exactly one step unless you say otherwise, and a scope at a depth nothing
sits at is silent: it never matches, and a derived value on it sits at its initial value
forever, which reads as a counter that works and is always zero. Count the steps from the
observable the scope starts at, or use `tree`, which does not care how deep the thing is.

`skip(Infinity)` is any run of steps. Ending the scope, it reaches every slot at every depth,
so `observer(doc).skip(Infinity).watch(fn)` hears every delta in the document, each on its
own. Every step of the run has to be open: a wildcard never consumes an object slot whose key
starts with an underscore, so a `_draft` slot and everything an object under it holds are
private from this scope at any depth, and never a slot named in `ignore`, which drops that
slot and everything under it. That is the scope for a recorder or a mirror: what the document
holds that its author did not mark private, and nothing the recorder has to know about.
Followed by a key, `skip(Infinity).path('done')` reaches `done` at any depth, which is what
`tree('done')` spells.

```ts
observer(doc).skip(Infinity).watch((change) => record(change.deltas));   // every public delta
observer(doc).skip(Infinity).ignore('scratch').watch(fn);               // except under scratch
```

## A snapshot rebuilds

`fromSnapshot(snapshot(doc))` is a live copy: same ids, kinds, slots, positions and
aliases, and it accepts commits addressed to the original's ids from then on. It holds
what the document says, not the detached observables the original still indexes, so
replaying a history that resurrects one is the commit log's job, not a snapshot's.

## Removing something takes it out of the document

Taking an observable out of the document leaves it readable and no longer writable. A write
to it throws `unreachable`, which is what a receiver does with the same delta.

```ts
const task = tasks[0];
tasks.splice(0, 1);
isReachable(task);  // false
task.done = true;   // throws unreachable
byId(board, id);    // undefined: the document no longer holds it
```

The commit that detached it takes it and everything under it out of the document as it
closes, so nothing that has been removed keeps the document alive. Hold the observable
yourself if you still want it; attaching it somewhere again is a commit that carries
everything it holds, so a replica gets it back in full (design 084).

Ask `isReachable` rather than catching the throw. `parentOf` cannot answer it: it returns
undefined for a document root, which is reachable, and for something detached, which is not.

## Finding a thing by its id

A delta names the observable it changes by id. `byId(doc, id)` answers with the observable,
and `pathOf(observable)` answers with the slot names from the root down, spelled the way the
format spells a slot. Both cost the depth, never the document, so a check that runs on every
commit can afford them.

```ts
const task = byId(board, delta.id);
pathOf(task);        // ['tasks', '80a1c2e3']
pathOf(board);       // []
pathOf(orphan);      // undefined: nothing attaches it, and byId no longer finds it
```

## Boundaries

The `Change` a watcher receives is a `Commit`: its `deltas` are exactly what crosses a
boundary, and `apply` on the far side takes them unchanged. The sibling codec package
turns a commit into bytes and back.

Deliberately not here: any transport, any persistence, any DOM. `sort`, `reverse`, `fill`
and `copyWithin` on an array throw, because they cannot be expressed as changes to the
slots they appear to touch.

The wire format lives in [`spec/`](/spec), the
reasoning in [`docs/design/`](https://github.com/torrinworx/aweft/tree/9a5bb24770dc7555257f8a307d79917efc58df70/docs/design), and a
complete program using all of the above in
[`recipes/core/`](/docs/recipes/core).

## Known limits

**Nothing records what a function read while it ran.** A transform is pure per input and what
it reads besides its input is not tracked, and no scope here records reads and re-runs on a
change to one of them. So anything reactive has to name what it follows, with `all([...])` or
with the scope you mean, and a callback that reaches for a cell nobody handed it goes quiet
when that cell changes. Settling it means a read-recording scope in this package, priced
against what recording adds to every read.

**`snapshot` can produce a document `fromSnapshot` refuses.** An `alias` may name an
observable that a later edit takes out of the document: `snapshot` leaves the observable out,
because nothing attaches it, and still writes the alias, so rebuilding that snapshot throws
`unreachable: <id> is named but not in the snapshot`. You meet it when you alias an
observable, detach it, and then save the document and build it again. `@aweftjs/store` drops
the dangling slot as it opens a document (design 050); which answer holds in general, dropping
the alias, carrying what is named as well as what is held, or refusing the detach, is open.

## The design notes

A `design NNN` above is the note of that number in
[`docs/design/`](https://github.com/torrinworx/aweft/tree/9a5bb24770dc7555257f8a307d79917efc58df70/docs/design), which says what was
decided, why, what it costs, and what would reverse it.

## API

Every export of `@aweftjs/core`, its signature as the compiler resolves it, and its block comment.

### `@aweftjs/core`

#### `ArrayChange`

```ts
type ArrayChange<T> = { readonly type: 'add'; readonly at: number; readonly value: T; } | { readonly type: 'replace'; readonly at: number; readonly value: T; } | { readonly type: 'remove'; readonly at: number; };
```

One step of a change, applied in order to the list as it was: `add` inserts before index
`at` (or at the end when `at` is the length), `replace` overwrites index `at`, `remove`
takes index `at` out. The same three words as a delta. A `splice` says its removes before
its adds, so a value it moves is never named at two places at once; `replace` comes only
from an index assignment.

#### `Change`

```ts
interface Change extends Commit { readonly deltas: readonly Delta[]; inverse(): Commit; }
```

What a watcher is handed: the deltas of one commit that fell inside its scope, and the
commit that undoes them.

`deltas` is exactly what crosses a boundary, so a watcher on the document root can hand it
to an encoder unchanged. `inverse` is built from the values the slots held before, captured
while the change was applied and, for a subtree the commit took out of the document, as the
commit closed. It is never transmitted (design 016).

A commit captures those values only for a watcher that asked, so `inverse()` refuses with
`inverse-not-asked` on a change delivered to a watcher registered without the option
(design 156).

#### `Commit`

```ts
interface Commit { readonly deltas: readonly Delta[]; readonly tag?: Tag; } (from @aweftjs/codec)
```

The unit that crosses every boundary.

A commit applies whole or not at all, and it carries at least one delta. Its deltas are a
set, written in the canonical order of section 6.9, and a receiver replaying them one at a
time would pass through states the sender never had.

`tag` is 4 to 32 bytes and is not a checksum of these bytes. It is a digest over the prior
values of the slots this commit addresses, computed by the sender against its own state
before the commit, and it answers whether the commit landed on the state the sender
expected. A receiver computing a different tag treats the replicas as diverged and
resynchronizes. The algorithm that fills it is still open, so nothing here computes one.

#### `Delta`

```ts
interface Delta { readonly type: DeltaType; readonly id: Id; readonly ref: Ref; readonly value?: Value; } (from @aweftjs/codec)
```

One change to one slot.

The target is `id` plus `ref` and never a path, so a delta means the same thing whatever
else moved in the same commit. `value` is absent exactly when the type is `remove`:
absent as in the key is omitted, not present holding undefined, and a decoded delta
reads the same way.

#### `Derived`

```ts
interface Derived<T> { get(): T; set(value: T): void; isImmutable(): boolean; watch(fn: (value: T) => void): () => void; effect(fn: (value: T) => void): () => void; map<U>(fn: (value: T) => U): Derived<U>; setter(fn: (value: T) => void): Derived<T>; unwrap(): Derived<unknown>; bool<U, V>(truthy: U, falsy: V): Derived<U | V>; def<U>(fallback: U): Derived<NonNullable<T> | U>; defined(): Derived<boolean>; selector(compare?: (value: T, key: unknown) => boolean): (key: unknown) => Derived<boolean>; throttle(ms: number): Derived<T>; wait(ms: number): Derived<T>; }
```

The value surface. `get` reads, `watch` delivers the value after it changes, and the
combinators return new chains without touching this one. `set` works only where a write
path was declared; ask `isImmutable` first.

#### `EdgeKind`

```ts
type EdgeKind = 'attach' | 'alias'; (from @aweftjs/codec)
```

What a reference to an observable means.

`attach` is where the observable lives, and it has exactly one. `alias` names it from
somewhere else and moves nothing.

#### `EventEmitting`

```ts
interface EventEmitting<E> { addEventListener(type: string, listener: (event: E) => void): void; removeEventListener(type: string, listener: (event: E) => void): void; }
```

Anything events can be heard from. Structural on purpose: core knows no DOM.

#### `Interceptor`

```ts
type Interceptor = (commit: Commit) => readonly Refusal[];
```

A rule that reads a closing commit and answers with the reasons to refuse it.

An empty answer lets the commit close. Anything else rolls it back. The rule runs inside
the transaction, so it may read the document and may not write to it.

#### `MutableArray`

```ts
interface MutableArray<T> extends Array<T> { watch(fn: (changes: readonly ArrayChange<T>[]) => void): () => void; derive<U>(fn: (items: readonly T[]) => U): Derived<U>; }
```

An array whose every edit can be heard. Reads and mutators are the array's own.

#### `ObservableKind`

```ts
type ObservableKind = 'object' | 'array' | 'map'; (from @aweftjs/codec)
```

Which of the three kinds an observable is. It is fixed when the observable is made.

#### `ObservableMap`

```ts
interface ObservableMap<T> { get(key: unknown): T | undefined; has(key: unknown): boolean; set(key: unknown, value: T): void; add(observable: T & object): void; delete(key: unknown): boolean; readonly size: number; keys(): string[]; values(): T[]; entries(): Generator<[ string, T ]>; [Symbol.iterator](): Generator<[ string, T ]>; }
```

What a map observable answers to. Slots are named by id, so methods cannot collide.

#### `Observer`

```ts
interface Observer extends Omit<Derived<unknown>, 'get' | 'set' | 'watch' | 'effect'> { get(): unknown; set(value: unknown): void; path(...keys: ScopeKey[]): Observer; ignore(...keys: ScopeKey[]): Observer; shallow(): Observer; skip(count?: number): Observer; tree(key: ScopeKey): Observer; watch(fn: (change: Change) => void, options?: { readonly inverse?: boolean; }): () => void; effect(fn: (value: unknown) => void): () => void; }
```

A scope: what part of the document a listener is about. Narrow it before watching.

A scope also carries the value combinators of design 023, inherited below: `map` is the
bridge from this surface to derived values, and `bool`, `def`, `selector` and the rest
ride on it.

#### `Primitive`

```ts
type Primitive = null | boolean | number | string | Uint8Array;
```

Everything a slot can hold that is not another observable.

#### `Refusal`

```ts
interface Refusal { readonly code: string; readonly message: string; readonly path?: readonly string[]; }
```

One reason a commit was refused.

`code` is the stable token to branch on and `message` is for a person. `path` names the
slot the reason is about, from the document root down, spelled the way the document spells
its own keys: an object key as itself, a map id in text form, an array position in hex.
Core never reads any of the three; it carries them so that a rule, a link and an
application all say refusal the same way.

#### `RefusedError`

```ts
RefusedError: typeof RefusedError
```

What is thrown at whoever made a commit a rule refused.

The commit is already rolled back when this arrives, and no watcher was told anything, so
catching it is a complete recovery: there is no half-applied state to repair.

**Example**

```ts
try { doc.title = ''; } catch (error) {
  if (error instanceof RefusedError) show(error.refusals[0]!.message);
}
```

#### `ScopeKey`

```ts
type ScopeKey = string | number;
```

A step in a path: an object or map slot by name, or an array position by index.

#### `Snapshot`

```ts
interface Snapshot { readonly root: string; readonly observables: Record<string, SnapshotObservable>; }
```

A whole document as plain data, flat, keyed by id in text form, with the root named.

#### `SnapshotObservable`

```ts
interface SnapshotObservable { readonly kind: ObservableKind; readonly slots: Record<string, SnapshotValue>; }
```

One observable as plain data: its kind, and its slots by key.

#### `SnapshotRef`

```ts
interface SnapshotRef { readonly ref: string; readonly kind: ObservableKind; readonly edge: EdgeKind; }
```

A slot naming another observable: which one, what kind, and which edge names it.

#### `SnapshotValue`

```ts
type SnapshotValue = Primitive | SnapshotRef;
```

What a slot holds in a snapshot: a primitive, or the name of another observable.

#### `alias`

```ts
alias: (observable: object) => object
```

Name an observable without giving it a home.

**Params**

- `observable`: the observable to name

**Returns** a marker to assign. The slot then holds an alias edge, which grants nothing and revokes nothing. Every observable lives at exactly one attach edge, and an alias is not it.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
post.author = alias(users.get(id));
```

#### `all`

```ts
all: (inputs: readonly unknown[]) => Derived<unknown[]>
```

Combine several inputs into one derived value of their current values, in order.

**Params**

- `inputs`: scopes, cells, derived values, or plain values, mixed freely. A plain value is carried as itself and never changes

**Returns** a derived value of an array, one slot per input. It recomputes when any input changes, once per burst, after the burst's commit has fully delivered.

**Example**

```ts
all([observer(doc).path('width'), observer(doc).path('height')])
  .map(([w, h]) => Number(w) * Number(h))
  .effect((area) => console.log(area));
```

#### `apply`

```ts
apply: (observable: unknown, commit: Commit) => void
```

Apply a whole commit to a document.

**Params**

- `observable`: any observable in the document. The commit is applied to its root
- `commit`: the commit, its deltas in any order

**Throws** an error naming the rule broken, having changed nothing. An integrity tag is not checked here: the algorithm is open in [`spec/format.md`](/spec/format) 8, and a tag is opaque bytes until it is settled. Watchers are called once, after every delta has been applied, with the deltas that fell in their scope. A watcher cannot tell an applied commit from a local mutation, so anything that records what a watcher delivers, an undo stack included, receives the commits it applies itself and wants a way to tell its own apart, such as a flag held for the duration of the call ([`recipes/core`](/docs/recipes/core) does exactly this). That flag only covers this call when the call is made from ordinary code. Userspace calls are deferred, so calling `apply` from inside a watcher hands the commit to the second document's watchers after the outer watcher has returned and cleared the flag. Queue the commit and apply it once the delivery has finished, which is what a transport does anyway. A commit only names observables reachable in the receiving document, so a replica of an existing document starts from its root id, `createObject(undefined, idOf(source))` for an object root; from there, applying the source's commits in the order they happened rebuilds it. A plain empty observable has a different root id and refuses them as unreachable.

**Example**

```ts
apply(doc, decodeCommit(bytes));
```

#### `atomic`

```ts
atomic: <T>(run: () => T) => T
```

Make everything inside one commit.

**Params**

- `run`: the mutations. Nesting joins the block already open and closes with it.

**Returns** whatever `run` returned.

**Throws** whatever `run` threw, after rolling every mutation it made back out of the tree. No commit is emitted and no document watcher hears one, but a list cell the block edited is not rolled back and tells its watchers after the rollback (the README says why, beside `atomic`).

**Example**

```ts
atomic(() => { doc.width = 3; doc.height = 4; });
```

#### `byId`

```ts
byId: (document: unknown, id: string | Id) => object | undefined
```

The observable in a document with a given id.

**Params**

- `document`: any observable in the document
- `id`: the twelve bytes a delta carries, or the same id in text form

**Returns** the observable, or undefined when the document holds nothing by that id. An observable that lost its attach edge is not found: the commit that detached it took it out of the document as it closed, and attaching it again re-sends its contents (design 084). Hold the observable itself if you need it after a detach, and ask `isReachable` whether it is still in the document.

**Throws** `not-observable` when `document` is not an observable, and `invalid-id` when `id` is bytes that are not an id.

**Example**

```ts
const task = byId(board, delta.id);
```

#### `createArray`

```ts
createArray: <T = unknown>(items?: Iterable<T> | undefined, id?: Id | undefined) => T[]
```

Make an array observable.

**Params**

- `items`: the values it starts with
- `id`: its id, when it has to be a particular one. Minted otherwise

**Returns** a proxy that reads as an array. `map`, `filter`, `find`, `join`, iteration and `length` all work. `push`, `pop`, `shift`, `unshift` and `splice` produce commits, and `sort`, `reverse`, `fill` and `copyWithin` throw, because they cannot be expressed as changes to the slots they appear to touch.

**Throws** `invalid-value`, `cell-in-document` or `inline-container` for an item a slot cannot hold, `multiple-attach`, `unreachable` or `duplicate-id` for an observable that already has a home or an id, and `invalid-id` for an id that is not twelve bytes. Attaching an observable that already has slots carries those slots in the same commit, one delta for the attach and one for each slot under it, however deep. That is what lets a replica be built from the commits alone: pushing a filled object sends its contents, not just its id.

**Example**

```ts
const blocks = createArray([createObject({ text: 'hi' })]);
blocks.push(createObject({ text: 'there' }));
```

#### `createMap`

```ts
createMap: <T = unknown>(entries?: Iterable<readonly [unknown, T]> | undefined, id?: Id | undefined) => ObservableMap<T>
```

Make a map observable.

**Params**

- `entries`: the entries it starts with, as pairs of id and value
- `T`: what the values are, so `get` answers with something better than unknown
- `id`: its id, when it has to be a particular one. Minted otherwise

**Returns** a map of ids to values. `add` files an observable under its own id, which is the common case; `set` names the id itself.

**Throws** `invalid-key` or `invalid-id` for an entry key that is not an id, `invalid-value`, `cell-in-document` or `inline-container` for a value a slot cannot hold, and `multiple-attach`, `unreachable` or `duplicate-id` for an observable that already has a home or an id.

**Example**

```ts
const presence = createMap();
presence.add(createObject({ cursor: 42 }));
```

#### `createObject`

```ts
createObject: <T extends object = Record<string, unknown>>(init?: Readonly<Partial<T>> | undefined, id?: Id | undefined) => T
```

Make an object observable.

**Params**

- `init`: the slots it starts with. Values are primitives or other observables; a plain object is refused rather than copied
- `id`: its id, when it has to be a particular one. Minted otherwise. The one common case is a replica, which starts from the source root's id: see `apply`

**Returns** a proxy whose properties are its slots. Assigning one is a commit; so is deleting one. Reading gives the primitive, or the observable the slot names.

**Throws** `invalid-value`, `cell-in-document` or `inline-container` for an init value a slot cannot hold, `multiple-attach`, `unreachable` or `duplicate-id` for an observable that already has a home or an id, and `invalid-id` for an id that is not twelve bytes. Construction is not a commit: the slots in `init` exist before anything can watch, so a replica wired afterwards never hears about them. A document that will replicate starts empty and assigns its slots once the watcher is wired, or hands the receiver `fromSnapshot(snapshot(doc))` as its starting point. Once it is attached, taking it back out leaves it readable but no longer writable: a write to a detached observable throws `unreachable`, because a receiver refuses the same delta. `isReachable` asks before writing, rather than finding out from the throw. It also leaves the document, so `byId` stops answering for it; attaching it somewhere again re-sends everything it holds (design 084).

**Example**

```ts
const doc = createObject({ title: 'notes', blocks: createArray([]) });
doc.title = 'aweft';
```

#### `fromEvent`

```ts
fromEvent: <E>(target: EventEmitting<E>, type: string) => Derived<E | undefined>
```

A cell of the last event of a type, or undefined before the first one.

**Params**

- `target`: anything with addEventListener and removeEventListener
- `type`: the event type to hear

**Returns** a chain of the most recent event. The listener is registered only while the chain is observed.

**Example**

```ts
fromEvent(window, 'resize').wait(100).effect(relayout);
```

#### `fromSnapshot`

```ts
fromSnapshot: (snap: Snapshot) => object
```

Build a live document from a snapshot (design 029).

**Params**

- `snap`: what `snapshot` returned, or the same shape written by hand

**Returns** the root observable, with the snapshot's ids, kinds, slots, positions and aliases. `snapshot(fromSnapshot(s))` deep-equals `s`, and commits addressed to the original document's ids apply to the rebuilt one. The rebuilt document holds what the snapshot says, which is the document, not the detached observables the original may still index. A commit that re-attaches one of those names an id the rebuilt document has never held, so its subtree arrives empty and deltas over its old slots are refused. A replica that must replay that kind of history replays the commit log rather than starting from a snapshot. Throws with the vocabulary `apply` uses when the snapshot does not describe a document: a ref naming an id the snapshot does not hold, an observable attached twice or not at all, a kind that disagrees with its target, or a slot key invalid for its kind.

**Example**

```ts
const copy = fromSnapshot(snapshot(doc));
```

#### `idOf`

```ts
idOf: (observable: unknown) => Id
```

An observable's id.

**Params**

- `observable`: any observable

**Returns** its 12 bytes of id. Two replicas agree on which observable a change is about by this and nothing else. It is published to everyone who can read the document, so it is never a credential.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
const mirror = createObject(undefined, idOf(doc));
```

#### `immutable`

```ts
immutable: { <T>(value: Derived<T>): Derived<T>; <T>(value: T): Derived<T>; }
```

A read-only view.

**Params**

- `value`: a chain, cell or scope to wrap read-only, or any plain value to carry as a constant

**Returns** a chain that reads what `value` reads and can never be written. Wrapping a chain keeps its changes flowing; wrapping a plain value never delivers one.

**Example**

```ts
const label = immutable('untitled');
const width = immutable(observer(doc).path('width'));
```

#### `insertAt`

```ts
insertAt: (list: object, position: Position, value: unknown) => void
```

Insert a value at a position of the caller's choosing.

**Params**

- `list`: the array observable
- `position`: the position key. It must be free, and it decides where the value sorts
- `value`: what to put there

**Throws** `not-observable` when `list` is not an array observable, `invalid-position` for a key the format forbids, and `slot-exists` when something already holds that position. This is what bridging two trees needs: a receiver that has been told a position must be able to honour it rather than generate its own and diverge.

**Example**

```ts
insertAt(mirror, positionsOf(list)[0]!, list[0]);
```

#### `intercept`

```ts
intercept: (document: unknown, fn: Interceptor) => () => void
```

Let a rule refuse commits on a document.

**Params**

- `document`: any observable in the document. The rule covers the whole document it belongs to, not the subtree under the observable named here
- `fn`: the rule. It is handed the commit about to close, after every delta has been applied and before anything has been delivered, and answers with the reasons to refuse it. An empty answer lets the commit close

**Returns** the function that removes the rule. Every way of making a commit closes through one transaction, so an assignment, an `atomic` block and a commit landed with `apply` are all read by the same rule, and a block is read once with everything it wrote. Refusing rolls the whole commit back, tells no watcher, and throws a `RefusedError` at whoever made it. The rule may read the document, which reads as the commit would leave it, and may not write to it: a write from inside a rule throws `sealed`, because it would be a change to the commit being decided. Several rules on one document all run and their refusals are concatenated.

**Throws** `not-observable` when `document` is not an observable.

**Example**

```ts
const stop = intercept(doc, (commit) =>
  doc.title === '' ? [{ code: 'invalid', message: 'a title cannot be empty' }] : []);
```

#### `isMutableArray`

```ts
isMutableArray: (value: unknown) => boolean
```

Is this a mutable array?

**Params**

- `value`: anything

**Returns** true for a list made by `mutableArray`; false for a document array, a plain array, or anything else.

**Example**

```ts
if (isMutableArray(items)) items.watch(reconcile);
```

#### `isObservable`

```ts
isObservable: (value: unknown) => boolean
```

Is this an observable?

**Params**

- `value`: anything

**Returns** true for an object, array or map made by this library.

**Example**

```ts
const doc = isObservable(input) ? input : createObject(input);
```

#### `isReachable`

```ts
isReachable: (observable: unknown) => boolean
```

Can this observable still be written to?

**Params**

- `observable`: any observable

**Returns** true while an attach path from the document root reaches it. Detaching an observable takes that path away, and a write to it then throws `unreachable`, which is exactly what a receiver does with the same delta. `parentOf` cannot answer this: it returns undefined both for a document root, which is reachable, and for something detached, which is not. Ask here rather than by catching the throw, because control flow through an error hides the ordinary case.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
if (isReachable(task)) task.done = true;
```

#### `kindOf`

```ts
kindOf: (observable: unknown) => ObservableKind
```

Which of the three kinds this is.

**Params**

- `observable`: any observable

**Returns** 'object', 'array' or 'map'. The kind never changes, and a commit that calls one observable two kinds is refused.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
if (kindOf(value) === 'array') count = (value as unknown[]).length;
```

#### `mutable`

```ts
mutable: <T>(initial: T) => Derived<T>
```

A standalone reactive value.

**Params**

- `initial`: the value it starts with

**Returns** a chain whose `set` writes the cell. A write to an equal value (`Object.is`) changes nothing and notifies nobody. Cells are not observables: assigning one into a document slot is refused, because a cell does not replicate and a document does.

**Example**

```ts
const open = mutable(false);
open.watch((now) => menu.hidden = !now);
open.set(true);
```

#### `mutableArray`

```ts
mutableArray: <T = unknown>(items?: Iterable<T> | undefined) => MutableArray<T>
```

A list outside the document, whose slots hold anything.

**Params**

- `items`: what it starts with

**Returns** an array. `push`, `pop`, `shift`, `unshift`, `splice`, index assignment and length assignment work and each delivers its changes to `watch`; every read is the array's own. `sort`, `reverse`, `fill` and `copyWithin` throw `unsupported`, as on a document array. Writing one into a document slot is refused with `cell-in-document`: it does not replicate. Inside `atomic`, the calls in the block deliver once at its close, as one list in call order, so a swap written as two index assignments is one change list (design 087). A block that throws still delivers them: the list was mutated and nothing rolls it back.

**Example**

```ts
const layers = mutableArray<Layer>();
layers.watch((changes) => { for (const c of changes) reconcile(c); });
layers.push(popup);
```

#### `observer`

```ts
observer: (observable: unknown) => Observer
```

Start a scope at an observable.

**Params**

- `observable`: where the scope is rooted. Paths and delivery are relative to it

**Returns** an observer. Every narrowing returns a new one, so an observer can be kept and narrowed differently in two places without either affecting the other.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
const stop = observer(doc).path('settings').ignore('draft').watch((change) => {
  send(change.deltas);
});
```

#### `parentOf`

```ts
parentOf: (observable: unknown) => object | undefined
```

Where an observable lives.

**Params**

- `observable`: any observable

**Returns** the observable holding its one attach edge, or undefined when nothing does, which is the case for a document root and for anything that has been detached. A reference from somewhere else is an alias and does not answer this question, which is the point: where something lives is a walk up, never a search.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
const list = parentOf(entry); // the array the entry sits in, or undefined
```

#### `pathOf`

```ts
pathOf: (observable: unknown) => readonly string[] | undefined
```

The attach path from the document root down to an observable.

**Params**

- `observable`: any observable

**Returns** the slot names from the root down, spelled the way the format spells a slot: an object key, a map id in text form, an array position in hex. Empty for the root, and undefined for anything nothing attaches. A walk up the parent pointers, so it costs the depth and never the document.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
pathOf(task);   // ['tasks', '80a1c2e3']
```

#### `positionsOf`

```ts
positionsOf: (list: object) => Position[]
```

The position keys of an array, in order.

**Params**

- `list`: the array observable

**Returns** one key per element, so a caller can name a place rather than an index.

**Throws** `not-observable` when `list` is not an array observable.

**Example**

```ts
const first = positionsOf(list)[0]; // survives edits elsewhere; list[0] does not
```

#### `snapshot`

```ts
snapshot: (observable: unknown) => Snapshot
```

Read a document out as plain data.

**Params**

- `observable`: any observable in the document. The document is read from its root

**Returns** every observable reachable from the root, by id in text form, with each slot either a primitive or the name of another observable and the kind of edge naming it. An observable that has lost its attach edge is not here. Nothing reaches it, and a delta naming it is refused, so it is not part of what the document says.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
assert.deepStrictEqual(snapshot(mirror), snapshot(doc));
```

#### `textIdOf`

```ts
textIdOf: (observable: unknown) => string
```

The same id in text form.

**Params**

- `observable`: any observable

**Returns** sixteen base64url characters, safe in a URL, a log line, or a key in a plain object. It is the form a map slot is named by.

**Throws** `not-observable` when `observable` is not an observable.

**Example**

```ts
history.set(textIdOf(entry), createObject({ opened: true }));
```

#### `timer`

```ts
timer: (ms: number) => Derived<number>
```

A cell that counts up while something observes it.

**Params**

- `ms`: milliseconds between ticks

**Returns** a chain of the tick count. The interval runs only while the chain is observed; an abandoned timer holds nothing. Map over it for a clock:

**Example**

```ts
timer(1000).map(() => new Date().toLocaleTimeString()).effect(show);
```

## Refusals

Every error this package raises carries a `reason` to switch on and a `fix` that says what to do. These are its reasons, from `errors.txt`.

| reason | fix |
|---|---|
| `async-atomic` | Await outside the block, then pass the settled values to atomic. |
| `cell-in-document` | Write its get() into the slot, and keep the cell in the interface. |
| `duplicate-id` | Let createObject mint the id rather than reusing one this document holds. |
| `empty-commit` | Drop the commit instead of applying it, or add the delta it was meant to carry. |
| `inline-container` | Build it with createObject, createArray or createMap, then assign that. |
| `invalid-key` | Assign a whole number index of zero or more. |
| `invalid-key` | Index the array with a number, and keep symbol-keyed data elsewhere. |
| `invalid-key` | Index the list with a number, and keep symbol-keyed data elsewhere. |
| `invalid-key` | Key an array slot with the hex of a position from positionsOf, not an index. |
| `invalid-key` | Pass an id, the same id in text form, or the observable it names. |
| `invalid-key` | Use a string key, and keep symbol-keyed data outside the document. |
| `invalid-position` | Name the slot yourself with insertAt rather than asking for one between these. |
| `invalid-position` | Pass the lower position as a and the higher as b, or null for an open end. |
| `invalid-value` | Store a string, number, boolean, null, Uint8Array, or an observable. |
| `invalid-value` | Use delete on the slot, or assign null when the slot should stay. |
| `invalid-write` | Assign the property instead of calling Object.defineProperty. |
| `invalid-write` | Call push or splice to grow it; assign a smaller length to shorten it. |
| `invalid-write` | Call splice to take the element out and close the gap. |
| `invalid-write` | Push the value on the end, or splice it in where you want it. |
| `inverse-not-asked` | Register the watcher as watch(fn, { inverse: true }). |
| `kind-conflict` | Give the ref the kind the observable it names declares. |
| `kind-conflict` | Keep one kind per id; make a new observable rather than reusing the id. |
| `kind-conflict` | Set kind to 'object', 'array' or 'map'. |
| `multi-target` | Narrow the scope with path until it names one slot, then set that. |
| `multiple-attach` | Keep one attach edge and make the others 'alias'. |
| `multiple-attach` | Keep one attach edge in the commit and send the rest as 'alias'. |
| `multiple-attach` | Leave one slot holding it, and make the other an alias. |
| `multiple-attach` | Wrap it in alias here, or clear the slot holding it first. |
| `not-observable` | Pass an observable, or call set to file a value under an id you choose. |
| `not-observable` | Pass the document createObject, createArray or createMap returned. |
| `not-observable` | Pass what createArray returned. |
| `not-observable` | Pass what createObject, createArray or createMap returned. |
| `read-only` | Build the chain over a writable source rather than an immutable one. |
| `read-only` | Give the chain a write path with setter before calling set. |
| `sealed` | Return a Refusal from the rule instead of writing to the document. |
| `slot-exists` | Pick a position no element holds, or assign over the element already there. |
| `slot-exists` | Replace the slot instead of adding it, or remove what is there first. |
| `slot-missing` | Add the slot before replacing or removing it. |
| `slot-missing` | Create the observables along the path first, or check get() before setting. |
| `unreachable` | Add the observable it names to observables, or drop the slot naming it. |
| `unreachable` | Add the root to observables, or name a root that is already there. |
| `unreachable` | Apply the commit that attaches it first, or replicate from the source root id. |
| `unreachable` | Attach it somewhere outside its own subtree. |
| `unreachable` | Attach it under the root, or take it out of the snapshot. |
| `unreachable` | Attach the observable to the document before writing to it. |
| `unsupported` | Assign the order you want, or hold the sort outside the list. |
| `unsupported` | Assign the order you want, or hold the sort outside the state. |
| `unsupported` | Assign the order you want. |
| `unsupported` | Assign the slots you mean. |

## Recipes

The programs in the stack's gate that use this package, each a job someone would have.

- [`recipes/todo-list`](/docs/recipes/todo-list): A list you add to, toggle, filter and reorder, with the list following each edit rather than being rebuilt
- [`recipes/two-clients`](/docs/recipes/two-clients): Two people editing one document at once, including what happens when they write the same slot and who yields
- [`recipes/optimistic-write`](/docs/recipes/optimistic-write): A write that applies locally before the server sees it, is refused, and is rolled back
- [`recipes/debug`](/docs/recipes/debug): Finding a bug in a document you did not write
- [`recipes/ui`](/docs/recipes/ui): A page with themes, contexts, control flow, a popup and a suspend, built by vite and driven in a real browser
- [`recipes/routed-site`](/docs/recipes/routed-site): A site with real URLs: nested pages, a page with a parameter, a page that arrives later, a dialog the back button dismisses, and a title per page
- [`recipes/translated-site`](/docs/recipes/translated-site): A site written in one language and launched in three: the build finds every string and writes the catalog an agent fills, each language is a tree of pages with the plural rules of its own, a stored act is compiled where it runs, and the Ukrainian page hydrates in place
- [`recipes/core`](/docs/recipes/core): the package's own recipe
