@typeonce/effect-machine 0.19.1 → 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 (54) hide show
  1. package/README.md +90 -14
  2. package/dist/Machine.d.ts +155 -18
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +15 -3
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts.map +1 -1
  7. package/dist/internal/machine/atom.js +18 -4
  8. package/dist/internal/machine/atom.js.map +1 -1
  9. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  10. package/dist/internal/machine/executionPlan.js +33 -3
  11. package/dist/internal/machine/executionPlan.js.map +1 -1
  12. package/dist/internal/machine/invocation.d.ts.map +1 -1
  13. package/dist/internal/machine/invocation.js +7 -0
  14. package/dist/internal/machine/invocation.js.map +1 -1
  15. package/dist/internal/machine/machine.d.ts +1 -0
  16. package/dist/internal/machine/machine.d.ts.map +1 -1
  17. package/dist/internal/machine/machine.js +49 -13
  18. package/dist/internal/machine/machine.js.map +1 -1
  19. package/dist/internal/machine/planner.d.ts +11 -2
  20. package/dist/internal/machine/planner.d.ts.map +1 -1
  21. package/dist/internal/machine/planner.js +49 -9
  22. package/dist/internal/machine/planner.js.map +1 -1
  23. package/dist/internal/machine/runtime.d.ts +4 -3
  24. package/dist/internal/machine/runtime.d.ts.map +1 -1
  25. package/dist/internal/machine/runtime.js +12 -1
  26. package/dist/internal/machine/runtime.js.map +1 -1
  27. package/dist/internal/machine/topology.d.ts +9 -1
  28. package/dist/internal/machine/topology.d.ts.map +1 -1
  29. package/dist/internal/machine/topology.js +7 -0
  30. package/dist/internal/machine/topology.js.map +1 -1
  31. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  32. package/dist/internal/testing/machine/verification.js +15 -3
  33. package/dist/internal/testing/machine/verification.js.map +1 -1
  34. package/dist/testing/MachineTest.d.ts +2 -0
  35. package/dist/testing/MachineTest.d.ts.map +1 -1
  36. package/dist/testing/MachineTest.js.map +1 -1
  37. package/dist/unstable/reactivity/AtomMachine.d.ts +10 -8
  38. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  39. package/dist/unstable/reactivity/AtomMachine.js +2 -2
  40. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  41. package/docs/agent-guide.md +48 -0
  42. package/docs/effect-atom-react.md +28 -0
  43. package/package.json +1 -1
  44. package/src/Machine.ts +371 -45
  45. package/src/internal/machine/atom.ts +26 -18
  46. package/src/internal/machine/executionPlan.ts +37 -3
  47. package/src/internal/machine/invocation.ts +13 -1
  48. package/src/internal/machine/machine.ts +75 -15
  49. package/src/internal/machine/planner.ts +64 -11
  50. package/src/internal/machine/runtime.ts +48 -19
  51. package/src/internal/machine/topology.ts +18 -1
  52. package/src/internal/testing/machine/verification.ts +16 -3
  53. package/src/testing/MachineTest.ts +2 -0
  54. package/src/unstable/reactivity/AtomMachine.ts +10 -8
@@ -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)
@@ -7,7 +7,7 @@
7
7
  import * as Cause from "effect/Cause"
8
8
  import * as Effect from "effect/Effect"
9
9
  import * as Stream from "effect/Stream"
10
- import type { ChildMachine, Inspection, Logic, Machine } from "../../Machine.js"
10
+ import type { ChildMachine, ChildOwner, Inspection, Logic, Machine } from "../../Machine.js"
11
11
  import * as Configuration from "./configuration.js"
12
12
  import { InfiniteTransitionError, MachineSchemaDecodeError, StoppedError } from "./errors.js"
13
13
  import * as InvocationEvent from "./invocationEvent.js"
@@ -63,6 +63,16 @@ const streamLogic = (
63
63
  const resolveValue = (value: unknown, context: Machine.InvokeContext<any, any, any, any>): unknown =>
64
64
  typeof value === "function" ? value(context) : value
65
65
 
66
+ const makeChildOwner = (scope: Runtime.ProcessScope<any>): ChildOwner<any> => ({
67
+ spawn:
68
+ ((descriptor: ChildMachine.Any, options?: { readonly input?: unknown }) =>
69
+ (scope.spawn as any)(descriptor, options)) as ChildOwner<any>["spawn"],
70
+ sendTo: ((descriptor: ChildMachine.Any, event: unknown) => scope.sendTo(descriptor, event)) as ChildOwner<
71
+ any
72
+ >["sendTo"],
73
+ stop: ((descriptor: ChildMachine.Any) => scope.stopChild(descriptor)) as ChildOwner<any>["stop"]
74
+ })
75
+
66
76
  const resolveOne = (
67
77
  raw: Record<PropertyKey, any>,
68
78
  context: Machine.InvokeContext<any, any, any, any>,
@@ -291,11 +301,13 @@ export const startAll = (
291
301
  paths: ReadonlyArray<string>,
292
302
  event: Machine.LifecycleEvent<any>
293
303
  ): Effect.Effect<void, any, any> | undefined => {
304
+ const children = makeChildOwner(scope)
294
305
  const effects = Planner.sortEntryPaths(machine, paths)
295
306
  .filter((path) => configuration.active.has(path))
296
307
  .flatMap((path) => {
297
308
  const context = {
298
309
  ...(Configuration.getMachineReferences(configuration) ?? { self: scope.self, parent: scope.parent }),
310
+ children,
299
311
  state: configuration.values.get(path),
300
312
  containingState: Configuration.getParentValue(machine, configuration, path),
301
313
  ancestors: Configuration.getParentValues(machine, configuration, path),
@@ -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,
@@ -2124,15 +2172,30 @@ export const transition = <State, Event, Error = never, Requirements = never>(
2124
2172
  export const child = <const Id extends string, M extends Machine.Any>(
2125
2173
  id: Id,
2126
2174
  machine: M
2175
+ ): ChildMachine<Id, M> =>
2176
+ makeChild(id, machine, (input) =>
2177
+ machine.input === undefined
2178
+ ? (internalProcess.toProcessLogic as any)(machine)
2179
+ : (internalProcess.toProcessLogic as any)(machine, input))
2180
+
2181
+ const makeChild = <const Id extends string, M extends Machine.Any>(
2182
+ id: Id,
2183
+ machine: M,
2184
+ makeLogic: (input?: unknown) => Logic<any, any, any, any, any, any>
2127
2185
  ): ChildMachine<Id, M> => ({
2128
2186
  [ChildMachineTypeId]: ChildMachineTypeId,
2129
2187
  id,
2130
2188
  machine,
2131
- [ChildMachineLogicTypeId]: (input) =>
2189
+ [ChildMachineLogicTypeId]: makeLogic
2190
+ })
2191
+
2192
+ export const childFamily = <M extends Machine.Any>(machine: M): ChildMachine.Family<M> => {
2193
+ const makeLogic = (input?: unknown): Logic<any, any, any, any, any, any> =>
2132
2194
  machine.input === undefined
2133
2195
  ? (internalProcess.toProcessLogic as any)(machine)
2134
2196
  : (internalProcess.toProcessLogic as any)(machine, input)
2135
- })
2197
+ return (id) => makeChild(id, machine, makeLogic)
2198
+ }
2136
2199
 
2137
2200
  export const childAddress = <Event = never>(id: string): ChildAddress<Event> => id as ChildAddress<Event>
2138
2201
 
@@ -2174,10 +2237,7 @@ export const spawn: {
2174
2237
  SpawnError<Options>,
2175
2238
  ChildInitialError
2176
2239
  >
2177
- } = ((
2178
- logic: Logic<any, any, any, any, any, any>,
2179
- options?: SpawnOptions
2180
- ) =>
2240
+ } = ((logic: Logic<any, any, any, any, any, any>, options?: SpawnOptions) =>
2181
2241
  Effect.flatMap(
2182
2242
  internalRuntime.MachineRuntime,
2183
2243
  (runtime) => options === undefined ? runtime.spawn(logic) : (runtime.spawn as any)(logic, options)
@@ -52,6 +52,7 @@ import {
52
52
  isInitialTarget,
53
53
  isNoTarget,
54
54
  isSnapshot,
55
+ isStateUpdate,
55
56
  isTarget,
56
57
  makeChoiceTarget,
57
58
  makeTarget,
@@ -78,6 +79,11 @@ export type MicrostepPlan<State, Event, E, R> = {
78
79
  readonly changed: boolean
79
80
  }
80
81
 
82
+ type SettlingMicrostep<State, Event, E, R> = MicrostepPlan<State, Event, E, R> & {
83
+ /** Internal signal that eventless stabilization must run again. */
84
+ readonly stabilize: boolean
85
+ }
86
+
81
87
  export type MacrostepPlan<State, Event, E, R, Output> =
82
88
  & {
83
89
  readonly next: State
@@ -580,10 +586,15 @@ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R
580
586
  | Machine.Snapshot<States>
581
587
  | Machine.Target<States, Machine.StateIdentifier<States>>
582
588
  | undefined
589
+ readonly update: {
590
+ readonly path: string
591
+ readonly value: unknown
592
+ } | undefined
583
593
  readonly commands: ReadonlyArray<RuntimeCommand>
584
594
  readonly raisedEvents: ReadonlyArray<Event>
585
595
  readonly emittedEvents: ReadonlyArray<unknown>
586
596
  readonly changed: boolean
597
+ readonly stabilize: boolean
587
598
  readonly exitPaths: ReadonlyArray<string>
588
599
  readonly entryPaths: ReadonlyArray<string>
589
600
  readonly choiceTransitions: ReadonlyArray<{
@@ -1143,7 +1154,9 @@ export const removeConflictingTransitions = <
1143
1154
  let preempted = false
1144
1155
  const transitionsToRemove = new Set<EvaluatedTransition<States, Event, E, R, Context>>()
1145
1156
  for (const selected of filtered) {
1146
- if (hasPathIntersection(transition.exitPaths, selected.exitPaths)) {
1157
+ const writesSameState = transition.update !== undefined && selected.update !== undefined &&
1158
+ transition.update.path === selected.update.path
1159
+ if (hasPathIntersection(transition.exitPaths, selected.exitPaths) || writesSameState) {
1147
1160
  if (isDescendantOf(transition.selection.sourcePath, selected.selection.sourcePath)) {
1148
1161
  transitionsToRemove.add(selected)
1149
1162
  } else {
@@ -1344,6 +1357,7 @@ const collectEvaluatedTransition = <
1344
1357
  throw new Error("Machine transition returned decline without declaring declinable: true")
1345
1358
  }
1346
1359
  const unresolvedTarget = transitionResult.state === undefined
1360
+ || isStateUpdate(transitionResult.state)
1347
1361
  ? undefined
1348
1362
  : transitionResult.state as
1349
1363
  | Machine.Snapshot<States>
@@ -1356,6 +1370,21 @@ const collectEvaluatedTransition = <
1356
1370
  selection.transition.targets,
1357
1371
  unresolvedTarget
1358
1372
  )
1373
+ const update = isStateUpdate(transitionResult.state)
1374
+ ? (() => {
1375
+ const node = getNode(machine, transitionResult.state.path)
1376
+ if (
1377
+ !state.active.has(node.path) || node.schema === undefined ||
1378
+ (node.type !== "compound" && node.type !== "parallel")
1379
+ ) {
1380
+ throw new Error(`Machine state update owner "${node.path}" must be an active valued compound or parallel state`)
1381
+ }
1382
+ return {
1383
+ path: node.path,
1384
+ value: decodeStateValueSync(machine, node, transitionResult.state.value)
1385
+ }
1386
+ })()
1387
+ : undefined
1359
1388
  const choiceResolution = unresolvedTarget === undefined
1360
1389
  ? undefined
1361
1390
  : resolveChoiceTarget(
@@ -1456,7 +1485,10 @@ const collectEvaluatedTransition = <
1456
1485
  ? getTargetNodePath(target)
1457
1486
  : getTargetNodePath(unresolvedTarget)
1458
1487
  let stateAfterTransition = target === undefined
1459
- ? state
1488
+ ? update === undefined ? state : {
1489
+ ...state,
1490
+ values: new Map(state.values).set(update.path, update.value)
1491
+ }
1460
1492
  : normalizeTargetConfigurationSync<States>(machine, state, target)
1461
1493
  for (const additionalTarget of additionalChoiceTargets) {
1462
1494
  stateAfterTransition = normalizeTargetConfigurationSync<States>(
@@ -1466,6 +1498,7 @@ const collectEvaluatedTransition = <
1466
1498
  )
1467
1499
  }
1468
1500
  const changed = selection.transition.reenter || !hasSameActivePaths(state, stateAfterTransition)
1501
+ const stabilize = changed || update !== undefined
1469
1502
 
1470
1503
  if (!changed) {
1471
1504
  return {
@@ -1474,6 +1507,7 @@ const collectEvaluatedTransition = <
1474
1507
  branchKey: transitionResult.branchKey,
1475
1508
  unresolvedTarget,
1476
1509
  target,
1510
+ update,
1477
1511
  commands: [
1478
1512
  ...transitionResult.commands,
1479
1513
  ...(choiceResolution?.commands ?? []),
@@ -1496,6 +1530,7 @@ const collectEvaluatedTransition = <
1496
1530
  ...additionalTargetEmittedEvents
1497
1531
  ],
1498
1532
  changed,
1533
+ stabilize,
1499
1534
  exitPaths: [],
1500
1535
  entryPaths: [],
1501
1536
  choiceTransitions: [
@@ -1521,6 +1556,7 @@ const collectEvaluatedTransition = <
1521
1556
  branchKey: transitionResult.branchKey,
1522
1557
  unresolvedTarget,
1523
1558
  target,
1559
+ update,
1524
1560
  commands: [
1525
1561
  ...transitionResult.commands,
1526
1562
  ...(choiceResolution?.commands ?? []),
@@ -1543,6 +1579,7 @@ const collectEvaluatedTransition = <
1543
1579
  ...additionalTargetEmittedEvents
1544
1580
  ],
1545
1581
  changed,
1582
+ stabilize,
1546
1583
  exitPaths: reenteredHistoryTarget !== undefined
1547
1584
  ? sortExitPaths(
1548
1585
  machine,
@@ -1744,7 +1781,8 @@ export const planInitialSync = <
1744
1781
  emittedEvents: [...choiceResolution.emittedEvents, ...initialHistoryEmittedEvents],
1745
1782
  exitPaths: [],
1746
1783
  entryPaths: [],
1747
- changed: false
1784
+ changed: false,
1785
+ stabilize: false
1748
1786
  }]
1749
1787
  )
1750
1788
 
@@ -1850,7 +1888,8 @@ const microstep = <
1850
1888
  emittedEvents: [],
1851
1889
  exitPaths: [],
1852
1890
  entryPaths: [],
1853
- changed: false
1891
+ changed: false,
1892
+ stabilize: false
1854
1893
  }
1855
1894
  }
1856
1895
 
@@ -1881,6 +1920,17 @@ const microstep = <
1881
1920
  ...transition.choiceTransitions
1882
1921
  ])
1883
1922
  let stateAfterTransition = state
1923
+ // Updates never reactivate topology. Apply every retained value write to the
1924
+ // original active configuration before any control target becomes
1925
+ // authoritative.
1926
+ for (const transition of sortedTransitions) {
1927
+ if (transition.update !== undefined) {
1928
+ stateAfterTransition = {
1929
+ ...stateAfterTransition,
1930
+ values: new Map(stateAfterTransition.values).set(transition.update.path, transition.update.value)
1931
+ }
1932
+ }
1933
+ }
1884
1934
  // Value-only targets are evaluated against the original configuration. If
1885
1935
  // one is applied after a control-changing transition, it can resurrect a
1886
1936
  // branch that the changing transition exited. Apply value-only updates
@@ -1901,6 +1951,7 @@ const microstep = <
1901
1951
  }
1902
1952
 
1903
1953
  const changed = transitions.some((transition) => transition.changed)
1954
+ const stabilize = transitions.some((transition) => transition.stabilize)
1904
1955
  const transitionActions = sortedTransitions
1905
1956
  .flatMap((transition) => transition.commands)
1906
1957
  const transitionRaisedEvents = sortedTransitions
@@ -1918,7 +1969,8 @@ const microstep = <
1918
1969
  emittedEvents: transitionEmittedEvents,
1919
1970
  exitPaths: [],
1920
1971
  entryPaths: [],
1921
- changed: false
1972
+ changed: false,
1973
+ stabilize
1922
1974
  }
1923
1975
  }
1924
1976
 
@@ -1951,7 +2003,8 @@ const microstep = <
1951
2003
  emittedEvents: [...exit.emittedEvents, ...transitionEmittedEvents, ...entry.emittedEvents],
1952
2004
  exitPaths,
1953
2005
  entryPaths,
1954
- changed: true
2006
+ changed: true,
2007
+ stabilize
1955
2008
  }
1956
2009
  }
1957
2010
 
@@ -1974,7 +2027,7 @@ const settle = <
1974
2027
  commands: Array<RuntimeCommand>,
1975
2028
  raisedEvents: Array<Machine.EventOf<Events>>,
1976
2029
  emittedEvents: Array<unknown>,
1977
- microsteps: Array<MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R>>
2030
+ microsteps: Array<SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R>>
1978
2031
  ) => {
1979
2032
  let currentState = state
1980
2033
  let currentEvent = event
@@ -2010,7 +2063,7 @@ const settle = <
2010
2063
  pendingCompletions.length === 0 ? [] : [pendingCompletions.shift()!]
2011
2064
  )
2012
2065
  if (done.length > 0) {
2013
- const doneStep: MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2066
+ const doneStep: SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2014
2067
  machine,
2015
2068
  currentState,
2016
2069
  currentEvent,
@@ -2021,7 +2074,7 @@ const settle = <
2021
2074
  emittedEvents.push(...doneStep.emittedEvents)
2022
2075
  microsteps.push(doneStep)
2023
2076
  currentState = doneStep.next
2024
- shouldRunAlways = doneStep.changed
2077
+ shouldRunAlways = doneStep.stabilize
2025
2078
  continue
2026
2079
  }
2027
2080
  if (isActiveFinalConfiguration(machine, currentState)) {
@@ -2038,7 +2091,7 @@ const settle = <
2038
2091
  ? selectAlwaysTransitions<States, Events, Emits, E, R>(machine, currentState, currentEvent)
2039
2092
  : []
2040
2093
  if (always.length > 0) {
2041
- const alwaysStep: MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2094
+ const alwaysStep: SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2042
2095
  machine,
2043
2096
  currentState,
2044
2097
  currentEvent,
@@ -2049,7 +2102,7 @@ const settle = <
2049
2102
  emittedEvents.push(...alwaysStep.emittedEvents)
2050
2103
  microsteps.push(alwaysStep)
2051
2104
  currentState = alwaysStep.next
2052
- shouldRunAlways = alwaysStep.changed
2105
+ shouldRunAlways = alwaysStep.stabilize
2053
2106
  continue
2054
2107
  }
2055
2108
 
@@ -18,9 +18,10 @@ import * as Scope from "effect/Scope"
18
18
  import * as Stream from "effect/Stream"
19
19
  import * as SynchronizedRef from "effect/SynchronizedRef"
20
20
  import type * as Take from "effect/Take"
21
- import type { Inspection, Machine as MachineDefinition, MachineTarget } from "../../Machine.js"
21
+ import type { ChildMachine, Inspection, Machine as MachineDefinition, MachineTarget } from "../../Machine.js"
22
22
  import { ChildAlreadyExistsError, StoppedError } from "./errors.js"
23
23
  import * as InspectionRuntime from "./inspectionRuntime.js"
24
+ import { ChildMachineLogicTypeId } from "./symbols.js"
24
25
 
25
26
  type ChildDescriptor = {
26
27
  readonly id: string
@@ -376,7 +377,7 @@ const sendMachineTarget = (
376
377
  export interface ProcessScope<Event> {
377
378
  readonly self: ProcessAddress<Event>
378
379
  readonly parent: ProcessAddress<unknown> | undefined
379
- readonly spawn: ProcessSpawn
380
+ readonly spawn: ProcessSpawn<Event>
380
381
  readonly sendParent: (event: unknown) => Effect.Effect<void, StoppedError>
381
382
  readonly emit: (event: unknown) => Effect.Effect<void>
382
383
  readonly sendTo: {
@@ -552,7 +553,15 @@ export interface ProcessLogic<
552
553
  run(context: ProcessContext<State, Event>): Effect.Effect<Output, Error, Requirements>
553
554
  }
554
555
 
555
- export interface ProcessSpawn {
556
+ export interface ProcessSpawn<OwnerEvent = unknown> {
557
+ <const Child extends ChildMachine.Any>(
558
+ child: Child & ChildMachine.Executable<Child> & ChildMachine.ParentCompatibility<Child, OwnerEvent>,
559
+ ...options: ChildMachine.SpawnArgs<Child>
560
+ ): Effect.Effect<
561
+ ChildMachine.Ref<Child>,
562
+ ChildAlreadyExistsError | ChildMachine.StartError<Child>,
563
+ ChildMachine.StartRequirements<Child>
564
+ >
556
565
  <ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
557
566
  logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>
558
567
  ): Effect.Effect<
@@ -1207,6 +1216,14 @@ const makeChildRuntimeSync = (
1207
1216
  })
1208
1217
  }
1209
1218
 
1219
+ function spawn<const Child extends ChildMachine.Any>(
1220
+ child: Child & ChildMachine.Executable<Child> & ChildMachine.ParentCompatibility<Child, unknown>,
1221
+ ...options: ChildMachine.SpawnArgs<Child>
1222
+ ): Effect.Effect<
1223
+ ChildMachine.Ref<Child>,
1224
+ ChildAlreadyExistsError | ChildMachine.StartError<Child>,
1225
+ ChildMachine.StartRequirements<Child>
1226
+ >
1210
1227
  function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
1211
1228
  logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>
1212
1229
  ): Effect.Effect<
@@ -1232,32 +1249,44 @@ const makeChildRuntimeSync = (
1232
1249
  ChildAlreadyExistsError | ChildInitialError,
1233
1250
  Exclude<ChildRequirements, Scope.Scope>
1234
1251
  >
1235
- function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
1236
- logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>,
1237
- spawnOptions?: {
1252
+ function spawn(
1253
+ logicOrChild: ProcessLogic<any, any, any, any, any, any> | ChildMachine.Any,
1254
+ options?: {
1238
1255
  readonly id: string
1239
1256
  readonly descriptor?: ChildDescriptor
1240
1257
  readonly onOutcome?: (
1241
- outcome: RuntimeOutcome<ChildState, ChildError, ChildOutput>
1258
+ outcome: RuntimeOutcome<any, any, any>
1242
1259
  ) => Effect.Effect<void>
1243
1260
  readonly [activeSnapshotObserver]?: (
1244
- snapshot: Extract<RuntimeSnapshot<ChildState, ChildError, ChildOutput>, { readonly status: "active" }>
1261
+ snapshot: Extract<RuntimeSnapshot<any, any, any>, { readonly status: "active" }>
1245
1262
  ) => Effect.Effect<void>
1246
1263
  readonly [sendParentOverride]?: (event: unknown) => Effect.Effect<void, StoppedError>
1247
- }
1248
- ): Effect.Effect<
1249
- MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
1250
- ChildAlreadyExistsError | ChildInitialError,
1251
- Exclude<ChildRequirements, Scope.Scope>
1252
- > {
1264
+ } | { readonly input?: unknown }
1265
+ ): Effect.Effect<MachineRef<any, any, any, any>, any, any> {
1266
+ const descriptor = typeof logicOrChild === "object" && logicOrChild !== null &&
1267
+ ChildMachineLogicTypeId in logicOrChild
1268
+ ? logicOrChild as ChildMachine.Any
1269
+ : undefined
1270
+ const logic = descriptor === undefined
1271
+ ? logicOrChild as ProcessLogic<any, any, any, any, any, any>
1272
+ : descriptor[ChildMachineLogicTypeId](
1273
+ (options as { readonly input?: unknown } | undefined)?.input
1274
+ ) as unknown as ProcessLogic<any, any, any, any, any, any>
1275
+ const spawnOptions = descriptor === undefined
1276
+ ? options as {
1277
+ readonly id: string
1278
+ readonly descriptor?: ChildDescriptor
1279
+ readonly onOutcome?: (outcome: RuntimeOutcome<any, any, any>) => Effect.Effect<void>
1280
+ readonly [activeSnapshotObserver]?: (
1281
+ snapshot: Extract<RuntimeSnapshot<any, any, any>, { readonly status: "active" }>
1282
+ ) => Effect.Effect<void>
1283
+ readonly [sendParentOverride]?: (event: unknown) => Effect.Effect<void, StoppedError>
1284
+ } | undefined
1285
+ : { id: descriptor.id, descriptor }
1253
1286
  const token = Symbol()
1254
1287
  const key = spawnOptions?.id ?? token
1255
1288
  let startedChild: MachineRef<any, any, any, any> | undefined
1256
- return Effect.suspend((): Effect.Effect<
1257
- MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
1258
- ChildAlreadyExistsError | ChildInitialError,
1259
- Exclude<ChildRequirements, Scope.Scope>
1260
- > => {
1289
+ return Effect.suspend(() => {
1261
1290
  if (registry.closed) {
1262
1291
  return Effect.interrupt
1263
1292
  }