Skip to content

Commit f229e7a

Browse files
Specialize compiled invoked child startup (#72)
* Specialize compiled invoked child startup * Avoid retained state in compiled child startup
1 parent aa6f95f commit f229e7a

5 files changed

Lines changed: 229 additions & 46 deletions

File tree

.changeset/fast-static-children.md

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+
Start eligible compiled invoked machines directly from their synchronous initial kernel while preserving per-instance input evaluation, inherited services, scoped ownership, observation, and terminal behavior. Reuse the immutable process descriptor captured by `Machine.invokeMachine` across parent instances.

src/Machine.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6643,14 +6643,16 @@ export const invokeMachine: {
66436643
readonly onDone?: (context: Machine.InvokeDoneContext<any>) => unknown
66446644
}) => {
66456645
const machine = config.child.machine
6646+
// An invoke descriptor fixes both its machine and input. Compile its process
6647+
// logic once; all mutable execution state belongs to the process instance.
6648+
const logic = machine.input === undefined
6649+
? (internalProcess.toProcessLogic as any)(machine)
6650+
: (internalProcess.toProcessLogic as any)(machine, config.input)
66466651
return {
66476652
id: config.child.id,
66486653
address: config.child.id,
66496654
descriptor: config.child,
6650-
src: () =>
6651-
machine.input === undefined
6652-
? (internalProcess.toProcessLogic as any)(machine)
6653-
: (internalProcess.toProcessLogic as any)(machine, config.input),
6655+
src: () => logic,
66546656
snapshot: config.snapshot,
66556657
onDone: config.onDone,
66566658
[Activities.ActivityMetadataTypeId]: {

src/internal/machineProcess.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,30 @@ const makeProcessLogic: <
535535
const executionPlan = internalPlanner.compileExecutionPlan(machine)
536536
const initialArgs = entry._tag === "Initial" ? entry.args : []
537537
const compiledInitial = entry._tag === "Initial" ? executionPlan.initial : undefined
538+
const makeCompiledInitial = compiledInitial === undefined ? undefined : () => {
539+
try {
540+
const planned = compiledInitial(initialArgs)
541+
const result = {
542+
state: planned.state as Machine.Snapshot<States>,
543+
done: planned.done,
544+
output: planned.output as Output | undefined
545+
}
546+
return hasInvokes
547+
? {
548+
...result,
549+
executionState: new InvokeExecutionKernel({
550+
configuration: planned.configuration,
551+
activeConfiguration: planned.activeConfiguration,
552+
entryPaths: planned.initialEntryPaths
553+
})
554+
}
555+
: result
556+
} catch (error) {
557+
throw error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError
558+
? error
559+
: new StartupError({ cause: Cause.die(error) })
560+
}
561+
}
538562
const makeInitial = (
539563
scope: internalRuntime.ProcessScope<Machine.EventOf<Events>>
540564
) =>
@@ -565,34 +589,12 @@ const makeProcessLogic: <
565589
),
566590
scope
567591
)
568-
: Effect.try({
569-
try: () => {
570-
const planned = compiledInitial(initialArgs)
571-
const result = {
572-
state: planned.state as Machine.Snapshot<States>,
573-
done: planned.done,
574-
output: planned.output as Output | undefined
575-
}
576-
return hasInvokes
577-
? {
578-
...result,
579-
executionState: new InvokeExecutionKernel({
580-
configuration: planned.configuration,
581-
activeConfiguration: planned.activeConfiguration,
582-
entryPaths: planned.initialEntryPaths
583-
})
584-
}
585-
: result
586-
},
587-
catch: (error) =>
588-
error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError
589-
? error
590-
: new StartupError({ cause: Cause.die(error) })
591-
})
592+
: Effect.try({ try: makeCompiledInitial!, catch: (error) => error as any })
592593
return ({
593594
[internalRuntime.childlessProcess]: hasInvokes ? undefined : true,
594595
[internalRuntime.compiledProcess]: true,
595596
[internalRuntime.compiledProcessInitial]: entry._tag === "Initial" ? makeInitial : undefined,
597+
[internalRuntime.compiledProcessInitialSync]: makeCompiledInitial,
596598
[internalRuntime.compiledProcessDrain]: hasInvokes
597599
? makeInvokingCompiledDrain(machine, entry._tag === "Resume")
598600
: makeChildlessCompiledDrain(machine, entry._tag === "Resume"),

src/internal/machineRuntime.ts

Lines changed: 108 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export const compiledProcessDrain: unique symbol = Symbol.for("effect/Machine/co
6262
/** @internal */
6363
export const compiledProcessInitial: unique symbol = Symbol.for("effect/Machine/compiledProcessInitial")
6464

65+
/** @internal */
66+
export const compiledProcessInitialSync: unique symbol = Symbol.for("effect/Machine/compiledProcessInitialSync")
67+
6568
/** @internal */
6669
export const sendParentOverride: unique symbol = Symbol.for("effect/Machine/sendParentOverride")
6770

@@ -363,6 +366,18 @@ export interface ProcessLogic<
363366
InitialError,
364367
Requirements
365368
>
369+
/** @internal */
370+
readonly [compiledProcessInitialSync]?: (
371+
scope: ProcessScope<Event>
372+
) =>
373+
| { readonly state: State; readonly done: false; readonly output: undefined }
374+
| { readonly state: State; readonly done: true; readonly output: Output }
375+
| {
376+
readonly state: State
377+
readonly done: boolean
378+
readonly output: Output | undefined
379+
readonly executionState: unknown
380+
}
366381
run(context: ProcessContext<State, Event>): Effect.Effect<Output, Error, Requirements>
367382
/** @internal */
368383
readonly drain?: (
@@ -563,10 +578,13 @@ interface ChildRuntime {
563578
}
564579

565580
class OwnedChildRuntimeImpl implements OwnedChildRuntime {
581+
private scopedServices: Context.Context<any> | undefined
582+
566583
constructor(
567584
private readonly registry: ChildRegistry,
568585
private readonly self: ProcessAddress<any>,
569-
private readonly runtime: ProcessRuntime
586+
private readonly runtime: ProcessRuntime,
587+
private readonly services?: Context.Context<any>
570588
) {}
571589

572590
private has(key: string): boolean {
@@ -596,15 +614,15 @@ class OwnedChildRuntimeImpl implements OwnedChildRuntime {
596614
if (this.has(options.key) || this.registry.children.has(options.id)) {
597615
return Effect.fail(new ChildAlreadyExistsError({ id: options.duplicateId }))
598616
}
599-
this.registry.scope ??= Scope.makeUnsafe("parallel")
617+
const scope = this.registry.scope ??= Scope.makeUnsafe("parallel")
600618
this.registry.children.set(options.id, {
601619
_tag: "Starting",
602620
token,
603621
ownerKey: options.key,
604622
ownerPath: options.path,
605623
ownerActive: true
606624
})
607-
return startLogicInternal(logic, {
625+
const startOptions: StartInternalOptions = {
608626
detached: true,
609627
id: options.id,
610628
sendParent: (event) => options.sendParent(isCurrent, event),
@@ -620,15 +638,30 @@ class OwnedChildRuntimeImpl implements OwnedChildRuntime {
620638
skipStoppedOutcome: true,
621639
parent: this.self,
622640
runtime: this.runtime
623-
}).pipe(
641+
}
642+
const synchronous = this.services !== undefined && options.onSnapshot === undefined &&
643+
logic[childlessProcess] === true && logic[compiledProcessDrain] !== undefined &&
644+
logic[compiledProcessInitialSync] !== undefined
645+
const start = synchronous
646+
? Effect.flatMap(
647+
this.runtime.nextSessionId,
648+
(sessionId) =>
649+
new CompiledProcess(
650+
logic,
651+
startOptions,
652+
this.scopedServices ??= Context.add(this.services!, Scope.Scope, scope),
653+
sessionId
654+
).initializeOwnedSync()
655+
)
656+
: startLogicInternal(logic, startOptions)
657+
const guarded = start.pipe(
624658
Effect.onExit((exit) => {
625659
if (Exit.isSuccess(exit)) return Effect.void
626660
unregisterChild(this.registry, options.id, token)
627661
return startedChild === undefined ? Effect.void : startedChild.stop
628-
}),
629-
Scope.provide(this.registry.scope),
630-
Effect.asVoid
662+
})
631663
)
664+
return (synchronous ? guarded : Scope.provide(guarded, scope)).pipe(Effect.asVoid)
632665
})
633666
}
634667

@@ -682,7 +715,8 @@ const childlessRuntime: ChildRuntime = {
682715

683716
const makeChildRuntime = (
684717
self: ProcessAddress<any>,
685-
runtime: ProcessRuntime
718+
runtime: ProcessRuntime,
719+
services?: Context.Context<any>
686720
): Effect.Effect<ChildRuntime> =>
687721
Effect.sync(() => {
688722
// Child-registry decisions are synchronous and every access below runs in
@@ -923,7 +957,7 @@ const makeChildRuntime = (
923957
changes,
924958
sendTo,
925959
stop,
926-
owned: new OwnedChildRuntimeImpl(registry, self, runtime)
960+
owned: new OwnedChildRuntimeImpl(registry, self, runtime, services)
927961
}
928962
})
929963

@@ -1407,6 +1441,13 @@ type CompiledTermination =
14071441
| { readonly _tag: "Done"; readonly output: unknown }
14081442
| { readonly _tag: "Failure"; readonly cause: Cause.Cause<unknown> }
14091443

1444+
type CompiledInitialized = {
1445+
readonly state: unknown
1446+
readonly done: boolean | undefined
1447+
readonly output: unknown
1448+
readonly executionState?: unknown
1449+
}
1450+
14101451
// Stopping is commonly used only for resource cleanup. Keep that path free of
14111452
// Error stack capture and materialize the typed join failure only if observed.
14121453
const CompiledStoppedCompletion: unique symbol = Symbol("effect/Machine/CompiledStoppedCompletion")
@@ -1466,11 +1507,58 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
14661507
}
14671508
}
14681509

1510+
initializeOwnedSync(): Effect.Effect<MachineRef<any, any, any, any>, unknown> {
1511+
const parent = this.options.parent
1512+
const sendParent = this.options.sendParent ?? (parent === undefined ? noParentSend : parent.send)
1513+
this.processScope = {
1514+
self: this.address,
1515+
parent,
1516+
spawn: this.childRuntime.spawn,
1517+
sendParent,
1518+
sendTo: this.childRuntime.sendTo,
1519+
stopChild: this.childRuntime.stop,
1520+
failCause: (cause: Cause.Cause<unknown>) => this.failCause(cause)
1521+
}
1522+
const compiledInitial = this.logic[compiledProcessInitialSync]!
1523+
let initialized: CompiledInitialized
1524+
try {
1525+
initialized = compiledInitial(this.processScope)
1526+
} catch (error) {
1527+
this.initializing = false
1528+
return Effect.fail(error)
1529+
}
1530+
this.initializing = false
1531+
this.current = {
1532+
revision: 0,
1533+
terminalizing: false,
1534+
changes: undefined,
1535+
snapshot: { status: "active", state: initialized.state }
1536+
}
1537+
this.compiledContext = new CompiledProcessContextImpl(this.processScope, this.childRuntime.owned, this)
1538+
if ("executionState" in initialized) {
1539+
this.compiledContext.executionState = initialized.executionState
1540+
}
1541+
if (this.options.onReadySync !== undefined && !this.options.onReadySync(this)) {
1542+
this.requestTerminationSync({ _tag: "Stopped" })
1543+
}
1544+
if (initialized.done === true && this.requestedTermination === undefined) {
1545+
this.requestTerminationSync({ _tag: "Done", output: initialized.output })
1546+
}
1547+
if (
1548+
initialized.done === false && this.requestedTermination === undefined &&
1549+
this.mailbox.items === undefined
1550+
) {
1551+
return Effect.succeed(this)
1552+
}
1553+
this.draining = true
1554+
return Effect.provideContext(this.drainRuntime(), this.services).pipe(Effect.as(this))
1555+
}
1556+
14691557
initialize(): Effect.Effect<MachineRef<any, any, any, any>, unknown, any> {
14701558
const self = this
14711559
return Effect.gen(function*() {
14721560
if (self.logic[childlessProcess] !== true) {
1473-
self.childRuntime = yield* makeChildRuntime(self.address, self.options.runtime)
1561+
self.childRuntime = yield* makeChildRuntime(self.address, self.options.runtime, self.services)
14741562
}
14751563
const parent = self.options.parent
14761564
const sendParent = self.options.sendParent ?? (parent === undefined ? noParentSend : parent.send)
@@ -1594,14 +1682,16 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
15941682
}
15951683

15961684
private requestTermination(requested: CompiledTermination): Effect.Effect<boolean> {
1597-
return Effect.sync(() => {
1598-
if (this.requestedTermination !== undefined) {
1599-
return false
1600-
}
1601-
this.requestedTermination = requested
1602-
this.reservedTerminationSnapshot = this.reserveTermination(requested)
1603-
return true
1604-
})
1685+
return Effect.sync(() => this.requestTerminationSync(requested))
1686+
}
1687+
1688+
private requestTerminationSync(requested: CompiledTermination): boolean {
1689+
if (this.requestedTermination !== undefined) {
1690+
return false
1691+
}
1692+
this.requestedTermination = requested
1693+
this.reservedTerminationSnapshot = this.reserveTermination(requested)
1694+
return true
16051695
}
16061696

16071697
private reserveTermination(

0 commit comments

Comments
 (0)