@typeonce/effect-machine 0.17.0 → 0.19.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 (45) hide show
  1. package/README.md +133 -80
  2. package/dist/Machine.d.ts +434 -297
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +13 -55
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/cluster.d.ts +2 -2
  7. package/dist/internal/machine/cluster.d.ts.map +1 -1
  8. package/dist/internal/machine/cluster.js +2 -1
  9. package/dist/internal/machine/cluster.js.map +1 -1
  10. package/dist/internal/machine/invocation.d.ts.map +1 -1
  11. package/dist/internal/machine/invocation.js +1 -1
  12. package/dist/internal/machine/invocation.js.map +1 -1
  13. package/dist/internal/machine/machine.d.ts.map +1 -1
  14. package/dist/internal/machine/machine.js +48 -10
  15. package/dist/internal/machine/machine.js.map +1 -1
  16. package/dist/internal/machine/serialization.d.ts.map +1 -1
  17. package/dist/internal/machine/serialization.js +75 -18
  18. package/dist/internal/machine/serialization.js.map +1 -1
  19. package/dist/internal/testing/machine/verification.d.ts +1 -1
  20. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  21. package/dist/internal/testing/machine/verification.js +9 -6
  22. package/dist/internal/testing/machine/verification.js.map +1 -1
  23. package/dist/testing/MachineTest.d.ts +13 -10
  24. package/dist/testing/MachineTest.d.ts.map +1 -1
  25. package/dist/testing/MachineTest.js +5 -3
  26. package/dist/testing/MachineTest.js.map +1 -1
  27. package/dist/unstable/cluster/ClusterMachine.d.ts +22 -2
  28. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  29. package/dist/unstable/cluster/ClusterMachine.js +4 -0
  30. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  31. package/dist/unstable/reactivity/AtomMachine.d.ts +1 -1
  32. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  33. package/docs/agent-guide.md +189 -83
  34. package/package.json +4 -4
  35. package/src/Machine.ts +845 -1051
  36. package/src/internal/machine/cluster.ts +4 -1
  37. package/src/internal/machine/invocation.ts +1 -1
  38. package/src/internal/machine/machine.ts +64 -8
  39. package/src/internal/machine/serialization.ts +100 -25
  40. package/src/internal/testing/machine/exploration.ts +1 -1
  41. package/src/internal/testing/machine/trace.ts +1 -1
  42. package/src/internal/testing/machine/verification.ts +16 -11
  43. package/src/testing/MachineTest.ts +13 -10
  44. package/src/unstable/cluster/ClusterMachine.ts +51 -1
  45. package/src/unstable/reactivity/AtomMachine.ts +1 -1
@@ -54,12 +54,15 @@ testing, or simulation variant.
54
54
 
55
55
  ```ts
56
56
  const State = Schema.TaggedUnion({
57
- Idle: {},
58
57
  Saving: { draft: Draft },
59
58
  Failed: { message: Schema.String }
60
59
  })
61
60
 
62
- const States = Machine.states(State.cases)
61
+ const States = Machine.states({
62
+ Idle: {},
63
+ Saving: State.cases.Saving,
64
+ Failed: State.cases.Failed
65
+ })
63
66
  export const Event = Machine.events(
64
67
  Schema.TaggedUnion({
65
68
  Save: {}
@@ -125,22 +128,38 @@ its extra control is required:
125
128
  - Bind a shared Atom runtime once with `AtomMachine.bind(runtime)`, then use the
126
129
  returned `make` or `resume`. Use `AtomMachine.make(machine)` and
127
130
  `AtomMachine.resume(machine, snapshot)` for service-free machines.
128
- - Use one invocation object: `effect` for one-shot work, `stream` for repeated
129
- externally produced values, `after` for a timer, `logic` for reusable process
130
- logic, and `child` for a complete child
131
- statechart. `Machine.invoke({...})` preserves owner state and source channels
132
- across sibling lifecycle handlers. Inside `.handle(...)`, `self` and any
133
- declared `parent` use the owning definition's exact protocols; no intermediate
134
- definition method is required.
131
+ - Use the state-local `invoke: (from) => ...` selector: `from.effect` for
132
+ one-shot work, `from.stream` for repeated externally produced values,
133
+ `from.timer` for a timer, `from.logic` for reusable process logic, and
134
+ `from.child` for a complete child statechart. Its chain preserves owner state
135
+ and source channels across lifecycle handlers. Inside `.handle(...)`, `self`
136
+ and any declared `parent` use the owning definition's exact protocols.
135
137
  - Use `Machine.child(id, machine)` for a complete statechart descriptor and
136
138
  `Machine.childAddress<Event>(id)` for a low-level process address. A logic
137
- invocation is addressable only when `Machine.invoke` receives that
138
- address explicitly.
139
+ invocation is addressable only when `from.logic` receives that address
140
+ explicitly.
139
141
  - Use the callback's `enqueue` argument for `raise`, `emit`, `sendTo`, and
140
142
  `stop`. These operations record closed machine commands and do not run Effects.
141
143
 
142
144
  ## Atomic, compound, parallel, and history states
143
145
 
146
+ ### Topology is a validity boundary
147
+
148
+ Design the state tree so invalid domain situations cannot be constructed. A
149
+ parallel node is not merely a convenient grouping of related concepts: it
150
+ declares the Cartesian product of its regions. Every combination must be
151
+ meaningful in snapshots, explicit targets, decoding, and resume.
152
+
153
+ If a handler reads a sibling region to decide whether entering its target is
154
+ legal, treat that as a topology smell and try a compound hierarchy first. Do
155
+ not move the same invariant into a disabled UI control, redundant event field,
156
+ or invoked-service failure. Cross-region reads remain useful for coordinating
157
+ genuinely independent regions and for projecting snapshots into views.
158
+
159
+ Place an invoked Effect or resource-dependent state beneath the state that
160
+ guarantees the resource exists. Exiting the owner should structurally exit and
161
+ interrupt all dependent work.
162
+
144
163
  ### Inline topology by default; extract only repeated states
145
164
 
146
165
  Prefer writing the complete topology inline in `Machine.states`. A one-off
@@ -218,8 +237,25 @@ const offeredIfSlot = (
218
237
  Do not derive this type with `Parameters<typeof States.get>[0]`; that depends
219
238
  on overload order and does not express ownership by the state definition.
220
239
 
240
+ The same extractor accepts a machine when that is the object exported at the
241
+ consumer boundary. Use `Value` for a decoded schema-backed state payload and
242
+ `SnapshotAt` for the snapshot rooted at one active path:
243
+
244
+ ```ts
245
+ type Complete = Machine.Snapshot<typeof machine>
246
+ type Session = Machine.Value<typeof States, "root.trading.InSession">
247
+ type Trading = Machine.SnapshotAt<typeof machine, "root.trading">
248
+ ```
249
+
250
+ `Value` accepts only paths that own a schema, matching `States.get`.
251
+ `SnapshotAt` also accepts structural paths, matching `States.getSnapshot`.
252
+ Both reject stale or misspelled paths. Prefer these definition- or
253
+ machine-bound forms over `.cases.Case.Type`, `typeof States.states`, or
254
+ composing `Machine.Machine.States` with raw-tree path extractors.
255
+
221
256
  An active state does not need a schema unless it owns data. Omit `schema` for
222
- control-only atomic, compound, parallel, and final states:
257
+ control-only atomic, compound, parallel, and final states. In particular, use
258
+ `{}` instead of an empty tagged-union case or tagged class:
223
259
 
224
260
  ```ts
225
261
  const States = Machine.states({
@@ -243,11 +279,7 @@ in snapshots. They do not have a state value:
243
279
  ```ts
244
280
  Idle: {
245
281
  on: {
246
- Start: (to) =>
247
- to.full.Form.initial.resolve(({ state, target }) => {
248
- // state: undefined
249
- return target.from((form) => form.Editing.from())
250
- })
282
+ Start: (to) => to.full.Form.initial
251
283
  }
252
284
  }
253
285
 
@@ -262,6 +294,13 @@ also omitted from `ancestors`; an immediate structural containing state is
262
294
  typed as `undefined`. Add `schema` when a state begins to own data or needs runtime
263
295
  validation and persistence for that data.
264
296
 
297
+ Declare data-bearing states together with the named
298
+ `const State = Schema.TaggedUnion(...)` pattern, then reference `State.cases`
299
+ from the topology. This keeps the state value protocol visible and reusable.
300
+ Use a named `Schema.TaggedClass` instead when a standalone state benefits from
301
+ class identity. Do not bury one-off tagged schema declarations inside
302
+ `Machine.states`.
303
+
265
304
  Use an atomic state when no child phase can be active beneath it.
266
305
 
267
306
  Use a compound state when exactly one child phase is active. It must declare an
@@ -311,6 +350,11 @@ Every parallel region needs an active state in initial and full snapshot
311
350
  builders. The same rule applies when a local or branch target enters an
312
351
  inactive nested parallel state.
313
352
 
353
+ This is also a semantic product: `Online + Closed`, `Online + Open`,
354
+ `Offline + Closed`, and `Offline + Open` are all valid configurations in the
355
+ example above. If even one combination must be prevented for correctness, use
356
+ a compound hierarchy or redesign the regions.
357
+
314
358
  Use `type: "final"` for a terminal leaf in `Machine.states`. A final
315
359
  child completes its compound parent. Put `onDone` on that completed parent,
316
360
  never on the final leaf. The definition owns the output schema and the handler
@@ -328,7 +372,7 @@ const States = Machine.states({
328
372
  const machine = Machine.make({
329
373
  states: States.states,
330
374
  events: Machine.events(),
331
- initial: (to) => to.Done().resolve(({ target }) => target.from())
375
+ initial: (to) => to.Done()
332
376
  }).handle({
333
377
  Done: {
334
378
  output: () => "done"
@@ -348,6 +392,20 @@ with `.initial`. This is available on top-level state methods under
348
392
  Open: (to) => to.full.opened.initial.resolve(({ target }) => target.from({ teamId: "team-1" }))
349
393
  ```
350
394
 
395
+ Return the `.initial` transition directly when the selected state owns no data
396
+ and the transition has no commands to enqueue:
397
+
398
+ ```ts
399
+ Close: (to) => to.full.closed.initial
400
+ ```
401
+
402
+ Do not manually reconstruct the declared initial descendants at ordinary entry
403
+ transitions. Reserve explicit descendant builders for deliberately non-default
404
+ configurations and for replacing an already-active parallel root with one
405
+ complete canonical configuration. `Machine.make({ initial })` still constructs
406
+ the first complete snapshot through its initial selector; that selector
407
+ statically restricts a compound node to its declared initial child.
408
+
351
409
  The definition-time `.initial` property is a topology value. The exact
352
410
  resolver `target` is still a callable runtime builder.
353
411
 
@@ -425,11 +483,11 @@ checkout: {
425
483
  Target it without a value:
426
484
 
427
485
  ```ts
428
- Resume: (to) => to.history.checkout.exact.resolve(({ target }) => target())
486
+ Resume: (to) => to.history.checkout.exact
429
487
  ```
430
488
 
431
- Each declared history leaf is a topology value; the resolver's selected
432
- history builder remains callable to construct restoration evidence.
489
+ Each declared history leaf is a topology value and can be returned directly
490
+ when no resolver work is needed.
433
491
 
434
492
  Deep history restores the complete remembered subtree and its decoded values.
435
493
  Shallow history restores only parent and direct-child values. If the remembered
@@ -511,8 +569,11 @@ Refresh: (to) =>
511
569
  to.full.Ready().resolve(({ state, target }) => target.from({ value: state.value }), { reenter: true })
512
570
  ```
513
571
 
514
- When no resolver is needed, use the selected target directly and append
515
- `.reenter()` only when restart semantics are intentional:
572
+ When no resolver is needed and the selected builder supports zero-argument
573
+ construction, return the selected target directly. This applies the same
574
+ default construction as `target.from()`; the compiler rejects the shorthand
575
+ when state data or nested configuration is required. Append `.reenter()` only
576
+ when restart semantics are intentional:
516
577
 
517
578
  ```ts
518
579
  Finish: (to) => to.full.Done()
@@ -609,6 +670,11 @@ Use the existing `States.matches`, `States.get`, `States.getWithParents`, and
609
670
  selected in one microstep receive the same capture. Synchronous handlers use
610
671
  that captured value and cannot consult live runtime state later.
611
672
 
673
+ Before using a cross-region read to permit or reject a target, verify that all
674
+ combinations of the parallel regions are valid. If the check excludes an
675
+ invalid combination, move the invariant into a compound hierarchy. Observer,
676
+ view, diagnostic, and test queries do not have this concern.
677
+
612
678
  Do not expect `snapshot` in entry, exit, invoke, initializer, history-default,
613
679
  or choice contexts. Choice is an important soundness boundary: a startup or
614
680
  chained choice can run without a complete stable configuration containing the
@@ -886,7 +952,7 @@ const definition = Machine.make({
886
952
  states: States.states,
887
953
  events: Events,
888
954
  internalEvents: InternalEvents,
889
- initial: (to) => to.Idle().resolve(({ target }) => target.from())
955
+ initial: (to) => to.Idle()
890
956
  })
891
957
  ```
892
958
 
@@ -919,8 +985,14 @@ Use the exported utility types when another API must preserve the boundary:
919
985
  ```ts
920
986
  type PublicEvent = Machine.Machine.InputEvent<typeof definition>
921
987
  type AnyHandledEvent = Machine.Machine.Event<typeof definition>
988
+ type StartupInput = Machine.Machine.Input<typeof definition>
989
+ type StartupInputSchema = Machine.Machine.InputSchema<typeof definition>
922
990
  ```
923
991
 
992
+ `Input` is the decoded value accepted at startup. It is `never` for a machine
993
+ whose input schema is `Schema.Void`; use `InputSchema` only when an API needs
994
+ the schema object itself.
995
+
924
996
  `MachineRef.send`, `machineAtom.send`, and `Machine.plan` accept decoded public
925
997
  events or constructions returned by `Machine.events`. Transition handlers
926
998
  receive only decoded events. Raised events additionally accept constructions
@@ -933,18 +1005,28 @@ Cluster RPC payloads are additionally decoded against the public `events`
933
1005
  schemas at the transport boundary. Never repeat an `_tag` within a list or
934
1006
  across both configuration lists.
935
1007
 
1008
+ Do not extract `enqueue`, target builders, transition contexts, command or
1009
+ inspection unions, or event-construction `ReturnType`s into application helper
1010
+ APIs. Keep commands inside transition resolvers, where the owning state,
1011
+ protocols, references, and capabilities are inferred. Likewise, do not add
1012
+ Atom `State` or `Event` aliases: selectors infer from their bridge, while
1013
+ consumer props use `Snapshot`, `Value`, or `InputEvent` from the exported state
1014
+ definition or machine.
1015
+
936
1016
  ## Recoverable state-scoped work
937
1017
 
938
- Use `Machine.invoke` with an `effect` for one-shot work. Lifecycle callbacks
939
- receive the typed Effect channels and can transition directly:
1018
+ Use `from.effect` for one-shot work. Lifecycle callbacks receive the typed
1019
+ Effect channels and can transition directly:
940
1020
 
941
1021
  ```ts
942
- invoke: Machine.invoke({
943
- id: "save",
944
- effect: () => SaveService.save(draft),
945
- onDone: (to) => to.full.Saved().resolve(({ output, target }) => target.from({ entry: output })),
946
- onFailure: (to) =>
947
- to.full.SaveFailed().resolve(({ error, target }) => target.from({ message: error.message }))
1022
+ machine.handle({
1023
+ Saving: {
1024
+ invoke: (from) =>
1025
+ from.effect("save", () => SaveService.save(draft))
1026
+ .onDone((to) => to.full.Saved().resolve(({ output, target }) => target.from({ entry: output })))
1027
+ .onFailure((to) =>
1028
+ to.full.SaveFailed().resolve(({ error, target }) => target.from({ message: error.message })))
1029
+ }
948
1030
  })
949
1031
  ```
950
1032
 
@@ -964,15 +1046,17 @@ events. `onElement` maps each value into an owner transition, while `onDone`
964
1046
  handles normal Stream completion and `onFailure` handles the typed Stream error:
965
1047
 
966
1048
  ```ts
967
- invoke: Machine.invoke({
968
- id: "broadcast-channel",
969
- stream: () => messages,
970
- onElement: (to) =>
971
- to.none.resolve(({ element }, enqueue) => {
972
- enqueue.raise(Events.MessageReceived({ message: element }))
973
- }),
974
- onDone: (to) => to.none,
975
- onFailure: (to) => to.full.Disconnected().resolve(({ error, target }) => target.from({ error }))
1049
+ machine.handle({
1050
+ Listening: {
1051
+ invoke: (from) =>
1052
+ from.stream("broadcast-channel", () => messages)
1053
+ .onElement((to) =>
1054
+ to.none.resolve(({ element }, enqueue) => {
1055
+ enqueue.raise(Events.MessageReceived({ message: element }))
1056
+ }))
1057
+ .onDone((to) => to.none)
1058
+ .onFailure((to) => to.full.Disconnected().resolve(({ error, target }) => target.from({ error })))
1059
+ }
976
1060
  })
977
1061
  ```
978
1062
 
@@ -985,16 +1069,18 @@ Use `to.none` when a transition keeps the current configuration. Call
985
1069
  `to.none.resolve(...)` when it also enqueues commands; a block resolver may
986
1070
  omit its return because it is contextually typed to return `undefined`.
987
1071
 
988
- When a source function reads `state`, `containingState`, `ancestors`, or the entry `event`,
989
- `Machine.invoke` infers that owner context and the returned Effect's output,
990
- error, and service channels together. No return annotation is needed:
1072
+ When a source function reads `state`, `containingState`, `ancestors`, or the
1073
+ entry `event`, `from.effect` infers that owner context and the returned Effect's
1074
+ output, error, and service channels together. No return annotation is needed:
991
1075
 
992
1076
  ```ts
993
- invoke: Machine.invoke({
994
- id: "load",
995
- effect: ({ state }) => LoadService.load(state.userId),
996
- onDone: (to) => to.full.Loaded().resolve(({ output, target }) => target.from({ user: output })),
997
- onFailure: (to) => to.full.LoadFailed().resolve(({ error, target }) => target.from({ error }))
1077
+ machine.handle({
1078
+ Loading: {
1079
+ invoke: (from) =>
1080
+ from.effect("load", ({ state }) => LoadService.load(state.userId))
1081
+ .onDone((to) => to.full.Loaded().resolve(({ output, target }) => target.from({ user: output })))
1082
+ .onFailure((to) => to.full.LoadFailed().resolve(({ error, target }) => target.from({ error })))
1083
+ }
998
1084
  })
999
1085
  ```
1000
1086
 
@@ -1010,42 +1096,44 @@ const machine = Machine.make({
1010
1096
  // ...
1011
1097
  }).handle({
1012
1098
  Saving: {
1013
- invoke: Machine.invoke({
1014
- id: "notify-parent",
1015
- effect: () => saveDocument,
1016
- onDone: (to) =>
1017
- to.none.resolve(({ parent, self }, enqueue) => {
1018
- enqueue.sendTo(self, Commands.Save())
1019
- enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
1020
- }),
1021
- onFailure: (to) => to.none
1022
- })
1099
+ invoke: (from) =>
1100
+ from.effect("notify-parent", () => saveDocument)
1101
+ .onDone((to) =>
1102
+ to.none.resolve(({ parent, self }, enqueue) => {
1103
+ enqueue.sendTo(self, Commands.Save())
1104
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
1105
+ }))
1106
+ .onFailure((to) => to.none)
1023
1107
  }
1024
1108
  })
1025
1109
  ```
1026
1110
 
1027
- The standard `Machine.invoke(...)` form retains the owning machine protocols
1028
- even when the definition is named separately. A direct `invoke: { ... }` object
1029
- remains available when lifecycle handlers do not need source-derived context.
1111
+ The computation, logic, or child descriptor may be named separately. The
1112
+ invocation chain remains inline because it is bound to its owning state and
1113
+ machine protocols. Return an array of completed chains when a state owns more
1114
+ than one activity.
1030
1115
 
1031
- A cancellable timer uses the same object:
1116
+ A cancellable timer uses its dedicated source selector:
1032
1117
 
1033
1118
  ```ts
1034
- invoke: Machine.invoke({
1035
- id: "clear-status",
1036
- after: "3 seconds",
1037
- onDone: (to) => to.full.Clear().resolve(({ target }) => target())
1119
+ machine.handle({
1120
+ Waiting: {
1121
+ invoke: (from) =>
1122
+ from.timer("clear-status", "3 seconds")
1123
+ .onDone((to) => to.full.Clear())
1124
+ }
1038
1125
  })
1039
1126
  ```
1040
1127
 
1041
1128
  The timer starts on state entry and is interrupted on exit. Its `onDone` is
1042
- always required. `effect: () => Effect.sleep(...)` has the same scoped
1043
- cancellation behavior, but `after` records timer intent and exposes a static
1044
- duration through `Machine.activityDefinitions`. Effect sources are always
1045
- factories evaluated when their state is entered. For reusable process logic,
1046
- provide `logic`, a state-local lifecycle `id`, and a typed `address`. TypeScript
1047
- checks the address protocol against the logic event protocol. Lifecycle ids and
1048
- addresses serve different purposes and must both be explicit.
1129
+ always required. An Effect containing `Effect.sleep(...)` has the same scoped
1130
+ cancellation behavior, but `from.timer` records timer intent and exposes a
1131
+ static duration through `Machine.activityDefinitions`. Effect sources are
1132
+ always factories evaluated when their state is entered. For reusable process
1133
+ logic, pass a state-local lifecycle id plus `{ logic, address }` to
1134
+ `from.logic`. TypeScript checks the address protocol against the logic event
1135
+ protocol. Lifecycle ids and addresses serve different purposes and must both
1136
+ be explicit.
1049
1137
 
1050
1138
  ## Invoked child statecharts
1051
1139
 
@@ -1058,10 +1146,12 @@ const Editor = Machine.child("editor", EditorMachine)
1058
1146
  Invoke it from its owning state:
1059
1147
 
1060
1148
  ```ts
1061
- invoke: Machine.invoke({
1062
- child: Editor,
1063
- input: editorInput,
1064
- onDone: (to) => to.full.EditorDone().resolve(({ output, target }) => target.from({ output }))
1149
+ machine.handle({
1150
+ Editing: {
1151
+ invoke: (from) =>
1152
+ from.child(Editor, { input: editorInput })
1153
+ .onDone((to) => to.full.EditorDone().resolve(({ output, target }) => target.from({ output })))
1154
+ }
1065
1155
  })
1066
1156
  ```
1067
1157
 
@@ -1089,7 +1179,7 @@ logic that does not have a complete machine descriptor.
1089
1179
  ### Inspecting state-owned activities
1090
1180
 
1091
1181
  Use `Machine.activityDefinitions(machine)` to inspect invokes without running
1092
- them. Static inline `Machine.invoke` definitions expose serializable ownership
1182
+ them. Static inline fluent invocation definitions expose serializable ownership
1093
1183
  metadata:
1094
1184
 
1095
1185
  ```ts
@@ -1189,6 +1279,22 @@ const decoded = yield* Machine.decodeSnapshot(machine, encoded)
1189
1279
  const ref = yield* Machine.resume(machine, decoded)
1190
1280
  ```
1191
1281
 
1282
+ `Machine.Snapshot` is the decoded, process-local representation. It may retain
1283
+ `Schema.Class` instances and capabilities that cannot cross a JSON boundary.
1284
+ `Machine.EncodedSnapshot` is different: successful `encodeSnapshot` calls
1285
+ guarantee canonical `Schema.Json` for every active value, completion output,
1286
+ and history value. Schema codecs convert rich values such as `Date`, `bigint`,
1287
+ and `undefined` to their declared JSON forms. Cycles, functions, symbols, and
1288
+ opaque values without a JSON representation fail with the typed
1289
+ `MachineSchemaEncodeError`; they never become a later `JSON.stringify` defect.
1290
+
1291
+ Keep DOM nodes, open handles, services, and similar capabilities in an Effect
1292
+ service or UI adapter. Local events may carry those values when they stay inside
1293
+ one process. Cluster public input events, persisted state, and completion
1294
+ outputs are transport protocols and must instead declare JSON-compatible
1295
+ encoded forms; use an explicit transform or `Schema.toCodecJson` where the
1296
+ canonical codec is the intended wire contract.
1297
+
1192
1298
  Pass only a decoded `Machine.Snapshot` to `resume`; encoded or arbitrary
1193
1299
  transport data belongs at `decodeSnapshot`. Resumption validates and normalizes
1194
1300
  the logical snapshot again, then publishes it as the fresh runtime's first
@@ -1382,7 +1488,7 @@ reference model when correctness of the expected behavior matters.
1382
1488
  Select the initial root separately from constructing its value:
1383
1489
 
1384
1490
  ```ts
1385
- initial: (to) => to.Idle().resolve(({ target }) => target.from())
1491
+ initial: (to) => to.Idle()
1386
1492
  ```
1387
1493
 
1388
1494
  ### Invoked child expects events not accepted by the parent
@@ -1467,6 +1573,6 @@ The current API does not include:
1467
1573
  - declarative first-class guards;
1468
1574
  - a complete inspectable graph for arbitrary transition Effects.
1469
1575
 
1470
- Use ordinary TypeScript conditions for guards and an inline `Machine.invoke`
1471
- with `after` for state-scoped timers. Do not invent undocumented state-node
1472
- properties such as `guard`.
1576
+ Use ordinary TypeScript conditions for guards and inline
1577
+ `invoke: (from) => from.timer(...)` chains for state-scoped timers. Do not
1578
+ invent undocumented state-node properties such as `guard`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/effect-machine",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Schema-first state machines and statecharts for Effect",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {
@@ -46,14 +46,14 @@
46
46
  "provenance": true
47
47
  },
48
48
  "peerDependencies": {
49
- "effect": "4.0.0-rc.109"
49
+ "effect": "4.0.0-rc.110"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@changesets/cli": "2.31.0",
53
- "@effect/vitest": "4.0.0-rc.109",
53
+ "@effect/vitest": "4.0.0-rc.110",
54
54
  "@types/node": "25.7.0",
55
55
  "dprint": "0.55.2",
56
- "effect": "4.0.0-rc.109",
56
+ "effect": "4.0.0-rc.110",
57
57
  "pagefind": "1.5.2",
58
58
  "tinybench": "2.9.0",
59
59
  "tstyche": "7.2.1",