@typeonce/effect-machine 0.9.0 → 0.11.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 (93) hide show
  1. package/README.md +167 -28
  2. package/dist/Machine.d.ts +491 -283
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +74 -76
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +7 -3
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +42 -6
  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 +15 -19
  32. package/dist/internal/machine/machine.d.ts.map +1 -1
  33. package/dist/internal/machine/machine.js +16 -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 +3 -0
  40. package/dist/internal/machine/process.d.ts.map +1 -1
  41. package/dist/internal/machine/process.js +11 -8
  42. package/dist/internal/machine/process.js.map +1 -1
  43. package/dist/internal/machine/protocol.d.ts +7 -13
  44. package/dist/internal/machine/protocol.d.ts.map +1 -1
  45. package/dist/internal/machine/protocol.js +128 -50
  46. package/dist/internal/machine/protocol.js.map +1 -1
  47. package/dist/internal/machine/runtime.d.ts +22 -2
  48. package/dist/internal/machine/runtime.d.ts.map +1 -1
  49. package/dist/internal/machine/runtime.js +138 -10
  50. package/dist/internal/machine/runtime.js.map +1 -1
  51. package/dist/internal/machine/topology.d.ts +7 -0
  52. package/dist/internal/machine/topology.d.ts.map +1 -1
  53. package/dist/internal/machine/topology.js +6 -0
  54. package/dist/internal/machine/topology.js.map +1 -1
  55. package/dist/internal/testing/machine/finiteModel.js +1 -1
  56. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  57. package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
  58. package/dist/internal/testing/machine/runtime.js +2 -1
  59. package/dist/internal/testing/machine/runtime.js.map +1 -1
  60. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  61. package/dist/internal/testing/machine/verification.js +5 -3
  62. package/dist/internal/testing/machine/verification.js.map +1 -1
  63. package/dist/testing/MachineTest.d.ts +7 -7
  64. package/dist/testing/MachineTest.js +7 -7
  65. package/dist/unstable/cluster/ClusterMachine.d.ts +1 -1
  66. package/dist/unstable/cluster/ClusterMachine.js +1 -1
  67. package/dist/unstable/reactivity/AtomMachine.d.ts +27 -8
  68. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  69. package/dist/unstable/reactivity/AtomMachine.js +20 -3
  70. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  71. package/docs/agent-guide.md +191 -54
  72. package/package.json +1 -1
  73. package/src/Machine.ts +1220 -360
  74. package/src/internal/machine/atom.ts +76 -53
  75. package/src/internal/machine/cluster.ts +2 -1
  76. package/src/internal/machine/command.ts +2 -2
  77. package/src/internal/machine/commandRuntime.ts +9 -4
  78. package/src/internal/machine/configuration.ts +23 -5
  79. package/src/internal/machine/errors.ts +1 -1
  80. package/src/internal/machine/executionPlan.ts +74 -29
  81. package/src/internal/machine/invocation.ts +3 -2
  82. package/src/internal/machine/machine.ts +74 -49
  83. package/src/internal/machine/planner.ts +68 -24
  84. package/src/internal/machine/process.ts +75 -8
  85. package/src/internal/machine/protocol.ts +207 -68
  86. package/src/internal/machine/runtime.ts +266 -11
  87. package/src/internal/machine/topology.ts +15 -0
  88. package/src/internal/testing/machine/finiteModel.ts +1 -1
  89. package/src/internal/testing/machine/runtime.ts +2 -1
  90. package/src/internal/testing/machine/verification.ts +5 -3
  91. package/src/testing/MachineTest.ts +7 -7
  92. package/src/unstable/cluster/ClusterMachine.ts +1 -1
  93. package/src/unstable/reactivity/AtomMachine.ts +44 -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,132 @@ 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. Use `Machine.prepare` when
192
+ an observer must be installed before initial-entry actions run:
193
+
194
+ ```ts
195
+ const prepared = yield * Machine.prepare(machine)
196
+
197
+ yield * prepared.emissions.pipe(
198
+ Stream.runForEach(handleEmission),
199
+ Effect.forkScoped({ startImmediately: true })
200
+ )
201
+
202
+ const ref = yield * prepared.start
203
+ ```
204
+
205
+ `Machine.start(machine)` remains the one-step convenience for callers that do
206
+ not observe startup emissions. Preparation does not retain or replay an
207
+ emission: the observer is simply subscribed before initialization begins.
208
+
209
+ Invalid event and emission constructions fail the machine with a typed
210
+ `MachineSchemaDecodeError`; they do not throw from the constructor call.
211
+
212
+ ### Send explicitly between actors
213
+
214
+ `raise` targets the current machine in the same macrostep. `sendTo` targets an
215
+ actor mailbox and is processed later. A child declares the subset of parent
216
+ inputs it may send with `parentEvents`:
217
+
218
+ ```ts
219
+ const ParentEvents = Machine.events(ChildFinished)
220
+
221
+ const child = Machine.make({
222
+ states: ChildStates.states,
223
+ events: ChildEvents,
224
+ parentEvents: ParentEvents,
225
+ initial: () => ChildStates.initial.Working.from()
226
+ }).handle({
227
+ Working: {
228
+ on: {
229
+ Finish: ({ parent, target }, enqueue) => {
230
+ if (parent !== undefined) {
231
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
232
+ }
233
+ return target.full.Done.from()
234
+ }
235
+ }
236
+ },
237
+ Done: {}
238
+ })
239
+
240
+ const Child = Machine.child("worker", child)
241
+ const ParentInputs = Machine.events(Start, ParentEvents)
242
+ ```
243
+
244
+ The same child remains isolated and may be started as a root, where `parent` is
245
+ `undefined`. When `Child` is invoked, the parent definition must accept every
246
+ event in `parentEvents`; otherwise `.handle(...)` is a compile-time error.
247
+ Inside the child, the parent reference accepts only those declared events.
248
+ `emit` never sends to the parent: it only publishes on the emitting actor's
249
+ `emissions` stream.
250
+
251
+ Every handler also receives `self`, which can be targeted with `sendTo` when a
252
+ later mailbox turn is required. Use `raise` instead for same-macrostep work.
253
+ Structural state values use distinct names: `containingState` is the immediate
254
+ valued state in the same statechart, while `ancestors` maps valued ancestor
255
+ paths. `parent` always means the owning actor reference.
163
256
 
164
257
  ### Choose the target by scope
165
258
 
166
259
  | Builder | Use when | Preserves |
167
260
  | ---------------- | ---------------------------------------- | ------------------------------------------------- |
261
+ | `target.none()` | Handling without selecting a destination | The complete current configuration |
168
262
  | `target.local` | Moving inside the nearest compound scope | Ancestors and unrelated parallel regions |
169
263
  | `target.branch` | Moving elsewhere under the active root | Omitted active ancestors and parallel regions |
170
264
  | `target.full` | Replacing or selecting a complete root | Nothing implicit for a newly selected root |
171
265
  | `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
172
266
 
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.
267
+ Every installed transition handler returns either a concrete target or
268
+ `target.none()`. An absent handler ignores the trigger; `target.none()` handles
269
+ it and retains queued commands, raised events, and emitted events without
270
+ selecting a destination. Declared `targets` constrain only concrete
271
+ destinations, so `target.none()` is always permitted. Builders describe the
272
+ next logical configuration. Shared states exit and enter only when paths
273
+ change; use `{ reenter: true, transition }` when the source must restart. With
274
+ `target.none()`, reentry restarts the source while retaining its configuration.
176
275
 
177
276
  ## Statechart capabilities
178
277
 
@@ -233,6 +332,35 @@ invoke: Machine.invoke({
233
332
  })
234
333
  ```
235
334
 
335
+ The standalone `Machine.invoke(...)` constructor does not know the owning
336
+ definition, so its `self` and `parent` references are non-sendable. When an
337
+ invocation callback sends through either reference, construct it through the
338
+ owning definition so those references use its exact public input and
339
+ `parentEvents` protocols:
340
+
341
+ ```ts
342
+ const definition = Machine.make({
343
+ events: Commands,
344
+ internalEvents: InternalEvents,
345
+ parentEvents: ParentEvents
346
+ // ...
347
+ })
348
+
349
+ const machine = definition.handle({
350
+ Saving: {
351
+ invoke: definition.invoke({
352
+ id: "notify-parent",
353
+ effect: ({ parent }) =>
354
+ parent === undefined
355
+ ? Effect.void
356
+ : parent.send(ParentEvents.SaveStarted()),
357
+ onDone: ({ target }) => target.none(),
358
+ onFailure: ({ target }) => target.none()
359
+ })
360
+ }
361
+ })
362
+ ```
363
+
236
364
  A direct `invoke: { ... }` object is also supported when its lifecycle handlers
237
365
  do not need source-derived context. Reuse one exported
238
366
  `Machine.child(id, machine)` descriptor for invocation, `sendTo`, and child
@@ -265,6 +393,17 @@ The bridge exposes `ref`, `snapshot`, `state`, fail-aware `result`, writable
265
393
  equality-aware derivations. React applications using `@effect/atom-react` need
266
394
  a `RegistryProvider`.
267
395
 
396
+ Emissions stay streams rather than becoming retained atom state:
397
+
398
+ ```ts
399
+ const rootEmissions = AtomMachine.emissions(counterAtom)
400
+ const childEmissions = AtomMachine.childEmissions(counterAtom.child(Worker))
401
+ ```
402
+
403
+ These streams require the same `AtomRegistry`, follow the currently mounted
404
+ actor instance, and do not replay notifications from an earlier subscription
405
+ or child instance.
406
+
268
407
  ## Persistence
269
408
 
270
409
  Logical snapshots can be validated for storage or transport:
@@ -296,8 +435,8 @@ import { MachineTest } from "@typeonce/effect-machine/testing"
296
435
 
297
436
  const trace = yield* MachineTest.run(Counter, {
298
437
  events: [
299
- Machine.event(Counter, Event.cases.Start),
300
- Machine.event(Counter, Event.cases.Increment)
438
+ { _tag: "Start" },
439
+ { _tag: "Increment" }
301
440
  ]
302
441
  })
303
442
 
@@ -305,9 +444,9 @@ yield* MachineTest.verify(Counter, trace)
305
444
  ```
306
445
 
307
446
  `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.
447
+ pass complete decoded objects when defining scenarios manually. Pure planner
448
+ tests do not execute invokes or time. Use a started machine and a probe when
449
+ those semantics matter.
311
450
 
312
451
  ## Entrypoints
313
452