@typeonce/effect-machine 0.14.1 → 0.16.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 (74) hide show
  1. package/README.md +99 -48
  2. package/dist/Machine.d.ts +436 -156
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +114 -57
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +2 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +4 -0
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  11. package/dist/internal/machine/executionPlan.js +6 -1
  12. package/dist/internal/machine/executionPlan.js.map +1 -1
  13. package/dist/internal/machine/invocation.d.ts +1 -1
  14. package/dist/internal/machine/invocation.d.ts.map +1 -1
  15. package/dist/internal/machine/invocation.js +25 -3
  16. package/dist/internal/machine/invocation.js.map +1 -1
  17. package/dist/internal/machine/invocationEvent.d.ts +8 -0
  18. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  19. package/dist/internal/machine/invocationEvent.js +8 -0
  20. package/dist/internal/machine/invocationEvent.js.map +1 -1
  21. package/dist/internal/machine/machine.d.ts +9 -4
  22. package/dist/internal/machine/machine.d.ts.map +1 -1
  23. package/dist/internal/machine/machine.js +131 -55
  24. package/dist/internal/machine/machine.js.map +1 -1
  25. package/dist/internal/machine/planner.d.ts +7 -0
  26. package/dist/internal/machine/planner.d.ts.map +1 -1
  27. package/dist/internal/machine/planner.js +20 -11
  28. package/dist/internal/machine/planner.js.map +1 -1
  29. package/dist/internal/machine/runtime.d.ts +1 -0
  30. package/dist/internal/machine/runtime.d.ts.map +1 -1
  31. package/dist/internal/machine/runtime.js +25 -16
  32. package/dist/internal/machine/runtime.js.map +1 -1
  33. package/dist/internal/machine/stateDefinition.d.ts +3 -1
  34. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  35. package/dist/internal/machine/stateDefinition.js +35 -0
  36. package/dist/internal/machine/stateDefinition.js.map +1 -1
  37. package/dist/internal/machine/topology.d.ts +11 -0
  38. package/dist/internal/machine/topology.d.ts.map +1 -1
  39. package/dist/internal/machine/topology.js +17 -6
  40. package/dist/internal/machine/topology.js.map +1 -1
  41. package/dist/internal/testing/machine/finiteModel.js +1 -1
  42. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  43. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
  44. package/dist/internal/testing/machine/transitionCoverage.js +6 -2
  45. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
  46. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  47. package/dist/internal/testing/machine/verification.js +6 -0
  48. package/dist/internal/testing/machine/verification.js.map +1 -1
  49. package/dist/testing/MachineTest.d.ts +9 -8
  50. package/dist/testing/MachineTest.d.ts.map +1 -1
  51. package/dist/testing/MachineTest.js +7 -7
  52. package/dist/testing/MachineTest.js.map +1 -1
  53. package/dist/unstable/cluster/ClusterMachine.d.ts +1 -1
  54. package/dist/unstable/cluster/ClusterMachine.js +1 -1
  55. package/dist/unstable/reactivity/AtomMachine.d.ts +3 -3
  56. package/dist/unstable/reactivity/AtomMachine.js +3 -3
  57. package/docs/agent-guide.md +207 -88
  58. package/package.json +1 -1
  59. package/src/Machine.ts +871 -256
  60. package/src/internal/machine/activities.ts +7 -0
  61. package/src/internal/machine/executionPlan.ts +6 -1
  62. package/src/internal/machine/invocation.ts +39 -4
  63. package/src/internal/machine/invocationEvent.ts +16 -0
  64. package/src/internal/machine/machine.ts +187 -67
  65. package/src/internal/machine/planner.ts +17 -3
  66. package/src/internal/machine/runtime.ts +61 -25
  67. package/src/internal/machine/stateDefinition.ts +41 -1
  68. package/src/internal/machine/topology.ts +31 -2
  69. package/src/internal/testing/machine/finiteModel.ts +1 -1
  70. package/src/internal/testing/machine/transitionCoverage.ts +6 -2
  71. package/src/internal/testing/machine/verification.ts +11 -0
  72. package/src/testing/MachineTest.ts +9 -7
  73. package/src/unstable/cluster/ClusterMachine.ts +1 -1
  74. package/src/unstable/reactivity/AtomMachine.ts +3 -3
@@ -35,15 +35,19 @@ currently coupled to the exact Effect peer version listed in its `package.json`.
35
35
  Use this order so inference has all schemas available when handlers are
36
36
  declared:
37
37
 
38
- 1. Domain schemas used by state and event fields.
39
- 2. Tagged schemas for states that own data.
40
- 3. Tagged public-event, internal-event, parent-event, and emitted-event schemas.
41
- 4. `Machine.defineStates`.
42
- 5. `Machine.make`, including input, `events`, `internalEvents`, `parentEvents`,
43
- `emittedEvents`, and the initial function.
44
- 6. One or more `.handle(...)` calls.
45
- 7. Child descriptors.
46
- 8. Runtime, Atom, or Cluster adapters.
38
+ 1. Domain schemas used by state, and by event fields when they are shared.
39
+ 2. `Machine.states`, using a tagged state union and `.cases` when state
40
+ schemas need to be reused.
41
+ 3. `Machine.events`, `Machine.internalEvents`, `Machine.emittedEvents`, and
42
+ `parentEvents`; pass `Schema.TaggedUnion({...})` or tagged classes directly.
43
+ 4. `Machine.make({...}).handle({...})`.
44
+ 5. Child descriptors, then runtime, Atom, or Cluster adapters.
45
+
46
+ `Machine.make` returns a reusable definition. Each `handle` call creates one
47
+ independent machine implementation and the result does not expose `handle`
48
+ again. Put one implementation's complete behavior in a single handler tree;
49
+ call `handle` again on the original definition for a separate production,
50
+ testing, or simulation variant.
47
51
 
48
52
  `Schema.TaggedUnion` avoids one class declaration per case:
49
53
 
@@ -54,22 +58,23 @@ const State = Schema.TaggedUnion({
54
58
  Failed: { message: Schema.String }
55
59
  })
56
60
 
57
- const Event = Schema.TaggedUnion({
58
- Save: {}
59
- })
60
-
61
- const InternalEvent = Schema.TaggedUnion({
62
- Saved: { id: Schema.String },
63
- SaveFailed: { message: Schema.String }
64
- })
65
-
66
- const States = Machine.defineStates(State.cases)
67
- const Events = Machine.events(Event)
68
- const InternalEvents = Machine.internalEvents(InternalEvent)
61
+ const States = Machine.states(State.cases)
62
+ export const Event = Machine.events(
63
+ Schema.TaggedUnion({
64
+ Save: {}
65
+ })
66
+ )
67
+ export const Internal = Machine.internalEvents(
68
+ Schema.TaggedUnion({
69
+ Saved: { id: Schema.String },
70
+ SaveFailed: { message: Schema.String }
71
+ })
72
+ )
69
73
  ```
70
74
 
71
- Pass these descriptors to `Machine.make` and export `Events` instead of the raw
72
- event schema. Construct new state values through the target or initial
75
+ Pass these descriptors to `Machine.make`; the event descriptor is the public
76
+ handle, so do not introduce a tagged-union binding used only by an event helper.
77
+ Construct new state values through the target or initial
73
78
  builder's `.from(...)` method. Both event constructors and state `.from(...)`
74
79
  defer schema construction until planning, so validation failures remain typed
75
80
  machine errors. Use
@@ -118,12 +123,13 @@ its extra control is required:
118
123
  - Bind a shared Atom runtime once with `AtomMachine.bind(runtime)`, then use the
119
124
  returned `make` or `resume`. Use `AtomMachine.make(machine)` and
120
125
  `AtomMachine.resume(machine, snapshot)` for service-free machines.
121
- - Use one invocation object: `effect` for one-shot work, `after` for a timer,
122
- `logic` for reusable process logic, and `child` for a complete child
126
+ - Use one invocation object: `effect` for one-shot work, `stream` for repeated
127
+ externally produced values, `after` for a timer, `logic` for reusable process
128
+ logic, and `child` for a complete child
123
129
  statechart. `Machine.invoke({...})` preserves owner state and source channels
124
- across sibling lifecycle handlers. Use `definition.invoke({...})` when a
125
- callback uses `self` or `parent`; the bound constructor preserves the
126
- definition's exact public input and `parentEvents` protocols.
130
+ across sibling lifecycle handlers. Inside `.handle(...)`, `self` and `parent`
131
+ use the owning definition's exact public input and `parentEvents` protocols.
132
+ The bound `definition.invoke({...})` form is equivalent, not required.
127
133
  - Use `Machine.child(id, machine)` for a complete statechart descriptor and
128
134
  `Machine.childAddress<Event>(id)` for a low-level process address. A logic
129
135
  invocation is addressable only when `Machine.invoke` receives that
@@ -133,11 +139,88 @@ its extra control is required:
133
139
 
134
140
  ## Atomic, compound, parallel, and history states
135
141
 
142
+ ### Inline topology by default; extract only repeated states
143
+
144
+ Prefer writing the complete topology inline in `Machine.states`. A one-off
145
+ compound or parallel area is easier to understand in place, and extracting it
146
+ does not improve its types. Use `Machine.state` only when the same active state
147
+ definition is mounted more than once. Tagged schemas are already reusable and
148
+ do not need `Machine.state`.
149
+
150
+ ```ts
151
+ type TeamSlot = 1 | 2 | 3 | 4 | 5 | 6
152
+
153
+ const TradingSlot = Machine.state({
154
+ initial: "Idle",
155
+ states: {
156
+ Idle: {},
157
+ InSession: State.cases.InSession,
158
+ Applying: State.cases.Applying
159
+ }
160
+ })
161
+
162
+ const States = Machine.states({
163
+ root: {
164
+ type: "parallel",
165
+ states: {
166
+ trading: {
167
+ type: "parallel",
168
+ states: {
169
+ slot1: TradingSlot,
170
+ slot2: TradingSlot,
171
+ slot3: TradingSlot,
172
+ slot4: TradingSlot,
173
+ slot5: TradingSlot,
174
+ slot6: TradingSlot
175
+ }
176
+ },
177
+ // Other explicit regions stay visible here.
178
+ }
179
+ }
180
+ })
181
+ ```
182
+
183
+ `Machine.state` accepts one active atomic, compound, or parallel node. It
184
+ checks child keys and the compound `initial` at the reusable definition. It is
185
+ not a second model builder, does not define handlers, and does not accept
186
+ history or choice nodes as roots. `Machine.states` remains the complete model
187
+ boundary and captures every mount independently.
188
+
189
+ For a finite family of paths, bind the template to that definition instead of
190
+ maintaining a parallel string table:
191
+
192
+ ```ts
193
+ const inSessionPath = <const Slot extends TeamSlot>(slot: Slot) =>
194
+ States.path(`root.trading.slot${slot}.InSession`)
195
+
196
+ States.matches(snapshot, inSessionPath(slot))
197
+ AtomMachine.matches(machineAtom, inSessionPath(slot))
198
+ ```
199
+
200
+ `States.path` is a compile-time identity helper. It accepts a literal or a
201
+ finite template-literal union only when every member is an active path in this
202
+ tree. Renaming a slot or child therefore breaks the path helper at its
203
+ definition rather than leaving a stale catalog.
204
+
205
+ Use the definition-bound snapshot type when a query genuinely needs the full
206
+ machine snapshot:
207
+
208
+ ```ts
209
+ const offeredIfSlot = (
210
+ snapshot: Machine.Snapshot<typeof States>,
211
+ slot: TeamSlot
212
+ ) =>
213
+ !States.matches(snapshot, inSessionPath(slot))
214
+ ```
215
+
216
+ Do not derive this type with `Parameters<typeof States.get>[0]`; that depends
217
+ on overload order and does not express ownership by the state definition.
218
+
136
219
  An active state does not need a schema unless it owns data. Omit `schema` for
137
220
  control-only atomic, compound, parallel, and final states:
138
221
 
139
222
  ```ts
140
- const States = Machine.defineStates({
223
+ const States = Machine.states({
141
224
  Idle: {},
142
225
  Form: {
143
226
  initial: "Editing",
@@ -190,7 +273,7 @@ Use a compound state when exactly one child phase is active. It must declare an
190
273
  ```ts
191
274
  const FormState = Schema.TaggedUnion({ Saving: { draft: Schema.String } })
192
275
 
193
- const FormStates = Machine.defineStates({
276
+ const FormStates = Machine.states({
194
277
  Form: {
195
278
  initial: "Editing",
196
279
  states: {
@@ -204,7 +287,7 @@ const FormStates = Machine.defineStates({
204
287
  Use a parallel state when every direct region is active:
205
288
 
206
289
  ```ts
207
- const ParallelStates = Machine.defineStates({
290
+ const ParallelStates = Machine.states({
208
291
  Screen: {
209
292
  type: "parallel",
210
293
  states: {
@@ -231,13 +314,13 @@ Every parallel region needs an active state in initial and full snapshot
231
314
  builders. The same rule applies when a local or branch target enters an
232
315
  inactive nested parallel state.
233
316
 
234
- Use `type: "final"` for a terminal leaf in `Machine.defineStates`. A final
317
+ Use `type: "final"` for a terminal leaf in `Machine.states`. A final
235
318
  child completes its compound parent. Put `onDone` on that completed parent,
236
319
  never on the final leaf. The definition owns the output schema and the handler
237
320
  computes its value:
238
321
 
239
322
  ```ts
240
- const States = Machine.defineStates({
323
+ const States = Machine.states({
241
324
  Done: {
242
325
  schema: State.cases.Done,
243
326
  type: "final",
@@ -311,7 +394,7 @@ should remember. It has no schema, is excluded from active state identifiers,
311
394
  and is addressed only through `target.history`:
312
395
 
313
396
  ```ts
314
- const States = Machine.defineStates({
397
+ const States = Machine.states({
315
398
  checkout: {
316
399
  schema: Checkout,
317
400
  initial: "shipping",
@@ -455,7 +538,7 @@ through `schema.makeEffect`, including refinements.
455
538
 
456
539
  ## Reading state and structural ancestors
457
540
 
458
- `Machine.defineStates` returns typed helpers:
541
+ `Machine.states` returns typed helpers:
459
542
 
460
543
  ```ts
461
544
  States.get(snapshot, "Route.Ready")
@@ -496,21 +579,14 @@ microstep, before any selected transition is applied:
496
579
 
497
580
  ```ts
498
581
  BufferReady: Machine.transition({
499
- cases: (branch) => [
500
- branch({
501
- title: "online",
502
- when: ({ snapshot }) =>
503
- States.matches(snapshot, "Player.Network.Online")
504
- ? Option.some(undefined)
505
- : Option.none(),
506
- target: (to) => to.local.Playing(),
507
- resolve: ({ target }) => target.from()
508
- })
509
- ],
510
- otherwise: {
511
- target: (to) => to.none(),
512
- resolve: () => undefined
513
- }
582
+ branches: (to) => ({
583
+ online: { target: to.local.Playing() },
584
+ unchanged: { target: to.none() }
585
+ }),
586
+ resolve: ({ snapshot, select }) =>
587
+ States.matches(snapshot, "Player.Network.Online")
588
+ ? select.online.from()
589
+ : select.unchanged()
514
590
  })
515
591
  ```
516
592
 
@@ -566,30 +642,30 @@ synchronously:
566
642
 
567
643
  ```ts
568
644
  Submit: Machine.transition({
569
- cases: (branch) => [
570
- branch({
571
- title: "valid",
572
- when: ({ state }) => state.valid ? Option.some(state.draft) : Option.none(),
573
- target: (to) => to.local.Saving(),
574
- resolve: ({ match, target }) => target.from({ draft: match })
575
- })
576
- ],
577
- otherwise: {
578
- target: (to) => to.none(),
579
- resolve: () => undefined
580
- }
645
+ branches: (to) => ({
646
+ valid: { target: to.local.Saving() },
647
+ invalid: { target: to.none() }
648
+ }),
649
+ resolve: ({ state, select }) => state.valid
650
+ ? select.valid.from({ draft: state.draft })
651
+ : select.invalid()
581
652
  })
582
653
  ```
583
654
 
584
655
  Every installed event, `always`, `onDone`, choice, and invoke lifecycle handler
585
- must use `Machine.transition`. Each direct branch declares one `target`; a
586
- conditional transition declares ordered `cases` and a required `otherwise`.
587
- Construct each case with the locally supplied `branch` function. Every call
588
- independently infers its `when` match and target, so a transition may declare
589
- any number of heterogeneous cases without losing resolver inference. `when`
590
- returns `Option.some(match)` to select a case and expose `match` in its resolver.
591
- Selecting `to.none()` handles the transition without a destination, while
592
- retaining queued commands, raised events, and emitted events.
656
+ must use `Machine.transition`. A direct transition declares one `target`. A
657
+ branching transition declares every possible target in a named `branches`
658
+ record, then uses ordinary TypeScript control flow in `resolve` to return one
659
+ typed `select` builder. Branch keys are stable testing and inspection identities;
660
+ an optional `title` controls presentation and otherwise defaults to the key.
661
+ Selecting a branch whose target is `to.none()` handles the transition without a
662
+ destination while retaining queued commands, raised events, and emitted events.
663
+
664
+ The `branches` callback runs once when handlers are installed. Its record uses
665
+ the deterministic ECMAScript property order for presentation and `branchIndex`;
666
+ array-index and symbol keys are rejected. Treat the string key as semantic:
667
+ reordering named properties may change their display index, but visualizers,
668
+ coverage, and trace verification identify each branch by its key.
593
669
 
594
670
  `reenter: true` remains meaningful with `to.none()`: the source exits and
595
671
  enters again while its logical configuration is retained.
@@ -764,8 +840,17 @@ an event for the parent. Both operations validate their schemas.
764
840
  union handled inside the statechart:
765
841
 
766
842
  ```ts
767
- const Events = Machine.events(Event)
768
- const InternalEvents = Machine.internalEvents(InternalEvent)
843
+ const Events = Machine.events(
844
+ Schema.TaggedUnion({
845
+ Save: {}
846
+ })
847
+ )
848
+ const InternalEvents = Machine.internalEvents(
849
+ Schema.TaggedUnion({
850
+ Saved: { id: Schema.String },
851
+ SaveFailed: { message: Schema.String }
852
+ })
853
+ )
769
854
 
770
855
  const definition = Machine.make({
771
856
  states: States.states,
@@ -852,6 +937,38 @@ needs `state`, `containingState`, `ancestors`, or the entry `event`. Source cons
852
937
  errors, defects, and interruption are machine failures rather than a second
853
938
  phase in `onFailure`.
854
939
 
940
+ Use a Stream invocation for repeated values that are not themselves machine
941
+ events. `onElement` maps each value into an owner transition, while `onDone`
942
+ handles normal Stream completion and `onFailure` handles the typed Stream error:
943
+
944
+ ```ts
945
+ invoke: Machine.invoke({
946
+ id: "broadcast-channel",
947
+ stream: () => messages,
948
+ onElement: {
949
+ target: Machine.targetless,
950
+ resolve: ({ element }, enqueue) => {
951
+ enqueue.raise(Events.MessageReceived({ message: element }))
952
+ }
953
+ },
954
+ onDone: { target: Machine.targetless },
955
+ onFailure: Machine.transition({
956
+ target: (to) => to.full.Disconnected(),
957
+ resolve: ({ error, target }) => target.from({ error })
958
+ })
959
+ })
960
+ ```
961
+
962
+ Element delivery is owner-scoped and backpressured: the Stream pulls again only
963
+ after the selected parent macrostep commits. Exiting or reentering the owner
964
+ interrupts the Stream and runs its finalizers. A later entry starts a fresh
965
+ Stream. Stream defects and self-interruption fail the owning machine.
966
+
967
+ The direct `{ target: Machine.targetless, resolve }` shorthand is available
968
+ when a transition only enqueues commands. It is non-reentering and the resolver
969
+ must return `undefined`. Keep `Machine.transition(...)` for full state selection,
970
+ named branches, or reentry.
971
+
855
972
  When a source function reads `state`, `containingState`, `ancestors`, or the entry `event`,
856
973
  `Machine.invoke` infers that owner context and the returned Effect's output,
857
974
  error, and service channels together. No return annotation is needed:
@@ -871,29 +988,30 @@ invoke: Machine.invoke({
871
988
  })
872
989
  ```
873
990
 
874
- The standalone constructor cannot know the owning machine's input protocols,
875
- so its `self` and `parent` references are non-sendable. When a source sends
876
- through either reference, use the owning definition's bound constructor:
991
+ Inside `.handle(...)`, the constructor receives the owning machine's public
992
+ input and `parentEvents` protocols contextually. Sources and lifecycle handlers
993
+ can send through `self` and `parent` without naming the definition:
877
994
 
878
995
  ```ts
879
- const definition = Machine.make({
996
+ const machine = Machine.make({
880
997
  events: Commands,
881
998
  internalEvents: InternalEvents,
882
999
  parentEvents: ParentEvents,
883
1000
  // ...
884
- })
885
-
886
- const machine = definition.handle({
1001
+ }).handle({
887
1002
  Saving: {
888
- invoke: definition.invoke({
1003
+ invoke: Machine.invoke({
889
1004
  id: "notify-parent",
890
- effect: ({ parent }) =>
891
- parent === undefined
892
- ? Effect.void
893
- : parent.send(ParentEvents.SaveStarted()),
1005
+ effect: () => saveDocument,
894
1006
  onDone: Machine.transition({
895
1007
  target: (to) => to.none(),
896
- resolve: () => undefined
1008
+ resolve: ({ parent, self }, enqueue) => {
1009
+ enqueue.sendTo(self, Commands.Save())
1010
+ if (parent !== undefined) {
1011
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
1012
+ }
1013
+ return undefined
1014
+ }
897
1015
  }),
898
1016
  onFailure: Machine.transition({
899
1017
  target: (to) => to.none(),
@@ -904,8 +1022,9 @@ const machine = definition.handle({
904
1022
  })
905
1023
  ```
906
1024
 
907
- A direct `invoke: { ... }` object remains available when lifecycle handlers do
908
- not need source-derived context.
1025
+ The machine-bound `definition.invoke(...)` form remains equivalent when the
1026
+ definition is already named. A direct `invoke: { ... }` object remains available
1027
+ when lifecycle handlers do not need source-derived context.
909
1028
 
910
1029
  A cancellable timer uses the same object:
911
1030
 
@@ -1314,7 +1433,7 @@ repeat the final marker in this handler.
1314
1433
 
1315
1434
  ### `type: "final"` is rejected by `handle`
1316
1435
 
1317
- Move it to `Machine.defineStates`. Definitions own statechart topology;
1436
+ Move it to `Machine.states`. Definitions own statechart topology;
1318
1437
  handlers own behavior.
1319
1438
 
1320
1439
  ### Parent property does not exist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/effect-machine",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "description": "Schema-first state machines and statecharts for Effect",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {