@typeonce/effect-machine 0.20.0 → 0.21.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 (34) hide show
  1. package/README.md +41 -0
  2. package/dist/Machine.d.ts +62 -16
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +4 -3
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  7. package/dist/internal/machine/executionPlan.js +33 -3
  8. package/dist/internal/machine/executionPlan.js.map +1 -1
  9. package/dist/internal/machine/machine.d.ts.map +1 -1
  10. package/dist/internal/machine/machine.js +38 -9
  11. package/dist/internal/machine/machine.js.map +1 -1
  12. package/dist/internal/machine/planner.d.ts +11 -2
  13. package/dist/internal/machine/planner.d.ts.map +1 -1
  14. package/dist/internal/machine/planner.js +49 -9
  15. package/dist/internal/machine/planner.js.map +1 -1
  16. package/dist/internal/machine/topology.d.ts +9 -1
  17. package/dist/internal/machine/topology.d.ts.map +1 -1
  18. package/dist/internal/machine/topology.js +7 -0
  19. package/dist/internal/machine/topology.js.map +1 -1
  20. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  21. package/dist/internal/testing/machine/verification.js +15 -3
  22. package/dist/internal/testing/machine/verification.js.map +1 -1
  23. package/dist/testing/MachineTest.d.ts +2 -0
  24. package/dist/testing/MachineTest.d.ts.map +1 -1
  25. package/dist/testing/MachineTest.js.map +1 -1
  26. package/docs/agent-guide.md +17 -0
  27. package/package.json +1 -1
  28. package/src/Machine.ts +230 -43
  29. package/src/internal/machine/executionPlan.ts +37 -3
  30. package/src/internal/machine/machine.ts +57 -9
  31. package/src/internal/machine/planner.ts +64 -11
  32. package/src/internal/machine/topology.ts +18 -1
  33. package/src/internal/testing/machine/verification.ts +16 -3
  34. package/src/testing/MachineTest.ts +2 -0
package/src/Machine.ts CHANGED
@@ -3577,9 +3577,10 @@ export declare namespace Machine {
3577
3577
  *
3578
3578
  * **Details**
3579
3579
  *
3580
- * Every branch exposes its selected target without executing its resolver.
3581
- * A compound local or branch target covers its descendants;
3582
- * `undefined` identifies an explicitly targetless branch.
3580
+ * Every branch exposes its static selection without executing its resolver.
3581
+ * A compound local or branch target covers its descendants. An `update`
3582
+ * selection keeps `target` undefined and records its value owner in
3583
+ * `selection.path`; `none` identifies an explicitly targetless branch.
3583
3584
  *
3584
3585
  * @category models
3585
3586
  * @since 0.4.0
@@ -4461,6 +4462,33 @@ export declare namespace Machine {
4461
4462
  >
4462
4463
  }
4463
4464
 
4465
+ /**
4466
+ * Opaque instruction that replaces one active compound or parallel state's
4467
+ * value without changing its active descendants.
4468
+ *
4469
+ * @category models
4470
+ * @since 0.21.0
4471
+ */
4472
+ export interface StateUpdate<
4473
+ States extends StateSchemas,
4474
+ StateId extends ValuedStateIdentifier<States>
4475
+ > {
4476
+ readonly [Topology.StateUpdateTypeId]: typeof Topology.StateUpdateTypeId
4477
+ readonly path: StateId
4478
+ readonly value: StateByIdentifier<States, StateId>
4479
+ }
4480
+
4481
+ /** @internal */
4482
+ type StateUpdateBuilder<
4483
+ States extends StateSchemas,
4484
+ StateId extends ValuedStateIdentifier<States>
4485
+ > =
4486
+ & ((value: StateByIdentifier<States, StateId>) => StateUpdate<States, StateId>)
4487
+ & FromMethod<
4488
+ readonly [input: SchemaByIdentifier<States, StateId>["~type.make.in"]],
4489
+ StateUpdate<States, StateId>
4490
+ >
4491
+
4464
4492
  /**
4465
4493
  * Opaque result returned by an explicitly targetless transition.
4466
4494
  *
@@ -4745,6 +4773,45 @@ export declare namespace Machine {
4745
4773
  & SelectionTreeWithPrefix<AllStates, Children, Path, Scope, Builder>
4746
4774
  : SelectionMethod<Builder, Path>
4747
4775
 
4776
+ /** @internal */
4777
+ type StateUpdateSelectionForNode<
4778
+ AllStates extends StateSchemas,
4779
+ Node,
4780
+ Path extends string
4781
+ > = Node extends { readonly states: StateSchemas } ? NodeSchema<Node> extends never ? {}
4782
+ : {
4783
+ readonly update: SelectionValue<
4784
+ StateUpdateBuilder<AllStates, Extract<Path, ValuedStateIdentifier<AllStates>>>,
4785
+ Path,
4786
+ "update"
4787
+ >
4788
+ }
4789
+ : {}
4790
+
4791
+ /** @internal */
4792
+ type BranchUpdateSelectionPath<
4793
+ AllStates extends StateSchemas,
4794
+ Node,
4795
+ Path extends string,
4796
+ Rest extends string
4797
+ > =
4798
+ & StateUpdateSelectionForNode<AllStates, Node, Path>
4799
+ & (Node extends { readonly states: infer Children extends StateSchemas } ?
4800
+ Rest extends `${infer Head}.${infer Tail}` ? Head extends keyof Children ? {
4801
+ readonly [Key in Head]: BranchUpdateSelectionPath<
4802
+ AllStates,
4803
+ Children[Head],
4804
+ JoinPath<Path, Head>,
4805
+ Tail
4806
+ >
4807
+ }
4808
+ : {}
4809
+ : Rest extends keyof Children ? {
4810
+ readonly [Key in Rest]: StateUpdateSelectionForNode<AllStates, Children[Rest], JoinPath<Path, Rest>>
4811
+ }
4812
+ : {}
4813
+ : {})
4814
+
4748
4815
  type FullSelectionNode<
4749
4816
  AllStates extends StateSchemas,
4750
4817
  Node,
@@ -4769,13 +4836,17 @@ export declare namespace Machine {
4769
4836
  Source extends StateNodeIdentifier<States>,
4770
4837
  Root extends string = Source extends `${infer Head}.${string}` ? Head : Source
4771
4838
  > = Root extends ActiveStateKey<States> ? Root extends keyof BranchTargetBuilder<States, Source> ? {
4772
- readonly [Key in Root]: SelectionNode<
4773
- States,
4774
- States[Key],
4775
- Key,
4776
- "branch",
4777
- BranchTargetBuilder<States, Source>[Key]
4778
- >
4839
+ readonly [Key in Root]:
4840
+ & SelectionNode<
4841
+ States,
4842
+ States[Key],
4843
+ Key,
4844
+ "branch",
4845
+ BranchTargetBuilder<States, Source>[Key]
4846
+ >
4847
+ & (Source extends ChoiceIdentifier<States> ? {}
4848
+ : Source extends `${Key}.${infer Rest}` ? BranchUpdateSelectionPath<States, States[Key], Key, Rest>
4849
+ : StateUpdateSelectionForNode<States, States[Key], Key>)
4779
4850
  }
4780
4851
  : {}
4781
4852
  : {}
@@ -4783,14 +4854,21 @@ export declare namespace Machine {
4783
4854
  type LocalTargetSelector<
4784
4855
  States extends StateSchemas,
4785
4856
  Source extends StateNodeIdentifier<States>
4786
- > = NearestCompoundScope<States, Source> extends infer Scope extends StateIdentifier<States> ?
4787
- ChildrenOf<States, Scope> extends infer Children extends StateSchemas ?
4788
- LocalTargetBuilder<States, Source> extends infer Builder ?
4789
- & SelectionTreeWithPrefix<States, Children, Scope, "local", Builder>
4790
- & ("with" extends keyof Builder ? {
4791
- readonly with: SelectionValue<Builder["with"], Scope>
4792
- }
4793
- : {})
4857
+ > = NearestCompoundScope<States, Source> extends infer Scope ? [Scope] extends [never] ? {}
4858
+ : Scope extends StateIdentifier<States> ?
4859
+ ChildrenOf<States, Scope> extends infer Children extends StateSchemas ?
4860
+ LocalTargetBuilder<States, Source> extends infer Builder ?
4861
+ & SelectionTreeWithPrefix<States, Children, Scope, "local", Builder>
4862
+ & ("with" extends keyof Builder ? {
4863
+ readonly with: SelectionValue<Builder["with"], Scope>
4864
+ }
4865
+ : {})
4866
+ & (Source extends ChoiceIdentifier<States> ? {}
4867
+ : Scope extends ValuedStateIdentifier<States> ? {
4868
+ readonly update: SelectionValue<StateUpdateBuilder<States, Scope>, Scope, "update">
4869
+ }
4870
+ : {})
4871
+ : {}
4794
4872
  : {}
4795
4873
  : {}
4796
4874
  : {}
@@ -4831,9 +4909,9 @@ export declare namespace Machine {
4831
4909
  > {
4832
4910
  /** Handles the trigger without selecting a destination. */
4833
4911
  readonly none: SelectionValue<TargetBuilder<States, Source>["none"], never, "none">
4834
- /** Selects a destination inside the nearest active compound scope. */
4912
+ /** Selects a destination or updates the nearest active compound scope. */
4835
4913
  readonly local: LocalTargetSelector<States, Source>
4836
- /** Selects a destination elsewhere under the currently active root. */
4914
+ /** Selects a destination or updates a valued active ancestor under the current root. */
4837
4915
  readonly branch: BranchTargetSelector<States, Source>
4838
4916
  /** Selects a complete destination under any top-level state. */
4839
4917
  readonly full: FullTargetSelector<States>
@@ -5243,9 +5321,9 @@ export declare namespace Machine {
5243
5321
  * **Details**
5244
5322
  *
5245
5323
  * Handlers return snapshots for complete state replacement, target builder
5246
- * results for path-safe partial transitions, or `target.none()` for an
5247
- * explicitly targetless transition. Raw decoded state values and `void` are
5248
- * not accepted at transition boundaries.
5324
+ * results for path-safe partial transitions, state-value updates, or
5325
+ * `target.none()` for an explicitly targetless transition. Raw decoded state
5326
+ * values and `void` are not accepted at transition boundaries.
5249
5327
  *
5250
5328
  * @category utility types
5251
5329
  * @since 0.4.0
@@ -5255,11 +5333,13 @@ export declare namespace Machine {
5255
5333
  | Target<States, StateIdentifier<States>>
5256
5334
  | HistoryTarget<States, HistoryIdentifier<States>>
5257
5335
  | ChoiceTarget<States, ChoiceIdentifier<States>>
5336
+ | StateUpdate<States, ValuedStateIdentifier<States>>
5258
5337
  | StateConstruction<
5259
5338
  | Snapshot<States>
5260
5339
  | Target<States, StateIdentifier<States>>
5261
5340
  | HistoryTarget<States, HistoryIdentifier<States>>
5262
5341
  | ChoiceTarget<States, ChoiceIdentifier<States>>
5342
+ | StateUpdate<States, ValuedStateIdentifier<States>>
5263
5343
  >
5264
5344
  | NoTarget
5265
5345
 
@@ -5639,6 +5719,28 @@ export declare namespace Machine {
5639
5719
  | (SelectionKind<Selection> extends "none" ? undefined : SelectedTargetResult<Selection> | undefined)
5640
5720
  | Declined
5641
5721
 
5722
+ /** @internal */
5723
+ type StateUpdateResolver<
5724
+ Events extends ReadonlyArray<TaggedSchema>,
5725
+ Emits extends ReadonlyArray<TaggedSchema>,
5726
+ Context,
5727
+ Selection
5728
+ > = (
5729
+ context: TransitionResolveContext<Context, Selection>,
5730
+ enqueue: Enqueue<EventOf<Events>, EmitOf<Emits>>
5731
+ ) => SelectedTargetResult<Selection>
5732
+
5733
+ /** @internal */
5734
+ type DeclinableStateUpdateResolver<
5735
+ Events extends ReadonlyArray<TaggedSchema>,
5736
+ Emits extends ReadonlyArray<TaggedSchema>,
5737
+ Context,
5738
+ Selection
5739
+ > = (
5740
+ context: TransitionResolveContext<Context, Selection> & DeclineCapability,
5741
+ enqueue: Enqueue<EventOf<Events>, EmitOf<Emits>>
5742
+ ) => SelectedTargetResult<Selection> | Declined
5743
+
5642
5744
  /** One named destination declared by a branching transition. */
5643
5745
  export interface TransitionBranchInput<
5644
5746
  Selection extends TargetSelection<any, any, any> = TargetSelection<any, any, any>
@@ -5880,6 +5982,80 @@ export declare namespace Machine {
5880
5982
  : {}
5881
5983
  : {})
5882
5984
 
5985
+ /** @internal */
5986
+ interface StateUpdateTransitionRequired<
5987
+ States extends StateSchemas,
5988
+ Events extends ReadonlyArray<TaggedSchema>,
5989
+ Emits extends ReadonlyArray<TaggedSchema>,
5990
+ StateId extends StateNodeIdentifier<States>,
5991
+ Context,
5992
+ Reenter extends boolean,
5993
+ Selection extends TargetSelection<any, any, "update">
5994
+ > {
5995
+ (
5996
+ resolve: StateUpdateResolver<Events, Emits, Context, Selection>,
5997
+ options?: TransitionRequiredOptions<Reenter>
5998
+ ): BuiltTransition<
5999
+ States,
6000
+ Events,
6001
+ Emits,
6002
+ StateId,
6003
+ Context,
6004
+ Reenter,
6005
+ SelectedTargetResult<Selection>,
6006
+ "required"
6007
+ >
6008
+ }
6009
+
6010
+ /** @internal */
6011
+ interface StateUpdateTransitionDeclinable<
6012
+ States extends StateSchemas,
6013
+ Events extends ReadonlyArray<TaggedSchema>,
6014
+ Emits extends ReadonlyArray<TaggedSchema>,
6015
+ StateId extends StateNodeIdentifier<States>,
6016
+ Context,
6017
+ Reenter extends boolean,
6018
+ Selection extends TargetSelection<any, any, "update">
6019
+ > {
6020
+ (
6021
+ resolve: DeclinableStateUpdateResolver<Events, Emits, Context, Selection>,
6022
+ options: TransitionDeclinableOptions<Reenter>
6023
+ ): BuiltTransition<
6024
+ States,
6025
+ Events,
6026
+ Emits,
6027
+ StateId,
6028
+ Context,
6029
+ Reenter,
6030
+ SelectedTargetResult<Selection> | Declined,
6031
+ "declinable"
6032
+ >
6033
+ }
6034
+
6035
+ /** @internal */
6036
+ type StateUpdateTransition<
6037
+ States extends StateSchemas,
6038
+ Events extends ReadonlyArray<TaggedSchema>,
6039
+ Emits extends ReadonlyArray<TaggedSchema>,
6040
+ StateId extends StateNodeIdentifier<States>,
6041
+ Context,
6042
+ Reenter extends boolean,
6043
+ Acceptance extends TransitionAcceptance,
6044
+ Selection extends TargetSelection<any, any, "update">
6045
+ > =
6046
+ & Selection
6047
+ & StateUpdateTransitionRequired<States, Events, Emits, StateId, Context, Reenter, Selection>
6048
+ & ("declinable" extends Acceptance ? StateUpdateTransitionDeclinable<
6049
+ States,
6050
+ Events,
6051
+ Emits,
6052
+ StateId,
6053
+ Context,
6054
+ Reenter,
6055
+ Selection
6056
+ >
6057
+ : {})
6058
+
5883
6059
  /** @internal Type evidence retained by a machine initial-entry declaration. */
5884
6060
  export interface InitialBuilderEvidence<out Selection> {
5885
6061
  readonly [InitialBuilderTypeId]: Types.Covariant<Selection>
@@ -5933,19 +6109,18 @@ export declare namespace Machine {
5933
6109
  Reenter extends boolean,
5934
6110
  Acceptance extends TransitionAcceptance,
5935
6111
  Node
5936
- > = Node extends (...args: infer Args) => infer Selection ? Selection extends TargetSelection<any, any, any> ?
5937
- & ((...args: Args) => TransitionTarget<
5938
- States,
5939
- Events,
5940
- Emits,
5941
- StateId,
5942
- Context,
5943
- Reenter,
5944
- Acceptance,
5945
- Selection
5946
- >)
5947
- & {
5948
- readonly [Key in keyof Node]: TransitionSelectorNode<
6112
+ > = Node extends TargetSelection<any, any, "update"> ? StateUpdateTransition<
6113
+ States,
6114
+ Events,
6115
+ Emits,
6116
+ StateId,
6117
+ Context,
6118
+ Reenter,
6119
+ Acceptance,
6120
+ Node
6121
+ >
6122
+ : Node extends (...args: infer Args) => infer Selection ? Selection extends TargetSelection<any, any, any> ?
6123
+ & ((...args: Args) => TransitionTarget<
5949
6124
  States,
5950
6125
  Events,
5951
6126
  Emits,
@@ -5953,10 +6128,21 @@ export declare namespace Machine {
5953
6128
  Context,
5954
6129
  Reenter,
5955
6130
  Acceptance,
5956
- Node[Key]
5957
- >
5958
- }
5959
- : never
6131
+ Selection
6132
+ >)
6133
+ & {
6134
+ readonly [Key in keyof Node]: TransitionSelectorNode<
6135
+ States,
6136
+ Events,
6137
+ Emits,
6138
+ StateId,
6139
+ Context,
6140
+ Reenter,
6141
+ Acceptance,
6142
+ Node[Key]
6143
+ >
6144
+ }
6145
+ : never
5960
6146
  : Node extends TargetSelection<any, any, any> ? TransitionTarget<
5961
6147
  States,
5962
6148
  Events,
@@ -8760,9 +8946,10 @@ export const initialDefinition: <M extends Machine.Any>(machine: M) => Machine.I
8760
8946
  *
8761
8947
  * Event handlers retain their handler-key order within each source state and
8762
8948
  * are followed by eventless and completion handlers. This function does not
8763
- * execute resolvers. Every direct, named, and targetless branch exposes the
8764
- * destination selected by its required static `target` declaration, while
8765
- * `acceptance` reports whether the resolver may decline the transition.
8949
+ * execute resolvers. Every branch exposes its static selection. State updates
8950
+ * retain the updated owner in `selection.path` while leaving `target`
8951
+ * undefined because they do not change topology. `acceptance` reports whether
8952
+ * the resolver may decline the transition.
8766
8953
  *
8767
8954
  * @category getters
8768
8955
  * @since 0.4.0
@@ -47,7 +47,7 @@ import {
47
47
  validateDeclaredTransitionTarget
48
48
  } from "./planner.js"
49
49
  import { decodeEmitSync, decodeEventSync, decodeInputSync, decodeStateValueSync } from "./protocol.js"
50
- import { isInitialTarget, isNoTarget, isSnapshot, isTarget, TargetSnapshotTypeId } from "./topology.js"
50
+ import { isInitialTarget, isNoTarget, isSnapshot, isStateUpdate, isTarget, TargetSnapshotTypeId } from "./topology.js"
51
51
 
52
52
  interface IndexedExecutionDescriptor {
53
53
  readonly flat: boolean
@@ -548,7 +548,25 @@ const collectIndexedEvaluatedTransition = (
548
548
  selection.context,
549
549
  selection.transition.evaluate
550
550
  )
551
- const unresolvedTarget = transitionResult.state
551
+ const update = isStateUpdate(transitionResult.state)
552
+ ? (() => {
553
+ const index = descriptor.indexByPath.get(transitionResult.state.path)
554
+ const node = index === undefined ? undefined : descriptor.nodes[index]
555
+ if (
556
+ index === undefined || node === undefined || state.active[index] !== 1 || node.schema === undefined ||
557
+ (node.type !== "compound" && node.type !== "parallel")
558
+ ) {
559
+ throw new Error(
560
+ `Machine state update owner "${transitionResult.state.path}" must be an active valued compound or parallel state`
561
+ )
562
+ }
563
+ return {
564
+ path: node.path,
565
+ value: decodeStateValueSync(machine, node, transitionResult.state.value)
566
+ }
567
+ })()
568
+ : undefined
569
+ const unresolvedTarget = update === undefined ? transitionResult.state : undefined
552
570
  validateDeclaredTransitionTarget(
553
571
  selection.sourcePath,
554
572
  selection.trigger,
@@ -568,9 +586,14 @@ const collectIndexedEvaluatedTransition = (
568
586
  throw new Error("Machine expected indexed transition target to be a snapshot or target builder result")
569
587
  }
570
588
  const next = target === undefined
571
- ? state
589
+ ? update === undefined ? state : (() => {
590
+ const next = copyOwnedIndexedState(state)
591
+ next.values[descriptor.indexByPath.get(update.path)!] = update.value
592
+ return next
593
+ })()
572
594
  : normalizeIndexedTargetStateSync(machine, descriptor, state, target as any, selection.leafIndex)
573
595
  const changed = selection.transition.reenter || !hasSameIndexedActive(state, next)
596
+ const stabilize = changed || update !== undefined
574
597
  if (!changed) {
575
598
  return {
576
599
  selection,
@@ -578,11 +601,13 @@ const collectIndexedEvaluatedTransition = (
578
601
  branchKey: transitionResult.branchKey,
579
602
  unresolvedTarget: unresolvedTarget as any,
580
603
  target: target as any,
604
+ update,
581
605
  next,
582
606
  commands: [...transitionResult.commands, ...(initialResolution?.commands ?? [])],
583
607
  raisedEvents: [...transitionResult.raisedEvents, ...(initialResolution?.raisedEvents ?? [])],
584
608
  emittedEvents: [...transitionResult.emittedEvents, ...(initialResolution?.emittedEvents ?? [])],
585
609
  changed: false,
610
+ stabilize,
586
611
  exitPaths: [],
587
612
  entryPaths: [],
588
613
  choiceTransitions: initialResolution?.transitions ?? []
@@ -603,11 +628,13 @@ const collectIndexedEvaluatedTransition = (
603
628
  branchKey: transitionResult.branchKey,
604
629
  unresolvedTarget: unresolvedTarget as any,
605
630
  target: target as any,
631
+ update,
606
632
  next,
607
633
  commands: [...transitionResult.commands, ...(initialResolution?.commands ?? [])],
608
634
  raisedEvents: [...transitionResult.raisedEvents, ...(initialResolution?.raisedEvents ?? [])],
609
635
  emittedEvents: [...transitionResult.emittedEvents, ...(initialResolution?.emittedEvents ?? [])],
610
636
  changed: true,
637
+ stabilize,
611
638
  exitPaths: getExitPaths(machine, activeConfigurationFromIndexedState(descriptor, state), boundary),
612
639
  entryPaths: getEntryPaths(machine, activeConfigurationFromIndexedState(descriptor, next), boundary),
613
640
  choiceTransitions: initialResolution?.transitions ?? []
@@ -663,6 +690,13 @@ const indexedMicrostep = (
663
690
  if (transitions.length === 1) {
664
691
  next = transitions[0]!.next
665
692
  } else {
693
+ for (const transition of transitions) {
694
+ if (transition.update !== undefined) {
695
+ const values = next.values.slice()
696
+ values[descriptor.indexByPath.get(transition.update.path)!] = transition.update.value
697
+ next = { ...next, values }
698
+ }
699
+ }
666
700
  const applicationOrder = [
667
701
  ...transitions.filter((transition) => !transition.changed),
668
702
  ...transitions.filter((transition) => transition.changed)
@@ -227,6 +227,15 @@ const decorateTransitionSelection = (selection: Topology.TargetSelection): Topol
227
227
  reenter: () => makeDirectTransitionDescriptor(selection, undefined, { reenter: true })
228
228
  })
229
229
 
230
+ const decorateStateUpdateSelection = (selection: Topology.TargetSelection): Topology.TargetSelection => {
231
+ const update = (
232
+ resolve: (context: any, enqueue: unknown) => unknown,
233
+ options?: unknown
234
+ ) => makeDirectTransitionDescriptor(selection, resolve, options)
235
+ Object.assign(update, selection)
236
+ return Object.freeze(update) as unknown as Topology.TargetSelection
237
+ }
238
+
230
239
  const noneTransitionSelection = decorateTransitionSelection(Topology.noneTargetSelection)
231
240
 
232
241
  const makeInitialBuilderDescriptor = (
@@ -268,6 +277,7 @@ const decorateInitialSelectorNode = (node: unknown): unknown => {
268
277
 
269
278
  const decorateTransitionSelectorNode = (node: unknown): unknown => {
270
279
  if (Topology.isTargetSelection(node)) {
280
+ if (node.kind === "update") return decorateStateUpdateSelection(node)
271
281
  return node === Topology.noneTargetSelection ? noneTransitionSelection : decorateTransitionSelection(node)
272
282
  }
273
283
  if (typeof node === "function") {
@@ -363,22 +373,29 @@ const makeSelectionValue = (
363
373
  scope: Topology.TargetSelectionScope
364
374
  ): Topology.TargetSelection => Topology.makeTargetSelection(kind, path, scope)
365
375
 
376
+ const makeStateUpdateSelection = (
377
+ path: string,
378
+ scope: "local" | "branch"
379
+ ): Topology.TargetSelection => Topology.makeTargetSelection("update", path, scope)
380
+
366
381
  const addSelectionChildren = (
367
382
  builder: Record<string, unknown>,
368
383
  stateNodes: Machine.StateNodes,
369
384
  parent: string,
370
- scope: "local" | "branch"
385
+ scope: "local" | "branch",
386
+ source?: string
371
387
  ): void => {
372
388
  for (const node of stateNodes.byPath.values()) {
373
389
  if (node.parent !== parent || node.type === "history") continue
374
- builder[node.key] = makeSelectionNode(stateNodes, node.path, scope)
390
+ builder[node.key] = makeSelectionNode(stateNodes, node.path, scope, source)
375
391
  }
376
392
  }
377
393
 
378
394
  const makeSelectionNode = (
379
395
  stateNodes: Machine.StateNodes,
380
396
  path: string,
381
- scope: Topology.TargetSelectionScope
397
+ scope: Topology.TargetSelectionScope,
398
+ source?: string
382
399
  ): unknown => {
383
400
  const node = getTargetBuilderNode(stateNodes, path)
384
401
  const kind: Topology.TargetSelectionKind = node.type === "choice" ? "choice" : "state"
@@ -389,7 +406,17 @@ const makeSelectionNode = (
389
406
  enumerable: true
390
407
  })
391
408
  if (scope === "local" || scope === "branch") {
392
- addSelectionChildren(method, stateNodes, path, scope)
409
+ addSelectionChildren(method, stateNodes, path, scope, source)
410
+ }
411
+ if (
412
+ scope === "branch" && source !== undefined && node.schema !== undefined &&
413
+ (source === path || source.startsWith(`${path}.`)) &&
414
+ getTargetBuilderNode(stateNodes, source).type !== "choice"
415
+ ) {
416
+ Object.defineProperty(method, "update", {
417
+ value: makeStateUpdateSelection(path, "branch"),
418
+ enumerable: true
419
+ })
393
420
  }
394
421
  }
395
422
  return method
@@ -424,13 +451,16 @@ const makeTargetSelector = (
424
451
  }
425
452
  const branch: Record<string, unknown> = {}
426
453
  const root = getTargetBuilderNode(stateNodes, source.split(".")[0]!)
427
- branch[root.key] = makeSelectionNode(stateNodes, root.path, "branch")
454
+ branch[root.key] = makeSelectionNode(stateNodes, root.path, "branch", source)
428
455
  const local: Record<string, unknown> = {}
429
456
  const localScope = getLocalTargetScope(stateNodes, source)
430
457
  if (localScope !== undefined) {
431
458
  const localScopeNode = getTargetBuilderNode(stateNodes, localScope)
432
459
  if (localScopeNode.schema !== undefined) {
433
460
  local.with = makeSelectionValue("state", localScope, "local")
461
+ if (getTargetBuilderNode(stateNodes, source).type !== "choice") {
462
+ local.update = makeStateUpdateSelection(localScope, "local")
463
+ }
434
464
  }
435
465
  addSelectionChildren(local, stateNodes, localScope, "local")
436
466
  }
@@ -469,6 +499,13 @@ const getSelectionBuilder = (
469
499
  source: string
470
500
  ): unknown => {
471
501
  if (selection.kind === "none") return target.none
502
+ if (selection.kind === "update") {
503
+ return withFrom(
504
+ (value: unknown) => Topology.makeStateUpdate(selection.path!, value),
505
+ "leaf",
506
+ true
507
+ )
508
+ }
472
509
  let builder: any
473
510
  let parts = selection.path!.split(".")
474
511
  if (selection.kind === "history") {
@@ -514,6 +551,12 @@ const validateResolvedSelection = (
514
551
  }
515
552
  return
516
553
  }
554
+ if (selection.kind === "update") {
555
+ if (!Topology.isStateUpdate(result) || result.path !== selection.path) {
556
+ throw new Error(`Machine state update for "${selection.path}" must return its selected update builder`)
557
+ }
558
+ return
559
+ }
517
560
  if (result === undefined) return
518
561
  const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path") &&
519
562
  typeof result.path === "string"
@@ -556,6 +599,9 @@ const runCapturedBranch = (
556
599
  return resolved === undefined ? constructSelectedTarget(selectedTarget) : resolved
557
600
  }
558
601
 
602
+ const topologyTargetPath = (selection: Topology.TargetSelection): string | undefined =>
603
+ selection.kind === "update" ? undefined : selection.path
604
+
559
605
  const isArrayIndexKey = (key: string): boolean => {
560
606
  const index = Number(key)
561
607
  return Number.isInteger(index) && index >= 0 && index < 0xffff_ffff && String(index) === key
@@ -738,7 +784,9 @@ const captureTransition = (
738
784
  declinable,
739
785
  targets: [
740
786
  ...new Set(
741
- branches.flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
787
+ branches.flatMap((branch) =>
788
+ topologyTargetPath(branch.selection) === undefined ? [] : [branch.selection.path!]
789
+ )
742
790
  )
743
791
  ],
744
792
  branches: branches.map((branch) =>
@@ -746,7 +794,7 @@ const captureTransition = (
746
794
  type: "branch" as const,
747
795
  key: branch.key,
748
796
  title: branch.title,
749
- target: branch.selection.path,
797
+ target: topologyTargetPath(branch.selection),
750
798
  selection: transitionTargetSelection(branch.selection)
751
799
  })
752
800
  ),
@@ -763,10 +811,10 @@ const captureTransition = (
763
811
  return {
764
812
  reenter,
765
813
  declinable,
766
- targets: branch.selection.path === undefined ? [] : [branch.selection.path],
814
+ targets: topologyTargetPath(branch.selection) === undefined ? [] : [branch.selection.path!],
767
815
  branches: [{
768
816
  type: "direct" as const,
769
- target: branch.selection.path,
817
+ target: topologyTargetPath(branch.selection),
770
818
  selection: transitionTargetSelection(branch.selection)
771
819
  }],
772
820
  evaluate,