Introduction

dirtytalk is a set of dirty-tracking primitives that answer what changed, who cares, and when to tell them, for any domain.

View .md Edit on GitHub

dirtytalk is a small monorepo of dirty-tracking primitives. After a mutation, it computes what changed once, tells only the consumers that care, and does so at a moment you choose.

What dirtytalk does

After any mutation, three questions need answers:

  1. What changed? A Space defines what a region is in your domain (a set of keys, a set of paths, a list of rectangles). Every mark is a region, and marks within one window are unioned into a single dirty region.
  2. Who cares? Each subscriber supplies an interest thunk that returns a region. On flush, only subscribers whose interest intersects the dirty region are called.
  3. When do we tell them? A Scheduler decides when the flush runs: immediately, at the end of the microtask, on the next animation frame, or when you call pump().

See Concepts for the full model.

Packages

PackageRoleDocs
@dirtytalk/engineThe algebra and the glue: Space<Region>, Scheduler and its four implementations, and DirtyChannel.Engine
@dirtytalk/structuralA Space over interned state paths (PathSet), plus a state container, render tracking and a React hook.Structural
@dirtytalk/spatialA Space over damage rectangles (Damage[]), plus a scene graph, a damage-driven renderer and pointer routing.Spatial

structural and spatial are built on engine. Neither re-implements scheduling or subscription.

Which package do I need

  • You need to coalesce marks and notify interested subscribers for your own kind of region: use @dirtytalk/engine and implement a Space. See Custom spaces.
  • You have plain object state and want consumers (such as React components) to re-run only when the paths they read change: use @dirtytalk/structural.
  • You draw to a canvas or similar surface and want to repaint only the rectangles that changed: use @dirtytalk/spatial.
  • You only need the scheduling behavior (sync, microtask, animation frame, manual): use @dirtytalk/engine alone.

Example

A channel over a set of string keys, using @dirtytalk/engine only. Marks in one microtask coalesce into a single flush, and subscribers whose interest does not intersect the dirty region are skipped.

import { DirtyChannel, MicrotaskScheduler } 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) => {
    for (const key of interest) if (dirty.has(key)) return true;
    return false;
  },
};

const channel = new DirtyChannel(KeySpace, new MicrotaskScheduler());

channel.subscribe(
  () => new Set(['users']),
  (dirty) => console.log('users view:', [...dirty]),
);
channel.subscribe(
  () => new Set(['theme']),
  (dirty) => console.log('theme view:', [...dirty]),
);

channel.mark(new Set(['users']));
channel.mark(new Set(['session']));

await Promise.resolve();
// users view: [ 'users', 'session' ]
// (theme view is not called: its interest did not intersect)

Next steps