@typeonce/effect-machine 0.8.0 → 0.10.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 (108) hide show
  1. package/README.md +180 -40
  2. package/dist/Machine.d.ts +659 -481
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +86 -222
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +5 -12
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +47 -17
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/atom.d.ts +6 -2
  11. package/dist/internal/machine/atom.d.ts.map +1 -1
  12. package/dist/internal/machine/atom.js +5 -0
  13. package/dist/internal/machine/atom.js.map +1 -1
  14. package/dist/internal/machine/cluster.d.ts.map +1 -1
  15. package/dist/internal/machine/cluster.js +2 -1
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/command.js +2 -2
  18. package/dist/internal/machine/command.js.map +1 -1
  19. package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
  20. package/dist/internal/machine/commandRuntime.js +6 -3
  21. package/dist/internal/machine/commandRuntime.js.map +1 -1
  22. package/dist/internal/machine/configuration.d.ts +8 -1
  23. package/dist/internal/machine/configuration.d.ts.map +1 -1
  24. package/dist/internal/machine/configuration.js +9 -4
  25. package/dist/internal/machine/configuration.js.map +1 -1
  26. package/dist/internal/machine/errors.d.ts +1 -1
  27. package/dist/internal/machine/errors.d.ts.map +1 -1
  28. package/dist/internal/machine/executionPlan.d.ts +3 -3
  29. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  30. package/dist/internal/machine/executionPlan.js +74 -24
  31. package/dist/internal/machine/executionPlan.js.map +1 -1
  32. package/dist/internal/machine/invocation.d.ts +10 -2
  33. package/dist/internal/machine/invocation.d.ts.map +1 -1
  34. package/dist/internal/machine/invocation.js +94 -34
  35. package/dist/internal/machine/invocation.js.map +1 -1
  36. package/dist/internal/machine/invocationEvent.d.ts +39 -0
  37. package/dist/internal/machine/invocationEvent.d.ts.map +1 -0
  38. package/dist/internal/machine/invocationEvent.js +43 -0
  39. package/dist/internal/machine/invocationEvent.js.map +1 -0
  40. package/dist/internal/machine/machine.d.ts +16 -65
  41. package/dist/internal/machine/machine.d.ts.map +1 -1
  42. package/dist/internal/machine/machine.js +18 -73
  43. package/dist/internal/machine/machine.js.map +1 -1
  44. package/dist/internal/machine/planner.d.ts +4 -2
  45. package/dist/internal/machine/planner.d.ts.map +1 -1
  46. package/dist/internal/machine/planner.js +104 -24
  47. package/dist/internal/machine/planner.js.map +1 -1
  48. package/dist/internal/machine/process.d.ts.map +1 -1
  49. package/dist/internal/machine/process.js +8 -8
  50. package/dist/internal/machine/process.js.map +1 -1
  51. package/dist/internal/machine/protocol.d.ts +8 -5
  52. package/dist/internal/machine/protocol.d.ts.map +1 -1
  53. package/dist/internal/machine/protocol.js +207 -15
  54. package/dist/internal/machine/protocol.js.map +1 -1
  55. package/dist/internal/machine/runtime.d.ts +8 -2
  56. package/dist/internal/machine/runtime.d.ts.map +1 -1
  57. package/dist/internal/machine/runtime.js +86 -8
  58. package/dist/internal/machine/runtime.js.map +1 -1
  59. package/dist/internal/machine/symbols.d.ts +2 -0
  60. package/dist/internal/machine/symbols.d.ts.map +1 -1
  61. package/dist/internal/machine/symbols.js +2 -0
  62. package/dist/internal/machine/symbols.js.map +1 -1
  63. package/dist/internal/machine/topology.d.ts +7 -0
  64. package/dist/internal/machine/topology.d.ts.map +1 -1
  65. package/dist/internal/machine/topology.js +29 -0
  66. package/dist/internal/machine/topology.js.map +1 -1
  67. package/dist/internal/testing/machine/finiteModel.js +1 -1
  68. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  69. package/dist/internal/testing/machine/runtime.d.ts.map +1 -1
  70. package/dist/internal/testing/machine/runtime.js +2 -1
  71. package/dist/internal/testing/machine/runtime.js.map +1 -1
  72. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  73. package/dist/internal/testing/machine/verification.js +5 -3
  74. package/dist/internal/testing/machine/verification.js.map +1 -1
  75. package/dist/testing/MachineTest.d.ts +7 -7
  76. package/dist/testing/MachineTest.js +7 -7
  77. package/dist/unstable/cluster/ClusterMachine.d.ts +1 -1
  78. package/dist/unstable/cluster/ClusterMachine.js +1 -1
  79. package/dist/unstable/reactivity/AtomMachine.d.ts +25 -8
  80. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  81. package/dist/unstable/reactivity/AtomMachine.js +18 -3
  82. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  83. package/docs/agent-guide.md +242 -122
  84. package/package.json +2 -2
  85. package/src/Machine.ts +1453 -812
  86. package/src/internal/machine/activities.ts +48 -33
  87. package/src/internal/machine/atom.ts +38 -13
  88. package/src/internal/machine/cluster.ts +2 -1
  89. package/src/internal/machine/command.ts +2 -2
  90. package/src/internal/machine/commandRuntime.ts +9 -4
  91. package/src/internal/machine/configuration.ts +23 -5
  92. package/src/internal/machine/errors.ts +1 -1
  93. package/src/internal/machine/executionPlan.ts +102 -28
  94. package/src/internal/machine/invocation.ts +182 -50
  95. package/src/internal/machine/invocationEvent.ts +72 -0
  96. package/src/internal/machine/machine.ts +80 -387
  97. package/src/internal/machine/planner.ts +127 -32
  98. package/src/internal/machine/process.ts +18 -8
  99. package/src/internal/machine/protocol.ts +313 -25
  100. package/src/internal/machine/runtime.ts +135 -9
  101. package/src/internal/machine/symbols.ts +3 -0
  102. package/src/internal/machine/topology.ts +38 -0
  103. package/src/internal/testing/machine/finiteModel.ts +1 -1
  104. package/src/internal/testing/machine/runtime.ts +2 -1
  105. package/src/internal/testing/machine/verification.ts +5 -3
  106. package/src/testing/MachineTest.ts +7 -7
  107. package/src/unstable/cluster/ClusterMachine.ts +1 -1
  108. package/src/unstable/reactivity/AtomMachine.ts +45 -13
@@ -22,10 +22,10 @@ declared:
22
22
 
23
23
  1. Domain schemas used by state and event fields.
24
24
  2. Tagged schemas for states that own data.
25
- 3. Tagged public-event, internal-event, and emitted-event schemas.
25
+ 3. Tagged public-event, internal-event, parent-event, and emitted-event schemas.
26
26
  4. `Machine.defineStates`.
27
- 5. `Machine.make`, including input, events, internal events, emits, and the
28
- initial function.
27
+ 5. `Machine.make`, including input, `events`, `internalEvents`, `parentEvents`,
28
+ `emittedEvents`, and the initial function.
29
29
  6. One or more `.handle(...)` calls.
30
30
  7. Child descriptors.
31
31
  8. Runtime, Atom, or Cluster adapters.
@@ -49,13 +49,17 @@ const InternalEvent = Schema.TaggedUnion({
49
49
  })
50
50
 
51
51
  const States = Machine.defineStates(State.cases)
52
+ const Events = Machine.events(Event)
53
+ const InternalEvents = Machine.internalEvents(InternalEvent)
52
54
  ```
53
55
 
54
- Construct event values with `Event.cases.Save.make({})`. Construct new state
55
- values through the target or initial builder's `.from(...)` method so schema
56
- construction runs inside planning. Pass a state directly only when it is
57
- already decoded. Use `Schema.TaggedClass` when a case needs class methods or
58
- nominal class identity; `.from(...)` preserves that identity.
56
+ Pass these descriptors to `Machine.make` and export `Events` instead of the raw
57
+ event schema. Construct new state values through the target or initial
58
+ builder's `.from(...)` method. Both event constructors and state `.from(...)`
59
+ defer schema construction until planning, so validation failures remain typed
60
+ machine errors. Use
61
+ `Schema.TaggedClass` when a case needs class methods or nominal class identity;
62
+ the deferred constructors preserve that identity after decoding.
59
63
 
60
64
  ## Hard invariants
61
65
 
@@ -68,23 +72,26 @@ nominal class identity; `.from(...)` preserves that identity.
68
72
  - Return snapshots or typed target-builder results from transitions. Do not
69
73
  return raw decoded state values.
70
74
  - Transition and lifecycle callbacks are synchronous. Put asynchronous work in
71
- an invoked Effect, actor, or child machine and map its result to an event.
75
+ an invoked Effect, logic process, or child machine and handle its lifecycle
76
+ with `onDone`, `onFailure`, and `onSnapshot`.
72
77
  - Put data on the narrowest state where it is valid. Put data shared by sibling
73
78
  phases on their compound parent.
74
79
  - Declare finality only in the state definition. Do not put `type: "final"` in
75
80
  a handler.
76
81
  - Every declared output schema needs a matching handler implementation before
77
82
  planning or execution.
78
- - `parents` keys are full dotted paths.
83
+ - Handler `ancestors` keys are full dotted paths.
79
84
  - Invoke lifetimes follow state entry and exit, not the spelling of the target
80
85
  builder.
81
- - Recover expected invoked Effect failures into machine events. Unrecovered
82
- child failures terminate the owning machine.
83
- - Reuse the exact child descriptor value for `invokeMachine`, `sendTo`, and
84
- child lookup.
85
- - `events` is the public input protocol. `internalEvents` contains machine-local
86
- deliveries such as invoke results and invoked-child emissions. Handlers see
87
- both; typed public `send` and `Machine.plan` accept only `events`.
86
+ - Handle every typed invoked Effect failure with `onFailure`. Defects and
87
+ interruption terminate the owning machine.
88
+ - Reuse an exported child descriptor for inline invocation, `sendTo`, and child
89
+ lookup. Independently constructed descriptors are equivalent only when both
90
+ their id and machine identity match.
91
+ - `events` is the public actor-input protocol. `internalEvents` contains
92
+ machine-local raised events. `parentEvents` describes the public events a
93
+ child may send to its owner. `emittedEvents` describes outward ephemeral
94
+ notifications and is never delivered implicitly to a parent.
88
95
  - Event tags in `events` and `internalEvents` must be disjoint.
89
96
  - Event tags must also be unique within each protocol list.
90
97
 
@@ -96,13 +103,15 @@ its extra control is required:
96
103
  - Bind a shared Atom runtime once with `AtomMachine.bind(runtime)`, then use the
97
104
  returned `make` or `resume`. Use `AtomMachine.make(machine)` and
98
105
  `AtomMachine.resume(machine, snapshot)` for service-free machines.
99
- - Use `Machine.invokeEffect` for a typed one-shot Effect and `Machine.after` for
100
- a timer. Use `Machine.invoke` with `Machine.effect` only for custom child
101
- process behavior or snapshot mapping.
106
+ - Use one invocation object: `effect` for one-shot work, `after` for a timer,
107
+ `logic` for reusable process logic, and `child` for a complete child
108
+ statechart. `Machine.invoke({...})` preserves owner context and source
109
+ channels across sibling lifecycle handlers. Use a direct object only when its
110
+ lifecycle handlers do not need source-derived context.
102
111
  - Use `Machine.child(id, machine)` for a complete statechart descriptor and
103
- `Machine.childAddress<Event>(id)` for a low-level process address. An
104
- invocation is addressable only when `Machine.invoke` receives that address
105
- explicitly.
112
+ `Machine.childAddress<Event>(id)` for a low-level process address. A logic
113
+ invocation is addressable only when `Machine.invoke` receives that
114
+ address explicitly.
106
115
  - Use the callback's `enqueue` argument for `raise`, `emit`, `sendTo`, and
107
116
  `stop`. These operations record closed actor commands and do not run Effects.
108
117
 
@@ -148,8 +157,8 @@ States.get(snapshot, "Form") // type error: no value schema
148
157
 
149
158
  For a schema-less path, builders expose only `.from(...)`; the direct callable
150
159
  form is reserved for already-decoded schema values. Structural ancestors are
151
- also omitted from `parents`; an immediate structural parent is typed as
152
- `undefined`. Add `schema` when a state begins to own data or needs runtime
160
+ also omitted from `ancestors`; an immediate structural containing state is
161
+ typed as `undefined`. Add `schema` when a state begins to own data or needs runtime
153
162
  validation and persistence for that data.
154
163
 
155
164
  Use an atomic state when no child phase can be active beneath it.
@@ -217,7 +226,7 @@ const States = Machine.defineStates({
217
226
 
218
227
  const machine = Machine.make({
219
228
  states: States.states,
220
- events: [],
229
+ events: Machine.events(),
221
230
  initial: () => States.initial.Done.from()
222
231
  }).handle({
223
232
  Done: {
@@ -372,7 +381,7 @@ parallel builders still require a callback selecting their active child or
372
381
  every active region. Omitted input is normalized to `{}` and still passes
373
382
  through `schema.makeEffect`, including refinements.
374
383
 
375
- ## Reading state and parents
384
+ ## Reading state and structural ancestors
376
385
 
377
386
  `Machine.defineStates` returns typed helpers:
378
387
 
@@ -394,16 +403,18 @@ States.matches(ready, "Route.Ready.Saving")
394
403
 
395
404
  All paths are checked against the definition. `get` and `getWithParents` accept
396
405
  only schema-backed paths; use `matches` or `getSnapshot` for any active path.
397
- `context.parent` is the immediate typed parent value (`undefined` at a root or
398
- when that parent is schema-less). `parents` contains only valued ancestors. Use
399
- its full paths when another ancestor value is needed:
406
+ `context.containingState` is the immediate typed state value (`undefined` at a
407
+ root or when that state is schema-less). `context.ancestors` contains only
408
+ valued structural ancestors. This is separate from `context.parent`, which is
409
+ the owning actor reference or `undefined` for a root actor. Use full state paths
410
+ when another ancestor value is needed:
400
411
 
401
412
  ```ts
402
- parents["Route.Ready"]
403
- parents["Route.Ready.Editing"]
413
+ ancestors["Route.Ready"]
414
+ ancestors["Route.Ready.Editing"]
404
415
  ```
405
416
 
406
- Do not guess short properties such as `parents.Ready`.
417
+ Do not guess short properties such as `ancestors.Ready`.
407
418
 
408
419
  ### Inspecting the full transition configuration
409
420
 
@@ -415,7 +426,7 @@ microstep, before any selected transition is applied:
415
426
  BufferReady: ({ snapshot, target }) =>
416
427
  States.matches(snapshot, "Player.Network.Online")
417
428
  ? target.local.Playing.from()
418
- : undefined
429
+ : target.none()
419
430
  ```
420
431
 
421
432
  Use the existing `States.matches`, `States.get`, `States.getWithParents`, and
@@ -446,11 +457,19 @@ history definitions may declare an `annotations` object containing only
446
457
  they cannot change behavior, identity, or targeting. Visualization may show a
447
458
  title, while the structural path remains authoritative.
448
459
 
449
- Use `Machine.retag(TargetCase, source, patch?)` when sibling state payloads
450
- share fields. It removes the source discriminator, reuses only compatible
451
- fields, and requires a patch for every missing or incompatible required field.
452
- Prefer moving broadly shared data to the compound parent rather than retagging
453
- it through every phase.
460
+ When sibling state payloads share fields, destructure away the source
461
+ discriminator and construct the destination through its target builder:
462
+
463
+ ```ts
464
+ Submit: ({ state, target }) => {
465
+ const { _tag: _, ...fields } = state
466
+ return target.local.Saving.from({ ...fields, attempt: 1 })
467
+ }
468
+ ```
469
+
470
+ The target schema remains responsible for defaults, transforms, refinements,
471
+ and class identity. Prefer moving broadly shared data to the compound parent
472
+ rather than copying it through every phase.
454
473
 
455
474
  ## Planning, actions, raised events, and emissions
456
475
 
@@ -458,18 +477,89 @@ A transition returns a target synchronously:
458
477
 
459
478
  ```ts
460
479
  Submit: ({ state, target }) =>
461
- state.valid ? target.local.Saving.from({ draft: state.draft }) : undefined
480
+ state.valid ? target.local.Saving.from({ draft: state.draft }) : target.none()
462
481
  ```
463
482
 
483
+ Every installed event, `always`, `onDone`, and invoke lifecycle handler must
484
+ return a concrete target or `target.none()`. An absent event handler means the
485
+ event is ignored. Returning `target.none()` means it was handled without a
486
+ destination, so queued commands, raised events, and emitted events are still
487
+ retained. It remains valid when a transition declares `targets`: those paths
488
+ are an upper bound on concrete destinations, not an exhaustive result set.
489
+
490
+ `reenter: true` remains meaningful with `target.none()`: the source exits and
491
+ enters again while its logical configuration is retained.
492
+
464
493
  Closed statechart and actor operations use `enqueue`:
465
494
 
466
495
  ```ts
467
496
  Submit: ({ target }, enqueue) => {
468
- enqueue.emit(new SaveRequested({}))
497
+ enqueue.emit(Emissions.SaveRequested())
469
498
  return target.local.Saving.from()
470
499
  }
471
500
  ```
472
501
 
502
+ Declare emission constructors separately from actor inputs:
503
+
504
+ ```ts
505
+ const Emissions = Machine.emittedEvents(SaveRequested, AuditRecorded)
506
+
507
+ const definition = Machine.make({
508
+ events: Commands,
509
+ internalEvents: InternalEvents,
510
+ emittedEvents: Emissions,
511
+ // ...
512
+ })
513
+ ```
514
+
515
+ `enqueue.raise(...)` is a same-macrostep input to self. `enqueue.sendTo(...)`
516
+ targets an actor mailbox and is processed later. `enqueue.emit(...)` is neither:
517
+ it publishes a one-off outward notification. Observe it with
518
+ `ref.emissions`, a hot non-replayed `Stream` that completes with the actor.
519
+ `ref.changes` is stateful and begins with the current lifecycle snapshot.
520
+ Startup emissions occur before `Machine.start` returns and therefore are not
521
+ visible through the returned ref; use state for facts that must be retained.
522
+
523
+ For child-to-parent input, export a public builder protocol and reuse it at both
524
+ composition boundaries:
525
+
526
+ ```ts
527
+ export const ParentEvents = Machine.events(ChildFinished)
528
+
529
+ const child = Machine.make({
530
+ events: ChildEvents,
531
+ parentEvents: ParentEvents,
532
+ // ...
533
+ }).handle({
534
+ Working: {
535
+ on: {
536
+ Finish: ({ parent }, enqueue) => {
537
+ if (parent !== undefined) {
538
+ enqueue.sendTo(parent, ParentEvents.ChildFinished())
539
+ }
540
+ }
541
+ }
542
+ }
543
+ })
544
+
545
+ const parent = Machine.make({
546
+ events: Machine.events(ParentCommands, ParentEvents),
547
+ // ...
548
+ })
549
+ ```
550
+
551
+ Invoking the child under a parent that lacks any required `parentEvents` case
552
+ is a type error. Within child handlers, `parent` accepts only that protocol.
553
+ The same child may run as a root, where `parent` is `undefined`. `self` accepts
554
+ the machine's public inputs. Neither actor reference is a structural state
555
+ value; use `containingState` and `ancestors` for statechart ancestry.
556
+
557
+ Atom-backed actors retain the same transient semantics. Use
558
+ `AtomMachine.emissions(machineAtom)` for a root and
559
+ `AtomMachine.childEmissions(childAtom)` for the currently active child. Both
560
+ return streams requiring the corresponding `AtomRegistry`; emissions are not
561
+ stored as atom state.
562
+
473
563
  For asynchronous validation or persistence, invoke an Effect or child machine
474
564
  from the state and handle its typed success or failure event in a later
475
565
  transition. This keeps `(state, event) => [nextState, commands]` synchronous.
@@ -499,86 +589,119 @@ an event for the parent. Both operations validate their schemas.
499
589
  union handled inside the statechart:
500
590
 
501
591
  ```ts
502
- const machine = Machine.make({
592
+ const Events = Machine.events(Event)
593
+ const InternalEvents = Machine.internalEvents(InternalEvent)
594
+
595
+ const definition = Machine.make({
503
596
  states: States.states,
504
- events: [Event.cases.Save],
505
- internalEvents: [InternalEvent.cases.Saved, InternalEvent.cases.SaveFailed],
597
+ events: Events,
598
+ internalEvents: InternalEvents,
506
599
  initial: () => States.initial.Idle.from()
507
600
  })
508
601
  ```
509
602
 
510
- When the same already-constructed event may be delivered repeatedly, construct
511
- it once through its owning machine protocol:
603
+ Use the protocol-bound constructors at every machine delivery boundary:
512
604
 
513
605
  ```ts
514
- const save = Machine.event(machine, Event.cases.Save)
515
- yield* ref.send(save)
606
+ yield* ref.send(Events.Save())
607
+ enqueue.raise(InternalEvents.Saved({ id: "entry-1" }))
516
608
  ```
517
609
 
518
- `Machine.event` runs the configured schema constructor once. That machine and
519
- definitions derived from it with `handle` then recognize the decoded event as
520
- trusted and do not decode it again. Tagged-union case schemas are recognized
521
- when their union is configured. Treat the returned event as immutable. Raw
522
- objects and values constructed for another machine continue through normal
523
- runtime validation on every delivery.
610
+ `Machine.events` exposes only public constructors;
611
+ `Machine.internalEvents` exposes only machine-local constructors. Both flatten
612
+ configured tagged unions and preserve tagged classes, finite discriminator
613
+ unions, required inputs, and constructor defaults. A constructor returns an
614
+ opaque instruction whose `_tag` is available for activity metadata. Its decoded
615
+ fields are intentionally unavailable until the owning machine processes it.
616
+
617
+ Invalid constructor input fails `Machine.plan` or the running machine with
618
+ `MachineSchemaDecodeError`; creating the instruction itself never performs
619
+ schema validation. APIs that explicitly retain decoded events, such as manual
620
+ model-testing scenarios or transport messages, can receive complete event
621
+ objects directly.
622
+
623
+ An open discriminator such as `_tag: Schema.String` cannot produce named
624
+ constructors because its tag set is not finite. The schema still participates
625
+ in the protocol; pass a complete event object at the delivery boundary.
524
626
 
525
627
  Use the exported utility types when another API must preserve the boundary:
526
628
 
527
629
  ```ts
528
- type PublicEvent = Machine.Machine.InputEvent<typeof machine>
529
- type AnyHandledEvent = Machine.Machine.Event<typeof machine>
630
+ type PublicEvent = Machine.Machine.InputEvent<typeof definition>
631
+ type AnyHandledEvent = Machine.Machine.Event<typeof definition>
530
632
  ```
531
633
 
532
- `MachineRef.send`, `machineAtom.send`, and `Machine.plan` use `InputEvent` at
533
- their TypeScript boundary. Transition handlers, raised events, invoke results,
534
- and mapped child events use the complete `Event` union. The local planner and
535
- runtime intentionally share the complete decoder to support those internal
536
- deliveries, so JavaScript or `any` can bypass the local public distinction.
634
+ `MachineRef.send`, `machineAtom.send`, and `Machine.plan` accept decoded public
635
+ events or constructions returned by `Machine.events`. Transition handlers
636
+ receive only decoded events. Raised events additionally accept constructions
637
+ from `Machine.internalEvents`; outward notifications accept constructions from
638
+ `Machine.emittedEvents`. The
639
+ local planner and runtime intentionally share the complete decoder to support
640
+ those internal deliveries, so JavaScript or `any` can bypass the local public
641
+ distinction.
537
642
  Cluster RPC payloads are additionally decoded against the public `events`
538
643
  schemas at the transport boundary. Never repeat an `_tag` within a list or
539
644
  across both configuration lists.
540
645
 
541
646
  ## Recoverable state-scoped work
542
647
 
543
- Use `Machine.invokeEffect` for a one-shot Effect. Its callbacks preserve the
544
- typed success and failure channels while mapping both into machine events:
648
+ Use `Machine.invoke` with an `effect` for one-shot work. Lifecycle callbacks
649
+ receive the typed Effect channels and can transition directly:
545
650
 
546
651
  ```ts
547
- invoke: ({ state }) =>
548
- Machine.invokeEffect({
549
- id: "save",
550
- effect: SaveService.save(state.draft),
551
- onSuccess: (entry) => new Saved({ entry }),
552
- onFailure: (error) => new SaveFailed({ message: error.message })
553
- })
652
+ invoke: Machine.invoke({
653
+ id: "save",
654
+ effect: SaveService.save(draft),
655
+ onDone: ({ output, target }) => target.full.Saved({ entry: output }),
656
+ onFailure: ({ error, target }) =>
657
+ target.full.SaveFailed({ message: error.message })
658
+ })
554
659
  ```
555
660
 
556
661
  The owning state scopes the child. Owner-driven interruption on state exit is
557
- normal cancellation and stale output is ignored. A child Effect that defects
558
- or self-interrupts fails the parent. Omit `onFailure` only when the Effect error
559
- type is `never`; defects and interruption are not mapped.
662
+ normal cancellation and stale output is ignored. An Effect that defects or
663
+ self-interrupts fails the parent. `onDone` is required when the output is not
664
+ `never`; `onFailure` is required when the typed error is not `never`. Handlers
665
+ are forbidden when their channel is `never`.
666
+
667
+ The source may also be a function of the owning state's entry context when it
668
+ needs `state`, `containingState`, `ancestors`, or the entry `event`. Source construction
669
+ errors, defects, and interruption are machine failures rather than a second
670
+ phase in `onFailure`.
671
+
672
+ When a source function reads `state`, `containingState`, `ancestors`, or the entry `event`,
673
+ `Machine.invoke` infers that owner context and the returned Effect's output,
674
+ error, and service channels together. No return annotation is needed:
675
+
676
+ ```ts
677
+ invoke: Machine.invoke({
678
+ id: "load",
679
+ effect: ({ state }) => LoadService.load(state.userId),
680
+ onDone: ({ output, target }) => target.full.Loaded({ user: output }),
681
+ onFailure: ({ error, target }) => target.full.LoadFailed({ error })
682
+ })
683
+ ```
560
684
 
561
- Successful non-void output is delivered as a parent event. Include every
562
- possible mapped result schema in the parent machine's `internalEvents` array and
563
- add handlers for the relevant tags. Leave defects and interruption fatal;
564
- recover only expected typed failures.
685
+ A direct `invoke: { ... }` object remains available when lifecycle handlers do
686
+ not need source-derived context.
565
687
 
566
- A cancellable timer uses `Machine.after`:
688
+ A cancellable timer uses the same object:
567
689
 
568
690
  ```ts
569
- invoke: Machine.after("3 seconds", new ClearStatus({}), {
570
- id: "clear-status"
691
+ invoke: Machine.invoke({
692
+ id: "clear-status",
693
+ after: "3 seconds",
694
+ onDone: ({ target }) => target.full.Clear()
571
695
  })
572
696
  ```
573
697
 
574
- The timer starts on state entry and is interrupted on exit. Supply an explicit
575
- id when more than one active timer could deliver the same event tag. Use
576
- lower-level `Machine.invoke` with `Machine.effect` when custom child logic or
577
- snapshot mapping is required. In that API, `id` is only the invocation's
578
- state-local lifecycle key. To communicate with the invocation, create a
579
- `Machine.childAddress<Event>("worker")` and pass it as `address`; TypeScript
580
- checks the address protocol against the child logic. Lifecycle ids must be
581
- unique among simultaneously active invokes owned by the same state.
698
+ The timer starts on state entry and is interrupted on exit. Its `onDone` is
699
+ always required. `effect: Effect.sleep(...)` has the same scoped cancellation
700
+ behavior, but `after` records timer intent and exposes a static duration through
701
+ `Machine.activityDefinitions`. For reusable process logic, provide `logic`, a
702
+ state-local lifecycle `id`, and a typed `address`. TypeScript checks the address
703
+ protocol against the logic event protocol. Lifecycle ids and addresses serve
704
+ different purposes and must both be explicit.
582
705
 
583
706
  ## Invoked child statecharts
584
707
 
@@ -591,24 +714,24 @@ const Editor = Machine.child("editor", EditorMachine)
591
714
  Invoke it from its owning state:
592
715
 
593
716
  ```ts
594
- invoke: Machine.invokeMachine({
717
+ invoke: Machine.invoke({
595
718
  child: Editor,
596
719
  input: editorInput,
597
- onDone: ({ output }) => new EditorCompleted({ output })
720
+ onDone: ({ output, target }) => target.full.EditorDone({ output })
598
721
  })
599
722
  ```
600
723
 
601
724
  Use `Editor` for:
602
725
 
603
726
  ```ts
604
- Machine.sendTo(Editor, new Reset({}))
727
+ Machine.sendTo(Editor, EditorEvent.Reset())
605
728
  parentRef.child(Editor)
606
729
  parentAtom.child(Editor)
607
730
  ```
608
731
 
609
- Child emissions, mapped snapshots, and mapped completion output are delivered as
610
- parent events and must be accepted by the parent's `internalEvents` list.
611
- Invoked child IDs must be unique while simultaneously active.
732
+ Child emissions are delivered through the parent's internal protocol.
733
+ `onSnapshot`, `onDone`, and `onFailure` are direct parent transitions. Invoked
734
+ child IDs must be unique while simultaneously active.
612
735
 
613
736
  Descriptors with the same id and machine identity address the same child, even
614
737
  when independently constructed. The descriptor objects themselves are not
@@ -620,21 +743,18 @@ logic that does not have a complete machine descriptor.
620
743
  ### Inspecting state-owned activities
621
744
 
622
745
  Use `Machine.activityDefinitions(machine)` to inspect invokes without running
623
- them. Static `Machine.invoke`, `Machine.invokeEffect`, `Machine.after`, and
624
- `Machine.invokeMachine` descriptors expose serializable ownership metadata:
746
+ them. Static inline `Machine.invoke` definitions expose serializable ownership
747
+ metadata:
625
748
 
626
749
  ```ts
627
750
  Machine.activityDefinitions(machine)
628
751
  // [{ source: "Loading", id: "load-timeout", type: "timer",
629
- // duration: "10s", event: "LoadTimedOut" }]
752
+ // duration: "10s" }]
630
753
  ```
631
754
 
632
- Effect success/failure mappers are closures and therefore appear as dynamic
633
- outcomes. Child machines expose descriptor identity, never their runtime or
634
- implementation. A function-valued `invoke` factory is represented as a dynamic
635
- activity because inspection must not evaluate user code. The existing invoke
636
- helpers remain the only execution API; this metadata does not add lifecycle
637
- configuration syntax or affect execution.
755
+ Child machines expose descriptor identity, never their runtime or
756
+ implementation. Function-valued sources and durations are represented as
757
+ dynamic because inspection must not evaluate user code.
638
758
 
639
759
  ## AtomMachine and React
640
760
 
@@ -743,8 +863,8 @@ Encoding does not preserve:
743
863
  - completion and history records survive but do not retrigger `onDone`;
744
864
  - active-state invokes start once in ordinary ancestor/document order with
745
865
  `Machine.InitialEvent`;
746
- - `invokeEffect` restarts, `invokeMachine` creates a fresh child from its normal
747
- initial state, and `Machine.after` restarts its complete duration;
866
+ - inline Effects restart, child machines start fresh from their normal initial
867
+ state, and timers restart their complete duration;
748
868
  - inactive invokes, spawned children, child snapshots, elapsed timer time, and
749
869
  prior `RuntimeSnapshot` status/errors are not restored;
750
870
  - a final logical snapshot creates an immediately completed ref;
@@ -918,26 +1038,26 @@ Wrap the initial builder result:
918
1038
  initial: () => States.initial.Idle.from()
919
1039
  ```
920
1040
 
921
- ### Invoked child output must be a machine event
1041
+ ### Invoked child expects events not accepted by the parent
922
1042
 
923
- Add the output's tagged schema to the parent machine's `internalEvents` array,
924
- or map/ignore the output before it reaches the parent.
1043
+ Export one parent-event protocol from the child boundary and compose it into
1044
+ the parent's public events:
925
1045
 
926
- ### Invoked child emits events not accepted by the parent
1046
+ ```ts
1047
+ export const ChildParentEvents = Machine.events(ChildFinished)
927
1048
 
928
- Add the child's emitted schemas to the parent machine's `internalEvents` array:
1049
+ // child
1050
+ parentEvents: ChildParentEvents
929
1051
 
930
- ```ts
931
- events: [Submit],
932
- internalEvents: [...ChildMachine.emits]
1052
+ // parent
1053
+ events: Machine.events(Submit, ChildParentEvents)
933
1054
  ```
934
1055
 
935
1056
  ### An internal event is rejected by `send`
936
1057
 
937
1058
  This is intentional. Public input boundaries accept only schemas declared in
938
- `events`. Handle the event as an invoke result, child delivery, or raised event;
939
- move it to `events` only if external callers should genuinely be allowed to
940
- send it.
1059
+ `events`. Handle the event as a child delivery or raised event; move it to
1060
+ `events` only if external callers should genuinely be allowed to send it.
941
1061
 
942
1062
  ### Public and internal event tags overlap
943
1063
 
@@ -965,15 +1085,15 @@ handlers own behavior.
965
1085
 
966
1086
  ### Parent property does not exist
967
1087
 
968
- Use its full path:
1088
+ Use the structural ancestor's full path:
969
1089
 
970
1090
  ```ts
971
- parents["Route.Ready"]
1091
+ ancestors["Route.Ready"]
972
1092
  ```
973
1093
 
974
1094
  ### Child descriptor types are unrelated
975
1095
 
976
- Use the descriptor exported by the module that configured `invokeMachine`.
1096
+ Use the descriptor exported by the module that configured the child invocation.
977
1097
  An independently created descriptor with the same id and machine identity also
978
1098
  matches; the same id paired with a different machine remains a distinct child.
979
1099
 
@@ -997,6 +1117,6 @@ The current API does not include:
997
1117
  - declarative first-class guards;
998
1118
  - a complete inspectable graph for arbitrary transition Effects.
999
1119
 
1000
- Use ordinary TypeScript conditions for guards and `Machine.after` for
1001
- state-scoped timers. Do not invent undocumented state-node properties such as
1002
- `guard`.
1120
+ Use ordinary TypeScript conditions for guards and an inline `Machine.invoke`
1121
+ with `after` for state-scoped timers. Do not invent undocumented state-node
1122
+ properties such as `guard`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/effect-machine",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Schema-first state machines and statecharts for Effect",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {
@@ -69,7 +69,7 @@
69
69
  "test": "vitest run",
70
70
  "test:types": "tstyche",
71
71
  "check:architecture": "node --test scripts/check-architecture.test.mjs && node scripts/check-architecture.mjs",
72
- "check:ci": "node --test scripts/ci-changes.test.mjs scripts/runtime-performance-regression.test.mjs",
72
+ "check:ci": "node --test scripts/ci-changes.test.mjs scripts/runtime-performance-compatibility.test.mjs scripts/runtime-performance-regression.test.mjs",
73
73
  "typecheck": "tsc -p tsconfig.json --noEmit",
74
74
  "perf:types": "pnpm build && node scripts/type-performance.mjs",
75
75
  "perf:runtime": "pnpm build && node --expose-gc scripts/runtime-performance.mjs",