@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.
Files changed (92) hide show
  1. package/README.md +128 -28
  2. package/dist/Machine.d.ts +334 -238
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +45 -76
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +6 -2
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +5 -0
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/cluster.d.ts.map +1 -1
  11. package/dist/internal/machine/cluster.js +2 -1
  12. package/dist/internal/machine/cluster.js.map +1 -1
  13. package/dist/internal/machine/command.js +2 -2
  14. package/dist/internal/machine/command.js.map +1 -1
  15. package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
  16. package/dist/internal/machine/commandRuntime.js +6 -3
  17. package/dist/internal/machine/commandRuntime.js.map +1 -1
  18. package/dist/internal/machine/configuration.d.ts +8 -1
  19. package/dist/internal/machine/configuration.d.ts.map +1 -1
  20. package/dist/internal/machine/configuration.js +9 -4
  21. package/dist/internal/machine/configuration.js.map +1 -1
  22. package/dist/internal/machine/errors.d.ts +1 -1
  23. package/dist/internal/machine/errors.d.ts.map +1 -1
  24. package/dist/internal/machine/executionPlan.d.ts +3 -3
  25. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  26. package/dist/internal/machine/executionPlan.js +50 -25
  27. package/dist/internal/machine/executionPlan.js.map +1 -1
  28. package/dist/internal/machine/invocation.d.ts.map +1 -1
  29. package/dist/internal/machine/invocation.js +3 -2
  30. package/dist/internal/machine/invocation.js.map +1 -1
  31. package/dist/internal/machine/machine.d.ts +14 -19
  32. package/dist/internal/machine/machine.d.ts.map +1 -1
  33. package/dist/internal/machine/machine.js +12 -10
  34. package/dist/internal/machine/machine.js.map +1 -1
  35. package/dist/internal/machine/planner.d.ts +2 -1
  36. package/dist/internal/machine/planner.d.ts.map +1 -1
  37. package/dist/internal/machine/planner.js +61 -24
  38. package/dist/internal/machine/planner.js.map +1 -1
  39. package/dist/internal/machine/process.d.ts.map +1 -1
  40. package/dist/internal/machine/process.js +8 -8
  41. package/dist/internal/machine/process.js.map +1 -1
  42. package/dist/internal/machine/protocol.d.ts +7 -13
  43. package/dist/internal/machine/protocol.d.ts.map +1 -1
  44. package/dist/internal/machine/protocol.js +128 -50
  45. package/dist/internal/machine/protocol.js.map +1 -1
  46. package/dist/internal/machine/runtime.d.ts +8 -2
  47. package/dist/internal/machine/runtime.d.ts.map +1 -1
  48. package/dist/internal/machine/runtime.js +86 -8
  49. package/dist/internal/machine/runtime.js.map +1 -1
  50. package/dist/internal/machine/topology.d.ts +7 -0
  51. package/dist/internal/machine/topology.d.ts.map +1 -1
  52. package/dist/internal/machine/topology.js +6 -0
  53. package/dist/internal/machine/topology.js.map +1 -1
  54. package/dist/internal/testing/machine/finiteModel.js +1 -1
  55. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  56. package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
  57. package/dist/internal/testing/machine/runtime.js +2 -1
  58. package/dist/internal/testing/machine/runtime.js.map +1 -1
  59. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  60. package/dist/internal/testing/machine/verification.js +5 -3
  61. package/dist/internal/testing/machine/verification.js.map +1 -1
  62. package/dist/testing/MachineTest.d.ts +7 -7
  63. package/dist/testing/MachineTest.js +7 -7
  64. package/dist/unstable/cluster/ClusterMachine.d.ts +1 -1
  65. package/dist/unstable/cluster/ClusterMachine.js +1 -1
  66. package/dist/unstable/reactivity/AtomMachine.d.ts +25 -8
  67. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  68. package/dist/unstable/reactivity/AtomMachine.js +18 -3
  69. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  70. package/docs/agent-guide.md +143 -48
  71. package/package.json +1 -1
  72. package/src/Machine.ts +559 -307
  73. package/src/internal/machine/atom.ts +35 -10
  74. package/src/internal/machine/cluster.ts +2 -1
  75. package/src/internal/machine/command.ts +2 -2
  76. package/src/internal/machine/commandRuntime.ts +9 -4
  77. package/src/internal/machine/configuration.ts +23 -5
  78. package/src/internal/machine/errors.ts +1 -1
  79. package/src/internal/machine/executionPlan.ts +74 -29
  80. package/src/internal/machine/invocation.ts +3 -2
  81. package/src/internal/machine/machine.ts +68 -49
  82. package/src/internal/machine/planner.ts +68 -24
  83. package/src/internal/machine/process.ts +18 -8
  84. package/src/internal/machine/protocol.ts +207 -68
  85. package/src/internal/machine/runtime.ts +135 -9
  86. package/src/internal/machine/topology.ts +15 -0
  87. package/src/internal/testing/machine/finiteModel.ts +1 -1
  88. package/src/internal/testing/machine/runtime.ts +2 -1
  89. package/src/internal/testing/machine/verification.ts +5 -3
  90. package/src/testing/MachineTest.ts +7 -7
  91. package/src/unstable/cluster/ClusterMachine.ts +1 -1
  92. 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: [Event],
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 `changes` or
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 the protocol and initializer with `Machine.make`.
84
- 4. Implement every active state with `.handle(...)`.
85
- 5. Add runtime, Atom, testing, or cluster adapters at the application boundary.
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, such as a value returned by `Machine.retag`.
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 public and internal events
137
+ ### Separate inputs, raised events, and emissions
126
138
 
127
- `events` is the public command protocol. Invoke results, timer deliveries,
128
- raised events, and child emissions belong in `internalEvents`:
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: [Command],
140
- internalEvents: [Internal],
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
- Use `Machine.events(machine)` and `Machine.internalEvents(machine)` as the
152
- standard constructors for their respective protocols:
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
- Builders describe the next logical configuration. Shared states exit and enter
174
- only when paths change; use `{ reenter: true, transition }` when the source must
175
- restart even if its path is unchanged.
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
- Machine.event(Counter, Event.cases.Start),
300
- Machine.event(Counter, Event.cases.Increment)
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
- this is the main case for the eager `Machine.event` API. Pure planner tests do
309
- not execute invokes or time. Use a started machine and a probe when those
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