@demlik/tea
v0.20.0
Published
TEA-faithful state machine substrate — one package, every host adapter. Core + subs + testing, plus React, Durable Object, Chrome extension, Node, in-memory, devtools, and property-testing modules as subpaths.
Maintainers
Readme
@demlik/tea
A TEA / Elm-Architecture TypeScript library for durable, replayable state machines — one pure reducer, every host adapter.
Install
pnpm add @demlik/teaThe one runtime dependency is @standard-schema/spec, a types-only package
installed for you. Cmd.define takes any Standard Schema, so bring the schema
library you already use (zod, or Effect Schema through
Schema.toStandardSchemaV1). The host and
testing adapters ride on optional peers you add only for the surface you use:
react / react-dom (the ./react hooks), ws (Node WebSocket subs),
fast-check (the ./pbt property-testing helpers), vitest (the testing
utilities), effect (the ./effect engine), and @opentelemetry/api (the
./otel spans).
Quickstart
A machine is plain data: an init and an update map keyed by Msg type.
run drives it, and when it has effects you hand run the interpret map
that performs them; dispatch folds a Msg; getState reads the current
Model. You name the Model and the Msg union
once, under types; everything else is inferred from the machine itself. Each of
these words has an entry in the glossary.
import { defineMachine } from "@demlik/tea";
import { run } from "@demlik/tea/promise";
type State = { readonly count: number };
type Msg = { readonly type: "increment" } | { readonly type: "reset" };
const counter = defineMachine({
types: { model: {} as State, msg: {} as Msg },
init: (loaded) => (loaded !== null ? [loaded, []] : [{ count: 0 }, []]),
update: {
increment: (s) => [{ count: s.count + 1 }, []],
reset: () => [{ count: 0 }, []],
},
});
const runtime = await run(counter, {}).ready;
await runtime.dispatch({ type: "increment" });
runtime.getState(); // { count: 1 }Two engines, one machine
The core is @demlik/tea, and each engine has its own entry point. A machine
file imports only the core, so it runs unchanged on either engine.
@demlik/tea— the core:defineMachine,Cmd,replayand the types. It imports no engine.@demlik/tea/promise— the Promise engine. Handlers return Promises.@demlik/tea/effect— the Effect engine (stable, Effect v4). Handlers return Effects, services come from your Layers, the handle's verbs return Effects with typed errors, and closing the scope interrupts whatever is in flight.
Run a machine on the Promise engine
and on the Effect engine
build on the same machine file.
Run many machines under one parent
starts child machines on the Effect engine with spawn, tell and stop.
Coming from 0.15?
The migration guide
shows every removed or reshaped API, before and after.
Documentation
The four Diátaxis quadrants live in docs/:
- Tutorials — learning-oriented lessons that take you through the library by building a real machine.
- How-to guides — goal-oriented directions for getting a specific job done.
- Reference — information-oriented API description, generated per public module.
- Explanation — understanding-oriented discussion of how it works and why it is shaped this way.
The glossary defines each word those pages use: Model, Msg, Cmd, Sub, Store and the rest.
Two more surfaces sit behind those, for people working on the library rather than using it:
.patterns/ is how the code is shaped, and
.decisions/ is why it was shaped that way.
