@typeonce/effect-machine 0.12.0 → 0.14.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 (103) hide show
  1. package/README.md +130 -30
  2. package/dist/Machine.d.ts +722 -262
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +130 -62
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +1 -0
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +23 -0
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
  11. package/dist/internal/machine/commandRuntime.js +2 -5
  12. package/dist/internal/machine/commandRuntime.js.map +1 -1
  13. package/dist/internal/machine/configuration.d.ts +5 -0
  14. package/dist/internal/machine/configuration.d.ts.map +1 -1
  15. package/dist/internal/machine/configuration.js +6 -2
  16. package/dist/internal/machine/configuration.js.map +1 -1
  17. package/dist/internal/machine/executionPlan.d.ts +1 -0
  18. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  19. package/dist/internal/machine/executionPlan.js +57 -24
  20. package/dist/internal/machine/executionPlan.js.map +1 -1
  21. package/dist/internal/machine/initialization.d.ts +5 -0
  22. package/dist/internal/machine/initialization.d.ts.map +1 -0
  23. package/dist/internal/machine/initialization.js +61 -0
  24. package/dist/internal/machine/initialization.js.map +1 -0
  25. package/dist/internal/machine/inspectionRuntime.d.ts +25 -0
  26. package/dist/internal/machine/inspectionRuntime.d.ts.map +1 -0
  27. package/dist/internal/machine/inspectionRuntime.js +79 -0
  28. package/dist/internal/machine/inspectionRuntime.js.map +1 -0
  29. package/dist/internal/machine/invocation.d.ts +2 -1
  30. package/dist/internal/machine/invocation.d.ts.map +1 -1
  31. package/dist/internal/machine/invocation.js +17 -10
  32. package/dist/internal/machine/invocation.js.map +1 -1
  33. package/dist/internal/machine/machine.d.ts +4 -3
  34. package/dist/internal/machine/machine.d.ts.map +1 -1
  35. package/dist/internal/machine/machine.js +327 -32
  36. package/dist/internal/machine/machine.js.map +1 -1
  37. package/dist/internal/machine/planner.d.ts +28 -0
  38. package/dist/internal/machine/planner.d.ts.map +1 -1
  39. package/dist/internal/machine/planner.js +132 -51
  40. package/dist/internal/machine/planner.js.map +1 -1
  41. package/dist/internal/machine/process.d.ts.map +1 -1
  42. package/dist/internal/machine/process.js +4 -2
  43. package/dist/internal/machine/process.js.map +1 -1
  44. package/dist/internal/machine/runtime.d.ts +27 -3
  45. package/dist/internal/machine/runtime.d.ts.map +1 -1
  46. package/dist/internal/machine/runtime.js +440 -64
  47. package/dist/internal/machine/runtime.js.map +1 -1
  48. package/dist/internal/machine/symbols.d.ts +2 -0
  49. package/dist/internal/machine/symbols.d.ts.map +1 -1
  50. package/dist/internal/machine/symbols.js +2 -0
  51. package/dist/internal/machine/symbols.js.map +1 -1
  52. package/dist/internal/machine/topology.d.ts +25 -0
  53. package/dist/internal/machine/topology.d.ts.map +1 -1
  54. package/dist/internal/machine/topology.js +26 -10
  55. package/dist/internal/machine/topology.js.map +1 -1
  56. package/dist/internal/testing/machine/exploration.d.ts.map +1 -1
  57. package/dist/internal/testing/machine/exploration.js +11 -2
  58. package/dist/internal/testing/machine/exploration.js.map +1 -1
  59. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  60. package/dist/internal/testing/machine/finiteModel.js +75 -69
  61. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  62. package/dist/internal/testing/machine/transitionCoverage.d.ts +20 -0
  63. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -0
  64. package/dist/internal/testing/machine/transitionCoverage.js +80 -0
  65. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -0
  66. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  67. package/dist/internal/testing/machine/verification.js +171 -33
  68. package/dist/internal/testing/machine/verification.js.map +1 -1
  69. package/dist/testing/MachineTest.d.ts +97 -22
  70. package/dist/testing/MachineTest.d.ts.map +1 -1
  71. package/dist/testing/MachineTest.js +58 -16
  72. package/dist/testing/MachineTest.js.map +1 -1
  73. package/dist/unstable/cluster/ClusterMachine.d.ts +4 -1
  74. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  75. package/dist/unstable/cluster/ClusterMachine.js +4 -1
  76. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  77. package/dist/unstable/reactivity/AtomMachine.d.ts +21 -3
  78. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  79. package/dist/unstable/reactivity/AtomMachine.js +21 -3
  80. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  81. package/docs/agent-guide.md +240 -65
  82. package/package.json +5 -5
  83. package/src/Machine.ts +1240 -495
  84. package/src/internal/machine/atom.ts +26 -0
  85. package/src/internal/machine/commandRuntime.ts +2 -9
  86. package/src/internal/machine/configuration.ts +28 -3
  87. package/src/internal/machine/executionPlan.ts +72 -24
  88. package/src/internal/machine/initialization.ts +73 -0
  89. package/src/internal/machine/inspectionRuntime.ts +102 -0
  90. package/src/internal/machine/invocation.ts +37 -12
  91. package/src/internal/machine/machine.ts +438 -39
  92. package/src/internal/machine/planner.ts +180 -55
  93. package/src/internal/machine/process.ts +4 -2
  94. package/src/internal/machine/runtime.ts +661 -89
  95. package/src/internal/machine/symbols.ts +3 -0
  96. package/src/internal/machine/topology.ts +65 -12
  97. package/src/internal/testing/machine/exploration.ts +10 -2
  98. package/src/internal/testing/machine/finiteModel.ts +114 -80
  99. package/src/internal/testing/machine/transitionCoverage.ts +116 -0
  100. package/src/internal/testing/machine/verification.ts +216 -58
  101. package/src/testing/MachineTest.ts +118 -28
  102. package/src/unstable/cluster/ClusterMachine.ts +4 -1
  103. package/src/unstable/reactivity/AtomMachine.ts +24 -3
@@ -139,6 +139,32 @@ export const emissions = <State, Event, Error, Output, StartError, Emitted>(
139
139
  )
140
140
  }
141
141
 
142
+ export const inspection = <State, Event, Error, Output, StartError, Emitted>(
143
+ self: MachineAtom<State, Event, Error, Output, StartError, Emitted>
144
+ ): Stream.Stream<Machine.Inspection.Event, StartError, AtomRegistry.AtomRegistry> => {
145
+ const prepared = preparedByMachineAtom.get(self as object)
146
+ if (prepared === undefined) return Stream.empty
147
+ return Stream.unwrap(
148
+ Effect.gen(function*() {
149
+ const registry = yield* AtomRegistry.AtomRegistry
150
+ const releasePrepared = yield* Effect.sync(() => registry.mount(prepared))
151
+ yield* Effect.addFinalizer(() => Effect.sync(releasePrepared))
152
+ const machine = yield* Atom.getResult(prepared)
153
+ const pull = yield* Stream.toPull(machine.inspection)
154
+ const firstPull = yield* pull.pipe(Effect.forkScoped({ startImmediately: true }))
155
+ const releaseRef = yield* Effect.sync(() => registry.mount(self.ref))
156
+ yield* Effect.addFinalizer(() => Effect.sync(releaseRef))
157
+ yield* Atom.getResult(self.ref)
158
+ let first = true
159
+ return Stream.fromPull(Effect.succeed(Effect.suspend(() => {
160
+ if (!first) return pull
161
+ first = false
162
+ return Fiber.join(firstPull)
163
+ })))
164
+ })
165
+ )
166
+ }
167
+
142
168
  export const childEmissions = <Child extends Machine.ChildMachine.Any, StartError>(
143
169
  self: ChildMachineAtom<Child, StartError>
144
170
  ): Stream.Stream<RefEmitted<Machine.ChildMachine.Ref<Child>>, StartError, AtomRegistry.AtomRegistry> =>
@@ -10,18 +10,13 @@ import type { RuntimeCommand } from "./command.js"
10
10
  import { decodeEmit, decodeEvent } from "./protocol.js"
11
11
  import type { ProcessScope } from "./runtime.js"
12
12
 
13
- const isMachineTarget = (
14
- target: unknown
15
- ): target is { readonly send: (event: unknown) => Effect.Effect<void, unknown> } =>
16
- typeof target === "object" && target !== null && "send" in target && typeof target.send === "function"
17
-
18
13
  export const makeLiveRuntime = <Events, Emits>(
19
14
  machine: Machine.Any,
20
15
  scope: ProcessScope<Events>
21
16
  ): Runtime<Events, Emits> => ({
22
17
  raise: (event) =>
23
18
  decodeEvent(machine, event).pipe(
24
- Effect.flatMap((event) => scope.self.send(event as Events))
19
+ Effect.flatMap((event) => scope.sendTo(scope.self, event as Events))
25
20
  ),
26
21
  emit: (event) =>
27
22
  decodeEmit(machine, event).pipe(
@@ -35,9 +30,7 @@ export const runCommands = <Event>(
35
30
  ) =>
36
31
  Effect.forEach(commands, (command) =>
37
32
  command._tag === "SendTo"
38
- ? isMachineTarget(command.target)
39
- ? command.target.send(command.event)
40
- : scope.sendTo(command.target as never, command.event)
33
+ ? scope.sendTo(command.target as never, command.event)
41
34
  : scope.stopChild(command.child as never), { discard: true })
42
35
 
43
36
  export const runEmittedEvents = <Events, Emits>(
@@ -17,7 +17,14 @@ import {
17
17
  decodeStateValue,
18
18
  decodeStateValueSync
19
19
  } from "./protocol.js"
20
- import { getNode, getStateNodeSchema, isSnapshot, isTarget, TargetSnapshotTypeId } from "./topology.js"
20
+ import {
21
+ getNode,
22
+ getStateNodeSchema,
23
+ type InitialTarget as InitialTargetInstruction,
24
+ isSnapshot,
25
+ isTarget,
26
+ TargetSnapshotTypeId
27
+ } from "./topology.js"
21
28
 
22
29
  export interface HistoryRecord {
23
30
  readonly mode: "shallow" | "deep"
@@ -952,7 +959,8 @@ const configurationFromTargetPathSync = (
952
959
  current: ActiveConfiguration,
953
960
  path: string,
954
961
  value: unknown,
955
- providedValues: Readonly<Record<string, unknown>> | undefined
962
+ providedValues: Readonly<Record<string, unknown>> | undefined,
963
+ allowIncomplete = false
956
964
  ): ActiveConfiguration => {
957
965
  const node = getNode(machine, path)
958
966
  const active = new Set<string>()
@@ -998,12 +1006,29 @@ const configurationFromTargetPathSync = (
998
1006
  }
999
1007
  }
1000
1008
 
1001
- if (node.type === "compound" || node.type === "parallel") {
1009
+ if (!allowIncomplete && (node.type === "compound" || node.type === "parallel")) {
1002
1010
  throw new Error(`Machine target "${node.path}" must include an active child state`)
1003
1011
  }
1004
1012
  return { active, values, outputs, history: current.history }
1005
1013
  }
1006
1014
 
1015
+ /** Builds the selected path and its ancestors without requiring nested active
1016
+ * children. The planner completes those children through declared initial
1017
+ * topology and `initialize` handlers before normalization continues. */
1018
+ export const configurationFromInitialTargetSync = (
1019
+ machine: Machine.Any,
1020
+ current: ActiveConfiguration,
1021
+ target: InitialTargetInstruction
1022
+ ): ActiveConfiguration =>
1023
+ configurationFromTargetPathSync(
1024
+ machine,
1025
+ current,
1026
+ target.path,
1027
+ target.value,
1028
+ target.values as Readonly<Record<string, unknown>> | undefined,
1029
+ true
1030
+ )
1031
+
1007
1032
  const configurationFromTargetSnapshotSync = (
1008
1033
  machine: Machine.Any,
1009
1034
  current: ActiveConfiguration,
@@ -38,6 +38,7 @@ import {
38
38
  normalizeTransition,
39
39
  planConfiguration,
40
40
  removeConflictingTransitions,
41
+ resolveInitialTarget,
41
42
  type SelectedTransition,
42
43
  sortEntryPaths,
43
44
  sortEvaluatedTransitions,
@@ -46,7 +47,7 @@ import {
46
47
  validateDeclaredTransitionTarget
47
48
  } from "./planner.js"
48
49
  import { decodeEmitSync, decodeEventSync, decodeInputSync, decodeStateValueSync } from "./protocol.js"
49
- import { isNoTarget, isSnapshot, isTarget, TargetSnapshotTypeId } from "./topology.js"
50
+ import { isInitialTarget, isNoTarget, isSnapshot, isTarget, TargetSnapshotTypeId } from "./topology.js"
50
51
 
51
52
  interface IndexedExecutionDescriptor {
52
53
  readonly flat: boolean
@@ -91,7 +92,7 @@ const resolveMachineReferences = (
91
92
  : { self: machineReferences.self, parent: machineReferences.parent }
92
93
 
93
94
  const indexedStateConfigKeys: ReadonlySet<PropertyKey> = new Set([
94
- "initial",
95
+ "initialize",
95
96
  "invoke",
96
97
  "on",
97
98
  "output"
@@ -149,7 +150,7 @@ const compileIndexedExecutionDescriptor = (
149
150
  }
150
151
  const byEvent = new Map<PropertyKey, MicrostepTransition<any, any, any, any>>()
151
152
  for (const tag of Reflect.ownKeys(config.on)) {
152
- const transition = normalizeTransition(config.on[tag])
153
+ const transition = normalizeTransition(config.on[tag] as Parameters<typeof normalizeTransition>[0])
153
154
  if (transition !== undefined) {
154
155
  byEvent.set(tag, transition)
155
156
  }
@@ -402,6 +403,7 @@ const emptyExecutionValues: ReadonlyArray<never> = Object.freeze([])
402
403
  export interface ExecutionMicrostep<State = unknown> {
403
404
  readonly next: State
404
405
  readonly event: unknown
406
+ readonly transitions?: ReadonlyArray<Machine.RetainedTransition>
405
407
  readonly commands: ReadonlyArray<RuntimeCommand>
406
408
  readonly raisedEvents: ReadonlyArray<unknown>
407
409
  readonly emittedEvents: ReadonlyArray<unknown>
@@ -422,12 +424,13 @@ export interface ExecutionMacrostep<State = unknown> {
422
424
  const collectIndexedTransition = (
423
425
  machine: Machine.Any,
424
426
  transition: TransitionHandler<any, any, any, any>,
425
- context: any
427
+ context: any,
428
+ evaluate?: NonNullable<MicrostepTransition<any, any, any, any>["evaluate"]>
426
429
  ) => {
427
430
  let commands: Array<RuntimeCommand> | undefined
428
431
  let raisedEvents: Array<any> | undefined
429
432
  let emittedEvents: Array<unknown> | undefined
430
- const result = transition(context, {
433
+ const enqueue = {
431
434
  raise: (event: unknown) => {
432
435
  ;(raisedEvents ??= []).push(decodeEventSync(machine, event))
433
436
  },
@@ -440,9 +443,13 @@ const collectIndexedTransition = (
440
443
  stop: (child: unknown) => {
441
444
  ;(commands ??= []).push({ _tag: "Stop", child: child as any })
442
445
  }
443
- })
446
+ }
447
+ const evaluated = evaluate === undefined
448
+ ? { result: transition(context, enqueue), branchIndex: 0 }
449
+ : evaluate(context, enqueue)
444
450
  return {
445
- state: isNoTarget(result) ? undefined : result,
451
+ state: isNoTarget(evaluated.result) ? undefined : evaluated.result,
452
+ branchIndex: evaluated.branchIndex,
446
453
  commands: commands ?? emptyExecutionValues,
447
454
  raisedEvents: raisedEvents ?? emptyExecutionValues,
448
455
  emittedEvents: emittedEvents ?? emptyExecutionValues
@@ -529,14 +536,28 @@ const collectIndexedEvaluatedTransition = (
529
536
  state: OwnedIndexedState,
530
537
  selection: IndexedSelectedTransition
531
538
  ): IndexedEvaluatedTransition => {
532
- const transitionResult = collectIndexedTransition(machine, selection.transition.transition, selection.context)
533
- const target = transitionResult.state
539
+ const transitionResult = collectIndexedTransition(
540
+ machine,
541
+ selection.transition.transition,
542
+ selection.context,
543
+ selection.transition.evaluate
544
+ )
545
+ const unresolvedTarget = transitionResult.state
534
546
  validateDeclaredTransitionTarget(
535
547
  selection.sourcePath,
536
548
  selection.trigger,
537
549
  selection.transition.targets,
538
- target
550
+ unresolvedTarget
539
551
  )
552
+ const initialResolution = unresolvedTarget !== undefined && isInitialTarget(unresolvedTarget)
553
+ ? resolveInitialTarget(
554
+ machine,
555
+ activeConfigurationFromIndexedState(descriptor, state),
556
+ unresolvedTarget,
557
+ (selection.context as any).event
558
+ )
559
+ : undefined
560
+ const target = initialResolution?.target ?? unresolvedTarget
540
561
  if (target !== undefined && !isTarget(target) && !isSnapshot(target)) {
541
562
  throw new Error("Machine expected indexed transition target to be a snapshot or target builder result")
542
563
  }
@@ -547,16 +568,17 @@ const collectIndexedEvaluatedTransition = (
547
568
  if (!changed) {
548
569
  return {
549
570
  selection,
550
- unresolvedTarget: target as any,
571
+ branchIndex: transitionResult.branchIndex,
572
+ unresolvedTarget: unresolvedTarget as any,
551
573
  target: target as any,
552
574
  next,
553
- commands: transitionResult.commands,
554
- raisedEvents: transitionResult.raisedEvents,
555
- emittedEvents: transitionResult.emittedEvents,
575
+ commands: [...transitionResult.commands, ...(initialResolution?.commands ?? [])],
576
+ raisedEvents: [...transitionResult.raisedEvents, ...(initialResolution?.raisedEvents ?? [])],
577
+ emittedEvents: [...transitionResult.emittedEvents, ...(initialResolution?.emittedEvents ?? [])],
556
578
  changed: false,
557
579
  exitPaths: [],
558
580
  entryPaths: [],
559
- choiceTransitions: []
581
+ choiceTransitions: initialResolution?.transitions ?? []
560
582
  }
561
583
  }
562
584
 
@@ -570,16 +592,17 @@ const collectIndexedEvaluatedTransition = (
570
592
  : naturalBoundary
571
593
  return {
572
594
  selection,
573
- unresolvedTarget: target as any,
595
+ branchIndex: transitionResult.branchIndex,
596
+ unresolvedTarget: unresolvedTarget as any,
574
597
  target: target as any,
575
598
  next,
576
- commands: transitionResult.commands,
577
- raisedEvents: transitionResult.raisedEvents,
578
- emittedEvents: transitionResult.emittedEvents,
599
+ commands: [...transitionResult.commands, ...(initialResolution?.commands ?? [])],
600
+ raisedEvents: [...transitionResult.raisedEvents, ...(initialResolution?.raisedEvents ?? [])],
601
+ emittedEvents: [...transitionResult.emittedEvents, ...(initialResolution?.emittedEvents ?? [])],
579
602
  changed: true,
580
603
  exitPaths: getExitPaths(machine, activeConfigurationFromIndexedState(descriptor, state), boundary),
581
604
  entryPaths: getEntryPaths(machine, activeConfigurationFromIndexedState(descriptor, next), boundary),
582
- choiceTransitions: []
605
+ choiceTransitions: initialResolution?.transitions ?? []
583
606
  }
584
607
  }
585
608
 
@@ -588,13 +611,23 @@ const indexedMicrostep = (
588
611
  descriptor: IndexedExecutionDescriptor,
589
612
  state: OwnedIndexedState,
590
613
  event: any,
591
- selections: ReadonlyArray<IndexedSelectedTransition>
614
+ selections: ReadonlyArray<IndexedSelectedTransition>,
615
+ retainTransitions: boolean
592
616
  ): ExecutionMicrostep<OwnedIndexedState> => {
617
+ const retain = (transition: IndexedEvaluatedTransition): Machine.RetainedTransition => ({
618
+ source: transition.selection.sourcePath,
619
+ trigger: transition.selection.trigger,
620
+ reenter: transition.selection.transition.reenter,
621
+ branchIndex: transition.branchIndex,
622
+ target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
623
+ resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
624
+ })
593
625
  if (selections.length === 1) {
594
626
  const transition = collectIndexedEvaluatedTransition(machine, descriptor, state, selections[0]!)
595
627
  return {
596
628
  next: transition.next,
597
629
  event,
630
+ ...(retainTransitions ? { transitions: [retain(transition)] } : undefined),
598
631
  commands: transition.commands,
599
632
  raisedEvents: transition.raisedEvents,
600
633
  emittedEvents: transition.emittedEvents,
@@ -645,6 +678,7 @@ const indexedMicrostep = (
645
678
  return {
646
679
  next,
647
680
  event,
681
+ ...(retainTransitions ? { transitions: transitions.map(retain) } : undefined),
648
682
  commands,
649
683
  raisedEvents,
650
684
  emittedEvents,
@@ -719,7 +753,8 @@ const planIndexedFlatState = (
719
753
  event,
720
754
  snapshot: snapshotFromIndexedState(descriptor, current),
721
755
  target: getTargetBuilder(machine, sourcePath)
722
- }
756
+ },
757
+ transition.evaluate
723
758
  )
724
759
  const target = transitionResult.state
725
760
  validateDeclaredTransitionTarget(
@@ -762,6 +797,18 @@ const planIndexedFlatState = (
762
797
  const step: ExecutionMicrostep<OwnedIndexedState> = {
763
798
  next,
764
799
  event,
800
+ ...(retainMicrosteps
801
+ ? {
802
+ transitions: [{
803
+ source: sourcePath,
804
+ trigger: { type: "event", event: event._tag },
805
+ reenter: transition.reenter,
806
+ branchIndex: transitionResult.branchIndex,
807
+ target: target === undefined ? undefined : getTargetNodePath(target as any),
808
+ resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any)
809
+ }]
810
+ }
811
+ : undefined),
765
812
  commands: transitionResult.commands,
766
813
  raisedEvents: transitionResult.raisedEvents,
767
814
  emittedEvents: transitionResult.emittedEvents,
@@ -828,6 +875,7 @@ const planIndexedState = (
828
875
  microsteps: planned.microsteps.map((step) => ({
829
876
  next: ownedIndexedStateFromActive(descriptor, step.next),
830
877
  event: step.event,
878
+ transitions: step.transitions,
831
879
  commands: step.commands,
832
880
  raisedEvents: step.raisedEvents,
833
881
  emittedEvents: step.emittedEvents,
@@ -874,7 +922,7 @@ const planIndexedState = (
874
922
  }
875
923
  }
876
924
 
877
- const first = indexedMicrostep(machine, descriptor, configuration, decoded, selections)
925
+ const first = indexedMicrostep(machine, descriptor, configuration, decoded, selections, retainMicrosteps)
878
926
  let current = first.next
879
927
  let currentEvent: any = decoded
880
928
  const commands = [...first.commands]
@@ -932,7 +980,7 @@ const planIndexedState = (
932
980
  currentEvent = raised
933
981
  const raisedSelections = selectIndexedEventTransitions(machine, descriptor, current, raised, machineReferences)
934
982
  if (raisedSelections.length === 0) continue
935
- const step = indexedMicrostep(machine, descriptor, current, raised, raisedSelections)
983
+ const step = indexedMicrostep(machine, descriptor, current, raised, raisedSelections, retainMicrosteps)
936
984
  current = step.next
937
985
  commands.push(...step.commands)
938
986
  raisedEvents.push(...step.raisedEvents)
@@ -0,0 +1,73 @@
1
+ /** Internal builders for schema-valued default state entry. */
2
+
3
+ import { hasProperty } from "effect/Predicate"
4
+ import type { Machine } from "../../Machine.js"
5
+ import { SnapshotBuilderStateTypeId } from "./symbols.js"
6
+ import * as Topology from "./topology.js"
7
+
8
+ const getNode = (machine: Machine.Any, path: string): Machine.StateNode => {
9
+ const node = machine.stateNodes.byPath.get(path)
10
+ if (node === undefined) {
11
+ throw new Error(`Machine expected state path "${path}" to exist`)
12
+ }
13
+ return node
14
+ }
15
+
16
+ const withFrom = <Method extends (value: unknown) => unknown>(method: Method) => {
17
+ Object.defineProperty(method, "from", {
18
+ value: (...args: ReadonlyArray<unknown>) => method(Topology.makeStateInput(args.length === 0 ? {} : args[0])),
19
+ enumerable: false
20
+ })
21
+ return method as Method & { readonly from: (...args: ReadonlyArray<unknown>) => unknown }
22
+ }
23
+
24
+ const makeCompletion = (values: Readonly<Record<string, unknown>>): object => {
25
+ const completion = {}
26
+ Object.defineProperty(completion, SnapshotBuilderStateTypeId, {
27
+ value: values,
28
+ enumerable: false
29
+ })
30
+ return completion
31
+ }
32
+
33
+ const makeParallelBuilder = (
34
+ machine: Machine.Any,
35
+ node: Machine.StateNode,
36
+ values: Readonly<Record<string, unknown>>
37
+ ): object => {
38
+ const builder = makeCompletion(values) as Record<string | symbol, unknown>
39
+ for (const childPath of node.children) {
40
+ const child = getNode(machine, childPath)
41
+ if (child.schema === undefined || hasProperty(values, child.key)) continue
42
+ builder[child.key] = withFrom((value: unknown) => {
43
+ const nextValues: Record<string, unknown> = Object.assign({}, values)
44
+ nextValues[child.key] = value
45
+ return makeParallelBuilder(machine, node, nextValues)
46
+ })
47
+ }
48
+ return builder
49
+ }
50
+
51
+ export const makeStateInitializeBuilder = (machine: Machine.Any, path: string): object => {
52
+ const node = getNode(machine, path)
53
+ if (node.type === "parallel") {
54
+ return makeParallelBuilder(machine, node, {})
55
+ }
56
+ if (node.type !== "compound") {
57
+ throw new Error(`Machine state "${path}" cannot initialize child states`)
58
+ }
59
+ const child = getNode(machine, node.initial)
60
+ if (child.schema === undefined) {
61
+ throw new Error(`Machine state "${path}" has no schema-valued initial child`)
62
+ }
63
+ return withFrom((value: unknown) => makeCompletion({ [child.key]: value }))
64
+ }
65
+
66
+ export const getStateInitializeValues = (path: string, result: unknown): Readonly<Record<string, unknown>> => {
67
+ if (typeof result !== "object" || result === null || !hasProperty(result, SnapshotBuilderStateTypeId)) {
68
+ throw new Error(`Machine initialize handler for "${path}" must return its builder result`)
69
+ }
70
+ return (result as { readonly [SnapshotBuilderStateTypeId]: Readonly<Record<string, unknown>> })[
71
+ SnapshotBuilderStateTypeId
72
+ ]
73
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Root-scoped live inspection publication and evidence projection.
3
+ *
4
+ * @since 0.13.0
5
+ */
6
+
7
+ import * as Effect from "effect/Effect"
8
+ import * as PubSub from "effect/PubSub"
9
+ import * as Stream from "effect/Stream"
10
+ import type { Inspection } from "../../Machine.js"
11
+
12
+ export type Draft<Event extends Inspection.Event = Inspection.Event> = Event extends Inspection.Event
13
+ ? Omit<Event, "sequence" | "rootSessionId">
14
+ : never
15
+
16
+ export interface Runtime {
17
+ readonly rootSessionId: string
18
+ readonly stream: Stream.Stream<Inspection.Event>
19
+ readonly isActive: () => boolean
20
+ readonly nextDeliveryId: () => number
21
+ readonly nextMacrostepId: () => number
22
+ readonly publishUnsafe: (event: Draft) => void
23
+ readonly close: Effect.Effect<void>
24
+ }
25
+
26
+ export const make = (rootSessionId: string): Effect.Effect<Runtime> =>
27
+ Effect.gen(function*() {
28
+ const pubsub = yield* PubSub.unbounded<Inspection.Event>()
29
+ let subscribers = 0
30
+ let sequence = 0
31
+ let deliveryId = 0
32
+ let macrostepId = 0
33
+ return {
34
+ rootSessionId,
35
+ stream: Stream.fromPubSub(pubsub).pipe(
36
+ Stream.onStart(Effect.sync(() => {
37
+ subscribers += 1
38
+ })),
39
+ Stream.ensuring(Effect.sync(() => {
40
+ subscribers -= 1
41
+ }))
42
+ ),
43
+ isActive: () => subscribers > 0,
44
+ nextDeliveryId: () => deliveryId++,
45
+ nextMacrostepId: () => macrostepId++,
46
+ publishUnsafe: (event) => {
47
+ if (subscribers === 0) return
48
+ PubSub.publishUnsafe(pubsub, {
49
+ ...event,
50
+ sequence: sequence++,
51
+ rootSessionId
52
+ } as Inspection.Event)
53
+ },
54
+ close: PubSub.shutdown(pubsub)
55
+ }
56
+ })
57
+
58
+ export const endpoint = (
59
+ target: { readonly id: string; readonly sessionId: string }
60
+ ): Inspection.Endpoint => ({
61
+ id: target.id,
62
+ sessionId: target.sessionId
63
+ })
64
+
65
+ const commands = (values: ReadonlyArray<unknown>): ReadonlyArray<Inspection.Command> =>
66
+ values.flatMap((command): ReadonlyArray<Inspection.Command> => {
67
+ if (typeof command !== "object" || command === null || !("_tag" in command)) return []
68
+ if (command._tag === "SendTo" && "target" in command && "event" in command) {
69
+ const target = command.target
70
+ return typeof target === "object" && target !== null && "id" in target
71
+ ? [{
72
+ _tag: "SendTo",
73
+ target: "sessionId" in target ? endpoint(target as any) : { id: String(target.id) },
74
+ event: command.event
75
+ }]
76
+ : []
77
+ }
78
+ if (command._tag === "Stop" && "child" in command) {
79
+ const child = command.child
80
+ if (typeof child === "string") return [{ _tag: "Stop", target: { id: child } }]
81
+ return typeof child === "object" && child !== null && "id" in child
82
+ ? [{ _tag: "Stop", target: "sessionId" in child ? endpoint(child as any) : { id: String(child.id) } }]
83
+ : []
84
+ }
85
+ return []
86
+ })
87
+
88
+ export const microsteps = (plan: unknown): ReadonlyArray<Inspection.Microstep> => {
89
+ if (typeof plan !== "object" || plan === null || !("microsteps" in plan) || !Array.isArray(plan.microsteps)) {
90
+ return []
91
+ }
92
+ return plan.microsteps.map((step: any) => ({
93
+ event: step.event,
94
+ transitions: Array.isArray(step.transitions) ? step.transitions : [],
95
+ raisedEvents: Array.isArray(step.raisedEvents) ? step.raisedEvents : [],
96
+ emittedEvents: Array.isArray(step.emittedEvents) ? step.emittedEvents : [],
97
+ commands: commands(Array.isArray(step.commands) ? step.commands : []),
98
+ exitPaths: Array.isArray(step.exitPaths) ? step.exitPaths : [],
99
+ entryPaths: Array.isArray(step.entryPaths) ? step.entryPaths : [],
100
+ changed: step.changed === true
101
+ }))
102
+ }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import * as Cause from "effect/Cause"
8
8
  import * as Effect from "effect/Effect"
9
- import type { ChildMachine, Logic, Machine } from "../../Machine.js"
9
+ import type { ChildMachine, Inspection, Logic, Machine } from "../../Machine.js"
10
10
  import * as Configuration from "./configuration.js"
11
11
  import { InfiniteTransitionError, MachineSchemaDecodeError, StoppedError } from "./errors.js"
12
12
  import * as InvocationEvent from "./invocationEvent.js"
@@ -23,6 +23,7 @@ export interface AnyConfig {
23
23
  readonly onDone?: unknown
24
24
  readonly onFailure?: unknown
25
25
  readonly onSnapshot?: unknown
26
+ readonly activityKind?: Inspection.Activity["kind"]
26
27
  }
27
28
 
28
29
  /** @internal */
@@ -47,7 +48,7 @@ const resolveOne = (
47
48
  return {
48
49
  id: String(raw.id),
49
50
  src: () =>
50
- oneShot(resolveValue(raw.effect, context) as Effect.Effect<any, any, any>) as unknown as Runtime.ProcessLogic<
51
+ oneShot(raw.effect(context) as Effect.Effect<any, any, any>) as unknown as Runtime.ProcessLogic<
51
52
  any,
52
53
  any,
53
54
  any,
@@ -57,7 +58,8 @@ const resolveOne = (
57
58
  >,
58
59
  onDone: raw.onDone,
59
60
  onFailure: raw.onFailure,
60
- onSnapshot: raw.onSnapshot
61
+ onSnapshot: raw.onSnapshot,
62
+ activityKind: "Effect"
61
63
  }
62
64
  }
63
65
  if ("after" in raw) {
@@ -72,7 +74,8 @@ const resolveOne = (
72
74
  any,
73
75
  any
74
76
  >,
75
- onDone: raw.onDone
77
+ onDone: raw.onDone,
78
+ activityKind: "Timer"
76
79
  }
77
80
  }
78
81
  if ("logic" in raw) {
@@ -115,8 +118,18 @@ const runSequentialDiscard = <E, R>(
115
118
 
116
119
  const sendLifecycle = (
117
120
  scope: Runtime.ProcessScope<any>,
118
- event: InvocationEvent.InvocationEvent
119
- ): Effect.Effect<void> => scope.self.send(event).pipe(Effect.catchTag("StoppedError", () => Effect.void))
121
+ event: InvocationEvent.InvocationEvent,
122
+ activitySessionId?: string
123
+ ): Effect.Effect<void> =>
124
+ (activitySessionId === undefined
125
+ ? scope.self.send(event)
126
+ : scope.self.sendInspected === undefined
127
+ ? scope.self.send(event)
128
+ : scope.self.sendInspected(
129
+ event,
130
+ scope.self.inspectionSubject,
131
+ { _tag: "Activity", activitySessionId }
132
+ )).pipe(Effect.catchTag("StoppedError", () => Effect.void))
120
133
 
121
134
  const isFrameworkFailure = (error: unknown): boolean =>
122
135
  error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError || error instanceof StoppedError
@@ -131,7 +144,8 @@ const startResolved = (
131
144
  src: () => Runtime.ProcessLogic<any, any, any, any, any, any>,
132
145
  onDone: unknown,
133
146
  onFailure: unknown,
134
- onSnapshot: unknown
147
+ onSnapshot: unknown,
148
+ activityKind?: Inspection.Activity["kind"]
135
149
  ): Effect.Effect<void, any, any> =>
136
150
  Effect.suspend(() => {
137
151
  const key = makeKey(path, invokeId)
@@ -141,8 +155,9 @@ const startResolved = (
141
155
  id: childId,
142
156
  duplicateId: invokeId,
143
157
  ...(descriptor === undefined ? undefined : { descriptor }),
158
+ ...(activityKind === undefined ? undefined : { activityKind }),
144
159
  sendParent: (isCurrent, event) => isCurrent() ? scope.self.send(event) : Effect.void,
145
- onOutcome: (isCurrent, outcome) => {
160
+ onOutcome: (isCurrent, outcome, activitySessionId) => {
146
161
  if (outcome._tag === "Stopped" || !isCurrent()) return Effect.void
147
162
  if (outcome._tag === "Done") {
148
163
  if (onDone === undefined) {
@@ -152,14 +167,22 @@ const startResolved = (
152
167
  )
153
168
  ))
154
169
  }
155
- return sendLifecycle(scope, InvocationEvent.done(path, invokeId, outcome.output))
170
+ return sendLifecycle(
171
+ scope,
172
+ InvocationEvent.done(path, invokeId, outcome.output),
173
+ activityKind === undefined ? undefined : activitySessionId
174
+ )
156
175
  }
157
176
  if (
158
177
  outcome._tag === "Failure" &&
159
178
  onFailure !== undefined &&
160
179
  (descriptor === undefined || !isFrameworkFailure(outcome.error))
161
180
  ) {
162
- return sendLifecycle(scope, InvocationEvent.failure(path, invokeId, outcome.error))
181
+ return sendLifecycle(
182
+ scope,
183
+ InvocationEvent.failure(path, invokeId, outcome.error),
184
+ activityKind === undefined ? undefined : activitySessionId
185
+ )
163
186
  }
164
187
  return scope.failCause(outcome.cause)
165
188
  },
@@ -189,7 +212,8 @@ const start = (
189
212
  config.src,
190
213
  config.onDone,
191
214
  config.onFailure,
192
- config.onSnapshot
215
+ config.onSnapshot,
216
+ config.activityKind
193
217
  )
194
218
  }
195
219
 
@@ -213,7 +237,8 @@ const startStaticChild = (
213
237
  descriptor[ChildMachineLogicTypeId],
214
238
  raw.onDone,
215
239
  raw.onFailure,
216
- raw.onSnapshot
240
+ raw.onSnapshot,
241
+ undefined
217
242
  )
218
243
  }
219
244