From 95c758988419a9e4bffb8682d4b49ae79e545c1e Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sun, 9 Aug 2026 11:44:28 +0200 Subject: [PATCH] Share zero-input process logic --- .changeset/calm-machines-share.md | 5 ++ src/internal/machineProcess.ts | 23 ++++++++- test/Machine.test.ts | 81 +++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-machines-share.md diff --git a/.changeset/calm-machines-share.md b/.changeset/calm-machines-share.md new file mode 100644 index 0000000..dbe838b --- /dev/null +++ b/.changeset/calm-machines-share.md @@ -0,0 +1,5 @@ +--- +"@typeonce/effect-machine": patch +--- + +Reduce retained machine memory by sharing immutable zero-input execution descriptors across process instances while preserving per-instance invoke state. diff --git a/src/internal/machineProcess.ts b/src/internal/machineProcess.ts index 1b85aa8..1fd6659 100644 --- a/src/internal/machineProcess.ts +++ b/src/internal/machineProcess.ts @@ -782,6 +782,11 @@ const makeProcessLogic: < > } +const initialProcessLogicCache = new WeakMap< + Machine.Any, + internalRuntime.ProcessLogic +>() + export const toProcessLogic: < const States extends Machine.StateSchemas, const Events extends ReadonlyArray, @@ -814,7 +819,23 @@ export const toProcessLogic: < | MachineSchemaDecodeError | StartupError | StoppedError -> = (machine, ...args) => makeProcessLogic(machine, { _tag: "Initial", args }) +> = (machine, ...args) => { + if (args.length > 0) { + return makeProcessLogic(machine, { _tag: "Initial", args }) + } + // The execution descriptor stores process-local invoke sessions by each + // runtime address and evaluates initialization/services on every start. A + // zero-argument descriptor is therefore safe to share for the lifetime of + // its immutable machine definition. Input-bearing and resumed starts retain + // their instance-specific entry values below. + const cached = initialProcessLogicCache.get(machine) + if (cached !== undefined) { + return cached as any + } + const logic = makeProcessLogic(machine, { _tag: "Initial", args }) + initialProcessLogicCache.set(machine, logic as any) + return logic +} const toResumedProcessLogic = ( machine: Machine.Any, diff --git a/test/Machine.test.ts b/test/Machine.test.ts index 9e78e6a..fcd1087 100644 --- a/test/Machine.test.ts +++ b/test/Machine.test.ts @@ -3953,6 +3953,87 @@ describe("Machine", () => { }) })) + it.effect("isolates invoked children across concurrent zero-input starts", () => + Effect.gen(function*() { + const childStates = Machine.defineStates({ Idle }) + const childMachine = Machine.make({ + states: childStates.states, + events: [], + initial: () => childStates.initial.Idle(new Idle({ userId: "child" })) + }) + const Child = Machine.child("shared-child", childMachine) + const parentStates = Machine.defineStates({ Loading }) + const parentMachine = Machine.make({ + states: parentStates.states, + events: [], + initial: () => parentStates.initial.Loading(new Loading({ requestId: "parent" })) + }).handle({ + Loading: { + invoke: Machine.invokeMachine({ child: Child }) + } + }) + + const [first, second] = yield* Effect.all( + [Machine.start(parentMachine), Machine.start(parentMachine)], + { concurrency: "unbounded" } + ) + const firstChild = yield* first.childChanges(Child).pipe( + Stream.filter(Option.isSome), + Stream.runHead, + Effect.map(Option.flatten) + ) + const secondChild = yield* second.childChanges(Child).pipe( + Stream.filter(Option.isSome), + Stream.runHead, + Effect.map(Option.flatten) + ) + + assert(Option.isSome(firstChild)) + assert(Option.isSome(secondChild)) + assert.notStrictEqual(firstChild.value, secondChild.value) + + yield* first.stop + assert.deepStrictEqual(yield* second.snapshot, { + status: "active", + state: { path: "Loading", value: new Loading({ requestId: "parent" }) } + }) + assert.deepStrictEqual(yield* secondChild.value.snapshot, { + status: "active", + state: { path: "Idle", value: new Idle({ userId: "child" }) } + }) + yield* second.stop + })) + + it.effect("keeps input-bearing process descriptors instance-specific", () => + Effect.gen(function*() { + const states = Machine.defineStates({ Idle }) + const machine = Machine.make({ + states: states.states, + events: [], + input: Input, + initial: (input) => states.initial.Idle(new Idle({ userId: input.userId })) + }).handle({ + Idle: {} + }) + const [first, second] = yield* Effect.all( + [ + Machine.start(machine, { userId: "first" }), + Machine.start(machine, { userId: "second" }) + ], + { concurrency: "unbounded" } + ) + + assert.deepStrictEqual(yield* first.state, { + path: "Idle", + value: new Idle({ userId: "first" }) + }) + assert.deepStrictEqual(yield* second.state, { + path: "Idle", + value: new Idle({ userId: "second" }) + }) + yield* Effect.all([first.stop, second.stop], { concurrency: "unbounded" }) + })) + it.effect("invokeMachine rejects duplicate active child addresses", () => Effect.gen(function*() { const childStates = Machine.defineStates({ Idle })