# Concepts

> The model shared by all dirtytalk packages: regions, spaces, mark-coalesce-flush, interest thunks, schedulers and re-entrancy.

Source: https://dirtytalk.42.dog/docs/concepts/

Every dirtytalk package is the same pipeline: producers mark regions dirty, the channel coalesces them, and a scheduler flushes them to subscribers whose interest intersects the result. The packages differ only in what a region is.

## Region

A **Region** is a value that describes a part of your domain. It is used for two things: what changed (a mark) and what a subscriber cares about (an interest). The engine never inspects a region. It only passes regions to the `Space` you provide.

## Space

`Space<Region>` is the algebra over regions. It is the only interface a domain must implement.

```ts
import type { Space } from '@dirtytalk/engine';

const NumberBitsetSpace: Space<number> = {
  empty: () => 0,
  isEmpty: (r) => r === 0,
  union: (a, b) => a | b,
  intersects: (interest, dirty) => (interest & dirty) !== 0,
};
```

| Method                        | Meaning                                                          |
| ----------------------------- | ---------------------------------------------------------------- |
| `empty()`                     | The region containing nothing.                                   |
| `isEmpty(r)`                  | Whether `r` contains nothing.                                    |
| `union(a, b)`                 | The region covering both `a` and `b`.                            |
| `intersects(interest, dirty)` | Whether a subscriber with `interest` must be told about `dirty`. |

Contracts: `union(empty(), r)` equals `r`, `intersects(empty(), _)` is `false`, and all operations are pure.

An optional `unionInto(acc, b)` accumulates in place. It may mutate and return `acc`, but must not mutate `b`. `DirtyChannel` calls it only on an accumulator it owns, so it is an optimization that avoids O(N²) copying for bursts of marks. If absent, `union` is used.

See [Custom spaces](/docs/engine/custom-spaces/).

## Mark, coalesce, flush

`DirtyChannel<Region>` runs the cycle.

1. **Mark.** `channel.mark(region)` records that `region` changed. The first mark in a window schedules a flush through the scheduler.
2. **Coalesce.** Further marks before the flush are merged with `space.union`. However many marks happen, the scheduler is asked for one flush.
3. **Flush.** The scheduler runs the flush. The channel takes the accumulated region and resets it. If it is empty, nothing is called. Otherwise each subscriber is considered in registration order.

```ts
import { DirtyChannel, ManualScheduler } from '@dirtytalk/engine';
import type { Space } from '@dirtytalk/engine';

const BitsSpace: Space<number> = {
  empty: () => 0,
  isEmpty: (r) => r === 0,
  union: (a, b) => a | b,
  intersects: (i, d) => (i & d) !== 0,
};

const scheduler = new ManualScheduler();
const channel = new DirtyChannel(BitsSpace, scheduler);

channel.subscribe(
  () => 0b011,
  (dirty) => console.log('flushed with', dirty),
);

channel.mark(0b001);
channel.mark(0b010);
scheduler.pump();
// flushed with 3
```

`channel.subscribe(interest, cb)` returns an unsubscribe function. `channel.dispose()` cancels any pending flush and drops all subscribers. Subscribers that throw do not stop later subscribers: with one error it is rethrown after the flush, with several they are thrown as an `AggregateError`, and with the `onError` option (`new DirtyChannel(space, scheduler, { onError })`) errors go to that callback instead.

## Interest thunks and intersection

A subscriber's interest is a function, `() => Region`, not a fixed value. The channel calls it once per flush for each subscriber, so interest can change over time without re-subscribing. For each subscriber, the callback runs only if `space.intersects(interest, dirty)` is `true`. The callback receives the full coalesced dirty region, not only the part that intersects.

An interest thunk that returns `null` or `undefined` means no interest for that flush.

## Schedulers

A `Scheduler` decides when a flush runs. Its interface is `request(flush)` and an optional `cancel(flush)`.

| Scheduler            | Flush runs                                                                    |
| -------------------- | ----------------------------------------------------------------------------- |
| `SyncScheduler`      | Immediately, inside `mark`.                                                   |
| `MicrotaskScheduler` | At the end of the current microtask checkpoint, via `queueMicrotask`.         |
| `RAFScheduler`       | On the next animation frame (or after 16 ms without `requestAnimationFrame`). |
| `ManualScheduler`    | When you call `pump()`.                                                       |

`MicrotaskScheduler`, `RAFScheduler` and `ManualScheduler` batch the flushes of every channel sharing the instance into one drain. See [Schedulers](/docs/engine/schedulers/).

## Re-entrancy

Marks made during a flush, for example from a subscriber callback, do not join the flush in progress. They accumulate for the next flush, which the channel schedules once the current one finishes. Subscribers added during a flush are also skipped until the next flush. A subscriber unsubscribed during a flush before its turn does not run.

```ts
import { DirtyChannel, ManualScheduler } from '@dirtytalk/engine';
import type { Space } from '@dirtytalk/engine';

const BitsSpace: Space<number> = {
  empty: () => 0,
  isEmpty: (r) => r === 0,
  union: (a, b) => a | b,
  intersects: (i, d) => (i & d) !== 0,
};

const scheduler = new ManualScheduler();
const channel = new DirtyChannel(BitsSpace, scheduler);

channel.subscribe(
  () => 0b001,
  () => channel.mark(0b100),
);
channel.subscribe(
  () => 0b100,
  (dirty) => console.log('second flush', dirty),
);

channel.mark(0b001);
scheduler.pump(); // first flush: the 0b100 mark is queued
scheduler.pump(); // second flush
// second flush 4
```

> **Note:** A subscriber that marks a region it is itself interested in is notified again on the next flush. Avoid unconditional self-marking to prevent a flush loop.

## Structural and spatial are Spaces

The other packages add no new notification machinery. Each supplies a `Space` and builds an API around a `DirtyChannel` that uses it.

- **Structural.** `PathSetSpace` is a `Space<PathSet>`. A region is a `Set<PathId>` of interned state paths, or the `ALL_PATHS` symbol meaning everything. Union merges sets, and intersection is a shared path. A consumer's interest is the set of paths it read. See [Structural](/docs/structural/).
- **Spatial.** `RectSpace` is a `Space<DirtyRegion>`, where `DirtyRegion` is `readonly Damage[]`. Each `Damage` has a `rect`, a `kind` (`'paint'`, `'layout'` or `'data'`) and an optional `node`. Union concatenates, and a `'paint'` damage intersects an interest only when their rectangles overlap. `'layout'` and `'data'` damage intersect any non-empty interest. See [Spatial](/docs/spatial/).

## Glossary

| Term          | Definition                                                                       |
| ------------- | -------------------------------------------------------------------------------- |
| Region        | A value describing part of a domain; used for both marks and interests.          |
| Space         | The `Space<Region>` algebra: `empty`, `isEmpty`, `union`, `intersects`.          |
| Mark          | A call to `channel.mark(region)` recording that a region changed.                |
| Coalesce      | Merging marks within one window into a single dirty region with `union`.         |
| Flush         | The scheduled step that notifies intersecting subscribers.                       |
| Interest      | A `() => Region` thunk evaluated per flush to say what a subscriber cares about. |
| Subscriber    | An interest thunk plus a callback registered with `subscribe`.                   |
| Scheduler     | Decides when a flush runs: sync, microtask, animation frame or manual.           |
| Dirty channel | `DirtyChannel`, which accumulates marks and delivers them on flush.              |
| PathSet       | Structural's region: `Set<PathId>` or `ALL_PATHS`.                               |
| Damage        | Spatial's region element: a rect, a kind and an optional node.                   |
