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