@typeonce/effect-machine 0.16.0 → 0.18.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 (68) hide show
  1. package/README.md +157 -150
  2. package/dist/Machine.d.ts +416 -592
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +55 -136
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +7 -7
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js.map +1 -1
  9. package/dist/internal/machine/cluster.d.ts +2 -2
  10. package/dist/internal/machine/cluster.d.ts.map +1 -1
  11. package/dist/internal/machine/cluster.js +6 -5
  12. package/dist/internal/machine/cluster.js.map +1 -1
  13. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  14. package/dist/internal/machine/executionPlan.js +10 -3
  15. package/dist/internal/machine/executionPlan.js.map +1 -1
  16. package/dist/internal/machine/invocation.d.ts.map +1 -1
  17. package/dist/internal/machine/invocation.js +1 -1
  18. package/dist/internal/machine/invocation.js.map +1 -1
  19. package/dist/internal/machine/machine.d.ts +8 -6
  20. package/dist/internal/machine/machine.d.ts.map +1 -1
  21. package/dist/internal/machine/machine.js +239 -35
  22. package/dist/internal/machine/machine.js.map +1 -1
  23. package/dist/internal/machine/planner.d.ts +19 -4
  24. package/dist/internal/machine/planner.d.ts.map +1 -1
  25. package/dist/internal/machine/planner.js +56 -20
  26. package/dist/internal/machine/planner.js.map +1 -1
  27. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  28. package/dist/internal/machine/stateDefinition.js +14 -0
  29. package/dist/internal/machine/stateDefinition.js.map +1 -1
  30. package/dist/internal/machine/topology.d.ts +9 -0
  31. package/dist/internal/machine/topology.d.ts.map +1 -1
  32. package/dist/internal/machine/topology.js +16 -0
  33. package/dist/internal/machine/topology.js.map +1 -1
  34. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  35. package/dist/internal/testing/machine/finiteModel.js +15 -20
  36. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  37. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
  38. package/dist/internal/testing/machine/transitionCoverage.js +2 -0
  39. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
  40. package/dist/testing/MachineTest.d.ts +15 -15
  41. package/dist/testing/MachineTest.d.ts.map +1 -1
  42. package/dist/testing/MachineTest.js +5 -8
  43. package/dist/testing/MachineTest.js.map +1 -1
  44. package/dist/unstable/cluster/ClusterMachine.d.ts +2 -2
  45. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  46. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  47. package/dist/unstable/reactivity/AtomMachine.d.ts +13 -13
  48. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  49. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  50. package/docs/agent-guide.md +229 -193
  51. package/package.json +1 -1
  52. package/src/Machine.ts +1686 -2234
  53. package/src/internal/machine/atom.ts +20 -15
  54. package/src/internal/machine/cluster.ts +14 -9
  55. package/src/internal/machine/executionPlan.ts +8 -3
  56. package/src/internal/machine/invocation.ts +1 -1
  57. package/src/internal/machine/machine.ts +365 -48
  58. package/src/internal/machine/planner.ts +89 -27
  59. package/src/internal/machine/stateDefinition.ts +39 -0
  60. package/src/internal/machine/topology.ts +28 -0
  61. package/src/internal/testing/machine/exploration.ts +1 -1
  62. package/src/internal/testing/machine/finiteModel.ts +18 -21
  63. package/src/internal/testing/machine/trace.ts +1 -1
  64. package/src/internal/testing/machine/transitionCoverage.ts +2 -0
  65. package/src/internal/testing/machine/verification.ts +1 -1
  66. package/src/testing/MachineTest.ts +18 -15
  67. package/src/unstable/cluster/ClusterMachine.ts +8 -4
  68. package/src/unstable/reactivity/AtomMachine.ts +20 -13
@@ -38,8 +38,9 @@ declared:
38
38
  1. Domain schemas used by state, and by event fields when they are shared.
39
39
  2. `Machine.states`, using a tagged state union and `.cases` when state
40
40
  schemas need to be reused.
41
- 3. `Machine.events`, `Machine.internalEvents`, `Machine.emittedEvents`, and
42
- `parentEvents`; pass `Schema.TaggedUnion({...})` or tagged classes directly.
41
+ 3. `Machine.events`, `Machine.internalEvents`, `Machine.emittedEvents`, and any
42
+ protocol passed to `Machine.parent` or `Machine.optionalParent`; pass
43
+ `Schema.TaggedUnion({...})` or tagged classes directly.
43
44
  4. `Machine.make({...}).handle({...})`.
44
45
  5. Child descriptors, then runtime, Atom, or Cluster adapters.
45
46
 
@@ -109,8 +110,9 @@ the deferred constructors preserve that identity after decoding.
109
110
  lookup. Independently constructed descriptors are equivalent only when both
110
111
  their id and machine identity match.
111
112
  - `events` is the public machine-input protocol. `internalEvents` contains
112
- machine-local raised events. `parentEvents` describes the public events a
113
- child may send to its owner. `emittedEvents` describes outward ephemeral
113
+ machine-local raised events. `parent: Machine.parent(events)` requires an
114
+ owner, while `parent: Machine.optionalParent(events)` permits a root and
115
+ exposes an optional owner. `emittedEvents` describes outward ephemeral
114
116
  notifications and is never delivered implicitly to a parent.
115
117
  - Event tags in `events` and `internalEvents` must be disjoint.
116
118
  - Event tags must also be unique within each protocol list.
@@ -123,17 +125,16 @@ its extra control is required:
123
125
  - Bind a shared Atom runtime once with `AtomMachine.bind(runtime)`, then use the
124
126
  returned `make` or `resume`. Use `AtomMachine.make(machine)` and
125
127
  `AtomMachine.resume(machine, snapshot)` for service-free machines.
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
129
- statechart. `Machine.invoke({...})` preserves owner state and source channels
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.
128
+ - Use the state-local `invoke: (from) => ...` selector: `from.effect` for
129
+ one-shot work, `from.stream` for repeated externally produced values,
130
+ `from.timer` for a timer, `from.logic` for reusable process logic, and
131
+ `from.child` for a complete child statechart. Its chain preserves owner state
132
+ and source channels across lifecycle handlers. Inside `.handle(...)`, `self`
133
+ and any declared `parent` use the owning definition's exact protocols.
133
134
  - Use `Machine.child(id, machine)` for a complete statechart descriptor and
134
135
  `Machine.childAddress<Event>(id)` for a low-level process address. A logic
135
- invocation is addressable only when `Machine.invoke` receives that
136
- address explicitly.
136
+ invocation is addressable only when `from.logic` receives that address
137
+ explicitly.
137
138
  - Use the callback's `enqueue` argument for `raise`, `emit`, `sendTo`, and
138
139
  `stop`. These operations record closed machine commands and do not run Effects.
139
140
 
@@ -216,6 +217,22 @@ const offeredIfSlot = (
216
217
  Do not derive this type with `Parameters<typeof States.get>[0]`; that depends
217
218
  on overload order and does not express ownership by the state definition.
218
219
 
220
+ The same extractor accepts a machine when that is the object exported at the
221
+ consumer boundary. Use `Value` for a decoded schema-backed state payload and
222
+ `SnapshotAt` for the snapshot rooted at one active path:
223
+
224
+ ```ts
225
+ type Complete = Machine.Snapshot<typeof machine>
226
+ type Session = Machine.Value<typeof States, "root.trading.InSession">
227
+ type Trading = Machine.SnapshotAt<typeof machine, "root.trading">
228
+ ```
229
+
230
+ `Value` accepts only paths that own a schema, matching `States.get`.
231
+ `SnapshotAt` also accepts structural paths, matching `States.getSnapshot`.
232
+ Both reject stale or misspelled paths. Prefer these definition- or
233
+ machine-bound forms over `.cases.Case.Type`, `typeof States.states`, or
234
+ composing `Machine.Machine.States` with raw-tree path extractors.
235
+
219
236
  An active state does not need a schema unless it owns data. Omit `schema` for
220
237
  control-only atomic, compound, parallel, and final states:
221
238
 
@@ -231,10 +248,7 @@ const States = Machine.states({
231
248
  }
232
249
  })
233
250
 
234
- initial: {
235
- target: (to) => to.Form.initial(),
236
- resolve: ({ target }) => target((form) => form.Editing.from())
237
- }
251
+ initial: (to) => to.Form.initial.resolve(({ target }) => target((form) => form.Editing.from()))
238
252
  ```
239
253
 
240
254
  Schema-less states have the same control semantics as schema-backed states:
@@ -244,13 +258,11 @@ in snapshots. They do not have a state value:
244
258
  ```ts
245
259
  Idle: {
246
260
  on: {
247
- Start: Machine.transition({
248
- target: (to) => to.full.Form.initial(),
249
- resolve: ({ state, target }) => {
261
+ Start: (to) =>
262
+ to.full.Form.initial.resolve(({ state, target }) => {
250
263
  // state: undefined
251
264
  return target.from((form) => form.Editing.from())
252
- }
253
- })
265
+ })
254
266
  }
255
267
  }
256
268
 
@@ -331,10 +343,7 @@ const States = Machine.states({
331
343
  const machine = Machine.make({
332
344
  states: States.states,
333
345
  events: Machine.events(),
334
- initial: {
335
- target: (to) => to.Done(),
336
- resolve: ({ target }) => target.from()
337
- }
346
+ initial: (to) => to.Done().resolve(({ target }) => target.from())
338
347
  }).handle({
339
348
  Done: {
340
349
  output: () => "done"
@@ -351,12 +360,12 @@ with `.initial`. This is available on top-level state methods under
351
360
  `target.branch`; atomic and final state methods do not expose it:
352
361
 
353
362
  ```ts
354
- Open: Machine.transition({
355
- target: (to) => to.full.opened.initial(),
356
- resolve: ({ target }) => target.from({ teamId: "team-1" })
357
- })
363
+ Open: (to) => to.full.opened.initial.resolve(({ target }) => target.from({ teamId: "team-1" }))
358
364
  ```
359
365
 
366
+ The definition-time `.initial` property is a topology value. The exact
367
+ resolver `target` is still a callable runtime builder.
368
+
360
369
  The selected state's own value is passed directly to `initial(value)` or
361
370
  constructed inside planning with `initial.from(input)`. A structural selected
362
371
  state uses `initial()`.
@@ -431,12 +440,12 @@ checkout: {
431
440
  Target it without a value:
432
441
 
433
442
  ```ts
434
- Resume: Machine.transition({
435
- target: (to) => to.history.checkout.exact(),
436
- resolve: ({ target }) => target()
437
- })
443
+ Resume: (to) => to.history.checkout.exact.resolve(({ target }) => target())
438
444
  ```
439
445
 
446
+ Each declared history leaf is a topology value; the resolver's selected
447
+ history builder remains callable to construct restoration evidence.
448
+
440
449
  Deep history restores the complete remembered subtree and its decoded values.
441
450
  Shallow history restores only parent and direct-child values. If the remembered
442
451
  child is compound, its configured initial child needs a freshly constructed
@@ -486,6 +495,22 @@ prior effects, machine instances, and timers are not rewound.
486
495
  | `target.full` | The destination may be under any top-level root | Nothing is inferred for a newly selected root; build its complete active snapshot |
487
496
  | `target.history` | The destination is a declared history pseudo-state | Its parent's remembered configuration, or a source-independent complete default containing that owner before the first capture |
488
497
 
498
+ Definition-time instructions that only identify topology are values:
499
+ `to.none`, `to.full.Flow.initial`, `to.history.Flow.recent`, and
500
+ `to.local.with`. State and choice destinations remain calls, such as
501
+ `to.full.Running()` and `to.local.Routing()`, because those calls select the
502
+ node. Resolver-time builders also remain callable because they construct and,
503
+ for named branches, brand runtime evidence such as `select.unchanged()`.
504
+
505
+ Use `to.local.with` when a descendant transition updates the nearest
506
+ schema-backed compound value while retaining that same compound scope:
507
+
508
+ ```ts
509
+ Play: (to) =>
510
+ to.local.with.resolve(({ containingState, target }) =>
511
+ target.from({ ...containingState, playing: true }, (flow) => flow.Playing.from()))
512
+ ```
513
+
489
514
  Entering an inactive parallel state through `target.local` or `target.branch`
490
515
  requires a complete callback with one selection per region. A parallel state
491
516
  that is already active remains partially addressable through `target.branch`;
@@ -497,11 +522,16 @@ sets. A `target.full` result with the same active paths can update values withou
497
522
  exiting shared states. To force the source to exit and enter again:
498
523
 
499
524
  ```ts
500
- Refresh: Machine.transition({
501
- target: (to) => to.full.Ready(),
502
- reenter: true,
503
- resolve: ({ state, target }) => target.from({ value: state.value })
504
- })
525
+ Refresh: (to) =>
526
+ to.full.Ready().resolve(({ state, target }) => target.from({ value: state.value }), { reenter: true })
527
+ ```
528
+
529
+ When no resolver is needed, use the selected target directly and append
530
+ `.reenter()` only when restart semantics are intentional:
531
+
532
+ ```ts
533
+ Finish: (to) => to.full.Done()
534
+ Restart: (to) => to.none.reenter()
505
535
  ```
506
536
 
507
537
  Do not use `target.full` merely because it is easiest to discover. Prefer the
@@ -561,8 +591,9 @@ only schema-backed paths; use `matches` or `getSnapshot` for any active path.
561
591
  `context.containingState` is the immediate typed state value (`undefined` at a
562
592
  root or when that state is schema-less). `context.ancestors` contains only
563
593
  valued structural ancestors. This is separate from `context.parent`, which is
564
- the owning machine target or `undefined` for a root machine. Use full state paths
565
- when another ancestor value is needed:
594
+ present only when declared by the machine. `Machine.parent` makes it a required
595
+ owning-machine target; `Machine.optionalParent` makes it a target or
596
+ `undefined`. Use full state paths when another ancestor value is needed:
566
597
 
567
598
  ```ts
568
599
  ancestors["Route.Ready"]
@@ -578,16 +609,14 @@ Event, `always`, and `onDone` transition contexts include a fully typed
578
609
  microstep, before any selected transition is applied:
579
610
 
580
611
  ```ts
581
- BufferReady: Machine.transition({
582
- branches: (to) => ({
612
+ BufferReady: (to) =>
613
+ to.branches({
583
614
  online: { target: to.local.Playing() },
584
- unchanged: { target: to.none() }
585
- }),
586
- resolve: ({ snapshot, select }) =>
615
+ unchanged: { target: to.none }
616
+ }).resolve(({ snapshot, select }) =>
587
617
  States.matches(snapshot, "Player.Network.Online")
588
618
  ? select.online.from()
589
619
  : select.unchanged()
590
- })
591
620
  ```
592
621
 
593
622
  Use the existing `States.matches`, `States.get`, `States.getWithParents`, and
@@ -622,13 +651,11 @@ When sibling state payloads share fields, destructure away the source
622
651
  discriminator and construct the destination through its target builder:
623
652
 
624
653
  ```ts
625
- Submit: Machine.transition({
626
- target: (to) => to.local.Saving(),
627
- resolve: ({ state, target }) => {
654
+ Submit: (to) =>
655
+ to.local.Saving().resolve(({ state, target }) => {
628
656
  const { _tag: _, ...fields } = state
629
657
  return target.from({ ...fields, attempt: 1 })
630
- }
631
- })
658
+ })
632
659
  ```
633
660
 
634
661
  The target schema remains responsible for defaults, transforms, refinements,
@@ -641,45 +668,66 @@ A transition declares every possible branch and resolves the selected target
641
668
  synchronously:
642
669
 
643
670
  ```ts
644
- Submit: Machine.transition({
645
- branches: (to) => ({
671
+ Submit: (to) =>
672
+ to.branches({
646
673
  valid: { target: to.local.Saving() },
647
- invalid: { target: to.none() }
648
- }),
649
- resolve: ({ state, select }) => state.valid
674
+ invalid: { target: to.none }
675
+ }).resolve(({ state, select }) => state.valid
650
676
  ? select.valid.from({ draft: state.draft })
651
677
  : select.invalid()
652
- })
653
678
  ```
654
679
 
655
680
  Every installed event, `always`, `onDone`, choice, and invoke lifecycle handler
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
681
+ receives a bound `to` selector. A direct transition selects one target and calls
682
+ its `resolve` method. A branching transition calls `to.branches` with every
683
+ possible target, then uses ordinary TypeScript control flow in `resolve` to return one
659
684
  typed `select` builder. Branch keys are stable testing and inspection identities;
660
685
  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
686
+ Selecting a branch whose target is `to.none` handles the transition without a
662
687
  destination while retaining queued commands, raised events, and emitted events.
663
688
 
689
+ Set `declinable: true` only when the resolver may decide that its transition is
690
+ not enabled. The flag adds a typed `decline()` capability to that resolver and
691
+ permits its opaque result:
692
+
693
+ ```ts
694
+ Submit: (to) =>
695
+ to.branches({
696
+ accepted: { target: to.local.Saving() },
697
+ consumed: { target: to.none }
698
+ }).resolve(({ event, select, decline }) => {
699
+ if (!belongsToThisState(event)) return decline()
700
+ return event.consume ? select.consumed() : select.accepted.from()
701
+ }, { declinable: true })
702
+ ```
703
+
704
+ Declining discards that resolver's enqueue buffer and resumes hierarchical
705
+ event or eventless selection at the next eligible ancestor. If no candidate
706
+ accepts, the trigger is unhandled. This is deliberately different from
707
+ `to.none`, which consumes the trigger. `decline()` is absent and its result is
708
+ rejected unless the literal flag is present. Choice and initial routing remain
709
+ total and cannot decline. Static inspection exposes the distinction through
710
+ `TransitionDefinition.acceptance` without executing resolver code. Completion
711
+ and invocation outcomes have no ancestor candidate; declining one ignores that
712
+ lifecycle occurrence and leaves the current configuration active.
713
+
664
714
  The `branches` callback runs once when handlers are installed. Its record uses
665
715
  the deterministic ECMAScript property order for presentation and `branchIndex`;
666
716
  array-index and symbol keys are rejected. Treat the string key as semantic:
667
717
  reordering named properties may change their display index, but visualizers,
668
718
  coverage, and trace verification identify each branch by its key.
669
719
 
670
- `reenter: true` remains meaningful with `to.none()`: the source exits and
720
+ `reenter: true` remains meaningful with `to.none`: the source exits and
671
721
  enters again while its logical configuration is retained.
672
722
 
673
723
  Closed statechart and machine operations use `enqueue`:
674
724
 
675
725
  ```ts
676
- Submit: Machine.transition({
677
- target: (to) => to.local.Saving(),
678
- resolve: ({ target }, enqueue) => {
726
+ Submit: (to) =>
727
+ to.local.Saving().resolve(({ target }, enqueue) => {
679
728
  enqueue.emit(Emissions.SaveRequested())
680
729
  return target.from()
681
- }
682
- })
730
+ })
683
731
  ```
684
732
 
685
733
  Declare emission constructors separately from machine inputs:
@@ -768,20 +816,15 @@ export const ParentEvents = Machine.events(ChildFinished)
768
816
 
769
817
  const child = Machine.make({
770
818
  events: ChildEvents,
771
- parentEvents: ParentEvents,
819
+ parent: Machine.parent(ParentEvents),
772
820
  // ...
773
821
  }).handle({
774
822
  Working: {
775
823
  on: {
776
- Finish: Machine.transition({
777
- target: (to) => to.none(),
778
- resolve: ({ parent }, enqueue) => {
779
- if (parent !== undefined) {
780
- enqueue.sendTo(parent, ParentEvents.ChildFinished())
781
- }
782
- return undefined
783
- }
784
- })
824
+ Finish: (to) =>
825
+ to.none.resolve(({ parent }, enqueue) => {
826
+ enqueue.sendTo(parent, ParentEvents.ChildFinished())
827
+ })
785
828
  }
786
829
  }
787
830
  })
@@ -792,12 +835,14 @@ const parent = Machine.make({
792
835
  })
793
836
  ```
794
837
 
795
- Invoking the child under a parent that lacks any required `parentEvents` case
796
- is a type error. Within child handlers, `parent` accepts only that protocol.
797
- The same child may run as a root, where `parent` is `undefined`. `self` accepts
798
- the machine's public inputs. Both are minimal `MachineTarget<Event>` values,
799
- provided by the shared `MachineReferences<InputEvents, ParentEvents>` handler
800
- context. Neither machine target is a structural state value; use
838
+ Invoking the child under a parent that lacks any required parent event is a
839
+ type error. Within child handlers, `parent` accepts only that protocol and is
840
+ not optional. Root APIs reject the machine. Use
841
+ `Machine.optionalParent(ParentEvents)` instead when the same definition must
842
+ also run as a root; then `parent` is optional. With no declaration, callbacks
843
+ have no `parent` property. `self` accepts the machine's public inputs. Both
844
+ targets are minimal `MachineTarget<Event>` values. Neither machine target is a
845
+ structural state value; use
801
846
  `containingState` and `ancestors` for statechart ancestry.
802
847
 
803
848
  Atom-backed machines retain the same transient semantics. Use
@@ -856,10 +901,7 @@ const definition = Machine.make({
856
901
  states: States.states,
857
902
  events: Events,
858
903
  internalEvents: InternalEvents,
859
- initial: {
860
- target: (to) => to.Idle(),
861
- resolve: ({ target }) => target.from()
862
- }
904
+ initial: (to) => to.Idle().resolve(({ target }) => target.from())
863
905
  })
864
906
  ```
865
907
 
@@ -892,8 +934,14 @@ Use the exported utility types when another API must preserve the boundary:
892
934
  ```ts
893
935
  type PublicEvent = Machine.Machine.InputEvent<typeof definition>
894
936
  type AnyHandledEvent = Machine.Machine.Event<typeof definition>
937
+ type StartupInput = Machine.Machine.Input<typeof definition>
938
+ type StartupInputSchema = Machine.Machine.InputSchema<typeof definition>
895
939
  ```
896
940
 
941
+ `Input` is the decoded value accepted at startup. It is `never` for a machine
942
+ whose input schema is `Schema.Void`; use `InputSchema` only when an API needs
943
+ the schema object itself.
944
+
897
945
  `MachineRef.send`, `machineAtom.send`, and `Machine.plan` accept decoded public
898
946
  events or constructions returned by `Machine.events`. Transition handlers
899
947
  receive only decoded events. Raised events additionally accept constructions
@@ -906,23 +954,28 @@ Cluster RPC payloads are additionally decoded against the public `events`
906
954
  schemas at the transport boundary. Never repeat an `_tag` within a list or
907
955
  across both configuration lists.
908
956
 
957
+ Do not extract `enqueue`, target builders, transition contexts, command or
958
+ inspection unions, or event-construction `ReturnType`s into application helper
959
+ APIs. Keep commands inside transition resolvers, where the owning state,
960
+ protocols, references, and capabilities are inferred. Likewise, do not add
961
+ Atom `State` or `Event` aliases: selectors infer from their bridge, while
962
+ consumer props use `Snapshot`, `Value`, or `InputEvent` from the exported state
963
+ definition or machine.
964
+
909
965
  ## Recoverable state-scoped work
910
966
 
911
- Use `Machine.invoke` with an `effect` for one-shot work. Lifecycle callbacks
912
- receive the typed Effect channels and can transition directly:
967
+ Use `from.effect` for one-shot work. Lifecycle callbacks receive the typed
968
+ Effect channels and can transition directly:
913
969
 
914
970
  ```ts
915
- invoke: Machine.invoke({
916
- id: "save",
917
- effect: () => SaveService.save(draft),
918
- onDone: Machine.transition({
919
- target: (to) => to.full.Saved(),
920
- resolve: ({ output, target }) => target.from({ entry: output })
921
- }),
922
- onFailure: Machine.transition({
923
- target: (to) => to.full.SaveFailed(),
924
- resolve: ({ error, target }) => target.from({ message: error.message })
925
- })
971
+ machine.handle({
972
+ Saving: {
973
+ invoke: (from) =>
974
+ from.effect("save", () => SaveService.save(draft))
975
+ .onDone((to) => to.full.Saved().resolve(({ output, target }) => target.from({ entry: output })))
976
+ .onFailure((to) =>
977
+ to.full.SaveFailed().resolve(({ error, target }) => target.from({ message: error.message })))
978
+ }
926
979
  })
927
980
  ```
928
981
 
@@ -942,20 +995,17 @@ events. `onElement` maps each value into an owner transition, while `onDone`
942
995
  handles normal Stream completion and `onFailure` handles the typed Stream error:
943
996
 
944
997
  ```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
- })
998
+ machine.handle({
999
+ Listening: {
1000
+ invoke: (from) =>
1001
+ from.stream("broadcast-channel", () => messages)
1002
+ .onElement((to) =>
1003
+ to.none.resolve(({ element }, enqueue) => {
1004
+ enqueue.raise(Events.MessageReceived({ message: element }))
1005
+ }))
1006
+ .onDone((to) => to.none)
1007
+ .onFailure((to) => to.full.Disconnected().resolve(({ error, target }) => target.from({ error })))
1008
+ }
959
1009
  })
960
1010
  ```
961
1011
 
@@ -964,89 +1014,75 @@ after the selected parent macrostep commits. Exiting or reentering the owner
964
1014
  interrupts the Stream and runs its finalizers. A later entry starts a fresh
965
1015
  Stream. Stream defects and self-interruption fail the owning machine.
966
1016
 
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
-
972
- When a source function reads `state`, `containingState`, `ancestors`, or the entry `event`,
973
- `Machine.invoke` infers that owner context and the returned Effect's output,
974
- error, and service channels together. No return annotation is needed:
975
-
976
- ```ts
977
- invoke: Machine.invoke({
978
- id: "load",
979
- effect: ({ state }) => LoadService.load(state.userId),
980
- onDone: Machine.transition({
981
- target: (to) => to.full.Loaded(),
982
- resolve: ({ output, target }) => target.from({ user: output })
983
- }),
984
- onFailure: Machine.transition({
985
- target: (to) => to.full.LoadFailed(),
986
- resolve: ({ error, target }) => target.from({ error })
987
- })
1017
+ Use `to.none` when a transition keeps the current configuration. Call
1018
+ `to.none.resolve(...)` when it also enqueues commands; a block resolver may
1019
+ omit its return because it is contextually typed to return `undefined`.
1020
+
1021
+ When a source function reads `state`, `containingState`, `ancestors`, or the
1022
+ entry `event`, `from.effect` infers that owner context and the returned Effect's
1023
+ output, error, and service channels together. No return annotation is needed:
1024
+
1025
+ ```ts
1026
+ machine.handle({
1027
+ Loading: {
1028
+ invoke: (from) =>
1029
+ from.effect("load", ({ state }) => LoadService.load(state.userId))
1030
+ .onDone((to) => to.full.Loaded().resolve(({ output, target }) => target.from({ user: output })))
1031
+ .onFailure((to) => to.full.LoadFailed().resolve(({ error, target }) => target.from({ error })))
1032
+ }
988
1033
  })
989
1034
  ```
990
1035
 
991
1036
  Inside `.handle(...)`, the constructor receives the owning machine's public
992
- input and `parentEvents` protocols contextually. Sources and lifecycle handlers
1037
+ input and declared parent protocol contextually. Sources and lifecycle handlers
993
1038
  can send through `self` and `parent` without naming the definition:
994
1039
 
995
1040
  ```ts
996
1041
  const machine = Machine.make({
997
1042
  events: Commands,
998
1043
  internalEvents: InternalEvents,
999
- parentEvents: ParentEvents,
1044
+ parent: Machine.parent(ParentEvents),
1000
1045
  // ...
1001
1046
  }).handle({
1002
1047
  Saving: {
1003
- invoke: Machine.invoke({
1004
- id: "notify-parent",
1005
- effect: () => saveDocument,
1006
- onDone: Machine.transition({
1007
- target: (to) => to.none(),
1008
- resolve: ({ parent, self }, enqueue) => {
1009
- enqueue.sendTo(self, Commands.Save())
1010
- if (parent !== undefined) {
1048
+ invoke: (from) =>
1049
+ from.effect("notify-parent", () => saveDocument)
1050
+ .onDone((to) =>
1051
+ to.none.resolve(({ parent, self }, enqueue) => {
1052
+ enqueue.sendTo(self, Commands.Save())
1011
1053
  enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
1012
- }
1013
- return undefined
1014
- }
1015
- }),
1016
- onFailure: Machine.transition({
1017
- target: (to) => to.none(),
1018
- resolve: () => undefined
1019
- })
1020
- })
1054
+ }))
1055
+ .onFailure((to) => to.none)
1021
1056
  }
1022
1057
  })
1023
1058
  ```
1024
1059
 
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.
1060
+ The computation, logic, or child descriptor may be named separately. The
1061
+ invocation chain remains inline because it is bound to its owning state and
1062
+ machine protocols. Return an array of completed chains when a state owns more
1063
+ than one activity.
1028
1064
 
1029
- A cancellable timer uses the same object:
1065
+ A cancellable timer uses its dedicated source selector:
1030
1066
 
1031
1067
  ```ts
1032
- invoke: Machine.invoke({
1033
- id: "clear-status",
1034
- after: "3 seconds",
1035
- onDone: Machine.transition({
1036
- target: (to) => to.full.Clear(),
1037
- resolve: ({ target }) => target()
1038
- })
1068
+ machine.handle({
1069
+ Waiting: {
1070
+ invoke: (from) =>
1071
+ from.timer("clear-status", "3 seconds")
1072
+ .onDone((to) => to.full.Clear().resolve(({ target }) => target()))
1073
+ }
1039
1074
  })
1040
1075
  ```
1041
1076
 
1042
1077
  The timer starts on state entry and is interrupted on exit. Its `onDone` is
1043
- always required. `effect: () => Effect.sleep(...)` has the same scoped
1044
- cancellation behavior, but `after` records timer intent and exposes a static
1045
- duration through `Machine.activityDefinitions`. Effect sources are always
1046
- factories evaluated when their state is entered. For reusable process logic,
1047
- provide `logic`, a state-local lifecycle `id`, and a typed `address`. TypeScript
1048
- checks the address protocol against the logic event protocol. Lifecycle ids and
1049
- addresses serve different purposes and must both be explicit.
1078
+ always required. An Effect containing `Effect.sleep(...)` has the same scoped
1079
+ cancellation behavior, but `from.timer` records timer intent and exposes a
1080
+ static duration through `Machine.activityDefinitions`. Effect sources are
1081
+ always factories evaluated when their state is entered. For reusable process
1082
+ logic, pass a state-local lifecycle id plus `{ logic, address }` to
1083
+ `from.logic`. TypeScript checks the address protocol against the logic event
1084
+ protocol. Lifecycle ids and addresses serve different purposes and must both
1085
+ be explicit.
1050
1086
 
1051
1087
  ## Invoked child statecharts
1052
1088
 
@@ -1059,13 +1095,12 @@ const Editor = Machine.child("editor", EditorMachine)
1059
1095
  Invoke it from its owning state:
1060
1096
 
1061
1097
  ```ts
1062
- invoke: Machine.invoke({
1063
- child: Editor,
1064
- input: editorInput,
1065
- onDone: Machine.transition({
1066
- target: (to) => to.full.EditorDone(),
1067
- resolve: ({ output, target }) => target.from({ output })
1068
- })
1098
+ machine.handle({
1099
+ Editing: {
1100
+ invoke: (from) =>
1101
+ from.child(Editor, { input: editorInput })
1102
+ .onDone((to) => to.full.EditorDone().resolve(({ output, target }) => target.from({ output })))
1103
+ }
1069
1104
  })
1070
1105
  ```
1071
1106
 
@@ -1093,7 +1128,7 @@ logic that does not have a complete machine descriptor.
1093
1128
  ### Inspecting state-owned activities
1094
1129
 
1095
1130
  Use `Machine.activityDefinitions(machine)` to inspect invokes without running
1096
- them. Static inline `Machine.invoke` definitions expose serializable ownership
1131
+ them. Static inline fluent invocation definitions expose serializable ownership
1097
1132
  metadata:
1098
1133
 
1099
1134
  ```ts
@@ -1313,9 +1348,10 @@ const step = yield * probe.sendAndAwait(event)
1313
1348
  ```
1314
1349
 
1315
1350
  Inspect `step.before`, `step.after`, `step.plan`, `step.handled`, and
1316
- `step.configurationChanged`. An ignored event has `handled: false` and an
1317
- empty microstep list, but still completes its acknowledgement. A targetless
1318
- handler has `handled: true` even if its before and after snapshots are equal.
1351
+ `step.configurationChanged`. An ignored event, including one for which every
1352
+ eligible candidate declines, has `handled: false` and an empty microstep list,
1353
+ but still completes its acknowledgement. A targetless handler has
1354
+ `handled: true` even if its before and after snapshots are equal.
1319
1355
 
1320
1356
  Do not use a probe as a substitute for a domain completion event. The
1321
1357
  acknowledgement covers the submitted event's synchronous macrostep, state
@@ -1385,10 +1421,7 @@ reference model when correctness of the expected behavior matters.
1385
1421
  Select the initial root separately from constructing its value:
1386
1422
 
1387
1423
  ```ts
1388
- initial: {
1389
- target: (to) => to.Idle(),
1390
- resolve: ({ target }) => target.from()
1391
- }
1424
+ initial: (to) => to.Idle().resolve(({ target }) => target.from())
1392
1425
  ```
1393
1426
 
1394
1427
  ### Invoked child expects events not accepted by the parent
@@ -1399,13 +1432,16 @@ the parent's public events:
1399
1432
  ```ts
1400
1433
  export const ChildParentEvents = Machine.events(ChildFinished)
1401
1434
 
1402
- // child
1403
- parentEvents: ChildParentEvents
1435
+ // child-only machine
1436
+ parent: Machine.parent(ChildParentEvents)
1404
1437
 
1405
1438
  // parent
1406
1439
  events: Machine.events(Submit, ChildParentEvents)
1407
1440
  ```
1408
1441
 
1442
+ Use `Machine.optionalParent(ChildParentEvents)` only when the child is also a
1443
+ valid independent root and narrow `parent` before sending.
1444
+
1409
1445
  ### An internal event is rejected by `send`
1410
1446
 
1411
1447
  This is intentional. Public input boundaries accept only schemas declared in
@@ -1470,6 +1506,6 @@ The current API does not include:
1470
1506
  - declarative first-class guards;
1471
1507
  - a complete inspectable graph for arbitrary transition Effects.
1472
1508
 
1473
- Use ordinary TypeScript conditions for guards and an inline `Machine.invoke`
1474
- with `after` for state-scoped timers. Do not invent undocumented state-node
1475
- properties such as `guard`.
1509
+ Use ordinary TypeScript conditions for guards and inline
1510
+ `invoke: (from) => from.timer(...)` chains for state-scoped timers. Do not
1511
+ invent undocumented state-node properties such as `guard`.