@typeonce/effect-machine 0.6.0 → 0.6.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 (38) hide show
  1. package/package.json +5 -5
  2. package/src/Machine.ts +6873 -0
  3. package/src/index.ts +1 -0
  4. package/src/internal/machine/activities.ts +108 -0
  5. package/src/internal/machine/atom.ts +636 -0
  6. package/src/internal/machine/cluster.ts +394 -0
  7. package/src/internal/machine/command.ts +58 -0
  8. package/src/internal/machine/commandRuntime.ts +43 -0
  9. package/src/internal/machine/configuration.ts +1331 -0
  10. package/src/internal/machine/errors.ts +87 -0
  11. package/src/internal/machine/executionPlan.ts +996 -0
  12. package/src/internal/machine/invocation.ts +119 -0
  13. package/src/internal/machine/machine.ts +1747 -0
  14. package/src/internal/machine/planner.ts +1933 -0
  15. package/src/internal/machine/process.ts +906 -0
  16. package/src/internal/machine/protocol.ts +322 -0
  17. package/src/internal/machine/readiness.ts +10 -0
  18. package/src/internal/machine/runtime.ts +2512 -0
  19. package/src/internal/machine/serialization.ts +498 -0
  20. package/src/internal/machine/stateDefinition.ts +270 -0
  21. package/src/internal/machine/symbols.ts +2 -0
  22. package/src/internal/machine/topology.ts +479 -0
  23. package/src/internal/testing/machine/arbitrary.ts +102 -0
  24. package/src/internal/testing/machine/exploration.ts +331 -0
  25. package/src/internal/testing/machine/finiteModel.ts +1498 -0
  26. package/src/internal/testing/machine/invariant.ts +372 -0
  27. package/src/internal/testing/machine/probe.ts +79 -0
  28. package/src/internal/testing/machine/referenceModel.ts +1505 -0
  29. package/src/internal/testing/machine/runtime.ts +1710 -0
  30. package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
  31. package/src/internal/testing/machine/trace.ts +150 -0
  32. package/src/internal/testing/machine/verification.ts +1890 -0
  33. package/src/testing/MachineTest.ts +2067 -0
  34. package/src/testing/index.ts +7 -0
  35. package/src/unstable/cluster/ClusterMachine.ts +390 -0
  36. package/src/unstable/cluster/index.ts +1 -0
  37. package/src/unstable/reactivity/AtomMachine.ts +649 -0
  38. package/src/unstable/reactivity/index.ts +1 -0
@@ -0,0 +1,906 @@
1
+ /**
2
+ * Internal machine process integration.
3
+ *
4
+ * @since 0.4.0
5
+ */
6
+
7
+ import * as Cause from "effect/Cause"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Option from "effect/Option"
10
+ import type * as Schema from "effect/Schema"
11
+ import type { ActionError, ExecutionServices, Machine, Runtime } from "../../Machine.js"
12
+ import * as CommandRuntime from "./commandRuntime.js"
13
+ import * as Configuration from "./configuration.js"
14
+ import { InfiniteTransitionError, MachineSchemaDecodeError, StartupError } from "./errors.js"
15
+ import type { StoppedError } from "./errors.js"
16
+ import * as ExecutionPlan from "./executionPlan.js"
17
+ import * as Invocation from "./invocation.js"
18
+ import * as internalPlanner from "./planner.js"
19
+ import * as internalRuntime from "./runtime.js"
20
+ import * as Serialization from "./serialization.js"
21
+
22
+ type IsAny<A> = 0 extends (1 & A) ? true : false
23
+
24
+ type ExcludeCompatibleRuntime<Requirements, Events, Emits> = Requirements extends Runtime.Requirement<
25
+ infer RequiredEvents,
26
+ infer RequiredEmits
27
+ > ? IsAny<Requirements> extends true ? Requirements
28
+ : [RequiredEvents] extends [Events] ? [RequiredEmits] extends [Emits] ? never : Requirements
29
+ : Requirements
30
+ : Requirements
31
+
32
+ type ProcessEntry<States extends Machine.StateSchemas, Input extends Schema.Top> =
33
+ | {
34
+ readonly _tag: "Initial"
35
+ readonly args: [...Machine.InputArgs<Input>]
36
+ }
37
+ | {
38
+ readonly _tag: "Resume"
39
+ readonly snapshot: Machine.Snapshot<States>
40
+ }
41
+
42
+ const runSequentialDiscard = <E, R>(
43
+ effects: ReadonlyArray<Effect.Effect<void, E, R>>
44
+ ): Effect.Effect<void, E, R> =>
45
+ effects.length === 0
46
+ ? Effect.void
47
+ : effects.length === 1
48
+ ? effects[0]!
49
+ : Effect.all(effects, { discard: true })
50
+
51
+ const acknowledgedPlan = (
52
+ planned: {
53
+ readonly next: unknown
54
+ readonly commands: ReadonlyArray<unknown>
55
+ readonly emittedEvents: ReadonlyArray<unknown>
56
+ readonly microsteps: ReadonlyArray<{
57
+ readonly next: unknown
58
+ readonly event: unknown
59
+ readonly transitions?: ReadonlyArray<unknown>
60
+ readonly commands: ReadonlyArray<unknown>
61
+ readonly raisedEvents: ReadonlyArray<unknown>
62
+ readonly emittedEvents: ReadonlyArray<unknown>
63
+ readonly exitPaths: ReadonlyArray<string>
64
+ readonly entryPaths: ReadonlyArray<string>
65
+ readonly changed: boolean
66
+ }>
67
+ readonly done: boolean
68
+ readonly output: unknown
69
+ },
70
+ snapshot: (state: unknown) => unknown
71
+ ): unknown => ({
72
+ next: snapshot(planned.next),
73
+ commands: planned.commands,
74
+ emittedEvents: planned.emittedEvents,
75
+ microsteps: planned.microsteps.map((microstep) => {
76
+ const { transitions: _, ...evidence } = microstep
77
+ return { ...evidence, next: snapshot(microstep.next) }
78
+ }),
79
+ done: planned.done,
80
+ output: planned.output
81
+ })
82
+
83
+ const invokeCapabilityCache = new WeakMap<Machine.Any, boolean>()
84
+
85
+ const hasInvokeCapability = (machine: Machine.Any): boolean => {
86
+ const cached = invokeCapabilityCache.get(machine)
87
+ if (cached !== undefined) {
88
+ return cached
89
+ }
90
+ const hasInvokes = Object.values(
91
+ machine.handlers as Record<string, Machine.AnyStateConfig>
92
+ ).some((config) => config.invoke !== undefined)
93
+ invokeCapabilityCache.set(machine, hasInvokes)
94
+ return hasInvokes
95
+ }
96
+
97
+ const makeChildlessCompiledDrain = (
98
+ machine: Machine.Any,
99
+ checkInitialFinal: boolean
100
+ ): (
101
+ context: internalRuntime.CompiledProcessContext<any, any>
102
+ ) => Effect.Effect<Option.Option<any>, any, any> => {
103
+ const executionPlan = ExecutionPlan.compileExecutionPlan(machine)
104
+ return (context) => {
105
+ let current = context.state()
106
+ if (checkInitialFinal && internalPlanner.isFinalState(machine, current)) {
107
+ return internalPlanner.getFinalOutputEffect(
108
+ machine,
109
+ current,
110
+ internalPlanner.InitialEvent
111
+ ).pipe(Effect.map(Option.some))
112
+ }
113
+
114
+ let configuration = context.executionState
115
+ let liveRuntime: Runtime<unknown, unknown> | undefined
116
+ let loop: Effect.Effect<Option.Option<any>, any, any>
117
+ loop = Effect.suspend(() => {
118
+ const pending = context.pollMessage()
119
+ if (Option.isNone(pending)) {
120
+ context.executionState = undefined
121
+ return Effect.succeed(Option.none())
122
+ }
123
+
124
+ const message = pending.value
125
+ const acknowledged = internalRuntime.isAcknowledgedMessage(message)
126
+ const event = acknowledged ? message.event : message
127
+ const before = current
128
+
129
+ let planned
130
+ try {
131
+ planned = executionPlan.plan(
132
+ configuration ?? executionPlan.fromConfiguration(Configuration.normalizeConfigurationSync(machine, current)),
133
+ event,
134
+ acknowledged
135
+ )
136
+ } catch (error) {
137
+ return error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError
138
+ ? Effect.fail(error)
139
+ : Effect.die(error)
140
+ }
141
+ configuration = planned.next
142
+ context.executionState = configuration
143
+ if (planned.microsteps.length === 0) {
144
+ if (acknowledged) {
145
+ context.completeMessage({ before, plan: acknowledgedPlan(planned, executionPlan.snapshot), after: current })
146
+ }
147
+ return loop
148
+ }
149
+
150
+ const next = executionPlan.snapshot(planned.next)
151
+ const beforeCommit = planned.commands.length === 0
152
+ ? undefined
153
+ : CommandRuntime.runCommands(planned.commands, context.scope)
154
+ const afterCommit = planned.emittedEvents.length === 0
155
+ ? undefined
156
+ : CommandRuntime.runEmittedEvents(
157
+ planned.emittedEvents,
158
+ liveRuntime ??= CommandRuntime.makeLiveRuntime(machine, context.scope)
159
+ )
160
+ const commit = (): Effect.Effect<void> | undefined => {
161
+ const notification = context.commit(next)
162
+ current = next
163
+ return notification
164
+ }
165
+ const continueAfterCommit = (): Effect.Effect<Option.Option<any>, any, any> => {
166
+ const continued = Effect.suspend(() => {
167
+ if (acknowledged) {
168
+ context.completeMessage({ before, plan: acknowledgedPlan(planned, executionPlan.snapshot), after: next })
169
+ }
170
+ return planned.done ? Effect.succeed(Option.some(planned.output)) : loop
171
+ })
172
+ return afterCommit === undefined ? continued : afterCommit.pipe(Effect.andThen(continued))
173
+ }
174
+ const commitAndContinue = (): Effect.Effect<Option.Option<any>, any, any> => {
175
+ const notification = commit()
176
+ const continued = continueAfterCommit()
177
+ const effect = notification === undefined ? continued : notification.pipe(Effect.andThen(continued))
178
+ return notification === undefined && afterCommit === undefined
179
+ ? effect
180
+ : context.runAfterChanges(effect)
181
+ }
182
+
183
+ if (beforeCommit === undefined) {
184
+ return commitAndContinue()
185
+ }
186
+ return context.runAfterChanges(
187
+ beforeCommit.pipe(Effect.andThen(Effect.suspend(commitAndContinue)))
188
+ )
189
+ })
190
+ return internalRuntime.provideMachineRuntime(loop, context.scope)
191
+ }
192
+ }
193
+
194
+ class InvokeExecutionState {
195
+ initialized = false
196
+ initial:
197
+ | {
198
+ readonly configuration: unknown
199
+ readonly activeConfiguration: Configuration.ActiveConfiguration
200
+ readonly entryPaths: ReadonlyArray<string>
201
+ }
202
+ | undefined
203
+
204
+ constructor(initial?: {
205
+ readonly configuration: unknown
206
+ readonly activeConfiguration: Configuration.ActiveConfiguration
207
+ readonly entryPaths: ReadonlyArray<string>
208
+ }) {
209
+ this.initial = initial
210
+ }
211
+ }
212
+
213
+ const makeInvokingCompiledDrain = (
214
+ machine: Machine.Any,
215
+ checkInitialFinal: boolean
216
+ ): (
217
+ context: internalRuntime.CompiledProcessContext<any, any>
218
+ ) => Effect.Effect<Option.Option<any>, any, any> => {
219
+ const executionPlan = ExecutionPlan.compileExecutionPlan(machine)
220
+ return (context) => {
221
+ let current = context.state()
222
+ if (checkInitialFinal && internalPlanner.isFinalState(machine, current)) {
223
+ return internalPlanner.getFinalOutputEffect(
224
+ machine,
225
+ current,
226
+ internalPlanner.InitialEvent
227
+ ).pipe(Effect.map(Option.some))
228
+ }
229
+
230
+ const scope = context.scope
231
+ const stored = context.executionState
232
+ const execution = stored instanceof InvokeExecutionState
233
+ ? stored
234
+ : new InvokeExecutionState()
235
+ context.executionState = execution
236
+ let liveRuntime: Runtime<any, any> | undefined
237
+ let configuration: unknown
238
+
239
+ let loop: Effect.Effect<Option.Option<any>, any, any>
240
+ loop = Effect.suspend(() => {
241
+ const pending = context.pollMessage()
242
+ if (Option.isNone(pending)) {
243
+ configuration = undefined
244
+ return Effect.succeed(Option.none())
245
+ }
246
+
247
+ const message = pending.value
248
+ const acknowledged = internalRuntime.isAcknowledgedMessage(message)
249
+ const event = acknowledged ? message.event : message
250
+ const before = current
251
+
252
+ let planned
253
+ try {
254
+ planned = executionPlan.plan(
255
+ configuration ??
256
+ executionPlan.fromConfiguration(Configuration.normalizeConfigurationSync(machine, current)),
257
+ event,
258
+ acknowledged
259
+ )
260
+ } catch (error) {
261
+ return error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError
262
+ ? Effect.fail(error)
263
+ : Effect.die(error)
264
+ }
265
+ configuration = planned.next
266
+ if (planned.microsteps.length === 0) {
267
+ if (acknowledged) {
268
+ context.completeMessage({ before, plan: acknowledgedPlan(planned, executionPlan.snapshot), after: current })
269
+ }
270
+ return loop
271
+ }
272
+
273
+ const changed = planned.microsteps.some((step) => step.changed)
274
+ const exitPaths = changed ? planned.microsteps.flatMap((step) => step.exitPaths) : []
275
+ const entryEvents = new Map<string, Machine.LifecycleEvent<any>>()
276
+ if (changed) {
277
+ for (const step of planned.microsteps) {
278
+ if (step.changed) {
279
+ for (const path of step.entryPaths) {
280
+ entryEvents.set(path, step.event)
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ const activeConfiguration = executionPlan.toConfiguration(planned.next)
287
+ const next = executionPlan.snapshot(planned.next)
288
+ const beforeCommit: Array<Effect.Effect<void, any, any>> = []
289
+ if (planned.commands.length > 0) {
290
+ beforeCommit.push(CommandRuntime.runCommands(planned.commands, scope))
291
+ }
292
+ if (changed) {
293
+ const stopping = context.ownedChildren.stopPaths(exitPaths)
294
+ if (stopping !== undefined) beforeCommit.push(stopping)
295
+ }
296
+ const afterCommit: Array<Effect.Effect<void, any, any>> = []
297
+ if (planned.emittedEvents.length > 0) {
298
+ afterCommit.push(
299
+ CommandRuntime.runEmittedEvents(
300
+ planned.emittedEvents,
301
+ liveRuntime ??= CommandRuntime.makeLiveRuntime(machine, scope)
302
+ )
303
+ )
304
+ }
305
+ if (planned.done) {
306
+ afterCommit.push(context.ownedChildren.stopAll())
307
+ } else if (changed) {
308
+ for (const [path, entryEvent] of entryEvents) {
309
+ const starting = Invocation.startAll(
310
+ machine,
311
+ scope,
312
+ context.ownedChildren,
313
+ activeConfiguration,
314
+ [path],
315
+ entryEvent
316
+ )
317
+ if (starting !== undefined) afterCommit.push(starting)
318
+ }
319
+ }
320
+
321
+ const commit = (): Effect.Effect<void> | undefined => {
322
+ const notification = context.commit(next)
323
+ current = next
324
+ return notification
325
+ }
326
+ const continueAfterCommit = (): Effect.Effect<Option.Option<any>, any, any> => {
327
+ const continued = Effect.suspend(() => {
328
+ if (acknowledged) {
329
+ context.completeMessage({ before, plan: acknowledgedPlan(planned, executionPlan.snapshot), after: next })
330
+ }
331
+ return planned.done ? Effect.succeed(Option.some(planned.output)) : loop
332
+ })
333
+ return afterCommit.length === 0
334
+ ? continued
335
+ : runSequentialDiscard(afterCommit).pipe(Effect.andThen(continued))
336
+ }
337
+ const commitAndContinue = (): Effect.Effect<Option.Option<any>, any, any> => {
338
+ const notification = commit()
339
+ const continued = continueAfterCommit()
340
+ const effect = notification === undefined ? continued : notification.pipe(Effect.andThen(continued))
341
+ return notification === undefined && afterCommit.length === 0
342
+ ? effect
343
+ : context.runAfterChanges(effect)
344
+ }
345
+ if (beforeCommit.length === 0) {
346
+ return commitAndContinue()
347
+ }
348
+ return context.runAfterChanges(
349
+ runSequentialDiscard(beforeCommit).pipe(Effect.andThen(Effect.suspend(commitAndContinue)))
350
+ )
351
+ })
352
+
353
+ const initialize = (): Effect.Effect<Option.Option<any>, any, any> => {
354
+ if (execution.initialized) {
355
+ return loop
356
+ }
357
+ const seeded = execution.initial
358
+ const initialConfiguration = seeded?.activeConfiguration ??
359
+ Configuration.normalizeConfigurationSync(machine, current)
360
+ configuration = seeded?.configuration ?? executionPlan.fromConfiguration(initialConfiguration)
361
+ const starting = Invocation.startAll(
362
+ machine,
363
+ scope,
364
+ context.ownedChildren,
365
+ initialConfiguration,
366
+ seeded?.entryPaths ?? Configuration.getInitialEntryPaths(machine, initialConfiguration),
367
+ internalPlanner.InitialEvent
368
+ )
369
+ execution.initial = undefined
370
+ execution.initialized = true
371
+ return starting === undefined ? loop : starting.pipe(Effect.andThen(loop))
372
+ }
373
+ return internalRuntime.provideMachineRuntime(Effect.suspend(initialize), scope)
374
+ }
375
+ }
376
+
377
+ const makeProcessLogic: <
378
+ const States extends Machine.StateSchemas,
379
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
380
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
381
+ const Input extends Schema.Top = typeof Schema.Void,
382
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
383
+ E = never,
384
+ R = never,
385
+ InitialE = never,
386
+ InitialR = never,
387
+ FinalStates extends Machine.StateIdentifier<States> = never,
388
+ Output = never
389
+ >(
390
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
391
+ entry: ProcessEntry<States, Input>
392
+ ) => internalRuntime.ProcessLogic<
393
+ Machine.Snapshot<States>,
394
+ Machine.EventOf<Events>,
395
+ E | ActionError<R> | InfiniteTransitionError | MachineSchemaDecodeError | StoppedError,
396
+ ExcludeCompatibleRuntime<
397
+ Exclude<ExecutionServices<InitialR | R>, internalRuntime.MachineRuntime>,
398
+ Machine.EventOf<Events>,
399
+ Machine.EmitOf<Emits>
400
+ >,
401
+ Output,
402
+ | InitialE
403
+ | E
404
+ | ActionError<InitialR | R>
405
+ | InfiniteTransitionError
406
+ | MachineSchemaDecodeError
407
+ | StartupError
408
+ | StoppedError
409
+ > = <
410
+ const States extends Machine.StateSchemas,
411
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
412
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
413
+ const Input extends Schema.Top = typeof Schema.Void,
414
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
415
+ E = never,
416
+ R = never,
417
+ InitialE = never,
418
+ InitialR = never,
419
+ FinalStates extends Machine.StateIdentifier<States> = never,
420
+ Output = never
421
+ >(
422
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
423
+ entry: ProcessEntry<States, Input>
424
+ ) => {
425
+ const hasInvokes = hasInvokeCapability(machine)
426
+ const executionPlan = ExecutionPlan.compileExecutionPlan(machine)
427
+ const initialArgs = entry._tag === "Initial" ? entry.args : []
428
+ const compiledInitial = entry._tag === "Initial" ? executionPlan.initial : undefined
429
+ const makeCompiledInitial = compiledInitial === undefined ? undefined : () => {
430
+ try {
431
+ const planned = compiledInitial(initialArgs)
432
+ const result = {
433
+ state: planned.state as Machine.Snapshot<States>,
434
+ done: planned.done,
435
+ output: planned.output as Output | undefined
436
+ }
437
+ return hasInvokes
438
+ ? {
439
+ ...result,
440
+ executionState: new InvokeExecutionState({
441
+ configuration: planned.configuration,
442
+ activeConfiguration: planned.activeConfiguration,
443
+ entryPaths: planned.initialEntryPaths
444
+ })
445
+ }
446
+ : result
447
+ } catch (error) {
448
+ throw error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError
449
+ ? error
450
+ : new StartupError({ cause: Cause.die(error) })
451
+ }
452
+ }
453
+ const makeInitial = (
454
+ scope: internalRuntime.ProcessScope<Machine.EventOf<Events>>
455
+ ) =>
456
+ compiledInitial === undefined
457
+ ? internalRuntime.provideMachineRuntime(
458
+ internalPlanner.planInitial(machine, ...initialArgs).pipe(
459
+ Effect.flatMap((planned) => {
460
+ const commands = planned.commands.length === 0
461
+ ? undefined
462
+ : CommandRuntime.runCommands(planned.commands, scope)
463
+ const emitted = planned.emittedEvents.length === 0
464
+ ? undefined
465
+ : CommandRuntime.runEmittedEvents(
466
+ planned.emittedEvents,
467
+ CommandRuntime.makeLiveRuntime<Machine.EventOf<Events>, Machine.EmitOf<Emits>>(machine, scope)
468
+ )
469
+ const result = Effect.succeed({
470
+ state: planned.state,
471
+ done: planned.done,
472
+ output: planned.output
473
+ })
474
+ return commands === undefined
475
+ ? emitted === undefined ? result : emitted.pipe(Effect.andThen(result))
476
+ : emitted === undefined
477
+ ? commands.pipe(Effect.andThen(result))
478
+ : commands.pipe(Effect.andThen(emitted), Effect.andThen(result))
479
+ })
480
+ ),
481
+ scope
482
+ )
483
+ : Effect.try({ try: makeCompiledInitial!, catch: (error) => error as any })
484
+ return ({
485
+ execution: {
486
+ _tag: "Compiled",
487
+ childless: !hasInvokes,
488
+ initial: entry._tag === "Initial" ? makeInitial : undefined,
489
+ initialSync: makeCompiledInitial,
490
+ drain: {
491
+ _tag: "Owned",
492
+ run: hasInvokes
493
+ ? makeInvokingCompiledDrain(machine, entry._tag === "Resume")
494
+ : makeChildlessCompiledDrain(machine, entry._tag === "Resume")
495
+ }
496
+ },
497
+ initial: (scope) =>
498
+ entry._tag === "Resume"
499
+ ? internalRuntime.provideMachineRuntime(Serialization.normalizeSnapshotEffect(machine, entry.snapshot), scope)
500
+ : makeInitial(scope).pipe(Effect.map((initialized) => initialized.state)),
501
+ run: (context) =>
502
+ internalRuntime.provideMachineRuntime(
503
+ Effect.gen(function*() {
504
+ const { completeMessage, pollMessage, receiveMessage, state, setState } = context
505
+ if (completeMessage === undefined || pollMessage === undefined || receiveMessage === undefined) {
506
+ return yield* Effect.die(new Error("Machine statechart started without acknowledged mailbox access"))
507
+ }
508
+ let terminal: { readonly output: Output } | undefined
509
+
510
+ let current = yield* state
511
+ if (internalPlanner.isFinalState(machine, current)) {
512
+ return yield* internalPlanner.getFinalOutputEffect<States, Events, Output>(
513
+ machine,
514
+ current,
515
+ internalPlanner.InitialEvent
516
+ )
517
+ }
518
+
519
+ if (!hasInvokes) {
520
+ // A queued batch is produced entirely by this worker, so its
521
+ // configuration is already validated. Drop both caches before
522
+ // blocking again so idle machines retain only the public snapshot.
523
+ // Keeping the loop in this generator avoids a suspended generator
524
+ // per iteration; every iteration still crosses Effect boundaries,
525
+ // so the Effect scheduler remains responsible for cooperative yield.
526
+ let configuration: Configuration.ActiveConfiguration | undefined
527
+ let pendingMessage: Option.Option<internalRuntime.ProcessMessage<Machine.EventOf<Events>>> = Option.none()
528
+ let liveRuntime: Runtime<Machine.EventOf<Events>, Machine.EmitOf<Emits>> | undefined
529
+ while (terminal === undefined) {
530
+ const message = Option.isSome(pendingMessage) ? pendingMessage.value : yield* receiveMessage
531
+ pendingMessage = Option.none()
532
+ const acknowledged = internalRuntime.isAcknowledgedMessage(message)
533
+ const event = acknowledged ? message.event : message
534
+ const before = current
535
+ let planned
536
+ try {
537
+ planned = internalPlanner.planConfiguration(
538
+ machine,
539
+ configuration ?? Configuration.normalizeConfigurationSync(machine, current),
540
+ event
541
+ )
542
+ } catch (error) {
543
+ if (error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError) {
544
+ return yield* error
545
+ }
546
+ throw error
547
+ }
548
+ configuration = planned.next
549
+
550
+ if (planned.microsteps.length > 0) {
551
+ const next = Configuration.snapshotFromConfiguration<States>(machine, planned.next)
552
+ yield* CommandRuntime.runCommands(planned.commands, context)
553
+ yield* setState(next)
554
+ current = next
555
+ if (planned.emittedEvents.length > 0) {
556
+ yield* CommandRuntime.runEmittedEvents(
557
+ planned.emittedEvents as ReadonlyArray<Machine.EmitOf<Emits>>,
558
+ liveRuntime ??= CommandRuntime.makeLiveRuntime(machine, context)
559
+ )
560
+ }
561
+
562
+ if (planned.done) {
563
+ terminal = { output: planned.output }
564
+ }
565
+ }
566
+
567
+ if (acknowledged) {
568
+ completeMessage({
569
+ before,
570
+ plan: acknowledgedPlan(
571
+ planned,
572
+ (state) =>
573
+ Configuration.snapshotFromConfiguration(machine, state as Configuration.ActiveConfiguration)
574
+ ),
575
+ after: current
576
+ })
577
+ }
578
+
579
+ if (terminal === undefined) {
580
+ pendingMessage = yield* pollMessage
581
+ if (Option.isNone(pendingMessage)) {
582
+ configuration = undefined
583
+ }
584
+ }
585
+ }
586
+
587
+ if (terminal === undefined) {
588
+ return yield* Effect.die(
589
+ new Error("Machine process stopped receiving events before reaching a terminal configuration")
590
+ )
591
+ }
592
+ return terminal.output
593
+ }
594
+
595
+ // The execution descriptor requests this owner-local capability only
596
+ // when an invoking statechart is deliberately run by the generic
597
+ // reference strategy.
598
+ const ownedChildren = context.ownedChildren
599
+ if (ownedChildren === undefined) {
600
+ return yield* Effect.die(new Error("Invoking statechart started without an owned child runtime"))
601
+ }
602
+ const startInvokes: (
603
+ configuration: Configuration.ActiveConfiguration,
604
+ paths: ReadonlyArray<string>,
605
+ event: Machine.LifecycleEvent<Events>
606
+ ) => Effect.Effect<void, E | MachineSchemaDecodeError, R> = (configuration, paths, event) =>
607
+ (Invocation.startAll(
608
+ machine,
609
+ context,
610
+ ownedChildren,
611
+ configuration,
612
+ paths,
613
+ event
614
+ ) ?? Effect.void) as Effect.Effect<void, E | MachineSchemaDecodeError, R>
615
+ const stopInvokes = (paths: ReadonlyArray<string>): Effect.Effect<void> =>
616
+ ownedChildren.stopPaths(paths) ?? Effect.void
617
+
618
+ return yield* Effect.gen(function*() {
619
+ let configuration: Configuration.ActiveConfiguration | undefined = yield* Configuration
620
+ .normalizeConfigurationEffect(
621
+ machine,
622
+ current
623
+ )
624
+ yield* startInvokes(
625
+ configuration,
626
+ Configuration.getInitialEntryPaths(machine, configuration),
627
+ internalPlanner.InitialEvent
628
+ )
629
+ // As above, keep the normalized configuration only while this
630
+ // worker can continue draining an already queued batch.
631
+ configuration = undefined
632
+ let pendingMessage: Option.Option<internalRuntime.ProcessMessage<Machine.EventOf<Events>>> = Option.none()
633
+ let liveRuntime: Runtime<Machine.EventOf<Events>, Machine.EmitOf<Emits>> | undefined
634
+
635
+ // Match the compact non-invoke loop while retaining state-scoped
636
+ // child lifecycle work at the same ordered Effect boundaries.
637
+ while (terminal === undefined) {
638
+ const message = Option.isSome(pendingMessage) ? pendingMessage.value : yield* receiveMessage
639
+ pendingMessage = Option.none()
640
+ const acknowledged = internalRuntime.isAcknowledgedMessage(message)
641
+ const event = acknowledged ? message.event : message
642
+ const before = current
643
+ let planned
644
+ try {
645
+ planned = internalPlanner.planConfiguration(
646
+ machine,
647
+ configuration ?? Configuration.normalizeConfigurationSync(machine, current),
648
+ event
649
+ )
650
+ } catch (error) {
651
+ if (error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError) {
652
+ return yield* error
653
+ }
654
+ throw error
655
+ }
656
+ configuration = planned.next
657
+ if (planned.microsteps.length > 0) {
658
+ const changed = planned.microsteps.some((step) => step.changed)
659
+ const exitPaths = planned.microsteps.flatMap((step) => step.exitPaths)
660
+ const entryEvents = new Map<string, Machine.LifecycleEvent<Events>>()
661
+ for (const step of planned.microsteps) {
662
+ if (step.changed) {
663
+ for (const path of step.entryPaths) {
664
+ entryEvents.set(path, step.event as Machine.LifecycleEvent<Events>)
665
+ }
666
+ }
667
+ }
668
+
669
+ const next = Configuration.snapshotFromConfiguration<States>(machine, planned.next)
670
+ yield* CommandRuntime.runCommands(planned.commands, context)
671
+ if (changed) {
672
+ yield* stopInvokes(exitPaths)
673
+ }
674
+ yield* setState(next)
675
+ current = next
676
+ if (planned.emittedEvents.length > 0) {
677
+ yield* CommandRuntime.runEmittedEvents(
678
+ planned.emittedEvents as ReadonlyArray<Machine.EmitOf<Emits>>,
679
+ liveRuntime ??= CommandRuntime.makeLiveRuntime(machine, context)
680
+ )
681
+ }
682
+
683
+ if (planned.done) {
684
+ terminal = { output: planned.output }
685
+ yield* ownedChildren.stopAll()
686
+ } else if (changed) {
687
+ for (const [path, entryEvent] of entryEvents) {
688
+ yield* startInvokes(planned.next, [path], entryEvent)
689
+ }
690
+ }
691
+ }
692
+
693
+ if (acknowledged) {
694
+ completeMessage({
695
+ before,
696
+ plan: acknowledgedPlan(
697
+ planned,
698
+ (state) =>
699
+ Configuration.snapshotFromConfiguration(machine, state as Configuration.ActiveConfiguration)
700
+ ),
701
+ after: current
702
+ })
703
+ }
704
+
705
+ if (terminal === undefined) {
706
+ pendingMessage = yield* pollMessage
707
+ if (Option.isNone(pendingMessage)) {
708
+ configuration = undefined
709
+ }
710
+ }
711
+ }
712
+
713
+ if (terminal === undefined) {
714
+ return yield* Effect.die(
715
+ new Error("Machine process stopped receiving events before reaching a terminal configuration")
716
+ )
717
+ }
718
+ return terminal.output
719
+ }).pipe(
720
+ Effect.onExit(() => ownedChildren.stopAll())
721
+ )
722
+ }),
723
+ context
724
+ )
725
+ }) as internalRuntime.ProcessLogic<
726
+ Machine.Snapshot<States>,
727
+ Machine.EventOf<Events>,
728
+ E | ActionError<R> | InfiniteTransitionError | MachineSchemaDecodeError | StoppedError,
729
+ ExcludeCompatibleRuntime<
730
+ Exclude<ExecutionServices<InitialR | R>, internalRuntime.MachineRuntime>,
731
+ Machine.EventOf<Events>,
732
+ Machine.EmitOf<Emits>
733
+ >,
734
+ Output,
735
+ | InitialE
736
+ | E
737
+ | ActionError<InitialR | R>
738
+ | InfiniteTransitionError
739
+ | MachineSchemaDecodeError
740
+ | StartupError
741
+ | StoppedError
742
+ >
743
+ }
744
+
745
+ const initialProcessLogicCache = new WeakMap<
746
+ Machine.Any,
747
+ internalRuntime.ProcessLogic<any, any, any, any, any, any>
748
+ >()
749
+
750
+ export const toProcessLogic: <
751
+ const States extends Machine.StateSchemas,
752
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
753
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
754
+ const Input extends Schema.Top = typeof Schema.Void,
755
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
756
+ E = never,
757
+ R = never,
758
+ InitialE = never,
759
+ InitialR = never,
760
+ FinalStates extends Machine.StateIdentifier<States> = never,
761
+ Output = never
762
+ >(
763
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
764
+ ...args: [...Machine.InputArgs<Input>]
765
+ ) => internalRuntime.ProcessLogic<
766
+ Machine.Snapshot<States>,
767
+ Machine.EventOf<Events>,
768
+ E | ActionError<R> | InfiniteTransitionError | MachineSchemaDecodeError | StoppedError,
769
+ ExcludeCompatibleRuntime<
770
+ Exclude<ExecutionServices<InitialR | R>, internalRuntime.MachineRuntime>,
771
+ Machine.EventOf<Events>,
772
+ Machine.EmitOf<Emits>
773
+ >,
774
+ Output,
775
+ | InitialE
776
+ | E
777
+ | ActionError<InitialR | R>
778
+ | InfiniteTransitionError
779
+ | MachineSchemaDecodeError
780
+ | StartupError
781
+ | StoppedError
782
+ > = (machine, ...args) => {
783
+ if (args.length > 0) {
784
+ return makeProcessLogic(machine, { _tag: "Initial", args })
785
+ }
786
+ // The execution descriptor stores process-local invoke sessions by each
787
+ // runtime address and evaluates initialization/services on every start. A
788
+ // zero-argument descriptor is therefore safe to share for the lifetime of
789
+ // its immutable machine definition. Input-bearing and resumed starts retain
790
+ // their instance-specific entry values below.
791
+ const cached = initialProcessLogicCache.get(machine)
792
+ if (cached !== undefined) {
793
+ return cached as any
794
+ }
795
+ const logic = makeProcessLogic(machine, { _tag: "Initial", args })
796
+ initialProcessLogicCache.set(machine, logic as any)
797
+ return logic
798
+ }
799
+
800
+ const toResumedProcessLogic = (
801
+ machine: Machine.Any,
802
+ snapshot: Machine.Snapshot<any>
803
+ ): internalRuntime.ProcessLogic<any, any, any, any, any, any> =>
804
+ (makeProcessLogic as any)(machine, { _tag: "Resume", snapshot })
805
+
806
+ /** @internal Test-only runtime strategy selection for a fresh machine. */
807
+ export const startWithRuntimeStrategyForTesting = (
808
+ machine: Machine.Any,
809
+ strategy: internalRuntime.ProcessRuntimeStrategy,
810
+ ...args: ReadonlyArray<unknown>
811
+ ): Effect.Effect<internalRuntime.MachineRef<any, any, any, any>, any, any> =>
812
+ internalRuntime.startProcessWithStrategyForTesting(
813
+ (toProcessLogic as any)(machine, ...args),
814
+ strategy,
815
+ machine.id === undefined ? undefined : { id: machine.id }
816
+ )
817
+
818
+ /** @internal Test-only runtime strategy selection for a resumed machine. */
819
+ export const resumeWithRuntimeStrategyForTesting = (
820
+ machine: Machine.Any,
821
+ snapshot: Machine.Snapshot<any>,
822
+ strategy: internalRuntime.ProcessRuntimeStrategy
823
+ ): Effect.Effect<internalRuntime.MachineRef<any, any, any, any>, any, any> =>
824
+ internalRuntime.startProcessWithStrategyForTesting(
825
+ toResumedProcessLogic(machine, snapshot),
826
+ strategy,
827
+ machine.id === undefined ? undefined : { id: machine.id }
828
+ )
829
+
830
+ export const start: <
831
+ const States extends Machine.StateSchemas,
832
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
833
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
834
+ const Input extends Schema.Top = typeof Schema.Void,
835
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
836
+ E = never,
837
+ R = never,
838
+ InitialE = never,
839
+ InitialR = never,
840
+ FinalStates extends Machine.StateIdentifier<States> = never,
841
+ Output = never
842
+ >(
843
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
844
+ ...args: [...Machine.InputArgs<Input>]
845
+ ) => Effect.Effect<
846
+ internalRuntime.MachineRef<
847
+ Machine.Snapshot<States>,
848
+ Machine.EventOf<Events>,
849
+ | E
850
+ | ActionError<R>
851
+ | InfiniteTransitionError
852
+ | MachineSchemaDecodeError
853
+ | StoppedError,
854
+ Output
855
+ >,
856
+ | InitialE
857
+ | E
858
+ | ActionError<InitialR | R>
859
+ | InfiniteTransitionError
860
+ | MachineSchemaDecodeError
861
+ | StartupError
862
+ | StoppedError,
863
+ ExcludeCompatibleRuntime<
864
+ Exclude<ExecutionServices<InitialR | R>, internalRuntime.MachineRuntime>,
865
+ Machine.EventOf<Events>,
866
+ Machine.EmitOf<Emits>
867
+ >
868
+ > = (machine, ...args) =>
869
+ internalRuntime.startProcess(
870
+ toProcessLogic(machine, ...args),
871
+ machine.id === undefined ? undefined : { id: machine.id }
872
+ ) as any
873
+
874
+ export const resume: <
875
+ const States extends Machine.StateSchemas,
876
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
877
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
878
+ const Input extends Schema.Top = typeof Schema.Void,
879
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
880
+ E = never,
881
+ R = never,
882
+ InitialE = never,
883
+ InitialR = never,
884
+ FinalStates extends Machine.StateIdentifier<States> = never,
885
+ Output = never
886
+ >(
887
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
888
+ snapshot: Machine.Snapshot<States>
889
+ ) => Effect.Effect<
890
+ internalRuntime.MachineRef<
891
+ Machine.Snapshot<States>,
892
+ Machine.EventOf<Events>,
893
+ E | ActionError<R> | InfiniteTransitionError | MachineSchemaDecodeError | StoppedError,
894
+ Output
895
+ >,
896
+ MachineSchemaDecodeError,
897
+ ExcludeCompatibleRuntime<
898
+ Exclude<ExecutionServices<R>, internalRuntime.MachineRuntime>,
899
+ Machine.EventOf<Events>,
900
+ Machine.EmitOf<Emits>
901
+ >
902
+ > = (machine, snapshot) =>
903
+ internalRuntime.startProcess(
904
+ toResumedProcessLogic(machine, snapshot),
905
+ machine.id === undefined ? undefined : { id: machine.id }
906
+ ) as any