@typeonce/effect-machine 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +130 -30
  2. package/dist/Machine.d.ts +722 -262
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +130 -62
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +1 -0
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +23 -0
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
  11. package/dist/internal/machine/commandRuntime.js +2 -5
  12. package/dist/internal/machine/commandRuntime.js.map +1 -1
  13. package/dist/internal/machine/configuration.d.ts +5 -0
  14. package/dist/internal/machine/configuration.d.ts.map +1 -1
  15. package/dist/internal/machine/configuration.js +6 -2
  16. package/dist/internal/machine/configuration.js.map +1 -1
  17. package/dist/internal/machine/executionPlan.d.ts +1 -0
  18. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  19. package/dist/internal/machine/executionPlan.js +57 -24
  20. package/dist/internal/machine/executionPlan.js.map +1 -1
  21. package/dist/internal/machine/initialization.d.ts +5 -0
  22. package/dist/internal/machine/initialization.d.ts.map +1 -0
  23. package/dist/internal/machine/initialization.js +61 -0
  24. package/dist/internal/machine/initialization.js.map +1 -0
  25. package/dist/internal/machine/inspectionRuntime.d.ts +25 -0
  26. package/dist/internal/machine/inspectionRuntime.d.ts.map +1 -0
  27. package/dist/internal/machine/inspectionRuntime.js +79 -0
  28. package/dist/internal/machine/inspectionRuntime.js.map +1 -0
  29. package/dist/internal/machine/invocation.d.ts +2 -1
  30. package/dist/internal/machine/invocation.d.ts.map +1 -1
  31. package/dist/internal/machine/invocation.js +17 -10
  32. package/dist/internal/machine/invocation.js.map +1 -1
  33. package/dist/internal/machine/machine.d.ts +4 -3
  34. package/dist/internal/machine/machine.d.ts.map +1 -1
  35. package/dist/internal/machine/machine.js +327 -32
  36. package/dist/internal/machine/machine.js.map +1 -1
  37. package/dist/internal/machine/planner.d.ts +28 -0
  38. package/dist/internal/machine/planner.d.ts.map +1 -1
  39. package/dist/internal/machine/planner.js +132 -51
  40. package/dist/internal/machine/planner.js.map +1 -1
  41. package/dist/internal/machine/process.d.ts.map +1 -1
  42. package/dist/internal/machine/process.js +4 -2
  43. package/dist/internal/machine/process.js.map +1 -1
  44. package/dist/internal/machine/runtime.d.ts +27 -3
  45. package/dist/internal/machine/runtime.d.ts.map +1 -1
  46. package/dist/internal/machine/runtime.js +440 -64
  47. package/dist/internal/machine/runtime.js.map +1 -1
  48. package/dist/internal/machine/symbols.d.ts +2 -0
  49. package/dist/internal/machine/symbols.d.ts.map +1 -1
  50. package/dist/internal/machine/symbols.js +2 -0
  51. package/dist/internal/machine/symbols.js.map +1 -1
  52. package/dist/internal/machine/topology.d.ts +25 -0
  53. package/dist/internal/machine/topology.d.ts.map +1 -1
  54. package/dist/internal/machine/topology.js +26 -10
  55. package/dist/internal/machine/topology.js.map +1 -1
  56. package/dist/internal/testing/machine/exploration.d.ts.map +1 -1
  57. package/dist/internal/testing/machine/exploration.js +11 -2
  58. package/dist/internal/testing/machine/exploration.js.map +1 -1
  59. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  60. package/dist/internal/testing/machine/finiteModel.js +75 -69
  61. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  62. package/dist/internal/testing/machine/transitionCoverage.d.ts +20 -0
  63. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -0
  64. package/dist/internal/testing/machine/transitionCoverage.js +80 -0
  65. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -0
  66. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  67. package/dist/internal/testing/machine/verification.js +171 -33
  68. package/dist/internal/testing/machine/verification.js.map +1 -1
  69. package/dist/testing/MachineTest.d.ts +97 -22
  70. package/dist/testing/MachineTest.d.ts.map +1 -1
  71. package/dist/testing/MachineTest.js +58 -16
  72. package/dist/testing/MachineTest.js.map +1 -1
  73. package/dist/unstable/cluster/ClusterMachine.d.ts +4 -1
  74. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  75. package/dist/unstable/cluster/ClusterMachine.js +4 -1
  76. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  77. package/dist/unstable/reactivity/AtomMachine.d.ts +21 -3
  78. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  79. package/dist/unstable/reactivity/AtomMachine.js +21 -3
  80. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  81. package/docs/agent-guide.md +240 -65
  82. package/package.json +5 -5
  83. package/src/Machine.ts +1240 -495
  84. package/src/internal/machine/atom.ts +26 -0
  85. package/src/internal/machine/commandRuntime.ts +2 -9
  86. package/src/internal/machine/configuration.ts +28 -3
  87. package/src/internal/machine/executionPlan.ts +72 -24
  88. package/src/internal/machine/initialization.ts +73 -0
  89. package/src/internal/machine/inspectionRuntime.ts +102 -0
  90. package/src/internal/machine/invocation.ts +37 -12
  91. package/src/internal/machine/machine.ts +438 -39
  92. package/src/internal/machine/planner.ts +180 -55
  93. package/src/internal/machine/process.ts +4 -2
  94. package/src/internal/machine/runtime.ts +661 -89
  95. package/src/internal/machine/symbols.ts +3 -0
  96. package/src/internal/machine/topology.ts +65 -12
  97. package/src/internal/testing/machine/exploration.ts +10 -2
  98. package/src/internal/testing/machine/finiteModel.ts +114 -80
  99. package/src/internal/testing/machine/transitionCoverage.ts +116 -0
  100. package/src/internal/testing/machine/verification.ts +216 -58
  101. package/src/testing/MachineTest.ts +118 -28
  102. package/src/unstable/cluster/ClusterMachine.ts +4 -1
  103. package/src/unstable/reactivity/AtomMachine.ts +24 -3
@@ -33,7 +33,7 @@ import type { EnsureExecutable } from "./readiness.js"
33
33
  import * as internalRuntime from "./runtime.js"
34
34
  import * as Serialization from "./serialization.js"
35
35
  import * as StateDefinition from "./stateDefinition.js"
36
- import { ChildMachineLogicTypeId } from "./symbols.js"
36
+ import { ChildMachineLogicTypeId, SnapshotBuilderStateTypeId } from "./symbols.js"
37
37
  import * as Topology from "./topology.js"
38
38
 
39
39
  export {
@@ -45,11 +45,11 @@ export {
45
45
  StartupError,
46
46
  StoppedError
47
47
  } from "./errors.js"
48
- export { ChildMachineLogicTypeId, InitialEventTypeId } from "./symbols.js"
48
+ export { ChildMachineLogicTypeId, InitialEventTypeId, SnapshotBuilderStateTypeId } from "./symbols.js"
49
49
 
50
50
  const TypeId = "~effect/Machine"
51
- export const SnapshotBuilderStateTypeId: unique symbol = Symbol("effect/Machine/SnapshotBuilderState")
52
51
  export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke")
52
+ export const TransitionTypeId: unique symbol = Symbol.for("effect/Machine/Transition")
53
53
  const ChildMachineTypeId = "~effect/Machine/ChildMachine"
54
54
  type IsAny<A> = 0 extends 1 & A ? true : false
55
55
  type MachineRuntimeRequirement = internalRuntime.MachineRuntime
@@ -119,6 +119,7 @@ const cloneWithHandlers = (
119
119
  machine.input = self.input
120
120
  machine.id = self.id
121
121
  machine.initial = self.initial
122
+ machine.initialDefinition = self.initialDefinition
122
123
  machine.stateNodes = self.stateNodes
123
124
  machine.makeTargetBuilder = self.makeTargetBuilder
124
125
  machine.handlers = handlers
@@ -128,47 +129,326 @@ const cloneWithHandlers = (
128
129
  return machine
129
130
  }
130
131
 
131
- 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 = (
132
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) 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,
133
236
  path: string,
134
- trigger: PropertyKey,
135
- 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
136
289
  ): void => {
137
- 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
+ }
138
294
  return
139
295
  }
140
- 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
+ ) {
141
308
  throw new Error(
142
- `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>"}"`
143
310
  )
144
311
  }
145
- for (const target of transition.targets) {
146
- if (typeof target !== "string" || !stateNodes.byPath.has(target)) {
147
- throw new Error(
148
- `Machine transition for state "${path}" on "${String(trigger)}" declares unknown target "${String(target)}"`
149
- )
150
- }
151
- }
152
312
  }
153
313
 
154
- 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 => {
155
339
  if (typeof transition !== "object" || transition === null) {
156
- return transition
340
+ throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must be an object`)
157
341
  }
158
- const captured = { ...(transition as Record<PropertyKey, unknown>) }
159
- if (Array.isArray(captured.targets)) {
160
- captured.targets = captured.targets.slice()
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
+ }
406
+ }
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
161
422
  }
162
- return captured
163
423
  }
164
424
 
165
- const captureEventHandlers = (on: object): Record<PropertyKey, unknown> => {
425
+ const captureEventHandlers = (
426
+ on: object,
427
+ stateNodes: Machine.StateNodes,
428
+ path: string
429
+ ): Record<PropertyKey, unknown> => {
166
430
  // The machine owns its dispatch table. Compiled plans may snapshot these
167
431
  // definitions, so retaining caller-owned containers would let strategies
168
432
  // observe different handlers after an unsafe external mutation.
169
433
  const captured: Record<PropertyKey, unknown> = Object.create(null)
170
434
  for (const event of Reflect.ownKeys(on)) {
171
- 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
+ }
172
452
  }
173
453
  return captured
174
454
  }
@@ -192,15 +472,21 @@ const flattenHandlers = (
192
472
  const { states: childConfig, ...stateConfig } = nodeConfig as Record<string, unknown>
193
473
  const on = stateConfig.on
194
474
  if (typeof on === "object" && on !== null) {
195
- const capturedOn = captureEventHandlers(on)
475
+ const capturedOn = captureEventHandlers(on, stateNodes, path)
196
476
  stateConfig.on = capturedOn
197
- for (const event of Reflect.ownKeys(capturedOn)) {
198
- validateTransitionTargets(stateNodes, path, event, capturedOn[event])
199
- }
200
477
  }
201
- validateTransitionTargets(stateNodes, path, "always", stateConfig.always)
202
- validateTransitionTargets(stateNodes, path, "done", stateConfig.onDone)
203
- 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
+ }
204
490
  const node = stateNodes.byPath.get(path)
205
491
  if (node?.type === "choice") {
206
492
  if (
@@ -209,7 +495,7 @@ const flattenHandlers = (
209
495
  !hasProperty(stateConfig.choice, "targets") || !Array.isArray(stateConfig.choice.targets) ||
210
496
  stateConfig.choice.targets.length === 0
211
497
  ) {
212
- 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`)
213
499
  }
214
500
  }
215
501
  handlers[path] = stateConfig as Machine.AnyStateConfig
@@ -300,6 +586,24 @@ const withFrom = <Method extends (value: unknown, ...args: ReadonlyArray<any>) =
300
586
  return method as Method & { readonly from: (...args: ReadonlyArray<any>) => unknown }
301
587
  }
302
588
 
589
+ const withInitial = <Builder extends object>(
590
+ builder: Builder,
591
+ path: string,
592
+ valued: boolean,
593
+ values?: Readonly<Record<string, unknown>>
594
+ ): Builder => {
595
+ const initial = withFrom(
596
+ (value: unknown) => Topology.makeInitialTarget(path, value, values),
597
+ "leaf",
598
+ valued
599
+ )
600
+ Object.defineProperty(builder, "initial", {
601
+ value: initial,
602
+ enumerable: false
603
+ })
604
+ return builder
605
+ }
606
+
303
607
  const makeSnapshotBuilder = (
304
608
  states: Machine.StateTree,
305
609
  options: SnapshotBuilderOptions
@@ -317,12 +621,15 @@ const makeSnapshotBuilder = (
317
621
  continue
318
622
  }
319
623
  const node = Topology.getStateNodeDefinition(path, definition)
320
- builder[key] = withFrom(
624
+ const method = withFrom(
321
625
  (value: unknown, selector?: (builder: unknown) => unknown) =>
322
626
  makeSnapshotForNode(definition, key, value, selector, options),
323
627
  node.states === undefined ? "leaf" : "nested",
324
628
  node.schema !== undefined
325
629
  )
630
+ builder[key] = node.states === undefined || options.mode !== "full" || options.prefix !== ""
631
+ ? method
632
+ : withInitial(method, path, node.schema !== undefined)
326
633
  }
327
634
  return builder
328
635
  }
@@ -348,7 +655,7 @@ const makeParallelSnapshotBuilder = (
348
655
  }
349
656
  const path = options.prefix === "" ? key : `${options.prefix}.${key}`
350
657
  const node = Topology.getStateNodeDefinition(path, definition)
351
- builder[key] = withFrom(
658
+ const method = withFrom(
352
659
  (value: unknown, selector?: (builder: unknown) => unknown) => {
353
660
  const nextRegions: Record<string, unknown> = {}
354
661
  for (const regionKey of Object.keys(regions)) {
@@ -360,6 +667,7 @@ const makeParallelSnapshotBuilder = (
360
667
  node.states === undefined ? "leaf" : "nested",
361
668
  node.schema !== undefined
362
669
  )
670
+ builder[key] = method
363
671
  }
364
672
  return builder
365
673
  }
@@ -538,7 +846,7 @@ const makeLocalTargetChildBuilder = (
538
846
  builder[child.key] = () => Topology.makeChoiceTarget(child.path, parent.path, values)
539
847
  continue
540
848
  }
541
- builder[child.key] = withFrom(
849
+ const method = withFrom(
542
850
  (value: unknown, selector?: (builder: unknown) => unknown) => {
543
851
  if (child.type === "atomic" || child.type === "final") {
544
852
  return makeTargetWithValues(child.path, value, values)
@@ -572,6 +880,9 @@ const makeLocalTargetChildBuilder = (
572
880
  child.type === "atomic" || child.type === "final" ? "leaf" : "nested",
573
881
  child.schema !== undefined
574
882
  )
883
+ builder[child.key] = child.type === "atomic" || child.type === "final"
884
+ ? method
885
+ : withInitial(method, child.path, child.schema !== undefined, values)
575
886
  }
576
887
  return builder
577
888
  }
@@ -677,6 +988,7 @@ const makeBranchTargetNodeBuilder = (
677
988
  "nested",
678
989
  node.schema !== undefined
679
990
  ) as unknown as Record<string, unknown>
991
+ withInitial(builder, node.path, node.schema !== undefined, values)
680
992
  if (node.type !== "parallel" || source === node.path || source.startsWith(`${node.path}.`)) {
681
993
  addBranchTargetChildren(builder, states, stateNodes, node.path, values, source)
682
994
  }
@@ -739,13 +1051,89 @@ const makeTargetBuilder = <const States extends Machine.StateSchemas>(
739
1051
  }) as Machine.TargetBuilder<States, Source>
740
1052
  }
741
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
+
742
1131
  export const defineStates: DefineStates = (<const States extends Machine.StateSchemas>(
743
1132
  states: States
744
1133
  ): Machine.DefinedStates<States> => {
745
1134
  StateDefinition.validateStateDefinitions(states, "Machine.defineStates")
746
1135
  return {
747
1136
  states,
748
- initial: makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Machine.InitialBuilder<States>,
749
1137
  get:
750
1138
  ((snapshot: Machine.AtomicSnapshot<string, unknown>, path: string) =>
751
1139
  Topology.getSnapshotByPath(snapshot, path).pipe(
@@ -788,7 +1176,7 @@ type MakeConfig<
788
1176
  readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
789
1177
  readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
790
1178
  readonly input?: Input
791
- readonly initial: (...args: [...Machine.InputArgs<Input>]) => Machine.InitialResult<States, InitialE, InitialR>
1179
+ readonly initial: unknown
792
1180
  }
793
1181
 
794
1182
  type MakeResult<
@@ -865,7 +1253,7 @@ export const make: Make = (<
865
1253
  readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
866
1254
  readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
867
1255
  readonly input?: Input
868
- readonly initial: (...args: [...Machine.InputArgs<Input>]) => Machine.InitialResult<States, InitialE, InitialR>
1256
+ readonly initial: unknown
869
1257
  }
870
1258
  ): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents> => {
871
1259
  StateDefinition.validateStateDefinitions(config.states, "Machine.make")
@@ -877,8 +1265,10 @@ export const make: Make = (<
877
1265
  self.parentEvents = config.parentEvents ?? Protocol.makeEventProtocol("public", [] as const)
878
1266
  self.input = config.input
879
1267
  self.id = config.id
880
- self.initial = config.initial
881
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
882
1272
  self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes)
883
1273
  self.handlers = Object.create(null)
884
1274
  self.handle = makeHandle(self)
@@ -1084,6 +1474,15 @@ export const stateNodes = <M extends Machine.Any>(
1084
1474
  >
1085
1475
  >
1086
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
+
1087
1486
  export const transitionDefinitions = <M extends Machine.Any>(
1088
1487
  machine: M
1089
1488
  ): ReadonlyArray<