@typeonce/effect-machine 0.16.0 → 0.17.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 (61) hide show
  1. package/README.md +98 -106
  2. package/dist/Machine.d.ts +324 -367
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +61 -96
  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/machine.d.ts +8 -6
  17. package/dist/internal/machine/machine.d.ts.map +1 -1
  18. package/dist/internal/machine/machine.js +191 -25
  19. package/dist/internal/machine/machine.js.map +1 -1
  20. package/dist/internal/machine/planner.d.ts +19 -4
  21. package/dist/internal/machine/planner.d.ts.map +1 -1
  22. package/dist/internal/machine/planner.js +56 -20
  23. package/dist/internal/machine/planner.js.map +1 -1
  24. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  25. package/dist/internal/machine/stateDefinition.js +14 -0
  26. package/dist/internal/machine/stateDefinition.js.map +1 -1
  27. package/dist/internal/machine/topology.d.ts +9 -0
  28. package/dist/internal/machine/topology.d.ts.map +1 -1
  29. package/dist/internal/machine/topology.js +16 -0
  30. package/dist/internal/machine/topology.js.map +1 -1
  31. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  32. package/dist/internal/testing/machine/finiteModel.js +15 -20
  33. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  34. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
  35. package/dist/internal/testing/machine/transitionCoverage.js +2 -0
  36. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
  37. package/dist/testing/MachineTest.d.ts +11 -11
  38. package/dist/testing/MachineTest.d.ts.map +1 -1
  39. package/dist/testing/MachineTest.js +5 -8
  40. package/dist/testing/MachineTest.js.map +1 -1
  41. package/dist/unstable/cluster/ClusterMachine.d.ts +2 -2
  42. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  43. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  44. package/dist/unstable/reactivity/AtomMachine.d.ts +12 -12
  45. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  46. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  47. package/docs/agent-guide.md +144 -147
  48. package/package.json +1 -1
  49. package/src/Machine.ts +1118 -1178
  50. package/src/internal/machine/atom.ts +20 -15
  51. package/src/internal/machine/cluster.ts +14 -9
  52. package/src/internal/machine/executionPlan.ts +8 -3
  53. package/src/internal/machine/machine.ts +301 -40
  54. package/src/internal/machine/planner.ts +89 -27
  55. package/src/internal/machine/stateDefinition.ts +39 -0
  56. package/src/internal/machine/topology.ts +28 -0
  57. package/src/internal/testing/machine/finiteModel.ts +18 -21
  58. package/src/internal/testing/machine/transitionCoverage.ts +2 -0
  59. package/src/testing/MachineTest.ts +14 -11
  60. package/src/unstable/cluster/ClusterMachine.ts +8 -4
  61. package/src/unstable/reactivity/AtomMachine.ts +19 -12
@@ -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.
@@ -127,9 +129,9 @@ its extra control is required:
127
129
  externally produced values, `after` for a timer, `logic` for reusable process
128
130
  logic, and `child` for a complete child
129
131
  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.
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.
133
135
  - Use `Machine.child(id, machine)` for a complete statechart descriptor and
134
136
  `Machine.childAddress<Event>(id)` for a low-level process address. A logic
135
137
  invocation is addressable only when `Machine.invoke` receives that
@@ -231,10 +233,7 @@ const States = Machine.states({
231
233
  }
232
234
  })
233
235
 
234
- initial: {
235
- target: (to) => to.Form.initial(),
236
- resolve: ({ target }) => target((form) => form.Editing.from())
237
- }
236
+ initial: (to) => to.Form.initial.resolve(({ target }) => target((form) => form.Editing.from()))
238
237
  ```
239
238
 
240
239
  Schema-less states have the same control semantics as schema-backed states:
@@ -244,13 +243,11 @@ in snapshots. They do not have a state value:
244
243
  ```ts
245
244
  Idle: {
246
245
  on: {
247
- Start: Machine.transition({
248
- target: (to) => to.full.Form.initial(),
249
- resolve: ({ state, target }) => {
246
+ Start: (to) =>
247
+ to.full.Form.initial.resolve(({ state, target }) => {
250
248
  // state: undefined
251
249
  return target.from((form) => form.Editing.from())
252
- }
253
- })
250
+ })
254
251
  }
255
252
  }
256
253
 
@@ -331,10 +328,7 @@ const States = Machine.states({
331
328
  const machine = Machine.make({
332
329
  states: States.states,
333
330
  events: Machine.events(),
334
- initial: {
335
- target: (to) => to.Done(),
336
- resolve: ({ target }) => target.from()
337
- }
331
+ initial: (to) => to.Done().resolve(({ target }) => target.from())
338
332
  }).handle({
339
333
  Done: {
340
334
  output: () => "done"
@@ -351,12 +345,12 @@ with `.initial`. This is available on top-level state methods under
351
345
  `target.branch`; atomic and final state methods do not expose it:
352
346
 
353
347
  ```ts
354
- Open: Machine.transition({
355
- target: (to) => to.full.opened.initial(),
356
- resolve: ({ target }) => target.from({ teamId: "team-1" })
357
- })
348
+ Open: (to) => to.full.opened.initial.resolve(({ target }) => target.from({ teamId: "team-1" }))
358
349
  ```
359
350
 
351
+ The definition-time `.initial` property is a topology value. The exact
352
+ resolver `target` is still a callable runtime builder.
353
+
360
354
  The selected state's own value is passed directly to `initial(value)` or
361
355
  constructed inside planning with `initial.from(input)`. A structural selected
362
356
  state uses `initial()`.
@@ -431,12 +425,12 @@ checkout: {
431
425
  Target it without a value:
432
426
 
433
427
  ```ts
434
- Resume: Machine.transition({
435
- target: (to) => to.history.checkout.exact(),
436
- resolve: ({ target }) => target()
437
- })
428
+ Resume: (to) => to.history.checkout.exact.resolve(({ target }) => target())
438
429
  ```
439
430
 
431
+ Each declared history leaf is a topology value; the resolver's selected
432
+ history builder remains callable to construct restoration evidence.
433
+
440
434
  Deep history restores the complete remembered subtree and its decoded values.
441
435
  Shallow history restores only parent and direct-child values. If the remembered
442
436
  child is compound, its configured initial child needs a freshly constructed
@@ -486,6 +480,22 @@ prior effects, machine instances, and timers are not rewound.
486
480
  | `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
481
  | `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
482
 
483
+ Definition-time instructions that only identify topology are values:
484
+ `to.none`, `to.full.Flow.initial`, `to.history.Flow.recent`, and
485
+ `to.local.with`. State and choice destinations remain calls, such as
486
+ `to.full.Running()` and `to.local.Routing()`, because those calls select the
487
+ node. Resolver-time builders also remain callable because they construct and,
488
+ for named branches, brand runtime evidence such as `select.unchanged()`.
489
+
490
+ Use `to.local.with` when a descendant transition updates the nearest
491
+ schema-backed compound value while retaining that same compound scope:
492
+
493
+ ```ts
494
+ Play: (to) =>
495
+ to.local.with.resolve(({ containingState, target }) =>
496
+ target.from({ ...containingState, playing: true }, (flow) => flow.Playing.from()))
497
+ ```
498
+
489
499
  Entering an inactive parallel state through `target.local` or `target.branch`
490
500
  requires a complete callback with one selection per region. A parallel state
491
501
  that is already active remains partially addressable through `target.branch`;
@@ -497,11 +507,16 @@ sets. A `target.full` result with the same active paths can update values withou
497
507
  exiting shared states. To force the source to exit and enter again:
498
508
 
499
509
  ```ts
500
- Refresh: Machine.transition({
501
- target: (to) => to.full.Ready(),
502
- reenter: true,
503
- resolve: ({ state, target }) => target.from({ value: state.value })
504
- })
510
+ Refresh: (to) =>
511
+ to.full.Ready().resolve(({ state, target }) => target.from({ value: state.value }), { reenter: true })
512
+ ```
513
+
514
+ When no resolver is needed, use the selected target directly and append
515
+ `.reenter()` only when restart semantics are intentional:
516
+
517
+ ```ts
518
+ Finish: (to) => to.full.Done()
519
+ Restart: (to) => to.none.reenter()
505
520
  ```
506
521
 
507
522
  Do not use `target.full` merely because it is easiest to discover. Prefer the
@@ -561,8 +576,9 @@ only schema-backed paths; use `matches` or `getSnapshot` for any active path.
561
576
  `context.containingState` is the immediate typed state value (`undefined` at a
562
577
  root or when that state is schema-less). `context.ancestors` contains only
563
578
  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:
579
+ present only when declared by the machine. `Machine.parent` makes it a required
580
+ owning-machine target; `Machine.optionalParent` makes it a target or
581
+ `undefined`. Use full state paths when another ancestor value is needed:
566
582
 
567
583
  ```ts
568
584
  ancestors["Route.Ready"]
@@ -578,16 +594,14 @@ Event, `always`, and `onDone` transition contexts include a fully typed
578
594
  microstep, before any selected transition is applied:
579
595
 
580
596
  ```ts
581
- BufferReady: Machine.transition({
582
- branches: (to) => ({
597
+ BufferReady: (to) =>
598
+ to.branches({
583
599
  online: { target: to.local.Playing() },
584
- unchanged: { target: to.none() }
585
- }),
586
- resolve: ({ snapshot, select }) =>
600
+ unchanged: { target: to.none }
601
+ }).resolve(({ snapshot, select }) =>
587
602
  States.matches(snapshot, "Player.Network.Online")
588
603
  ? select.online.from()
589
604
  : select.unchanged()
590
- })
591
605
  ```
592
606
 
593
607
  Use the existing `States.matches`, `States.get`, `States.getWithParents`, and
@@ -622,13 +636,11 @@ When sibling state payloads share fields, destructure away the source
622
636
  discriminator and construct the destination through its target builder:
623
637
 
624
638
  ```ts
625
- Submit: Machine.transition({
626
- target: (to) => to.local.Saving(),
627
- resolve: ({ state, target }) => {
639
+ Submit: (to) =>
640
+ to.local.Saving().resolve(({ state, target }) => {
628
641
  const { _tag: _, ...fields } = state
629
642
  return target.from({ ...fields, attempt: 1 })
630
- }
631
- })
643
+ })
632
644
  ```
633
645
 
634
646
  The target schema remains responsible for defaults, transforms, refinements,
@@ -641,45 +653,66 @@ A transition declares every possible branch and resolves the selected target
641
653
  synchronously:
642
654
 
643
655
  ```ts
644
- Submit: Machine.transition({
645
- branches: (to) => ({
656
+ Submit: (to) =>
657
+ to.branches({
646
658
  valid: { target: to.local.Saving() },
647
- invalid: { target: to.none() }
648
- }),
649
- resolve: ({ state, select }) => state.valid
659
+ invalid: { target: to.none }
660
+ }).resolve(({ state, select }) => state.valid
650
661
  ? select.valid.from({ draft: state.draft })
651
662
  : select.invalid()
652
- })
653
663
  ```
654
664
 
655
665
  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
666
+ receives a bound `to` selector. A direct transition selects one target and calls
667
+ its `resolve` method. A branching transition calls `to.branches` with every
668
+ possible target, then uses ordinary TypeScript control flow in `resolve` to return one
659
669
  typed `select` builder. Branch keys are stable testing and inspection identities;
660
670
  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
671
+ Selecting a branch whose target is `to.none` handles the transition without a
662
672
  destination while retaining queued commands, raised events, and emitted events.
663
673
 
674
+ Set `declinable: true` only when the resolver may decide that its transition is
675
+ not enabled. The flag adds a typed `decline()` capability to that resolver and
676
+ permits its opaque result:
677
+
678
+ ```ts
679
+ Submit: (to) =>
680
+ to.branches({
681
+ accepted: { target: to.local.Saving() },
682
+ consumed: { target: to.none }
683
+ }).resolve(({ event, select, decline }) => {
684
+ if (!belongsToThisState(event)) return decline()
685
+ return event.consume ? select.consumed() : select.accepted.from()
686
+ }, { declinable: true })
687
+ ```
688
+
689
+ Declining discards that resolver's enqueue buffer and resumes hierarchical
690
+ event or eventless selection at the next eligible ancestor. If no candidate
691
+ accepts, the trigger is unhandled. This is deliberately different from
692
+ `to.none`, which consumes the trigger. `decline()` is absent and its result is
693
+ rejected unless the literal flag is present. Choice and initial routing remain
694
+ total and cannot decline. Static inspection exposes the distinction through
695
+ `TransitionDefinition.acceptance` without executing resolver code. Completion
696
+ and invocation outcomes have no ancestor candidate; declining one ignores that
697
+ lifecycle occurrence and leaves the current configuration active.
698
+
664
699
  The `branches` callback runs once when handlers are installed. Its record uses
665
700
  the deterministic ECMAScript property order for presentation and `branchIndex`;
666
701
  array-index and symbol keys are rejected. Treat the string key as semantic:
667
702
  reordering named properties may change their display index, but visualizers,
668
703
  coverage, and trace verification identify each branch by its key.
669
704
 
670
- `reenter: true` remains meaningful with `to.none()`: the source exits and
705
+ `reenter: true` remains meaningful with `to.none`: the source exits and
671
706
  enters again while its logical configuration is retained.
672
707
 
673
708
  Closed statechart and machine operations use `enqueue`:
674
709
 
675
710
  ```ts
676
- Submit: Machine.transition({
677
- target: (to) => to.local.Saving(),
678
- resolve: ({ target }, enqueue) => {
711
+ Submit: (to) =>
712
+ to.local.Saving().resolve(({ target }, enqueue) => {
679
713
  enqueue.emit(Emissions.SaveRequested())
680
714
  return target.from()
681
- }
682
- })
715
+ })
683
716
  ```
684
717
 
685
718
  Declare emission constructors separately from machine inputs:
@@ -768,20 +801,15 @@ export const ParentEvents = Machine.events(ChildFinished)
768
801
 
769
802
  const child = Machine.make({
770
803
  events: ChildEvents,
771
- parentEvents: ParentEvents,
804
+ parent: Machine.parent(ParentEvents),
772
805
  // ...
773
806
  }).handle({
774
807
  Working: {
775
808
  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
- })
809
+ Finish: (to) =>
810
+ to.none.resolve(({ parent }, enqueue) => {
811
+ enqueue.sendTo(parent, ParentEvents.ChildFinished())
812
+ })
785
813
  }
786
814
  }
787
815
  })
@@ -792,12 +820,14 @@ const parent = Machine.make({
792
820
  })
793
821
  ```
794
822
 
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
823
+ Invoking the child under a parent that lacks any required parent event is a
824
+ type error. Within child handlers, `parent` accepts only that protocol and is
825
+ not optional. Root APIs reject the machine. Use
826
+ `Machine.optionalParent(ParentEvents)` instead when the same definition must
827
+ also run as a root; then `parent` is optional. With no declaration, callbacks
828
+ have no `parent` property. `self` accepts the machine's public inputs. Both
829
+ targets are minimal `MachineTarget<Event>` values. Neither machine target is a
830
+ structural state value; use
801
831
  `containingState` and `ancestors` for statechart ancestry.
802
832
 
803
833
  Atom-backed machines retain the same transient semantics. Use
@@ -856,10 +886,7 @@ const definition = Machine.make({
856
886
  states: States.states,
857
887
  events: Events,
858
888
  internalEvents: InternalEvents,
859
- initial: {
860
- target: (to) => to.Idle(),
861
- resolve: ({ target }) => target.from()
862
- }
889
+ initial: (to) => to.Idle().resolve(({ target }) => target.from())
863
890
  })
864
891
  ```
865
892
 
@@ -915,14 +942,9 @@ receive the typed Effect channels and can transition directly:
915
942
  invoke: Machine.invoke({
916
943
  id: "save",
917
944
  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
- })
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 }))
926
948
  })
927
949
  ```
928
950
 
@@ -945,17 +967,12 @@ handles normal Stream completion and `onFailure` handles the typed Stream error:
945
967
  invoke: Machine.invoke({
946
968
  id: "broadcast-channel",
947
969
  stream: () => messages,
948
- onElement: {
949
- target: Machine.targetless,
950
- resolve: ({ element }, enqueue) => {
970
+ onElement: (to) =>
971
+ to.none.resolve(({ element }, enqueue) => {
951
972
  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
- })
973
+ }),
974
+ onDone: (to) => to.none,
975
+ onFailure: (to) => to.full.Disconnected().resolve(({ error, target }) => target.from({ error }))
959
976
  })
960
977
  ```
961
978
 
@@ -964,10 +981,9 @@ after the selected parent macrostep commits. Exiting or reentering the owner
964
981
  interrupts the Stream and runs its finalizers. A later entry starts a fresh
965
982
  Stream. Stream defects and self-interruption fail the owning machine.
966
983
 
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.
984
+ Use `to.none` when a transition keeps the current configuration. Call
985
+ `to.none.resolve(...)` when it also enqueues commands; a block resolver may
986
+ omit its return because it is contextually typed to return `undefined`.
971
987
 
972
988
  When a source function reads `state`, `containingState`, `ancestors`, or the entry `event`,
973
989
  `Machine.invoke` infers that owner context and the returned Effect's output,
@@ -977,54 +993,40 @@ error, and service channels together. No return annotation is needed:
977
993
  invoke: Machine.invoke({
978
994
  id: "load",
979
995
  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
- })
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 }))
988
998
  })
989
999
  ```
990
1000
 
991
1001
  Inside `.handle(...)`, the constructor receives the owning machine's public
992
- input and `parentEvents` protocols contextually. Sources and lifecycle handlers
1002
+ input and declared parent protocol contextually. Sources and lifecycle handlers
993
1003
  can send through `self` and `parent` without naming the definition:
994
1004
 
995
1005
  ```ts
996
1006
  const machine = Machine.make({
997
1007
  events: Commands,
998
1008
  internalEvents: InternalEvents,
999
- parentEvents: ParentEvents,
1009
+ parent: Machine.parent(ParentEvents),
1000
1010
  // ...
1001
1011
  }).handle({
1002
1012
  Saving: {
1003
1013
  invoke: Machine.invoke({
1004
1014
  id: "notify-parent",
1005
1015
  effect: () => saveDocument,
1006
- onDone: Machine.transition({
1007
- target: (to) => to.none(),
1008
- resolve: ({ parent, self }, enqueue) => {
1016
+ onDone: (to) =>
1017
+ to.none.resolve(({ parent, self }, enqueue) => {
1009
1018
  enqueue.sendTo(self, Commands.Save())
1010
- if (parent !== undefined) {
1011
- 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
- })
1019
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
1020
+ }),
1021
+ onFailure: (to) => to.none
1020
1022
  })
1021
1023
  }
1022
1024
  })
1023
1025
  ```
1024
1026
 
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.
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.
1028
1030
 
1029
1031
  A cancellable timer uses the same object:
1030
1032
 
@@ -1032,10 +1034,7 @@ A cancellable timer uses the same object:
1032
1034
  invoke: Machine.invoke({
1033
1035
  id: "clear-status",
1034
1036
  after: "3 seconds",
1035
- onDone: Machine.transition({
1036
- target: (to) => to.full.Clear(),
1037
- resolve: ({ target }) => target()
1038
- })
1037
+ onDone: (to) => to.full.Clear().resolve(({ target }) => target())
1039
1038
  })
1040
1039
  ```
1041
1040
 
@@ -1062,10 +1061,7 @@ Invoke it from its owning state:
1062
1061
  invoke: Machine.invoke({
1063
1062
  child: Editor,
1064
1063
  input: editorInput,
1065
- onDone: Machine.transition({
1066
- target: (to) => to.full.EditorDone(),
1067
- resolve: ({ output, target }) => target.from({ output })
1068
- })
1064
+ onDone: (to) => to.full.EditorDone().resolve(({ output, target }) => target.from({ output }))
1069
1065
  })
1070
1066
  ```
1071
1067
 
@@ -1313,9 +1309,10 @@ const step = yield * probe.sendAndAwait(event)
1313
1309
  ```
1314
1310
 
1315
1311
  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.
1312
+ `step.configurationChanged`. An ignored event, including one for which every
1313
+ eligible candidate declines, has `handled: false` and an empty microstep list,
1314
+ but still completes its acknowledgement. A targetless handler has
1315
+ `handled: true` even if its before and after snapshots are equal.
1319
1316
 
1320
1317
  Do not use a probe as a substitute for a domain completion event. The
1321
1318
  acknowledgement covers the submitted event's synchronous macrostep, state
@@ -1385,10 +1382,7 @@ reference model when correctness of the expected behavior matters.
1385
1382
  Select the initial root separately from constructing its value:
1386
1383
 
1387
1384
  ```ts
1388
- initial: {
1389
- target: (to) => to.Idle(),
1390
- resolve: ({ target }) => target.from()
1391
- }
1385
+ initial: (to) => to.Idle().resolve(({ target }) => target.from())
1392
1386
  ```
1393
1387
 
1394
1388
  ### Invoked child expects events not accepted by the parent
@@ -1399,13 +1393,16 @@ the parent's public events:
1399
1393
  ```ts
1400
1394
  export const ChildParentEvents = Machine.events(ChildFinished)
1401
1395
 
1402
- // child
1403
- parentEvents: ChildParentEvents
1396
+ // child-only machine
1397
+ parent: Machine.parent(ChildParentEvents)
1404
1398
 
1405
1399
  // parent
1406
1400
  events: Machine.events(Submit, ChildParentEvents)
1407
1401
  ```
1408
1402
 
1403
+ Use `Machine.optionalParent(ChildParentEvents)` only when the child is also a
1404
+ valid independent root and narrow `parent` before sending.
1405
+
1409
1406
  ### An internal event is rejected by `send`
1410
1407
 
1411
1408
  This is intentional. Public input boundaries accept only schemas declared in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/effect-machine",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Schema-first state machines and statecharts for Effect",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {