Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/ci-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/ci-setup
# The sandbox machines written in Rust (crates/sandbox -> wasm).
# The sandbox machines written in Rust (sandbox/rust -> wasm).
- uses: ./.github/actions/wasm-setup
- run: pnpm test:ci

Expand All @@ -43,7 +43,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/ci-setup
# The sandbox machines written in Rust (crates/sandbox -> wasm).
# The sandbox machines written in Rust (sandbox/rust -> wasm).
- uses: ./.github/actions/wasm-setup
- run: pnpm typecheck

Expand Down Expand Up @@ -77,17 +77,17 @@ jobs:
with:
workspaces: |
. -> target
crates/uniffi -> target
crates/jsi -> target
- run: cargo fmt --all --check
- run: cargo lint
- run: cargo test --workspace
# Again in release: overflow checks and debug assertions are off there.
- run: cargo test --workspace --release
# crates/uniffi (React Native over JSI) sits outside the workspace with its own
# crates/jsi (React Native over JSI) sits outside the workspace with its own
# lockfile; its checks run on the host and need no mobile toolchain.
- run: cargo fmt --manifest-path crates/uniffi/Cargo.toml --check
- run: cargo clippy --manifest-path crates/uniffi/Cargo.toml --all-targets -- -D warnings
- run: cargo test --manifest-path crates/uniffi/Cargo.toml
- run: cargo fmt --manifest-path crates/jsi/Cargo.toml --check
- run: cargo clippy --manifest-path crates/jsi/Cargo.toml --all-targets -- -D warnings
- run: cargo test --manifest-path crates/jsi/Cargo.toml

versioning:
# The release PR consumes changesets rather than adding one — never flag it.
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dist
# Rust
/target
# wasm-bindgen output (built by `pnpm build:wasm`)
crates/sandbox/pkg
sandbox/rust/pkg

# Expo prebuild output (`expo prebuild` / `expo run:*` in the RN sandbox)
sandbox/native/ios
Expand Down
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@
}
},
"globals": {},
"ignorePatterns": ["**/dist/**", "website/src/snippets/**", "crates/sandbox/pkg/**"]
"ignorePatterns": ["**/dist/**", "website/src/snippets/**", "sandbox/rust/pkg/**"]
}
14 changes: 7 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ editing files in that scope — it overrides anything here for that scope

## Scopes

| Scope | Path | What it is |
| --------- | ------------- | -------------------------------------------------------------------------------------------------------------------- |
| Packages | `packages/**` | The core machine, substrate targets (react, solid, native, opentui), and shared internals |
| Benchmark | `benchmark/` | Perf suites: `ts/` (the TS engine against competitor libraries) and `rust/` (the Rust engine) |
| Sandbox | `sandbox/` | Per-substrate demo apps for manual verification |
| Website | `website/` | The docs site |
| Crates | `crates/**` | The Rust engine (`core`, `macros`), its JS binding (`wasm`), the sandbox machines in Rust, the RN binding (`uniffi`) |
| Scope | Path | What it is |
| --------- | ------------- | ------------------------------------------------------------------------------------------------------------------ |
| Packages | `packages/**` | The core machine, substrate targets (react, solid, native, opentui), and shared internals |
| Benchmark | `benchmark/` | Perf suites: `ts/` (the TS engine against competitor libraries) and `rust/` (the Rust engine) |
| Sandbox | `sandbox/` | Per-substrate demo apps for manual verification, the shared TS machines (`shared/`) and their Rust twins (`rust/`) |
| Website | `website/` | The docs site |
| Crates | `crates/**` | The Rust engine (`core`, with its proc macros in `core/macros`) and its bridges (`wasm`, `jsi`) |

Some changes are cross-scope: a change in `core/` may need follow-up in
the targets, sandboxes, and docs — and vice versa. Check what else your
Expand Down
49 changes: 27 additions & 22 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,19 @@ Zag, whose machines read props directly.)

## Project structure

| File / location | What it owns |
| --------------------------- | ----------------------------------------------------------------- |
| `packages/core/` | State-machine engine (plain-mutation kernel) |
| `packages/shared/bindings/` | Substrate-agnostic event + attr vocabulary (onPress, role, …) |
| `packages/shared/utils/` | mergeProps, composeHandlers |
| `packages/<target>/` | Hook + normalize per substrate (react, solid, native, opentui, …) |
| `crates/core/` | The Rust engine: the same spec, in Rust (`dunky-state-machine`) |
| `crates/macros/` | `#[derive(State, Event, Context)]` for Rust machines |
| `crates/wasm/` | Rust machines as JS classes (`export_machine!`) |
| `packages/wasm/` | `fromWasm`: a Rust machine behind the TS `Machine` interface |
| `crates/sandbox/` | The sandbox machines in Rust, twins of `sandbox/shared` ones |
| `crates/uniffi/` | Rust machines for React Native over JSI (uniffi; facade planned) |
| `sandbox/uniffi/` | The turbo module uniffi-bindgen-react-native generates from it |
| File / location | What it owns |
| --------------------------- | ---------------------------------------------------------------------------------- |
| `packages/core/` | State-machine engine (plain-mutation kernel) |
| `packages/shared/bindings/` | Substrate-agnostic event + attr vocabulary (onPress, role, …) |
| `packages/shared/utils/` | mergeProps, composeHandlers |
| `packages/<target>/` | Hook + normalize per substrate (react, solid, native, opentui, …) |
| `crates/core/` | The Rust engine: the same spec, in Rust (`dunky-state-machine`) |
| `crates/core/macros/` | `#[derive(State, Event, Context)]`; its own crate only because proc macros must be |
| `crates/wasm/` | Bridge: Rust machines as JS classes (`export_machine!`), for JS runtimes with wasm |
| `packages/wasm/` | `fromWasm`: a Rust machine behind the TS `Machine` interface |
| `sandbox/rust/` | The sandbox machines in Rust, twins of `sandbox/shared` ones |
| `crates/jsi/` | Bridge: Rust machines for React Native over JSI (uniffi today; facade planned) |
| `sandbox/jsi/` | The turbo module uniffi-bindgen-react-native generates from it |

## The map

Expand Down Expand Up @@ -159,8 +159,8 @@ This is the target shape of the repo; the boxes marked _planned_ do not exist ye
v v
+---------------------------+ +---------------------------+
| TS engine | | Rust engine |
| packages/core | | crates/core, crates/macros|
| @dunky.dev/state-machine | | dunky-state-machine |
| packages/core | | crates/core (+ its macros)|
| @dunky.dev/state-machine | | dunky-state-machine |
+---------------------------+ +---------------------------+
| |
| authors | authors
Expand All @@ -174,9 +174,9 @@ This is the target shape of the repo; the boxes marked _planned_ do not exist ye
| web: wasm32 | | native: lib | Rust app
| v v |
| +----------------+ +----------------+ |
| | crates/wasm | | crates/uniffi | |
| | crates/wasm | | crates/jsi | |
| | export_machine!| | push protocol | |
| | wasm-bindgen | | ubrn turbo mod | |
| | wasm-bindgen | | uniffi today | |
| +----------------+ +----------------+ |
| | | |
| notify(state, fields), timers, field reads |
Expand Down Expand Up @@ -226,19 +226,24 @@ How to read it:
- **A machine is authored against one engine, with its connector next to it.** TS
machines against the TS engine; Rust machines against the Rust engine, which is the
only place they run. The sandbox palette exists in both (`sandbox/shared`,
`crates/sandbox`), as a worked example of the twin rule.
`sandbox/rust`), as a worked example of the twin rule.
- **The TS engine is the default in every JS runtime**, React Native included. A
machine is written in Rust when it is wanted in Rust too (a Rust app) or shared
across the web, React Native and Rust from one source.
- **The facades put a Rust machine behind the TS `Machine` interface.** They ship no
engine: they map names to numbers, keep a JS mirror of the context, re-read only the
fields each notify names, and fan changes out to subscribers. `packages/wasm`
(`fromWasm`) does it over `crates/wasm`; the React Native facade does the same over
`crates/uniffi`, which speaks the same push protocol (one `notify` per change, timers
`crates/jsi`, which speaks the same push protocol (one `notify` per change, timers
as host calls).
- **One binding per substrate.** `crates/wasm` for JS runtimes with wasm (web, Node,
Bun, so OpenTUI too). `crates/uniffi` for React Native over JSI, generated into a
turbo module by uniffi-bindgen-react-native (`sandbox/uniffi`).
- **One bridge per substrate, one protocol.** `crates/wasm` for JS runtimes with wasm
(web, Node, Bun, so OpenTUI too). `crates/jsi` for React Native over JSI: today a
uniffi crate, generated into a turbo module by uniffi-bindgen-react-native
(`sandbox/jsi`). The contract is the push protocol (`crates/wasm/SPEC.md`), not the
tool: a bridge built another way sits behind the same facade. "Bridges" cross a
runtime boundary; "bindings" (`packages/shared/bindings`, and its Rust twin to come)
are the a11y and event vocabulary targets consume. The two words are kept apart on
purpose.
- **Targets are engine-blind.** They take a `Machine` through `toMachine` and
`connect`, so react, solid, native and opentui work unchanged whichever engine built
it.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The Rust engine (`crates/`) is a plain Cargo workspace; run cargo at the repo ro
| `cargo fmt` / `--check` | `rustfmt` |
| `cargo bench -p dunky-benchmark` | The Rust engine's benchmark (`benchmark/rust`) |

`pnpm build:wasm` builds the sandbox machines written in Rust to wasm (`crates/sandbox/pkg`),
`pnpm build:wasm` builds the sandbox machines written in Rust to wasm (`sandbox/rust/pkg`),
with their TS types: the sandbox's `?machine=rust` mode and its Rust tests need it. It
needs the `wasm32-unknown-unknown` target and `wasm-bindgen-cli` at the version
`Cargo.toml` pins.
Expand Down
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
# `cargo test --workspace` for the others.
[workspace]
resolver = "3"
members = ["crates/core", "crates/macros", "crates/wasm", "crates/sandbox", "benchmark/rust"]
# Built by its own pipeline (uniffi-bindgen-react-native) for iOS/Android.
exclude = ["crates/uniffi"]
members = ["crates/core", "crates/core/macros", "crates/wasm", "sandbox/rust", "benchmark/rust"]
# The React Native bridge: built by its own pipeline (uniffi-bindgen-react-native today).
exclude = ["crates/jsi"]

[workspace.package]
version = "0.1.0"
Expand All @@ -17,9 +17,9 @@ rust-version = "1.85"

[workspace.dependencies]
dunky-state-machine = { path = "crates/core" }
dunky-state-machine-macros = { path = "crates/macros" }
dunky-state-machine-macros = { path = "crates/core/macros" }
dunky-state-machine-wasm = { path = "crates/wasm" }
dunky-sandbox = { path = "crates/sandbox" }
dunky-sandbox = { path = "sandbox/rust" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Must match the installed wasm-bindgen CLI exactly (wasm-bindgen --version).
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ every target takes it like any other machine.
| |
| +---------+---------+
| | |
| wasm, uniffi Rust apps
| wasm, jsi Rust apps
| v (iced, planned)
| fromWasm: the TS Machine interface
| over a Rust machine (packages/wasm)
Expand Down Expand Up @@ -94,7 +94,7 @@ elements — the left two are agnostic, the right three are per-target:
([`packages/core`](./packages/core)) and in Rust ([`crates/core`](./crates/core)),
two engines on one spec. A machine written in Rust reaches TypeScript through
wasm ([`crates/wasm`](./crates/wasm) + [`packages/wasm`](./packages/wasm)) and
React Native through uniffi ([`crates/uniffi`](./crates/uniffi), in progress).
React Native over JSI ([`crates/jsi`](./crates/jsi), uniffi today, in progress).
- **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
Expand Down
8 changes: 4 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ ran on the Rust engine, is on the local branch `rust-engine-under-ts` (head `4e4
## Rust machines on React Native (JSI)

Decided on 2026-10-09. The TS engine stays the default on React Native. A machine
written in Rust (e.g. a dropdown) is consumed as wasm on the web and over uniffi on React
written in Rust (e.g. a dropdown) is consumed as wasm on the web and over JSI on React
Native, behind a facade that gives it the TS `Machine` interface, as `fromWasm` does.

Spiked in PR #76: `crates/uniffi` runs in the Expo sandbox through
uniffi-bindgen-react-native (`sandbox/uniffi`, a turbo module Expo autolinks into a dev
Spiked in PR #76: `crates/jsi` runs in the Expo sandbox through
uniffi-bindgen-react-native (`sandbox/jsi`, a turbo module Expo autolinks into a dev
build), in push mode (`attach(host)`: a `notify(state, lo, hi)` per change and timers as
host calls, as `crates/wasm` speaks) next to the pull mode. The sandbox app times both
against the TS engine (`sandbox/native/rust-bench.tsx`). Hermes, Release build, iPhone 17
Expand All @@ -30,7 +30,7 @@ simulator, per event:
dropdown sends a handful. Too slow for a hot path (many machines updated per frame,
or field reads on every render): the facade must keep the hot path on the JS side.

- [ ] The RN facade over `crates/uniffi` (`fromUniffi`, the twin of `fromWasm`): push
- [ ] The RN facade over `crates/jsi` (`fromNative`, the twin of `fromWasm`): push
mode, a JS context mirror re-read only on the fields a notify names, renders read
the mirror. Reuse `RustMachine` from `packages/wasm`; only the handle differs.
- [ ] Typed payloads instead of JSON strings: uniffi records and enums as arguments
Expand Down
2 changes: 1 addition & 1 deletion crates/core/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ reporting.

- **No IO, no clock, no platform.** The engine never reads time and never schedules work.
A host does.
- **No JS.** Bindings live in their own crates (`crates/uniffi` for React Native).
- **No JS.** Bindings live in their own crates (`crates/jsi` for React Native).
- **One thread.** A machine and what it shares stay on one thread. A binding that crosses
threads enforces that itself.
- **At most 64 context fields** per machine: each field is one bit of the change mask.
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
18 changes: 9 additions & 9 deletions crates/uniffi/Cargo.lock → crates/jsi/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions crates/uniffi/Cargo.toml → crates/jsi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
[workspace]

[package]
name = "dunky-state-machine-uniffi"
name = "dunky-state-machine-jsi"
version = "0.1.0"
edition = "2024"
license = "MIT"
repository = "https://github.com/dunky-dev/state-machine"
publish = false

[lib]
name = "dunky_state_machine_uniffi"
name = "dunky_state_machine_jsi"
# staticlib: what ubrn links into the iOS xcframework and the Android turbo-module.
# cdylib: what ubrn's `jsi2` flavour loads instead, keeping that path open.
crate-type = ["cdylib", "staticlib"]

[dependencies]
dunky-state-machine = { path = "../core" }
dunky-sandbox = { path = "../sandbox" }
dunky-sandbox = { path = "../../sandbox/rust" }
serde_json = "1"
# Must stay on the uniffi minor that uniffi-bindgen-react-native is built with
# (ubrn 0.31.0-6 -> uniffi 0.31): the generated C++/TS check the contract version.
Expand Down
File renamed without changes.
34 changes: 17 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading