@typeonce/effect-machine 0.15.0 → 0.17.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 (85) hide show
  1. package/README.md +115 -95
  2. package/dist/Machine.d.ts +464 -337
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +72 -107
  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/atom.d.ts +7 -7
  11. package/dist/internal/machine/atom.d.ts.map +1 -1
  12. package/dist/internal/machine/atom.js.map +1 -1
  13. package/dist/internal/machine/cluster.d.ts +2 -2
  14. package/dist/internal/machine/cluster.d.ts.map +1 -1
  15. package/dist/internal/machine/cluster.js +6 -5
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  18. package/dist/internal/machine/executionPlan.js +16 -4
  19. package/dist/internal/machine/executionPlan.js.map +1 -1
  20. package/dist/internal/machine/invocation.d.ts +1 -1
  21. package/dist/internal/machine/invocation.d.ts.map +1 -1
  22. package/dist/internal/machine/invocation.js +25 -3
  23. package/dist/internal/machine/invocation.js.map +1 -1
  24. package/dist/internal/machine/invocationEvent.d.ts +8 -0
  25. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  26. package/dist/internal/machine/invocationEvent.js +8 -0
  27. package/dist/internal/machine/invocationEvent.js.map +1 -1
  28. package/dist/internal/machine/machine.d.ts +8 -6
  29. package/dist/internal/machine/machine.d.ts.map +1 -1
  30. package/dist/internal/machine/machine.js +308 -71
  31. package/dist/internal/machine/machine.js.map +1 -1
  32. package/dist/internal/machine/planner.d.ts +26 -4
  33. package/dist/internal/machine/planner.d.ts.map +1 -1
  34. package/dist/internal/machine/planner.js +76 -31
  35. package/dist/internal/machine/planner.js.map +1 -1
  36. package/dist/internal/machine/runtime.d.ts +1 -0
  37. package/dist/internal/machine/runtime.d.ts.map +1 -1
  38. package/dist/internal/machine/runtime.js +25 -16
  39. package/dist/internal/machine/runtime.js.map +1 -1
  40. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  41. package/dist/internal/machine/stateDefinition.js +14 -0
  42. package/dist/internal/machine/stateDefinition.js.map +1 -1
  43. package/dist/internal/machine/topology.d.ts +20 -0
  44. package/dist/internal/machine/topology.d.ts.map +1 -1
  45. package/dist/internal/machine/topology.js +33 -6
  46. package/dist/internal/machine/topology.js.map +1 -1
  47. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  48. package/dist/internal/testing/machine/finiteModel.js +15 -20
  49. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  50. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
  51. package/dist/internal/testing/machine/transitionCoverage.js +8 -2
  52. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
  53. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  54. package/dist/internal/testing/machine/verification.js +6 -0
  55. package/dist/internal/testing/machine/verification.js.map +1 -1
  56. package/dist/testing/MachineTest.d.ts +13 -12
  57. package/dist/testing/MachineTest.d.ts.map +1 -1
  58. package/dist/testing/MachineTest.js +5 -8
  59. package/dist/testing/MachineTest.js.map +1 -1
  60. package/dist/unstable/cluster/ClusterMachine.d.ts +2 -2
  61. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  62. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  63. package/dist/unstable/reactivity/AtomMachine.d.ts +12 -12
  64. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  65. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  66. package/docs/agent-guide.md +182 -159
  67. package/package.json +1 -1
  68. package/src/Machine.ts +1073 -700
  69. package/src/internal/machine/activities.ts +7 -0
  70. package/src/internal/machine/atom.ts +20 -15
  71. package/src/internal/machine/cluster.ts +14 -9
  72. package/src/internal/machine/executionPlan.ts +14 -4
  73. package/src/internal/machine/invocation.ts +39 -4
  74. package/src/internal/machine/invocationEvent.ts +16 -0
  75. package/src/internal/machine/machine.ts +460 -86
  76. package/src/internal/machine/planner.ts +106 -30
  77. package/src/internal/machine/runtime.ts +61 -25
  78. package/src/internal/machine/stateDefinition.ts +39 -0
  79. package/src/internal/machine/topology.ts +59 -2
  80. package/src/internal/testing/machine/finiteModel.ts +18 -21
  81. package/src/internal/testing/machine/transitionCoverage.ts +8 -2
  82. package/src/internal/testing/machine/verification.ts +11 -0
  83. package/src/testing/MachineTest.ts +16 -11
  84. package/src/unstable/cluster/ClusterMachine.ts +8 -4
  85. package/src/unstable/reactivity/AtomMachine.ts +19 -12
@@ -19,6 +19,8 @@ import type {
19
19
  MachineRef,
20
20
  MachineSchemaDecodeError,
21
21
  MachineSchemaEncodeError,
22
+ Parent,
23
+ ParentMode,
22
24
  Runtime,
23
25
  RuntimeOutcome,
24
26
  SpawnOptions,
@@ -49,6 +51,7 @@ export {
49
51
  export { ChildMachineLogicTypeId, InitialEventTypeId, SnapshotBuilderStateTypeId } from "./symbols.js"
50
52
 
51
53
  const TypeId = "~effect/Machine"
54
+ const ParentTypeId = "~effect/Machine/Parent"
52
55
  export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke")
53
56
  export const TransitionTypeId: unique symbol = Symbol.for("effect/Machine/Transition")
54
57
  const ChildMachineTypeId = "~effect/Machine/ChildMachine"
@@ -112,8 +115,6 @@ const Proto = {
112
115
  }
113
116
  }
114
117
 
115
- const makeBoundInvoke = (config: unknown): unknown => config
116
-
117
118
  const makeWithHandlers = (
118
119
  self: Definition.Any,
119
120
  handlers: Machine.StateConfigs<any, any, any, any, any, any, any>
@@ -123,7 +124,7 @@ const makeWithHandlers = (
123
124
  machine.events = self.events
124
125
  machine.internalEvents = self.internalEvents
125
126
  machine.emittedEvents = self.emittedEvents
126
- machine.parentEvents = self.parentEvents
127
+ machine.parent = self.parent
127
128
  machine.input = self.input
128
129
  machine.id = self.id
129
130
  machine.initial = self.initial
@@ -131,22 +132,214 @@ const makeWithHandlers = (
131
132
  machine.stateNodes = self.stateNodes
132
133
  machine.makeTargetBuilder = self.makeTargetBuilder
133
134
  machine.handlers = handlers
134
- machine.invoke = makeBoundInvoke
135
135
  Protocol.copyProtocol(self, machine)
136
136
  return machine
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
142
+ readonly declinable?: boolean
144
143
  }
145
144
 
146
145
  type CapturedBranch = DefinitionBranch & {
147
146
  readonly selection: Topology.TargetSelection
148
147
  }
149
148
 
149
+ type BranchDeclaration = {
150
+ readonly title?: string
151
+ readonly target: unknown
152
+ }
153
+
154
+ type CapturedNamedBranch = {
155
+ readonly key: string
156
+ readonly title: string
157
+ readonly selection: Topology.TargetSelection
158
+ }
159
+
160
+ const TransitionBuilderDescriptorTypeId: unique symbol = Symbol("effect/Machine/TransitionBuilderDescriptor")
161
+
162
+ type DirectTransitionDescriptor = {
163
+ readonly [TransitionBuilderDescriptorTypeId]: typeof TransitionBuilderDescriptorTypeId
164
+ readonly type: "direct"
165
+ readonly selection: Topology.TargetSelection
166
+ readonly resolve?: (context: any, enqueue: unknown) => unknown
167
+ readonly reenter: boolean
168
+ readonly declinable: boolean
169
+ }
170
+
171
+ type BranchesTransitionDescriptor = {
172
+ readonly [TransitionBuilderDescriptorTypeId]: typeof TransitionBuilderDescriptorTypeId
173
+ readonly type: "branches"
174
+ readonly declarations: unknown
175
+ readonly resolve: (context: any, enqueue: unknown) => unknown
176
+ readonly reenter: boolean
177
+ readonly declinable: boolean
178
+ }
179
+
180
+ type TransitionBuilderDescriptor = DirectTransitionDescriptor | BranchesTransitionDescriptor
181
+
182
+ const InitialBuilderDescriptorTypeId: unique symbol = Symbol("effect/Machine/InitialBuilderDescriptor")
183
+
184
+ type InitialBuilderDescriptor = {
185
+ readonly [InitialBuilderDescriptorTypeId]: typeof InitialBuilderDescriptorTypeId
186
+ readonly selection: Topology.TargetSelection
187
+ readonly resolve: (context: any) => unknown
188
+ }
189
+
190
+ const plainTargetSelection = (selection: Topology.TargetSelection): Topology.TargetSelection =>
191
+ selection.kind === "none"
192
+ ? Topology.noneTargetSelection
193
+ : Topology.makeTargetSelection(selection.kind, selection.path, selection.scope)
194
+
195
+ const transitionOptions = (options: unknown): { readonly reenter: boolean; readonly declinable: boolean } => {
196
+ const configuration = typeof options === "object" && options !== null
197
+ ? options as { readonly reenter?: unknown; readonly declinable?: unknown }
198
+ : {}
199
+ return {
200
+ reenter: configuration.reenter === true,
201
+ declinable: configuration.declinable === true
202
+ }
203
+ }
204
+
205
+ const makeDirectTransitionDescriptor = (
206
+ selection: Topology.TargetSelection,
207
+ resolve: ((context: any, enqueue: unknown) => unknown) | undefined,
208
+ options: unknown
209
+ ): DirectTransitionDescriptor => {
210
+ const shared: Omit<DirectTransitionDescriptor, "resolve"> = {
211
+ [TransitionBuilderDescriptorTypeId]: TransitionBuilderDescriptorTypeId,
212
+ type: "direct",
213
+ selection: plainTargetSelection(selection),
214
+ ...transitionOptions(options)
215
+ }
216
+ return resolve === undefined
217
+ ? Object.freeze(shared)
218
+ : Object.freeze({ ...shared, resolve })
219
+ }
220
+
221
+ const decorateTransitionSelection = (selection: Topology.TargetSelection): Topology.TargetSelection =>
222
+ Object.freeze({
223
+ ...selection,
224
+ resolve: (resolve: (context: any, enqueue: unknown) => unknown, options?: unknown) =>
225
+ makeDirectTransitionDescriptor(selection, resolve, options),
226
+ reenter: () => makeDirectTransitionDescriptor(selection, undefined, { reenter: true })
227
+ })
228
+
229
+ const noneTransitionSelection = decorateTransitionSelection(Topology.noneTargetSelection)
230
+
231
+ const makeInitialBuilderDescriptor = (
232
+ selection: Topology.TargetSelection,
233
+ resolve: (context: any) => unknown
234
+ ): InitialBuilderDescriptor =>
235
+ Object.freeze({
236
+ [InitialBuilderDescriptorTypeId]: InitialBuilderDescriptorTypeId,
237
+ selection: plainTargetSelection(selection),
238
+ resolve
239
+ })
240
+
241
+ const decorateInitialSelection = (selection: Topology.TargetSelection): Topology.TargetSelection =>
242
+ Object.freeze({
243
+ ...selection,
244
+ resolve: (resolve: (context: any) => unknown) => makeInitialBuilderDescriptor(selection, resolve)
245
+ })
246
+
247
+ const decorateInitialSelectorNode = (node: unknown): unknown => {
248
+ if (Topology.isTargetSelection(node)) return decorateInitialSelection(node)
249
+ if (typeof node === "function") {
250
+ const wrapped = ((...args: ReadonlyArray<unknown>) => decorateInitialSelection(node(...args))) as
251
+ & ((...args: ReadonlyArray<unknown>) => unknown)
252
+ & Record<string, unknown>
253
+ for (const key of Object.keys(node)) {
254
+ wrapped[key] = decorateInitialSelectorNode((node as unknown as Record<string, unknown>)[key])
255
+ }
256
+ return Object.freeze(wrapped)
257
+ }
258
+ if (typeof node === "object" && node !== null) {
259
+ const wrapped: Record<string, unknown> = {}
260
+ for (const key of Object.keys(node)) {
261
+ wrapped[key] = decorateInitialSelectorNode((node as Record<string, unknown>)[key])
262
+ }
263
+ return Object.freeze(wrapped)
264
+ }
265
+ return node
266
+ }
267
+
268
+ const decorateTransitionSelectorNode = (node: unknown): unknown => {
269
+ if (Topology.isTargetSelection(node)) {
270
+ return node === Topology.noneTargetSelection ? noneTransitionSelection : decorateTransitionSelection(node)
271
+ }
272
+ if (typeof node === "function") {
273
+ const wrapped = ((...args: ReadonlyArray<unknown>) => decorateTransitionSelection(node(...args))) as
274
+ & ((...args: ReadonlyArray<unknown>) => unknown)
275
+ & Record<string, unknown>
276
+ for (const key of Object.keys(node)) {
277
+ wrapped[key] = decorateTransitionSelectorNode((node as unknown as Record<string, unknown>)[key])
278
+ }
279
+ return Object.freeze(wrapped)
280
+ }
281
+ if (typeof node === "object" && node !== null) {
282
+ const wrapped: Record<string, unknown> = {}
283
+ for (const key of Object.keys(node)) {
284
+ wrapped[key] = decorateTransitionSelectorNode((node as Record<string, unknown>)[key])
285
+ }
286
+ return Object.freeze(wrapped)
287
+ }
288
+ return node
289
+ }
290
+
291
+ const makeTransitionSelector = (
292
+ stateNodes: Machine.StateNodes,
293
+ source: string
294
+ ): unknown => {
295
+ const selector = {
296
+ ...decorateTransitionSelectorNode(makeTargetSelector(stateNodes, source)) as Record<string, unknown>
297
+ }
298
+ selector.branches = (declarations: unknown) =>
299
+ Object.freeze({
300
+ resolve: (
301
+ resolve: (context: any, enqueue: unknown) => unknown,
302
+ options?: unknown
303
+ ): BranchesTransitionDescriptor =>
304
+ Object.freeze({
305
+ [TransitionBuilderDescriptorTypeId]: TransitionBuilderDescriptorTypeId,
306
+ type: "branches",
307
+ declarations,
308
+ resolve,
309
+ ...transitionOptions(options)
310
+ })
311
+ })
312
+ return Object.freeze(selector)
313
+ }
314
+
315
+ const normalizeTransitionBuilder = (
316
+ transition: (selector: unknown) => unknown,
317
+ stateNodes: Machine.StateNodes,
318
+ path: string
319
+ ): unknown => {
320
+ const result = transition(makeTransitionSelector(stateNodes, path))
321
+ if (Topology.isTargetSelection(result)) {
322
+ const selection = plainTargetSelection(result)
323
+ return { target: () => selection }
324
+ }
325
+ if (!hasProperty(result, TransitionBuilderDescriptorTypeId)) return result
326
+ const descriptor = result as TransitionBuilderDescriptor
327
+ if (descriptor.type === "branches") {
328
+ return {
329
+ branches: () => descriptor.declarations,
330
+ resolve: descriptor.resolve,
331
+ reenter: descriptor.reenter,
332
+ declinable: descriptor.declinable
333
+ }
334
+ }
335
+ return {
336
+ target: () => descriptor.selection,
337
+ resolve: descriptor.resolve,
338
+ reenter: descriptor.reenter,
339
+ declinable: descriptor.declinable
340
+ }
341
+ }
342
+
150
343
  const transitionTargetSelection = (
151
344
  selection: Topology.TargetSelection
152
345
  ): Machine.TransitionTargetSelection =>
@@ -163,6 +356,12 @@ const makeSelectionMethod = (
163
356
  ): () => Topology.TargetSelection =>
164
357
  () => Topology.makeTargetSelection(kind, path, scope)
165
358
 
359
+ const makeSelectionValue = (
360
+ kind: Topology.TargetSelectionKind,
361
+ path: string | undefined,
362
+ scope: Topology.TargetSelectionScope
363
+ ): Topology.TargetSelection => Topology.makeTargetSelection(kind, path, scope)
364
+
166
365
  const addSelectionChildren = (
167
366
  builder: Record<string, unknown>,
168
367
  stateNodes: Machine.StateNodes,
@@ -185,7 +384,7 @@ const makeSelectionNode = (
185
384
  const method = makeSelectionMethod(kind, path, scope) as unknown as Record<string, unknown>
186
385
  if (node.type !== "atomic" && node.type !== "final" && node.type !== "choice" && node.type !== "history") {
187
386
  Object.defineProperty(method, "initial", {
188
- value: makeSelectionMethod("initial", path, scope),
387
+ value: makeSelectionValue("initial", path, scope),
189
388
  enumerable: true
190
389
  })
191
390
  if (scope === "local" || scope === "branch") {
@@ -203,7 +402,7 @@ const makeHistorySelectionTree = (
203
402
  for (const node of stateNodes.byPath.values()) {
204
403
  if (node.parent !== parent) continue
205
404
  if (node.type === "history") {
206
- builder[node.key] = makeSelectionMethod("history", node.path, "full")
405
+ builder[node.key] = makeSelectionValue("history", node.path, "full")
207
406
  } else if (node.type !== "choice") {
208
407
  const children = makeHistorySelectionTree(stateNodes, node.path)
209
408
  if (Object.keys(children).length > 0) builder[node.key] = children
@@ -230,12 +429,12 @@ const makeTargetSelector = (
230
429
  if (localScope !== undefined) {
231
430
  const localScopeNode = getTargetBuilderNode(stateNodes, localScope)
232
431
  if (localScopeNode.schema !== undefined) {
233
- local.with = makeSelectionMethod("state", localScope, "local")
432
+ local.with = makeSelectionValue("state", localScope, "local")
234
433
  }
235
434
  addSelectionChildren(local, stateNodes, localScope, "local")
236
435
  }
237
436
  return {
238
- none: makeSelectionMethod("none", undefined, "local"),
437
+ none: Topology.noneTargetSelection,
239
438
  local,
240
439
  branch,
241
440
  full,
@@ -300,7 +499,8 @@ const getSelectionBuilder = (
300
499
  return builder
301
500
  }
302
501
 
303
- const constructSelectedTarget = (builder: any): unknown => typeof builder === "function" ? builder() : builder.from()
502
+ const constructSelectedTarget = (builder: any): unknown =>
503
+ typeof builder?.from === "function" ? builder.from() : builder()
304
504
 
305
505
  const validateResolvedSelection = (
306
506
  result: unknown,
@@ -336,90 +536,219 @@ const runCapturedBranch = (
336
536
  context: Record<string, any>,
337
537
  enqueue: unknown,
338
538
  stateNodes: Machine.StateNodes,
339
- source: string,
340
- match?: { readonly value: unknown }
539
+ source: string
341
540
  ): unknown => {
342
541
  const selectedTarget = getSelectionBuilder(context.target, branch.selection, stateNodes, source)
343
542
  if (branch.resolve === undefined) return constructSelectedTarget(selectedTarget)
344
543
  const resolverContext = { ...context }
345
544
  if (branch.selection.kind === "none") delete resolverContext.target
346
545
  else resolverContext.target = selectedTarget
347
- if (match !== undefined) resolverContext.match = match.value
546
+ if (branch.declinable === true) resolverContext.decline = Topology.makeDeclined
348
547
  const resolved = branch.resolve(resolverContext, enqueue)
548
+ if (Topology.isDeclined(resolved)) {
549
+ if (branch.declinable !== true) {
550
+ throw new Error(`Machine transition for state "${source}" returned decline without declaring declinable: true`)
551
+ }
552
+ return resolved
553
+ }
349
554
  validateResolvedSelection(resolved, branch.selection, stateNodes)
350
555
  return resolved === undefined ? constructSelectedTarget(selectedTarget) : resolved
351
556
  }
352
557
 
558
+ const isArrayIndexKey = (key: string): boolean => {
559
+ const index = Number(key)
560
+ return Number.isInteger(index) && index >= 0 && index < 0xffff_ffff && String(index) === key
561
+ }
562
+
563
+ const captureNamedBranches = (
564
+ declarations: unknown,
565
+ path: string,
566
+ trigger: PropertyKey
567
+ ): ReadonlyArray<CapturedNamedBranch> => {
568
+ if (typeof declarations !== "object" || declarations === null || Array.isArray(declarations)) {
569
+ throw new Error(`Machine branching transition for state "${path}" on "${String(trigger)}" requires a branch record`)
570
+ }
571
+ if (Object.getOwnPropertySymbols(declarations).length > 0) {
572
+ throw new Error(`Machine branching transition for state "${path}" on "${String(trigger)}" cannot use symbol keys`)
573
+ }
574
+ const keys = Object.keys(declarations)
575
+ if (keys.length === 0) {
576
+ throw new Error(`Machine branching transition for state "${path}" on "${String(trigger)}" requires a branch`)
577
+ }
578
+ return Object.freeze(keys.map((key) => {
579
+ if (key.length === 0 || isArrayIndexKey(key)) {
580
+ throw new Error(
581
+ `Machine branching transition for state "${path}" on "${String(trigger)}" requires non-index string branch keys`
582
+ )
583
+ }
584
+ const declaration = (declarations as Record<string, unknown>)[key]
585
+ if (typeof declaration !== "object" || declaration === null || !hasProperty(declaration, "target")) {
586
+ throw new Error(`Machine transition branch "${key}" requires a target selection`)
587
+ }
588
+ const { target, title } = declaration as BranchDeclaration
589
+ if (!Topology.isTargetSelection(target)) {
590
+ throw new Error(`Machine transition branch "${key}" must select exactly one target`)
591
+ }
592
+ if (title !== undefined && (typeof title !== "string" || title.length === 0)) {
593
+ throw new Error(`Machine transition branch "${key}" title must be a non-empty string`)
594
+ }
595
+ return Object.freeze({ key, title: title ?? key, selection: plainTargetSelection(target) })
596
+ }))
597
+ }
598
+
599
+ const wrapSelectedBranchBuilder = (
600
+ builder: unknown,
601
+ owner: object,
602
+ branchIndex: number,
603
+ branchKey: string
604
+ ): unknown => {
605
+ if (typeof builder === "function") {
606
+ const wrapped = (...args: ReadonlyArray<unknown>) =>
607
+ Topology.makeSelectedBranch(owner, branchIndex, branchKey, builder(...args))
608
+ for (const property of Reflect.ownKeys(builder)) {
609
+ if (
610
+ property === "length" || property === "name" || property === "prototype" || property === "caller" ||
611
+ property === "arguments"
612
+ ) continue
613
+ const descriptor = Object.getOwnPropertyDescriptor(builder, property)
614
+ if (descriptor === undefined) continue
615
+ if ("value" in descriptor && typeof descriptor.value === "function") {
616
+ descriptor.value = wrapSelectedBranchBuilder(descriptor.value, owner, branchIndex, branchKey)
617
+ }
618
+ Object.defineProperty(wrapped, property, descriptor)
619
+ }
620
+ return wrapped
621
+ }
622
+ if (typeof builder === "object" && builder !== null) {
623
+ const wrapped: Record<PropertyKey, unknown> = {}
624
+ for (const property of Reflect.ownKeys(builder)) {
625
+ const descriptor = Object.getOwnPropertyDescriptor(builder, property)
626
+ if (descriptor === undefined) continue
627
+ if ("value" in descriptor && typeof descriptor.value === "function") {
628
+ descriptor.value = wrapSelectedBranchBuilder(descriptor.value, owner, branchIndex, branchKey)
629
+ }
630
+ Object.defineProperty(wrapped, property, descriptor)
631
+ }
632
+ return wrapped
633
+ }
634
+ throw new Error(`Machine could not construct transition branch "${branchKey}"`)
635
+ }
636
+
637
+ const makeBranchSelectors = (
638
+ context: Record<string, any>,
639
+ branches: ReadonlyArray<CapturedNamedBranch>,
640
+ owner: object,
641
+ stateNodes: Machine.StateNodes,
642
+ source: string
643
+ ): Readonly<Record<string, unknown>> => {
644
+ const select: Record<string, unknown> = Object.create(null)
645
+ for (let branchIndex = 0; branchIndex < branches.length; branchIndex++) {
646
+ const branch = branches[branchIndex]!
647
+ select[branch.key] = wrapSelectedBranchBuilder(
648
+ getSelectionBuilder(context.target, branch.selection, stateNodes, source),
649
+ owner,
650
+ branchIndex,
651
+ branch.key
652
+ )
653
+ }
654
+ return Object.freeze(select)
655
+ }
656
+
657
+ const validateSelectedBranchResult = (
658
+ result: unknown,
659
+ selection: Topology.TargetSelection,
660
+ stateNodes: Machine.StateNodes
661
+ ): void => {
662
+ if (selection.kind === "none") {
663
+ if (!Topology.isNoTarget(result)) {
664
+ throw new Error("Machine targetless branch must return its selected targetless builder")
665
+ }
666
+ return
667
+ }
668
+ if (result === undefined) {
669
+ throw new Error(`Machine transition branch selected "${selection.path}" without constructing its target`)
670
+ }
671
+ validateResolvedSelection(result, selection, stateNodes)
672
+ }
673
+
353
674
  const captureTransition = (
354
- transition: unknown,
675
+ rawTransition: unknown,
355
676
  stateNodes: Machine.StateNodes,
356
677
  path: string,
357
678
  trigger: PropertyKey
358
679
  ): unknown => {
680
+ if (typeof rawTransition !== "function") {
681
+ throw new Error(
682
+ `Machine transition for state "${path}" on "${String(trigger)}" must be a target-first callback`
683
+ )
684
+ }
685
+ const transition = normalizeTransitionBuilder(rawTransition as (selector: unknown) => unknown, stateNodes, path)
359
686
  if (typeof transition !== "object" || transition === null) {
360
687
  throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must be an object`)
361
688
  }
362
689
  const definition = transition as Record<PropertyKey, unknown>
363
690
  const selector = makeTargetSelector(stateNodes, path)
364
691
  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")) {
692
+ const declinable = definition.declinable === true
693
+ if (hasProperty(definition, "branches")) {
694
+ const branching = definition as { readonly branches: unknown; readonly resolve?: unknown }
695
+ if (typeof branching.branches !== "function" || typeof branching.resolve !== "function") {
368
696
  throw new Error(
369
- `Machine conditional transition for state "${path}" on "${String(trigger)}" requires cases and otherwise`
697
+ `Machine branching transition for state "${path}" on "${String(trigger)}" requires branches and resolve`
370
698
  )
371
699
  }
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") {
700
+ const resolve = branching.resolve
701
+ const branches = captureNamedBranches(branching.branches(selector), path, trigger)
702
+ const owner = Object.freeze({})
703
+ const evaluate = (context: Record<string, any>, enqueue: unknown) => {
704
+ const resolverContext = { ...context }
705
+ delete resolverContext.target
706
+ resolverContext.select = makeBranchSelectors(context, branches, owner, stateNodes, path)
707
+ if (declinable) resolverContext.decline = Topology.makeDeclined
708
+ const selected = resolve(resolverContext, enqueue)
709
+ if (Topology.isDeclined(selected)) {
710
+ if (!declinable) {
711
+ throw new Error(
712
+ `Machine branching transition for state "${path}" on "${
713
+ String(trigger)
714
+ }" returned decline without declaring declinable: true`
715
+ )
716
+ }
717
+ return { result: selected, branchIndex: -1, branchKey: undefined }
718
+ }
719
+ if (!Topology.isSelectedBranch(selected) || selected.owner !== owner) {
375
720
  throw new Error(
376
- `Machine conditional transition case for state "${path}" on "${String(trigger)}" requires title and when`
721
+ `Machine branching transition for state "${path}" on "${String(trigger)}" must select one declared branch`
377
722
  )
378
723
  }
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
- }
724
+ const branch = branches[selected.branchIndex]
725
+ if (branch === undefined || selected.branchKey !== branch.key) {
726
+ throw new Error(`Machine branching transition returned invalid branch evidence`)
397
727
  }
728
+ validateSelectedBranchResult(selected.result, branch.selection, stateNodes)
398
729
  return {
399
- result: runCapturedBranch(otherwise, context, enqueue, stateNodes, path),
400
- branchIndex: cases.length
730
+ result: selected.result,
731
+ branchIndex: selected.branchIndex,
732
+ branchKey: selected.branchKey
401
733
  }
402
734
  }
403
735
  return {
404
736
  reenter,
737
+ declinable,
405
738
  targets: [
406
739
  ...new Set(
407
- [...cases, otherwise].flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
740
+ branches.flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
408
741
  )
409
742
  ],
410
- branches: [
411
- ...cases.map((branch) => ({
412
- type: "case" as const,
413
- title: branch.title!,
743
+ branches: branches.map((branch) =>
744
+ Object.freeze({
745
+ type: "branch" as const,
746
+ key: branch.key,
747
+ title: branch.title,
414
748
  target: branch.selection.path,
415
749
  selection: transitionTargetSelection(branch.selection)
416
- })),
417
- {
418
- type: "otherwise" as const,
419
- target: otherwise.selection.path,
420
- selection: transitionTargetSelection(otherwise.selection)
421
- }
422
- ],
750
+ })
751
+ ),
423
752
  evaluate,
424
753
  transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
425
754
  }
@@ -427,10 +756,12 @@ const captureTransition = (
427
756
  const branch = captureDefinitionBranch(transition, selector, path, trigger)
428
757
  const evaluate = (context: Record<string, any>, enqueue: unknown) => ({
429
758
  result: runCapturedBranch(branch, context, enqueue, stateNodes, path),
430
- branchIndex: 0
759
+ branchIndex: 0,
760
+ branchKey: undefined
431
761
  })
432
762
  return {
433
763
  reenter,
764
+ declinable,
434
765
  targets: branch.selection.path === undefined ? [] : [branch.selection.path],
435
766
  branches: [{
436
767
  type: "direct" as const,
@@ -465,7 +796,7 @@ const captureInvokeDefinition = (
465
796
  if (Array.isArray(invoke)) return invoke.map((item) => captureInvokeDefinition(item, stateNodes, path))
466
797
  if (typeof invoke !== "object" || invoke === null) return invoke
467
798
  const captured = { ...(invoke as Record<PropertyKey, unknown>) }
468
- for (const key of ["onDone", "onFailure", "onSnapshot"] as const) {
799
+ for (const key of ["onElement", "onDone", "onFailure", "onSnapshot"] as const) {
469
800
  if (captured[key] !== undefined) {
470
801
  captured[key] = captureTransition(captured[key], stateNodes, path, key)
471
802
  }
@@ -1072,10 +1403,12 @@ const makeInitialSelector = (stateNodes: Machine.StateNodes): unknown => {
1072
1403
  const selector: Record<string, unknown> = {}
1073
1404
  for (const node of stateNodes.byPath.values()) {
1074
1405
  if (node.parent === undefined && node.type !== "history" && node.type !== "choice") {
1075
- selector[node.key] = makeSelectionNode(stateNodes, node.path, "initial")
1406
+ selector[node.key] = node.type === "atomic" || node.type === "final"
1407
+ ? makeSelectionMethod("state", node.path, "initial")
1408
+ : Object.freeze({ initial: makeSelectionValue("initial", node.path, "initial") })
1076
1409
  }
1077
1410
  }
1078
- return selector
1411
+ return Object.freeze(selector)
1079
1412
  }
1080
1413
 
1081
1414
  const getInitialSelectionBuilder = (
@@ -1094,15 +1427,37 @@ const getInitialSelectionBuilder = (
1094
1427
  }
1095
1428
 
1096
1429
  const captureInitialBranch = (
1097
- branch: unknown,
1098
- selector: unknown,
1430
+ definition: unknown,
1431
+ stateNodes: Machine.StateNodes,
1099
1432
  initialBuilder: Record<string, any>
1100
- ): CapturedBranch & { readonly builder: (...args: ReadonlyArray<any>) => unknown } => {
1101
- const captured = captureDefinitionBranch(branch, selector, "<machine>", "initial")
1102
- if (captured.selection.kind !== "state" && captured.selection.kind !== "initial") {
1433
+ ): {
1434
+ readonly selection: Topology.TargetSelection
1435
+ readonly resolve?: (context: any) => unknown
1436
+ readonly builder: (...args: ReadonlyArray<any>) => unknown
1437
+ } => {
1438
+ if (typeof definition !== "function") {
1439
+ throw new Error("Machine initial definition must be a target-first callback")
1440
+ }
1441
+ const result = definition(decorateInitialSelectorNode(makeInitialSelector(stateNodes)))
1442
+ let selection: Topology.TargetSelection
1443
+ let resolve: ((context: any) => unknown) | undefined
1444
+ if (Topology.isTargetSelection(result)) {
1445
+ selection = plainTargetSelection(result)
1446
+ } else if (hasProperty(result, InitialBuilderDescriptorTypeId)) {
1447
+ const descriptor = result as InitialBuilderDescriptor
1448
+ selection = descriptor.selection
1449
+ resolve = descriptor.resolve
1450
+ } else {
1451
+ throw new Error("Machine initial definition must select exactly one target")
1452
+ }
1453
+ if (selection.kind !== "state" && selection.kind !== "initial") {
1103
1454
  throw new Error("Machine initial target must select a top-level state or its declared initial entry")
1104
1455
  }
1105
- return { ...captured, builder: getInitialSelectionBuilder(initialBuilder, captured.selection) }
1456
+ const captured = {
1457
+ selection,
1458
+ builder: getInitialSelectionBuilder(initialBuilder, selection)
1459
+ }
1460
+ return resolve === undefined ? Object.freeze(captured) : Object.freeze({ ...captured, resolve })
1106
1461
  }
1107
1462
 
1108
1463
  const validateInitialSelection = (result: unknown, selection: Topology.TargetSelection): void => {
@@ -1124,17 +1479,13 @@ const compileInitial = (
1124
1479
  readonly initial: (input?: unknown) => unknown
1125
1480
  readonly definition: Machine.InitialDefinition
1126
1481
  } => {
1127
- if (typeof definition !== "object" || definition === null) {
1128
- throw new Error("Machine initial definition must be an object")
1129
- }
1130
- const selector = makeInitialSelector(stateNodes)
1131
1482
  const initialBuilder = makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Record<string, any>
1132
- const branch = captureInitialBranch(definition, selector, initialBuilder)
1483
+ const branch = captureInitialBranch(definition, stateNodes, initialBuilder)
1133
1484
  return {
1134
1485
  initial: (input?: unknown) => {
1135
1486
  const result = branch.resolve === undefined
1136
- ? branch.builder()
1137
- : branch.resolve({ input, target: branch.builder }, undefined)
1487
+ ? constructSelectedTarget(branch.builder)
1488
+ : branch.resolve({ input, target: branch.builder })
1138
1489
  validateInitialSelection(result, branch.selection)
1139
1490
  return result
1140
1491
  },
@@ -1184,7 +1535,7 @@ type MakeConfig<
1184
1535
  InitialE,
1185
1536
  InitialR,
1186
1537
  InternalEvents extends ReadonlyArray<Machine.TaggedSchema>,
1187
- ParentEvents extends ReadonlyArray<Machine.TaggedSchema>
1538
+ ParentDeclaration extends Parent.Any | undefined
1188
1539
  > = {
1189
1540
  readonly id?: string
1190
1541
  readonly states: States & DefineStateTreeInput<NoInfer<States>>
@@ -1198,7 +1549,7 @@ type MakeConfig<
1198
1549
  NoInfer<InternalEvents>
1199
1550
  >
1200
1551
  readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
1201
- readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
1552
+ readonly parent?: ParentDeclaration
1202
1553
  readonly input?: Input
1203
1554
  readonly initial: unknown
1204
1555
  }
@@ -1211,7 +1562,7 @@ type MakeResult<
1211
1562
  InitialE,
1212
1563
  InitialR,
1213
1564
  InternalEvents extends ReadonlyArray<Machine.TaggedSchema>,
1214
- ParentEvents extends ReadonlyArray<Machine.TaggedSchema>
1565
+ ParentDeclaration extends Parent.Any | undefined
1215
1566
  > = Definition<
1216
1567
  States,
1217
1568
  readonly [...InputEvents, ...InternalEvents],
@@ -1222,7 +1573,7 @@ type MakeResult<
1222
1573
  Machine.TerminalOutput<States>,
1223
1574
  Emits,
1224
1575
  InputEvents,
1225
- ParentEvents
1576
+ Machine.ParentEventsOf<ParentDeclaration>
1226
1577
  >
1227
1578
 
1228
1579
  interface Make {
@@ -1234,11 +1585,11 @@ interface Make {
1234
1585
  InitialE = never,
1235
1586
  InitialR = never,
1236
1587
  const InternalEvents extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
1237
- const ParentEvents extends ReadonlyArray<Machine.TaggedSchema> = readonly []
1588
+ const ParentDeclaration extends Parent.Any | undefined = undefined
1238
1589
  >(
1239
- config: MakeConfig<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents>,
1590
+ config: MakeConfig<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentDeclaration>,
1240
1591
  ..._validation: ValidateDefinedStates<NoInfer<States>>
1241
- ): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents>
1592
+ ): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentDeclaration>
1242
1593
  <
1243
1594
  const States extends Machine.StateSchemas,
1244
1595
  const InputEvents extends ReadonlyArray<Machine.TaggedSchema>,
@@ -1247,10 +1598,13 @@ interface Make {
1247
1598
  InitialE = never,
1248
1599
  InitialR = never,
1249
1600
  const InternalEvents extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
1250
- const ParentEvents extends ReadonlyArray<Machine.TaggedSchema> = readonly []
1601
+ const ParentDeclaration extends Parent.Any | undefined = undefined
1251
1602
  >(
1252
1603
  config:
1253
- & Omit<MakeConfig<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents>, "states">
1604
+ & Omit<
1605
+ MakeConfig<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentDeclaration>,
1606
+ "states"
1607
+ >
1254
1608
  & { readonly states: InvalidDefinedStateTreeInput<States> }
1255
1609
  ): never
1256
1610
  }
@@ -1263,7 +1617,7 @@ export const make: Make = (<
1263
1617
  InitialE = never,
1264
1618
  InitialR = never,
1265
1619
  const InternalEvents extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
1266
- const ParentEvents extends ReadonlyArray<Machine.TaggedSchema> = readonly []
1620
+ const ParentDeclaration extends Parent.Any | undefined = undefined
1267
1621
  >(
1268
1622
  config: {
1269
1623
  readonly id?: string
@@ -1271,18 +1625,18 @@ export const make: Make = (<
1271
1625
  readonly events: Machine.EventProtocol<"public", InputEvents>
1272
1626
  readonly internalEvents?: Machine.EventProtocol<"internal", InternalEvents>
1273
1627
  readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
1274
- readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
1628
+ readonly parent?: ParentDeclaration
1275
1629
  readonly input?: Input
1276
1630
  readonly initial: unknown
1277
1631
  }
1278
- ): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents> => {
1632
+ ): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentDeclaration> => {
1279
1633
  StateDefinition.validateStateDefinitions(config.states, "Machine.make")
1280
1634
  const self = Object.create(Proto)
1281
1635
  self.states = config.states
1282
1636
  self.events = config.events
1283
1637
  self.internalEvents = config.internalEvents ?? Protocol.makeEventProtocol("internal", [] as const)
1284
1638
  self.emittedEvents = config.emittedEvents ?? Protocol.makeEventProtocol("emitted", [] as const)
1285
- self.parentEvents = config.parentEvents ?? Protocol.makeEventProtocol("public", [] as const)
1639
+ self.parent = config.parent
1286
1640
  self.input = config.input
1287
1641
  self.id = config.id
1288
1642
  self.stateNodes = Topology.compileStateNodes(config.states)
@@ -1292,7 +1646,6 @@ export const make: Make = (<
1292
1646
  self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes)
1293
1647
  self.handlers = Object.create(null)
1294
1648
  self.handle = makeHandle(self)
1295
- self.invoke = makeBoundInvoke
1296
1649
  Protocol.setProtocol(self)
1297
1650
  return self
1298
1651
  }) as Make
@@ -1315,6 +1668,27 @@ export const events = <const Inputs extends ReadonlyArray<Machine.EventProtocolI
1315
1668
  flattenEventProtocolInputs("public", inputs)
1316
1669
  ) as Machine.EventProtocol<"public", Machine.EventProtocolInputSchemasOf<"public", Inputs>>
1317
1670
 
1671
+ const makeParent = <
1672
+ const Mode extends ParentMode,
1673
+ const Events extends ReadonlyArray<Machine.TaggedSchema>
1674
+ >(
1675
+ mode: Mode,
1676
+ events: Machine.EventProtocol<"public", Events>
1677
+ ): Parent<Mode, Events> => {
1678
+ if (!Protocol.isEventProtocol(events, "public")) {
1679
+ throw new Error("Machine parent declarations require a protocol created with Machine.events")
1680
+ }
1681
+ return Object.freeze({ [ParentTypeId]: ParentTypeId, mode, events })
1682
+ }
1683
+
1684
+ export const parent = <const Events extends ReadonlyArray<Machine.TaggedSchema>>(
1685
+ events: Machine.EventProtocol<"public", Events>
1686
+ ): Parent<"required", Events> => makeParent("required", events)
1687
+
1688
+ export const optionalParent = <const Events extends ReadonlyArray<Machine.TaggedSchema>>(
1689
+ events: Machine.EventProtocol<"public", Events>
1690
+ ): Parent<"optional", Events> => makeParent("optional", events)
1691
+
1318
1692
  export const internalEvents = <const Inputs extends ReadonlyArray<Machine.EventProtocolInput<"internal">>>(
1319
1693
  ...inputs: Inputs
1320
1694
  ): Machine.EventProtocol<"internal", Machine.EventProtocolInputSchemasOf<"internal", Inputs>> =>