All projects

Cells

10 kB minifiedNo UI framework requiredOpen source

Async state is a graph. Write it that way.

Cells is the 10 kB TypeScript dataflow runtime behind our products, starting with Chainwall. A Sheet connects writable and computed values, waits for promises, propagates errors, ignores obsolete results, and follows dependencies that change at runtime.

Change an input. Watch the graph settle.

This demo runs Cells itself. Each edit waits 620 ms, then the sum resolves after 240 ms. Run the race to start a slow result before requesting a newer one.

Readers can keep using the last stable sum while a new one runs. Subscribers receive the new value after the graph settles.

Graph settled

Change either value.

Cells waits 620 ms after typing stops, then resolves the sum after 240 ms. The last stable result remains readable while the next one runs.

Delayed sum
30

Published after the graph settled

debouncer(620) → delayed(a + b, 240)
Dynamic pointer
10

The downstream cell follows the selected source. Its consumer does not need to be rebuilt.

Latest result wins

When the slow result arrives, Cells discards it because the request for 9 is newer.

Your state already has dependencies.

Most application state comes from other values. Some of that work is still running; some is already obsolete. Cells stores those relationships in the program, so adding a derived value adds one node to the graph instead of another synchronization path.

Use promises as values

A source or computation can return a promise. Dependent work starts when the value is ready, without a separate loading reducer or effect.

Ignore late results

Every computation has a rank. When an old request finishes late, Cells discards it instead of replacing the value from newer work.

Publish after the graph settles

Cells updates affected nodes in dependency order. Subscribers hear about the change after that update wave has settled.

Follow a changing cell

A computation can return another cell. Its consumers follow that cell as a pointer, which works well for selections, routes, caches, and editable records.

Choose how fresh a read must be

Read the stable value now, wait for the current computation, or wait for all work owned by a Sheet or scoped proxy.

Name and remove whole subgraphs

A SheetProxy can own and destroy a subgraph as one unit. Cell names and the debugger show its dependencies during development.

Choose how long a read should wait.

A UI can keep showing a stable value while it refreshes. A test or transaction boundary can wait for the current computation. Each read states which behavior it needs.

cell.value

Read now

Use the last stable value immediately.

await cell.get()

Wait for first value

Wait only when the cell has not produced a value yet.

await cell.consolidatedValue

Wait for current result

Wait for the computation this cell most recently requested.

await sheet.wait()

Wait for all Sheet work

Wait for every pending computation registered with the Sheet.

Where Cells fits

Use Cells when selections, requests, permissions, and computed views form a changing async pipeline. Dynamic pointers suit routes and editable records. A SheetProxy can remove every cell owned by an unmounted feature.

When another tool fits

Use an event log for replay and audit history, a query cache for standard server data, or a stream library for time-based events. Cells is built for live dependency graphs.

Security labels are still a proposal

We are exploring security labels, value provenance, and protected UI sinks as layers above Cells. Applications must still enforce authorization and validate data at the server or onchain boundary.

Inspect the runtime and its tests.

The repository includes the runtime, package documentation, and tests for async races, pointer changes, and graph propagation.

GitHub