@typeonce/effect-machine 0.9.0 → 0.10.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 +128 -28
- package/dist/Machine.d.ts +334 -238
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +45 -76
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +6 -2
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +5 -0
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/cluster.d.ts.map +1 -1
- package/dist/internal/machine/cluster.js +2 -1
- package/dist/internal/machine/cluster.js.map +1 -1
- package/dist/internal/machine/command.js +2 -2
- package/dist/internal/machine/command.js.map +1 -1
- package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
- package/dist/internal/machine/commandRuntime.js +6 -3
- package/dist/internal/machine/commandRuntime.js.map +1 -1
- package/dist/internal/machine/configuration.d.ts +8 -1
- package/dist/internal/machine/configuration.d.ts.map +1 -1
- package/dist/internal/machine/configuration.js +9 -4
- package/dist/internal/machine/configuration.js.map +1 -1
- package/dist/internal/machine/errors.d.ts +1 -1
- package/dist/internal/machine/errors.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts +3 -3
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +50 -25
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +3 -2
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +14 -19
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +12 -10
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +2 -1
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +61 -24
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +8 -8
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/protocol.d.ts +7 -13
- package/dist/internal/machine/protocol.d.ts.map +1 -1
- package/dist/internal/machine/protocol.js +128 -50
- package/dist/internal/machine/protocol.js.map +1 -1
- package/dist/internal/machine/runtime.d.ts +8 -2
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +86 -8
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +7 -0
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +6 -0
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +1 -1
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
- package/dist/internal/testing/machine/runtime.js +2 -1
- package/dist/internal/testing/machine/runtime.js.map +1 -1
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +5 -3
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +7 -7
- package/dist/testing/MachineTest.js +7 -7
- package/dist/unstable/cluster/ClusterMachine.d.ts +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +25 -8
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +18 -3
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +143 -48
- package/package.json +1 -1
- package/src/Machine.ts +559 -307
- package/src/internal/machine/atom.ts +35 -10
- package/src/internal/machine/cluster.ts +2 -1
- package/src/internal/machine/command.ts +2 -2
- package/src/internal/machine/commandRuntime.ts +9 -4
- package/src/internal/machine/configuration.ts +23 -5
- package/src/internal/machine/errors.ts +1 -1
- package/src/internal/machine/executionPlan.ts +74 -29
- package/src/internal/machine/invocation.ts +3 -2
- package/src/internal/machine/machine.ts +68 -49
- package/src/internal/machine/planner.ts +68 -24
- package/src/internal/machine/process.ts +18 -8
- package/src/internal/machine/protocol.ts +207 -68
- package/src/internal/machine/runtime.ts +135 -9
- package/src/internal/machine/topology.ts +15 -0
- package/src/internal/testing/machine/finiteModel.ts +1 -1
- package/src/internal/testing/machine/runtime.ts +2 -1
- package/src/internal/testing/machine/verification.ts +5 -3
- package/src/testing/MachineTest.ts +7 -7
- package/src/unstable/cluster/ClusterMachine.ts +1 -1
- package/src/unstable/reactivity/AtomMachine.ts +42 -10
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Define schemas first, derive the state topology, then add behavior:
|
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
import { Machine } from "@typeonce/effect-machine"
|
|
28
|
-
import { Effect, Schema } from "effect"
|
|
28
|
+
import { Effect, Schema, Stream } from "effect"
|
|
29
29
|
|
|
30
30
|
const State = Schema.TaggedUnion({
|
|
31
31
|
Idle: {},
|
|
@@ -39,11 +39,12 @@ const Event = Schema.TaggedUnion({
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
const States = Machine.defineStates(State.cases)
|
|
42
|
+
const CounterEvent = Machine.events(Event)
|
|
42
43
|
|
|
43
44
|
const CounterDefinition = Machine.make({
|
|
44
45
|
id: "Counter",
|
|
45
46
|
states: States.states,
|
|
46
|
-
events:
|
|
47
|
+
events: CounterEvent,
|
|
47
48
|
initial: () => States.initial.Idle.from()
|
|
48
49
|
})
|
|
49
50
|
|
|
@@ -61,8 +62,6 @@ const Counter = CounterDefinition.handle({
|
|
|
61
62
|
}
|
|
62
63
|
})
|
|
63
64
|
|
|
64
|
-
const CounterEvent = Machine.events(Counter)
|
|
65
|
-
|
|
66
65
|
const program = Effect.gen(function*() {
|
|
67
66
|
const ref = yield* Machine.start(Counter)
|
|
68
67
|
yield* ref.send(CounterEvent.Start())
|
|
@@ -71,8 +70,8 @@ const program = Effect.gen(function*() {
|
|
|
71
70
|
```
|
|
72
71
|
|
|
73
72
|
`Machine.start` returns a `MachineRef` with `send`, `state`, `snapshot`,
|
|
74
|
-
`changes`, `join`, and `stop`. Sending enqueues an event; observe
|
|
75
|
-
use the testing probe when work must be causally acknowledged.
|
|
73
|
+
`changes`, `emissions`, `join`, and `stop`. Sending enqueues an event; observe
|
|
74
|
+
`changes` or use the testing probe when work must be causally acknowledged.
|
|
76
75
|
|
|
77
76
|
## Modeling workflow
|
|
78
77
|
|
|
@@ -80,9 +79,11 @@ Use this order to preserve inference and keep boundaries explicit:
|
|
|
80
79
|
|
|
81
80
|
1. Define domain, state, public-event, internal-event, and emitted-event schemas.
|
|
82
81
|
2. Declare topology with `Machine.defineStates`.
|
|
83
|
-
3. Create
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
3. Create public and internal event descriptors with `Machine.events` and
|
|
83
|
+
`Machine.internalEvents`.
|
|
84
|
+
4. Create the machine protocol and initializer with `Machine.make`.
|
|
85
|
+
5. Implement every active state with `.handle(...)`.
|
|
86
|
+
6. Add runtime, Atom, testing, or cluster adapters at the application boundary.
|
|
86
87
|
|
|
87
88
|
### Construct state through builders
|
|
88
89
|
|
|
@@ -96,7 +97,18 @@ States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
|
|
|
96
97
|
The machine runs these inputs through the state schema while planning. Schema
|
|
97
98
|
defaults, refinements, and tagged-class identity are therefore preserved, and
|
|
98
99
|
decode failures remain typed machine failures. Pass a value directly only when
|
|
99
|
-
it is already decoded
|
|
100
|
+
it is already decoded.
|
|
101
|
+
|
|
102
|
+
When sibling states share fields, remove the source discriminator and pass the
|
|
103
|
+
remaining fields through the target schema:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
Submit: ;
|
|
107
|
+
;(({ state, target }) => {
|
|
108
|
+
const { _tag: _, ...fields } = state
|
|
109
|
+
return target.local.Saving.from({ ...fields, attempt: 1 })
|
|
110
|
+
})
|
|
111
|
+
```
|
|
100
112
|
|
|
101
113
|
Omit `schema` when a state represents control flow but owns no data:
|
|
102
114
|
|
|
@@ -122,10 +134,11 @@ schema-backed paths. Add a schema later if the state starts owning data.
|
|
|
122
134
|
Put data on the narrowest state where it is valid. If sibling phases share
|
|
123
135
|
data, put it on their compound parent.
|
|
124
136
|
|
|
125
|
-
### Separate
|
|
137
|
+
### Separate inputs, raised events, and emissions
|
|
126
138
|
|
|
127
|
-
`events` is the public
|
|
128
|
-
|
|
139
|
+
`events` is the public actor-input protocol. Events raised to the same machine
|
|
140
|
+
belong in `internalEvents`. Ephemeral outward notifications have their own
|
|
141
|
+
`emittedEvents` protocol:
|
|
129
142
|
|
|
130
143
|
```ts
|
|
131
144
|
const Command = Schema.TaggedUnion({ Save: {} })
|
|
@@ -133,46 +146,122 @@ const Internal = Schema.TaggedUnion({
|
|
|
133
146
|
Saved: { id: Schema.String },
|
|
134
147
|
SaveFailed: { message: Schema.String }
|
|
135
148
|
})
|
|
149
|
+
const Emitted = Schema.TaggedUnion({
|
|
150
|
+
SaveObserved: { id: Schema.String }
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
export const CommandEvent = Machine.events(Command)
|
|
154
|
+
export type PublicCommandEvent = Machine.EventOf<typeof CommandEvent>
|
|
155
|
+
const InternalEvent = Machine.internalEvents(Internal)
|
|
156
|
+
const Emissions = Machine.emittedEvents(Emitted)
|
|
136
157
|
|
|
137
158
|
const definition = Machine.make({
|
|
138
159
|
states: States.states,
|
|
139
|
-
events:
|
|
140
|
-
internalEvents:
|
|
160
|
+
events: CommandEvent,
|
|
161
|
+
internalEvents: InternalEvent,
|
|
162
|
+
emittedEvents: Emissions,
|
|
141
163
|
initial: () => States.initial.Idle.from()
|
|
142
164
|
})
|
|
143
|
-
|
|
144
|
-
const CommandEvent = Machine.events(definition)
|
|
145
|
-
const InternalEvent = Machine.internalEvents(definition)
|
|
146
165
|
```
|
|
147
166
|
|
|
148
167
|
Handlers see both protocols. Typed `send` and `Machine.plan` accept only public
|
|
149
168
|
events. Event tags must be unique and public/internal tags must be disjoint.
|
|
150
169
|
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
Export the descriptor returned by `Machine.events` instead of exporting its
|
|
171
|
+
schemas. This keeps the deferred constructors as the standard way to create
|
|
172
|
+
events without exposing schema `.make` methods:
|
|
153
173
|
|
|
154
174
|
```ts
|
|
155
175
|
ref.send(CommandEvent.Save())
|
|
156
176
|
enqueue.raise(InternalEvent.Saved({ id: "entry-1" }))
|
|
177
|
+
enqueue.emit(Emissions.SaveObserved({ id: "entry-1" }))
|
|
157
178
|
```
|
|
158
179
|
|
|
159
180
|
The returned constructors preserve each schema's make input, including required
|
|
160
181
|
fields and constructor defaults. They defer schema construction until delivery,
|
|
161
182
|
so invalid values fail planning or the running machine with
|
|
162
183
|
`MachineSchemaDecodeError` instead of throwing at the call site.
|
|
184
|
+
Schemas with an open discriminator such as `_tag: Schema.String` remain valid
|
|
185
|
+
protocols but cannot expose a finite constructor set; pass a complete event
|
|
186
|
+
object to `send` or `Machine.plan` for those events.
|
|
187
|
+
|
|
188
|
+
`ref.emissions` is a hot `Stream`: it publishes only notifications produced
|
|
189
|
+
after subscription, replays nothing, and completes when the actor terminates.
|
|
190
|
+
Snapshots remain separate and stateful: `ref.changes` begins with the current
|
|
191
|
+
lifecycle snapshot and then follows later changes. Because `Machine.start`
|
|
192
|
+
returns only after initialization, startup emissions are not observable from
|
|
193
|
+
the returned ref; represent startup facts in state when they must be retained.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const next = ref.emissions.pipe(Stream.take(1), Stream.runHead)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Invalid event and emission constructions fail the machine with a typed
|
|
200
|
+
`MachineSchemaDecodeError`; they do not throw from the constructor call.
|
|
201
|
+
|
|
202
|
+
### Send explicitly between actors
|
|
203
|
+
|
|
204
|
+
`raise` targets the current machine in the same macrostep. `sendTo` targets an
|
|
205
|
+
actor mailbox and is processed later. A child declares the subset of parent
|
|
206
|
+
inputs it may send with `parentEvents`:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const ParentEvents = Machine.events(ChildFinished)
|
|
210
|
+
|
|
211
|
+
const child = Machine.make({
|
|
212
|
+
states: ChildStates.states,
|
|
213
|
+
events: ChildEvents,
|
|
214
|
+
parentEvents: ParentEvents,
|
|
215
|
+
initial: () => ChildStates.initial.Working.from()
|
|
216
|
+
}).handle({
|
|
217
|
+
Working: {
|
|
218
|
+
on: {
|
|
219
|
+
Finish: ({ parent, target }, enqueue) => {
|
|
220
|
+
if (parent !== undefined) {
|
|
221
|
+
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
222
|
+
}
|
|
223
|
+
return target.full.Done.from()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
Done: {}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const Child = Machine.child("worker", child)
|
|
231
|
+
const ParentInputs = Machine.events(Start, ParentEvents)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The same child remains isolated and may be started as a root, where `parent` is
|
|
235
|
+
`undefined`. When `Child` is invoked, the parent definition must accept every
|
|
236
|
+
event in `parentEvents`; otherwise `.handle(...)` is a compile-time error.
|
|
237
|
+
Inside the child, the parent reference accepts only those declared events.
|
|
238
|
+
`emit` never sends to the parent: it only publishes on the emitting actor's
|
|
239
|
+
`emissions` stream.
|
|
240
|
+
|
|
241
|
+
Every handler also receives `self`, which can be targeted with `sendTo` when a
|
|
242
|
+
later mailbox turn is required. Use `raise` instead for same-macrostep work.
|
|
243
|
+
Structural state values use distinct names: `containingState` is the immediate
|
|
244
|
+
valued state in the same statechart, while `ancestors` maps valued ancestor
|
|
245
|
+
paths. `parent` always means the owning actor reference.
|
|
163
246
|
|
|
164
247
|
### Choose the target by scope
|
|
165
248
|
|
|
166
249
|
| Builder | Use when | Preserves |
|
|
167
250
|
| ---------------- | ---------------------------------------- | ------------------------------------------------- |
|
|
251
|
+
| `target.none()` | Handling without selecting a destination | The complete current configuration |
|
|
168
252
|
| `target.local` | Moving inside the nearest compound scope | Ancestors and unrelated parallel regions |
|
|
169
253
|
| `target.branch` | Moving elsewhere under the active root | Omitted active ancestors and parallel regions |
|
|
170
254
|
| `target.full` | Replacing or selecting a complete root | Nothing implicit for a newly selected root |
|
|
171
255
|
| `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
|
|
172
256
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
257
|
+
Every installed transition handler returns either a concrete target or
|
|
258
|
+
`target.none()`. An absent handler ignores the trigger; `target.none()` handles
|
|
259
|
+
it and retains queued commands, raised events, and emitted events without
|
|
260
|
+
selecting a destination. Declared `targets` constrain only concrete
|
|
261
|
+
destinations, so `target.none()` is always permitted. Builders describe the
|
|
262
|
+
next logical configuration. Shared states exit and enter only when paths
|
|
263
|
+
change; use `{ reenter: true, transition }` when the source must restart. With
|
|
264
|
+
`target.none()`, reentry restarts the source while retaining its configuration.
|
|
176
265
|
|
|
177
266
|
## Statechart capabilities
|
|
178
267
|
|
|
@@ -265,6 +354,17 @@ The bridge exposes `ref`, `snapshot`, `state`, fail-aware `result`, writable
|
|
|
265
354
|
equality-aware derivations. React applications using `@effect/atom-react` need
|
|
266
355
|
a `RegistryProvider`.
|
|
267
356
|
|
|
357
|
+
Emissions stay streams rather than becoming retained atom state:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
const rootEmissions = AtomMachine.emissions(counterAtom)
|
|
361
|
+
const childEmissions = AtomMachine.childEmissions(counterAtom.child(Worker))
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
These streams require the same `AtomRegistry`, follow the currently mounted
|
|
365
|
+
actor instance, and do not replay notifications from an earlier subscription
|
|
366
|
+
or child instance.
|
|
367
|
+
|
|
268
368
|
## Persistence
|
|
269
369
|
|
|
270
370
|
Logical snapshots can be validated for storage or transport:
|
|
@@ -296,8 +396,8 @@ import { MachineTest } from "@typeonce/effect-machine/testing"
|
|
|
296
396
|
|
|
297
397
|
const trace = yield* MachineTest.run(Counter, {
|
|
298
398
|
events: [
|
|
299
|
-
|
|
300
|
-
|
|
399
|
+
{ _tag: "Start" },
|
|
400
|
+
{ _tag: "Increment" }
|
|
301
401
|
]
|
|
302
402
|
})
|
|
303
403
|
|
|
@@ -305,9 +405,9 @@ yield* MachineTest.verify(Counter, trace)
|
|
|
305
405
|
```
|
|
306
406
|
|
|
307
407
|
`MachineTest` scenarios retain decoded event values for model inspection, so
|
|
308
|
-
|
|
309
|
-
not execute invokes or time. Use a started machine and a probe when
|
|
310
|
-
semantics matter.
|
|
408
|
+
pass complete decoded objects when defining scenarios manually. Pure planner
|
|
409
|
+
tests do not execute invokes or time. Use a started machine and a probe when
|
|
410
|
+
those semantics matter.
|
|
311
411
|
|
|
312
412
|
## Entrypoints
|
|
313
413
|
|