@typeonce/effect-machine 0.15.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 (85) hide show
  1. package/README.md +115 -95
  2. package/dist/Machine.d.ts +464 -337
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +72 -107
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +2 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +4 -0
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/atom.d.ts +7 -7
  11. package/dist/internal/machine/atom.d.ts.map +1 -1
  12. package/dist/internal/machine/atom.js.map +1 -1
  13. package/dist/internal/machine/cluster.d.ts +2 -2
  14. package/dist/internal/machine/cluster.d.ts.map +1 -1
  15. package/dist/internal/machine/cluster.js +6 -5
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  18. package/dist/internal/machine/executionPlan.js +16 -4
  19. package/dist/internal/machine/executionPlan.js.map +1 -1
  20. package/dist/internal/machine/invocation.d.ts +1 -1
  21. package/dist/internal/machine/invocation.d.ts.map +1 -1
  22. package/dist/internal/machine/invocation.js +25 -3
  23. package/dist/internal/machine/invocation.js.map +1 -1
  24. package/dist/internal/machine/invocationEvent.d.ts +8 -0
  25. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  26. package/dist/internal/machine/invocationEvent.js +8 -0
  27. package/dist/internal/machine/invocationEvent.js.map +1 -1
  28. package/dist/internal/machine/machine.d.ts +8 -6
  29. package/dist/internal/machine/machine.d.ts.map +1 -1
  30. package/dist/internal/machine/machine.js +308 -71
  31. package/dist/internal/machine/machine.js.map +1 -1
  32. package/dist/internal/machine/planner.d.ts +26 -4
  33. package/dist/internal/machine/planner.d.ts.map +1 -1
  34. package/dist/internal/machine/planner.js +76 -31
  35. package/dist/internal/machine/planner.js.map +1 -1
  36. package/dist/internal/machine/runtime.d.ts +1 -0
  37. package/dist/internal/machine/runtime.d.ts.map +1 -1
  38. package/dist/internal/machine/runtime.js +25 -16
  39. package/dist/internal/machine/runtime.js.map +1 -1
  40. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  41. package/dist/internal/machine/stateDefinition.js +14 -0
  42. package/dist/internal/machine/stateDefinition.js.map +1 -1
  43. package/dist/internal/machine/topology.d.ts +20 -0
  44. package/dist/internal/machine/topology.d.ts.map +1 -1
  45. package/dist/internal/machine/topology.js +33 -6
  46. package/dist/internal/machine/topology.js.map +1 -1
  47. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  48. package/dist/internal/testing/machine/finiteModel.js +15 -20
  49. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  50. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
  51. package/dist/internal/testing/machine/transitionCoverage.js +8 -2
  52. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
  53. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  54. package/dist/internal/testing/machine/verification.js +6 -0
  55. package/dist/internal/testing/machine/verification.js.map +1 -1
  56. package/dist/testing/MachineTest.d.ts +13 -12
  57. package/dist/testing/MachineTest.d.ts.map +1 -1
  58. package/dist/testing/MachineTest.js +5 -8
  59. package/dist/testing/MachineTest.js.map +1 -1
  60. package/dist/unstable/cluster/ClusterMachine.d.ts +2 -2
  61. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  62. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  63. package/dist/unstable/reactivity/AtomMachine.d.ts +12 -12
  64. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  65. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  66. package/docs/agent-guide.md +182 -159
  67. package/package.json +1 -1
  68. package/src/Machine.ts +1073 -700
  69. package/src/internal/machine/activities.ts +7 -0
  70. package/src/internal/machine/atom.ts +20 -15
  71. package/src/internal/machine/cluster.ts +14 -9
  72. package/src/internal/machine/executionPlan.ts +14 -4
  73. package/src/internal/machine/invocation.ts +39 -4
  74. package/src/internal/machine/invocationEvent.ts +16 -0
  75. package/src/internal/machine/machine.ts +460 -86
  76. package/src/internal/machine/planner.ts +106 -30
  77. package/src/internal/machine/runtime.ts +61 -25
  78. package/src/internal/machine/stateDefinition.ts +39 -0
  79. package/src/internal/machine/topology.ts +59 -2
  80. package/src/internal/testing/machine/finiteModel.ts +18 -21
  81. package/src/internal/testing/machine/transitionCoverage.ts +8 -2
  82. package/src/internal/testing/machine/verification.ts +11 -0
  83. package/src/testing/MachineTest.ts +16 -11
  84. package/src/unstable/cluster/ClusterMachine.ts +8 -4
  85. 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.
@@ -123,12 +125,13 @@ 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, `after` for a timer,
127
- `logic` for reusable process logic, and `child` for a complete child
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
128
131
  statechart. `Machine.invoke({...})` preserves owner state and source channels
129
- across sibling lifecycle handlers. Inside `.handle(...)`, `self` and `parent`
130
- use the owning definition's exact public input and `parentEvents` protocols.
131
- The bound `definition.invoke({...})` form is equivalent, not required.
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.
132
135
  - Use `Machine.child(id, machine)` for a complete statechart descriptor and
133
136
  `Machine.childAddress<Event>(id)` for a low-level process address. A logic
134
137
  invocation is addressable only when `Machine.invoke` receives that
@@ -230,10 +233,7 @@ const States = Machine.states({
230
233
  }
231
234
  })
232
235
 
233
- initial: {
234
- target: (to) => to.Form.initial(),
235
- resolve: ({ target }) => target((form) => form.Editing.from())
236
- }
236
+ initial: (to) => to.Form.initial.resolve(({ target }) => target((form) => form.Editing.from()))
237
237
  ```
238
238
 
239
239
  Schema-less states have the same control semantics as schema-backed states:
@@ -243,13 +243,11 @@ in snapshots. They do not have a state value:
243
243
  ```ts
244
244
  Idle: {
245
245
  on: {
246
- Start: Machine.transition({
247
- target: (to) => to.full.Form.initial(),
248
- resolve: ({ state, target }) => {
246
+ Start: (to) =>
247
+ to.full.Form.initial.resolve(({ state, target }) => {
249
248
  // state: undefined
250
249
  return target.from((form) => form.Editing.from())
251
- }
252
- })
250
+ })
253
251
  }
254
252
  }
255
253
 
@@ -330,10 +328,7 @@ const States = Machine.states({
330
328
  const machine = Machine.make({
331
329
  states: States.states,
332
330
  events: Machine.events(),
333
- initial: {
334
- target: (to) => to.Done(),
335
- resolve: ({ target }) => target.from()
336
- }
331
+ initial: (to) => to.Done().resolve(({ target }) => target.from())
337
332
  }).handle({
338
333
  Done: {
339
334
  output: () => "done"
@@ -350,12 +345,12 @@ with `.initial`. This is available on top-level state methods under
350
345
  `target.branch`; atomic and final state methods do not expose it:
351
346
 
352
347
  ```ts
353
- Open: Machine.transition({
354
- target: (to) => to.full.opened.initial(),
355
- resolve: ({ target }) => target.from({ teamId: "team-1" })
356
- })
348
+ Open: (to) => to.full.opened.initial.resolve(({ target }) => target.from({ teamId: "team-1" }))
357
349
  ```
358
350
 
351
+ The definition-time `.initial` property is a topology value. The exact
352
+ resolver `target` is still a callable runtime builder.
353
+
359
354
  The selected state's own value is passed directly to `initial(value)` or
360
355
  constructed inside planning with `initial.from(input)`. A structural selected
361
356
  state uses `initial()`.
@@ -430,12 +425,12 @@ checkout: {
430
425
  Target it without a value:
431
426
 
432
427
  ```ts
433
- Resume: Machine.transition({
434
- target: (to) => to.history.checkout.exact(),
435
- resolve: ({ target }) => target()
436
- })
428
+ Resume: (to) => to.history.checkout.exact.resolve(({ target }) => target())
437
429
  ```
438
430
 
431
+ Each declared history leaf is a topology value; the resolver's selected
432
+ history builder remains callable to construct restoration evidence.
433
+
439
434
  Deep history restores the complete remembered subtree and its decoded values.
440
435
  Shallow history restores only parent and direct-child values. If the remembered
441
436
  child is compound, its configured initial child needs a freshly constructed
@@ -485,6 +480,22 @@ prior effects, machine instances, and timers are not rewound.
485
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 |
486
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 |
487
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
+
488
499
  Entering an inactive parallel state through `target.local` or `target.branch`
489
500
  requires a complete callback with one selection per region. A parallel state
490
501
  that is already active remains partially addressable through `target.branch`;
@@ -496,11 +507,16 @@ sets. A `target.full` result with the same active paths can update values withou
496
507
  exiting shared states. To force the source to exit and enter again:
497
508
 
498
509
  ```ts
499
- Refresh: Machine.transition({
500
- target: (to) => to.full.Ready(),
501
- reenter: true,
502
- resolve: ({ state, target }) => target.from({ value: state.value })
503
- })
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()
504
520
  ```
505
521
 
506
522
  Do not use `target.full` merely because it is easiest to discover. Prefer the
@@ -560,8 +576,9 @@ only schema-backed paths; use `matches` or `getSnapshot` for any active path.
560
576
  `context.containingState` is the immediate typed state value (`undefined` at a
561
577
  root or when that state is schema-less). `context.ancestors` contains only
562
578
  valued structural ancestors. This is separate from `context.parent`, which is
563
- the owning machine target or `undefined` for a root machine. Use full state paths
564
- 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:
565
582
 
566
583
  ```ts
567
584
  ancestors["Route.Ready"]
@@ -577,23 +594,14 @@ Event, `always`, and `onDone` transition contexts include a fully typed
577
594
  microstep, before any selected transition is applied:
578
595
 
579
596
  ```ts
580
- BufferReady: Machine.transition({
581
- cases: (branch) => [
582
- branch({
583
- title: "online",
584
- when: ({ snapshot }) =>
585
- States.matches(snapshot, "Player.Network.Online")
586
- ? Option.some(undefined)
587
- : Option.none(),
588
- target: (to) => to.local.Playing(),
589
- resolve: ({ target }) => target.from()
590
- })
591
- ],
592
- otherwise: {
593
- target: (to) => to.none(),
594
- resolve: () => undefined
595
- }
596
- })
597
+ BufferReady: (to) =>
598
+ to.branches({
599
+ online: { target: to.local.Playing() },
600
+ unchanged: { target: to.none }
601
+ }).resolve(({ snapshot, select }) =>
602
+ States.matches(snapshot, "Player.Network.Online")
603
+ ? select.online.from()
604
+ : select.unchanged()
597
605
  ```
598
606
 
599
607
  Use the existing `States.matches`, `States.get`, `States.getWithParents`, and
@@ -628,13 +636,11 @@ When sibling state payloads share fields, destructure away the source
628
636
  discriminator and construct the destination through its target builder:
629
637
 
630
638
  ```ts
631
- Submit: Machine.transition({
632
- target: (to) => to.local.Saving(),
633
- resolve: ({ state, target }) => {
639
+ Submit: (to) =>
640
+ to.local.Saving().resolve(({ state, target }) => {
634
641
  const { _tag: _, ...fields } = state
635
642
  return target.from({ ...fields, attempt: 1 })
636
- }
637
- })
643
+ })
638
644
  ```
639
645
 
640
646
  The target schema remains responsible for defaults, transforms, refinements,
@@ -647,45 +653,66 @@ A transition declares every possible branch and resolves the selected target
647
653
  synchronously:
648
654
 
649
655
  ```ts
650
- Submit: Machine.transition({
651
- cases: (branch) => [
652
- branch({
653
- title: "valid",
654
- when: ({ state }) => state.valid ? Option.some(state.draft) : Option.none(),
655
- target: (to) => to.local.Saving(),
656
- resolve: ({ match, target }) => target.from({ draft: match })
657
- })
658
- ],
659
- otherwise: {
660
- target: (to) => to.none(),
661
- resolve: () => undefined
662
- }
663
- })
656
+ Submit: (to) =>
657
+ to.branches({
658
+ valid: { target: to.local.Saving() },
659
+ invalid: { target: to.none }
660
+ }).resolve(({ state, select }) => state.valid
661
+ ? select.valid.from({ draft: state.draft })
662
+ : select.invalid()
664
663
  ```
665
664
 
666
665
  Every installed event, `always`, `onDone`, choice, and invoke lifecycle handler
667
- must use `Machine.transition`. Each direct branch declares one `target`; a
668
- conditional transition declares ordered `cases` and a required `otherwise`.
669
- Construct each case with the locally supplied `branch` function. Every call
670
- independently infers its `when` match and target, so a transition may declare
671
- any number of heterogeneous cases without losing resolver inference. `when`
672
- returns `Option.some(match)` to select a case and expose `match` in its resolver.
673
- Selecting `to.none()` handles the transition without a destination, while
674
- retaining queued commands, raised events, and emitted events.
675
-
676
- `reenter: true` remains meaningful with `to.none()`: the source exits and
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
669
+ typed `select` builder. Branch keys are stable testing and inspection identities;
670
+ an optional `title` controls presentation and otherwise defaults to the key.
671
+ Selecting a branch whose target is `to.none` handles the transition without a
672
+ destination while retaining queued commands, raised events, and emitted events.
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
+
699
+ The `branches` callback runs once when handlers are installed. Its record uses
700
+ the deterministic ECMAScript property order for presentation and `branchIndex`;
701
+ array-index and symbol keys are rejected. Treat the string key as semantic:
702
+ reordering named properties may change their display index, but visualizers,
703
+ coverage, and trace verification identify each branch by its key.
704
+
705
+ `reenter: true` remains meaningful with `to.none`: the source exits and
677
706
  enters again while its logical configuration is retained.
678
707
 
679
708
  Closed statechart and machine operations use `enqueue`:
680
709
 
681
710
  ```ts
682
- Submit: Machine.transition({
683
- target: (to) => to.local.Saving(),
684
- resolve: ({ target }, enqueue) => {
711
+ Submit: (to) =>
712
+ to.local.Saving().resolve(({ target }, enqueue) => {
685
713
  enqueue.emit(Emissions.SaveRequested())
686
714
  return target.from()
687
- }
688
- })
715
+ })
689
716
  ```
690
717
 
691
718
  Declare emission constructors separately from machine inputs:
@@ -774,20 +801,15 @@ export const ParentEvents = Machine.events(ChildFinished)
774
801
 
775
802
  const child = Machine.make({
776
803
  events: ChildEvents,
777
- parentEvents: ParentEvents,
804
+ parent: Machine.parent(ParentEvents),
778
805
  // ...
779
806
  }).handle({
780
807
  Working: {
781
808
  on: {
782
- Finish: Machine.transition({
783
- target: (to) => to.none(),
784
- resolve: ({ parent }, enqueue) => {
785
- if (parent !== undefined) {
786
- enqueue.sendTo(parent, ParentEvents.ChildFinished())
787
- }
788
- return undefined
789
- }
790
- })
809
+ Finish: (to) =>
810
+ to.none.resolve(({ parent }, enqueue) => {
811
+ enqueue.sendTo(parent, ParentEvents.ChildFinished())
812
+ })
791
813
  }
792
814
  }
793
815
  })
@@ -798,12 +820,14 @@ const parent = Machine.make({
798
820
  })
799
821
  ```
800
822
 
801
- Invoking the child under a parent that lacks any required `parentEvents` case
802
- is a type error. Within child handlers, `parent` accepts only that protocol.
803
- The same child may run as a root, where `parent` is `undefined`. `self` accepts
804
- the machine's public inputs. Both are minimal `MachineTarget<Event>` values,
805
- provided by the shared `MachineReferences<InputEvents, ParentEvents>` handler
806
- 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
807
831
  `containingState` and `ancestors` for statechart ancestry.
808
832
 
809
833
  Atom-backed machines retain the same transient semantics. Use
@@ -862,10 +886,7 @@ const definition = Machine.make({
862
886
  states: States.states,
863
887
  events: Events,
864
888
  internalEvents: InternalEvents,
865
- initial: {
866
- target: (to) => to.Idle(),
867
- resolve: ({ target }) => target.from()
868
- }
889
+ initial: (to) => to.Idle().resolve(({ target }) => target.from())
869
890
  })
870
891
  ```
871
892
 
@@ -921,14 +942,9 @@ receive the typed Effect channels and can transition directly:
921
942
  invoke: Machine.invoke({
922
943
  id: "save",
923
944
  effect: () => SaveService.save(draft),
924
- onDone: Machine.transition({
925
- target: (to) => to.full.Saved(),
926
- resolve: ({ output, target }) => target.from({ entry: output })
927
- }),
928
- onFailure: Machine.transition({
929
- target: (to) => to.full.SaveFailed(),
930
- resolve: ({ error, target }) => target.from({ message: error.message })
931
- })
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 }))
932
948
  })
933
949
  ```
934
950
 
@@ -943,6 +959,32 @@ needs `state`, `containingState`, `ancestors`, or the entry `event`. Source cons
943
959
  errors, defects, and interruption are machine failures rather than a second
944
960
  phase in `onFailure`.
945
961
 
962
+ Use a Stream invocation for repeated values that are not themselves machine
963
+ events. `onElement` maps each value into an owner transition, while `onDone`
964
+ handles normal Stream completion and `onFailure` handles the typed Stream error:
965
+
966
+ ```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 }))
976
+ })
977
+ ```
978
+
979
+ Element delivery is owner-scoped and backpressured: the Stream pulls again only
980
+ after the selected parent macrostep commits. Exiting or reentering the owner
981
+ interrupts the Stream and runs its finalizers. A later entry starts a fresh
982
+ Stream. Stream defects and self-interruption fail the owning machine.
983
+
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`.
987
+
946
988
  When a source function reads `state`, `containingState`, `ancestors`, or the entry `event`,
947
989
  `Machine.invoke` infers that owner context and the returned Effect's output,
948
990
  error, and service channels together. No return annotation is needed:
@@ -951,54 +993,40 @@ error, and service channels together. No return annotation is needed:
951
993
  invoke: Machine.invoke({
952
994
  id: "load",
953
995
  effect: ({ state }) => LoadService.load(state.userId),
954
- onDone: Machine.transition({
955
- target: (to) => to.full.Loaded(),
956
- resolve: ({ output, target }) => target.from({ user: output })
957
- }),
958
- onFailure: Machine.transition({
959
- target: (to) => to.full.LoadFailed(),
960
- resolve: ({ error, target }) => target.from({ error })
961
- })
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 }))
962
998
  })
963
999
  ```
964
1000
 
965
1001
  Inside `.handle(...)`, the constructor receives the owning machine's public
966
- input and `parentEvents` protocols contextually. Sources and lifecycle handlers
1002
+ input and declared parent protocol contextually. Sources and lifecycle handlers
967
1003
  can send through `self` and `parent` without naming the definition:
968
1004
 
969
1005
  ```ts
970
1006
  const machine = Machine.make({
971
1007
  events: Commands,
972
1008
  internalEvents: InternalEvents,
973
- parentEvents: ParentEvents,
1009
+ parent: Machine.parent(ParentEvents),
974
1010
  // ...
975
1011
  }).handle({
976
1012
  Saving: {
977
1013
  invoke: Machine.invoke({
978
1014
  id: "notify-parent",
979
1015
  effect: () => saveDocument,
980
- onDone: Machine.transition({
981
- target: (to) => to.none(),
982
- resolve: ({ parent, self }, enqueue) => {
1016
+ onDone: (to) =>
1017
+ to.none.resolve(({ parent, self }, enqueue) => {
983
1018
  enqueue.sendTo(self, Commands.Save())
984
- if (parent !== undefined) {
985
- enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
986
- }
987
- return undefined
988
- }
989
- }),
990
- onFailure: Machine.transition({
991
- target: (to) => to.none(),
992
- resolve: () => undefined
993
- })
1019
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
1020
+ }),
1021
+ onFailure: (to) => to.none
994
1022
  })
995
1023
  }
996
1024
  })
997
1025
  ```
998
1026
 
999
- The machine-bound `definition.invoke(...)` form remains equivalent when the
1000
- definition is already named. A direct `invoke: { ... }` object remains available
1001
- when lifecycle handlers do not need source-derived context.
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.
1002
1030
 
1003
1031
  A cancellable timer uses the same object:
1004
1032
 
@@ -1006,10 +1034,7 @@ A cancellable timer uses the same object:
1006
1034
  invoke: Machine.invoke({
1007
1035
  id: "clear-status",
1008
1036
  after: "3 seconds",
1009
- onDone: Machine.transition({
1010
- target: (to) => to.full.Clear(),
1011
- resolve: ({ target }) => target()
1012
- })
1037
+ onDone: (to) => to.full.Clear().resolve(({ target }) => target())
1013
1038
  })
1014
1039
  ```
1015
1040
 
@@ -1036,10 +1061,7 @@ Invoke it from its owning state:
1036
1061
  invoke: Machine.invoke({
1037
1062
  child: Editor,
1038
1063
  input: editorInput,
1039
- onDone: Machine.transition({
1040
- target: (to) => to.full.EditorDone(),
1041
- resolve: ({ output, target }) => target.from({ output })
1042
- })
1064
+ onDone: (to) => to.full.EditorDone().resolve(({ output, target }) => target.from({ output }))
1043
1065
  })
1044
1066
  ```
1045
1067
 
@@ -1287,9 +1309,10 @@ const step = yield * probe.sendAndAwait(event)
1287
1309
  ```
1288
1310
 
1289
1311
  Inspect `step.before`, `step.after`, `step.plan`, `step.handled`, and
1290
- `step.configurationChanged`. An ignored event has `handled: false` and an
1291
- empty microstep list, but still completes its acknowledgement. A targetless
1292
- 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.
1293
1316
 
1294
1317
  Do not use a probe as a substitute for a domain completion event. The
1295
1318
  acknowledgement covers the submitted event's synchronous macrostep, state
@@ -1359,10 +1382,7 @@ reference model when correctness of the expected behavior matters.
1359
1382
  Select the initial root separately from constructing its value:
1360
1383
 
1361
1384
  ```ts
1362
- initial: {
1363
- target: (to) => to.Idle(),
1364
- resolve: ({ target }) => target.from()
1365
- }
1385
+ initial: (to) => to.Idle().resolve(({ target }) => target.from())
1366
1386
  ```
1367
1387
 
1368
1388
  ### Invoked child expects events not accepted by the parent
@@ -1373,13 +1393,16 @@ the parent's public events:
1373
1393
  ```ts
1374
1394
  export const ChildParentEvents = Machine.events(ChildFinished)
1375
1395
 
1376
- // child
1377
- parentEvents: ChildParentEvents
1396
+ // child-only machine
1397
+ parent: Machine.parent(ChildParentEvents)
1378
1398
 
1379
1399
  // parent
1380
1400
  events: Machine.events(Submit, ChildParentEvents)
1381
1401
  ```
1382
1402
 
1403
+ Use `Machine.optionalParent(ChildParentEvents)` only when the child is also a
1404
+ valid independent root and narrow `parent` before sending.
1405
+
1383
1406
  ### An internal event is rejected by `send`
1384
1407
 
1385
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.15.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": {