@typeonce/effect-machine 0.8.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 (108) hide show
  1. package/README.md +180 -40
  2. package/dist/Machine.d.ts +659 -481
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +86 -222
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +5 -12
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +47 -17
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/atom.d.ts +6 -2
  11. package/dist/internal/machine/atom.d.ts.map +1 -1
  12. package/dist/internal/machine/atom.js +5 -0
  13. package/dist/internal/machine/atom.js.map +1 -1
  14. package/dist/internal/machine/cluster.d.ts.map +1 -1
  15. package/dist/internal/machine/cluster.js +2 -1
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/command.js +2 -2
  18. package/dist/internal/machine/command.js.map +1 -1
  19. package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
  20. package/dist/internal/machine/commandRuntime.js +6 -3
  21. package/dist/internal/machine/commandRuntime.js.map +1 -1
  22. package/dist/internal/machine/configuration.d.ts +8 -1
  23. package/dist/internal/machine/configuration.d.ts.map +1 -1
  24. package/dist/internal/machine/configuration.js +9 -4
  25. package/dist/internal/machine/configuration.js.map +1 -1
  26. package/dist/internal/machine/errors.d.ts +1 -1
  27. package/dist/internal/machine/errors.d.ts.map +1 -1
  28. package/dist/internal/machine/executionPlan.d.ts +3 -3
  29. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  30. package/dist/internal/machine/executionPlan.js +74 -24
  31. package/dist/internal/machine/executionPlan.js.map +1 -1
  32. package/dist/internal/machine/invocation.d.ts +10 -2
  33. package/dist/internal/machine/invocation.d.ts.map +1 -1
  34. package/dist/internal/machine/invocation.js +94 -34
  35. package/dist/internal/machine/invocation.js.map +1 -1
  36. package/dist/internal/machine/invocationEvent.d.ts +39 -0
  37. package/dist/internal/machine/invocationEvent.d.ts.map +1 -0
  38. package/dist/internal/machine/invocationEvent.js +43 -0
  39. package/dist/internal/machine/invocationEvent.js.map +1 -0
  40. package/dist/internal/machine/machine.d.ts +16 -65
  41. package/dist/internal/machine/machine.d.ts.map +1 -1
  42. package/dist/internal/machine/machine.js +18 -73
  43. package/dist/internal/machine/machine.js.map +1 -1
  44. package/dist/internal/machine/planner.d.ts +4 -2
  45. package/dist/internal/machine/planner.d.ts.map +1 -1
  46. package/dist/internal/machine/planner.js +104 -24
  47. package/dist/internal/machine/planner.js.map +1 -1
  48. package/dist/internal/machine/process.d.ts.map +1 -1
  49. package/dist/internal/machine/process.js +8 -8
  50. package/dist/internal/machine/process.js.map +1 -1
  51. package/dist/internal/machine/protocol.d.ts +8 -5
  52. package/dist/internal/machine/protocol.d.ts.map +1 -1
  53. package/dist/internal/machine/protocol.js +207 -15
  54. package/dist/internal/machine/protocol.js.map +1 -1
  55. package/dist/internal/machine/runtime.d.ts +8 -2
  56. package/dist/internal/machine/runtime.d.ts.map +1 -1
  57. package/dist/internal/machine/runtime.js +86 -8
  58. package/dist/internal/machine/runtime.js.map +1 -1
  59. package/dist/internal/machine/symbols.d.ts +2 -0
  60. package/dist/internal/machine/symbols.d.ts.map +1 -1
  61. package/dist/internal/machine/symbols.js +2 -0
  62. package/dist/internal/machine/symbols.js.map +1 -1
  63. package/dist/internal/machine/topology.d.ts +7 -0
  64. package/dist/internal/machine/topology.d.ts.map +1 -1
  65. package/dist/internal/machine/topology.js +29 -0
  66. package/dist/internal/machine/topology.js.map +1 -1
  67. package/dist/internal/testing/machine/finiteModel.js +1 -1
  68. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  69. package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
  70. package/dist/internal/testing/machine/runtime.js +2 -1
  71. package/dist/internal/testing/machine/runtime.js.map +1 -1
  72. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  73. package/dist/internal/testing/machine/verification.js +5 -3
  74. package/dist/internal/testing/machine/verification.js.map +1 -1
  75. package/dist/testing/MachineTest.d.ts +7 -7
  76. package/dist/testing/MachineTest.js +7 -7
  77. package/dist/unstable/cluster/ClusterMachine.d.ts +1 -1
  78. package/dist/unstable/cluster/ClusterMachine.js +1 -1
  79. package/dist/unstable/reactivity/AtomMachine.d.ts +25 -8
  80. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  81. package/dist/unstable/reactivity/AtomMachine.js +18 -3
  82. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  83. package/docs/agent-guide.md +242 -122
  84. package/package.json +2 -2
  85. package/src/Machine.ts +1453 -812
  86. package/src/internal/machine/activities.ts +48 -33
  87. package/src/internal/machine/atom.ts +38 -13
  88. package/src/internal/machine/cluster.ts +2 -1
  89. package/src/internal/machine/command.ts +2 -2
  90. package/src/internal/machine/commandRuntime.ts +9 -4
  91. package/src/internal/machine/configuration.ts +23 -5
  92. package/src/internal/machine/errors.ts +1 -1
  93. package/src/internal/machine/executionPlan.ts +102 -28
  94. package/src/internal/machine/invocation.ts +182 -50
  95. package/src/internal/machine/invocationEvent.ts +72 -0
  96. package/src/internal/machine/machine.ts +80 -387
  97. package/src/internal/machine/planner.ts +127 -32
  98. package/src/internal/machine/process.ts +18 -8
  99. package/src/internal/machine/protocol.ts +313 -25
  100. package/src/internal/machine/runtime.ts +135 -9
  101. package/src/internal/machine/symbols.ts +3 -0
  102. package/src/internal/machine/topology.ts +38 -0
  103. package/src/internal/testing/machine/finiteModel.ts +1 -1
  104. package/src/internal/testing/machine/runtime.ts +2 -1
  105. package/src/internal/testing/machine/verification.ts +5 -3
  106. package/src/testing/MachineTest.ts +7 -7
  107. package/src/unstable/cluster/ClusterMachine.ts +1 -1
  108. package/src/unstable/reactivity/AtomMachine.ts +45 -13
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,13 +39,16 @@ 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
- const Counter = Machine.make({
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
- }).handle({
49
+ })
50
+
51
+ const Counter = CounterDefinition.handle({
49
52
  Idle: {
50
53
  on: {
51
54
  Start: ({ target }) => target.full.Running.from({ count: 0 })
@@ -61,14 +64,14 @@ const Counter = Machine.make({
61
64
 
62
65
  const program = Effect.gen(function*() {
63
66
  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))
67
+ yield* ref.send(CounterEvent.Start())
68
+ yield* ref.send(CounterEvent.Increment())
66
69
  })
67
70
  ```
68
71
 
69
72
  `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.
73
+ `changes`, `emissions`, `join`, and `stop`. Sending enqueues an event; observe
74
+ `changes` or use the testing probe when work must be causally acknowledged.
72
75
 
73
76
  ## Modeling workflow
74
77
 
@@ -76,9 +79,11 @@ Use this order to preserve inference and keep boundaries explicit:
76
79
 
77
80
  1. Define domain, state, public-event, internal-event, and emitted-event schemas.
78
81
  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.
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.
82
87
 
83
88
  ### Construct state through builders
84
89
 
@@ -92,7 +97,18 @@ States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
92
97
  The machine runs these inputs through the state schema while planning. Schema
93
98
  defaults, refinements, and tagged-class identity are therefore preserved, and
94
99
  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`.
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
+ ```
96
112
 
97
113
  Omit `schema` when a state represents control flow but owns no data:
98
114
 
@@ -118,10 +134,11 @@ schema-backed paths. Add a schema later if the state starts owning data.
118
134
  Put data on the narrowest state where it is valid. If sibling phases share
119
135
  data, put it on their compound parent.
120
136
 
121
- ### Separate public and internal events
137
+ ### Separate inputs, raised events, and emissions
122
138
 
123
- `events` is the public command protocol. Invoke results, timer deliveries,
124
- 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:
125
142
 
126
143
  ```ts
127
144
  const Command = Schema.TaggedUnion({ Save: {} })
@@ -129,11 +146,20 @@ const Internal = Schema.TaggedUnion({
129
146
  Saved: { id: Schema.String },
130
147
  SaveFailed: { message: Schema.String }
131
148
  })
149
+ const Emitted = Schema.TaggedUnion({
150
+ SaveObserved: { id: Schema.String }
151
+ })
132
152
 
133
- const machine = Machine.make({
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)
157
+
158
+ const definition = Machine.make({
134
159
  states: States.states,
135
- events: [Command],
136
- internalEvents: [Internal],
160
+ events: CommandEvent,
161
+ internalEvents: InternalEvent,
162
+ emittedEvents: Emissions,
137
163
  initial: () => States.initial.Idle.from()
138
164
  })
139
165
  ```
@@ -141,22 +167,101 @@ const machine = Machine.make({
141
167
  Handlers see both protocols. Typed `send` and `Machine.plan` accept only public
142
168
  events. Event tags must be unique and public/internal tags must be disjoint.
143
169
 
144
- Use `Machine.event(machine, schema, fields?)` for reusable machine-owned event
145
- values. Ordinary objects and schema-constructed values are also accepted and
146
- decoded at the machine boundary.
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:
173
+
174
+ ```ts
175
+ ref.send(CommandEvent.Save())
176
+ enqueue.raise(InternalEvent.Saved({ id: "entry-1" }))
177
+ enqueue.emit(Emissions.SaveObserved({ id: "entry-1" }))
178
+ ```
179
+
180
+ The returned constructors preserve each schema's make input, including required
181
+ fields and constructor defaults. They defer schema construction until delivery,
182
+ so invalid values fail planning or the running machine with
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.
147
246
 
148
247
  ### Choose the target by scope
149
248
 
150
249
  | Builder | Use when | Preserves |
151
250
  | ---------------- | ---------------------------------------- | ------------------------------------------------- |
251
+ | `target.none()` | Handling without selecting a destination | The complete current configuration |
152
252
  | `target.local` | Moving inside the nearest compound scope | Ancestors and unrelated parallel regions |
153
253
  | `target.branch` | Moving elsewhere under the active root | Omitted active ancestors and parallel regions |
154
254
  | `target.full` | Replacing or selecting a complete root | Nothing implicit for a newly selected root |
155
255
  | `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
156
256
 
157
- Builders describe the next logical configuration. Shared states exit and enter
158
- only when paths change; use `{ reenter: true, transition }` when the source must
159
- 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.
160
265
 
161
266
  ## Statechart capabilities
162
267
 
@@ -185,29 +290,48 @@ State-scoped work starts on entry and is interrupted on exit:
185
290
 
186
291
  ```ts
187
292
  Loading: {
188
- invoke: Machine.invokeEffect({
293
+ invoke: Machine.invoke({
189
294
  id: "save-document",
190
295
  effect: saveDocument,
191
- onSuccess: (entry) => Internal.cases.Saved.make({ id: entry.id }),
192
- onFailure: (error) => Internal.cases.SaveFailed.make({ message: String(error) })
296
+ onDone: ({ output, target }) => target.full.Saved({ id: output.id }),
297
+ onFailure: ({ error, target }) => target.full.Failed({ message: String(error) })
193
298
  })
194
299
  }
195
300
 
196
301
  Waiting: {
197
- invoke: Machine.after(
198
- "3 seconds",
199
- Internal.cases.SaveFailed.make({ message: "Timed out" })
200
- )
302
+ invoke: Machine.invoke({
303
+ id: "save-timeout",
304
+ after: "3 seconds",
305
+ onDone: ({ target }) => target.full.Failed({ message: "Timed out" })
306
+ })
201
307
  }
202
308
  ```
203
309
 
204
- Use `Machine.invokeEffect` for one Effect, `Machine.after` for a cancellable
205
- delay, and lower-level `Machine.invoke` only for custom process behavior or
206
- snapshot mapping. Use one exported `Machine.child(id, machine)` descriptor for
207
- `invokeMachine`, `sendTo`, and child lookup.
310
+ Use `effect` for one Effect, `after` for a cancellable delay, `logic` for a
311
+ reusable process, and `child` for a complete child statechart—all through
312
+ `Machine.invoke({...})`. The helper is an identity at runtime and preserves
313
+ owner-context and source-channel inference across lifecycle handlers, including
314
+ for state-dependent Effects:
208
315
 
209
- Expected failures should become internal events. An unrecovered invoke or child
210
- failure terminates the owning runtime.
316
+ ```ts
317
+ invoke: Machine.invoke({
318
+ id: "load-document",
319
+ effect: ({ state }) => loadDocument(state.documentId),
320
+ onDone: ({ output, target }) => target.full.Ready({ document: output }),
321
+ onFailure: ({ error, target }) => target.full.Failed({ message: error.message })
322
+ })
323
+ ```
324
+
325
+ A direct `invoke: { ... }` object is also supported when its lifecycle handlers
326
+ do not need source-derived context. Reuse one exported
327
+ `Machine.child(id, machine)` descriptor for invocation, `sendTo`, and child
328
+ lookup.
329
+
330
+ `onDone` is required for a non-`never` output, and `onFailure` is required for a
331
+ non-`never` typed error; each handler is omitted when its channel is `never`.
332
+ Defects, interruption, and source-construction failures terminate the owning
333
+ runtime. `effect: Effect.sleep(...)` is valid, but `after` keeps timers explicit
334
+ and makes static durations visible through activity inspection.
211
335
 
212
336
  ## Reactivity
213
337
 
@@ -230,6 +354,17 @@ The bridge exposes `ref`, `snapshot`, `state`, fail-aware `result`, writable
230
354
  equality-aware derivations. React applications using `@effect/atom-react` need
231
355
  a `RegistryProvider`.
232
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
+
233
368
  ## Persistence
234
369
 
235
370
  Logical snapshots can be validated for storage or transport:
@@ -260,14 +395,19 @@ The testing entrypoint provides complementary layers:
260
395
  import { MachineTest } from "@typeonce/effect-machine/testing"
261
396
 
262
397
  const trace = yield* MachineTest.run(Counter, {
263
- events: [Event.cases.Start.make({}), Event.cases.Increment.make({})]
398
+ events: [
399
+ { _tag: "Start" },
400
+ { _tag: "Increment" }
401
+ ]
264
402
  })
265
403
 
266
404
  yield* MachineTest.verify(Counter, trace)
267
405
  ```
268
406
 
269
- Pure planner tests do not execute invokes or time. Use a started machine and a
270
- probe when those semantics matter.
407
+ `MachineTest` scenarios retain decoded event values for model inspection, so
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.
271
411
 
272
412
  ## Entrypoints
273
413