Skip to content

Commit 0189ba7

Browse files
Compile machine startup topology
1 parent da95851 commit 0189ba7

4 files changed

Lines changed: 141 additions & 34 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": patch
3+
---
4+
5+
Compile eligible machine initial-state normalization and reuse the validated startup configuration when initializing invoked children.

src/internal/machinePlanner.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1991,7 +1991,7 @@ const compileIndexedExecutionDescriptor = (
19911991
const config = machine.handlers[node.path] as Machine.AnyStateConfig | undefined
19921992
if (
19931993
config?.entry !== undefined || config?.exit !== undefined || config?.always !== undefined ||
1994-
config?.onDone !== undefined || config?.invoke !== undefined || (config as any)?.choice !== undefined ||
1994+
config?.onDone !== undefined || (config as any)?.choice !== undefined ||
19951995
(config as any)?.history !== undefined
19961996
) {
19971997
return undefined
@@ -2710,6 +2710,16 @@ export interface CompiledExecutionPlan {
27102710
state: unknown,
27112711
event: unknown
27122712
) => MacrostepPlan<any, any, any, any, any>
2713+
readonly initial?: (
2714+
args: ReadonlyArray<unknown>
2715+
) => {
2716+
readonly state: Machine.Snapshot<any>
2717+
readonly configuration: unknown
2718+
readonly activeConfiguration: ActiveConfiguration
2719+
readonly initialEntryPaths: ReadonlyArray<string>
2720+
readonly done: boolean
2721+
readonly output: unknown
2722+
}
27132723
}
27142724

27152725
const executionPlanCache = new WeakMap<Machine.Any, CompiledExecutionPlan>()
@@ -2733,7 +2743,43 @@ export const compileExecutionPlan = (machine: Machine.Any): CompiledExecutionPla
27332743
fromConfiguration: (configuration) => indexedConfigurationFromActive(indexed, configuration),
27342744
toConfiguration: (state) => activeConfigurationFromIndexed(indexed, state as IndexedConfiguration),
27352745
snapshot: (state) => snapshotFromIndexed(indexed, state as IndexedConfiguration),
2736-
plan: (state, event) => planIndexedConfiguration(machine, indexed, state as IndexedConfiguration, event)
2746+
plan: (state, event) => planIndexedConfiguration(machine, indexed, state as IndexedConfiguration, event),
2747+
initial: (args) => {
2748+
const inputArgs = machine.input === undefined
2749+
? args
2750+
: args.length === 0
2751+
? (decodeInputSync(machine, machine.input, undefined), args)
2752+
: [decodeInputSync(machine, machine.input, args[0])]
2753+
const initial = machine.initial(...inputArgs as any)
2754+
const active = normalizeConfigurationSync(machine, initial as Machine.Snapshot<any>)
2755+
validateInitialConfiguration(machine, active)
2756+
const completed = completeConfigurationSync(machine, active, InitialEvent).configuration
2757+
const configuration = indexedConfigurationFromActive(indexed, completed)
2758+
const state = snapshotFromIndexed(indexed, configuration)
2759+
const done = isActiveFinalConfiguration(machine, completed)
2760+
if (!done) {
2761+
return {
2762+
state,
2763+
configuration,
2764+
activeConfiguration: completed,
2765+
initialEntryPaths: getInitialEntryPaths(machine, completed),
2766+
done: false,
2767+
output: undefined
2768+
}
2769+
}
2770+
const root = getRootPath(machine, completed)
2771+
if (!completed.outputs.has(root)) {
2772+
throw new Error("Machine reached a terminal configuration without a completed root output")
2773+
}
2774+
return {
2775+
state,
2776+
configuration,
2777+
activeConfiguration: completed,
2778+
initialEntryPaths: getInitialEntryPaths(machine, completed),
2779+
done: true,
2780+
output: completed.outputs.get(root)
2781+
}
2782+
}
27372783
}
27382784
: activePlan((configuration, event) => planConfiguration(machine as any, configuration, event as any))
27392785
executionPlanCache.set(machine, compiled)

src/internal/machineProcess.ts

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
44
* @since 4.0.0
55
*/
66

7+
import * as Cause from "effect/Cause"
78
import * as Effect from "effect/Effect"
89
import * as Exit from "effect/Exit"
910
import * as Option from "effect/Option"
1011
import * as Queue from "effect/Queue"
1112
import type * as Schema from "effect/Schema"
1213
import type { ActionError, ExecutionServices, Machine, Runtime } from "../Machine.js"
13-
import { ChildAlreadyExistsError, InfiniteTransitionError, MachineSchemaDecodeError } from "./machineErrors.js"
14-
import type { StartupError, StoppedError } from "./machineErrors.js"
14+
import {
15+
ChildAlreadyExistsError,
16+
InfiniteTransitionError,
17+
MachineSchemaDecodeError,
18+
StartupError
19+
} from "./machineErrors.js"
20+
import type { StoppedError } from "./machineErrors.js"
1521
import * as Model from "./machineModel.js"
1622
import * as internalPlanner from "./machinePlanner.js"
1723
import * as internalRuntime from "./machineRuntime.js"
@@ -247,9 +253,21 @@ const makeChildlessCompiledDrain = (
247253
class InvokeExecutionKernel {
248254
readonly sessions: Map<string, InvokeSession>
249255
initialized = false
256+
initial:
257+
| {
258+
readonly configuration: unknown
259+
readonly activeConfiguration: Model.ActiveConfiguration
260+
readonly entryPaths: ReadonlyArray<string>
261+
}
262+
| undefined
250263

251-
constructor() {
264+
constructor(initial?: {
265+
readonly configuration: unknown
266+
readonly activeConfiguration: Model.ActiveConfiguration
267+
readonly entryPaths: ReadonlyArray<string>
268+
}) {
252269
this.sessions = new Map()
270+
this.initial = initial
253271
}
254272

255273
private makeSessionKey(path: string, id: string): string {
@@ -499,15 +517,17 @@ const makeInvokingCompiledDrain = (
499517
if (execution.initialized) {
500518
return loop
501519
}
502-
const initialConfiguration = Model.normalizeConfigurationSync(machine, current)
503-
configuration = executionPlan.fromConfiguration(initialConfiguration)
520+
const seeded = execution.initial
521+
const initialConfiguration = seeded?.activeConfiguration ?? Model.normalizeConfigurationSync(machine, current)
522+
configuration = seeded?.configuration ?? executionPlan.fromConfiguration(initialConfiguration)
504523
const starting = execution.startAll(
505524
machine,
506525
context,
507526
initialConfiguration,
508-
Model.getInitialEntryPaths(machine, initialConfiguration),
527+
seeded?.entryPaths ?? Model.getInitialEntryPaths(machine, initialConfiguration),
509528
internalPlanner.InitialEvent
510529
)
530+
execution.initial = undefined
511531
execution.initialized = true
512532
return starting === undefined ? loop : starting.pipe(Effect.andThen(loop))
513533
}
@@ -564,36 +584,63 @@ const makeProcessLogic: <
564584
entry: ProcessEntry<States, Input>
565585
) => {
566586
const hasInvokes = hasInvokeCapability(machine)
587+
const executionPlan = internalPlanner.compileExecutionPlan(machine)
567588
const initialArgs = entry._tag === "Initial" ? entry.args : []
589+
const compiledInitial = entry._tag === "Initial" ? executionPlan.initial : undefined
568590
const makeInitial = (
569591
scope: internalRuntime.ProcessScope<Machine.EventOf<Events>>
570592
) =>
571-
internalRuntime.provideMachineRuntime(
572-
internalPlanner.planInitial(machine, ...initialArgs).pipe(
573-
Effect.flatMap((planned) => {
574-
const commands = planned.commands.length === 0
575-
? undefined
576-
: internalPlanner.runCommands(planned.commands, scope)
577-
const emitted = planned.emittedEvents.length === 0
578-
? undefined
579-
: internalPlanner.runEmittedEvents(
580-
planned.emittedEvents,
581-
internalPlanner.makeLiveRuntime<Machine.EventOf<Events>, Machine.EmitOf<Emits>>(machine, scope)
582-
)
583-
const result = Effect.succeed({
584-
state: planned.state,
585-
done: planned.done,
586-
output: planned.output
593+
compiledInitial === undefined
594+
? internalRuntime.provideMachineRuntime(
595+
internalPlanner.planInitial(machine, ...initialArgs).pipe(
596+
Effect.flatMap((planned) => {
597+
const commands = planned.commands.length === 0
598+
? undefined
599+
: internalPlanner.runCommands(planned.commands, scope)
600+
const emitted = planned.emittedEvents.length === 0
601+
? undefined
602+
: internalPlanner.runEmittedEvents(
603+
planned.emittedEvents,
604+
internalPlanner.makeLiveRuntime<Machine.EventOf<Events>, Machine.EmitOf<Emits>>(machine, scope)
605+
)
606+
const result = Effect.succeed({
607+
state: planned.state,
608+
done: planned.done,
609+
output: planned.output
610+
})
611+
return commands === undefined
612+
? emitted === undefined ? result : emitted.pipe(Effect.andThen(result))
613+
: emitted === undefined
614+
? commands.pipe(Effect.andThen(result))
615+
: commands.pipe(Effect.andThen(emitted), Effect.andThen(result))
587616
})
588-
return commands === undefined
589-
? emitted === undefined ? result : emitted.pipe(Effect.andThen(result))
590-
: emitted === undefined
591-
? commands.pipe(Effect.andThen(result))
592-
: commands.pipe(Effect.andThen(emitted), Effect.andThen(result))
593-
})
594-
),
595-
scope
596-
)
617+
),
618+
scope
619+
)
620+
: Effect.try({
621+
try: () => {
622+
const planned = compiledInitial(initialArgs)
623+
const result = {
624+
state: planned.state as Machine.Snapshot<States>,
625+
done: planned.done,
626+
output: planned.output as Output | undefined
627+
}
628+
return hasInvokes
629+
? {
630+
...result,
631+
executionState: new InvokeExecutionKernel({
632+
configuration: planned.configuration,
633+
activeConfiguration: planned.activeConfiguration,
634+
entryPaths: planned.initialEntryPaths
635+
})
636+
}
637+
: result
638+
},
639+
catch: (error) =>
640+
error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError
641+
? error
642+
: new StartupError({ cause: Cause.die(error) })
643+
})
597644
return ({
598645
[internalRuntime.childlessProcess]: hasInvokes ? undefined : true,
599646
[internalRuntime.compiledProcess]: true,

src/internal/machineRuntime.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,13 @@ export interface ProcessLogic<
284284
scope: ProcessScope<Event>
285285
) => Effect.Effect<
286286
| { readonly state: State; readonly done: false; readonly output: undefined }
287-
| { readonly state: State; readonly done: true; readonly output: Output },
287+
| { readonly state: State; readonly done: true; readonly output: Output }
288+
| {
289+
readonly state: State
290+
readonly done: boolean
291+
readonly output: Output | undefined
292+
readonly executionState: unknown
293+
},
288294
InitialError,
289295
Requirements
290296
>
@@ -1358,6 +1364,9 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
13581364
}
13591365
} else {
13601366
self.compiledContext = new CompiledProcessContextImpl(self.processScope, self)
1367+
if ("executionState" in initialized) {
1368+
self.compiledContext.executionState = initialized.executionState
1369+
}
13611370
}
13621371

13631372
if (self.options.onReady !== undefined) {

0 commit comments

Comments
 (0)