@typeonce/effect-machine 0.13.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 (59) hide show
  1. package/README.md +74 -26
  2. package/dist/Machine.d.ts +379 -188
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +119 -53
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  7. package/dist/internal/machine/executionPlan.js +14 -6
  8. package/dist/internal/machine/executionPlan.js.map +1 -1
  9. package/dist/internal/machine/machine.d.ts +3 -1
  10. package/dist/internal/machine/machine.d.ts.map +1 -1
  11. package/dist/internal/machine/machine.js +306 -26
  12. package/dist/internal/machine/machine.js.map +1 -1
  13. package/dist/internal/machine/planner.d.ts +13 -0
  14. package/dist/internal/machine/planner.d.ts.map +1 -1
  15. package/dist/internal/machine/planner.js +15 -7
  16. package/dist/internal/machine/planner.js.map +1 -1
  17. package/dist/internal/machine/topology.d.ts +12 -0
  18. package/dist/internal/machine/topology.d.ts.map +1 -1
  19. package/dist/internal/machine/topology.js +17 -10
  20. package/dist/internal/machine/topology.js.map +1 -1
  21. package/dist/internal/testing/machine/exploration.d.ts.map +1 -1
  22. package/dist/internal/testing/machine/exploration.js +11 -2
  23. package/dist/internal/testing/machine/exploration.js.map +1 -1
  24. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  25. package/dist/internal/testing/machine/finiteModel.js +72 -66
  26. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  27. package/dist/internal/testing/machine/transitionCoverage.d.ts +20 -0
  28. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -0
  29. package/dist/internal/testing/machine/transitionCoverage.js +80 -0
  30. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -0
  31. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  32. package/dist/internal/testing/machine/verification.js +171 -33
  33. package/dist/internal/testing/machine/verification.js.map +1 -1
  34. package/dist/testing/MachineTest.d.ts +97 -22
  35. package/dist/testing/MachineTest.d.ts.map +1 -1
  36. package/dist/testing/MachineTest.js +58 -16
  37. package/dist/testing/MachineTest.js.map +1 -1
  38. package/dist/unstable/cluster/ClusterMachine.d.ts +4 -1
  39. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  40. package/dist/unstable/cluster/ClusterMachine.js +4 -1
  41. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  42. package/dist/unstable/reactivity/AtomMachine.d.ts +12 -3
  43. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  44. package/dist/unstable/reactivity/AtomMachine.js +12 -3
  45. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  46. package/docs/agent-guide.md +139 -57
  47. package/package.json +1 -1
  48. package/src/Machine.ts +750 -336
  49. package/src/internal/machine/executionPlan.ts +22 -7
  50. package/src/internal/machine/machine.ts +407 -33
  51. package/src/internal/machine/planner.ts +44 -13
  52. package/src/internal/machine/topology.ts +38 -12
  53. package/src/internal/testing/machine/exploration.ts +10 -2
  54. package/src/internal/testing/machine/finiteModel.ts +106 -73
  55. package/src/internal/testing/machine/transitionCoverage.ts +116 -0
  56. package/src/internal/testing/machine/verification.ts +216 -58
  57. package/src/testing/MachineTest.ts +118 -28
  58. package/src/unstable/cluster/ClusterMachine.ts +4 -1
  59. package/src/unstable/reactivity/AtomMachine.ts +12 -3
@@ -150,7 +150,7 @@ const compileIndexedExecutionDescriptor = (
150
150
  }
151
151
  const byEvent = new Map<PropertyKey, MicrostepTransition<any, any, any, any>>()
152
152
  for (const tag of Reflect.ownKeys(config.on)) {
153
- const transition = normalizeTransition(config.on[tag])
153
+ const transition = normalizeTransition(config.on[tag] as Parameters<typeof normalizeTransition>[0])
154
154
  if (transition !== undefined) {
155
155
  byEvent.set(tag, transition)
156
156
  }
@@ -424,12 +424,13 @@ export interface ExecutionMacrostep<State = unknown> {
424
424
  const collectIndexedTransition = (
425
425
  machine: Machine.Any,
426
426
  transition: TransitionHandler<any, any, any, any>,
427
- context: any
427
+ context: any,
428
+ evaluate?: NonNullable<MicrostepTransition<any, any, any, any>["evaluate"]>
428
429
  ) => {
429
430
  let commands: Array<RuntimeCommand> | undefined
430
431
  let raisedEvents: Array<any> | undefined
431
432
  let emittedEvents: Array<unknown> | undefined
432
- const result = transition(context, {
433
+ const enqueue = {
433
434
  raise: (event: unknown) => {
434
435
  ;(raisedEvents ??= []).push(decodeEventSync(machine, event))
435
436
  },
@@ -442,9 +443,13 @@ const collectIndexedTransition = (
442
443
  stop: (child: unknown) => {
443
444
  ;(commands ??= []).push({ _tag: "Stop", child: child as any })
444
445
  }
445
- })
446
+ }
447
+ const evaluated = evaluate === undefined
448
+ ? { result: transition(context, enqueue), branchIndex: 0 }
449
+ : evaluate(context, enqueue)
446
450
  return {
447
- state: isNoTarget(result) ? undefined : result,
451
+ state: isNoTarget(evaluated.result) ? undefined : evaluated.result,
452
+ branchIndex: evaluated.branchIndex,
448
453
  commands: commands ?? emptyExecutionValues,
449
454
  raisedEvents: raisedEvents ?? emptyExecutionValues,
450
455
  emittedEvents: emittedEvents ?? emptyExecutionValues
@@ -531,7 +536,12 @@ const collectIndexedEvaluatedTransition = (
531
536
  state: OwnedIndexedState,
532
537
  selection: IndexedSelectedTransition
533
538
  ): IndexedEvaluatedTransition => {
534
- const transitionResult = collectIndexedTransition(machine, selection.transition.transition, selection.context)
539
+ const transitionResult = collectIndexedTransition(
540
+ machine,
541
+ selection.transition.transition,
542
+ selection.context,
543
+ selection.transition.evaluate
544
+ )
535
545
  const unresolvedTarget = transitionResult.state
536
546
  validateDeclaredTransitionTarget(
537
547
  selection.sourcePath,
@@ -558,6 +568,7 @@ const collectIndexedEvaluatedTransition = (
558
568
  if (!changed) {
559
569
  return {
560
570
  selection,
571
+ branchIndex: transitionResult.branchIndex,
561
572
  unresolvedTarget: unresolvedTarget as any,
562
573
  target: target as any,
563
574
  next,
@@ -581,6 +592,7 @@ const collectIndexedEvaluatedTransition = (
581
592
  : naturalBoundary
582
593
  return {
583
594
  selection,
595
+ branchIndex: transitionResult.branchIndex,
584
596
  unresolvedTarget: unresolvedTarget as any,
585
597
  target: target as any,
586
598
  next,
@@ -606,6 +618,7 @@ const indexedMicrostep = (
606
618
  source: transition.selection.sourcePath,
607
619
  trigger: transition.selection.trigger,
608
620
  reenter: transition.selection.transition.reenter,
621
+ branchIndex: transition.branchIndex,
609
622
  target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
610
623
  resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
611
624
  })
@@ -740,7 +753,8 @@ const planIndexedFlatState = (
740
753
  event,
741
754
  snapshot: snapshotFromIndexedState(descriptor, current),
742
755
  target: getTargetBuilder(machine, sourcePath)
743
- }
756
+ },
757
+ transition.evaluate
744
758
  )
745
759
  const target = transitionResult.state
746
760
  validateDeclaredTransitionTarget(
@@ -789,6 +803,7 @@ const planIndexedFlatState = (
789
803
  source: sourcePath,
790
804
  trigger: { type: "event", event: event._tag },
791
805
  reenter: transition.reenter,
806
+ branchIndex: transitionResult.branchIndex,
792
807
  target: target === undefined ? undefined : getTargetNodePath(target as any),
793
808
  resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any)
794
809
  }]
@@ -49,6 +49,7 @@ export { ChildMachineLogicTypeId, InitialEventTypeId, SnapshotBuilderStateTypeId
49
49
 
50
50
  const TypeId = "~effect/Machine"
51
51
  export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke")
52
+ export const TransitionTypeId: unique symbol = Symbol.for("effect/Machine/Transition")
52
53
  const ChildMachineTypeId = "~effect/Machine/ChildMachine"
53
54
  type IsAny<A> = 0 extends 1 & A ? true : false
54
55
  type MachineRuntimeRequirement = internalRuntime.MachineRuntime
@@ -118,6 +119,7 @@ const cloneWithHandlers = (
118
119
  machine.input = self.input
119
120
  machine.id = self.id
120
121
  machine.initial = self.initial
122
+ machine.initialDefinition = self.initialDefinition
121
123
  machine.stateNodes = self.stateNodes
122
124
  machine.makeTargetBuilder = self.makeTargetBuilder
123
125
  machine.handlers = handlers
@@ -127,47 +129,326 @@ const cloneWithHandlers = (
127
129
  return machine
128
130
  }
129
131
 
130
- const validateTransitionTargets = (
132
+ type DefinitionBranch = {
133
+ readonly title?: string
134
+ readonly when?: (context: any) => Option.Option<unknown>
135
+ readonly target: (selector: unknown) => unknown
136
+ readonly resolve?: (context: any, enqueue: unknown) => unknown
137
+ }
138
+
139
+ type CapturedBranch = DefinitionBranch & {
140
+ readonly selection: Topology.TargetSelection
141
+ }
142
+
143
+ const transitionTargetSelection = (
144
+ selection: Topology.TargetSelection
145
+ ): Machine.TransitionTargetSelection =>
146
+ Object.freeze({
147
+ path: selection.path,
148
+ kind: selection.kind,
149
+ scope: selection.scope
150
+ })
151
+
152
+ const makeSelectionMethod = (
153
+ kind: Topology.TargetSelectionKind,
154
+ path: string | undefined,
155
+ scope: Topology.TargetSelectionScope
156
+ ): () => Topology.TargetSelection =>
157
+ () => Topology.makeTargetSelection(kind, path, scope)
158
+
159
+ const addSelectionChildren = (
160
+ builder: Record<string, unknown>,
161
+ stateNodes: Machine.StateNodes,
162
+ parent: string,
163
+ scope: "local" | "branch"
164
+ ): void => {
165
+ for (const node of stateNodes.byPath.values()) {
166
+ if (node.parent !== parent || node.type === "history") continue
167
+ builder[node.key] = makeSelectionNode(stateNodes, node.path, scope)
168
+ }
169
+ }
170
+
171
+ const makeSelectionNode = (
172
+ stateNodes: Machine.StateNodes,
173
+ path: string,
174
+ scope: Topology.TargetSelectionScope
175
+ ): unknown => {
176
+ const node = getTargetBuilderNode(stateNodes, path)
177
+ const kind: Topology.TargetSelectionKind = node.type === "choice" ? "choice" : "state"
178
+ const method = makeSelectionMethod(kind, path, scope) as unknown as Record<string, unknown>
179
+ if (node.type !== "atomic" && node.type !== "final" && node.type !== "choice" && node.type !== "history") {
180
+ Object.defineProperty(method, "initial", {
181
+ value: makeSelectionMethod("initial", path, scope),
182
+ enumerable: true
183
+ })
184
+ if (scope === "local" || scope === "branch") {
185
+ addSelectionChildren(method, stateNodes, path, scope)
186
+ }
187
+ }
188
+ return method
189
+ }
190
+
191
+ const makeHistorySelectionTree = (
192
+ stateNodes: Machine.StateNodes,
193
+ parent: string | undefined
194
+ ): Record<string, unknown> => {
195
+ const builder: Record<string, unknown> = {}
196
+ for (const node of stateNodes.byPath.values()) {
197
+ if (node.parent !== parent) continue
198
+ if (node.type === "history") {
199
+ builder[node.key] = makeSelectionMethod("history", node.path, "full")
200
+ } else if (node.type !== "choice") {
201
+ const children = makeHistorySelectionTree(stateNodes, node.path)
202
+ if (Object.keys(children).length > 0) builder[node.key] = children
203
+ }
204
+ }
205
+ return builder
206
+ }
207
+
208
+ const makeTargetSelector = (
131
209
  stateNodes: Machine.StateNodes,
210
+ source: string
211
+ ): unknown => {
212
+ const full: Record<string, unknown> = {}
213
+ for (const node of stateNodes.byPath.values()) {
214
+ if (node.parent === undefined && node.type !== "history" && node.type !== "choice") {
215
+ full[node.key] = makeSelectionNode(stateNodes, node.path, "full")
216
+ }
217
+ }
218
+ const branch: Record<string, unknown> = {}
219
+ const root = getTargetBuilderNode(stateNodes, source.split(".")[0]!)
220
+ branch[root.key] = makeSelectionNode(stateNodes, root.path, "branch")
221
+ const local: Record<string, unknown> = {}
222
+ const localScope = getLocalTargetScope(stateNodes, source)
223
+ if (localScope !== undefined) addSelectionChildren(local, stateNodes, localScope, "local")
224
+ return {
225
+ none: makeSelectionMethod("none", undefined, "local"),
226
+ local,
227
+ branch,
228
+ full,
229
+ history: makeHistorySelectionTree(stateNodes, undefined)
230
+ }
231
+ }
232
+
233
+ const captureDefinitionBranch = (
234
+ branch: unknown,
235
+ selector: unknown,
132
236
  path: string,
133
- trigger: PropertyKey,
134
- transition: unknown
237
+ trigger: PropertyKey
238
+ ): CapturedBranch => {
239
+ if (
240
+ typeof branch !== "object" || branch === null || !hasProperty(branch, "target") ||
241
+ typeof branch.target !== "function"
242
+ ) {
243
+ throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" requires a target selector`)
244
+ }
245
+ const selection = branch.target(selector)
246
+ if (!Topology.isTargetSelection(selection)) {
247
+ throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must select exactly one target`)
248
+ }
249
+ return { ...(branch as DefinitionBranch), selection }
250
+ }
251
+
252
+ const getSelectionBuilder = (
253
+ target: Record<string, any>,
254
+ selection: Topology.TargetSelection,
255
+ stateNodes: Machine.StateNodes,
256
+ source: string
257
+ ): unknown => {
258
+ if (selection.kind === "none") return target.none
259
+ let builder: any
260
+ let parts = selection.path!.split(".")
261
+ if (selection.kind === "history") {
262
+ builder = target.history
263
+ } else if (selection.scope === "local") {
264
+ builder = target.local
265
+ const scope = getLocalTargetScope(stateNodes, source)
266
+ if (scope !== undefined) parts = selection.path!.slice(scope.length + 1).split(".")
267
+ } else if (selection.scope === "branch") {
268
+ builder = target.branch
269
+ } else {
270
+ builder = target.full
271
+ }
272
+ for (const part of parts) builder = builder[part]
273
+ if (selection.kind === "initial") builder = builder.initial
274
+ if (
275
+ typeof builder !== "function" &&
276
+ (typeof builder !== "object" || builder === null || typeof builder.from !== "function")
277
+ ) {
278
+ throw new Error(`Machine could not construct selected transition target "${selection.path}"`)
279
+ }
280
+ return builder
281
+ }
282
+
283
+ const constructSelectedTarget = (builder: any): unknown => typeof builder === "function" ? builder() : builder.from()
284
+
285
+ const validateResolvedSelection = (
286
+ result: unknown,
287
+ selection: Topology.TargetSelection,
288
+ stateNodes: Machine.StateNodes
135
289
  ): void => {
136
- if (typeof transition !== "object" || transition === null || !hasProperty(transition, "targets")) {
290
+ if (selection.kind === "none") {
291
+ if (result !== undefined) {
292
+ throw new Error("Machine targetless transition resolver must return undefined")
293
+ }
137
294
  return
138
295
  }
139
- if (!Array.isArray(transition.targets)) {
296
+ if (result === undefined) return
297
+ const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path") &&
298
+ typeof result.path === "string"
299
+ ? result.path
300
+ : undefined
301
+ const selectedNode = selection.path === undefined ? undefined : stateNodes.byPath.get(selection.path)
302
+ const acceptsDescendant = (selection.scope === "local" || selection.scope === "branch") &&
303
+ (selectedNode?.type === "compound" || selectedNode?.type === "parallel")
304
+ if (
305
+ resultPath === undefined ||
306
+ (resultPath !== selection.path && !(acceptsDescendant && resultPath.startsWith(`${selection.path}.`)))
307
+ ) {
140
308
  throw new Error(
141
- `Machine expected transition targets for state "${path}" on "${String(trigger)}" to be an array`
309
+ `Machine transition resolver selected "${selection.path}" but constructed "${resultPath ?? "<invalid>"}"`
142
310
  )
143
311
  }
144
- for (const target of transition.targets) {
145
- if (typeof target !== "string" || !stateNodes.byPath.has(target)) {
146
- throw new Error(
147
- `Machine transition for state "${path}" on "${String(trigger)}" declares unknown target "${String(target)}"`
148
- )
149
- }
150
- }
151
312
  }
152
313
 
153
- const captureTransition = (transition: unknown): unknown => {
314
+ const runCapturedBranch = (
315
+ branch: CapturedBranch,
316
+ context: Record<string, any>,
317
+ enqueue: unknown,
318
+ stateNodes: Machine.StateNodes,
319
+ source: string,
320
+ match?: { readonly value: unknown }
321
+ ): unknown => {
322
+ const selectedTarget = getSelectionBuilder(context.target, branch.selection, stateNodes, source)
323
+ if (branch.resolve === undefined) return constructSelectedTarget(selectedTarget)
324
+ const resolverContext = { ...context }
325
+ if (branch.selection.kind === "none") delete resolverContext.target
326
+ else resolverContext.target = selectedTarget
327
+ if (match !== undefined) resolverContext.match = match.value
328
+ const resolved = branch.resolve(resolverContext, enqueue)
329
+ validateResolvedSelection(resolved, branch.selection, stateNodes)
330
+ return resolved === undefined ? constructSelectedTarget(selectedTarget) : resolved
331
+ }
332
+
333
+ const captureTransition = (
334
+ transition: unknown,
335
+ stateNodes: Machine.StateNodes,
336
+ path: string,
337
+ trigger: PropertyKey
338
+ ): unknown => {
154
339
  if (typeof transition !== "object" || transition === null) {
155
- return transition
340
+ throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must be an object`)
341
+ }
342
+ const definition = transition as Record<PropertyKey, unknown>
343
+ const selector = makeTargetSelector(stateNodes, path)
344
+ const reenter = definition.reenter === true
345
+ if (Array.isArray(definition.cases)) {
346
+ const rawCases = definition.cases as ReadonlyArray<unknown>
347
+ if (definition.cases.length === 0 || !hasProperty(definition, "otherwise")) {
348
+ throw new Error(
349
+ `Machine conditional transition for state "${path}" on "${String(trigger)}" requires cases and otherwise`
350
+ )
351
+ }
352
+ const cases = rawCases.map((branch) => {
353
+ const captured = captureDefinitionBranch(branch, selector, path, trigger)
354
+ if (typeof captured.title !== "string" || captured.title.length === 0 || typeof captured.when !== "function") {
355
+ throw new Error(
356
+ `Machine conditional transition case for state "${path}" on "${String(trigger)}" requires title and when`
357
+ )
358
+ }
359
+ return captured
360
+ })
361
+ const otherwise = captureDefinitionBranch(definition.otherwise, selector, path, trigger)
362
+ const evaluate = (context: Record<string, any>, enqueue: unknown) => {
363
+ const predicateContext = { ...context }
364
+ delete predicateContext.target
365
+ for (let branchIndex = 0; branchIndex < cases.length; branchIndex++) {
366
+ const branch = cases[branchIndex]!
367
+ const result = branch.when!(predicateContext)
368
+ if (!Option.isOption(result)) {
369
+ throw new Error(`Machine conditional transition case "${branch.title}" must return Option`)
370
+ }
371
+ if (Option.isSome(result)) {
372
+ return {
373
+ result: runCapturedBranch(branch, context, enqueue, stateNodes, path, { value: result.value }),
374
+ branchIndex
375
+ }
376
+ }
377
+ }
378
+ return {
379
+ result: runCapturedBranch(otherwise, context, enqueue, stateNodes, path),
380
+ branchIndex: cases.length
381
+ }
382
+ }
383
+ return {
384
+ reenter,
385
+ targets: [
386
+ ...new Set(
387
+ [...cases, otherwise].flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
388
+ )
389
+ ],
390
+ branches: [
391
+ ...cases.map((branch) => ({
392
+ type: "case" as const,
393
+ title: branch.title!,
394
+ target: branch.selection.path,
395
+ selection: transitionTargetSelection(branch.selection)
396
+ })),
397
+ {
398
+ type: "otherwise" as const,
399
+ target: otherwise.selection.path,
400
+ selection: transitionTargetSelection(otherwise.selection)
401
+ }
402
+ ],
403
+ evaluate,
404
+ transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
405
+ }
156
406
  }
157
- const captured = { ...(transition as Record<PropertyKey, unknown>) }
158
- if (Array.isArray(captured.targets)) {
159
- captured.targets = captured.targets.slice()
407
+ const branch = captureDefinitionBranch(transition, selector, path, trigger)
408
+ const evaluate = (context: Record<string, any>, enqueue: unknown) => ({
409
+ result: runCapturedBranch(branch, context, enqueue, stateNodes, path),
410
+ branchIndex: 0
411
+ })
412
+ return {
413
+ reenter,
414
+ targets: branch.selection.path === undefined ? [] : [branch.selection.path],
415
+ branches: [{
416
+ type: "direct" as const,
417
+ target: branch.selection.path,
418
+ selection: transitionTargetSelection(branch.selection)
419
+ }],
420
+ evaluate,
421
+ transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
160
422
  }
161
- return captured
162
423
  }
163
424
 
164
- const captureEventHandlers = (on: object): Record<PropertyKey, unknown> => {
425
+ const captureEventHandlers = (
426
+ on: object,
427
+ stateNodes: Machine.StateNodes,
428
+ path: string
429
+ ): Record<PropertyKey, unknown> => {
165
430
  // The machine owns its dispatch table. Compiled plans may snapshot these
166
431
  // definitions, so retaining caller-owned containers would let strategies
167
432
  // observe different handlers after an unsafe external mutation.
168
433
  const captured: Record<PropertyKey, unknown> = Object.create(null)
169
434
  for (const event of Reflect.ownKeys(on)) {
170
- captured[event] = captureTransition((on as Record<PropertyKey, unknown>)[event])
435
+ captured[event] = captureTransition((on as Record<PropertyKey, unknown>)[event], stateNodes, path, event)
436
+ }
437
+ return captured
438
+ }
439
+
440
+ const captureInvokeDefinition = (
441
+ invoke: unknown,
442
+ stateNodes: Machine.StateNodes,
443
+ path: string
444
+ ): unknown => {
445
+ if (Array.isArray(invoke)) return invoke.map((item) => captureInvokeDefinition(item, stateNodes, path))
446
+ if (typeof invoke !== "object" || invoke === null) return invoke
447
+ const captured = { ...(invoke as Record<PropertyKey, unknown>) }
448
+ for (const key of ["onDone", "onFailure", "onSnapshot"] as const) {
449
+ if (captured[key] !== undefined) {
450
+ captured[key] = captureTransition(captured[key], stateNodes, path, key)
451
+ }
171
452
  }
172
453
  return captured
173
454
  }
@@ -191,15 +472,21 @@ const flattenHandlers = (
191
472
  const { states: childConfig, ...stateConfig } = nodeConfig as Record<string, unknown>
192
473
  const on = stateConfig.on
193
474
  if (typeof on === "object" && on !== null) {
194
- const capturedOn = captureEventHandlers(on)
475
+ const capturedOn = captureEventHandlers(on, stateNodes, path)
195
476
  stateConfig.on = capturedOn
196
- for (const event of Reflect.ownKeys(capturedOn)) {
197
- validateTransitionTargets(stateNodes, path, event, capturedOn[event])
198
- }
199
477
  }
200
- validateTransitionTargets(stateNodes, path, "always", stateConfig.always)
201
- validateTransitionTargets(stateNodes, path, "done", stateConfig.onDone)
202
- validateTransitionTargets(stateNodes, path, "choice", stateConfig.choice)
478
+ if (stateConfig.always !== undefined) {
479
+ stateConfig.always = captureTransition(stateConfig.always, stateNodes, path, "always")
480
+ }
481
+ if (stateConfig.onDone !== undefined) {
482
+ stateConfig.onDone = captureTransition(stateConfig.onDone, stateNodes, path, "done")
483
+ }
484
+ if (stateConfig.choice !== undefined) {
485
+ stateConfig.choice = captureTransition(stateConfig.choice, stateNodes, path, "choice")
486
+ }
487
+ if (stateConfig.invoke !== undefined) {
488
+ stateConfig.invoke = captureInvokeDefinition(stateConfig.invoke, stateNodes, path)
489
+ }
203
490
  const node = stateNodes.byPath.get(path)
204
491
  if (node?.type === "choice") {
205
492
  if (
@@ -208,7 +495,7 @@ const flattenHandlers = (
208
495
  !hasProperty(stateConfig.choice, "targets") || !Array.isArray(stateConfig.choice.targets) ||
209
496
  stateConfig.choice.targets.length === 0
210
497
  ) {
211
- throw new Error(`Machine choice state "${path}" requires a transition and at least one declared target`)
498
+ throw new Error(`Machine choice state "${path}" requires a transition`)
212
499
  }
213
500
  }
214
501
  handlers[path] = stateConfig as Machine.AnyStateConfig
@@ -764,13 +1051,89 @@ const makeTargetBuilder = <const States extends Machine.StateSchemas>(
764
1051
  }) as Machine.TargetBuilder<States, Source>
765
1052
  }
766
1053
 
1054
+ const makeInitialSelector = (stateNodes: Machine.StateNodes): unknown => {
1055
+ const selector: Record<string, unknown> = {}
1056
+ for (const node of stateNodes.byPath.values()) {
1057
+ if (node.parent === undefined && node.type !== "history" && node.type !== "choice") {
1058
+ selector[node.key] = makeSelectionNode(stateNodes, node.path, "initial")
1059
+ }
1060
+ }
1061
+ return selector
1062
+ }
1063
+
1064
+ const getInitialSelectionBuilder = (
1065
+ initialBuilder: Record<string, any>,
1066
+ selection: Topology.TargetSelection
1067
+ ): (...args: ReadonlyArray<any>) => unknown => {
1068
+ const path = selection.path
1069
+ if (path === undefined || path.includes(".")) {
1070
+ throw new Error("Machine initial target must select one top-level state")
1071
+ }
1072
+ const builder = initialBuilder[path]
1073
+ if (typeof builder !== "function") {
1074
+ throw new Error(`Machine could not construct selected initial state "${path}"`)
1075
+ }
1076
+ return builder
1077
+ }
1078
+
1079
+ const captureInitialBranch = (
1080
+ branch: unknown,
1081
+ selector: unknown,
1082
+ initialBuilder: Record<string, any>
1083
+ ): CapturedBranch & { readonly builder: (...args: ReadonlyArray<any>) => unknown } => {
1084
+ const captured = captureDefinitionBranch(branch, selector, "<machine>", "initial")
1085
+ if (captured.selection.kind !== "state" && captured.selection.kind !== "initial") {
1086
+ throw new Error("Machine initial target must select a top-level state or its declared initial entry")
1087
+ }
1088
+ return { ...captured, builder: getInitialSelectionBuilder(initialBuilder, captured.selection) }
1089
+ }
1090
+
1091
+ const validateInitialSelection = (result: unknown, selection: Topology.TargetSelection): void => {
1092
+ if (
1093
+ typeof result !== "object" || result === null || !hasProperty(result, "path") || result.path !== selection.path
1094
+ ) {
1095
+ const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path")
1096
+ ? String(result.path)
1097
+ : "<invalid>"
1098
+ throw new Error(`Machine initial resolver selected "${selection.path}" but constructed "${resultPath}"`)
1099
+ }
1100
+ }
1101
+
1102
+ const compileInitial = (
1103
+ definition: unknown,
1104
+ states: Machine.StateTree,
1105
+ stateNodes: Machine.StateNodes
1106
+ ): {
1107
+ readonly initial: (input?: unknown) => unknown
1108
+ readonly definition: Machine.InitialDefinition
1109
+ } => {
1110
+ if (typeof definition !== "object" || definition === null) {
1111
+ throw new Error("Machine initial definition must be an object")
1112
+ }
1113
+ const selector = makeInitialSelector(stateNodes)
1114
+ const initialBuilder = makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Record<string, any>
1115
+ const branch = captureInitialBranch(definition, selector, initialBuilder)
1116
+ return {
1117
+ initial: (input?: unknown) => {
1118
+ const result = branch.resolve === undefined
1119
+ ? branch.builder()
1120
+ : branch.resolve({ input, target: branch.builder }, undefined)
1121
+ validateInitialSelection(result, branch.selection)
1122
+ return result
1123
+ },
1124
+ definition: Object.freeze({
1125
+ target: branch.selection.path!,
1126
+ selection: transitionTargetSelection(branch.selection) as Machine.InitialDefinition["selection"]
1127
+ })
1128
+ }
1129
+ }
1130
+
767
1131
  export const defineStates: DefineStates = (<const States extends Machine.StateSchemas>(
768
1132
  states: States
769
1133
  ): Machine.DefinedStates<States> => {
770
1134
  StateDefinition.validateStateDefinitions(states, "Machine.defineStates")
771
1135
  return {
772
1136
  states,
773
- initial: makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Machine.InitialBuilder<States>,
774
1137
  get:
775
1138
  ((snapshot: Machine.AtomicSnapshot<string, unknown>, path: string) =>
776
1139
  Topology.getSnapshotByPath(snapshot, path).pipe(
@@ -813,7 +1176,7 @@ type MakeConfig<
813
1176
  readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
814
1177
  readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
815
1178
  readonly input?: Input
816
- readonly initial: (...args: [...Machine.InputArgs<Input>]) => Machine.InitialResult<States, InitialE, InitialR>
1179
+ readonly initial: unknown
817
1180
  }
818
1181
 
819
1182
  type MakeResult<
@@ -890,7 +1253,7 @@ export const make: Make = (<
890
1253
  readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
891
1254
  readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
892
1255
  readonly input?: Input
893
- readonly initial: (...args: [...Machine.InputArgs<Input>]) => Machine.InitialResult<States, InitialE, InitialR>
1256
+ readonly initial: unknown
894
1257
  }
895
1258
  ): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents> => {
896
1259
  StateDefinition.validateStateDefinitions(config.states, "Machine.make")
@@ -902,8 +1265,10 @@ export const make: Make = (<
902
1265
  self.parentEvents = config.parentEvents ?? Protocol.makeEventProtocol("public", [] as const)
903
1266
  self.input = config.input
904
1267
  self.id = config.id
905
- self.initial = config.initial
906
1268
  self.stateNodes = Topology.compileStateNodes(config.states)
1269
+ const compiledInitial = compileInitial(config.initial, config.states, self.stateNodes)
1270
+ self.initial = compiledInitial.initial
1271
+ self.initialDefinition = compiledInitial.definition
907
1272
  self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes)
908
1273
  self.handlers = Object.create(null)
909
1274
  self.handle = makeHandle(self)
@@ -1109,6 +1474,15 @@ export const stateNodes = <M extends Machine.Any>(
1109
1474
  >
1110
1475
  >
1111
1476
 
1477
+ export const initialDefinition = <M extends Machine.Any>(
1478
+ machine: M
1479
+ ): Machine.InitialDefinition<
1480
+ Machine.RootStateIdentifier<Machine.StateIdentifier<Machine.States<M>>>
1481
+ > =>
1482
+ machine.initialDefinition as Machine.InitialDefinition<
1483
+ Machine.RootStateIdentifier<Machine.StateIdentifier<Machine.States<M>>>
1484
+ >
1485
+
1112
1486
  export const transitionDefinitions = <M extends Machine.Any>(
1113
1487
  machine: M
1114
1488
  ): ReadonlyArray<