Skip to content

DirtyTalk

DirtyTalk is a family of small, framework-agnostic libraries built around one question: after a mutation, what changed, who cares, and when do we tell them? Each package answers that question in a different domain, but they all share a single move — compute “what changed” once at the source, in a format every subscriber can intersect cheaply. This page is the entry point to the whole section; it explains the shared thesis and points you to the package you actually need.

Two unrelated problems turn out to be the same problem. A WebGPU renderer mutates a scene and must repaint; a state container mutates state and must notify subscribers. Both face the same three sub-questions after every mutation:

  1. What changed? — a region of the world the mutation touched.
  2. Who cares? — the subscribers whose interest overlaps that region.
  3. When do we tell them? — the scheduling window in which we batch and deliver.

The naive answer pushes all the work down to each consumer. A renderer with a single dirty bit repaints the whole canvas because the bit lost the information about where the change was. A state container re-walks the entire state tree once per subscriber — N consumers means N separate tree walks doing the same equality checks. Both approaches throw away the structure of the change and pay for it on every read.

DirtyTalk’s answer is to compute “what changed” once, at the source, and express it as a Region — a value in an algebra with empty, union, and intersects. The source accumulates a single dirty region per scheduling window. Each subscriber declares an interest region. Deciding who to wake is then just one cheap intersects check per subscriber against the one shared dirty region — work proportional to the number of subscribers, not to the size of the state or the scene multiplied by the number of subscribers.

DirtyTalk separates the abstract algebra from its concrete instantiations. The engine defines the shape of the problem; the two domain packages fill in what a Region actually is.

PackageRoleWhat a Region isBuilt for
@dirtytalk/engineThe abstract algebra + scheduling glue. Defines the Space<Region> interface (empty / isEmpty / union / intersects), the Scheduler interface (with SyncScheduler, ManualScheduler, MicrotaskScheduler, RAFScheduler), and DirtyChannel<Region> — the core that accumulates marks within a window and fans out to interested subscribers in one flush. Ships no concrete Region.abstract — you supply oneThe shared core both domains depend on
@dirtytalk/spatialThe concrete instantiation for 2D rendering. RectSpace implements Space<DirtyRegion> where a Region is a list of damage rects (Damage entries carrying a Rect and a kind). Adds a SceneNode / SceneRoot tree, a three-stage data → layout → paint pipeline, and a PointerRouter.a list of damage rectscanvas / GPU renderers
@dirtytalk/structuralThe concrete instantiation for state containers. PathSetSpace implements Space<PathSet> where a Region is a set of interned path IDs. Adds a PathInterner, a trackRender proxy recorder, diff helpers, and an abstract StructuralContainer.a set of interned path IDsobservable state / data stores

Each domain package answers the same algebra in its own terms. “Union two dirty regions” means “bounding-box-concatenate two damage lists” in spatial and “set-union two path-ID sets” in structural. “Does this interest intersect the dirty region?” means “do any rects overlap?” in spatial and “do the sets share an ID?” in structural. The DirtyChannel machinery — coalescing, selective fan-out, lazy interest thunks, re-entrancy handling, error isolation — is written once in the engine and reused by both.

The engine ships zero Region implementations — on purpose

@dirtytalk/engine never references React, the DOM, or a GPU, and it ships no concrete Space. RectSpace lives in spatial; PathSetSpace lives in structural. The engine only unions and intersects whatever Regions you hand it — producing a Region from a mutation is the consumer’s job. There is no dependency graph, no auto-tracking, and no diffing at the engine layer.

If you arrived here from the BlaC documentation: BlaC is the headline product of these docs. It is a state-container library with per-consumer path tracking, and it is a motivating consumer of the structural model — @dirtytalk/structural is the lower-level lineage of BlaC’s per-consumer path tracking.

These DirtyTalk packages are standalone and framework-agnostic. You can use @dirtytalk/engine, @dirtytalk/spatial, or @dirtytalk/structural entirely on their own, with no React and no BlaC. This section documents that lower-level engine family directly. If you want a batteries-included state library for React, start with the BlaC introduction; if you want the underlying machinery, read on.

Terminal window
# The abstract core (always available; the domain packages depend on it)
pnpm add @dirtytalk/engine
# 2D rendering domain
pnpm add @dirtytalk/spatial
# State-container domain
pnpm add @dirtytalk/structural

Both @dirtytalk/spatial and @dirtytalk/structural depend on @dirtytalk/engine, so installing either one pulls the engine in transitively. Install the engine on its own only when you are wiring up a brand-new domain with your own Space.

If you want to…UseStart at
Track changes to a 2D scene and repaint only what moved (canvas / WebGPU)@dirtytalk/spatialSpatial getting started
Observe a state container and wake only the consumers that read the changed fields@dirtytalk/structuralStructural getting started
Use BlaC’s state management in a React app@blac/core + @blac/reactBlaC introduction
Apply the “compute changes once, fan out cheaply” pattern to a new domain with your own Region@dirtytalk/engineEngine getting started
Understand the algebra and scheduling model before picking a domain@dirtytalk/engineEngine concepts

Pick a package and dive in:

  • Engine — the abstract core. Getting started builds your first DirtyChannel; concepts covers the Space algebra, schedulers, and the DirtyChannel flush model.
  • Spatial — the 2D rendering domain. Getting started builds your first scene; concepts covers the damage model, the render pipeline, and pointer routing.
  • Structural — the state-container domain. Getting started builds your first container; concepts covers path sets, the observed skeleton, and read tracking.