@typeonce/effect-machine 0.2.0 → 0.4.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 +345 -42
- package/dist/Machine.d.ts +1273 -347
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +161 -426
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/activities.d.ts +57 -0
- package/dist/internal/machine/activities.d.ts.map +1 -0
- package/dist/internal/machine/activities.js +49 -0
- package/dist/internal/machine/activities.js.map +1 -0
- package/dist/internal/machine/atom.d.ts +69 -0
- package/dist/internal/machine/atom.d.ts.map +1 -0
- package/dist/{AtomMachine.js → internal/machine/atom.js} +19 -94
- package/dist/internal/machine/atom.js.map +1 -0
- package/dist/internal/machine/cluster.d.ts +63 -0
- package/dist/internal/machine/cluster.d.ts.map +1 -0
- package/dist/{ClusterMachine.js → internal/machine/cluster.js} +15 -119
- package/dist/internal/machine/cluster.js.map +1 -0
- package/dist/internal/machine/command.d.ts +16 -0
- package/dist/internal/machine/command.d.ts.map +1 -0
- package/dist/internal/machine/command.js +45 -0
- package/dist/internal/machine/command.js.map +1 -0
- package/dist/internal/machine/commandRuntime.d.ts +13 -0
- package/dist/internal/machine/commandRuntime.d.ts.map +1 -0
- package/dist/internal/machine/commandRuntime.js +16 -0
- package/dist/internal/machine/commandRuntime.js.map +1 -0
- package/dist/internal/{machineModel.d.ts → machine/configuration.d.ts} +32 -38
- package/dist/internal/machine/configuration.d.ts.map +1 -0
- package/dist/internal/machine/configuration.js +965 -0
- package/dist/internal/machine/configuration.js.map +1 -0
- package/dist/internal/{machineErrors.d.ts → machine/errors.d.ts} +3 -3
- package/dist/internal/machine/errors.d.ts.map +1 -0
- package/dist/internal/{machineErrors.js → machine/errors.js} +1 -1
- package/dist/internal/machine/errors.js.map +1 -0
- package/dist/internal/machine/executionPlan.d.ts +49 -0
- package/dist/internal/machine/executionPlan.d.ts.map +1 -0
- package/dist/internal/machine/executionPlan.js +667 -0
- package/dist/internal/machine/executionPlan.js.map +1 -0
- package/dist/internal/machine/invocation.d.ts +23 -0
- package/dist/internal/machine/invocation.d.ts.map +1 -0
- package/dist/internal/machine/invocation.js +77 -0
- package/dist/internal/machine/invocation.js.map +1 -0
- package/dist/internal/machine/machine.d.ts +183 -0
- package/dist/internal/machine/machine.d.ts.map +1 -0
- package/dist/internal/machine/machine.js +552 -0
- package/dist/internal/machine/machine.js.map +1 -0
- package/dist/internal/machine/planner.d.ts +182 -0
- package/dist/internal/machine/planner.d.ts.map +1 -0
- package/dist/internal/machine/planner.js +1082 -0
- package/dist/internal/machine/planner.js.map +1 -0
- package/dist/internal/{machineProcess.d.ts → machine/process.d.ts} +10 -4
- package/dist/internal/machine/process.d.ts.map +1 -0
- package/dist/internal/machine/process.js +446 -0
- package/dist/internal/machine/process.js.map +1 -0
- package/dist/internal/machine/protocol.d.ts +34 -0
- package/dist/internal/machine/protocol.d.ts.map +1 -0
- package/dist/internal/machine/protocol.js +182 -0
- package/dist/internal/machine/protocol.js.map +1 -0
- package/dist/internal/machine/readiness.d.ts +4 -0
- package/dist/internal/machine/readiness.d.ts.map +1 -0
- package/dist/internal/machine/readiness.js +2 -0
- package/dist/internal/machine/readiness.js.map +1 -0
- package/dist/internal/{machineRuntime.d.ts → machine/runtime.d.ts} +104 -2
- package/dist/internal/machine/runtime.d.ts.map +1 -0
- package/dist/internal/machine/runtime.js +1381 -0
- package/dist/internal/machine/runtime.js.map +1 -0
- package/dist/internal/machine/serialization.d.ts +14 -0
- package/dist/internal/machine/serialization.d.ts.map +1 -0
- package/dist/internal/machine/serialization.js +338 -0
- package/dist/internal/machine/serialization.js.map +1 -0
- package/dist/internal/machine/stateDefinition.d.ts +15 -0
- package/dist/internal/machine/stateDefinition.d.ts.map +1 -0
- package/dist/internal/machine/stateDefinition.js +210 -0
- package/dist/internal/machine/stateDefinition.js.map +1 -0
- package/dist/internal/machine/symbols.d.ts +3 -0
- package/dist/internal/machine/symbols.d.ts.map +1 -0
- package/dist/internal/machine/symbols.js +3 -0
- package/dist/internal/machine/symbols.js.map +1 -0
- package/dist/internal/machine/topology.d.ts +98 -0
- package/dist/internal/machine/topology.d.ts.map +1 -0
- package/dist/internal/machine/topology.js +347 -0
- package/dist/internal/machine/topology.js.map +1 -0
- package/dist/internal/testing/machine/arbitrary.d.ts +36 -0
- package/dist/internal/testing/machine/arbitrary.d.ts.map +1 -0
- package/dist/internal/testing/machine/arbitrary.js +68 -0
- package/dist/internal/testing/machine/arbitrary.js.map +1 -0
- package/dist/internal/testing/machine/finiteModel.d.ts +322 -0
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -0
- package/dist/internal/testing/machine/finiteModel.js +976 -0
- package/dist/internal/testing/machine/finiteModel.js.map +1 -0
- package/dist/internal/testing/machine/referenceModel.d.ts +203 -0
- package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -0
- package/dist/internal/testing/machine/referenceModel.js +1012 -0
- package/dist/internal/testing/machine/referenceModel.js.map +1 -0
- package/dist/internal/testing/machine/runtime.d.ts +328 -0
- package/dist/internal/testing/machine/runtime.d.ts.map +1 -0
- package/dist/internal/testing/machine/runtime.js +437 -0
- package/dist/internal/testing/machine/runtime.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts +33 -0
- package/dist/internal/testing/machine/verification.d.ts.map +1 -0
- package/dist/internal/testing/machine/verification.js +1446 -0
- package/dist/internal/testing/machine/verification.js.map +1 -0
- package/dist/testing/MachineTest.d.ts +606 -0
- package/dist/testing/MachineTest.d.ts.map +1 -0
- package/dist/testing/MachineTest.js +102 -0
- package/dist/testing/MachineTest.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/{ClusterMachine.d.ts → unstable/cluster/ClusterMachine.d.ts} +22 -30
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -0
- package/dist/unstable/cluster/ClusterMachine.js +126 -0
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -0
- package/dist/{cluster.js → unstable/cluster/index.d.ts} +1 -1
- package/dist/unstable/cluster/index.d.ts.map +1 -0
- package/dist/{cluster.d.ts → unstable/cluster/index.js} +1 -1
- package/dist/unstable/cluster/index.js.map +1 -0
- package/dist/{AtomMachine.d.ts → unstable/reactivity/AtomMachine.d.ts} +40 -23
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -0
- package/dist/unstable/reactivity/AtomMachine.js +113 -0
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -0
- package/dist/{reactivity.js → unstable/reactivity/index.d.ts} +1 -1
- package/dist/unstable/reactivity/index.d.ts.map +1 -0
- package/dist/{reactivity.d.ts → unstable/reactivity/index.js} +1 -1
- package/dist/unstable/reactivity/index.js.map +1 -0
- package/docs/agent-guide.md +271 -38
- package/package.json +23 -13
- package/dist/AtomMachine.d.ts.map +0 -1
- package/dist/AtomMachine.js.map +0 -1
- package/dist/ClusterMachine.d.ts.map +0 -1
- package/dist/ClusterMachine.js.map +0 -1
- package/dist/cluster.d.ts.map +0 -1
- package/dist/cluster.js.map +0 -1
- package/dist/internal/machineErrors.d.ts.map +0 -1
- package/dist/internal/machineErrors.js.map +0 -1
- package/dist/internal/machineModel.d.ts.map +0 -1
- package/dist/internal/machineModel.js +0 -766
- package/dist/internal/machineModel.js.map +0 -1
- package/dist/internal/machinePlanner.d.ts +0 -74
- package/dist/internal/machinePlanner.d.ts.map +0 -1
- package/dist/internal/machinePlanner.js +0 -621
- package/dist/internal/machinePlanner.js.map +0 -1
- package/dist/internal/machineProcess.d.ts.map +0 -1
- package/dist/internal/machineProcess.js +0 -181
- package/dist/internal/machineProcess.js.map +0 -1
- package/dist/internal/machineRuntime.d.ts.map +0 -1
- package/dist/internal/machineRuntime.js +0 -365
- package/dist/internal/machineRuntime.js.map +0 -1
- package/dist/reactivity.d.ts.map +0 -1
- package/dist/reactivity.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,19 +8,19 @@ Schema-first state machines and statecharts for Effect.
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
pnpm add @typeonce/effect-machine effect@4.0.0-beta.
|
|
11
|
+
pnpm add @typeonce/effect-machine effect@4.0.0-beta.107
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
`effect` is an exact peer dependency, not a bundled runtime dependency.
|
|
15
|
-
Consumers must install `effect@4.0.0-beta.
|
|
15
|
+
Consumers must install `effect@4.0.0-beta.107`. Upgrading this package may
|
|
16
16
|
require upgrading Effect in lockstep; do not override the peer to another beta.
|
|
17
17
|
|
|
18
18
|
## Entrypoints
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
import { Machine } from "@typeonce/effect-machine"
|
|
22
|
-
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
23
22
|
import { ClusterMachine } from "@typeonce/effect-machine/cluster"
|
|
23
|
+
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Each ESM entrypoint is independent and tree-shakeable. Importing the root does
|
|
@@ -37,8 +37,8 @@ property contains the individual tagged schemas, and each case has a typed
|
|
|
37
37
|
`make` constructor.
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
|
-
import { Schema } from "effect"
|
|
41
40
|
import { Machine } from "@typeonce/effect-machine"
|
|
41
|
+
import { Schema } from "effect"
|
|
42
42
|
|
|
43
43
|
const State = Schema.TaggedUnion({
|
|
44
44
|
Idle: {},
|
|
@@ -55,20 +55,51 @@ const Counter = Machine.make({
|
|
|
55
55
|
id: "Counter",
|
|
56
56
|
states: States.states,
|
|
57
57
|
events: [Event.cases.Start],
|
|
58
|
-
initial: () => States.initial.Idle
|
|
58
|
+
initial: () => States.initial.Idle.from()
|
|
59
59
|
}).handle({
|
|
60
60
|
Idle: {
|
|
61
61
|
on: {
|
|
62
|
-
Start: ({ target }) => target.full.Running
|
|
62
|
+
Start: ({ target }) => target.full.Running.from()
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
Running: {}
|
|
66
66
|
})
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
Handler objects mirror the state definition recursively. `effect-machine` does
|
|
70
|
+
not impose a fixed handler-tree depth; inference continues until TypeScript's
|
|
71
|
+
normal, shape-dependent compiler limits.
|
|
72
|
+
|
|
69
73
|
`initial` is always a function. For a machine with an input schema, the
|
|
70
74
|
initializer receives the decoded input.
|
|
71
75
|
|
|
76
|
+
Builder methods accept an already constructed state value directly, or expose
|
|
77
|
+
`.from` for constructing one safely from the state schema's make input:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
target.local.Running(decodedRunning)
|
|
81
|
+
target.local.Running.from({ startedAt: event.at })
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use the direct call when a decoded value already exists. Use `.from` when
|
|
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.
|
|
92
|
+
|
|
93
|
+
When `{}` is valid constructor input, omit it. Required state fields remain
|
|
94
|
+
required, while compound and parallel states still require their active-child
|
|
95
|
+
callback:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
States.initial.Idle.from()
|
|
99
|
+
States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
|
|
100
|
+
States.initial.Flow.from((flow) => flow.Idle.from())
|
|
101
|
+
```
|
|
102
|
+
|
|
72
103
|
Tagged classes are equally valid when cases need class methods or nominal
|
|
73
104
|
identity:
|
|
74
105
|
|
|
@@ -95,10 +126,31 @@ const machine = Machine.make({
|
|
|
95
126
|
states: States.states,
|
|
96
127
|
events: [Command.cases.Save],
|
|
97
128
|
internalEvents: [InternalEvent.cases.Saved, InternalEvent.cases.SaveFailed],
|
|
98
|
-
initial: () => States.initial.Idle
|
|
129
|
+
initial: () => States.initial.Idle.from()
|
|
99
130
|
})
|
|
100
131
|
```
|
|
101
132
|
|
|
133
|
+
Construct reusable events with `Machine.event` when the schema is owned by the
|
|
134
|
+
machine protocol:
|
|
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.
|
|
147
|
+
|
|
148
|
+
Ordinary values remain valid and are decoded at every boundary:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
yield * ref.send({ _tag: "Save" })
|
|
152
|
+
```
|
|
153
|
+
|
|
102
154
|
Handlers and machine logic see the complete union. Local public APIs such as
|
|
103
155
|
`MachineRef.send`, `machineAtom.send`, and `Machine.plan` expose only `events`
|
|
104
156
|
in TypeScript. The local planner and runtime still share the complete event
|
|
@@ -120,7 +172,7 @@ masquerade as an internal result.
|
|
|
120
172
|
|
|
121
173
|
## Statechart structure
|
|
122
174
|
|
|
123
|
-
`Machine.defineStates` accepts atomic, compound, parallel, and
|
|
175
|
+
`Machine.defineStates` accepts atomic, compound, parallel, final, choice, and history
|
|
124
176
|
nodes:
|
|
125
177
|
|
|
126
178
|
```ts
|
|
@@ -157,8 +209,7 @@ Handlers implement behavior and output computation without repeating it:
|
|
|
157
209
|
const machine = Machine.make({
|
|
158
210
|
states: States.states,
|
|
159
211
|
events: [],
|
|
160
|
-
initial: () =>
|
|
161
|
-
States.initial.Form(State.cases.Form.make({ draft: "" }), (form) => form.Editing(State.cases.Editing.make({})))
|
|
212
|
+
initial: () => States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
|
|
162
213
|
}).handle({
|
|
163
214
|
Form: {
|
|
164
215
|
states: {
|
|
@@ -179,6 +230,162 @@ Put data on the narrowest state where it is valid. If several sibling phases
|
|
|
179
230
|
share data, prefer storing it on their compound parent instead of copying it
|
|
180
231
|
into every child state.
|
|
181
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
|
+
}
|
|
366
|
+
}
|
|
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
|
+
|
|
378
|
+
```ts
|
|
379
|
+
payment: {
|
|
380
|
+
initial: ;
|
|
381
|
+
;(({ state }) => new CardEntry({ attempt: state.attempt, cardNumber: "" }))
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Execution APIs remain unavailable until required history defaults and shallow
|
|
386
|
+
initializers have been implemented. History records are part of logical
|
|
387
|
+
snapshots and are schema-validated by `encodeSnapshot` and `decodeSnapshot`.
|
|
388
|
+
|
|
182
389
|
Transition between structurally related tagged states with `Machine.retag`.
|
|
183
390
|
The source `_tag` is discarded, compatible fields are reused, and missing or
|
|
184
391
|
incompatible required fields must be supplied:
|
|
@@ -189,13 +396,19 @@ const saving = Machine.retag(State.cases.Saving, editing)
|
|
|
189
396
|
|
|
190
397
|
## Choosing a target builder
|
|
191
398
|
|
|
192
|
-
Transition contexts expose
|
|
399
|
+
Transition contexts expose four typed target builders:
|
|
400
|
+
|
|
401
|
+
| Builder | Destination | Configuration behavior |
|
|
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 |
|
|
193
407
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
| `target.full` | Any top-level root | Builds a complete active snapshot for the selected root |
|
|
408
|
+
When `target.local` or `target.branch` enters an inactive nested parallel
|
|
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.
|
|
199
412
|
|
|
200
413
|
The builder controls how the next configuration is assembled; it does not by
|
|
201
414
|
itself decide which invokes restart. The runtime derives exit and entry paths
|
|
@@ -217,23 +430,58 @@ Refresh: {
|
|
|
217
430
|
keyed by full dotted paths, such as `parents["Form.Editing"]`; `context.parent`
|
|
218
431
|
provides the immediate parent directly and is `undefined` at a root state.
|
|
219
432
|
|
|
220
|
-
|
|
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:
|
|
221
437
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
438
|
+
```ts
|
|
439
|
+
BufferReady: ;
|
|
440
|
+
;(({ snapshot, target }) =>
|
|
441
|
+
States.matches(snapshot, "Player.Network.Online")
|
|
442
|
+
? target.local.Playing(State.cases.Playing.make({}))
|
|
443
|
+
: undefined)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
All non-conflicting handlers selected together observe the same captured
|
|
447
|
+
snapshot. Handlers are synchronous and cannot read mutable live runtime state
|
|
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.
|
|
451
|
+
|
|
452
|
+
Effect Schema annotations are the metadata source for active states. Annotate
|
|
453
|
+
the schema itself; `Machine.stateNodes` exposes the resolved annotation map:
|
|
226
454
|
|
|
227
455
|
```ts
|
|
228
|
-
|
|
456
|
+
const Saving = State.cases.Saving.annotate({
|
|
457
|
+
title: "Saving document",
|
|
458
|
+
description: "Persisting local changes to the server"
|
|
459
|
+
})
|
|
229
460
|
```
|
|
230
461
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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.
|
|
465
|
+
|
|
466
|
+
## Synchronous transitions and actor commands
|
|
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.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
const handlers = {
|
|
475
|
+
Save: ({ target }, enqueue) => {
|
|
476
|
+
enqueue.emit(new SaveRequested({}))
|
|
477
|
+
return target.local.Saving.from()
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
234
481
|
|
|
235
|
-
|
|
236
|
-
|
|
482
|
+
Use `Machine.invokeEffect`, `Machine.invoke`, or an invoked child machine for
|
|
483
|
+
asynchronous work. Their results return to the parent as typed events, keeping
|
|
484
|
+
the transition core deterministic and synchronous.
|
|
237
485
|
|
|
238
486
|
`Machine.plan` and `Machine.planInitial` return a `done` discriminator. When
|
|
239
487
|
`done` is `true`, `output` is the schema-derived structural terminal union;
|
|
@@ -252,16 +500,18 @@ state interrupts the child. For a one-shot Effect, `Machine.invokeEffect` maps
|
|
|
252
500
|
typed success and failure values directly to internal events:
|
|
253
501
|
|
|
254
502
|
```ts
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
503
|
+
const loading = {
|
|
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
|
+
}
|
|
265
515
|
```
|
|
266
516
|
|
|
267
517
|
Omit `onFailure` when the Effect cannot fail. Defects and interruption remain
|
|
@@ -306,6 +556,14 @@ Descriptors are matched by id and machine identity, so independently created
|
|
|
306
556
|
descriptors for the same pair address the same child without a global cache.
|
|
307
557
|
Exporting one descriptor remains the clearest module boundary.
|
|
308
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
|
+
|
|
309
567
|
## Reactivity
|
|
310
568
|
|
|
311
569
|
`AtomMachine.make` creates a lazy bridge backed by one running machine per
|
|
@@ -313,8 +571,8 @@ Exporting one descriptor remains the clearest module boundary.
|
|
|
313
571
|
disposing the registry-owned reference stops it.
|
|
314
572
|
|
|
315
573
|
```ts
|
|
316
|
-
import { Atom } from "effect/unstable/reactivity"
|
|
317
574
|
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
575
|
+
import { Atom } from "effect/unstable/reactivity"
|
|
318
576
|
|
|
319
577
|
const runtime = Atom.runtime(AppLayer)
|
|
320
578
|
const machines = AtomMachine.bind(runtime)
|
|
@@ -364,14 +622,44 @@ contain the machine definition, machine version, services, subscriptions, or
|
|
|
364
622
|
running child processes. Store machine identity and migration/version metadata
|
|
365
623
|
alongside it.
|
|
366
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)
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
`resume` does not call `initial`, require machine input, or replay entry,
|
|
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`.
|
|
652
|
+
|
|
367
653
|
`ClusterMachine` provides a separate persisted entity adapter. Its process-local
|
|
368
|
-
restrictions and delivery guarantees are documented on
|
|
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.
|
|
369
657
|
|
|
370
658
|
## Current limits
|
|
371
659
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
660
|
+
Declarative first-class guards are not part of the current API. Ordinary
|
|
661
|
+
TypeScript conditions implement guards. Use `Machine.after` for a cancellable
|
|
662
|
+
state-scoped delayed event.
|
|
375
663
|
|
|
376
664
|
## Guidance for agents and contributors
|
|
377
665
|
|
|
@@ -394,14 +682,29 @@ Individual commands are available for `build`, `test`, `test:types`,
|
|
|
394
682
|
packs the package, imports all public entrypoints, and compiles a strict
|
|
395
683
|
TypeScript consumer with `skipLibCheck: false`.
|
|
396
684
|
|
|
685
|
+
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before proposing a change. Pull
|
|
686
|
+
requests receive an automated base-versus-head type-instantiation report.
|
|
687
|
+
|
|
397
688
|
## Examples
|
|
398
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
|
+
|
|
399
697
|
The [Pokémon statechart example](./examples/pokemon) is a standalone React and
|
|
400
698
|
Vite project demonstrating compound and parallel states, state-scoped invokes,
|
|
401
699
|
invoked child statecharts, typed emissions, and Atom reactivity. It uses a local
|
|
402
700
|
`file:` dependency on this package while retaining an isolated dependency graph,
|
|
403
701
|
lockfile, build, and CI job.
|
|
404
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
|
+
|
|
405
708
|
## Releases
|
|
406
709
|
|
|
407
710
|
Add a changeset with `pnpm changeset`. CI validates frozen installation and the
|