From 039dcbd529e50ba851b2643827710d6f678ee0a8 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sun, 16 Aug 2026 16:15:06 +0200 Subject: [PATCH] Require factories for invoked Effects --- .changeset/curly-effects-invoke.md | 15 ++ README.md | 8 +- docs/agent-guide.md | 15 +- .../src/examples/media-player/machine.ts | 6 +- examples/pokemon/src/machine.ts | 9 +- examples/pokemon/src/machines/replace.ts | 2 +- package.json | 2 +- scripts/fixtures/consumer/consumer.ts | 2 +- scripts/fixtures/consumer/deep-bound.ts | 2 +- scripts/invoke-autocomplete.test.mjs | 92 +++++++++ src/Machine.ts | 194 +++++------------- src/internal/machine/invocation.ts | 2 +- test/internal/machine/activities.test.ts | 4 +- .../machine/strategyDifferential.test.ts | 4 +- test/machine/Invoke.test.ts | 4 +- test/machine/LiveInspection.test.ts | 2 +- test/machine/Machine.test.ts | 16 +- test/machine/Resume.test.ts | 9 +- test/unstable/cluster/ClusterMachine.test.ts | 2 +- typetest/machine/EventConstructors.tst.ts | 2 +- typetest/machine/Machine.tst.ts | 19 +- typetest/testing/MachineTest.tst.ts | 7 +- 22 files changed, 222 insertions(+), 196 deletions(-) create mode 100644 .changeset/curly-effects-invoke.md create mode 100644 scripts/invoke-autocomplete.test.mjs diff --git a/.changeset/curly-effects-invoke.md b/.changeset/curly-effects-invoke.md new file mode 100644 index 0000000..dc662c1 --- /dev/null +++ b/.changeset/curly-effects-invoke.md @@ -0,0 +1,15 @@ +--- +"@typeonce/effect-machine": minor +--- + +Require every `Machine.invoke` `effect` source to be a factory evaluated when its owning state is entered. This gives lifecycle callbacks immediate output and failure inference while making Effect construction timing explicit. + +Wrap previously direct Effects in a zero-argument function: + +```ts +Machine.invoke({ + id: "load", + effect: () => load, + onDone: ({ output, target }) => target.none() +}) +``` diff --git a/README.md b/README.md index db26630..0e31c6b 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ State-scoped work starts on entry and is interrupted on exit: Loading: { invoke: Machine.invoke({ id: "save-document", - effect: saveDocument, + effect: () => saveDocument, onDone: ({ output, target }) => target.full.Saved({ id: output.id }), onFailure: ({ error, target }) => target.full.Failed({ message: String(error) }) }) @@ -443,8 +443,10 @@ lookup. `onDone` is required for a non-`never` output, and `onFailure` is required for a non-`never` typed error; each handler is omitted when its channel is `never`. Defects, interruption, and source-construction failures terminate the owning -runtime. `effect: Effect.sleep(...)` is valid, but `after` keeps timers explicit -and makes static durations visible through activity inspection. +runtime. Effect sources are always factories evaluated when their state is +entered. Use `effect: () => Effect.sleep(...)` for a generic Effect, while +`after` keeps timers explicit and makes static durations visible through +activity inspection. ## Reactivity diff --git a/docs/agent-guide.md b/docs/agent-guide.md index c3c2b97..b3f8ebc 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -731,7 +731,7 @@ receive the typed Effect channels and can transition directly: ```ts invoke: Machine.invoke({ id: "save", - effect: SaveService.save(draft), + effect: () => SaveService.save(draft), onDone: ({ output, target }) => target.full.Saved({ entry: output }), onFailure: ({ error, target }) => target.full.SaveFailed({ message: error.message }) @@ -803,12 +803,13 @@ invoke: Machine.invoke({ ``` The timer starts on state entry and is interrupted on exit. Its `onDone` is -always required. `effect: Effect.sleep(...)` has the same scoped cancellation -behavior, but `after` records timer intent and exposes a static duration through -`Machine.activityDefinitions`. For reusable process logic, provide `logic`, a -state-local lifecycle `id`, and a typed `address`. TypeScript checks the address -protocol against the logic event protocol. Lifecycle ids and addresses serve -different purposes and must both be explicit. +always required. `effect: () => Effect.sleep(...)` has the same scoped +cancellation behavior, but `after` records timer intent and exposes a static +duration through `Machine.activityDefinitions`. Effect sources are always +factories evaluated when their state is entered. For reusable process logic, +provide `logic`, a state-local lifecycle `id`, and a typed `address`. TypeScript +checks the address protocol against the logic event protocol. Lifecycle ids and +addresses serve different purposes and must both be explicit. ## Invoked child statecharts diff --git a/examples/playground/src/examples/media-player/machine.ts b/examples/playground/src/examples/media-player/machine.ts index 5e647dc..56486af 100644 --- a/examples/playground/src/examples/media-player/machine.ts +++ b/examples/playground/src/examples/media-player/machine.ts @@ -53,7 +53,7 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ Paused: { invoke: Machine.invoke({ id: "pause-audio", - effect: pauseAudio, + effect: () => pauseAudio, onDone: ({ target }) => target.none(), onFailure: ({ error, target }, enqueue) => { enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) @@ -75,7 +75,7 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ invoke: [ Machine.invoke({ id: "play-audio", - effect: playAudio, + effect: () => playAudio, onDone: ({ target }) => target.none(), onFailure: ({ error, target }, enqueue) => { enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) @@ -145,7 +145,7 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ Restarting: { invoke: Machine.invoke({ id: "restart-audio", - effect: restartAudio, + effect: () => restartAudio, onDone: ({ target }, enqueue) => { enqueue.raise(MediaPlayerInternalEvents.RestartSucceeded()) return target.none() diff --git a/examples/pokemon/src/machine.ts b/examples/pokemon/src/machine.ts index c24c2c9..b26f3dc 100644 --- a/examples/pokemon/src/machine.ts +++ b/examples/pokemon/src/machine.ts @@ -23,10 +23,11 @@ const machine = Machine.make({ Loading: { invoke: Machine.invoke({ id: "load-team", - effect: Effect.gen(function*() { - const service = yield* PokemonService - return yield* service.getRandomTeam() - }), + effect: () => + Effect.gen(function*() { + const service = yield* PokemonService + return yield* service.getRandomTeam() + }), onDone: ({ output, target }) => target.full.ActiveTeam.from({ team: output }), onFailure: ({ target }) => target.full.Failed.from() }) diff --git a/examples/pokemon/src/machines/replace.ts b/examples/pokemon/src/machines/replace.ts index f116b2f..f8f746d 100644 --- a/examples/pokemon/src/machines/replace.ts +++ b/examples/pokemon/src/machines/replace.ts @@ -46,7 +46,7 @@ export const ReplaceMachine = Machine.make({ Replacing: { invoke: Machine.invoke({ id: "replaceWithRandom", - effect: replaceWithRandom, + effect: () => replaceWithRandom, onDone: ({ output, target }, enqueue) => { enqueue.raise(ReplaceInternalEvents.Replaced({ pokemon: output.pokemon })) return target.none() diff --git a/package.json b/package.json index dabab69..e5f076b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "test": "vitest run", "test:types": "tstyche", "check:architecture": "node --test scripts/check-architecture.test.mjs && node scripts/check-architecture.mjs", - "check:ci": "node --test scripts/ci-changes.test.mjs scripts/runtime-performance-compatibility.test.mjs scripts/runtime-performance-regression.test.mjs", + "check:ci": "node --test scripts/ci-changes.test.mjs scripts/invoke-autocomplete.test.mjs scripts/runtime-performance-compatibility.test.mjs scripts/runtime-performance-regression.test.mjs", "typecheck": "tsc -p tsconfig.json --noEmit", "perf:types": "pnpm build && node scripts/type-performance.mjs", "perf:runtime": "pnpm build && node --expose-gc scripts/runtime-performance.mjs", diff --git a/scripts/fixtures/consumer/consumer.ts b/scripts/fixtures/consumer/consumer.ts index ec4e84d..63e3aef 100644 --- a/scripts/fixtures/consumer/consumer.ts +++ b/scripts/fixtures/consumer/consumer.ts @@ -52,7 +52,7 @@ const cluster = ClusterMachine.make("ConsumerEntity", machine, { }) const invoked = Machine.invoke({ id: "fixture-load", - effect: Effect.succeed("ready"), + effect: () => Effect.succeed("ready"), onDone: ({ target }) => target.none() }) const delayed = Machine.invoke({ diff --git a/scripts/fixtures/consumer/deep-bound.ts b/scripts/fixtures/consumer/deep-bound.ts index 97d29cd..1052783 100644 --- a/scripts/fixtures/consumer/deep-bound.ts +++ b/scripts/fixtures/consumer/deep-bound.ts @@ -98,7 +98,7 @@ const machine = definition.handle({ Idle: { invoke: definition.invoke({ id: "deep-inline-invoke", - effect: Effect.asVoid(ExternalService), + effect: () => Effect.asVoid(ExternalService), onDone: ({ target }) => target.none() }), on: { diff --git a/scripts/invoke-autocomplete.test.mjs b/scripts/invoke-autocomplete.test.mjs new file mode 100644 index 0000000..6efd47a --- /dev/null +++ b/scripts/invoke-autocomplete.test.mjs @@ -0,0 +1,92 @@ +import { strict as assert } from "node:assert" +import fs from "node:fs" +import path from "node:path" +import { test } from "node:test" +import ts from "typescript" + +const projectRoot = path.resolve(import.meta.dirname, "..") +const virtualFile = path.join(projectRoot, "invoke-autocomplete.fixture.ts") +const source = ` +import { Effect } from "effect" +import { Machine } from "./src/index.js" + +const States = Machine.defineStates({ Loading: {}, Done: {}, Failed: {} }) +const definition = Machine.make({ + states: States.states, + events: Machine.events(), + initial: () => States.initial.Loading.from() +}) + +definition.handle({ + Loading: { + invoke: Machine.invoke({ + id: "load", + effect: () => Effect.fail("offline").pipe(Effect.as(1)), + onDone: ({ /*done-context*/ }) => States.initial.Done.from(), + onFailure: ({ /*failure-context*/ }) => States.initial.Failed.from() + }) + }, + Done: {}, + Failed: {} +}) + +definition.handle({ + Loading: { + invoke: Machine.invoke({ + id: "incomplete", + effect: () => Effect.fail("offline").pipe(Effect.as(1)), + /*invoke-properties*/ + }) + }, + Done: {}, + Failed: {} +}) +` + +const config = ts.readConfigFile(path.join(projectRoot, "tsconfig.json"), ts.sys.readFile) +const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, projectRoot) +const host = { + directoryExists: ts.sys.directoryExists, + fileExists: ts.sys.fileExists, + getCompilationSettings: () => parsed.options, + getCurrentDirectory: () => projectRoot, + getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options), + getDirectories: ts.sys.getDirectories, + getNewLine: () => ts.sys.newLine, + getScriptFileNames: () => [...parsed.fileNames, virtualFile], + getScriptSnapshot: (file) => + file === virtualFile + ? ts.ScriptSnapshot.fromString(source) + : fs.existsSync(file) + ? ts.ScriptSnapshot.fromString(fs.readFileSync(file, "utf8")) + : undefined, + getScriptVersion: () => "0", + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile, + realpath: ts.sys.realpath, + useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames +} + +const service = ts.createLanguageService(host) + +const completions = (marker) => { + const position = source.indexOf(`/*${marker}*/`) + assert.notEqual(position, -1) + return new Set(service.getCompletionsAtPosition(virtualFile, position, {})?.entries.map((entry) => entry.name)) +} + +test("contextually completes Effect invocation factories while authoring", () => { + const done = completions("done-context") + assert.equal(done.has("output"), true) + assert.equal(done.has("state"), true) + assert.equal(done.has("target"), true) + + const failure = completions("failure-context") + assert.equal(failure.has("error"), true) + assert.equal(failure.has("state"), true) + assert.equal(failure.has("target"), true) + + const properties = completions("invoke-properties") + assert.equal(properties.has("onDone"), true) + assert.equal(properties.has("onFailure"), true) +}) diff --git a/src/Machine.ts b/src/Machine.ts index cab0a81..4a1f5b8 100644 --- a/src/Machine.ts +++ b/src/Machine.ts @@ -4624,6 +4624,8 @@ export declare namespace Machine { */ export type InvokeResolvedSource = Source extends (...args: any) => infer Resolved ? Resolved : Source + type InvokeFactoryResult = Source extends (...args: any) => infer Resolved ? Resolved : never + type ChildMachineLogic = Child extends ChildMachine ? Logic< Snapshot>, EventInput>, @@ -4645,7 +4647,7 @@ export declare namespace Machine { : never export type InvokeLogic = Invoke extends { readonly effect: infer Source } ? - InvokeResolvedSource extends infer Fx extends Effect.Effect ? Logic< + InvokeFactoryResult extends infer Fx extends Effect.Effect ? Logic< void, never, Effect.Error, @@ -4852,10 +4854,9 @@ export declare namespace Machine { & ( | { readonly id: string - readonly effect: InvokeSource< - Effect.Effect, - InvokeContext - > + readonly effect: ( + context: InvokeContext + ) => Effect.Effect readonly after?: never readonly logic?: never readonly child?: never @@ -4939,60 +4940,6 @@ export declare namespace Machine { : { readonly onFailure?: never } : never - export type EffectInvokeArgs< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - Fx extends Effect.Effect, - Source = Fx, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > = - & { - readonly id: InvokeLifecycleId - readonly effect: Source - readonly after?: never - readonly logic?: never - readonly child?: never - readonly address?: never - readonly onSnapshot?: never - } - & InvokeDoneRequirement< - Effect.Success>, - InvokeTransition< - States, - Events, - Emits, - InvokeDoneContext< - States, - Events, - Emits, - StateId, - Effect.Success>, - InputEvents, - ParentEvents - > - > - > - & InvokeFailureRequirement< - Effect.Error>, - InvokeTransition< - States, - Events, - Emits, - InvokeFailureContext< - States, - Events, - Emits, - StateId, - Effect.Error>, - InputEvents, - ParentEvents - > - > - > - export type TimerInvokeArgs< States extends StateSchemas, Events extends ReadonlyArray, @@ -5178,7 +5125,7 @@ export declare namespace Machine { Raw > = Raw extends InvokeTyped ? unknown : Raw extends { readonly effect: infer Source } ? - InvokeResolvedSource extends infer Fx extends Effect.Effect ? + InvokeFactoryResult extends infer Fx extends Effect.Effect ? & InvokeDoneRequirement< Effect.Success, InvokeTransition< @@ -6918,7 +6865,7 @@ export const decodeSnapshot: < Machine.SnapshotDecodingServices > = internal.decodeSnapshot as any -type DynamicEffectInvokeSource< +type EffectInvokeSource< States extends Machine.StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -6936,7 +6883,7 @@ type DynamicEffectInvokeSource< readonly onSnapshot?: never } -type DynamicEffectDoneHandler< +type EffectDoneHandler< States extends Machine.StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -6957,7 +6904,7 @@ type DynamicEffectDoneHandler< > > -type DynamicEffectFailureHandler< +type EffectFailureHandler< States extends Machine.StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -6978,7 +6925,7 @@ type DynamicEffectFailureHandler< > > -type DynamicEffectInvokeResult< +type EffectInvokeResult< States extends Machine.StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -6995,7 +6942,7 @@ type DynamicEffectInvokeResult< type InvokeChannelIsNever = IsAny extends true ? false : [Value] extends [never] ? true : false -type BoundDynamicEffectInvokeSource< +type BoundEffectInvokeSource< States extends Machine.StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -7015,7 +6962,7 @@ type BoundDynamicEffectInvokeSource< readonly onSnapshot?: never } -type BoundDynamicEffectDoneHandler< +type BoundEffectDoneHandler< States extends Machine.StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -7038,7 +6985,7 @@ type BoundDynamicEffectDoneHandler< > > -type BoundDynamicEffectFailureHandler< +type BoundEffectFailureHandler< States extends Machine.StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -7061,7 +7008,7 @@ type BoundDynamicEffectFailureHandler< > > -type BoundDynamicEffectInvokeResult< +type BoundEffectInvokeResult< States extends Machine.StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -7099,9 +7046,9 @@ export interface Invoker< ) => Effect.Effect >( config: - & BoundDynamicEffectInvokeSource + & BoundEffectInvokeSource & { - readonly onDone: BoundDynamicEffectDoneHandler< + readonly onDone: BoundEffectDoneHandler< States, Events, Emits, @@ -7110,7 +7057,7 @@ export interface Invoker< StateId, Source > - readonly onFailure: BoundDynamicEffectFailureHandler< + readonly onFailure: BoundEffectFailureHandler< States, Events, Emits, @@ -7127,7 +7074,7 @@ export interface Invoker< "onFailure must be omitted when the Effect error is never" ] : [] - ): BoundDynamicEffectInvokeResult + ): BoundEffectInvokeResult < StateId extends Machine.StateIdentifier, const Source extends ( @@ -7135,9 +7082,9 @@ export interface Invoker< ) => Effect.Effect >( config: - & BoundDynamicEffectInvokeSource + & BoundEffectInvokeSource & { - readonly onDone: BoundDynamicEffectDoneHandler< + readonly onDone: BoundEffectDoneHandler< States, Events, Emits, @@ -7152,7 +7099,7 @@ export interface Invoker< "onDone must be omitted when the Effect output is never" ] : [] - ): BoundDynamicEffectInvokeResult + ): BoundEffectInvokeResult < StateId extends Machine.StateIdentifier, const Source extends ( @@ -7160,10 +7107,10 @@ export interface Invoker< ) => Effect.Effect >( config: - & BoundDynamicEffectInvokeSource + & BoundEffectInvokeSource & { readonly onDone?: never - readonly onFailure: BoundDynamicEffectFailureHandler< + readonly onFailure: BoundEffectFailureHandler< States, Events, Emits, @@ -7177,7 +7124,7 @@ export interface Invoker< "onFailure must be omitted when the Effect error is never" ] : [] - ): BoundDynamicEffectInvokeResult + ): BoundEffectInvokeResult < StateId extends Machine.StateIdentifier, const Source extends ( @@ -7185,33 +7132,12 @@ export interface Invoker< ) => Effect.Effect >( config: - & BoundDynamicEffectInvokeSource + & BoundEffectInvokeSource & { readonly onDone?: never readonly onFailure?: never } - ): BoundDynamicEffectInvokeResult - < - StateId extends Machine.StateIdentifier, - const Fx extends Effect.Effect, - const Config extends object - >( - config: - & Config - & Machine.EffectInvokeArgs< - States, - Events, - Emits, - StateId, - Fx, - Fx, - InputEvents, - ParentEvents - > - ): - & Config - & Machine.InvokeOwned - & Machine.InvokeTyped, Effect.Error, Effect.Services, never> + ): BoundEffectInvokeResult , const Config extends object>( config: Config & Machine.TimerInvokeArgs ): Config & Machine.InvokeOwned & Machine.InvokeTyped @@ -7323,12 +7249,13 @@ export interface Invoker< * is required only when the source has a typed failure channel. * * This constructor is an identity at runtime, but preserves lifecycle callback - * inference through published declarations. Effects and durations may be - * supplied directly or derived from the owning state's entry context. Dynamic - * Effect sources infer the owner context, output, error, and service channels - * together without a return annotation. Logic invocations require both a - * lifecycle `id` and a typed communication `address`. Child descriptors already - * own their identity, so `id` and `address` must not be repeated. + * inference through published declarations. Effect factories run when their + * owning state is entered and infer the owner context, output, error, and + * service channels together without a return annotation. Durations may be + * supplied directly or derived from the owning state's entry context. Logic + * invocations require both a lifecycle `id` and a typed communication + * `address`. Child descriptors already own their identity, so `id` and + * `address` must not be repeated. * * Owner references are intentionally non-sendable here because this standalone * constructor does not know the owning definition. When a callback sends to @@ -7339,10 +7266,11 @@ export interface Invoker< * ```ts * invoke: Machine.invoke({ * id: "load", - * effect: Effect.tryPromise({ - * try: () => fetch("/api/data").then((response) => response.json()), - * catch: (cause) => new LoadError({ cause }) - * }), + * effect: () => + * Effect.tryPromise({ + * try: () => fetch("/api/data").then((response) => response.json()), + * catch: (cause) => new LoadError({ cause }) + * }), * onDone: ({ output, target }) => target.full.Ready({ data: output }), * onFailure: ({ error, target }) => target.full.Failed({ error }) * }) @@ -7362,10 +7290,10 @@ export const invoke: { ) => Effect.Effect >( config: - & DynamicEffectInvokeSource + & EffectInvokeSource & { - readonly onDone: DynamicEffectDoneHandler - readonly onFailure: DynamicEffectFailureHandler + readonly onDone: EffectDoneHandler + readonly onFailure: EffectFailureHandler }, ..._validation: InvokeChannelIsNever>> extends true ? [ "onDone must be omitted when the Effect output is never" @@ -7374,7 +7302,7 @@ export const invoke: { "onFailure must be omitted when the Effect error is never" ] : [] - ): DynamicEffectInvokeResult + ): EffectInvokeResult < const States extends Machine.StateSchemas, const Events extends ReadonlyArray, @@ -7385,16 +7313,16 @@ export const invoke: { ) => Effect.Effect >( config: - & DynamicEffectInvokeSource + & EffectInvokeSource & { - readonly onDone: DynamicEffectDoneHandler + readonly onDone: EffectDoneHandler readonly onFailure?: never }, ..._validation: InvokeChannelIsNever>> extends true ? [ "onDone must be omitted when the Effect output is never" ] : [] - ): DynamicEffectInvokeResult + ): EffectInvokeResult < const States extends Machine.StateSchemas, const Events extends ReadonlyArray, @@ -7405,16 +7333,16 @@ export const invoke: { ) => Effect.Effect >( config: - & DynamicEffectInvokeSource + & EffectInvokeSource & { readonly onDone?: never - readonly onFailure: DynamicEffectFailureHandler + readonly onFailure: EffectFailureHandler }, ..._validation: InvokeChannelIsNever>> extends true ? [ "onFailure must be omitted when the Effect error is never" ] : [] - ): DynamicEffectInvokeResult + ): EffectInvokeResult < const States extends Machine.StateSchemas, const Events extends ReadonlyArray, @@ -7425,32 +7353,12 @@ export const invoke: { ) => Effect.Effect >( config: - & DynamicEffectInvokeSource + & EffectInvokeSource & { readonly onDone?: never readonly onFailure?: never } - ): DynamicEffectInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Fx extends Effect.Effect, - const Config extends object - >( - config: - & Config - & Machine.EffectInvokeArgs - ): - & Config - & Machine.InvokeOwned - & Machine.InvokeTyped< - Effect.Success, - Effect.Error, - Effect.Services, - never - > + ): EffectInvokeResult < const States extends Machine.StateSchemas, const Events extends ReadonlyArray, diff --git a/src/internal/machine/invocation.ts b/src/internal/machine/invocation.ts index 81fa5a9..a8f9b6e 100644 --- a/src/internal/machine/invocation.ts +++ b/src/internal/machine/invocation.ts @@ -48,7 +48,7 @@ const resolveOne = ( return { id: String(raw.id), src: () => - oneShot(resolveValue(raw.effect, context) as Effect.Effect) as unknown as Runtime.ProcessLogic< + oneShot(raw.effect(context) as Effect.Effect) as unknown as Runtime.ProcessLogic< any, any, any, diff --git a/test/internal/machine/activities.test.ts b/test/internal/machine/activities.test.ts index 26cb1d1..c198107 100644 --- a/test/internal/machine/activities.test.ts +++ b/test/internal/machine/activities.test.ts @@ -39,7 +39,7 @@ const activityMachine = Machine.make({ }), Machine.invoke({ id: "load-document", - effect: Effect.fail("unavailable").pipe(Effect.as(1)), + effect: () => Effect.fail("unavailable").pipe(Effect.as(1)), onDone: ({ target }) => target.none(), onFailure: ({ target }) => target.none() }), @@ -79,7 +79,7 @@ const machine = { invoke: [ { id: "load-document", - effect: Effect.void, + effect: () => Effect.void, onFailure: () => undefined, type: "effect" }, diff --git a/test/internal/machine/strategyDifferential.test.ts b/test/internal/machine/strategyDifferential.test.ts index b6c2933..062ac1e 100644 --- a/test/internal/machine/strategyDifferential.test.ts +++ b/test/internal/machine/strategyDifferential.test.ts @@ -533,7 +533,7 @@ describe("machine planner and runtime strategies", () => { Loading: { invoke: Machine.invoke({ id: "load", - effect: Effect.succeed(new Loaded({ value: "complete" })), + effect: () => Effect.succeed(new Loaded({ value: "complete" })), onDone: ({ output }) => states.initial.Success(new Success({ value: output.value })) }) }, @@ -575,7 +575,7 @@ describe("machine planner and runtime strategies", () => { Loading: { invoke: Machine.invoke({ id: "load", - effect: Effect.fail("unavailable"), + effect: () => Effect.fail("unavailable"), onFailure: ({ error, target }) => target.full.Failed(new Failed({ error })) }) }, diff --git a/test/machine/Invoke.test.ts b/test/machine/Invoke.test.ts index f6a8473..59925b9 100644 --- a/test/machine/Invoke.test.ts +++ b/test/machine/Invoke.test.ts @@ -25,7 +25,7 @@ describe("inline invoke", () => { Loading: { invoke: Machine.invoke({ id: "load", - effect: Effect.succeed("ready"), + effect: () => Effect.succeed("ready"), onDone: ({ output, target }) => target.full.Complete(new Complete({ value: output })) }) }, @@ -55,7 +55,7 @@ describe("inline invoke", () => { Loading: { invoke: Machine.invoke({ id: "load", - effect: Effect.fail("offline"), + effect: () => Effect.fail("offline"), onFailure: ({ error, target }) => target.full.Failed(new Failed({ message: error })) }) }, diff --git a/test/machine/LiveInspection.test.ts b/test/machine/LiveInspection.test.ts index 624f852..9e0381a 100644 --- a/test/machine/LiveInspection.test.ts +++ b/test/machine/LiveInspection.test.ts @@ -127,7 +127,7 @@ describe("Machine live inspection", () => { initial: () => states.initial.Idle(new Idle({})) }).handle({ Idle: { - invoke: Machine.invoke({ id: "worker", effect: Effect.never }) + invoke: Machine.invoke({ id: "worker", effect: () => Effect.never }) } }) const prepared = yield* Machine.prepare(active) diff --git a/test/machine/Machine.test.ts b/test/machine/Machine.test.ts index 576bbde..3becaa6 100644 --- a/test/machine/Machine.test.ts +++ b/test/machine/Machine.test.ts @@ -730,7 +730,7 @@ describe("Machine", () => { Loading: { invoke: Machine.invoke({ id: "load", - effect: Deferred.await(release), + effect: () => Deferred.await(release), onDone: ({ target }) => target.full.Waiting.from() }) }, @@ -4142,7 +4142,7 @@ describe("Machine", () => { Loading: { invoke: Machine.invoke({ id: "request", - effect: Effect.succeed("done:request-1"), + effect: () => Effect.succeed("done:request-1"), onDone: ({ output, target }) => target.full.Success(new Success({ requestId: output })) }) }, @@ -4183,7 +4183,7 @@ describe("Machine", () => { Loading: { invoke: Machine.invoke({ id: "request", - effect: Effect.succeed("done:request-1"), + effect: () => Effect.succeed("done:request-1"), onDone: ({ output, target }) => target.full.Success(new Success({ requestId: output })) }) }, @@ -4487,7 +4487,7 @@ describe("Machine", () => { Loading: { invoke: Machine.invoke({ id: "request", - effect: Effect.fail(error), + effect: () => Effect.fail(error), onFailure: ({ error, target }) => target.full.Failed(new Failed({ message: error.message })) }) }, @@ -4528,7 +4528,7 @@ describe("Machine", () => { Loading: { invoke: Machine.invoke({ id: "request", - effect: Effect.succeed("loaded"), + effect: () => Effect.succeed("loaded"), onDone: ({ output, target }) => target.full.Success(new Success({ requestId: output })) }) }, @@ -4648,7 +4648,7 @@ describe("Machine", () => { Loading: { invoke: Machine.invoke({ id: "request", - effect: Effect.fail(failure), + effect: () => Effect.fail(failure), onFailure: ({ error, target }) => target.full.Failed(new Failed({ message: error.message })) }) }, @@ -4676,7 +4676,7 @@ describe("Machine", () => { Loading: { invoke: Machine.invoke({ id: "request", - effect: requiredMessage, + effect: () => requiredMessage, onDone: ({ output, target }) => target.full.Success(new Success({ requestId: output })) }) }, @@ -4782,7 +4782,7 @@ describe("Machine", () => { Loading: { invoke: Machine.invoke({ id: "request", - effect: Effect.die(error) + effect: () => Effect.die(error) }) } }) diff --git a/test/machine/Resume.test.ts b/test/machine/Resume.test.ts index 16bc4f9..ed7ffd2 100644 --- a/test/machine/Resume.test.ts +++ b/test/machine/Resume.test.ts @@ -338,7 +338,7 @@ describe("Machine.resume", () => { Loading: { invoke: Machine.invoke({ id: "load", - effect: Ref.updateAndGet(runs, (n) => n + 1).pipe(Effect.as("fresh")), + effect: () => Ref.updateAndGet(runs, (n) => n + 1).pipe(Effect.as("fresh")), onDone: ({ output, target }) => target.full.Loaded(new Loaded({ value: output })) }) }, @@ -373,9 +373,10 @@ describe("Machine.resume", () => { Loading: { invoke: Machine.invoke({ id: "load", - effect: Ref.update(runs, (n) => n + 1).pipe( - Effect.andThen(Effect.fail(new LoadFailure({ message: "offline" }))) - ), + effect: () => + Ref.update(runs, (n) => n + 1).pipe( + Effect.andThen(Effect.fail(new LoadFailure({ message: "offline" }))) + ), onFailure: ({ error, target }) => target.full.Failed(new Failed({ message: error.message })) }) }, diff --git a/test/unstable/cluster/ClusterMachine.test.ts b/test/unstable/cluster/ClusterMachine.test.ts index 1296a90..71f89b7 100644 --- a/test/unstable/cluster/ClusterMachine.test.ts +++ b/test/unstable/cluster/ClusterMachine.test.ts @@ -567,7 +567,7 @@ describe("ClusterMachine", () => { Count: { invoke: Machine.invoke({ id: "child", - effect: Effect.void, + effect: () => Effect.void, onDone: ({ target }) => target.none() }) } diff --git a/typetest/machine/EventConstructors.tst.ts b/typetest/machine/EventConstructors.tst.ts index 686a196..ee45a6e 100644 --- a/typetest/machine/EventConstructors.tst.ts +++ b/typetest/machine/EventConstructors.tst.ts @@ -112,7 +112,7 @@ describe("Machine event constructor collections", () => { invoke: [ Machine.invoke({ id: "load", - effect: Effect.succeed("ready"), + effect: () => Effect.succeed("ready"), onDone: ({ output, target }, enqueue) => { enqueue.raise(internalEvents.Loaded({ value: output })) return target.none() diff --git a/typetest/machine/Machine.tst.ts b/typetest/machine/Machine.tst.ts index ce5c654..298d4d2 100644 --- a/typetest/machine/Machine.tst.ts +++ b/typetest/machine/Machine.tst.ts @@ -517,7 +517,7 @@ describe("Machine", () => { }) }) - it("invoke handles one-shot outputs directly in the owning state", () => { + it("invoke infers one-shot outputs from factories in the owning state", () => { const machine = Machine.make({ states: UpStates.states, events: Machine.events(SignIn), @@ -528,7 +528,7 @@ describe("Machine", () => { down: { invoke: Machine.invoke({ id: "valid", - effect: Effect.succeed(1), + effect: () => Effect.succeed(1), onDone: ({ output, state, target }) => { expect(output).type.toBe() expect(state).type.toBe() @@ -539,7 +539,12 @@ describe("Machine", () => { }) expect(Machine.invoke).type.not.toBeCallableWith({ id: "invalid", - effect: Effect.succeed(1) + effect: () => Effect.succeed(1) + }) + expect(Machine.invoke).type.not.toBeCallableWith({ + id: "direct-effect", + effect: Effect.succeed(1), + onDone: () => undefined }) }) @@ -754,8 +759,8 @@ describe("Machine", () => { }) it("invoke requires only the lifecycle handlers reachable from the source type", () => { - const failure = Effect.fail("unavailable" as const) - const erasedFailure = failure as Effect.Effect + const failure = () => Effect.fail("unavailable" as const) + const erasedFailure = () => Effect.fail("unavailable" as const) as Effect.Effect const machine = Machine.make({ states: UpStates.states, events: Machine.events(SignIn), @@ -769,7 +774,7 @@ describe("Machine", () => { }) expect(Machine.invoke).type.not.toBeCallableWith({ id: "unreachable-failure", - effect: Effect.succeed("user-1"), + effect: () => Effect.succeed("user-1"), onDone: () => undefined, onFailure: () => undefined }) @@ -918,7 +923,7 @@ describe("Machine", () => { signedOut: { invoke: Machine.invoke({ id: "nested", - effect: Effect.succeed(Option.some(1)), + effect: () => Effect.succeed(Option.some(1)), onDone: ({ output, state, target }) => { expect(output).type.toBe>() expect(state).type.toBe() diff --git a/typetest/testing/MachineTest.tst.ts b/typetest/testing/MachineTest.tst.ts index 4835b84..fe48753 100644 --- a/typetest/testing/MachineTest.tst.ts +++ b/typetest/testing/MachineTest.tst.ts @@ -102,9 +102,10 @@ describe("MachineTest", () => { idle: { invoke: Machine.invoke({ id: "service-backed-invoke", - effect: Effect.gen(function*() { - yield* InvokeRequirement - }), + effect: () => + Effect.gen(function*() { + yield* InvokeRequirement + }), onDone: ({ target }) => target.none() }) }