@typeonce/effect-machine 0.15.0 → 0.16.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 (58) hide show
  1. package/README.md +31 -3
  2. package/dist/Machine.d.ts +258 -88
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +33 -33
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +2 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +4 -0
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  11. package/dist/internal/machine/executionPlan.js +6 -1
  12. package/dist/internal/machine/executionPlan.js.map +1 -1
  13. package/dist/internal/machine/invocation.d.ts +1 -1
  14. package/dist/internal/machine/invocation.d.ts.map +1 -1
  15. package/dist/internal/machine/invocation.js +25 -3
  16. package/dist/internal/machine/invocation.js.map +1 -1
  17. package/dist/internal/machine/invocationEvent.d.ts +8 -0
  18. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  19. package/dist/internal/machine/invocationEvent.js +8 -0
  20. package/dist/internal/machine/invocationEvent.js.map +1 -1
  21. package/dist/internal/machine/machine.d.ts.map +1 -1
  22. package/dist/internal/machine/machine.js +119 -48
  23. package/dist/internal/machine/machine.js.map +1 -1
  24. package/dist/internal/machine/planner.d.ts +7 -0
  25. package/dist/internal/machine/planner.d.ts.map +1 -1
  26. package/dist/internal/machine/planner.js +20 -11
  27. package/dist/internal/machine/planner.js.map +1 -1
  28. package/dist/internal/machine/runtime.d.ts +1 -0
  29. package/dist/internal/machine/runtime.d.ts.map +1 -1
  30. package/dist/internal/machine/runtime.js +25 -16
  31. package/dist/internal/machine/runtime.js.map +1 -1
  32. package/dist/internal/machine/topology.d.ts +11 -0
  33. package/dist/internal/machine/topology.d.ts.map +1 -1
  34. package/dist/internal/machine/topology.js +17 -6
  35. package/dist/internal/machine/topology.js.map +1 -1
  36. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
  37. package/dist/internal/testing/machine/transitionCoverage.js +6 -2
  38. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
  39. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  40. package/dist/internal/testing/machine/verification.js +6 -0
  41. package/dist/internal/testing/machine/verification.js.map +1 -1
  42. package/dist/testing/MachineTest.d.ts +2 -1
  43. package/dist/testing/MachineTest.d.ts.map +1 -1
  44. package/dist/testing/MachineTest.js.map +1 -1
  45. package/docs/agent-guide.md +63 -37
  46. package/package.json +1 -1
  47. package/src/Machine.ts +592 -159
  48. package/src/internal/machine/activities.ts +7 -0
  49. package/src/internal/machine/executionPlan.ts +6 -1
  50. package/src/internal/machine/invocation.ts +39 -4
  51. package/src/internal/machine/invocationEvent.ts +16 -0
  52. package/src/internal/machine/machine.ts +160 -47
  53. package/src/internal/machine/planner.ts +17 -3
  54. package/src/internal/machine/runtime.ts +61 -25
  55. package/src/internal/machine/topology.ts +31 -2
  56. package/src/internal/testing/machine/transitionCoverage.ts +6 -2
  57. package/src/internal/testing/machine/verification.ts +11 -0
  58. package/src/testing/MachineTest.ts +2 -0
@@ -22,6 +22,9 @@ export type StaticActivityMetadata =
22
22
  readonly type: "timer"
23
23
  readonly duration: string | "dynamic"
24
24
  }
25
+ | {
26
+ readonly type: "stream"
27
+ }
25
28
  | {
26
29
  readonly type: "machine"
27
30
  readonly child: {
@@ -94,6 +97,10 @@ const appendStaticDefinition = (
94
97
  })
95
98
  return
96
99
  }
100
+ if (Reflect.has(descriptor, "stream")) {
101
+ definitions.push({ source, id, type: "stream" })
102
+ return
103
+ }
97
104
  if (Reflect.has(descriptor, "logic")) {
98
105
  definitions.push({ source, id, type: "process" })
99
106
  }
@@ -445,11 +445,12 @@ const collectIndexedTransition = (
445
445
  }
446
446
  }
447
447
  const evaluated = evaluate === undefined
448
- ? { result: transition(context, enqueue), branchIndex: 0 }
448
+ ? { result: transition(context, enqueue), branchIndex: 0, branchKey: undefined }
449
449
  : evaluate(context, enqueue)
450
450
  return {
451
451
  state: isNoTarget(evaluated.result) ? undefined : evaluated.result,
452
452
  branchIndex: evaluated.branchIndex,
453
+ branchKey: evaluated.branchKey,
453
454
  commands: commands ?? emptyExecutionValues,
454
455
  raisedEvents: raisedEvents ?? emptyExecutionValues,
455
456
  emittedEvents: emittedEvents ?? emptyExecutionValues
@@ -569,6 +570,7 @@ const collectIndexedEvaluatedTransition = (
569
570
  return {
570
571
  selection,
571
572
  branchIndex: transitionResult.branchIndex,
573
+ branchKey: transitionResult.branchKey,
572
574
  unresolvedTarget: unresolvedTarget as any,
573
575
  target: target as any,
574
576
  next,
@@ -593,6 +595,7 @@ const collectIndexedEvaluatedTransition = (
593
595
  return {
594
596
  selection,
595
597
  branchIndex: transitionResult.branchIndex,
598
+ branchKey: transitionResult.branchKey,
596
599
  unresolvedTarget: unresolvedTarget as any,
597
600
  target: target as any,
598
601
  next,
@@ -619,6 +622,7 @@ const indexedMicrostep = (
619
622
  trigger: transition.selection.trigger,
620
623
  reenter: transition.selection.transition.reenter,
621
624
  branchIndex: transition.branchIndex,
625
+ branchKey: transition.branchKey,
622
626
  target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
623
627
  resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
624
628
  })
@@ -804,6 +808,7 @@ const planIndexedFlatState = (
804
808
  trigger: { type: "event", event: event._tag },
805
809
  reenter: transition.reenter,
806
810
  branchIndex: transitionResult.branchIndex,
811
+ branchKey: transitionResult.branchKey,
807
812
  target: target === undefined ? undefined : getTargetNodePath(target as any),
808
813
  resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any)
809
814
  }]
@@ -6,12 +6,13 @@
6
6
 
7
7
  import * as Cause from "effect/Cause"
8
8
  import * as Effect from "effect/Effect"
9
+ import * as Stream from "effect/Stream"
9
10
  import type { ChildMachine, Inspection, Logic, Machine } from "../../Machine.js"
10
11
  import * as Configuration from "./configuration.js"
11
12
  import { InfiniteTransitionError, MachineSchemaDecodeError, StoppedError } from "./errors.js"
12
13
  import * as InvocationEvent from "./invocationEvent.js"
13
14
  import * as Planner from "./planner.js"
14
- import type * as Runtime from "./runtime.js"
15
+ import * as Runtime from "./runtime.js"
15
16
  import { ChildMachineLogicTypeId } from "./symbols.js"
16
17
 
17
18
  /** @internal */
@@ -37,12 +38,35 @@ const oneShot = (effect: Effect.Effect<any, any, any>): Logic<void, never, any,
37
38
  run: () => effect
38
39
  })
39
40
 
41
+ const streamLogic = (
42
+ stream: Stream.Stream<any, any, any>,
43
+ path: string,
44
+ id: string
45
+ ): Runtime.ProcessLogic<void, never, any, any, void> => ({
46
+ initial: () => Effect.void,
47
+ run: ({ parent }) => {
48
+ const send = parent?.[Runtime.acknowledgedSend]
49
+ if (send === undefined) {
50
+ return Effect.die(new Error("Stream invocation requires an acknowledged parent machine"))
51
+ }
52
+ return Stream.runForEach(
53
+ stream,
54
+ (element) =>
55
+ send(InvocationEvent.element(path, id, element)).pipe(
56
+ Effect.asVoid,
57
+ Effect.catchCause(() => Effect.interrupt)
58
+ )
59
+ )
60
+ }
61
+ })
62
+
40
63
  const resolveValue = (value: unknown, context: Machine.InvokeContext<any, any, any, any>): unknown =>
41
64
  typeof value === "function" ? value(context) : value
42
65
 
43
66
  const resolveOne = (
44
67
  raw: Record<PropertyKey, any>,
45
- context: Machine.InvokeContext<any, any, any, any>
68
+ context: Machine.InvokeContext<any, any, any, any>,
69
+ path: string
46
70
  ): AnyConfig => {
47
71
  if ("effect" in raw) {
48
72
  return {
@@ -78,6 +102,17 @@ const resolveOne = (
78
102
  activityKind: "Timer"
79
103
  }
80
104
  }
105
+ if ("stream" in raw) {
106
+ const id = String(raw.id)
107
+ return {
108
+ id,
109
+ src: () =>
110
+ streamLogic(raw.stream(context), path, id) as unknown as Runtime.ProcessLogic<any, any, any, any, any, any>,
111
+ onDone: raw.onDone,
112
+ onFailure: raw.onFailure,
113
+ activityKind: "Stream"
114
+ }
115
+ }
81
116
  if ("logic" in raw) {
82
117
  return {
83
118
  id: String(raw.id),
@@ -103,7 +138,7 @@ const resolveOne = (
103
138
  onSnapshot: raw.onSnapshot
104
139
  }
105
140
  }
106
- throw new Error("Machine invoke must define exactly one of effect, after, logic, or child")
141
+ throw new Error("Machine invoke must define exactly one of effect, stream, after, logic, or child")
107
142
  }
108
143
 
109
144
  /** @internal */
@@ -269,7 +304,7 @@ export const startAll = (
269
304
  return InvocationEvent.definitions(Configuration.getStateConfigByPath(machine, path)?.invoke).map((definition) =>
270
305
  "child" in definition && !("input" in definition)
271
306
  ? startStaticChild(scope, ownedChildren, path, definition)
272
- : start(scope, ownedChildren, path, resolveOne(definition, context))
307
+ : start(scope, ownedChildren, path, resolveOne(definition, context, path))
273
308
  )
274
309
  })
275
310
  return effects.length === 0 ? undefined : runSequentialDiscard(effects)
@@ -10,6 +10,13 @@ export const InvocationEventTypeId: unique symbol = Symbol("effect/Machine/Invoc
10
10
 
11
11
  /** @internal */
12
12
  export type InvocationEvent =
13
+ | {
14
+ readonly [InvocationEventTypeId]: true
15
+ readonly path: string
16
+ readonly id: string
17
+ readonly type: "element"
18
+ readonly element: unknown
19
+ }
13
20
  | {
14
21
  readonly [InvocationEventTypeId]: true
15
22
  readonly path: string
@@ -41,6 +48,15 @@ export const done = (path: string, id: string, output: unknown): InvocationEvent
41
48
  output
42
49
  })
43
50
 
51
+ /** @internal */
52
+ export const element = (path: string, id: string, value: unknown): InvocationEvent => ({
53
+ [InvocationEventTypeId]: true,
54
+ path,
55
+ id,
56
+ type: "element",
57
+ element: value
58
+ })
59
+
44
60
  /** @internal */
45
61
  export const failure = (path: string, id: string, error: unknown): InvocationEvent => ({
46
62
  [InvocationEventTypeId]: true,
@@ -137,8 +137,6 @@ const makeWithHandlers = (
137
137
  }
138
138
 
139
139
  type DefinitionBranch = {
140
- readonly title?: string
141
- readonly when?: (context: any) => Option.Option<unknown>
142
140
  readonly target: (selector: unknown) => unknown
143
141
  readonly resolve?: (context: any, enqueue: unknown) => unknown
144
142
  }
@@ -147,6 +145,17 @@ type CapturedBranch = DefinitionBranch & {
147
145
  readonly selection: Topology.TargetSelection
148
146
  }
149
147
 
148
+ type BranchDeclaration = {
149
+ readonly title?: string
150
+ readonly target: unknown
151
+ }
152
+
153
+ type CapturedNamedBranch = {
154
+ readonly key: string
155
+ readonly title: string
156
+ readonly selection: Topology.TargetSelection
157
+ }
158
+
150
159
  const transitionTargetSelection = (
151
160
  selection: Topology.TargetSelection
152
161
  ): Machine.TransitionTargetSelection =>
@@ -336,20 +345,134 @@ const runCapturedBranch = (
336
345
  context: Record<string, any>,
337
346
  enqueue: unknown,
338
347
  stateNodes: Machine.StateNodes,
339
- source: string,
340
- match?: { readonly value: unknown }
348
+ source: string
341
349
  ): unknown => {
342
350
  const selectedTarget = getSelectionBuilder(context.target, branch.selection, stateNodes, source)
343
351
  if (branch.resolve === undefined) return constructSelectedTarget(selectedTarget)
344
352
  const resolverContext = { ...context }
345
353
  if (branch.selection.kind === "none") delete resolverContext.target
346
354
  else resolverContext.target = selectedTarget
347
- if (match !== undefined) resolverContext.match = match.value
348
355
  const resolved = branch.resolve(resolverContext, enqueue)
349
356
  validateResolvedSelection(resolved, branch.selection, stateNodes)
350
357
  return resolved === undefined ? constructSelectedTarget(selectedTarget) : resolved
351
358
  }
352
359
 
360
+ const isArrayIndexKey = (key: string): boolean => {
361
+ const index = Number(key)
362
+ return Number.isInteger(index) && index >= 0 && index < 0xffff_ffff && String(index) === key
363
+ }
364
+
365
+ const captureNamedBranches = (
366
+ declarations: unknown,
367
+ path: string,
368
+ trigger: PropertyKey
369
+ ): ReadonlyArray<CapturedNamedBranch> => {
370
+ if (typeof declarations !== "object" || declarations === null || Array.isArray(declarations)) {
371
+ throw new Error(`Machine branching transition for state "${path}" on "${String(trigger)}" requires a branch record`)
372
+ }
373
+ if (Object.getOwnPropertySymbols(declarations).length > 0) {
374
+ throw new Error(`Machine branching transition for state "${path}" on "${String(trigger)}" cannot use symbol keys`)
375
+ }
376
+ const keys = Object.keys(declarations)
377
+ if (keys.length === 0) {
378
+ throw new Error(`Machine branching transition for state "${path}" on "${String(trigger)}" requires a branch`)
379
+ }
380
+ return Object.freeze(keys.map((key) => {
381
+ if (key.length === 0 || isArrayIndexKey(key)) {
382
+ throw new Error(
383
+ `Machine branching transition for state "${path}" on "${String(trigger)}" requires non-index string branch keys`
384
+ )
385
+ }
386
+ const declaration = (declarations as Record<string, unknown>)[key]
387
+ if (typeof declaration !== "object" || declaration === null || !hasProperty(declaration, "target")) {
388
+ throw new Error(`Machine transition branch "${key}" requires a target selection`)
389
+ }
390
+ const { target, title } = declaration as BranchDeclaration
391
+ if (!Topology.isTargetSelection(target)) {
392
+ throw new Error(`Machine transition branch "${key}" must select exactly one target`)
393
+ }
394
+ if (title !== undefined && (typeof title !== "string" || title.length === 0)) {
395
+ throw new Error(`Machine transition branch "${key}" title must be a non-empty string`)
396
+ }
397
+ return Object.freeze({ key, title: title ?? key, selection: target })
398
+ }))
399
+ }
400
+
401
+ const wrapSelectedBranchBuilder = (
402
+ builder: unknown,
403
+ owner: object,
404
+ branchIndex: number,
405
+ branchKey: string
406
+ ): unknown => {
407
+ if (typeof builder === "function") {
408
+ const wrapped = (...args: ReadonlyArray<unknown>) =>
409
+ Topology.makeSelectedBranch(owner, branchIndex, branchKey, builder(...args))
410
+ for (const property of Reflect.ownKeys(builder)) {
411
+ if (
412
+ property === "length" || property === "name" || property === "prototype" || property === "caller" ||
413
+ property === "arguments"
414
+ ) continue
415
+ const descriptor = Object.getOwnPropertyDescriptor(builder, property)
416
+ if (descriptor === undefined) continue
417
+ if ("value" in descriptor && typeof descriptor.value === "function") {
418
+ descriptor.value = wrapSelectedBranchBuilder(descriptor.value, owner, branchIndex, branchKey)
419
+ }
420
+ Object.defineProperty(wrapped, property, descriptor)
421
+ }
422
+ return wrapped
423
+ }
424
+ if (typeof builder === "object" && builder !== null) {
425
+ const wrapped: Record<PropertyKey, unknown> = {}
426
+ for (const property of Reflect.ownKeys(builder)) {
427
+ const descriptor = Object.getOwnPropertyDescriptor(builder, property)
428
+ if (descriptor === undefined) continue
429
+ if ("value" in descriptor && typeof descriptor.value === "function") {
430
+ descriptor.value = wrapSelectedBranchBuilder(descriptor.value, owner, branchIndex, branchKey)
431
+ }
432
+ Object.defineProperty(wrapped, property, descriptor)
433
+ }
434
+ return wrapped
435
+ }
436
+ throw new Error(`Machine could not construct transition branch "${branchKey}"`)
437
+ }
438
+
439
+ const makeBranchSelectors = (
440
+ context: Record<string, any>,
441
+ branches: ReadonlyArray<CapturedNamedBranch>,
442
+ owner: object,
443
+ stateNodes: Machine.StateNodes,
444
+ source: string
445
+ ): Readonly<Record<string, unknown>> => {
446
+ const select: Record<string, unknown> = Object.create(null)
447
+ for (let branchIndex = 0; branchIndex < branches.length; branchIndex++) {
448
+ const branch = branches[branchIndex]!
449
+ select[branch.key] = wrapSelectedBranchBuilder(
450
+ getSelectionBuilder(context.target, branch.selection, stateNodes, source),
451
+ owner,
452
+ branchIndex,
453
+ branch.key
454
+ )
455
+ }
456
+ return Object.freeze(select)
457
+ }
458
+
459
+ const validateSelectedBranchResult = (
460
+ result: unknown,
461
+ selection: Topology.TargetSelection,
462
+ stateNodes: Machine.StateNodes
463
+ ): void => {
464
+ if (selection.kind === "none") {
465
+ if (!Topology.isNoTarget(result)) {
466
+ throw new Error("Machine targetless branch must return its selected targetless builder")
467
+ }
468
+ return
469
+ }
470
+ if (result === undefined) {
471
+ throw new Error(`Machine transition branch selected "${selection.path}" without constructing its target`)
472
+ }
473
+ validateResolvedSelection(result, selection, stateNodes)
474
+ }
475
+
353
476
  const captureTransition = (
354
477
  transition: unknown,
355
478
  stateNodes: Machine.StateNodes,
@@ -362,64 +485,53 @@ const captureTransition = (
362
485
  const definition = transition as Record<PropertyKey, unknown>
363
486
  const selector = makeTargetSelector(stateNodes, path)
364
487
  const reenter = definition.reenter === true
365
- if (Array.isArray(definition.cases)) {
366
- const rawCases = definition.cases as ReadonlyArray<unknown>
367
- if (definition.cases.length === 0 || !hasProperty(definition, "otherwise")) {
488
+ if (hasProperty(definition, "branches")) {
489
+ const branching = definition as { readonly branches: unknown; readonly resolve?: unknown }
490
+ if (typeof branching.branches !== "function" || typeof branching.resolve !== "function") {
368
491
  throw new Error(
369
- `Machine conditional transition for state "${path}" on "${String(trigger)}" requires cases and otherwise`
492
+ `Machine branching transition for state "${path}" on "${String(trigger)}" requires branches and resolve`
370
493
  )
371
494
  }
372
- const cases = rawCases.map((branch) => {
373
- const captured = captureDefinitionBranch(branch, selector, path, trigger)
374
- if (typeof captured.title !== "string" || captured.title.length === 0 || typeof captured.when !== "function") {
495
+ const resolve = branching.resolve
496
+ const branches = captureNamedBranches(branching.branches(selector), path, trigger)
497
+ const owner = Object.freeze({})
498
+ const evaluate = (context: Record<string, any>, enqueue: unknown) => {
499
+ const resolverContext = { ...context }
500
+ delete resolverContext.target
501
+ resolverContext.select = makeBranchSelectors(context, branches, owner, stateNodes, path)
502
+ const selected = resolve(resolverContext, enqueue)
503
+ if (!Topology.isSelectedBranch(selected) || selected.owner !== owner) {
375
504
  throw new Error(
376
- `Machine conditional transition case for state "${path}" on "${String(trigger)}" requires title and when`
505
+ `Machine branching transition for state "${path}" on "${String(trigger)}" must select one declared branch`
377
506
  )
378
507
  }
379
- return captured
380
- })
381
- const otherwise = captureDefinitionBranch(definition.otherwise, selector, path, trigger)
382
- const evaluate = (context: Record<string, any>, enqueue: unknown) => {
383
- const predicateContext = { ...context }
384
- delete predicateContext.target
385
- for (let branchIndex = 0; branchIndex < cases.length; branchIndex++) {
386
- const branch = cases[branchIndex]!
387
- const result = branch.when!(predicateContext)
388
- if (!Option.isOption(result)) {
389
- throw new Error(`Machine conditional transition case "${branch.title}" must return Option`)
390
- }
391
- if (Option.isSome(result)) {
392
- return {
393
- result: runCapturedBranch(branch, context, enqueue, stateNodes, path, { value: result.value }),
394
- branchIndex
395
- }
396
- }
508
+ const branch = branches[selected.branchIndex]
509
+ if (branch === undefined || selected.branchKey !== branch.key) {
510
+ throw new Error(`Machine branching transition returned invalid branch evidence`)
397
511
  }
512
+ validateSelectedBranchResult(selected.result, branch.selection, stateNodes)
398
513
  return {
399
- result: runCapturedBranch(otherwise, context, enqueue, stateNodes, path),
400
- branchIndex: cases.length
514
+ result: selected.result,
515
+ branchIndex: selected.branchIndex,
516
+ branchKey: selected.branchKey
401
517
  }
402
518
  }
403
519
  return {
404
520
  reenter,
405
521
  targets: [
406
522
  ...new Set(
407
- [...cases, otherwise].flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
523
+ branches.flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
408
524
  )
409
525
  ],
410
- branches: [
411
- ...cases.map((branch) => ({
412
- type: "case" as const,
413
- title: branch.title!,
526
+ branches: branches.map((branch) =>
527
+ Object.freeze({
528
+ type: "branch" as const,
529
+ key: branch.key,
530
+ title: branch.title,
414
531
  target: branch.selection.path,
415
532
  selection: transitionTargetSelection(branch.selection)
416
- })),
417
- {
418
- type: "otherwise" as const,
419
- target: otherwise.selection.path,
420
- selection: transitionTargetSelection(otherwise.selection)
421
- }
422
- ],
533
+ })
534
+ ),
423
535
  evaluate,
424
536
  transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
425
537
  }
@@ -427,7 +539,8 @@ const captureTransition = (
427
539
  const branch = captureDefinitionBranch(transition, selector, path, trigger)
428
540
  const evaluate = (context: Record<string, any>, enqueue: unknown) => ({
429
541
  result: runCapturedBranch(branch, context, enqueue, stateNodes, path),
430
- branchIndex: 0
542
+ branchIndex: 0,
543
+ branchKey: undefined
431
544
  })
432
545
  return {
433
546
  reenter,
@@ -465,7 +578,7 @@ const captureInvokeDefinition = (
465
578
  if (Array.isArray(invoke)) return invoke.map((item) => captureInvokeDefinition(item, stateNodes, path))
466
579
  if (typeof invoke !== "object" || invoke === null) return invoke
467
580
  const captured = { ...(invoke as Record<PropertyKey, unknown>) }
468
- for (const key of ["onDone", "onFailure", "onSnapshot"] as const) {
581
+ for (const key of ["onElement", "onDone", "onFailure", "onSnapshot"] as const) {
469
582
  if (captured[key] !== undefined) {
470
583
  captured[key] = captureTransition(captured[key], stateNodes, path, key)
471
584
  }
@@ -71,6 +71,7 @@ export type MicrostepPlan<State, Event, E, R> = {
71
71
  readonly trigger: Machine.TransitionTrigger
72
72
  readonly reenter: boolean
73
73
  readonly branchIndex: number
74
+ readonly branchKey: string | undefined
74
75
  readonly target: string | undefined
75
76
  readonly resolvedTarget: string | undefined
76
77
  }>
@@ -108,6 +109,7 @@ export type TransitionHandler<States extends Machine.StateSchemas, E, R, Context
108
109
  type TransitionEvaluation<States extends Machine.StateSchemas, E, R> = {
109
110
  readonly result: Machine.HandlerResult<States, E, R>
110
111
  readonly branchIndex: number
112
+ readonly branchKey: string | undefined
111
113
  }
112
114
 
113
115
  type TransitionEvaluator<States extends Machine.StateSchemas, E, R, Context> = (
@@ -214,11 +216,12 @@ const collectTransition = <
214
216
  ) => {
215
217
  const collected = makeCollector<Event>(machine)
216
218
  const evaluated = evaluate === undefined
217
- ? { result: transition(context, collected.enqueue), branchIndex: 0 }
219
+ ? { result: transition(context, collected.enqueue), branchIndex: 0, branchKey: undefined }
218
220
  : evaluate(context, collected.enqueue)
219
221
  return {
220
222
  state: isNoTarget(evaluated.result) ? undefined : evaluated.result,
221
223
  branchIndex: evaluated.branchIndex,
224
+ branchKey: evaluated.branchKey,
222
225
  commands: collected.commands,
223
226
  raisedEvents: collected.raisedEvents,
224
227
  emittedEvents: collected.emittedEvents
@@ -523,6 +526,7 @@ export type SelectedTransition<States extends Machine.StateSchemas, E, R, Contex
523
526
  export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R, Context> = {
524
527
  readonly selection: SelectedTransition<States, E, R, Context>
525
528
  readonly branchIndex: number
529
+ readonly branchKey: string | undefined
526
530
  readonly unresolvedTarget:
527
531
  | Machine.Snapshot<States>
528
532
  | Machine.Target<States, Machine.StateIdentifier<States>>
@@ -544,6 +548,7 @@ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R
544
548
  readonly trigger: Machine.TransitionTrigger
545
549
  readonly reenter: false
546
550
  readonly branchIndex: number
551
+ readonly branchKey: string | undefined
547
552
  readonly target: string
548
553
  readonly resolvedTarget: string
549
554
  }>
@@ -952,7 +957,9 @@ const selectInvocationTransition = <
952
957
  return String(id) === event.id
953
958
  })
954
959
  if (invoke === undefined) return []
955
- const handler = event.type === "done"
960
+ const handler = event.type === "element"
961
+ ? invoke.onElement
962
+ : event.type === "done"
956
963
  ? invoke.onDone
957
964
  : event.type === "failure"
958
965
  ? invoke.onFailure
@@ -968,7 +975,9 @@ const selectInvocationTransition = <
968
975
  snapshot,
969
976
  target: getTargetBuilder(machine, event.path),
970
977
  id: event.id,
971
- ...(event.type === "done"
978
+ ...(event.type === "element"
979
+ ? { element: event.element }
980
+ : event.type === "done"
972
981
  ? { output: event.output }
973
982
  : event.type === "failure"
974
983
  ? { error: event.error }
@@ -1150,6 +1159,7 @@ interface ResolvedChoiceTransition {
1150
1159
  readonly trigger: Machine.TransitionTrigger
1151
1160
  readonly reenter: false
1152
1161
  readonly branchIndex: number
1162
+ readonly branchKey: string | undefined
1153
1163
  readonly target: string
1154
1164
  readonly resolvedTarget: string
1155
1165
  }
@@ -1228,6 +1238,7 @@ function resolveChoiceTarget(
1228
1238
  trigger: { type: "choice" },
1229
1239
  reenter: false,
1230
1240
  branchIndex: collected.branchIndex,
1241
+ branchKey: collected.branchKey,
1231
1242
  target: returnedPath,
1232
1243
  resolvedTarget: nested?.target.path ?? returnedPath
1233
1244
  })
@@ -1398,6 +1409,7 @@ const collectEvaluatedTransition = <
1398
1409
  return {
1399
1410
  selection,
1400
1411
  branchIndex: transitionResult.branchIndex,
1412
+ branchKey: transitionResult.branchKey,
1401
1413
  unresolvedTarget,
1402
1414
  target,
1403
1415
  commands: [
@@ -1444,6 +1456,7 @@ const collectEvaluatedTransition = <
1444
1456
  return {
1445
1457
  selection,
1446
1458
  branchIndex: transitionResult.branchIndex,
1459
+ branchKey: transitionResult.branchKey,
1447
1460
  unresolvedTarget,
1448
1461
  target,
1449
1462
  commands: [
@@ -1799,6 +1812,7 @@ const microstep = <
1799
1812
  trigger: transition.selection.trigger,
1800
1813
  reenter: transition.selection.transition.reenter,
1801
1814
  branchIndex: transition.branchIndex,
1815
+ branchKey: transition.branchKey,
1802
1816
  target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
1803
1817
  resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
1804
1818
  },