@typeonce/effect-machine 0.13.0 → 0.14.1

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 +318 -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 +420 -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,339 @@ 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>,
131
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 = (
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) {
224
+ const localScopeNode = getTargetBuilderNode(stateNodes, localScope)
225
+ if (localScopeNode.schema !== undefined) {
226
+ local.with = makeSelectionMethod("state", localScope, "local")
227
+ }
228
+ addSelectionChildren(local, stateNodes, localScope, "local")
229
+ }
230
+ return {
231
+ none: makeSelectionMethod("none", undefined, "local"),
232
+ local,
233
+ branch,
234
+ full,
235
+ history: makeHistorySelectionTree(stateNodes, undefined)
236
+ }
237
+ }
238
+
239
+ const captureDefinitionBranch = (
240
+ branch: unknown,
241
+ selector: unknown,
132
242
  path: string,
133
- trigger: PropertyKey,
134
- transition: unknown
243
+ trigger: PropertyKey
244
+ ): CapturedBranch => {
245
+ if (
246
+ typeof branch !== "object" || branch === null || !hasProperty(branch, "target") ||
247
+ typeof branch.target !== "function"
248
+ ) {
249
+ throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" requires a target selector`)
250
+ }
251
+ const selection = branch.target(selector)
252
+ if (!Topology.isTargetSelection(selection)) {
253
+ throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must select exactly one target`)
254
+ }
255
+ return { ...(branch as DefinitionBranch), selection }
256
+ }
257
+
258
+ const getSelectionBuilder = (
259
+ target: Record<string, any>,
260
+ selection: Topology.TargetSelection,
261
+ stateNodes: Machine.StateNodes,
262
+ source: string
263
+ ): unknown => {
264
+ if (selection.kind === "none") return target.none
265
+ let builder: any
266
+ let parts = selection.path!.split(".")
267
+ if (selection.kind === "history") {
268
+ builder = target.history
269
+ } else if (selection.scope === "local") {
270
+ builder = target.local
271
+ const scope = getLocalTargetScope(stateNodes, source)
272
+ if (scope !== undefined) {
273
+ if (selection.path === scope) {
274
+ builder = builder.with
275
+ parts = []
276
+ } else {
277
+ parts = selection.path!.slice(scope.length + 1).split(".")
278
+ }
279
+ }
280
+ } else if (selection.scope === "branch") {
281
+ builder = target.branch
282
+ } else {
283
+ builder = target.full
284
+ }
285
+ for (const part of parts) builder = builder[part]
286
+ if (selection.kind === "initial") builder = builder.initial
287
+ if (
288
+ typeof builder !== "function" &&
289
+ (typeof builder !== "object" || builder === null || typeof builder.from !== "function")
290
+ ) {
291
+ throw new Error(`Machine could not construct selected transition target "${selection.path}"`)
292
+ }
293
+ return builder
294
+ }
295
+
296
+ const constructSelectedTarget = (builder: any): unknown => typeof builder === "function" ? builder() : builder.from()
297
+
298
+ const validateResolvedSelection = (
299
+ result: unknown,
300
+ selection: Topology.TargetSelection,
301
+ stateNodes: Machine.StateNodes
135
302
  ): void => {
136
- if (typeof transition !== "object" || transition === null || !hasProperty(transition, "targets")) {
303
+ if (selection.kind === "none") {
304
+ if (result !== undefined) {
305
+ throw new Error("Machine targetless transition resolver must return undefined")
306
+ }
137
307
  return
138
308
  }
139
- if (!Array.isArray(transition.targets)) {
309
+ if (result === undefined) return
310
+ const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path") &&
311
+ typeof result.path === "string"
312
+ ? result.path
313
+ : undefined
314
+ const selectedNode = selection.path === undefined ? undefined : stateNodes.byPath.get(selection.path)
315
+ const acceptsDescendant = (selection.scope === "local" || selection.scope === "branch") &&
316
+ (selectedNode?.type === "compound" || selectedNode?.type === "parallel")
317
+ if (
318
+ resultPath === undefined ||
319
+ (resultPath !== selection.path && !(acceptsDescendant && resultPath.startsWith(`${selection.path}.`)))
320
+ ) {
140
321
  throw new Error(
141
- `Machine expected transition targets for state "${path}" on "${String(trigger)}" to be an array`
322
+ `Machine transition resolver selected "${selection.path}" but constructed "${resultPath ?? "<invalid>"}"`
142
323
  )
143
324
  }
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
325
  }
152
326
 
153
- const captureTransition = (transition: unknown): unknown => {
327
+ const runCapturedBranch = (
328
+ branch: CapturedBranch,
329
+ context: Record<string, any>,
330
+ enqueue: unknown,
331
+ stateNodes: Machine.StateNodes,
332
+ source: string,
333
+ match?: { readonly value: unknown }
334
+ ): unknown => {
335
+ const selectedTarget = getSelectionBuilder(context.target, branch.selection, stateNodes, source)
336
+ if (branch.resolve === undefined) return constructSelectedTarget(selectedTarget)
337
+ const resolverContext = { ...context }
338
+ if (branch.selection.kind === "none") delete resolverContext.target
339
+ else resolverContext.target = selectedTarget
340
+ if (match !== undefined) resolverContext.match = match.value
341
+ const resolved = branch.resolve(resolverContext, enqueue)
342
+ validateResolvedSelection(resolved, branch.selection, stateNodes)
343
+ return resolved === undefined ? constructSelectedTarget(selectedTarget) : resolved
344
+ }
345
+
346
+ const captureTransition = (
347
+ transition: unknown,
348
+ stateNodes: Machine.StateNodes,
349
+ path: string,
350
+ trigger: PropertyKey
351
+ ): unknown => {
154
352
  if (typeof transition !== "object" || transition === null) {
155
- return transition
353
+ throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must be an object`)
156
354
  }
157
- const captured = { ...(transition as Record<PropertyKey, unknown>) }
158
- if (Array.isArray(captured.targets)) {
159
- captured.targets = captured.targets.slice()
355
+ const definition = transition as Record<PropertyKey, unknown>
356
+ const selector = makeTargetSelector(stateNodes, path)
357
+ const reenter = definition.reenter === true
358
+ if (Array.isArray(definition.cases)) {
359
+ const rawCases = definition.cases as ReadonlyArray<unknown>
360
+ if (definition.cases.length === 0 || !hasProperty(definition, "otherwise")) {
361
+ throw new Error(
362
+ `Machine conditional transition for state "${path}" on "${String(trigger)}" requires cases and otherwise`
363
+ )
364
+ }
365
+ const cases = rawCases.map((branch) => {
366
+ const captured = captureDefinitionBranch(branch, selector, path, trigger)
367
+ if (typeof captured.title !== "string" || captured.title.length === 0 || typeof captured.when !== "function") {
368
+ throw new Error(
369
+ `Machine conditional transition case for state "${path}" on "${String(trigger)}" requires title and when`
370
+ )
371
+ }
372
+ return captured
373
+ })
374
+ const otherwise = captureDefinitionBranch(definition.otherwise, selector, path, trigger)
375
+ const evaluate = (context: Record<string, any>, enqueue: unknown) => {
376
+ const predicateContext = { ...context }
377
+ delete predicateContext.target
378
+ for (let branchIndex = 0; branchIndex < cases.length; branchIndex++) {
379
+ const branch = cases[branchIndex]!
380
+ const result = branch.when!(predicateContext)
381
+ if (!Option.isOption(result)) {
382
+ throw new Error(`Machine conditional transition case "${branch.title}" must return Option`)
383
+ }
384
+ if (Option.isSome(result)) {
385
+ return {
386
+ result: runCapturedBranch(branch, context, enqueue, stateNodes, path, { value: result.value }),
387
+ branchIndex
388
+ }
389
+ }
390
+ }
391
+ return {
392
+ result: runCapturedBranch(otherwise, context, enqueue, stateNodes, path),
393
+ branchIndex: cases.length
394
+ }
395
+ }
396
+ return {
397
+ reenter,
398
+ targets: [
399
+ ...new Set(
400
+ [...cases, otherwise].flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
401
+ )
402
+ ],
403
+ branches: [
404
+ ...cases.map((branch) => ({
405
+ type: "case" as const,
406
+ title: branch.title!,
407
+ target: branch.selection.path,
408
+ selection: transitionTargetSelection(branch.selection)
409
+ })),
410
+ {
411
+ type: "otherwise" as const,
412
+ target: otherwise.selection.path,
413
+ selection: transitionTargetSelection(otherwise.selection)
414
+ }
415
+ ],
416
+ evaluate,
417
+ transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
418
+ }
419
+ }
420
+ const branch = captureDefinitionBranch(transition, selector, path, trigger)
421
+ const evaluate = (context: Record<string, any>, enqueue: unknown) => ({
422
+ result: runCapturedBranch(branch, context, enqueue, stateNodes, path),
423
+ branchIndex: 0
424
+ })
425
+ return {
426
+ reenter,
427
+ targets: branch.selection.path === undefined ? [] : [branch.selection.path],
428
+ branches: [{
429
+ type: "direct" as const,
430
+ target: branch.selection.path,
431
+ selection: transitionTargetSelection(branch.selection)
432
+ }],
433
+ evaluate,
434
+ transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
160
435
  }
161
- return captured
162
436
  }
163
437
 
164
- const captureEventHandlers = (on: object): Record<PropertyKey, unknown> => {
438
+ const captureEventHandlers = (
439
+ on: object,
440
+ stateNodes: Machine.StateNodes,
441
+ path: string
442
+ ): Record<PropertyKey, unknown> => {
165
443
  // The machine owns its dispatch table. Compiled plans may snapshot these
166
444
  // definitions, so retaining caller-owned containers would let strategies
167
445
  // observe different handlers after an unsafe external mutation.
168
446
  const captured: Record<PropertyKey, unknown> = Object.create(null)
169
447
  for (const event of Reflect.ownKeys(on)) {
170
- captured[event] = captureTransition((on as Record<PropertyKey, unknown>)[event])
448
+ captured[event] = captureTransition((on as Record<PropertyKey, unknown>)[event], stateNodes, path, event)
449
+ }
450
+ return captured
451
+ }
452
+
453
+ const captureInvokeDefinition = (
454
+ invoke: unknown,
455
+ stateNodes: Machine.StateNodes,
456
+ path: string
457
+ ): unknown => {
458
+ if (Array.isArray(invoke)) return invoke.map((item) => captureInvokeDefinition(item, stateNodes, path))
459
+ if (typeof invoke !== "object" || invoke === null) return invoke
460
+ const captured = { ...(invoke as Record<PropertyKey, unknown>) }
461
+ for (const key of ["onDone", "onFailure", "onSnapshot"] as const) {
462
+ if (captured[key] !== undefined) {
463
+ captured[key] = captureTransition(captured[key], stateNodes, path, key)
464
+ }
171
465
  }
172
466
  return captured
173
467
  }
@@ -191,15 +485,21 @@ const flattenHandlers = (
191
485
  const { states: childConfig, ...stateConfig } = nodeConfig as Record<string, unknown>
192
486
  const on = stateConfig.on
193
487
  if (typeof on === "object" && on !== null) {
194
- const capturedOn = captureEventHandlers(on)
488
+ const capturedOn = captureEventHandlers(on, stateNodes, path)
195
489
  stateConfig.on = capturedOn
196
- for (const event of Reflect.ownKeys(capturedOn)) {
197
- validateTransitionTargets(stateNodes, path, event, capturedOn[event])
198
- }
199
490
  }
200
- validateTransitionTargets(stateNodes, path, "always", stateConfig.always)
201
- validateTransitionTargets(stateNodes, path, "done", stateConfig.onDone)
202
- validateTransitionTargets(stateNodes, path, "choice", stateConfig.choice)
491
+ if (stateConfig.always !== undefined) {
492
+ stateConfig.always = captureTransition(stateConfig.always, stateNodes, path, "always")
493
+ }
494
+ if (stateConfig.onDone !== undefined) {
495
+ stateConfig.onDone = captureTransition(stateConfig.onDone, stateNodes, path, "done")
496
+ }
497
+ if (stateConfig.choice !== undefined) {
498
+ stateConfig.choice = captureTransition(stateConfig.choice, stateNodes, path, "choice")
499
+ }
500
+ if (stateConfig.invoke !== undefined) {
501
+ stateConfig.invoke = captureInvokeDefinition(stateConfig.invoke, stateNodes, path)
502
+ }
203
503
  const node = stateNodes.byPath.get(path)
204
504
  if (node?.type === "choice") {
205
505
  if (
@@ -208,7 +508,7 @@ const flattenHandlers = (
208
508
  !hasProperty(stateConfig.choice, "targets") || !Array.isArray(stateConfig.choice.targets) ||
209
509
  stateConfig.choice.targets.length === 0
210
510
  ) {
211
- throw new Error(`Machine choice state "${path}" requires a transition and at least one declared target`)
511
+ throw new Error(`Machine choice state "${path}" requires a transition`)
212
512
  }
213
513
  }
214
514
  handlers[path] = stateConfig as Machine.AnyStateConfig
@@ -764,13 +1064,89 @@ const makeTargetBuilder = <const States extends Machine.StateSchemas>(
764
1064
  }) as Machine.TargetBuilder<States, Source>
765
1065
  }
766
1066
 
1067
+ const makeInitialSelector = (stateNodes: Machine.StateNodes): unknown => {
1068
+ const selector: Record<string, unknown> = {}
1069
+ for (const node of stateNodes.byPath.values()) {
1070
+ if (node.parent === undefined && node.type !== "history" && node.type !== "choice") {
1071
+ selector[node.key] = makeSelectionNode(stateNodes, node.path, "initial")
1072
+ }
1073
+ }
1074
+ return selector
1075
+ }
1076
+
1077
+ const getInitialSelectionBuilder = (
1078
+ initialBuilder: Record<string, any>,
1079
+ selection: Topology.TargetSelection
1080
+ ): (...args: ReadonlyArray<any>) => unknown => {
1081
+ const path = selection.path
1082
+ if (path === undefined || path.includes(".")) {
1083
+ throw new Error("Machine initial target must select one top-level state")
1084
+ }
1085
+ const builder = initialBuilder[path]
1086
+ if (typeof builder !== "function") {
1087
+ throw new Error(`Machine could not construct selected initial state "${path}"`)
1088
+ }
1089
+ return builder
1090
+ }
1091
+
1092
+ const captureInitialBranch = (
1093
+ branch: unknown,
1094
+ selector: unknown,
1095
+ initialBuilder: Record<string, any>
1096
+ ): CapturedBranch & { readonly builder: (...args: ReadonlyArray<any>) => unknown } => {
1097
+ const captured = captureDefinitionBranch(branch, selector, "<machine>", "initial")
1098
+ if (captured.selection.kind !== "state" && captured.selection.kind !== "initial") {
1099
+ throw new Error("Machine initial target must select a top-level state or its declared initial entry")
1100
+ }
1101
+ return { ...captured, builder: getInitialSelectionBuilder(initialBuilder, captured.selection) }
1102
+ }
1103
+
1104
+ const validateInitialSelection = (result: unknown, selection: Topology.TargetSelection): void => {
1105
+ if (
1106
+ typeof result !== "object" || result === null || !hasProperty(result, "path") || result.path !== selection.path
1107
+ ) {
1108
+ const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path")
1109
+ ? String(result.path)
1110
+ : "<invalid>"
1111
+ throw new Error(`Machine initial resolver selected "${selection.path}" but constructed "${resultPath}"`)
1112
+ }
1113
+ }
1114
+
1115
+ const compileInitial = (
1116
+ definition: unknown,
1117
+ states: Machine.StateTree,
1118
+ stateNodes: Machine.StateNodes
1119
+ ): {
1120
+ readonly initial: (input?: unknown) => unknown
1121
+ readonly definition: Machine.InitialDefinition
1122
+ } => {
1123
+ if (typeof definition !== "object" || definition === null) {
1124
+ throw new Error("Machine initial definition must be an object")
1125
+ }
1126
+ const selector = makeInitialSelector(stateNodes)
1127
+ const initialBuilder = makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Record<string, any>
1128
+ const branch = captureInitialBranch(definition, selector, initialBuilder)
1129
+ return {
1130
+ initial: (input?: unknown) => {
1131
+ const result = branch.resolve === undefined
1132
+ ? branch.builder()
1133
+ : branch.resolve({ input, target: branch.builder }, undefined)
1134
+ validateInitialSelection(result, branch.selection)
1135
+ return result
1136
+ },
1137
+ definition: Object.freeze({
1138
+ target: branch.selection.path!,
1139
+ selection: transitionTargetSelection(branch.selection) as Machine.InitialDefinition["selection"]
1140
+ })
1141
+ }
1142
+ }
1143
+
767
1144
  export const defineStates: DefineStates = (<const States extends Machine.StateSchemas>(
768
1145
  states: States
769
1146
  ): Machine.DefinedStates<States> => {
770
1147
  StateDefinition.validateStateDefinitions(states, "Machine.defineStates")
771
1148
  return {
772
1149
  states,
773
- initial: makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Machine.InitialBuilder<States>,
774
1150
  get:
775
1151
  ((snapshot: Machine.AtomicSnapshot<string, unknown>, path: string) =>
776
1152
  Topology.getSnapshotByPath(snapshot, path).pipe(
@@ -813,7 +1189,7 @@ type MakeConfig<
813
1189
  readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
814
1190
  readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
815
1191
  readonly input?: Input
816
- readonly initial: (...args: [...Machine.InputArgs<Input>]) => Machine.InitialResult<States, InitialE, InitialR>
1192
+ readonly initial: unknown
817
1193
  }
818
1194
 
819
1195
  type MakeResult<
@@ -890,7 +1266,7 @@ export const make: Make = (<
890
1266
  readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
891
1267
  readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
892
1268
  readonly input?: Input
893
- readonly initial: (...args: [...Machine.InputArgs<Input>]) => Machine.InitialResult<States, InitialE, InitialR>
1269
+ readonly initial: unknown
894
1270
  }
895
1271
  ): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents> => {
896
1272
  StateDefinition.validateStateDefinitions(config.states, "Machine.make")
@@ -902,8 +1278,10 @@ export const make: Make = (<
902
1278
  self.parentEvents = config.parentEvents ?? Protocol.makeEventProtocol("public", [] as const)
903
1279
  self.input = config.input
904
1280
  self.id = config.id
905
- self.initial = config.initial
906
1281
  self.stateNodes = Topology.compileStateNodes(config.states)
1282
+ const compiledInitial = compileInitial(config.initial, config.states, self.stateNodes)
1283
+ self.initial = compiledInitial.initial
1284
+ self.initialDefinition = compiledInitial.definition
907
1285
  self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes)
908
1286
  self.handlers = Object.create(null)
909
1287
  self.handle = makeHandle(self)
@@ -1109,6 +1487,15 @@ export const stateNodes = <M extends Machine.Any>(
1109
1487
  >
1110
1488
  >
1111
1489
 
1490
+ export const initialDefinition = <M extends Machine.Any>(
1491
+ machine: M
1492
+ ): Machine.InitialDefinition<
1493
+ Machine.RootStateIdentifier<Machine.StateIdentifier<Machine.States<M>>>
1494
+ > =>
1495
+ machine.initialDefinition as Machine.InitialDefinition<
1496
+ Machine.RootStateIdentifier<Machine.StateIdentifier<Machine.States<M>>>
1497
+ >
1498
+
1112
1499
  export const transitionDefinitions = <M extends Machine.Any>(
1113
1500
  machine: M
1114
1501
  ): ReadonlyArray<