API reference

Reference for every public export of @dirtytalk/engine with exact TypeScript signatures, parameters, return values, behaviour and examples.

View .md Edit on GitHub

All exports of @dirtytalk/engine, with signatures taken from the source. For explanations see DirtyChannel, Schedulers and Custom spaces.

import {
  DirtyChannel,
  SyncScheduler,
  ManualScheduler,
  MicrotaskScheduler,
  RAFScheduler,
} from '@dirtytalk/engine';
import type { Space, Scheduler } from '@dirtytalk/engine';

Types

Space

interface Space<Region> {
  empty(): Region;
  isEmpty(r: Region): boolean;
  union(a: Region, b: Region): Region;
  intersects(interest: Region, dirty: Region): boolean;
  unionInto?(acc: Region, b: Region): Region;
}

The algebra of “what changed” and “what I care about”. Contracts: union(empty(), r) equals r; intersects(empty(), _) is false; all operations are pure, except that unionInto may mutate and return acc.

MethodReturns
empty()A region meaning “nothing”.
isEmpty(r)true if r is empty.
union(a, b)A region covering both. May return a or b by reference.
intersects(interest, dirty)true if a subscriber with interest should be notified of dirty.
unionInto(acc, b)Optional. union that may mutate acc; must not mutate b.
import type { Space } from '@dirtytalk/engine';

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

See Custom spaces.

Scheduler

interface Scheduler {
  request(flush: () => void): void;
  cancel?(flush: () => void): void;
}

Decides when a flush runs. request asks for flush to be called later; cancel, if present, drops a pending request.

import type { Scheduler } from '@dirtytalk/engine';

const immediate: Scheduler = {
  request: (flush) => flush(),
};

See Schedulers.

Classes

DirtyChannel

class DirtyChannel<Region> {
  constructor(
    space: Space<Region>,
    scheduler: Scheduler,
    options?: { onError?: (err: unknown) => void },
  );
  mark(r: Region): void;
  subscribe(interest: () => Region, cb: (dirty: Region) => void): () => void;
  dispose(): void;
}

Accumulates marked regions and delivers them to interested subscribers in one flush per scheduler window.

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

const BitSpace: 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(BitSpace, scheduler, {
  onError: (err) => console.error(err),
});

const unsubscribe = channel.subscribe(
  () => 0b01,
  (dirty) => console.log('dirty', dirty),
);
channel.mark(0b01);
scheduler.pump(); // logs: dirty 1
unsubscribe();
channel.dispose();

constructor

ParameterDescription
spaceThe Space used to combine and compare regions.
schedulerThe Scheduler that triggers flushes. Not called during construction.
options.onErrorOptional handler for subscriber errors. Without it, errors are rethrown from the flush.

mark

mark(r: Region): void. Merges r into the pending dirty region and requests a flush if none is pending. Returns nothing. Does nothing after dispose(). Throws only if space.union/unionInto throws, or if the scheduler flushes synchronously and the flush rethrows subscriber errors.

subscribe

subscribe(interest, cb): () => void. Registers a subscriber. interest is evaluated once per flush; cb is called with the full dirty region when space.intersects(interest(), dirty) is true. Returns an idempotent unsubscribe function. After dispose(), returns a no-op function. Does not throw.

dispose

dispose(): void. Cancels a pending flush, drops subscribers and pending marks, and makes the channel inert. Idempotent.

Flush errors

During a flush, errors from interest, space.intersects and cb are isolated per subscriber. With onError, each is passed to it. Without, after all subscribers have run, a single error is rethrown as-is and several are rethrown as AggregateError with message DirtyChannel: subscriber errors during flush.

SyncScheduler

class SyncScheduler implements Scheduler {
  request(flush: () => void): void;
}

Calls flush immediately inside request. No deduplication, no cancel. Throws whatever flush throws.

import { DirtyChannel, SyncScheduler } from '@dirtytalk/engine';
import type { Space } from '@dirtytalk/engine';

declare const space: Space<number>;
const channel = new DirtyChannel(space, new SyncScheduler());

ManualScheduler

class ManualScheduler implements Scheduler {
  request(flush: () => void): void;
  cancel(flush: () => void): void;
  pump(): void;
}

Holds requests until pump(). Requests of the same function are deduplicated.

MethodDescription
request(flush)Adds flush to the pending set.
cancel(flush)Removes flush from the pending set.
pump()Runs the flushes pending now; requests made during the pump wait for the next one. No-op if none pending. Runs every flush even if some throw, then rethrows a single error as-is or an AggregateError (ManualScheduler: flush errors during drain).
import { ManualScheduler } from '@dirtytalk/engine';

const scheduler = new ManualScheduler();
scheduler.request(() => console.log('flushed'));
scheduler.pump(); // logs: flushed

MicrotaskScheduler

class MicrotaskScheduler implements Scheduler {
  request(flush: () => void): void;
  cancel(flush: () => void): void;
}

Runs all pending flushes in one queueMicrotask callback. Duplicates are deduplicated. cancel removes a flush from the pending set. Errors from flushes are rethrown inside the microtask after all have run (MicrotaskScheduler: flush errors during drain for several).

import { MicrotaskScheduler } from '@dirtytalk/engine';

const scheduler = new MicrotaskScheduler();
scheduler.request(() => console.log('flushed'));
await Promise.resolve(); // logs: flushed

RAFScheduler

class RAFScheduler implements Scheduler {
  request(flush: () => void): void;
  cancel(flush: () => void): void;
}

Runs all pending flushes on the next animation frame. If globalThis.requestAnimationFrame is not a function when the scheduler is constructed, it uses setTimeout(..., 16) instead. cancel removes a flush and, if nothing else is pending, cancels the frame or timer. Errors are rethrown after all flushes have run (RAFScheduler: flush errors during drain for several).

import { RAFScheduler } from '@dirtytalk/engine';

const scheduler = new RAFScheduler();
scheduler.request(() => console.log('next frame'));