Skip to content

dunky-dev/state-machine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

195 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Dunky

STATE-MACHINE

Define a behavior once. Render it anywhere.

A UI component is really two things tangled together: behavior and render. Dunky splits them. You describe behavior as a plain TypeScript state machine that knows nothing about the environment and a thin per-substrate layer plugs it into a runtime.

The same machine drives any render in a JS runtime. Same states, same transitions, same accessibility intent. Only the render differs.

          +------------------------------+
          |      ONE STATE MACHINE       |
          |   states Β· events Β· context  |
          |   pure behavior β€” no render  |
          +---------------+--------------+
                          |  connect() β†’ onPress Β· role Β· describedBy
          +---------------+---------------+
          v               v               v
    +-----------+   +-----------+   +-----------+
    | React DOM |   |   Native  |   |    TUI    |
    | β†’ onClick |   |β†’ Pressable|   | β†’ keypress|
    |  + aria-* |   |   + a11y  |   |  + cells  |
    +-----------+   +-----------+   +-----------+
     same behavior, byte-for-byte β€” only the render differs

Status: experimental. The engine (packages/core) is stable and tested. The target bridges are NOT production-ready yet.

This is an in-progress exploration.

The challenge

Truly agnostic

This project was inspired by Zag, which pioneered the component-as-a-headless-machine approach. Zag is agnostic about which framework renders the DOM, but it still assumes a DOM exists. Dunky takes that one step further: it assumes nothing about the environment. The machine is a pure behavioral kernel with no environment touchpoints, every place behavior meets the platform β€” a keydown listener, a timer, a focus β€” is pushed to the per-target layer (the view's effects.ts). So the same machine runs unchanged on the DOM, React Native, or any other JS runtime.

Fast at scale

The hard case is many machines reacting to many events inside one frame budget β€” things like a trading terminal with live tickers, a monitoring wall, a canvas board, a game HUD. There the cost of each transition and the memory per machine, multiplied by thousands, is what decides whether you hold the frame. The engine is built for it:

At scale (thousands of machines) Dunky XState Zag
Event throughput (ops/s) 7.2 M 897 K n/a ᡃ
Memory / machine, 2-field (KB) 3.6 3.6 9.1
Memory / machine, 64-field (KB) 4.1 4.1 134

β†’ ~8Γ— XState's throughput, on par with XState for memory but at least 3Γ— lighter than Zag β€” and the gap widens as context grows, because memory stays ~flat in field count (no per-field cell). ᡃ Zag uses async ops, so a synchronous ops/s loop can't time it. Full methodology + per-scenario tables in the benchmark README.

β–Ά Try the live benchmark demo β€” watch all three engines run in your browser.

How it's built

The machine's behavior flows out through a few thin layers until it reaches real elements β€” the left two are agnostic, the right three are per-target:

AGNOSTIC                                                            SUBSTRATE
    🫏 βš™οΈ       -->        🧠        -->       πŸ”Œ         -->       ✨
+--------------+     +--------------+     +--------------+     +--------------+
|   dunky      |     |   machine    |     |   binding    |     |  behavior    |
|              |     |              |     |              |     |              |
|  the engine  |     | states +     |     | neutral wire |     | live feature |
|  that runs   |     | events +     |     | agnostic     |     | on DOM /     |
|  machines    |     | logic        |     | events/attrs |     | TUI / RN     |
+--------------+     +--------------+     +--------------+     +--------------+
   powers              decides              connects             appears
  • core β€” the state-machine engine. Pure behavior: states, transitions, context, effects. Knows nothing about a renderer.
  • connector β€” turns machine state into agnostic bindings and keeps that view in sync as the machine changes.
  • normalize β€” per target, translates those bindings into real props (onPress β†’ onClick on web / a Pressable handler on RN). Always runs.
  • effects β€” per target, the platform listener that the machine can't own itself (a DOM keydown, an RN BackHandler).
  • view β€” the per-target render that spreads the normalized props onto the actual elements.

The full layered model and the "the machine never sees props" rule are in:

Inspiration & prior art

Dunky stands on the shoulders of the amazing libs:

  • XState β€” for the disciplined statechart model: queued run-to-completion transitions, guards, entry/exit, the rigor of treating UI as a state machine in the first place.
  • Zag β€” for proving the headless, framework-agnostic component-as-a-machine approach.

The engine here is an independent implementation β€” its own kernel, its own state-machine runtime β€” built around one bet those libraries aren't: that behavior should run with no environment assumption at all, fast enough to drive thousands of machines at once.

License

MIT Β© Ivan Banov