01 @dirtytalk/engine
Engine
The algebra. Mark, coalesce, flush.
Chapter 01 Watch engine run, interactively →@dirtytalk/engine has zero dependencies. It ships the algebra and the scheduling glue, but no concrete region type.
pnpm add @dirtytalk/engine
Space
A Space<Region> describes what “changed” means for your domain.
interface Space<Region> {
empty(): Region;
isEmpty(r: Region): boolean;
union(a: Region, b: Region): Region;
intersects(interest: Region, dirty: Region): boolean;
}
Both union and intersects must be pure.
Scheduler
A scheduler decides when a flush runs.
| Scheduler | Flushes |
|---|---|
SyncScheduler | immediately |
ManualScheduler | when you call .pump() |
MicrotaskScheduler | at the end of the microtask queue |
RAFScheduler | on the next animation frame |
DirtyChannel
import { DirtyChannel, SyncScheduler } from '@dirtytalk/engine';
import type { Space } from '@dirtytalk/engine';
const KeySpace: Space<Set<string>> = {
empty: () => new Set(),
isEmpty: (r) => r.size === 0,
union: (a, b) => new Set([...a, ...b]),
intersects: (interest, dirty) => [...interest].some((k) => dirty.has(k)),
};
const channel = new DirtyChannel(KeySpace, new SyncScheduler());
const unsub = channel.subscribe(
() => new Set(['users']),
(dirty) => console.log('dirty:', [...dirty]),
);
channel.mark(new Set(['users'])); // dirty: ['users']
channel.mark(new Set(['theme'])); // no output
unsub();
- Marks within one scheduler window coalesce into a single flush.
- The interest thunk is re-evaluated on every flush.
- Marks made inside a callback go to the next flush.
- A throwing subscriber doesn’t stop the flush; pass
onErrorto handle errors.