effect-machine 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -189,6 +189,53 @@ expect(result.states.map((s) => s._tag)).toEqual(["ReviewingCart", "ChargingCard
189
189
 
190
190
  `simulate` and `createTestHarness` test transition logic. They do not run `.spawn()` or `.background()` effects.
191
191
 
192
+ ## React, Solid, and Vue
193
+
194
+ Use the Effect Atom adapter to connect one actor to any Effect Atom framework binding. The actor remains the state owner. Atom writes send typed events.
195
+
196
+ ```ts
197
+ import { useAtomSet, useAtomValue } from "@effect/atom-react";
198
+ import * as ActorAtom from "effect-machine/atom";
199
+
200
+ const checkoutAtom = ActorAtom.make(actor);
201
+
202
+ function CheckoutTotal() {
203
+ const totalCents = useAtomValue(checkoutAtom, (state) =>
204
+ "totalCents" in state ? state.totalCents : 0,
205
+ );
206
+ const send = useAtomSet(checkoutAtom);
207
+
208
+ return <button onClick={() => send(CheckoutEvent.Submit)}>Pay {totalCents}</button>;
209
+ }
210
+ ```
211
+
212
+ `useAtomValue` subscribes to the selected value. It does not render again when another state field changes. Use `ActorAtom.select` when you need a reusable selector or a custom equality function:
213
+
214
+ ```ts
215
+ const totalAtom = ActorAtom.select(
216
+ checkoutAtom,
217
+ (state) => ("totalCents" in state ? { cents: state.totalCents } : { cents: 0 }),
218
+ (value, next) => value.cents === next.cents,
219
+ );
220
+ ```
221
+
222
+ The selected Atom stays writable. `useAtomSet(totalAtom)` still sends checkout events. Solid uses the same Atom with `@effect/atom-solid`. Vue uses the same Atom with `@effect/atom-vue`.
223
+
224
+ Runnable React and Solid package examples are in `examples/react` and `examples/solid`.
225
+
226
+ ```bash
227
+ bun run example:react
228
+ bun run example:solid
229
+ ```
230
+
231
+ Each example creates the actor through `Atom.make(Machine.scoped(...))`. React reads it with `useAtomSuspense`. Solid reads it with `useAtomResource` inside a Suspense boundary. The Atom scope owns actor cleanup.
232
+
233
+ Each example has a framework performance test. The React test counts component renders. The Solid test counts reactive computations. Both tests prove that an unrelated selector does not update.
234
+
235
+ The React example uses Motion `AnimatePresence`. The Solid example uses `solid-transition-group`. Both examples keep the old screen mounted for a 200 ms exit after the machine enters `Done`.
236
+
237
+ The terminal `Done` state keeps the displayed `count` and `label`. The old screen stays valid without a local retained-value hook. Keep exit data in the terminal state or handle the terminal variant in the selector. Do not cache the last non-null selector value.
238
+
192
239
  ## Cluster
193
240
 
194
241
  When the same machine needs to run behind `@effect/cluster`, turn it into an entity:
package/dist/atom.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { ActorRef } from "./actor.js";
2
+ import * as Atom from "effect/unstable/reactivity/Atom";
3
+ //#region src/atom.d.ts
4
+ /**
5
+ * A writable Atom projection of an actor.
6
+ *
7
+ * The Atom value is the current actor state. Atom writes send actor events.
8
+ */
9
+ type ActorAtom<State, Event> = Atom.Writable<State, Event>;
10
+ /**
11
+ * Make a writable Atom from an actor.
12
+ *
13
+ * The actor stays the single state owner. The Atom follows recovery,
14
+ * supervision restarts, and normal transitions through the actor's
15
+ * SubscriptionRef.
16
+ */
17
+ declare const make: <State extends {
18
+ readonly _tag: string;
19
+ }, Event>(actor: ActorRef<State, Event>) => ActorAtom<State, Event>;
20
+ /**
21
+ * Select part of an actor state.
22
+ *
23
+ * The selected Atom stays writable. Writes still send events to the actor.
24
+ * The equality function controls when Atom subscribers receive a new value.
25
+ */
26
+ declare const select: {
27
+ <State, Selection>(selector: (state: State) => Selection, equals?: (value: Selection, next: Selection) => boolean): <Event>(self: ActorAtom<State, Event>) => ActorAtom<Selection, Event>;
28
+ <State, Event, Selection>(self: ActorAtom<State, Event>, selector: (state: State) => Selection, equals?: (value: Selection, next: Selection) => boolean): ActorAtom<Selection, Event>;
29
+ };
30
+ //#endregion
31
+ export { ActorAtom, make, select };
package/dist/atom.js ADDED
@@ -0,0 +1,29 @@
1
+ import { dual } from "effect/Function";
2
+ import * as Atom from "effect/unstable/reactivity/Atom";
3
+ //#region src/atom.ts
4
+ /**
5
+ * Effect Atom integration for actors.
6
+ *
7
+ * The adapter keeps the actor as the state owner. Atom registries observe the
8
+ * actor's SubscriptionRef and write events through its synchronous boundary.
9
+ */
10
+ /**
11
+ * Make a writable Atom from an actor.
12
+ *
13
+ * The actor stays the single state owner. The Atom follows recovery,
14
+ * supervision restarts, and normal transitions through the actor's
15
+ * SubscriptionRef.
16
+ */
17
+ const make = (actor) => {
18
+ const state = Atom.subscriptionRef(actor.state);
19
+ return Atom.writable((get) => get(state), (_ctx, event) => actor.sync.send(event));
20
+ };
21
+ /**
22
+ * Select part of an actor state.
23
+ *
24
+ * The selected Atom stays writable. Writes still send events to the actor.
25
+ * The equality function controls when Atom subscribers receive a new value.
26
+ */
27
+ const select = dual((args) => Atom.isAtom(args[0]), (self, selector, equals = Object.is) => Atom.withEquality(Atom.map(self, selector), equals));
28
+ //#endregion
29
+ export { make, select };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"
@@ -21,6 +21,12 @@
21
21
  "types": "./dist/cluster/index.d.ts",
22
22
  "default": "./dist/cluster/index.js"
23
23
  }
24
+ },
25
+ "./atom": {
26
+ "import": {
27
+ "types": "./dist/atom.d.ts",
28
+ "default": "./dist/atom.js"
29
+ }
24
30
  }
25
31
  },
26
32
  "publishConfig": {
@@ -34,8 +40,11 @@
34
40
  "fmt:check": "oxfmt --check",
35
41
  "test": "bun test --tsconfig-override=tsconfig.json",
36
42
  "test:watch": "bun test --watch",
43
+ "example:react": "bun run --cwd examples/react dev",
44
+ "example:solid": "bun run --cwd examples/solid dev",
45
+ "examples:gate": "bun run --cwd examples/react gate && bun run --cwd examples/solid gate",
37
46
  "bench": "bun benchmark/core.ts",
38
- "gate": "concurrently -n type,lint,fmt,test,build -c blue,yellow,magenta,green,cyan \"bun run typecheck\" \"bun run lint:fix\" \"bun run fmt\" \"bun run test\" \"bun run build\"",
47
+ "gate": "concurrently -n type,lint,fmt,test,build,examples -c blue,yellow,magenta,green,cyan,white \"bun run typecheck\" \"bun run lint:fix\" \"bun run fmt\" \"bun run test\" \"bun run build\" \"bun run examples:gate\"",
39
48
  "prepare": "lefthook install && effect-tsgo patch",
40
49
  "version": "changeset version",
41
50
  "build": "bun --bun tsdown",
@@ -44,17 +53,33 @@
44
53
  "devDependencies": {
45
54
  "@changesets/changelog-github": "1.0.0",
46
55
  "@changesets/cli": "3.0.1",
56
+ "@effect/atom-react": "4.0.0-rc.112",
57
+ "@effect/atom-solid": "4.0.0-rc.112",
47
58
  "@effect/tsgo": "0.38.0",
59
+ "@solidjs/testing-library": "0.8.10",
60
+ "@testing-library/react": "16.3.3",
48
61
  "@types/bun": "1.4.0",
62
+ "@types/react": "19.2.18",
63
+ "@types/react-dom": "19.2.5",
64
+ "@vitejs/plugin-react": "6.1.1",
49
65
  "concurrently": "10.0.5",
50
66
  "effect": "4.0.0-rc.112",
51
67
  "effect-bun-test": "0.3.0",
68
+ "jsdom": "30.0.1",
52
69
  "lefthook": "2.1.12",
70
+ "motion": "13.1.1",
53
71
  "oxfmt": "0.65.0",
54
72
  "oxlint": "1.80.0",
55
73
  "oxlint-plugin-effect": "0.11.0",
74
+ "react": "19.2.8",
75
+ "react-dom": "19.2.8",
76
+ "solid-js": "1.9.15",
77
+ "solid-transition-group": "0.3.0",
56
78
  "tsdown": "0.22.14",
57
- "typescript": "7.0.2"
79
+ "typescript": "7.0.2",
80
+ "vite": "8.2.2",
81
+ "vite-plugin-solid": "2.11.14",
82
+ "vitest": "4.1.11"
58
83
  },
59
84
  "peerDependencies": {
60
85
  "effect": ">=4.0.0-rc.112 <5"