@typeonce/effect-machine 0.14.1 → 0.15.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.
@@ -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
@@ -121,9 +126,9 @@ its extra control is required:
121
126
  - Use one invocation object: `effect` for one-shot work, `after` for a timer,
122
127
  `logic` for reusable process logic, and `child` for a complete child
123
128
  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.
129
+ across sibling lifecycle handlers. Inside `.handle(...)`, `self` and `parent`
130
+ use the owning definition's exact public input and `parentEvents` protocols.
131
+ The bound `definition.invoke({...})` form is equivalent, not required.
127
132
  - Use `Machine.child(id, machine)` for a complete statechart descriptor and
128
133
  `Machine.childAddress<Event>(id)` for a low-level process address. A logic
129
134
  invocation is addressable only when `Machine.invoke` receives that
@@ -133,11 +138,88 @@ its extra control is required:
133
138
 
134
139
  ## Atomic, compound, parallel, and history states
135
140
 
141
+ ### Inline topology by default; extract only repeated states
142
+
143
+ Prefer writing the complete topology inline in `Machine.states`. A one-off
144
+ compound or parallel area is easier to understand in place, and extracting it
145
+ does not improve its types. Use `Machine.state` only when the same active state
146
+ definition is mounted more than once. Tagged schemas are already reusable and
147
+ do not need `Machine.state`.
148
+
149
+ ```ts
150
+ type TeamSlot = 1 | 2 | 3 | 4 | 5 | 6
151
+
152
+ const TradingSlot = Machine.state({
153
+ initial: "Idle",
154
+ states: {
155
+ Idle: {},
156
+ InSession: State.cases.InSession,
157
+ Applying: State.cases.Applying
158
+ }
159
+ })
160
+
161
+ const States = Machine.states({
162
+ root: {
163
+ type: "parallel",
164
+ states: {
165
+ trading: {
166
+ type: "parallel",
167
+ states: {
168
+ slot1: TradingSlot,
169
+ slot2: TradingSlot,
170
+ slot3: TradingSlot,
171
+ slot4: TradingSlot,
172
+ slot5: TradingSlot,
173
+ slot6: TradingSlot
174
+ }
175
+ },
176
+ // Other explicit regions stay visible here.
177
+ }
178
+ }
179
+ })
180
+ ```
181
+
182
+ `Machine.state` accepts one active atomic, compound, or parallel node. It
183
+ checks child keys and the compound `initial` at the reusable definition. It is
184
+ not a second model builder, does not define handlers, and does not accept
185
+ history or choice nodes as roots. `Machine.states` remains the complete model
186
+ boundary and captures every mount independently.
187
+
188
+ For a finite family of paths, bind the template to that definition instead of
189
+ maintaining a parallel string table:
190
+
191
+ ```ts
192
+ const inSessionPath = <const Slot extends TeamSlot>(slot: Slot) =>
193
+ States.path(`root.trading.slot${slot}.InSession`)
194
+
195
+ States.matches(snapshot, inSessionPath(slot))
196
+ AtomMachine.matches(machineAtom, inSessionPath(slot))
197
+ ```
198
+
199
+ `States.path` is a compile-time identity helper. It accepts a literal or a
200
+ finite template-literal union only when every member is an active path in this
201
+ tree. Renaming a slot or child therefore breaks the path helper at its
202
+ definition rather than leaving a stale catalog.
203
+
204
+ Use the definition-bound snapshot type when a query genuinely needs the full
205
+ machine snapshot:
206
+
207
+ ```ts
208
+ const offeredIfSlot = (
209
+ snapshot: Machine.Snapshot<typeof States>,
210
+ slot: TeamSlot
211
+ ) =>
212
+ !States.matches(snapshot, inSessionPath(slot))
213
+ ```
214
+
215
+ Do not derive this type with `Parameters<typeof States.get>[0]`; that depends
216
+ on overload order and does not express ownership by the state definition.
217
+
136
218
  An active state does not need a schema unless it owns data. Omit `schema` for
137
219
  control-only atomic, compound, parallel, and final states:
138
220
 
139
221
  ```ts
140
- const States = Machine.defineStates({
222
+ const States = Machine.states({
141
223
  Idle: {},
142
224
  Form: {
143
225
  initial: "Editing",
@@ -190,7 +272,7 @@ Use a compound state when exactly one child phase is active. It must declare an
190
272
  ```ts
191
273
  const FormState = Schema.TaggedUnion({ Saving: { draft: Schema.String } })
192
274
 
193
- const FormStates = Machine.defineStates({
275
+ const FormStates = Machine.states({
194
276
  Form: {
195
277
  initial: "Editing",
196
278
  states: {
@@ -204,7 +286,7 @@ const FormStates = Machine.defineStates({
204
286
  Use a parallel state when every direct region is active:
205
287
 
206
288
  ```ts
207
- const ParallelStates = Machine.defineStates({
289
+ const ParallelStates = Machine.states({
208
290
  Screen: {
209
291
  type: "parallel",
210
292
  states: {
@@ -231,13 +313,13 @@ Every parallel region needs an active state in initial and full snapshot
231
313
  builders. The same rule applies when a local or branch target enters an
232
314
  inactive nested parallel state.
233
315
 
234
- Use `type: "final"` for a terminal leaf in `Machine.defineStates`. A final
316
+ Use `type: "final"` for a terminal leaf in `Machine.states`. A final
235
317
  child completes its compound parent. Put `onDone` on that completed parent,
236
318
  never on the final leaf. The definition owns the output schema and the handler
237
319
  computes its value:
238
320
 
239
321
  ```ts
240
- const States = Machine.defineStates({
322
+ const States = Machine.states({
241
323
  Done: {
242
324
  schema: State.cases.Done,
243
325
  type: "final",
@@ -311,7 +393,7 @@ should remember. It has no schema, is excluded from active state identifiers,
311
393
  and is addressed only through `target.history`:
312
394
 
313
395
  ```ts
314
- const States = Machine.defineStates({
396
+ const States = Machine.states({
315
397
  checkout: {
316
398
  schema: Checkout,
317
399
  initial: "shipping",
@@ -455,7 +537,7 @@ through `schema.makeEffect`, including refinements.
455
537
 
456
538
  ## Reading state and structural ancestors
457
539
 
458
- `Machine.defineStates` returns typed helpers:
540
+ `Machine.states` returns typed helpers:
459
541
 
460
542
  ```ts
461
543
  States.get(snapshot, "Route.Ready")
@@ -764,8 +846,17 @@ an event for the parent. Both operations validate their schemas.
764
846
  union handled inside the statechart:
765
847
 
766
848
  ```ts
767
- const Events = Machine.events(Event)
768
- const InternalEvents = Machine.internalEvents(InternalEvent)
849
+ const Events = Machine.events(
850
+ Schema.TaggedUnion({
851
+ Save: {}
852
+ })
853
+ )
854
+ const InternalEvents = Machine.internalEvents(
855
+ Schema.TaggedUnion({
856
+ Saved: { id: Schema.String },
857
+ SaveFailed: { message: Schema.String }
858
+ })
859
+ )
769
860
 
770
861
  const definition = Machine.make({
771
862
  states: States.states,
@@ -871,29 +962,30 @@ invoke: Machine.invoke({
871
962
  })
872
963
  ```
873
964
 
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:
965
+ Inside `.handle(...)`, the constructor receives the owning machine's public
966
+ input and `parentEvents` protocols contextually. Sources and lifecycle handlers
967
+ can send through `self` and `parent` without naming the definition:
877
968
 
878
969
  ```ts
879
- const definition = Machine.make({
970
+ const machine = Machine.make({
880
971
  events: Commands,
881
972
  internalEvents: InternalEvents,
882
973
  parentEvents: ParentEvents,
883
974
  // ...
884
- })
885
-
886
- const machine = definition.handle({
975
+ }).handle({
887
976
  Saving: {
888
- invoke: definition.invoke({
977
+ invoke: Machine.invoke({
889
978
  id: "notify-parent",
890
- effect: ({ parent }) =>
891
- parent === undefined
892
- ? Effect.void
893
- : parent.send(ParentEvents.SaveStarted()),
979
+ effect: () => saveDocument,
894
980
  onDone: Machine.transition({
895
981
  target: (to) => to.none(),
896
- resolve: () => undefined
982
+ resolve: ({ parent, self }, enqueue) => {
983
+ enqueue.sendTo(self, Commands.Save())
984
+ if (parent !== undefined) {
985
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
986
+ }
987
+ return undefined
988
+ }
897
989
  }),
898
990
  onFailure: Machine.transition({
899
991
  target: (to) => to.none(),
@@ -904,8 +996,9 @@ const machine = definition.handle({
904
996
  })
905
997
  ```
906
998
 
907
- A direct `invoke: { ... }` object remains available when lifecycle handlers do
908
- not need source-derived context.
999
+ The machine-bound `definition.invoke(...)` form remains equivalent when the
1000
+ definition is already named. A direct `invoke: { ... }` object remains available
1001
+ when lifecycle handlers do not need source-derived context.
909
1002
 
910
1003
  A cancellable timer uses the same object:
911
1004
 
@@ -1314,7 +1407,7 @@ repeat the final marker in this handler.
1314
1407
 
1315
1408
  ### `type: "final"` is rejected by `handle`
1316
1409
 
1317
- Move it to `Machine.defineStates`. Definitions own statechart topology;
1410
+ Move it to `Machine.states`. Definitions own statechart topology;
1318
1411
  handlers own behavior.
1319
1412
 
1320
1413
  ### 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.15.0",
4
4
  "description": "Schema-first state machines and statecharts for Effect",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {