@typeonce/effect-machine 0.4.0 → 0.5.1
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 +166 -588
- package/dist/Machine.d.ts +426 -240
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +219 -43
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/activities.d.ts +1 -1
- package/dist/internal/machine/activities.js +1 -1
- package/dist/internal/machine/atom.d.ts +1 -1
- package/dist/internal/machine/atom.js +1 -1
- package/dist/internal/machine/cluster.d.ts +2 -2
- package/dist/internal/machine/cluster.js +1 -1
- package/dist/internal/machine/command.d.ts +1 -1
- package/dist/internal/machine/command.js +1 -1
- package/dist/internal/machine/commandRuntime.d.ts +1 -1
- package/dist/internal/machine/commandRuntime.js +1 -1
- package/dist/internal/machine/configuration.d.ts +1 -1
- package/dist/internal/machine/configuration.js +1 -1
- package/dist/internal/machine/errors.d.ts +7 -7
- package/dist/internal/machine/errors.js +7 -7
- package/dist/internal/machine/executionPlan.d.ts +2 -2
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +17 -8
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts +1 -1
- package/dist/internal/machine/invocation.js +1 -1
- package/dist/internal/machine/planner.d.ts +1 -1
- package/dist/internal/machine/planner.js +1 -1
- package/dist/internal/machine/process.d.ts +1 -1
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +76 -24
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/protocol.d.ts +1 -1
- package/dist/internal/machine/protocol.js +1 -1
- package/dist/internal/machine/runtime.d.ts +32 -4
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +120 -12
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/serialization.d.ts +1 -1
- package/dist/internal/machine/serialization.js +1 -1
- package/dist/internal/machine/topology.d.ts +1 -1
- package/dist/internal/machine/topology.js +1 -1
- package/dist/internal/testing/machine/arbitrary.d.ts +3 -3
- package/dist/internal/testing/machine/exploration.d.ts +30 -0
- package/dist/internal/testing/machine/exploration.d.ts.map +1 -0
- package/dist/internal/testing/machine/exploration.js +237 -0
- package/dist/internal/testing/machine/exploration.js.map +1 -0
- package/dist/internal/testing/machine/finiteModel.d.ts +20 -20
- package/dist/internal/testing/machine/finiteModel.js +2 -2
- package/dist/internal/testing/machine/invariant.d.ts +31 -0
- package/dist/internal/testing/machine/invariant.d.ts.map +1 -0
- package/dist/internal/testing/machine/invariant.js +214 -0
- package/dist/internal/testing/machine/invariant.js.map +1 -0
- package/dist/internal/testing/machine/probe.d.ts +26 -0
- package/dist/internal/testing/machine/probe.d.ts.map +1 -0
- package/dist/internal/testing/machine/probe.js +45 -0
- package/dist/internal/testing/machine/probe.js.map +1 -0
- package/dist/internal/testing/machine/referenceModel.d.ts +14 -14
- package/dist/internal/testing/machine/referenceModel.js +2 -2
- package/dist/internal/testing/machine/runtime.d.ts +329 -26
- package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
- package/dist/internal/testing/machine/runtime.js +303 -14
- package/dist/internal/testing/machine/runtime.js.map +1 -1
- package/dist/internal/testing/machine/runtimeInvariant.d.ts +32 -0
- package/dist/internal/testing/machine/runtimeInvariant.d.ts.map +1 -0
- package/dist/internal/testing/machine/runtimeInvariant.js +334 -0
- package/dist/internal/testing/machine/runtimeInvariant.js.map +1 -0
- package/dist/internal/testing/machine/trace.d.ts +17 -0
- package/dist/internal/testing/machine/trace.d.ts.map +1 -0
- package/dist/internal/testing/machine/trace.js +94 -0
- package/dist/internal/testing/machine/trace.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts +9 -6
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +8 -79
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +1029 -47
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +315 -11
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +38 -19
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +27 -9
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +82 -31
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +63 -12
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +160 -18
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -2,51 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
Schema-first state machines and statecharts for Effect.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
State, event, input, output, and persistence boundaries are described with
|
|
6
|
+
Effect Schema. The same definition can be planned synchronously, run as a
|
|
7
|
+
managed machine, mounted as an Atom, tested as a model, or hosted by the
|
|
8
|
+
cluster adapter.
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
> This is early-release software. Its API may change, and each release targets
|
|
11
|
+
> one exact Effect beta.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
9
14
|
|
|
10
15
|
```sh
|
|
11
16
|
pnpm add @typeonce/effect-machine effect@4.0.0-beta.107
|
|
12
17
|
```
|
|
13
18
|
|
|
14
|
-
`effect` is an exact peer dependency
|
|
15
|
-
|
|
16
|
-
require upgrading Effect in lockstep; do not override the peer to another beta.
|
|
17
|
-
|
|
18
|
-
## Entrypoints
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import { Machine } from "@typeonce/effect-machine"
|
|
22
|
-
import { ClusterMachine } from "@typeonce/effect-machine/cluster"
|
|
23
|
-
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Each ESM entrypoint is independent and tree-shakeable. Importing the root does
|
|
27
|
-
not load the reactivity or cluster adapters.
|
|
19
|
+
`effect` is an exact peer dependency. Install the version above and upgrade it
|
|
20
|
+
in lockstep with this package.
|
|
28
21
|
|
|
29
|
-
##
|
|
22
|
+
## Quick start
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
inputs, outputs, and running references. The public/internal event distinction
|
|
33
|
-
has an additional boundary described below.
|
|
34
|
-
|
|
35
|
-
Effect's `Schema.TaggedUnion` is a compact way to declare cases. Its `cases`
|
|
36
|
-
property contains the individual tagged schemas, and each case has a typed
|
|
37
|
-
`make` constructor.
|
|
24
|
+
Define schemas first, derive the state topology, then add behavior:
|
|
38
25
|
|
|
39
26
|
```ts
|
|
40
27
|
import { Machine } from "@typeonce/effect-machine"
|
|
41
|
-
import { Schema } from "effect"
|
|
28
|
+
import { Effect, Schema } from "effect"
|
|
42
29
|
|
|
43
30
|
const State = Schema.TaggedUnion({
|
|
44
31
|
Idle: {},
|
|
45
|
-
Running: {}
|
|
32
|
+
Running: { count: Schema.Number }
|
|
46
33
|
})
|
|
47
34
|
|
|
48
35
|
const Event = Schema.TaggedUnion({
|
|
49
|
-
Start: {}
|
|
36
|
+
Start: {},
|
|
37
|
+
Increment: {},
|
|
38
|
+
Stop: {}
|
|
50
39
|
})
|
|
51
40
|
|
|
52
41
|
const States = Machine.defineStates(State.cases)
|
|
@@ -54,620 +43,240 @@ const States = Machine.defineStates(State.cases)
|
|
|
54
43
|
const Counter = Machine.make({
|
|
55
44
|
id: "Counter",
|
|
56
45
|
states: States.states,
|
|
57
|
-
events: [Event
|
|
46
|
+
events: [Event],
|
|
58
47
|
initial: () => States.initial.Idle.from()
|
|
59
48
|
}).handle({
|
|
60
49
|
Idle: {
|
|
61
50
|
on: {
|
|
62
|
-
Start: ({ target }) => target.full.Running.from()
|
|
51
|
+
Start: ({ target }) => target.full.Running.from({ count: 0 })
|
|
63
52
|
}
|
|
64
53
|
},
|
|
65
|
-
Running: {
|
|
54
|
+
Running: {
|
|
55
|
+
on: {
|
|
56
|
+
Increment: ({ state, target }) => target.full.Running.from({ count: state.count + 1 }),
|
|
57
|
+
Stop: ({ target }) => target.full.Idle.from()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const program = Effect.gen(function*() {
|
|
63
|
+
const ref = yield* Machine.start(Counter)
|
|
64
|
+
yield* ref.send(Machine.event(Counter, Event.cases.Start))
|
|
65
|
+
yield* ref.send(Machine.event(Counter, Event.cases.Increment))
|
|
66
66
|
})
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
`Machine.start` returns a `MachineRef` with `send`, `state`, `snapshot`,
|
|
70
|
+
`changes`, `join`, and `stop`. Sending enqueues an event; observe `changes` or
|
|
71
|
+
use the testing probe when work must be causally acknowledged.
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
initializer receives the decoded input.
|
|
73
|
+
## Modeling workflow
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
`.from` for constructing one safely from the state schema's make input:
|
|
75
|
+
Use this order to preserve inference and keep boundaries explicit:
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
1. Define domain, state, public-event, internal-event, and emitted-event schemas.
|
|
78
|
+
2. Declare topology with `Machine.defineStates`.
|
|
79
|
+
3. Create the protocol and initializer with `Machine.make`.
|
|
80
|
+
4. Implement every active state with `.handle(...)`.
|
|
81
|
+
5. Add runtime, Atom, testing, or cluster adapters at the application boundary.
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
entering a state from fields. Construction runs through the schema's
|
|
86
|
-
`makeEffect` while the machine plans the configuration, so constructor
|
|
87
|
-
defaults and tagged-class identity are preserved and failed refinements become
|
|
88
|
-
`MachineSchemaDecodeError` failures instead of synchronous throws. The same
|
|
89
|
-
form is available on initial, local, branch, full, compound, parallel, and
|
|
90
|
-
final builders. A `.from` builder result is therefore a machine construction
|
|
91
|
-
instruction; it becomes a validated public snapshot when planning succeeds.
|
|
83
|
+
### Construct state through builders
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
required, while compound and parallel states still require their active-child
|
|
95
|
-
callback:
|
|
85
|
+
Use `.from(...)` when constructing a new state from fields:
|
|
96
86
|
|
|
97
87
|
```ts
|
|
98
|
-
|
|
88
|
+
target.local.Saving.from({ draft: event.draft })
|
|
99
89
|
States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
|
|
100
|
-
States.initial.Flow.from((flow) => flow.Idle.from())
|
|
101
90
|
```
|
|
102
91
|
|
|
103
|
-
|
|
104
|
-
identity
|
|
92
|
+
The machine runs these inputs through the state schema while planning. Schema
|
|
93
|
+
defaults, refinements, and tagged-class identity are therefore preserved, and
|
|
94
|
+
decode failures remain typed machine failures. Pass a value directly only when
|
|
95
|
+
it is already decoded, such as a value returned by `Machine.retag`.
|
|
105
96
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
```
|
|
97
|
+
Put data on the narrowest state where it is valid. If sibling phases share
|
|
98
|
+
data, put it on their compound parent.
|
|
109
99
|
|
|
110
|
-
|
|
100
|
+
### Separate public and internal events
|
|
111
101
|
|
|
112
|
-
|
|
113
|
-
|
|
102
|
+
`events` is the public command protocol. Invoke results, timer deliveries,
|
|
103
|
+
raised events, and child emissions belong in `internalEvents`:
|
|
114
104
|
|
|
115
105
|
```ts
|
|
116
|
-
const Command = Schema.TaggedUnion({
|
|
117
|
-
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
const InternalEvent = Schema.TaggedUnion({
|
|
106
|
+
const Command = Schema.TaggedUnion({ Save: {} })
|
|
107
|
+
const Internal = Schema.TaggedUnion({
|
|
121
108
|
Saved: { id: Schema.String },
|
|
122
109
|
SaveFailed: { message: Schema.String }
|
|
123
110
|
})
|
|
124
111
|
|
|
125
112
|
const machine = Machine.make({
|
|
126
113
|
states: States.states,
|
|
127
|
-
events: [Command
|
|
128
|
-
internalEvents: [
|
|
114
|
+
events: [Command],
|
|
115
|
+
internalEvents: [Internal],
|
|
129
116
|
initial: () => States.initial.Idle.from()
|
|
130
117
|
})
|
|
131
118
|
```
|
|
132
119
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
const save = Machine.event(machine, Command.cases.Save)
|
|
138
|
-
yield * ref.send(save)
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The schema constructor runs once and the decoded value is trusted by that
|
|
142
|
-
machine and definitions derived from it with `handle`. This avoids decoding a
|
|
143
|
-
known event again on every delivery. A configured `Schema.TaggedUnion` can use
|
|
144
|
-
either the union schema itself or one of its `cases`. Treat the returned event
|
|
145
|
-
as immutable; sending it to an unrelated machine goes through that machine's
|
|
146
|
-
normal decoder.
|
|
120
|
+
Handlers see both protocols. Typed `send` and `Machine.plan` accept only public
|
|
121
|
+
events. Event tags must be unique and public/internal tags must be disjoint.
|
|
147
122
|
|
|
148
|
-
|
|
123
|
+
Use `Machine.event(machine, schema, fields?)` for reusable machine-owned event
|
|
124
|
+
values. Ordinary objects and schema-constructed values are also accepted and
|
|
125
|
+
decoded at the machine boundary.
|
|
149
126
|
|
|
150
|
-
|
|
151
|
-
yield * ref.send({ _tag: "Save" })
|
|
152
|
-
```
|
|
127
|
+
### Choose the target by scope
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
incoming payloads against the public `events` schemas.
|
|
129
|
+
| Builder | Use when | Preserves |
|
|
130
|
+
| ---------------- | ---------------------------------------- | ------------------------------------------------- |
|
|
131
|
+
| `target.local` | Moving inside the nearest compound scope | Ancestors and unrelated parallel regions |
|
|
132
|
+
| `target.branch` | Moving elsewhere under the active root | Omitted active ancestors and parallel regions |
|
|
133
|
+
| `target.full` | Replacing or selecting a complete root | Nothing implicit for a newly selected root |
|
|
134
|
+
| `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
|
|
161
135
|
|
|
162
|
-
|
|
136
|
+
Builders describe the next logical configuration. Shared states exit and enter
|
|
137
|
+
only when paths change; use `{ reenter: true, transition }` when the source must
|
|
138
|
+
restart even if its path is unchanged.
|
|
163
139
|
|
|
164
|
-
|
|
165
|
-
type PublicCommand = Machine.Machine.InputEvent<typeof machine>
|
|
166
|
-
type HandledEvent = Machine.Machine.Event<typeof machine>
|
|
167
|
-
```
|
|
140
|
+
## Statechart capabilities
|
|
168
141
|
|
|
169
|
-
|
|
170
|
-
disjoint. Reusing a tag is a type error, so a command cannot accidentally
|
|
171
|
-
masquerade as an internal result.
|
|
142
|
+
`Machine.defineStates` supports:
|
|
172
143
|
|
|
173
|
-
|
|
144
|
+
- atomic states;
|
|
145
|
+
- compound states with one active child;
|
|
146
|
+
- parallel states with one active state in every region;
|
|
147
|
+
- final states and typed outputs;
|
|
148
|
+
- transient choice states;
|
|
149
|
+
- shallow and deep history states.
|
|
174
150
|
|
|
175
|
-
|
|
176
|
-
|
|
151
|
+
Declare topology—including finality, output schemas, choices, and history—only
|
|
152
|
+
in `defineStates`. Handlers implement behavior and output computation without
|
|
153
|
+
repeating structural metadata. Final children complete their parent, so
|
|
154
|
+
`onDone` belongs on that compound or parallel parent.
|
|
177
155
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Saving: {},
|
|
183
|
-
Done: {}
|
|
184
|
-
})
|
|
156
|
+
Transition, entry, exit, choice, initial, and history callbacks are
|
|
157
|
+
synchronous. Conditions use ordinary TypeScript control flow. Callbacks may
|
|
158
|
+
select state and enqueue explicit `raise`, `emit`, `sendTo`, or `stop` commands;
|
|
159
|
+
arbitrary asynchronous Effects do not run inside planning.
|
|
185
160
|
|
|
186
|
-
|
|
187
|
-
Form: {
|
|
188
|
-
schema: State.cases.Form,
|
|
189
|
-
initial: "Editing",
|
|
190
|
-
states: {
|
|
191
|
-
Editing: State.cases.Editing,
|
|
192
|
-
Saving: State.cases.Saving,
|
|
193
|
-
Done: {
|
|
194
|
-
schema: State.cases.Done,
|
|
195
|
-
type: "final",
|
|
196
|
-
output: Schema.String
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
```
|
|
161
|
+
## Effects, timers, and child machines
|
|
202
162
|
|
|
203
|
-
|
|
204
|
-
states use `type: "parallel"` and have one active state in every direct region.
|
|
205
|
-
Finality is topology, so declare `type: "final"` only in the state definition.
|
|
206
|
-
Handlers implement behavior and output computation without repeating it:
|
|
163
|
+
State-scoped work starts on entry and is interrupted on exit:
|
|
207
164
|
|
|
208
165
|
```ts
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Done: {
|
|
217
|
-
output: () => "saved"
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
Every declared output schema must have a matching handler implementation before
|
|
225
|
-
the machine can be planned, started, invoked, or adapted to Atom/Cluster.
|
|
226
|
-
Final children complete their parent; put `onDone` on that compound or parallel
|
|
227
|
-
parent, not on the final leaf.
|
|
228
|
-
|
|
229
|
-
Put data on the narrowest state where it is valid. If several sibling phases
|
|
230
|
-
share data, prefer storing it on their compound parent instead of copying it
|
|
231
|
-
into every child state.
|
|
232
|
-
|
|
233
|
-
### Choice states
|
|
234
|
-
|
|
235
|
-
A choice is a transient, targetable decision point. Declare it with only
|
|
236
|
-
`type: "choice"`; it has no schema, value, children, lifecycle actions, invoke,
|
|
237
|
-
or event handlers. A choice is therefore absent from `StateIdentifier`, stable
|
|
238
|
-
snapshots, configurations, and encoded snapshots.
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
const States = Machine.defineStates({
|
|
242
|
-
Flow: {
|
|
243
|
-
schema: State.cases.Flow,
|
|
244
|
-
initial: "Routing",
|
|
245
|
-
states: {
|
|
246
|
-
Routing: { type: "choice" },
|
|
247
|
-
Approved: State.cases.Approved,
|
|
248
|
-
Rejected: State.cases.Rejected
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
const machine = Machine.make({
|
|
254
|
-
states: States.states,
|
|
255
|
-
events: [Event],
|
|
256
|
-
initial: () =>
|
|
257
|
-
States.initial.Flow(
|
|
258
|
-
State.cases.Flow.make({ score: 80 }),
|
|
259
|
-
(flow) => flow.Routing()
|
|
260
|
-
)
|
|
261
|
-
}).handle({
|
|
262
|
-
Flow: {
|
|
263
|
-
states: {
|
|
264
|
-
Routing: {
|
|
265
|
-
choice: {
|
|
266
|
-
targets: ["Flow.Approved", "Flow.Rejected"],
|
|
267
|
-
transition: ({ parent, target }) =>
|
|
268
|
-
parent.score >= 70
|
|
269
|
-
? target.local.Approved(State.cases.Approved.make({}))
|
|
270
|
-
: target.local.Rejected(State.cases.Rejected.make({}))
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
})
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
The resolver is ordinary TypeScript or an `Effect`. It receives the triggering
|
|
279
|
-
lifecycle event, typed parent values, target builders, and the normal planning
|
|
280
|
-
capabilities, but no `state` because the choice itself has no state value. Its
|
|
281
|
-
declared `targets` are both a compile-time bound and inspectable graph edges.
|
|
282
|
-
The resolver must return one of them; missing, malformed, or undeclared targets
|
|
283
|
-
fail planning. Choice implementations are required before execution APIs are
|
|
284
|
-
available.
|
|
285
|
-
|
|
286
|
-
Initial, event, completion, always, history-default, and other choice
|
|
287
|
-
transitions settle in the same macrostep. Chained choices use the normal
|
|
288
|
-
infinite-transition limit. Choice nodes never run entry or exit actions and
|
|
289
|
-
never become active while their resolution remains visible in traces and
|
|
290
|
-
coverage as a `choice` transition trigger.
|
|
291
|
-
|
|
292
|
-
### History states
|
|
293
|
-
|
|
294
|
-
A history pseudo-state remembers the last active configuration of its parent.
|
|
295
|
-
It has no value schema and never appears in an active snapshot. History is
|
|
296
|
-
shallow by default; use `history: "deep"` to retain the complete descendant
|
|
297
|
-
configuration and its validated values:
|
|
298
|
-
|
|
299
|
-
```ts
|
|
300
|
-
const States = Machine.defineStates({
|
|
301
|
-
checkout: {
|
|
302
|
-
schema: Checkout,
|
|
303
|
-
initial: "shipping",
|
|
304
|
-
states: {
|
|
305
|
-
shipping: Shipping,
|
|
306
|
-
payment: {
|
|
307
|
-
schema: Payment,
|
|
308
|
-
initial: "cardEntry",
|
|
309
|
-
states: {
|
|
310
|
-
cardEntry: CardEntry,
|
|
311
|
-
verifying: Verifying
|
|
312
|
-
}
|
|
313
|
-
},
|
|
314
|
-
resume: { type: "history", history: "deep" }
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
support: Support
|
|
318
|
-
})
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
Implement a typed default for the first transition before any configuration
|
|
322
|
-
has been remembered, then target history without supplying a state value:
|
|
323
|
-
|
|
324
|
-
```ts
|
|
325
|
-
machine.handle({
|
|
326
|
-
checkout: {
|
|
327
|
-
history: {
|
|
328
|
-
resume: {
|
|
329
|
-
default: () => initialCheckoutSnapshot
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
},
|
|
333
|
-
support: {
|
|
334
|
-
on: {
|
|
335
|
-
Resume: ({ target }) => target.history.checkout.resume()
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
})
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
A history default is source-independent. It must construct a complete root
|
|
342
|
-
configuration containing the history owner, including every inactive ancestor
|
|
343
|
-
above a nested owner and every required region of a parallel ancestor. For a
|
|
344
|
-
top-level owner, its owner snapshot is already a complete root snapshot.
|
|
345
|
-
|
|
346
|
-
For example, a history node owned by `App.Workspace` can be targeted from an
|
|
347
|
-
unrelated `Closed` root and supplies the complete `App` configuration on first
|
|
348
|
-
use:
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
Workspace: {
|
|
352
|
-
history: {
|
|
353
|
-
resume: {
|
|
354
|
-
default: ({ target }) =>
|
|
355
|
-
target.App(
|
|
356
|
-
State.cases.App.make({ workspaceId: "default" }),
|
|
357
|
-
(app) =>
|
|
358
|
-
app.Workspace(
|
|
359
|
-
State.cases.Workspace.make({}),
|
|
360
|
-
(workspace) =>
|
|
361
|
-
workspace.Editing(State.cases.Editing.make({}))
|
|
362
|
-
)
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
}
|
|
166
|
+
Loading: {
|
|
167
|
+
invoke: Machine.invokeEffect({
|
|
168
|
+
id: "save-document",
|
|
169
|
+
effect: saveDocument,
|
|
170
|
+
onSuccess: (entry) => Internal.cases.Saved.make({ id: entry.id }),
|
|
171
|
+
onFailure: (error) => Internal.cases.SaveFailed.make({ message: String(error) })
|
|
172
|
+
})
|
|
366
173
|
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
The containing branch is enforced statically: unrelated roots, sibling
|
|
370
|
-
compound branches that omit the owner, owner-only nested snapshots, and
|
|
371
|
-
incomplete parallel configurations are rejected.
|
|
372
|
-
|
|
373
|
-
Deep history restores every remembered descendant value. Shallow history
|
|
374
|
-
restores the parent and direct-child values, then follows normal initial paths.
|
|
375
|
-
Only compound or parallel states that shallow restoration can enter implicitly
|
|
376
|
-
need an `initial` handler to construct those new child values:
|
|
377
174
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
175
|
+
Waiting: {
|
|
176
|
+
invoke: Machine.after(
|
|
177
|
+
"3 seconds",
|
|
178
|
+
Internal.cases.SaveFailed.make({ message: "Timed out" })
|
|
179
|
+
)
|
|
382
180
|
}
|
|
383
181
|
```
|
|
384
182
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
Transition between structurally related tagged states with `Machine.retag`.
|
|
390
|
-
The source `_tag` is discarded, compatible fields are reused, and missing or
|
|
391
|
-
incompatible required fields must be supplied:
|
|
392
|
-
|
|
393
|
-
```ts
|
|
394
|
-
const saving = Machine.retag(State.cases.Saving, editing)
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
## Choosing a target builder
|
|
398
|
-
|
|
399
|
-
Transition contexts expose four typed target builders:
|
|
183
|
+
Use `Machine.invokeEffect` for one Effect, `Machine.after` for a cancellable
|
|
184
|
+
delay, and lower-level `Machine.invoke` only for custom process behavior or
|
|
185
|
+
snapshot mapping. Use one exported `Machine.child(id, machine)` descriptor for
|
|
186
|
+
`invokeMachine`, `sendTo`, and child lookup.
|
|
400
187
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
| `target.local` | Inside the source's nearest compound scope | Keeps the compound value, active ancestors, and unrelated parallel regions |
|
|
404
|
-
| `target.branch` | Anywhere under the source's active top-level root | Replaces the selected branch while keeping omitted active ancestor values and parallel regions |
|
|
405
|
-
| `target.full` | Any top-level root | Builds a complete active snapshot for the selected root |
|
|
406
|
-
| `target.history` | A declared history pseudo-state | Restores its parent's remembered configuration or runs its typed default |
|
|
188
|
+
Expected failures should become internal events. An unrecovered invoke or child
|
|
189
|
+
failure terminates the owning runtime.
|
|
407
190
|
|
|
408
|
-
|
|
409
|
-
state, its callback must select every region, just like `initial` and
|
|
410
|
-
`target.full`. When that parallel state is already active, `target.branch`
|
|
411
|
-
can still update one region directly and preserves the other active regions.
|
|
191
|
+
## Reactivity
|
|
412
192
|
|
|
413
|
-
|
|
414
|
-
itself decide which invokes restart. The runtime derives exit and entry paths
|
|
415
|
-
from the previous and next active paths. Shared active ancestors remain entered,
|
|
416
|
-
even when `target.full` supplies their values again. Use an event transition
|
|
417
|
-
with `reenter: true` when the source state should explicitly exit and enter
|
|
418
|
-
again:
|
|
193
|
+
`AtomMachine` runs one lazy machine instance per `AtomRegistry`:
|
|
419
194
|
|
|
420
195
|
```ts
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
transition: ({ state, target }) =>
|
|
424
|
-
target.full.Ready(new Ready({ value: state.value }))
|
|
425
|
-
}
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
`States.get`, `States.getWithParents`, `States.getSnapshot`, and
|
|
429
|
-
`States.matches` accept typed dotted paths. Handler `parents` values are also
|
|
430
|
-
keyed by full dotted paths, such as `parents["Form.Editing"]`; `context.parent`
|
|
431
|
-
provides the immediate parent directly and is `undefined` at a root state.
|
|
432
|
-
|
|
433
|
-
Event, eventless, and completion transition contexts also expose `snapshot`, a
|
|
434
|
-
read-only view of the complete logical configuration captured at the beginning
|
|
435
|
-
of that transition microstep. This lets one parallel region inspect a sibling
|
|
436
|
-
without copying active-state facts into parent values:
|
|
196
|
+
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
197
|
+
import { Atom } from "effect/unstable/reactivity"
|
|
437
198
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
;(({ snapshot, target }) =>
|
|
441
|
-
States.matches(snapshot, "Player.Network.Online")
|
|
442
|
-
? target.local.Playing(State.cases.Playing.make({}))
|
|
443
|
-
: undefined)
|
|
199
|
+
const runtime = Atom.runtime(AppLayer)
|
|
200
|
+
const counterAtom = AtomMachine.bind(runtime).make(Counter)
|
|
444
201
|
```
|
|
445
202
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
later. `snapshot` is intentionally absent from entry, exit,
|
|
449
|
-
invoke, and choice contexts. In particular, startup and chained choices may run
|
|
450
|
-
before a complete stable snapshot containing their pseudo-source exists.
|
|
203
|
+
Binding a shared runtime once is the canonical form for service-backed
|
|
204
|
+
applications. Service-free machines can use `AtomMachine.make(Counter)`.
|
|
451
205
|
|
|
452
|
-
|
|
453
|
-
|
|
206
|
+
The bridge exposes `ref`, `snapshot`, `state`, fail-aware `result`, writable
|
|
207
|
+
`send` and `stop` atoms, and `child(descriptor)`. Use `AtomMachine.select` and
|
|
208
|
+
`AtomMachine.matches` for typed, equality-aware derivations. React applications
|
|
209
|
+
using `@effect/atom-react` need a `RegistryProvider`.
|
|
454
210
|
|
|
455
|
-
|
|
456
|
-
const Saving = State.cases.Saving.annotate({
|
|
457
|
-
title: "Saving document",
|
|
458
|
-
description: "Persisting local changes to the server"
|
|
459
|
-
})
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
Schema-less choice and history nodes accept only descriptive `title`,
|
|
463
|
-
`description`, and `documentation` annotations. Titles may be used as display
|
|
464
|
-
labels, but structural paths remain the only identity and targeting mechanism.
|
|
211
|
+
## Persistence
|
|
465
212
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
Transition, entry, exit, choice, initial, and history callbacks are synchronous.
|
|
469
|
-
They select state and may enqueue only explicit statechart or actor operations:
|
|
470
|
-
raise an internal event, emit to the parent, send to an invoked child, or stop a
|
|
471
|
-
child. Arbitrary Effects are not accepted at this boundary.
|
|
213
|
+
Logical snapshots can be validated for storage or transport:
|
|
472
214
|
|
|
473
215
|
```ts
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return target.local.Saving.from()
|
|
478
|
-
}
|
|
479
|
-
}
|
|
216
|
+
const encoded = yield * Machine.encodeSnapshot(machine, snapshot)
|
|
217
|
+
const decoded = yield * Machine.decodeSnapshot(machine, encoded)
|
|
218
|
+
const ref = yield * Machine.resume(machine, decoded)
|
|
480
219
|
```
|
|
481
220
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
221
|
+
Resumption restores logical state, values, completion, and history metadata.
|
|
222
|
+
It creates a fresh runtime: active invokes restart, timers restart at their
|
|
223
|
+
full duration, and prior fibers, subscriptions, queues, and child runtimes are
|
|
224
|
+
not restored. Store machine identity and migration/version metadata beside the
|
|
225
|
+
encoded snapshot.
|
|
485
226
|
|
|
486
|
-
|
|
487
|
-
`done` is `true`, `output` is the schema-derived structural terminal union;
|
|
488
|
-
while the machine remains active, it is `undefined`. A started machine's
|
|
489
|
-
`join` uses the same terminal union. Output-less structural terminal paths
|
|
490
|
-
contribute `undefined`, while active atomic roots do not.
|
|
227
|
+
## Testing
|
|
491
228
|
|
|
492
|
-
|
|
493
|
-
example, a root `onDone` transition may make one structurally terminal result
|
|
494
|
-
unreachable even though its schema remains in `Machine.TerminalOutput`.
|
|
229
|
+
The testing entrypoint provides complementary layers:
|
|
495
230
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
`
|
|
499
|
-
|
|
500
|
-
|
|
231
|
+
- `MachineTest.run` and `verify` inspect pure planner traces;
|
|
232
|
+
- invariants and generated scenarios check application laws;
|
|
233
|
+
- `explore` performs bounded breadth-first state-space exploration;
|
|
234
|
+
- `probe` causally acknowledges live runtime commands;
|
|
235
|
+
- runtime command models cover timers, invokes, bursts, and scheduling.
|
|
501
236
|
|
|
502
237
|
```ts
|
|
503
|
-
|
|
504
|
-
invoke: ({ state }) =>
|
|
505
|
-
Machine.invokeEffect({
|
|
506
|
-
id: "save",
|
|
507
|
-
effect: save(state),
|
|
508
|
-
onSuccess: (entry) => InternalEvent.cases.Saved.make({ id: entry.id }),
|
|
509
|
-
onFailure: (error) =>
|
|
510
|
-
InternalEvent.cases.SaveFailed.make({
|
|
511
|
-
message: String(error)
|
|
512
|
-
})
|
|
513
|
-
})
|
|
514
|
-
}
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
Omit `onFailure` when the Effect cannot fail. Defects and interruption remain
|
|
518
|
-
failures rather than being mapped.
|
|
519
|
-
|
|
520
|
-
`Machine.after` creates a cancellable, state-scoped delayed event with the same
|
|
521
|
-
lifetime:
|
|
238
|
+
import { MachineTest } from "@typeonce/effect-machine/testing"
|
|
522
239
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
id: "save-timeout"
|
|
240
|
+
const trace = yield* MachineTest.run(Counter, {
|
|
241
|
+
events: [Event.cases.Start.make({}), Event.cases.Increment.make({})]
|
|
526
242
|
})
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
Provide an explicit id when more than one active timer could deliver the same
|
|
530
|
-
event tag.
|
|
531
|
-
|
|
532
|
-
Use lower-level `Machine.invoke` with `Machine.effect` for custom child logic or
|
|
533
|
-
snapshot mapping. Its `id` is only the state-local lifecycle key. If the parent
|
|
534
|
-
must send events to that invocation, create a typed low-level address with
|
|
535
|
-
`Machine.childAddress<Event>("worker")` and pass it through the explicit
|
|
536
|
-
`address` option; the address protocol is checked against the child logic.
|
|
537
|
-
Lifecycle ids must be unique among simultaneously active invokes owned by the
|
|
538
|
-
same state.
|
|
539
|
-
|
|
540
|
-
Invoke outputs, invoke snapshot events, and invoked-child emissions belong in
|
|
541
|
-
`internalEvents`. They are available to typed handlers but are not accepted by
|
|
542
|
-
the typed public input APIs. Include a child machine's emitted protocol with
|
|
543
|
-
`internalEvents: [...ChildMachine.emits]` when those emissions should be handled
|
|
544
|
-
by the parent.
|
|
545
243
|
|
|
546
|
-
|
|
547
|
-
and child lookup:
|
|
548
|
-
|
|
549
|
-
```ts
|
|
550
|
-
const Editor = Machine.child("editor", EditorMachine)
|
|
244
|
+
yield* MachineTest.verify(Counter, trace)
|
|
551
245
|
```
|
|
552
246
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
Descriptors are matched by id and machine identity, so independently created
|
|
556
|
-
descriptors for the same pair address the same child without a global cache.
|
|
557
|
-
Exporting one descriptor remains the clearest module boundary.
|
|
558
|
-
|
|
559
|
-
`Machine.activityDefinitions(machine)` inspects state-owned work without
|
|
560
|
-
executing it. Static descriptors report their source path, lifecycle id, and
|
|
561
|
-
kind. Timers also report normalized duration and emitted event tag;
|
|
562
|
-
`invokeEffect` mappings are described as dynamic; invoked machines expose only
|
|
563
|
-
safe child identity. A function-valued `invoke` factory is reported as dynamic
|
|
564
|
-
and is never evaluated during inspection. The result is serializable and does
|
|
565
|
-
not contain Effects, closures, services, or child runtimes.
|
|
566
|
-
|
|
567
|
-
## Reactivity
|
|
247
|
+
Pure planner tests do not execute invokes or time. Use a started machine and a
|
|
248
|
+
probe when those semantics matter.
|
|
568
249
|
|
|
569
|
-
|
|
570
|
-
`AtomRegistry`. Mounting or reading one of its atoms starts the machine;
|
|
571
|
-
disposing the registry-owned reference stops it.
|
|
250
|
+
## Entrypoints
|
|
572
251
|
|
|
573
252
|
```ts
|
|
253
|
+
import { Machine } from "@typeonce/effect-machine"
|
|
254
|
+
import { ClusterMachine } from "@typeonce/effect-machine/cluster"
|
|
574
255
|
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
575
|
-
import {
|
|
576
|
-
|
|
577
|
-
const runtime = Atom.runtime(AppLayer)
|
|
578
|
-
const machines = AtomMachine.bind(runtime)
|
|
579
|
-
const machineAtom = machines.make(Counter)
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
For applications with a shared runtime, treat
|
|
583
|
-
`AtomMachine.bind(runtime).make(...)` as the canonical form. It keeps runtime
|
|
584
|
-
ownership at the composition boundary so it does not need to be passed through
|
|
585
|
-
every feature. Service-free machines may use `AtomMachine.make(machine)`
|
|
586
|
-
directly.
|
|
587
|
-
|
|
588
|
-
The bridge exposes:
|
|
589
|
-
|
|
590
|
-
- `ref`: the running `MachineRef`
|
|
591
|
-
- `result`: fail-aware logical state, combining startup and post-start runtime
|
|
592
|
-
failures
|
|
593
|
-
- `snapshot`: authoritative runtime lifecycle, including `active`, `done`,
|
|
594
|
-
`error`, and `stopped`
|
|
595
|
-
- `state`: the last logical state, including the retained state after a runtime
|
|
596
|
-
failure
|
|
597
|
-
- `send` and `stop`: writable command atoms
|
|
598
|
-
- `child(descriptor)`: a reactive bridge for a directly owned child
|
|
599
|
-
|
|
600
|
-
Use `AtomMachine.select` and `AtomMachine.matches` for equality-aware root
|
|
601
|
-
derivations. Use `selectChild` and `matchesChild` for child bridges. Selector
|
|
602
|
-
paths and selected value types are inferred directly from the bridge snapshot,
|
|
603
|
-
so these combinators do not need the `DefinedStates` object. They follow normal
|
|
604
|
-
Atom identity semantics and return a new atom on each call, so retain or memoize
|
|
605
|
-
them when constructing them in a component. The `child` method uses Effect's
|
|
606
|
-
`Atom.family` to reuse a live bridge for the same descriptor without maintaining
|
|
607
|
-
a package-level cache.
|
|
608
|
-
`AtomMachine.ChildMachineAtom<typeof Child>` uses `unknown` as its startup-error
|
|
609
|
-
default for general component props.
|
|
610
|
-
`AtomMachine.ChildOf<typeof parentAtom, typeof Child>` preserves the exact
|
|
611
|
-
parent startup-error channel.
|
|
612
|
-
|
|
613
|
-
Child state and snapshot atoms contain `Option.none()` while that child is
|
|
614
|
-
inactive. React applications using `@effect/atom-react` need a
|
|
615
|
-
`RegistryProvider`; see the [Pokémon example](./examples/pokemon).
|
|
616
|
-
|
|
617
|
-
## Snapshots and persistence
|
|
618
|
-
|
|
619
|
-
`Machine.encodeSnapshot` and `Machine.decodeSnapshot` validate logical
|
|
620
|
-
statechart data for storage or transport. The encoded representation does not
|
|
621
|
-
contain the machine definition, machine version, services, subscriptions, or
|
|
622
|
-
running child processes. Store machine identity and migration/version metadata
|
|
623
|
-
alongside it.
|
|
624
|
-
|
|
625
|
-
Resume a decoded logical snapshot explicitly:
|
|
626
|
-
|
|
627
|
-
```ts
|
|
628
|
-
const encoded = yield * Machine.encodeSnapshot(machine, snapshot)
|
|
629
|
-
const decoded = yield * Machine.decodeSnapshot(machine, encoded)
|
|
630
|
-
const ref = yield * Machine.resume(machine, decoded)
|
|
256
|
+
import { MachineTest } from "@typeonce/effect-machine/testing"
|
|
631
257
|
```
|
|
632
258
|
|
|
633
|
-
|
|
634
|
-
transition, completion, eventless, raised-event, or emitted-event work that
|
|
635
|
-
produced the snapshot. The decoded snapshot is the first published logical
|
|
636
|
-
state. A final snapshot immediately yields a completed ref with its output.
|
|
637
|
-
|
|
638
|
-
Resumption creates a fresh runtime. Invokes owned by active states start once in
|
|
639
|
-
normal ancestor/document order and receive `Machine.InitialEvent` as their
|
|
640
|
-
lifecycle event. `invokeEffect` runs again, invoked machines start from their
|
|
641
|
-
own initial state, and `Machine.after` timers restart from their full declared
|
|
642
|
-
duration. Spawned children, queued events, subscriptions, fibers, scopes,
|
|
643
|
-
elapsed timer time, child snapshots, and prior `RuntimeSnapshot` status/errors
|
|
644
|
-
are not restored. Completion and history metadata remain logical state and are
|
|
645
|
-
not replayed. A changed machine definition does not cause `resume` itself to
|
|
646
|
-
evaluate newly enabled `always` or `onDone` transitions.
|
|
647
|
-
|
|
648
|
-
Reactive applications use `AtomMachine.resume(machine, decoded)` for a
|
|
649
|
-
service-free machine or `AtomMachine.bind(runtime).resume(machine, decoded)`
|
|
650
|
-
for a service-backed machine. These bridges have the same lazy one-runtime-per-
|
|
651
|
-
registry ownership and disposal behavior as `AtomMachine.make`.
|
|
259
|
+
Each ESM entrypoint is independent and tree-shakeable.
|
|
652
260
|
|
|
653
|
-
|
|
654
|
-
restrictions, checkpoint planning, and delivery guarantees are documented on
|
|
655
|
-
that API. `Machine.resume` is logical resumption, not durable process or cluster
|
|
656
|
-
restoration.
|
|
261
|
+
## Examples
|
|
657
262
|
|
|
658
|
-
|
|
263
|
+
Every package directly under [`examples/`](./examples) has its own lockfile and
|
|
264
|
+
`check` script.
|
|
659
265
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
state-scoped
|
|
266
|
+
| Example | What it demonstrates |
|
|
267
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
268
|
+
| [Playground](./examples/playground) | Five focused React examples: atomic turnstile commands, state-scoped traffic-light timers, microwave safety across parallel regions, a service-backed media player, and a worker-hosted machine synchronized across tabs |
|
|
269
|
+
| [Pokémon](./examples/pokemon) | Compound and parallel states, invoked child machines, typed emissions, Atom reactivity, and a live Effect service |
|
|
270
|
+
| [Platformer](./examples/platformer) | Nested parallel statecharts, typed deep history, raised events, state-scoped timers, deterministic model tests, and a playable SVG adapter |
|
|
663
271
|
|
|
664
|
-
|
|
272
|
+
The playground is the shortest path from one concept to working code. The
|
|
273
|
+
standalone examples show larger composition and ownership boundaries.
|
|
665
274
|
|
|
666
|
-
|
|
667
|
-
definition order, modeling rules, lifecycle invariants, React recipe, common
|
|
668
|
-
compiler errors, and unsupported features.
|
|
275
|
+
## Reference and development
|
|
669
276
|
|
|
670
|
-
|
|
277
|
+
- [API reference](https://effect-machine.typeonce.dev)
|
|
278
|
+
- [Agent and implementation guide](./docs/agent-guide.md)
|
|
279
|
+
- [Contributing guide](./CONTRIBUTING.md)
|
|
671
280
|
|
|
672
281
|
Use pnpm 10 and Node.js 20 or newer:
|
|
673
282
|
|
|
@@ -676,40 +285,9 @@ pnpm install --frozen-lockfile
|
|
|
676
285
|
pnpm check
|
|
677
286
|
```
|
|
678
287
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
packs the package, imports all public entrypoints, and compiles a strict
|
|
683
|
-
TypeScript consumer with `skipLibCheck: false`.
|
|
684
|
-
|
|
685
|
-
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before proposing a change. Pull
|
|
686
|
-
requests receive an automated base-versus-head type-instantiation report.
|
|
687
|
-
|
|
688
|
-
## Examples
|
|
689
|
-
|
|
690
|
-
The [platformer statechart example](./examples/platformer) is a playable SVG
|
|
691
|
-
demo centered on a schema-first character machine. It demonstrates nested
|
|
692
|
-
compound locomotion, parallel airborne motion and air-jump regions, independent
|
|
693
|
-
facing and wall-contact regions, a pause/resume flow backed by typed deep
|
|
694
|
-
history, typed protocol events, state-scoped timers, and state-driven SVG
|
|
695
|
-
transforms.
|
|
696
|
-
|
|
697
|
-
The [Pokémon statechart example](./examples/pokemon) is a standalone React and
|
|
698
|
-
Vite project demonstrating compound and parallel states, state-scoped invokes,
|
|
699
|
-
invoked child statecharts, typed emissions, and Atom reactivity. It uses a local
|
|
700
|
-
`file:` dependency on this package while retaining an isolated dependency graph,
|
|
701
|
-
lockfile, build, and CI job.
|
|
702
|
-
|
|
703
|
-
The [playground](./examples/playground) collects focused interactive examples
|
|
704
|
-
for traffic lights, turnstiles, media players, microwaves, and worker-backed
|
|
705
|
-
machines. CI discovers every direct package under `examples/` and runs its
|
|
706
|
-
`check` script automatically.
|
|
707
|
-
|
|
708
|
-
## Releases
|
|
709
|
-
|
|
710
|
-
Add a changeset with `pnpm changeset`. CI validates frozen installation and the
|
|
711
|
-
complete check suite. The release workflow opens version PRs and publishes with
|
|
712
|
-
npm provenance through GitHub Actions.
|
|
288
|
+
Declarative first-class guards are not currently part of the API; use ordinary
|
|
289
|
+
TypeScript conditions. Pull requests that change `src/` or `package.json` need
|
|
290
|
+
a changeset and the performance checks described in `AGENTS.md`.
|
|
713
291
|
|
|
714
292
|
When equivalent Machine modules ship in Effect, this package is intended to
|
|
715
|
-
become a
|
|
293
|
+
become a compatibility re-export before eventual retirement.
|