diff --git a/.changeset/fluent-state-invocations.md b/.changeset/fluent-state-invocations.md new file mode 100644 index 0000000..e261ecd --- /dev/null +++ b/.changeset/fluent-state-invocations.md @@ -0,0 +1,18 @@ +--- +"@typeonce/effect-machine": minor +--- + +Replace `Machine.invoke` and its object-configuration helper types with state-local fluent invocation chains. Select an Effect, Stream, timer, process logic, or child from the handler's `from` parameter, then handle every reachable lifecycle channel before returning the chain: + +```ts +machine.handle({ + Loading: { + invoke: (from) => + from.effect("load", () => loadUser()) + .onDone((to) => to.full.Ready()) + .onFailure((to) => to.full.Failed()) + } +}) +``` + +Return an array of completed chains for multiple activities. Sources and child descriptors remain reusable, while keeping the invocation declaration local preserves exact owner-state, event, parent, output, failure, element, snapshot, and service inference. diff --git a/README.md b/README.md index 80e7966..6bffca1 100644 --- a/README.md +++ b/README.md @@ -437,37 +437,35 @@ arbitrary asynchronous Effects do not run inside planning. State-scoped work starts on entry and is interrupted on exit: ```ts -Loading: { - invoke: Machine.invoke({ - id: "save-document", - effect: () => saveDocument, - onDone: (to) => to.full.Saved().resolve(({ output, target }) => target.from({ id: output.id })), - onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: String(error) })) - }) -} - -Waiting: { - invoke: Machine.invoke({ - id: "save-timeout", - after: "3 seconds", - onDone: (to) => to.full.Failed().resolve(({ target }) => target.from({ message: "Timed out" })) - }) -} +machine.handle({ + Loading: { + invoke: (from) => + from.effect("save-document", () => saveDocument) + .onDone((to) => to.full.Saved().resolve(({ output, target }) => target.from({ id: output.id }))) + .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: String(error) }))) + }, + Waiting: { + invoke: (from) => + from.timer("save-timeout", "3 seconds") + .onDone((to) => to.full.Failed().resolve(({ target }) => target.from({ message: "Timed out" }))) + } +}) ``` -Use `effect` for one Effect, `stream` for a sequence of externally produced -values, `after` for a cancellable delay, `logic` for a reusable process, and -`child` for a complete child statechart—all through -`Machine.invoke({...})`. The helper is an identity at runtime and preserves -owner-context and source-channel inference across lifecycle handlers, including -for state-dependent Effects: +The state-local `from` selector starts an `effect`, `stream`, `timer`, reusable +`logic`, or complete `child` statechart. The selected source determines which +lifecycle methods the chain requires and which methods are available. For +example, an Effect with non-`never` output and error channels must handle both; +the completed chain is the value returned by the callback: ```ts -invoke: Machine.invoke({ - id: "load-document", - effect: ({ state }) => loadDocument(state.documentId), - onDone: (to) => to.full.Ready().resolve(({ output, target }) => target.from({ document: output })), - onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: error.message })) +machine.handle({ + Loading: { + invoke: (from) => + from.effect("load-document", ({ state }) => loadDocument(state.documentId)) + .onDone((to) => to.full.Ready().resolve(({ output, target }) => target.from({ document: output }))) + .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: error.message }))) + } }) ``` @@ -476,15 +474,18 @@ is mapped by `onElement`, and the next element is not pulled until that parent macrostep commits: ```ts -invoke: Machine.invoke({ - id: "channel", - stream: () => channelMessages, - onElement: (to) => - to.none.resolve(({ element }, enqueue) => { - enqueue.raise(Events.MessageReceived({ message: element })) - }), - onDone: (to) => to.none, - onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target.from({ error })) +machine.handle({ + Listening: { + invoke: (from) => + from.stream("channel", () => channelMessages) + .onElement((to) => + to.none.resolve(({ element }, enqueue) => { + enqueue.raise(Events.MessageReceived({ message: element })) + }) + ) + .onDone((to) => to.none) + .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ error }))) + } }) ``` @@ -493,10 +494,10 @@ current configuration, or call `.resolve(...)` when the transition only needs to enqueue commands. A block resolver may omit its return because it is contextually typed to return `undefined`. -Inside `.handle(...)`, `Machine.invoke(...)` receives the owning machine's -public input and declared parent protocol contextually. Its source and lifecycle -callbacks can send through `self` and `parent` while retaining the invoked -Effect's output and error inference: +Inside `.handle(...)`, `from` receives the owning machine's public input and +declared parent protocol contextually. Source and lifecycle callbacks can send +through `self` and `parent` while retaining the invoked Effect's output and +error inference: ```ts const machine = Machine.make({ @@ -506,36 +507,47 @@ const machine = Machine.make({ // ... }).handle({ Saving: { - invoke: Machine.invoke({ - id: "notify-parent", - effect: () => saveDocument, - onDone: (to) => - to.none.resolve(({ parent, self }, enqueue) => { - enqueue.sendTo(self, Commands.Save()) - enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" })) - }), - onFailure: (to) => to.none - }) + invoke: (from) => + from.effect("notify-parent", () => saveDocument) + .onDone((to) => + to.none.resolve(({ parent, self }, enqueue) => { + enqueue.sendTo(self, Commands.Save()) + enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" })) + }) + ) + .onFailure((to) => to.none) } }) ``` -The standard `Machine.invoke(...)` form retains exact `self` and `parent` -typing even when the definition is named separately; no intermediate -definition method is required. +Return an array of completed chains to compose multiple state-owned activities. +The source computation itself, process logic, or `Machine.child(id, machine)` +descriptor can be named and reused; the invocation chain stays local so its +transitions retain the exact owning state and machine protocols. -A direct `invoke: { ... }` object is also supported when its lifecycle handlers -do not need source-derived context. Reuse one exported -`Machine.child(id, machine)` descriptor for invocation, `sendTo`, and child -lookup. +```ts +const refreshCache = Cache.refresh + +machine.handle({ + Active: { + invoke: (from) => [ + from.effect("refresh-cache", () => refreshCache).onDone((to) => to.none).onFailure((to) => to.none), + from.timer("expire-session", "5 minutes").onDone((to) => to.full.Expired()) + ] + } +}) +``` `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 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. +non-`never` typed error. Streams additionally require `onElement` when their +element channel is non-`never` and always require `onDone`; logic and child +chains optionally expose `onSnapshot`. A handled method disappears from the +next builder step, so every reachable lifecycle channel is handled exactly +once. Defects, interruption, and source-construction failures terminate the +owning runtime. Effect sources are factories evaluated when their state is +entered. Use an Effect containing `Effect.sleep(...)` for generic work, while +`from.timer(...)` keeps timer intent explicit and makes static durations visible +through activity inspection. ## Reactivity diff --git a/api-reference.config.json b/api-reference.config.json index 658a7c6..eab21e6 100644 --- a/api-reference.config.json +++ b/api-reference.config.json @@ -30,7 +30,6 @@ "encodeSnapshot", "events", "internalEvents", - "invoke", "make", "plan", "planInitial", diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 1c6376f..b7f7518 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -125,17 +125,16 @@ its extra control is required: - Bind a shared Atom runtime once with `AtomMachine.bind(runtime)`, then use the returned `make` or `resume`. Use `AtomMachine.make(machine)` and `AtomMachine.resume(machine, snapshot)` for service-free machines. -- Use one invocation object: `effect` for one-shot work, `stream` for repeated - externally produced values, `after` for a timer, `logic` for reusable process - logic, and `child` for a complete child - statechart. `Machine.invoke({...})` preserves owner state and source channels - across sibling lifecycle handlers. Inside `.handle(...)`, `self` and any - declared `parent` use the owning definition's exact protocols; no intermediate - definition method is required. +- Use the state-local `invoke: (from) => ...` selector: `from.effect` for + one-shot work, `from.stream` for repeated externally produced values, + `from.timer` for a timer, `from.logic` for reusable process logic, and + `from.child` for a complete child statechart. Its chain preserves owner state + and source channels across lifecycle handlers. Inside `.handle(...)`, `self` + and any declared `parent` use the owning definition's exact protocols. - Use `Machine.child(id, machine)` for a complete statechart descriptor and `Machine.childAddress(id)` for a low-level process address. A logic - invocation is addressable only when `Machine.invoke` receives that - address explicitly. + invocation is addressable only when `from.logic` receives that address + explicitly. - Use the callback's `enqueue` argument for `raise`, `emit`, `sendTo`, and `stop`. These operations record closed machine commands and do not run Effects. @@ -935,16 +934,18 @@ across both configuration lists. ## Recoverable state-scoped work -Use `Machine.invoke` with an `effect` for one-shot work. Lifecycle callbacks -receive the typed Effect channels and can transition directly: +Use `from.effect` for one-shot work. Lifecycle callbacks receive the typed +Effect channels and can transition directly: ```ts -invoke: Machine.invoke({ - id: "save", - effect: () => SaveService.save(draft), - onDone: (to) => to.full.Saved().resolve(({ output, target }) => target.from({ entry: output })), - onFailure: (to) => - to.full.SaveFailed().resolve(({ error, target }) => target.from({ message: error.message })) +machine.handle({ + Saving: { + invoke: (from) => + from.effect("save", () => SaveService.save(draft)) + .onDone((to) => to.full.Saved().resolve(({ output, target }) => target.from({ entry: output }))) + .onFailure((to) => + to.full.SaveFailed().resolve(({ error, target }) => target.from({ message: error.message }))) + } }) ``` @@ -964,15 +965,17 @@ events. `onElement` maps each value into an owner transition, while `onDone` handles normal Stream completion and `onFailure` handles the typed Stream error: ```ts -invoke: Machine.invoke({ - id: "broadcast-channel", - stream: () => messages, - onElement: (to) => - to.none.resolve(({ element }, enqueue) => { - enqueue.raise(Events.MessageReceived({ message: element })) - }), - onDone: (to) => to.none, - onFailure: (to) => to.full.Disconnected().resolve(({ error, target }) => target.from({ error })) +machine.handle({ + Listening: { + invoke: (from) => + from.stream("broadcast-channel", () => messages) + .onElement((to) => + to.none.resolve(({ element }, enqueue) => { + enqueue.raise(Events.MessageReceived({ message: element })) + })) + .onDone((to) => to.none) + .onFailure((to) => to.full.Disconnected().resolve(({ error, target }) => target.from({ error }))) + } }) ``` @@ -985,16 +988,18 @@ Use `to.none` when a transition keeps the current configuration. Call `to.none.resolve(...)` when it also enqueues commands; a block resolver may omit its return because it is contextually typed to return `undefined`. -When a source function reads `state`, `containingState`, `ancestors`, or the entry `event`, -`Machine.invoke` infers that owner context and the returned Effect's output, -error, and service channels together. No return annotation is needed: +When a source function reads `state`, `containingState`, `ancestors`, or the +entry `event`, `from.effect` infers that owner context and the returned Effect's +output, error, and service channels together. No return annotation is needed: ```ts -invoke: Machine.invoke({ - id: "load", - effect: ({ state }) => LoadService.load(state.userId), - onDone: (to) => to.full.Loaded().resolve(({ output, target }) => target.from({ user: output })), - onFailure: (to) => to.full.LoadFailed().resolve(({ error, target }) => target.from({ error })) +machine.handle({ + Loading: { + invoke: (from) => + from.effect("load", ({ state }) => LoadService.load(state.userId)) + .onDone((to) => to.full.Loaded().resolve(({ output, target }) => target.from({ user: output }))) + .onFailure((to) => to.full.LoadFailed().resolve(({ error, target }) => target.from({ error }))) + } }) ``` @@ -1010,42 +1015,44 @@ const machine = Machine.make({ // ... }).handle({ Saving: { - invoke: Machine.invoke({ - id: "notify-parent", - effect: () => saveDocument, - onDone: (to) => - to.none.resolve(({ parent, self }, enqueue) => { - enqueue.sendTo(self, Commands.Save()) - enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" })) - }), - onFailure: (to) => to.none - }) + invoke: (from) => + from.effect("notify-parent", () => saveDocument) + .onDone((to) => + to.none.resolve(({ parent, self }, enqueue) => { + enqueue.sendTo(self, Commands.Save()) + enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" })) + })) + .onFailure((to) => to.none) } }) ``` -The standard `Machine.invoke(...)` form retains the owning machine protocols -even when the definition is named separately. A direct `invoke: { ... }` object -remains available when lifecycle handlers do not need source-derived context. +The computation, logic, or child descriptor may be named separately. The +invocation chain remains inline because it is bound to its owning state and +machine protocols. Return an array of completed chains when a state owns more +than one activity. -A cancellable timer uses the same object: +A cancellable timer uses its dedicated source selector: ```ts -invoke: Machine.invoke({ - id: "clear-status", - after: "3 seconds", - onDone: (to) => to.full.Clear().resolve(({ target }) => target()) +machine.handle({ + Waiting: { + invoke: (from) => + from.timer("clear-status", "3 seconds") + .onDone((to) => to.full.Clear().resolve(({ target }) => target())) + } }) ``` 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`. 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. +always required. An Effect containing `Effect.sleep(...)` has the same scoped +cancellation behavior, but `from.timer` 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, pass a state-local lifecycle id plus `{ logic, address }` to +`from.logic`. 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 @@ -1058,10 +1065,12 @@ const Editor = Machine.child("editor", EditorMachine) Invoke it from its owning state: ```ts -invoke: Machine.invoke({ - child: Editor, - input: editorInput, - onDone: (to) => to.full.EditorDone().resolve(({ output, target }) => target.from({ output })) +machine.handle({ + Editing: { + invoke: (from) => + from.child(Editor, { input: editorInput }) + .onDone((to) => to.full.EditorDone().resolve(({ output, target }) => target.from({ output }))) + } }) ``` @@ -1089,7 +1098,7 @@ logic that does not have a complete machine descriptor. ### Inspecting state-owned activities Use `Machine.activityDefinitions(machine)` to inspect invokes without running -them. Static inline `Machine.invoke` definitions expose serializable ownership +them. Static inline fluent invocation definitions expose serializable ownership metadata: ```ts @@ -1467,6 +1476,6 @@ The current API does not include: - declarative first-class guards; - a complete inspectable graph for arbitrary transition Effects. -Use ordinary TypeScript conditions for guards and an inline `Machine.invoke` -with `after` for state-scoped timers. Do not invent undocumented state-node -properties such as `guard`. +Use ordinary TypeScript conditions for guards and inline +`invoke: (from) => from.timer(...)` chains for state-scoped timers. Do not +invent undocumented state-node properties such as `guard`. diff --git a/examples/platformer/README.md b/examples/platformer/README.md index 2c1cb5d..e6bc449 100644 --- a/examples/platformer/README.md +++ b/examples/platformer/README.md @@ -73,7 +73,7 @@ only `Airborne` interprets the wall sample as a wall jump. It turns and pushes away, refreshes the air jump through `WallLock`, and the same wall may be used again after physically returning to it. Movement phases own their timestamps, and both landing and capability locks demonstrate state-scoped -inline `Machine.invoke({ after: ... })` timers. +inline `invoke: (from) => from.timer(...)` chains. Keyboard commands and physics facts share a typed `Schema.TaggedUnion` protocol. The adapter executes velocity and floor collision, then reports diff --git a/examples/platformer/src/machine.ts b/examples/platformer/src/machine.ts index 9b81daf..25f53c2 100644 --- a/examples/platformer/src/machine.ts +++ b/examples/platformer/src/machine.ts @@ -244,15 +244,13 @@ export const CharacterMachine = Machine.make({ } }, Landing: { - invoke: Machine.invoke({ - id: "landing-settle", - after: "140 millis", - onDone: (to) => + invoke: (from) => + from.timer("landing-settle", "140 millis").onDone((to) => to.none.resolve((_, enqueue) => { enqueue.raise(InternalEvents.LandingSettled()) return undefined }) - }), + ), on: { LandingSettled: (to) => to.branches({ @@ -332,19 +330,17 @@ export const CharacterMachine = Machine.make({ }, states: { AirJumpGroundLock: { - invoke: Machine.invoke({ - id: "ground-air-jump-unlock", - after: "120 millis", - onDone: (to) => to.local.AirJumpReady().resolve(({ target }) => target.from()) - }), + invoke: (from) => + from.timer("ground-air-jump-unlock", "120 millis").onDone((to) => + to.local.AirJumpReady().resolve(({ target }) => target.from()) + ), on: {} }, AirJumpWallLock: { - invoke: Machine.invoke({ - id: "wall-air-jump-unlock", - after: "240 millis", - onDone: (to) => to.local.AirJumpReady().resolve(({ target }) => target.from()) - }), + invoke: (from) => + from.timer("wall-air-jump-unlock", "240 millis").onDone((to) => + to.local.AirJumpReady().resolve(({ target }) => target.from()) + ), on: {} }, AirJumpReady: { diff --git a/examples/playground/src/examples/media-player/machine.ts b/examples/playground/src/examples/media-player/machine.ts index 237300d..d9bcb1f 100644 --- a/examples/playground/src/examples/media-player/machine.ts +++ b/examples/playground/src/examples/media-player/machine.ts @@ -23,20 +23,18 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ Empty: {}, Loading: { - invoke: Machine.invoke({ - id: "load-audio", - effect: ({ state }) => loadAudio(state.url), - onDone: (to) => + invoke: (from) => + from.effect("load-audio", ({ state }) => loadAudio(state.url)).onDone((to) => to.none.resolve((_, enqueue) => { enqueue.raise(MediaPlayerInternalEvents.LoadSucceeded()) return undefined - }), - onFailure: (to) => + }) + ).onFailure((to) => to.none.resolve(({ error }, enqueue) => { enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) return undefined }) - }), + ), on: { LoadSucceeded: (to) => to.local.Ready().resolve(({ target }) => target.from((ready) => ready.Paused.from(initialPlaybackData))) @@ -46,16 +44,13 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ Ready: { states: { Paused: { - invoke: Machine.invoke({ - id: "pause-audio", - effect: () => pauseAudio, - onDone: (to) => to.none, - onFailure: (to) => + invoke: (from) => + from.effect("pause-audio", () => pauseAudio).onDone((to) => to.none).onFailure((to) => to.none.resolve(({ error }, enqueue) => { enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) return undefined }) - }), + ), on: { PlayRequested: (to) => to.local.Playing().resolve(({ state, target }) => @@ -71,30 +66,24 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ }, Playing: { - invoke: [ - Machine.invoke({ - id: "play-audio", - effect: () => playAudio, - onDone: (to) => to.none, - onFailure: (to) => - to.none.resolve(({ error }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) - return undefined - }) - }), - Machine.invoke({ - id: "analyze-audio", - stream: () => analyzeAudio, - onElement: (to) => - to.none.resolve(({ element }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.LoudnessMeasured(element)) - }), - onDone: (to) => to.none, - onFailure: (to) => - to.none.resolve(({ error }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) - }) - }) + invoke: ( + from + ) => [ + from.effect("play-audio", () => playAudio).onDone((to) => to.none).onFailure((to) => + to.none.resolve(({ error }, enqueue) => { + enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) + return undefined + }) + ), + from.stream("analyze-audio", () => analyzeAudio).onElement((to) => + to.none.resolve(({ element }, enqueue) => { + enqueue.raise(MediaPlayerInternalEvents.LoudnessMeasured(element)) + }) + ).onDone((to) => to.none).onFailure((to) => + to.none.resolve(({ error }, enqueue) => { + enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) + }) + ) ], on: { PauseRequested: (to) => @@ -162,20 +151,18 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ }, Restarting: { - invoke: Machine.invoke({ - id: "restart-audio", - effect: () => restartAudio, - onDone: (to) => + invoke: (from) => + from.effect("restart-audio", () => restartAudio).onDone((to) => to.none.resolve((_, enqueue) => { enqueue.raise(MediaPlayerInternalEvents.RestartSucceeded()) return undefined - }), - onFailure: (to) => + }) + ).onFailure((to) => to.none.resolve(({ error }, enqueue) => { enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) return undefined }) - }), + ), on: { RestartSucceeded: (to) => to.local.Playing().resolve(({ target }) => target.from({ currentTime: 0, loudness: null })), @@ -200,15 +187,12 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ }, Failed: { - invoke: Machine.invoke({ - id: "report-error", - effect: ({ state }) => + invoke: (from) => + from.effect("report-error", ({ state }) => Effect.gen(function*() { const mediaPlayer = yield* MediaPlayer yield* mediaPlayer.reportError(state.message) - }), - onDone: (to) => to.none - }) + })).onDone((to) => to.none) } } }, @@ -216,11 +200,10 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ settings: { states: { Audible: { - invoke: Machine.invoke({ - id: "apply-audio-settings", - effect: ({ state }) => applyAudioSettings(state, false), - onDone: (to) => to.none - }), + invoke: (from) => + from.effect("apply-audio-settings", ({ state }) => applyAudioSettings(state, false)).onDone((to) => + to.none + ), on: { VolumeChanged: (to) => to.local.Audible().resolve(({ event, state, target }) => @@ -247,11 +230,10 @@ export const MediaPlayerMachine = MediaPlayerDefinition.handle({ }, Muted: { - invoke: Machine.invoke({ - id: "apply-audio-settings", - effect: ({ state }) => applyAudioSettings(state, true), - onDone: (to) => to.none - }), + invoke: (from) => + from.effect("apply-audio-settings", ({ state }) => applyAudioSettings(state, true)).onDone((to) => + to.none + ), on: { VolumeChanged: (to) => to.local.Muted().resolve(({ event, state, target }) => diff --git a/examples/playground/src/examples/microwave/machine.ts b/examples/playground/src/examples/microwave/machine.ts index cf68aff..21c102c 100644 --- a/examples/playground/src/examples/microwave/machine.ts +++ b/examples/playground/src/examples/microwave/machine.ts @@ -66,14 +66,12 @@ export const MicrowaveMachine = Machine.make({ } }, Cooking: { - invoke: Machine.invoke({ - id: "cooking-second", - after: "1 second", - onDone: (to) => + invoke: (from) => + from.timer("cooking-second", "1 second").onDone((to) => to.local.Cooking().resolve(({ state, target }) => target.from({ elapsedSeconds: state.elapsedSeconds + 1 }) ) - }), + ), on: { PowerPressed: (to) => to.local.Idle().resolve(({ target }) => target.from()), DoorOpened: (to) => to.local.Idle().resolve(({ target }) => target.from()) diff --git a/examples/playground/src/examples/traffic-light/machine.ts b/examples/playground/src/examples/traffic-light/machine.ts index e7af484..c0bccc3 100644 --- a/examples/playground/src/examples/traffic-light/machine.ts +++ b/examples/playground/src/examples/traffic-light/machine.ts @@ -28,41 +28,37 @@ export const TrafficLightMachine = Machine.make({ initial: (to) => to.Red().resolve(({ target }) => target.from()) }).handle({ Red: { - invoke: Machine.invoke({ - id: "red-timer", - after: trafficLightDurations.Red, - onDone: (to) => to.full.RedYellow().resolve(({ target }) => target.from()) - }), + invoke: (from) => + from.timer("red-timer", trafficLightDurations.Red).onDone((to) => + to.full.RedYellow().resolve(({ target }) => target.from()) + ), on: { Reset: (to) => to.full.Red().resolve(({ target }) => target.from(), { reenter: true }) } }, RedYellow: { - invoke: Machine.invoke({ - id: "red-yellow-timer", - after: trafficLightDurations.RedYellow, - onDone: (to) => to.full.Green().resolve(({ target }) => target.from()) - }), + invoke: (from) => + from.timer("red-yellow-timer", trafficLightDurations.RedYellow).onDone((to) => + to.full.Green().resolve(({ target }) => target.from()) + ), on: { Reset: (to) => to.full.Red().resolve(({ target }) => target.from()) } }, Green: { - invoke: Machine.invoke({ - id: "green-timer", - after: trafficLightDurations.Green, - onDone: (to) => to.full.Yellow().resolve(({ target }) => target.from()) - }), + invoke: (from) => + from.timer("green-timer", trafficLightDurations.Green).onDone((to) => + to.full.Yellow().resolve(({ target }) => target.from()) + ), on: { Reset: (to) => to.full.Red().resolve(({ target }) => target.from()) } }, Yellow: { - invoke: Machine.invoke({ - id: "yellow-timer", - after: trafficLightDurations.Yellow, - onDone: (to) => to.full.Red().resolve(({ target }) => target.from()) - }), + invoke: (from) => + from.timer("yellow-timer", trafficLightDurations.Yellow).onDone((to) => + to.full.Red().resolve(({ target }) => target.from()) + ), on: { Reset: (to) => to.full.Red().resolve(({ target }) => target.from()) } diff --git a/examples/pokemon/src/machine.ts b/examples/pokemon/src/machine.ts index ef910e3..45ef672 100644 --- a/examples/pokemon/src/machine.ts +++ b/examples/pokemon/src/machine.ts @@ -21,28 +21,22 @@ const machine = Machine.make({ initial: (to) => to.Loading().resolve(({ target }) => target.from()) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "load-team", - effect: () => + invoke: (from) => + from.effect("load-team", () => Effect.gen(function*() { const service = yield* PokemonService return yield* service.getRandomTeam() - }), - onDone: (to) => to.full.ActiveTeam().resolve(({ output, target }) => target.from({ team: output })), - onFailure: (to) => to.full.Failed().resolve(({ target }) => target.from()) - }) + })).onDone((to) => to.full.ActiveTeam().resolve(({ output, target }) => target.from({ team: output }))) + .onFailure((to) => to.full.Failed().resolve(({ target }) => target.from())) }, ActiveTeam: { - invoke: [ - Machine.invoke({ - child: SelectionChild, - onDone: (to) => to.none, - onFailure: (to) => to.full.Failed().resolve(({ target }) => target.from()) - }), - Machine.invoke({ - child: ReplaceChild, - onFailure: (to) => to.full.Failed().resolve(({ target }) => target.from()) - }) + invoke: ( + from + ) => [ + from.child(SelectionChild).onDone((to) => to.none).onFailure((to) => + to.full.Failed().resolve(({ target }) => target.from()) + ), + from.child(ReplaceChild).onFailure((to) => to.full.Failed().resolve(({ target }) => target.from())) ], on: { ReplaceInTeam: (to) => diff --git a/examples/pokemon/src/machines/replace.ts b/examples/pokemon/src/machines/replace.ts index c6d0db9..a50d7ac 100644 --- a/examples/pokemon/src/machines/replace.ts +++ b/examples/pokemon/src/machines/replace.ts @@ -44,15 +44,12 @@ export const ReplaceMachine = Machine.make({ } }, Replacing: { - invoke: Machine.invoke({ - id: "replaceWithRandom", - effect: () => replaceWithRandom, - onDone: (to) => + invoke: (from) => + from.effect("replaceWithRandom", () => replaceWithRandom).onDone((to) => to.none.resolve(({ output }, enqueue) => { enqueue.raise(ReplaceInternalEvents.Replaced({ pokemon: output.pokemon })) - }), - onFailure: (to) => to.full.Idle().resolve(({ target }) => target.from()) - }), + }) + ).onFailure((to) => to.full.Idle().resolve(({ target }) => target.from())), on: { Replaced: (to) => to.full.Idle().resolve(({ event, parent, state, target }, enqueue) => { diff --git a/examples/pokemon/src/machines/selection.ts b/examples/pokemon/src/machines/selection.ts index 3bd73b9..6a84f05 100644 --- a/examples/pokemon/src/machines/selection.ts +++ b/examples/pokemon/src/machines/selection.ts @@ -107,15 +107,14 @@ export const SelectionMachine = Machine.make({ } }, Searching: { - invoke: Machine.invoke({ - id: "search", - effect: ({ ancestors }) => searchPokemon(ancestors["form.search"].searchText), - onDone: (to) => + invoke: (from) => + from.effect("search", ({ ancestors }) => searchPokemon(ancestors["form.search"].searchText)).onDone(( + to + ) => to.none.resolve(({ output }, enqueue) => { enqueue.raise(output) - }), - onFailure: (to) => to.local.NoPokemon().resolve(({ target }) => target.from()) - }), + }) + ).onFailure((to) => to.local.NoPokemon().resolve(({ target }) => target.from())), on: { SearchResult: (to) => to.branches({ diff --git a/perf/runtime/effect-machine-compatibility.mjs b/perf/runtime/effect-machine-compatibility.mjs index 7452a5f..0fe335e 100644 --- a/perf/runtime/effect-machine-compatibility.mjs +++ b/perf/runtime/effect-machine-compatibility.mjs @@ -9,7 +9,8 @@ export const makeEffectMachineBenchmarkApi = (Machine) => { // The legacy process constructor takes `(initial, transition)`. The static // definition constructor is deliberately unary and returns its config. const hasStaticTransitions = typeof Machine.transition === "function" && Machine.transition.length === 1 - const hasFluentTransitions = !hasStaticTransitions && typeof Machine.invoke === "function" + const hasFluentTransitions = !hasStaticTransitions && typeof Machine.invokeMachine !== "function" + const hasFluentInvocations = typeof Machine.invoke !== "function" && typeof Machine.invokeMachine !== "function" const hasValueSelectors = hasFluentTransitions && Machine.targetless === undefined const targetless = ({ target }) => typeof target.none === "function" ? target.none() : undefined const selectInstruction = (selection) => typeof selection === "function" ? selection() : selection @@ -61,7 +62,17 @@ export const makeEffectMachineBenchmarkApi = (Machine) => { : hasStaticTransitions || hasFluentTransitions ? { target: Machine.targetless } : targetless, - invokeChild: typeof Machine.invokeMachine === "function" + invokeChild: hasFluentInvocations + ? (config) => (from) => { + let invoked = config.input === undefined + ? from.child(config.child) + : from.child(config.child, { input: config.input }) + if (config.onSnapshot !== undefined) invoked = invoked.onSnapshot(config.onSnapshot) + if (config.onDone !== undefined) invoked = invoked.onDone(config.onDone) + if (config.onFailure !== undefined) invoked = invoked.onFailure(config.onFailure) + return invoked + } + : typeof Machine.invokeMachine === "function" ? ({ onSnapshot, onFailure, ...config }) => { if (onFailure !== undefined) { throw new Error("The legacy child invocation API cannot handle failures as parent transitions") diff --git a/perf/types/adapter-readiness.ts b/perf/types/adapter-readiness.ts index a1058f5..f1bf983 100644 --- a/perf/types/adapter-readiness.ts +++ b/perf/types/adapter-readiness.ts @@ -31,7 +31,7 @@ type BoundAtomEventIsExact = Expect< void Machine.planInitial(machine) void Machine.start(machine) void Machine.resume(machine, snapshot) -void Machine.invoke({ child }) +void child void resumedAtom void cluster void boundResumedAtom diff --git a/perf/types/dynamic-invoke.ts b/perf/types/dynamic-invoke.ts index ca8189d..5dc793f 100644 --- a/perf/types/dynamic-invoke.ts +++ b/perf/types/dynamic-invoke.ts @@ -8,22 +8,20 @@ interface User { const invoked = machine.handle({ Loading: { - invoke: Machine.invoke({ - id: "load-user", - effect: ({ state }) => loadUser(state.userId), - onDone: (to) => + invoke: (from) => + from.effect("load-user", ({ state }) => loadUser(state.userId)).onDone((to) => to.none.resolve(({ output }) => { const user: User = output void user return undefined - }), - onFailure: (to) => + }) + ).onFailure((to) => to.none.resolve(({ error }) => { const loadError: LoadError = error void loadError return undefined }) - }) + ) } }) diff --git a/scripts/fixtures/consumer/consumer.ts b/scripts/fixtures/consumer/consumer.ts index 4801e14..b75b39c 100644 --- a/scripts/fixtures/consumer/consumer.ts +++ b/scripts/fixtures/consumer/consumer.ts @@ -41,6 +41,10 @@ const machine = Machine.make({ } }, Loading: { + invoke: (from) => [ + from.effect("fixture-load", () => Effect.succeed("ready")).onDone((to) => to.none), + from.timer("fixture-delay", "1 second").onDone((to) => to.none) + ], on: { Loaded: (to) => to.full.Done().resolve(({ event, target }) => target(State.cases.Done.make({ value: event.value }))) @@ -57,16 +61,6 @@ const invalidSelector = AtomMachine.select(atoms, "Missing") const cluster = ClusterMachine.make("ConsumerEntity", machine, { version: "1" }) -const invoked = Machine.invoke({ - id: "fixture-load", - effect: () => Effect.succeed("ready"), - onDone: (to) => to.none -}) -const delayed = Machine.invoke({ - id: "fixture-delay", - after: "1 second", - onDone: (to) => to.none -}) const generated = MachineTest.scenarios(machine, { minEvents: 1, maxEvents: 2 }) type InputEvent = Machine.Machine.InputEvent @@ -86,8 +80,6 @@ void [ loadingAtom, invalidSelector, cluster, - invoked, - delayed, generated, constructedStart, constructedLoaded, diff --git a/scripts/fixtures/consumer/deep-bound.ts b/scripts/fixtures/consumer/deep-bound.ts index d673f09..10c68eb 100644 --- a/scripts/fixtures/consumer/deep-bound.ts +++ b/scripts/fixtures/consumer/deep-bound.ts @@ -94,11 +94,7 @@ const definition = Machine.make({ }) const machine = definition.handle({ Idle: { - invoke: Machine.invoke({ - id: "deep-inline-invoke", - effect: () => Effect.asVoid(ExternalService), - onDone: (to) => to.none - }), + invoke: (from) => from.effect("deep-inline-invoke", () => Effect.asVoid(ExternalService)).onDone((to) => to.none), on: { Begin: (to) => to.full.Ready().resolve(({ target }) => @@ -127,11 +123,8 @@ const machine = definition.handle({ } }, Saving: { - invoke: Machine.invoke({ - child: Child, - input: ({ state }) => ({ value: state.value }), - onDone: (to) => to.none - }), + invoke: (from) => + from.child(Child, { input: ({ state }) => ({ value: state.value }) }).onDone((to) => to.none), on: { ChildNotice: (to) => to.local.Saving().resolve(({ event, target }, enqueue) => { diff --git a/scripts/invoke-autocomplete.test.mjs b/scripts/invoke-autocomplete.test.mjs index 3c57714..5c39d42 100644 --- a/scripts/invoke-autocomplete.test.mjs +++ b/scripts/invoke-autocomplete.test.mjs @@ -21,17 +21,15 @@ const definition = Machine.make({ definition.handle({ Loading: { - invoke: Machine.invoke({ - id: "load", - effect: ({ /*invoke-source-context*/ ...context }) => - Effect.fail("offline").pipe(Effect.as(context.state._tag)), - onDone: (to) => + invoke: (from) => + from./*invoke-sources*/effect("load", ({ /*invoke-source-context*/ ...context }) => + Effect.fail("offline").pipe(Effect.as(context.event._tag))) + .onDone((to) => to.full./*done-target*/Done()./*selected-operations*/resolve(({ /*done-context*/ ...context }) => - context.target./*done-exact-target*/from()), - onFailure: (to) => + context.target./*done-exact-target*/from())) + .onFailure((to) => to.full.Failed().resolve(({ /*failure-context*/ ...context }) => - context.target.from()) - }) + context.target.from())) }, Done: {}, Failed: {} @@ -46,10 +44,8 @@ const requiredParentDefinition = Machine.make({ requiredParentDefinition.handle({ Loading: { - invoke: Machine.invoke({ - id: "required-parent", - effect: ({ /*required-parent-context*/ ...context }) => Effect.never - }) + invoke: (from) => + from.effect("required-parent", ({ /*required-parent-context*/ ...context }) => Effect.never) }, Done: {}, Failed: {} @@ -64,10 +60,8 @@ const optionalParentDefinition = Machine.make({ optionalParentDefinition.handle({ Loading: { - invoke: Machine.invoke({ - id: "optional-parent", - effect: ({ /*optional-parent-context*/ ...context }) => Effect.never - }) + invoke: (from) => + from.effect("optional-parent", ({ /*optional-parent-context*/ ...context }) => Effect.never) }, Done: {}, Failed: {} @@ -75,13 +69,11 @@ optionalParentDefinition.handle({ definition.handle({ Loading: { - invoke: Machine.invoke({ - id: "updates", - stream: () => Stream.make(1), - onElement: (to) => - to.none.resolve(({ /*element-context*/ ...context }) => undefined), - onDone: (to) => to.none - }) + invoke: (from) => + from.stream("updates", () => Stream.make(1)) + .onElement((to) => + to.none.resolve(({ /*element-context*/ ...context }) => undefined)) + .onDone((to) => to.none) }, Done: {}, Failed: {} @@ -89,11 +81,10 @@ definition.handle({ definition.handle({ Loading: { - invoke: Machine.invoke({ - id: "incomplete", - effect: () => Effect.fail("offline").pipe(Effect.as(1)), - /*invoke-properties*/ - }) + invoke: (from) => + from.effect("incomplete", () => Effect.fail("offline").pipe(Effect.as(1))) + ./*invoke-properties*/onDone((to) => to.none) + .onFailure((to) => to.none) }, Done: {}, Failed: {} @@ -177,6 +168,11 @@ const host = { } const service = ts.createLanguageService(host) +const diagnostics = service.getSemanticDiagnostics(virtualFile) +assert.deepEqual( + diagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")), + [] +) const completions = (marker) => { const position = source.indexOf(`/*${marker}*/`) @@ -185,6 +181,15 @@ const completions = (marker) => { } test("contextually completes Effect invocation factories while authoring", () => { + const sources = completions("invoke-sources") + assert.deepEqual([...sources].filter((name) => ["effect", "stream", "timer", "logic", "child"].includes(name)).sort(), [ + "child", + "effect", + "logic", + "stream", + "timer" + ]) + const sourceContext = completions("invoke-source-context") assert.equal(sourceContext.has("state"), true) assert.equal(sourceContext.has("ancestors"), true) @@ -218,6 +223,8 @@ test("contextually completes Effect invocation factories while authoring", () => const properties = completions("invoke-properties") assert.equal(properties.has("onDone"), true) assert.equal(properties.has("onFailure"), true) + assert.equal(properties.has("onElement"), false) + assert.equal(properties.has("onSnapshot"), false) }) test("contextually completes Stream element handlers while authoring", () => { diff --git a/scripts/runtime-performance-compatibility.test.mjs b/scripts/runtime-performance-compatibility.test.mjs index 314f5c8..be210db 100644 --- a/scripts/runtime-performance-compatibility.test.mjs +++ b/scripts/runtime-performance-compatibility.test.mjs @@ -80,7 +80,7 @@ test("adapts benchmark definitions to value selectors and target-first initial e assert.equal(api.targetless({ none: selected }), selected) }) -test("uses the current child invocation capability when available", () => { +test("uses the object child invocation compatibility capability when available", () => { const calls = [] const noTarget = Symbol("no-target") const Machine = { @@ -105,6 +105,48 @@ test("uses the current child invocation capability when available", () => { assert.equal(makeEffectMachineBenchmarkApi(Machine).targetless({ none: noTarget }), noTarget) }) +test("adapts child invocations to the state-local fluent selector", () => { + const onSnapshot = () => undefined + const onDone = () => undefined + const onFailure = () => undefined + const calls = [] + const builder = { + onSnapshot: (handler) => { + calls.push(["snapshot", handler]) + return builder + }, + onDone: (handler) => { + calls.push(["done", handler]) + return builder + }, + onFailure: (handler) => { + calls.push(["failure", handler]) + return builder + } + } + const from = { + child: (...args) => { + calls.push(["child", ...args]) + return builder + } + } + const config = { + child: "counter", + input: { seed: 1 }, + onSnapshot, + onDone, + onFailure + } + + assert.equal(makeEffectMachineBenchmarkApi({}).invokeChild(config)(from), builder) + assert.deepEqual(calls, [ + ["child", "counter", { input: { seed: 1 } }], + ["snapshot", onSnapshot], + ["done", onDone], + ["failure", onFailure] + ]) +}) + test("adapts lifecycle names for the legacy child invocation capability", () => { const calls = [] const onDone = () => undefined diff --git a/scripts/type-performance.mjs b/scripts/type-performance.mjs index 36fb124..4532936 100644 --- a/scripts/type-performance.mjs +++ b/scripts/type-performance.mjs @@ -92,13 +92,13 @@ const scenarios = [ }, { id: "dynamic-invoke-control", - label: "dynamic Machine.invoke control", + label: "dynamic invocation control", file: "dynamic-invoke-control.ts", hidden: true }, { id: "dynamic-invoke", - label: "Machine.invoke (state-dependent Effect)", + label: "fluent invocation (state-dependent Effect)", file: "dynamic-invoke.ts", control: "dynamic-invoke-control", maxInstantiations: 122_000, diff --git a/src/Machine.ts b/src/Machine.ts index 660611d..277742c 100644 --- a/src/Machine.ts +++ b/src/Machine.ts @@ -634,6 +634,7 @@ type IncompatibleRuntime = Requirements extends Run const InvokeTypeId: typeof internal.InvokeTypeId = internal.InvokeTypeId const TransitionTypeId: typeof internal.TransitionTypeId = internal.TransitionTypeId declare const TransitionBuilderTypeId: unique symbol +declare const InvokeBuilderTypeId: unique symbol declare const InitialBuilderTypeId: unique symbol type StateDefinitionError< @@ -2192,7 +2193,7 @@ export interface MachineRef = Invoke extends unknown ? + | (Invoke extends { + readonly [InvokeTypeId]: { readonly outcomes: Types.Covariant } + } ? EventTransitionReturn : + never) | (Invoke extends { readonly onDone?: infer Handler } ? EventTransitionReturn> : never) | (Invoke extends { readonly onFailure?: infer Handler } ? EventTransitionReturn> : never) | (Invoke extends { readonly onElement?: infer Handler } ? EventTransitionReturn> : never) | (Invoke extends { readonly onSnapshot?: infer Handler } ? EventTransitionReturn> : never) : never + type InvokeOutcomeError = IsAny> extends true ? never + : Effect.Error> + type InvokeOutcomeServices = IsAny> extends true ? never + : Effect.Services> /** * Extracts the parent transition error contribution from invoked children. * @@ -5228,7 +5237,7 @@ export declare namespace Machine { : | ChildAlreadyExistsError | InvokeInitialError> - | Effect.Error>> + | InvokeOutcomeError> /** * Extracts the parent service requirement contribution from invoked children. * @@ -5239,9 +5248,7 @@ export declare namespace Machine { : | MachineRuntimeRequirement | InvokeServices> - | Effect.Services< - InvokeOutcomeReturn> - > + | InvokeOutcomeServices> /** * Extracts the return value from an eventless transition. * @@ -5796,8 +5803,16 @@ export declare namespace Machine { > } - /** Type evidence retained by {@link invoke} without affecting runtime data. */ - export interface InvokeTyped { + /** Type evidence retained by a completed state-owned invocation. */ + export interface InvokeTyped< + Output, + Error, + Requirements, + InitialError, + Emits = never, + ParentEvent = never, + Outcomes = never + > { readonly [InvokeTypeId]: { readonly output: Types.Covariant readonly error: Types.Covariant @@ -5805,10 +5820,11 @@ export declare namespace Machine { readonly initialError: Types.Covariant readonly emits: Types.Covariant readonly parentEvents: Types.Covariant + readonly outcomes: Types.Covariant } } - export type InvokeConfig< + type StoredInvokeConfig< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -5899,8 +5915,7 @@ export declare namespace Machine { } ) - /** State-bound inline invocation configuration. */ - export type InvokeDefinition< + type StoredInvokeDefinition< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -5908,468 +5923,523 @@ export declare namespace Machine { InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] > = - | InvokeConfig - | ReadonlyArray> - - type TypedInvokeDefinition = - | InvokeTyped - | ReadonlyArray> - - type InvokeHandlerRequirement = IsAny extends true ? { readonly handler: Handler } - : [Value] extends [never] ? { readonly handler?: never } - : { readonly handler: Handler } + | StoredInvokeConfig + | ReadonlyArray> - export type InvokeDoneRequirement = InvokeHandlerRequirement extends - infer Requirement ? Requirement extends { readonly handler: infer Required } ? { readonly onDone: Required } - : { readonly onDone?: never } + type LogicInitialEffectOf = Value extends { readonly initial: infer Initial } ? + Initial extends (...args: ReadonlyArray) => infer Result ? Result : never : never - export type InvokeFailureRequirement = InvokeHandlerRequirement extends - infer Requirement ? Requirement extends { readonly handler: infer Required } ? { readonly onFailure: Required } - : { readonly onFailure?: never } + type LogicRunEffectOf = Value extends { readonly run: infer Run } ? + Run extends (...args: ReadonlyArray) => infer Result ? Result : never : never - export type InvokeElementRequirement = InvokeHandlerRequirement extends - infer Requirement ? Requirement extends { readonly handler: infer Required } ? { readonly onElement: Required } - : { readonly onElement?: never } + export type LogicStateOf = Effect.Success> + export type LogicEventOf = Value extends { readonly initial: infer Initial } ? + Initial extends (scope: infer LogicScope, ...args: ReadonlyArray) => any ? + LogicScope extends Logic.Scope ? Event : never + : never : never + export type LogicErrorOf = Effect.Error> + export type LogicServicesOf = Effect.Services | LogicRunEffectOf> + export type LogicOutputOf = Effect.Success> + export type LogicInitialErrorOf = Effect.Error> + + type RequiredInvokeChannel = IsAny extends true ? Channel + : [Value] extends [never] ? never + : Channel - export type TimerInvokeArgs< + type InvokeBuilderResult< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, StateId extends StateIdentifier, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > = { - readonly id: InvokeLifecycleId - readonly after: InvokeSource< - Duration.Input, - InvokeContext - > - readonly effect?: never - readonly stream?: never - readonly logic?: never - readonly child?: never - readonly address?: never - readonly onFailure?: never - readonly onElement?: never - readonly onSnapshot?: never - readonly onDone: InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeDoneContext - > - } + InputEvents extends ReadonlyArray, + ParentEvents extends ReadonlyArray, + Output, + Error, + Requirements, + InitialError, + ChildEmits, + ChildParentEvent, + Outcomes + > = + & InvokeOwned + & InvokeTyped + & { readonly [InvokeBuilderTypeId]: true } - export type LogicInvokeArgs< + type InvokeBuilder< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, StateId extends StateIdentifier, - ChildState, - ChildEvent, - ChildError, - ChildRequirements, - ChildOutput, - ChildInitialError, - Address extends ChildAddress, - Source = Logic, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] + InputEvents extends ReadonlyArray, + ParentEvents extends ReadonlyArray, + Output, + Error, + Requirements, + InitialError, + ChildEmits, + ChildParentEvent, + Element, + Pending extends string, + SnapshotHandler, + Outcomes = never > = - & { - readonly id: InvokeLifecycleId - readonly address: Address & ChildAddress.Compatibility - readonly logic: Source - readonly effect?: never - readonly stream?: never - readonly after?: never - readonly child?: never - readonly onElement?: never - readonly onSnapshot?: InvokeTransition< + & ([Pending] extends [never] ? InvokeBuilderResult< States, Events, Emits, StateId, - InvokeSnapshotContext< + InputEvents, + ParentEvents, + Output, + Error, + Requirements, + InitialError, + ChildEmits, + ChildParentEvent, + Outcomes + > + : {}) + & ("done" extends Pending ? { + readonly onDone: < + const Handler extends InvokeTransition< + States, + Events, + Emits, + StateId, + InvokeDoneContext + > + >( + handler: + & Handler + & InvokeTransition< + States, + Events, + Emits, + StateId, + InvokeDoneContext + > + ) => InvokeBuilder< States, Events, Emits, StateId, - NoInfer, - NoInfer, - NoInfer, InputEvents, - ParentEvents + ParentEvents, + Output, + Error, + Requirements, + InitialError, + ChildEmits, + ChildParentEvent, + Element, + Exclude, + SnapshotHandler, + Outcomes | Handler > - > - } - & InvokeDoneRequirement< - NoInfer, - InvokeTransition< + } + : {}) + & ("failure" extends Pending ? { + readonly onFailure: < + const Handler extends InvokeTransition< + States, + Events, + Emits, + StateId, + InvokeFailureContext + > + >( + handler: + & Handler + & InvokeTransition< + States, + Events, + Emits, + StateId, + InvokeFailureContext + > + ) => InvokeBuilder< + States, + Events, + Emits, + StateId, + InputEvents, + ParentEvents, + Output, + Error, + Requirements, + InitialError, + ChildEmits, + ChildParentEvent, + Element, + Exclude, + SnapshotHandler, + Outcomes | Handler + > + } + : {}) + & ("element" extends Pending ? { + readonly onElement: < + const Handler extends InvokeTransition< + States, + Events, + Emits, + StateId, + InvokeElementContext + > + >( + handler: + & Handler + & InvokeTransition< + States, + Events, + Emits, + StateId, + InvokeElementContext + > + ) => InvokeBuilder< + States, + Events, + Emits, + StateId, + InputEvents, + ParentEvents, + Output, + Error, + Requirements, + InitialError, + ChildEmits, + ChildParentEvent, + Element, + Exclude, + SnapshotHandler, + Outcomes | Handler + > + } + : {}) + & ([SnapshotHandler] extends [never] ? {} : { + readonly onSnapshot: (handler: Handler & SnapshotHandler) => InvokeBuilder< States, Events, Emits, StateId, - InvokeDoneContext, InputEvents, ParentEvents> + InputEvents, + ParentEvents, + Output, + Error, + Requirements, + InitialError, + ChildEmits, + ChildParentEvent, + Element, + Pending, + never, + Outcomes | Handler > - > - & InvokeFailureRequirement< - NoInfer, - InvokeTransition< + }) + + type EffectInvokeBuilder< + States extends StateSchemas, + Events extends ReadonlyArray, + Emits extends ReadonlyArray, + StateId extends StateIdentifier, + InputEvents extends ReadonlyArray, + ParentEvents extends ReadonlyArray, + Source extends (...args: ReadonlyArray) => Effect.Effect + > = InvokeBuilder< + States, + Events, + Emits, + StateId, + InputEvents, + ParentEvents, + Effect.Success>, + Effect.Error>, + Effect.Services>, + never, + never, + never, + never, + | RequiredInvokeChannel>, "done"> + | RequiredInvokeChannel>, "failure">, + never + > + + type StreamInvokeBuilder< + States extends StateSchemas, + Events extends ReadonlyArray, + Emits extends ReadonlyArray, + StateId extends StateIdentifier, + InputEvents extends ReadonlyArray, + ParentEvents extends ReadonlyArray, + Source extends (...args: ReadonlyArray) => Stream.Stream + > = InvokeBuilder< + States, + Events, + Emits, + StateId, + InputEvents, + ParentEvents, + void, + Stream.Error>, + Stream.Services>, + never, + never, + never, + Stream.Success>, + | "done" + | RequiredInvokeChannel>, "element"> + | RequiredInvokeChannel>, "failure">, + never + > + + type LogicInvokeBuilder< + States extends StateSchemas, + Events extends ReadonlyArray, + Emits extends ReadonlyArray, + StateId extends StateIdentifier, + InputEvents extends ReadonlyArray, + ParentEvents extends ReadonlyArray, + ChildLogic + > = InvokeBuilder< + States, + Events, + Emits, + StateId, + InputEvents, + ParentEvents, + LogicOutputOf, + LogicErrorOf, + LogicServicesOf, + LogicInitialErrorOf, + never, + never, + never, + | RequiredInvokeChannel, "done"> + | RequiredInvokeChannel, "failure">, + InvokeTransition< + States, + Events, + Emits, + StateId, + InvokeSnapshotContext< States, Events, Emits, StateId, - InvokeFailureContext, InputEvents, ParentEvents> + LogicStateOf, + LogicErrorOf, + LogicOutputOf, + InputEvents, + ParentEvents > > + > - export type ChildInvokeArgs< + type ChildInvokeBuilder< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, StateId extends StateIdentifier, - ChildDefinition extends Machine.Any, - Child extends ChildMachine, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > = - & { - readonly child: - & Child - & (ChildDefinition extends EnsureExecutable< - Machine.States, - Machine.UnhandledStates, - Machine.OutputStates - > ? unknown : - never) - readonly id?: never - readonly address?: never - readonly effect?: never - readonly stream?: never - readonly after?: never - readonly logic?: never - readonly onElement?: never - readonly onSnapshot?: InvokeTransition< + InputEvents extends ReadonlyArray, + ParentEvents extends ReadonlyArray, + Child extends ChildMachine.Any, + ChildDefinition extends Machine.Any = Child["machine"] + > = InvokeBuilder< + States, + Events, + Emits, + StateId, + InputEvents, + ParentEvents, + Output, + Error | ActionError>, + Services, + InitialError, + Emit, + EventOf>, + never, + | RequiredInvokeChannel, "done"> + | RequiredInvokeChannel | ActionError>, "failure">, + InvokeTransition< + States, + Events, + Emits, + StateId, + InvokeSnapshotContext< States, Events, Emits, StateId, - InvokeSnapshotContext< - States, - Events, - Emits, - StateId, - Snapshot>, - Error, - Output, - InputEvents, - ParentEvents - > + Snapshot>, + Error, + Output, + InputEvents, + ParentEvents > - } - & (Input extends typeof Schema.Void ? { readonly input?: never } : { - readonly input: InvokeSource< - Input["Type"], + > + > + + /** + * Selects state-owned work and begins its lifecycle-handler chain. + * + * Each source exposes exactly the lifecycle methods that it can produce. A + * chain becomes returnable from `invoke` only after every reachable required + * channel has been handled. + * + * @category models + * @since 0.18.0 + */ + export interface InvokeSelector< + States extends StateSchemas, + Events extends ReadonlyArray, + Emits extends ReadonlyArray, + StateId extends StateIdentifier, + InputEvents extends ReadonlyArray = Events, + ParentEvents extends ReadonlyArray = readonly [] + > { + /** Starts a fresh Effect each time the owning state is entered. */ + readonly effect: < + const Source extends ( + context: InvokeContext + ) => Effect.Effect + >( + id: InvokeLifecycleId, + source: Source + ) => EffectInvokeBuilder + + /** Starts a fresh, backpressured Stream each time the owning state is entered. */ + readonly stream: < + const Source extends ( + context: InvokeContext + ) => Stream.Stream + >( + id: InvokeLifecycleId, + source: Source + ) => StreamInvokeBuilder + + /** Starts a cancellable state-scoped timer. */ + readonly timer: ( + id: InvokeLifecycleId, + duration: InvokeSource< + Duration.Input, InvokeContext > - }) - & InvokeDoneRequirement< - Output, - InvokeTransition< + ) => InvokeBuilder< + States, + Events, + Emits, + StateId, + InputEvents, + ParentEvents, + void, + never, + never, + never, + never, + never, + never, + "done", + never + > + + /** Starts reusable process logic at a typed parent-local address. */ + readonly logic: { + < + const Source extends ( + context: InvokeContext + ) => unknown, + Address extends ChildAddress + >( + id: InvokeLifecycleId, + options: { + readonly address: + & Address + & ChildAddress.Compatibility>>> + readonly logic: Source + }, + ..._validation: ReturnType extends { readonly initial: unknown; readonly run: unknown } ? [] : [ + "logic factory must return Machine.Logic" + ] + ): LogicInvokeBuilder< States, Events, Emits, StateId, - InvokeDoneContext< - States, - Events, - Emits, - StateId, - Output, - InputEvents, - ParentEvents - > + InputEvents, + ParentEvents, + ReturnType > - > - & InvokeFailureRequirement< - Error | ActionError>, - InvokeTransition< + >( + id: InvokeLifecycleId, + options: { + readonly address: Address & ChildAddress.Compatibility>> + readonly logic: Source + }, + ..._validation: Source extends { readonly initial: unknown; readonly run: unknown } ? [] : [ + "logic must implement Machine.Logic" + ] + ): LogicInvokeBuilder< States, Events, Emits, StateId, - InvokeFailureContext< - States, - Events, - Emits, - StateId, - Error | ActionError>, - InputEvents, - ParentEvents - > + InputEvents, + ParentEvents, + Source > - > - - type LogicInitialEffectOf = Value extends { readonly initial: infer Initial } ? - Initial extends (...args: ReadonlyArray) => infer Result ? Result : never - : never - - type LogicRunEffectOf = Value extends { readonly run: infer Run } ? - Run extends (...args: ReadonlyArray) => infer Result ? Result : never - : never - - export type LogicStateOf = Effect.Success> - export type LogicEventOf = Value extends { readonly initial: infer Initial } ? - Initial extends (scope: infer LogicScope, ...args: ReadonlyArray) => any ? - LogicScope extends Logic.Scope ? Event : never - : never - : never - export type LogicErrorOf = Effect.Error> - export type LogicServicesOf = Effect.Services | LogicRunEffectOf> - export type LogicOutputOf = Effect.Success> - export type LogicInitialErrorOf = Effect.Error> + } - type ContextualInvokeConfig< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Raw - > = Raw extends InvokeTyped ? unknown - : [Extract] extends [never] ? unknown - : Raw extends { readonly effect: infer Source } ? - InvokeFactoryResult extends infer Fx extends Effect.Effect ? - & InvokeDoneRequirement< - Effect.Success, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeDoneContext, InputEvents, ParentEvents> - > - > - & InvokeFailureRequirement< - Effect.Error, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeFailureContext, InputEvents, ParentEvents> - > - > - & { readonly onSnapshot?: never } - : never - : Raw extends { readonly stream: infer Source } ? - InvokeFactoryResult extends infer SourceStream extends Stream.Stream ? - & InvokeElementRequirement< - Stream.Success, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeElementContext< - States, - Events, - Emits, - StateId, - Stream.Success, - InputEvents, - ParentEvents - > - > - > - & { - readonly onDone: InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeDoneContext - > - } - & InvokeFailureRequirement< - Stream.Error, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeFailureContext< - States, - Events, - Emits, - StateId, - Stream.Error, - InputEvents, - ParentEvents - > - > - > - & { readonly onSnapshot?: never } - : never - : Raw extends { readonly after: unknown } ? { - readonly onDone: InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeDoneContext - > - readonly onFailure?: never - readonly onSnapshot?: never - } - : Raw extends { readonly logic: infer Source; readonly address: infer Address } ? - InvokeResolvedSource extends infer ChildLogic extends Logic ? - & InvokeDoneRequirement< - InvokeOutput, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeDoneContext, InputEvents, ParentEvents> - > - > - & InvokeFailureRequirement< - InvokeRuntimeError, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeFailureContext< - States, - Events, - Emits, - StateId, - InvokeRuntimeError, - InputEvents, - ParentEvents - > - > - > - & { - readonly address: - & ChildAddress - & ChildAddress.Compatibility< - Address, - ChildLogic extends Logic ? ChildEvent : never - > - readonly onSnapshot?: InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeSnapshotContext< - States, - Events, - Emits, - StateId, - ChildLogic extends Logic ? ChildState : never, - InvokeRuntimeError, - InvokeOutput, - InputEvents, - ParentEvents - > - > - } - : never - : Raw extends { readonly child: infer Child extends ChildMachine } ? - ChildMachineLogic extends infer ChildLogic extends Logic ? - & InvokeDoneRequirement< - Output, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeDoneContext< - States, - Events, - Emits, - StateId, - Output, - InputEvents, - ParentEvents - > - > - > - & InvokeFailureRequirement< - Error | ActionError>, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeFailureContext< - States, - Events, - Emits, - StateId, - Error | ActionError>, - InputEvents, - ParentEvents - > - > + /** Starts a complete child statechart represented by a reusable descriptor. */ + readonly child: ( + child: + & Child + & (Child["machine"] extends EnsureExecutable< + Machine.States, + Machine.UnhandledStates, + Machine.OutputStates + > ? unknown + : never), + ...options: Input extends typeof Schema.Void ? [options?: { readonly input?: never }] + : [options: { + readonly input: InvokeSource< + Input["Type"], + InvokeContext > - & (Input extends typeof Schema.Void ? { readonly input?: never } : { - readonly input: InvokeSource< - Input["Type"], - InvokeContext - > - }) - & { - readonly onSnapshot?: InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeSnapshotContext< - States, - Events, - Emits, - StateId, - Snapshot>, - Error, - Output, - InputEvents, - ParentEvents - > - > - } - : never - : never + }] + ) => ChildInvokeBuilder + } - type ContextualInvokeDefinition< + /** + * Inline invocation declaration accepted by an active state handler. + * + * Return one completed source chain or an array of completed chains. Source + * computations and child descriptors may be extracted, but the chain stays + * local to preserve the owning state and machine protocols. + * + * @category models + * @since 0.18.0 + */ + export type InvokeBuilderInput< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, StateId extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Raw - > = Raw extends ReadonlyArray ? { - readonly [Index in keyof Raw]: ContextualInvokeConfig< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Raw[Index] - > - } - : ContextualInvokeConfig + InputEvents extends ReadonlyArray = Events, + ParentEvents extends ReadonlyArray = readonly [] + > = ( + from: InvokeSelector + ) => + | (InvokeOwned & { + readonly [InvokeBuilderTypeId]: true + }) + | ReadonlyArray< + InvokeOwned & { + readonly [InvokeBuilderTypeId]: true + } + > type OutputHandlerConfig< States extends StateSchemas, @@ -6421,9 +6491,7 @@ export declare namespace Machine { context: StateActionContext, enqueue: Enqueue, EmitOf> ) => StateActionResult - readonly invoke?: - | InvokeDefinition - | TypedInvokeDefinition + readonly invoke?: InvokeBuilderInput readonly always?: TransitionConfig< States, Events, @@ -6727,55 +6795,6 @@ export declare namespace Machine { : Path extends keyof Config ? Config[Path] : never - type HandlerInvokeContextAtPath< - AllStates extends StateSchemas, - Events extends ReadonlyArray, - InputEvents extends ReadonlyArray, - Emits extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Config, - StateId extends StateNodeIdentifier, - NodeConfig = HandlerConfigAtPath - > = StateId extends StateIdentifier ? - NodeConfig extends { readonly invoke: infer Invoke } ? HandlerValidationAtPath< - StateId, - { - readonly invoke: ContextualInvokeDefinition< - AllStates, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Invoke - > - } - > - : unknown - : unknown - - type HandlerInvokeContexts< - AllStates extends StateSchemas, - Events extends ReadonlyArray, - InputEvents extends ReadonlyArray, - Emits extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Config - > = Types.UnionToIntersection< - StateNodeIdentifier extends infer StateId extends StateNodeIdentifier ? - StateId extends StateNodeIdentifier ? HandlerInvokeContextAtPath< - AllStates, - Events, - InputEvents, - Emits, - ParentEvents, - Config, - StateId - > - : never - : never - > - // Rebuild the public nested handler shape so branded validation errors stay // attached to the exact property that introduced them. type HandlerValidationAtPath = Path extends `${infer Head}.${infer Rest}` ? { @@ -7375,7 +7394,6 @@ export declare namespace Machine { >( config: & Config - & HandlerInvokeContexts> & HandlerTreeValidation< States, Events, @@ -7474,7 +7492,7 @@ export declare namespace Machine { context: StateActionContext, enqueue: Enqueue, EmitOf> ) => StateActionResult - readonly invoke?: InvokeDefinition + readonly invoke?: StoredInvokeDefinition readonly always?: TransitionConfig< States, Events, @@ -8111,199 +8129,6 @@ export const decodeSnapshot: < Machine.SnapshotDecodingServices > = internal.decodeSnapshot as any -type EffectInvokeSource< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - Source extends ( - context: Machine.InvokeContext - ) => Effect.Effect -> = { - readonly id: InvokeLifecycleId - readonly effect: Source - readonly stream?: never - readonly after?: never - readonly logic?: never - readonly child?: never - readonly address?: never - readonly onElement?: never - readonly onSnapshot?: never -} - -type EffectDoneHandler< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - Source extends (...args: ReadonlyArray) => Effect.Effect -> = Machine.InvokeTransition< - States, - Events, - Emits, - StateId, - Machine.InvokeDoneContext< - States, - Events, - Emits, - StateId, - Effect.Success>>, - InputEvents, - ParentEvents - > -> - -type EffectFailureHandler< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - Source extends (...args: ReadonlyArray) => Effect.Effect -> = Machine.InvokeTransition< - States, - Events, - Emits, - StateId, - Machine.InvokeFailureContext< - States, - Events, - Emits, - StateId, - Effect.Error>>, - InputEvents, - ParentEvents - > -> - -type EffectInvokeResult< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - Source extends (...args: ReadonlyArray) => Effect.Effect -> = - & Machine.InvokeOwned - & Machine.InvokeTyped< - Effect.Success>, - Effect.Error>, - Effect.Services>, - never - > - -type StreamInvokeSource< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - Source extends (...args: ReadonlyArray) => unknown -> = { - readonly id: InvokeLifecycleId - readonly stream: - & Source - & ((context: Machine.InvokeContext) => unknown) - readonly effect?: never - readonly after?: never - readonly logic?: never - readonly child?: never - readonly address?: never - readonly onSnapshot?: never -} - -type StreamSourceResult) => unknown> = ReturnType extends - infer Result extends Stream.Stream ? Result : never - -type StreamElementHandler< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - Source extends (...args: ReadonlyArray) => unknown -> = Machine.InvokeTransition< - States, - Events, - Emits, - StateId, - Machine.InvokeElementContext< - States, - Events, - Emits, - StateId, - Stream.Success>>, - InputEvents, - ParentEvents - > -> - -type StreamDoneHandler< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier -> = Machine.InvokeTransition< - States, - Events, - Emits, - StateId, - Machine.InvokeDoneContext -> - -type StreamFailureHandler< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - Source extends (...args: ReadonlyArray) => unknown -> = Machine.InvokeTransition< - States, - Events, - Emits, - StateId, - Machine.InvokeFailureContext< - States, - Events, - Emits, - StateId, - Stream.Error>>, - InputEvents, - ParentEvents - > -> - -type StreamInvokeResult< - States extends Machine.StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - Source extends (...args: ReadonlyArray) => unknown -> = - & Machine.InvokeOwned - & Machine.InvokeTyped< - void, - Stream.Error>, - Stream.Services>, - never - > - -type InvokeChannelIsNever = IsAny extends true ? false : [Value] extends [never] ? true : false - type TransitionBranchRecordError = { readonly "~effect/Machine/TransitionBranchRecordError": Message readonly key: Key @@ -8319,368 +8144,6 @@ type ValidateTransitionBranchRecord = [keyof Branches] extends [never] InvalidStaticTransitionBranchKey > -/** - * Preserves inference for a state-owned invocation configuration. - * - * Use `effect` for one-shot work, `stream` for repeated values, `after` for a - * cancellable timer, `logic` for reusable process logic, or `child` for a - * complete child machine. Stream elements are handled by `onElement` before - * the next element is pulled. `onDone` is required whenever the source can - * complete, while `onFailure` 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. Effect and Stream factories run - * when their owning state is entered and infer the owner context, value, - * 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. - * - * Inside `handle(...)`, the owning definition contextually supplies its public - * input and declared parent protocols. Invocation sources and lifecycle handlers - * can therefore send through `self` and `parent` without naming the definition. - * The standard `Machine.invoke(...)` constructor preserves these contexts - * directly; no intermediate definition method is required. - * - * ```ts - * invoke: Machine.invoke({ - * id: "load", - * effect: () => - * Effect.tryPromise({ - * try: () => fetch("/api/data").then((response) => response.json()), - * catch: (cause) => new LoadError({ cause }) - * }), - * onDone: (to) => - * to.full.Ready().resolve(({ output, target }) => - * target.from({ data: output })), - * onFailure: (to) => - * to.full.Failed().resolve(({ error, target }) => - * target.from({ error })) - * }) - * ``` - * - * @category constructors - * @since 0.9.0 - */ -export const invoke: { - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => Effect.Effect, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & EffectInvokeSource - & { - readonly onDone: EffectDoneHandler - readonly onFailure: EffectFailureHandler - }, - ..._validation: InvokeChannelIsNever>> extends true ? [ - "onDone must be omitted when the Effect output is never" - ] - : InvokeChannelIsNever>> extends true ? [ - "onFailure must be omitted when the Effect error is never" - ] - : [] - ): NoInfer & EffectInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => Effect.Effect, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & EffectInvokeSource - & { - readonly onDone: EffectDoneHandler - readonly onFailure?: never - }, - ..._validation: InvokeChannelIsNever>> extends true ? [ - "onDone must be omitted when the Effect output is never" - ] - : [] - ): NoInfer & EffectInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => Effect.Effect, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & EffectInvokeSource - & { - readonly onDone?: never - readonly onFailure: EffectFailureHandler - }, - ..._validation: InvokeChannelIsNever>> extends true ? [ - "onFailure must be omitted when the Effect error is never" - ] - : [] - ): NoInfer & EffectInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => Effect.Effect, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & EffectInvokeSource - & { - readonly onDone?: never - readonly onFailure?: never - } - ): NoInfer & EffectInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => Stream.Stream, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & StreamInvokeSource - & { - readonly onElement: StreamElementHandler - readonly onDone: StreamDoneHandler - readonly onFailure: StreamFailureHandler - }, - ..._validation: InvokeChannelIsNever>> extends true ? [ - "onElement must be omitted when the Stream element is never" - ] - : InvokeChannelIsNever>> extends true ? [ - "onFailure must be omitted when the Stream error is never" - ] - : [] - ): NoInfer & StreamInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => Stream.Stream, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & StreamInvokeSource - & { - readonly onElement?: never - readonly onDone: StreamDoneHandler - readonly onFailure: StreamFailureHandler - }, - ..._validation: InvokeChannelIsNever>> extends true ? [ - "onFailure must be omitted when the Stream error is never" - ] - : [] - ): NoInfer & StreamInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => Stream.Stream, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & StreamInvokeSource - & { - readonly onElement?: never - readonly onDone: StreamDoneHandler - readonly onFailure?: never - } - ): NoInfer & StreamInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => Stream.Stream, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & StreamInvokeSource - & { - readonly onElement: StreamElementHandler - readonly onDone: StreamDoneHandler - readonly onFailure?: never - } - ): NoInfer & StreamInvokeResult - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: Config & Machine.TimerInvokeArgs - ): - & NoInfer - & Machine.InvokeOwned - & Machine.InvokeTyped - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source extends ( - context: Machine.InvokeContext - ) => unknown, - Address extends ChildAddress, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & { readonly logic: Source } - & Machine.LogicInvokeArgs< - States, - Events, - Emits, - StateId, - Machine.LogicStateOf>, - Machine.LogicEventOf>, - Machine.LogicErrorOf>, - Machine.LogicServicesOf>, - Machine.LogicOutputOf>, - Machine.LogicInitialErrorOf>, - Address, - Source, - InputEvents, - ParentEvents - >, - ..._validation: ReturnType extends { readonly initial: unknown; readonly run: unknown } ? [] : [ - "logic factory must return Machine.Logic" - ] - ): - & NoInfer - & Machine.InvokeOwned - & Machine.InvokeTyped< - Machine.LogicOutputOf>, - Machine.LogicErrorOf>, - Machine.LogicServicesOf>, - Machine.LogicInitialErrorOf> - > - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Source, - Address extends ChildAddress, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & { readonly logic: Source } - & Machine.LogicInvokeArgs< - States, - Events, - Emits, - StateId, - Machine.LogicStateOf, - Machine.LogicEventOf, - Machine.LogicErrorOf, - Machine.LogicServicesOf, - Machine.LogicOutputOf, - Machine.LogicInitialErrorOf, - Address, - Source, - InputEvents, - ParentEvents - >, - ..._validation: Source extends { readonly initial: unknown; readonly run: unknown } ? [] : [ - "logic must implement Machine.Logic" - ] - ): - & NoInfer - & Machine.InvokeOwned - & Machine.InvokeTyped< - Machine.LogicOutputOf, - Machine.LogicErrorOf, - Machine.LogicServicesOf, - Machine.LogicInitialErrorOf - > - < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - StateId extends Machine.StateIdentifier, - const Child extends ChildMachine.Any, - const Config extends object, - const InputEvents extends ReadonlyArray = readonly [], - const ParentEvents extends ReadonlyArray = readonly [] - >( - config: - & Config - & Machine.ChildInvokeArgs - ): - & Config - & Machine.InvokeOwned - & Machine.InvokeTyped< - Machine.Output, - Machine.Error | ActionError>, - Machine.Services, - Machine.InitialError, - Machine.Emit, - Machine.EventOf> - > -} = ((config: unknown) => config) as any /** * Plans the initial state for a machine without executing machine commands. * @@ -9128,7 +8591,7 @@ export const child: (id: Id, mac * Creates a typed parent-local address for lower-level child process logic. * * The default event protocol is `never`; provide an event type before using - * the address with `spawn`, `invoke`, or `sendTo`. + * the address with `spawn`, a state-owned logic invocation, or `sendTo`. * * @category constructors * @since 0.4.0 @@ -9149,7 +8612,8 @@ export const childAddress: (id: string) => ChildAddress = * This Effect requires a managed process runtime. A named child id must be * unique for the current parent until that child stops. * - * @see {@link invoke} for children that start and stop with a state. + * Use a state's `invoke: (from) => from.logic(...)` declaration for children + * that start and stop with that state. * @see {@link sendTo} for sending events to named children. * @category runtime * @since 0.4.0 diff --git a/src/internal/machine/invocation.ts b/src/internal/machine/invocation.ts index 494c527..5e73b3b 100644 --- a/src/internal/machine/invocation.ts +++ b/src/internal/machine/invocation.ts @@ -31,7 +31,7 @@ export interface AnyConfig { export const makeKey = (path: string, id: string): string => `${path.length}:${path}${id}` /** @internal */ -export const makeChildId = (path: string, id: string): string => `Machine.invoke:${makeKey(path, id)}` +export const makeChildId = (path: string, id: string): string => `Machine.invocation:${makeKey(path, id)}` const oneShot = (effect: Effect.Effect): Logic => ({ initial: () => Effect.void, diff --git a/src/internal/machine/machine.ts b/src/internal/machine/machine.ts index 56db901..5951d10 100644 --- a/src/internal/machine/machine.ts +++ b/src/internal/machine/machine.ts @@ -54,6 +54,7 @@ const TypeId = "~effect/Machine" const ParentTypeId = "~effect/Machine/Parent" export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke") export const TransitionTypeId: unique symbol = Symbol.for("effect/Machine/Transition") +const InvokeBuilderDescriptorTypeId: unique symbol = Symbol("effect/Machine/InvokeBuilderDescriptor") const ChildMachineTypeId = "~effect/Machine/ChildMachine" type IsAny = 0 extends 1 & A ? true : false type MachineRuntimeRequirement = internalRuntime.MachineRuntime @@ -788,20 +789,75 @@ const captureEventHandlers = ( return captured } +interface InvokeBuilderDescriptor { + readonly [InvokeBuilderDescriptorTypeId]: typeof InvokeBuilderDescriptorTypeId + readonly config: Readonly> +} + +type InvokeBuilderChannel = "onDone" | "onFailure" | "onElement" | "onSnapshot" + +const makeInvokeBuilder = ( + config: Readonly>, + channels: ReadonlyArray +): InvokeBuilderDescriptor => { + const builder: Record = { + [InvokeBuilderDescriptorTypeId]: InvokeBuilderDescriptorTypeId as typeof InvokeBuilderDescriptorTypeId, + config + } + for (const channel of channels) { + if (!hasProperty(config, channel)) { + builder[channel] = (handler: unknown) => makeInvokeBuilder({ ...config, [channel]: handler }, channels) + } + } + return Object.freeze(builder) as unknown as InvokeBuilderDescriptor +} + +const invokeSelector = Object.freeze({ + effect: (id: string, effect: unknown) => makeInvokeBuilder({ id, effect }, ["onDone", "onFailure"]), + stream: (id: string, stream: unknown) => makeInvokeBuilder({ id, stream }, ["onElement", "onDone", "onFailure"]), + timer: (id: string, after: unknown) => makeInvokeBuilder({ id, after }, ["onDone"]), + logic: (id: string, options: Readonly>) => + makeInvokeBuilder({ id, ...options }, ["onSnapshot", "onDone", "onFailure"]), + child: (child: unknown, options?: Readonly>) => + makeInvokeBuilder(options === undefined ? { child } : { child, ...options }, [ + "onSnapshot", + "onDone", + "onFailure" + ]) +}) + +const invokeBuilderConfig = (value: unknown, path: string): Readonly> => { + if ( + typeof value !== "object" || value === null || + !hasProperty(value, InvokeBuilderDescriptorTypeId) || + value[InvokeBuilderDescriptorTypeId] !== InvokeBuilderDescriptorTypeId + ) { + throw new Error(`Machine invocation for state "${path}" must be constructed from its source selector`) + } + return (value as unknown as InvokeBuilderDescriptor).config +} + const captureInvokeDefinition = ( invoke: unknown, stateNodes: Machine.StateNodes, path: string ): unknown => { - if (Array.isArray(invoke)) return invoke.map((item) => captureInvokeDefinition(item, stateNodes, path)) - if (typeof invoke !== "object" || invoke === null) return invoke - const captured = { ...(invoke as Record) } - for (const key of ["onElement", "onDone", "onFailure", "onSnapshot"] as const) { - if (captured[key] !== undefined) { - captured[key] = captureTransition(captured[key], stateNodes, path, key) + if (typeof invoke !== "function") { + throw new Error(`Machine invocation for state "${path}" must be a source-first callback`) + } + const authored = invoke(invokeSelector) + const definitions = Array.isArray(authored) ? authored : [authored] + const capturedDefinitions = definitions.map((definition) => { + const captured = { ...invokeBuilderConfig(definition, path) } + for (const key of ["onElement", "onDone", "onFailure", "onSnapshot"] as const) { + if (captured[key] !== undefined) { + captured[key] = captureTransition(captured[key], stateNodes, path, key) + } } - } - return captured + return captured + }) + if (Array.isArray(authored)) return capturedDefinitions + return capturedDefinitions[0] } const flattenHandlers = ( diff --git a/test/internal/machine/activities.test.ts b/test/internal/machine/activities.test.ts index 56ef5d8..ad27817 100644 --- a/test/internal/machine/activities.test.ts +++ b/test/internal/machine/activities.test.ts @@ -32,40 +32,32 @@ const activityMachine = Machine.make({ initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({}))) }).handle({ Loading: { - invoke: [ - Machine.invoke({ - id: "poll-server", + invoke: ( + from + ) => [ + from.logic("poll-server", { address: Machine.childAddress("poll-server"), - logic: Machine.logic({ initial: undefined, run: () => Effect.never }) - }), - Machine.invoke({ - id: "load-document", - effect: () => Effect.fail("unavailable").pipe(Effect.as(1)), - onDone: (to) => to.none, - onFailure: (to) => to.none - }), - Machine.invoke({ - id: "load-timeout", - after: timerDuration, - onDone: (to) => to.none - }), - Machine.invoke({ - id: "updates", - stream: () => Stream.empty, - onDone: (to) => to.none + logic: Machine.logic({ + initial: undefined, + run: () => Effect.never + }) }), - Machine.invoke({ child }) + from.effect("load-document", () => Effect.fail("unavailable").pipe(Effect.as(1))).onDone((to) => to.none) + .onFailure((to) => to.none), + from.timer("load-timeout", timerDuration).onDone((to) => to.none), + from.stream("updates", () => Stream.empty).onDone((to) => to.none), + from.child(child) ] }, Dynamic: { - invoke: Machine.invoke({ - id: "context-owned", - address: Machine.childAddress("context-owned"), - logic: () => { - dynamicFactoryEvaluations++ - return Machine.logic({ initial: undefined, run: () => Effect.never }) - } - }) + invoke: (from) => + from.logic("context-owned", { + address: Machine.childAddress("context-owned"), + logic: () => { + dynamicFactoryEvaluations++ + return Machine.logic({ initial: undefined, run: () => Effect.never }) + } + }) } }) @@ -179,11 +171,7 @@ describe("machine activity metadata", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({}))) }).handle({ Loading: { - invoke: Machine.invoke({ - id, - after: durationMillis, - onDone: (to) => to.none - }) + invoke: (from) => from.timer(id, durationMillis).onDone((to) => to.none) } }) const definition = Machine.activityDefinitions(generated)[0] diff --git a/test/internal/machine/invocation.test.ts b/test/internal/machine/invocation.test.ts index 06e81da..26c9594 100644 --- a/test/internal/machine/invocation.test.ts +++ b/test/internal/machine/invocation.test.ts @@ -10,7 +10,7 @@ describe("machine invocation ownership", () => { it("derives stable child addresses in the invocation namespace", () => { assert.strictEqual( makeChildId("root.child", "worker"), - "Machine.invoke:10:root.childworker" + "Machine.invocation:10:root.childworker" ) }) }) diff --git a/test/internal/machine/strategyDifferential.test.ts b/test/internal/machine/strategyDifferential.test.ts index f280746..5fc963d 100644 --- a/test/internal/machine/strategyDifferential.test.ts +++ b/test/internal/machine/strategyDifferential.test.ts @@ -423,15 +423,14 @@ describe("machine planner and runtime strategies", () => { }) const machine = definition.handle({ Streaming: { - invoke: Machine.invoke({ - id: "values", - stream: () => Stream.fromIterable([1, 2, 3]), - onElement: (to) => + invoke: (from) => + from.stream("values", () => Stream.fromIterable([1, 2, 3])).onElement((to) => to.none.resolve(({ element }) => { seen.push(element) - }), - onDone: (to) => to.full.StreamDone().resolve(({ target }) => target(new StreamDone({ values: [...seen] }))) - }) + }) + ).onDone((to) => + to.full.StreamDone().resolve(({ target }) => target(new StreamDone({ values: [...seen] }))) + ) }, StreamDone: { output: ({ state }) => state.values } }) @@ -682,12 +681,10 @@ describe("machine planner and runtime strategies", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "load", - effect: () => Effect.succeed(new Loaded({ value: "complete" })), - onDone: (to) => + invoke: (from) => + from.effect("load", () => Effect.succeed(new Loaded({ value: "complete" }))).onDone((to) => to.full.Success().resolve(({ output, target }) => target(new Success({ value: output.value }))) - }) + ) }, Success: { output: ({ state }) => state.value } }) @@ -725,11 +722,10 @@ describe("machine planner and runtime strategies", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({}))) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "load", - effect: () => Effect.fail("unavailable"), - onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target(new Failed({ error }))) - }) + invoke: (from) => + from.effect("load", () => Effect.fail("unavailable")).onFailure((to) => + to.full.Failed().resolve(({ error, target }) => target(new Failed({ error }))) + ) }, Failed: { output: ({ state }) => state.error } }) @@ -766,12 +762,9 @@ describe("machine planner and runtime strategies", () => { initial: (to) => to.ChildIdle().resolve(({ target }) => target(new ChildIdle({}))) }).handle({ ChildIdle: { - invoke: Machine.invoke({ - id: "notify-parent", - effect: ({ parent }) => parent.send(ParentEvents.ChildReady()), - onDone: (to) => to.none, - onFailure: (to) => to.none - }) + invoke: (from) => + from.effect("notify-parent", ({ parent }) => parent.send(ParentEvents.ChildReady())).onDone((to) => to.none) + .onFailure((to) => to.none) } }) const Child = Machine.child("required-parent-child", childMachine) @@ -785,7 +778,7 @@ describe("machine planner and runtime strategies", () => { initial: (to) => to.ParentWaiting().resolve(({ target }) => target(new ParentWaiting({}))) }).handle({ ParentWaiting: { - invoke: Machine.invoke({ child: Child, onFailure: (to) => to.none }), + invoke: (from) => from.child(Child).onFailure((to) => to.none), on: { ChildReady: (to) => to.full.ParentDone().resolve(({ target }) => target(new ParentDone({}))) } @@ -821,29 +814,28 @@ describe("machine planner and runtime strategies", () => { }) const machine = definition.handle({ Loading: { - invoke: Machine.invoke({ - id: "worker", - address: Machine.childAddress("worker"), - logic: () => { - generation += 1 - const current = generation - return Machine.logic({ - initial: "active", - run: ({ parent, sendTo, setState }) => - parent === undefined ? - Effect.die("worker expected an owning machine") : - (current === 1 ? Deferred.succeed(firstStarted, undefined) : Effect.void).pipe( - Effect.andThen(Effect.never), - Effect.onInterrupt(() => - setState("stale").pipe( - Effect.andThen(sendTo(parent, new Stale({}))) + invoke: (from) => + from.logic("worker", { + address: Machine.childAddress("worker"), + logic: () => { + generation += 1 + const current = generation + return Machine.logic({ + initial: "active", + run: ({ parent, sendTo, setState }) => + parent === undefined ? + Effect.die("worker expected an owning machine") : + (current === 1 ? Deferred.succeed(firstStarted, undefined) : Effect.void).pipe( + Effect.andThen(Effect.never), + Effect.onInterrupt(() => + setState("stale").pipe( + Effect.andThen(sendTo(parent, new Stale({}))) + ) ) ) - ) - }) - }, - onFailure: (to) => to.none, - onSnapshot: (to) => + }) + } + }).onFailure((to) => to.none).onSnapshot((to) => to.branches({ stale: { title: "Worker is stale", target: to.full.Failed() }, unchanged: { target: to.none } @@ -852,7 +844,7 @@ describe("machine planner and runtime strategies", () => { ? select.stale(new Failed({})) : select.unchanged() ) - }), + ), on: { Reenter: (to) => to.full.Loading().resolve(({ state, target }) => target(new Loading({ epoch: state.epoch + 1 })), { diff --git a/test/machine/ActivityLifecycleModel.test.ts b/test/machine/ActivityLifecycleModel.test.ts index 84a647a..8366ed4 100644 --- a/test/machine/ActivityLifecycleModel.test.ts +++ b/test/machine/ActivityLifecycleModel.test.ts @@ -81,13 +81,11 @@ describe("machine activity lifecycle model", () => { } }, Active: { - invoke: Machine.invoke({ - id: "activity", - address: Machine.childAddress("activity"), - logic: probe.logic("active", { _tag: "Blocked" }), - onDone: (to) => to.none, - onFailure: (to) => to.none - }), + invoke: (from) => + from.logic("activity", { + address: Machine.childAddress("activity"), + logic: probe.logic("active", { _tag: "Blocked" }) + }).onDone((to) => to.none).onFailure((to) => to.none), on: { Leave: (to) => to.full.Idle().resolve(({ target }) => target(new Idle({}))), Restart: (to) => to.full.Active().resolve(({ target }) => target(new Active({})), { reenter: true }) @@ -148,13 +146,12 @@ describe("machine activity lifecycle model", () => { initial: (to) => to.Active().resolve(({ target }) => target(new Active({}))) }).handle({ Active: { - invoke: Machine.invoke({ - id: "immediate", - address: Machine.childAddress("immediate"), - logic: probe.immediate("immediate", (epoch) => new Completed({ epoch })), - onDone: (to) => to.full.Done().resolve(({ output, target }) => target(new Done({ epoch: output.epoch }))), - onFailure: (to) => to.none - }) + invoke: (from) => + from.logic("immediate", { + address: Machine.childAddress("immediate"), + logic: probe.immediate("immediate", (epoch) => new Completed({ epoch })) + }).onDone((to) => to.full.Done().resolve(({ output, target }) => target(new Done({ epoch: output.epoch })))) + .onFailure((to) => to.none) }, Done: { output: ({ state }) => state.epoch @@ -184,16 +181,14 @@ describe("machine activity lifecycle model", () => { initial: (to) => to.Active().resolve(({ target }) => target(new EpochActive({ acknowledged: 0 }))) }).handle({ Active: { - invoke: Machine.invoke({ - id: "epoch", - address: Machine.childAddress("epoch"), - logic: probe.logic("epoch", { - _tag: "StaleOnCancel", - event: (epoch) => new Completed({ epoch }) - }), - onDone: (to) => to.none, - onFailure: (to) => to.none - }), + invoke: (from) => + from.logic("epoch", { + address: Machine.childAddress("epoch"), + logic: probe.logic("epoch", { + _tag: "StaleOnCancel", + event: (epoch) => new Completed({ epoch }) + }) + }).onDone((to) => to.none).onFailure((to) => to.none), on: { Restart: (to) => to.full.Active().resolve( @@ -303,13 +298,11 @@ describe("machine activity lifecycle model", () => { left: { states: { active: { - invoke: Machine.invoke({ - id: "left-activity", - address: Machine.childAddress("left-activity"), - logic: probe.logic("left", { _tag: "Blocked" }), - onDone: (to) => to.none, - onFailure: (to) => to.none - }), + invoke: (from) => + from.logic("left-activity", { + address: Machine.childAddress("left-activity"), + logic: probe.logic("left", { _tag: "Blocked" }) + }).onDone((to) => to.none).onFailure((to) => to.none), on: { LeaveLeft: (to) => to.local.idle().resolve(({ target }) => target(new LeftIdle({}))) } @@ -319,13 +312,11 @@ describe("machine activity lifecycle model", () => { right: { states: { active: { - invoke: Machine.invoke({ - id: "right-activity", - address: Machine.childAddress("right-activity"), - logic: probe.logic("right", { _tag: "Blocked" }), - onDone: (to) => to.none, - onFailure: (to) => to.none - }) + invoke: (from) => + from.logic("right-activity", { + address: Machine.childAddress("right-activity"), + logic: probe.logic("right", { _tag: "Blocked" }) + }).onDone((to) => to.none).onFailure((to) => to.none) } } } @@ -368,19 +359,16 @@ describe("machine activity lifecycle model", () => { }).handle({ Idle: {}, Active: { - invoke: [ - Machine.invoke({ - id: "timed-activity", + invoke: ( + from + ) => [ + from.logic("timed-activity", { address: Machine.childAddress("timed-activity"), - logic: probe.logic("timed", { _tag: "Blocked" }), - onDone: (to) => to.none, - onFailure: (to) => to.none - }), - Machine.invoke({ - id: "deadline", - after: "1 hour", - onDone: (to) => to.full.Done().resolve(({ target }) => target(new Done({ epoch: -1 }))) - }) + logic: probe.logic("timed", { _tag: "Blocked" }) + }).onDone((to) => to.none).onFailure((to) => to.none), + from.timer("deadline", "1 hour").onDone((to) => + to.full.Done().resolve(({ target }) => target(new Done({ epoch: -1 }))) + ) ], on: { Leave: (to) => to.full.Idle().resolve(({ target }) => target(new Idle({}))) @@ -413,24 +401,21 @@ describe("machine activity lifecycle model", () => { initial: (to) => to.Active().resolve(({ target }) => target(new Active({}))) }).handle({ Active: { - invoke: [ - Machine.invoke({ - id: "failing", + invoke: ( + from + ) => [ + from.logic("failing", { address: Machine.childAddress("failing"), - logic: probe.logic("failing", { _tag: "Failure" }), - onDone: (to) => to.none, - onFailure: (to) => - to.none.resolve(({ error }) => { - throw error - }) - }), - Machine.invoke({ - id: "sibling", + logic: probe.logic("failing", { _tag: "Failure" }) + }).onDone((to) => to.none).onFailure((to) => + to.none.resolve(({ error }) => { + throw error + }) + ), + from.logic("sibling", { address: Machine.childAddress("sibling"), - logic: probe.logic("sibling", { _tag: "Blocked" }), - onDone: (to) => to.none, - onFailure: (to) => to.none - }) + logic: probe.logic("sibling", { _tag: "Blocked" }) + }).onDone((to) => to.none).onFailure((to) => to.none) ] } }) @@ -462,21 +447,17 @@ describe("machine activity lifecycle model", () => { initial: (to) => to.Active().resolve(({ target }) => target(new Active({}))) }).handle({ Active: { - invoke: [ - Machine.invoke({ - id: "first", + invoke: ( + from + ) => [ + from.logic("first", { address: Machine.childAddress("first"), - logic: probe.logic("first", { _tag: "Blocked" }), - onDone: (to) => to.none, - onFailure: (to) => to.none - }), - Machine.invoke({ - id: "second", + logic: probe.logic("first", { _tag: "Blocked" }) + }).onDone((to) => to.none).onFailure((to) => to.none), + from.logic("second", { address: Machine.childAddress("second"), - logic: probe.logic("second", { _tag: "Blocked" }), - onDone: (to) => to.none, - onFailure: (to) => to.none - }) + logic: probe.logic("second", { _tag: "Blocked" }) + }).onDone((to) => to.none).onFailure((to) => to.none) ] } }) diff --git a/test/machine/Invoke.test.ts b/test/machine/Invoke.test.ts index f77cdee..f45d586 100644 --- a/test/machine/Invoke.test.ts +++ b/test/machine/Invoke.test.ts @@ -24,6 +24,32 @@ class FinishStream extends Schema.TaggedClass("InvokeFinishStream" const States = Machine.states({ Idle, Loading, Complete, Failed }) describe("inline invoke", () => { + it("exposes only source-relevant, unhandled lifecycle methods", () => { + let inspected = false + Machine.make({ + states: States.states, + events: Machine.events(), + initial: (to) => to.Loading().resolve(({ target }) => target.from()) + }).handle({ + Loading: { + invoke: (from) => { + const timer = from.timer("timeout", "1 second") + assert.isFalse("onFailure" in timer) + assert.isFalse("onElement" in timer) + assert.isFalse("onSnapshot" in timer) + const completed = timer.onDone((to) => to.none) + assert.isFalse("onDone" in completed) + inspected = true + return completed + } + }, + Complete: {}, + Failed: {}, + Idle: {} + }) + assert.isTrue(inspected) + }) + it.effect("ignores an invocation outcome when its transition declines", () => Effect.gen(function*() { const machine = Machine.make({ @@ -32,11 +58,10 @@ describe("inline invoke", () => { initial: (to) => to.Loading().resolve(({ target }) => target.from()) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "load", - effect: () => Effect.succeed("ignored"), - onDone: (to) => to.full.Complete().resolve(({ decline }) => decline(), { declinable: true }) - }) + invoke: (from) => + from.effect("load", () => Effect.succeed("ignored")).onDone((to) => + to.full.Complete().resolve(({ decline }) => decline(), { declinable: true }) + ) }, Complete: {}, Failed: {}, @@ -59,16 +84,14 @@ describe("inline invoke", () => { }) const machine = definition.handle({ Collecting: { - invoke: Machine.invoke({ - id: "numbers", - stream: () => Stream.fromIterable([1, 2, 3]), - onElement: (to) => + invoke: (from) => + from.stream("numbers", () => Stream.fromIterable([1, 2, 3])).onElement((to) => to.none.resolve(({ element }, enqueue) => { enqueue.raise(new Add({ value: element })) - }), - onDone: (to) => + }) + ).onDone((to) => to.full.Complete().resolve(({ state, target }) => target(new Complete({ value: state.values.join(",") }))) - }), + ), on: { Add: (to) => to.full.Collecting().resolve(({ event, state, target }) => @@ -136,12 +159,10 @@ describe("inline invoke", () => { }) const machine = definition.handle({ Loading: { - invoke: Machine.invoke({ - id: "updates", - stream: () => Stream.fail("offline"), - onDone: (to) => to.none, - onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target(new Failed({ message: error }))) - }) + invoke: (from) => + from.stream("updates", () => Stream.fail("offline")).onDone((to) => to.none).onFailure((to) => + to.full.Failed().resolve(({ error, target }) => target(new Failed({ message: error }))) + ) }, Complete: {}, Failed: {} @@ -169,11 +190,7 @@ describe("inline invoke", () => { }) const machine = definition.handle({ Loading: { - invoke: Machine.invoke({ - id: "updates", - stream: () => Stream.die(defect), - onDone: (to) => to.none - }) + invoke: (from) => from.stream("updates", () => Stream.die(defect)).onDone((to) => to.none) }, Complete: {}, Failed: {} @@ -204,15 +221,12 @@ describe("inline invoke", () => { }) const machine = definition.handle({ Loading: { - invoke: Machine.invoke({ - id: "updates", - stream: () => source, - onElement: (to) => + invoke: (from) => + from.stream("updates", () => source).onElement((to) => to.none.resolve(({ element }, enqueue) => { enqueue.raise(new FinishStream({ value: element })) - }), - onDone: (to) => to.none - }), + }) + ).onDone((to) => to.none), on: { FinishStream: (to) => to.full.Complete().resolve(({ event, target }) => target(new Complete({ value: String(event.value) }))) @@ -242,11 +256,10 @@ describe("inline invoke", () => { initial: (to) => to.Loading().resolve(({ target }) => target.from()) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "load", - effect: () => Effect.succeed("ready"), - onDone: (to) => to.full.Complete().resolve(({ output, target }) => target(new Complete({ value: output }))) - }) + invoke: (from) => + from.effect("load", () => Effect.succeed("ready")).onDone((to) => + to.full.Complete().resolve(({ output, target }) => target(new Complete({ value: output }))) + ) }, Complete: {}, Failed: {} @@ -277,11 +290,10 @@ describe("inline invoke", () => { initial: (to) => to.Loading().resolve(({ target }) => target.from()) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "load", - effect: () => Effect.fail("offline"), - onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target(new Failed({ message: error }))) - }) + invoke: (from) => + from.effect("load", () => Effect.fail("offline")).onFailure((to) => + to.full.Failed().resolve(({ error, target }) => target(new Failed({ message: error }))) + ) }, Complete: {}, Failed: {} @@ -306,13 +318,10 @@ describe("inline invoke", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "load", - effect: (): Effect.Effect => { + invoke: (from) => + from.effect("load", (): Effect.Effect => { throw defect - }, - onDone: (to) => to.none - }) + }).onDone((to) => to.none) }, Complete: {}, Failed: {} @@ -346,11 +355,7 @@ describe("inline invoke", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "worker", - address: Machine.childAddress("worker"), - logic - }) + invoke: (from) => from.logic("worker", { address: Machine.childAddress("worker"), logic: logic }) }, Complete: {}, Failed: {} diff --git a/test/machine/LiveInspection.test.ts b/test/machine/LiveInspection.test.ts index 9459a32..af16479 100644 --- a/test/machine/LiveInspection.test.ts +++ b/test/machine/LiveInspection.test.ts @@ -131,7 +131,7 @@ describe("Machine live inspection", () => { initial: (to) => to.Idle().resolve(({ target }) => target(new Idle({}))) }).handle({ Idle: { - invoke: Machine.invoke({ id: "worker", effect: () => Effect.never }) + invoke: (from) => from.effect("worker", () => Effect.never) } }) const prepared = yield* Machine.prepare(active) @@ -180,11 +180,7 @@ describe("Machine live inspection", () => { initial: (to) => to.Idle().resolve(({ target }) => target(new Idle({}))) }).handle({ Idle: { - invoke: Machine.invoke({ - id: "updates", - stream: () => Stream.never, - onDone: (to) => to.none - }) + invoke: (from) => from.stream("updates", () => Stream.never).onDone((to) => to.none) } }) const prepared = yield* Machine.prepare(active) @@ -247,7 +243,7 @@ describe("Machine live inspection", () => { initial: (to) => to.ParentIdle().resolve(({ target }) => target(new ParentIdle({}))) }).handle({ ParentIdle: { - invoke: Machine.invoke({ child: Child }), + invoke: (from) => from.child(Child), on: { ChildReady: (to) => to.full.ParentDone().resolve(({ target }) => target(new ParentDone({}))) } diff --git a/test/machine/LocalTargetWith.test.ts b/test/machine/LocalTargetWith.test.ts index f11842b..dc39b3d 100644 --- a/test/machine/LocalTargetWith.test.ts +++ b/test/machine/LocalTargetWith.test.ts @@ -122,14 +122,12 @@ describe("local compound target selection", () => { search: { states: { Searching: { - invoke: Machine.invoke({ - id: "search", - effect: () => Effect.succeed("resolved"), - onDone: (to) => + invoke: (from) => + from.effect("search", () => Effect.succeed("resolved")).onDone((to) => to.local.with.resolve(({ output, target }) => target.from({ query: output }, (search) => search.Updated.from()) ) - }) + ) }, Updated: {} } diff --git a/test/machine/Machine.test.ts b/test/machine/Machine.test.ts index 51e9133..34b02f8 100644 --- a/test/machine/Machine.test.ts +++ b/test/machine/Machine.test.ts @@ -957,18 +957,14 @@ describe("Machine", () => { }) const machine = definition.handle({ Loading: { - invoke: Machine.invoke({ - id: "load", - effect: () => Deferred.await(release), - onDone: (to) => to.full.Waiting().resolve(({ target }) => target.from()) - }) + invoke: (from) => + from.effect("load", () => Deferred.await(release)).onDone((to) => + to.full.Waiting().resolve(({ target }) => target.from()) + ) }, Waiting: { - invoke: Machine.invoke({ - id: "timeout", - after: "1 second", - onDone: (to) => to.full.Done().resolve(({ target }) => target.from()) - }) + invoke: (from) => + from.timer("timeout", "1 second").onDone((to) => to.full.Done().resolve(({ target }) => target.from())) }, Done: {} }) @@ -4722,12 +4718,10 @@ describe("Machine", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "request", - effect: () => Effect.succeed("done:request-1"), - onDone: (to) => + invoke: (from) => + from.effect("request", () => Effect.succeed("done:request-1")).onDone((to) => to.full.Success().resolve(({ output, target }) => target(new Success({ requestId: output }))) - }) + ) }, Success: { output: ({ state }) => state.requestId @@ -4765,12 +4759,10 @@ describe("Machine", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "request", - effect: () => Effect.succeed("done:request-1"), - onDone: (to) => + invoke: (from) => + from.effect("request", () => Effect.succeed("done:request-1")).onDone((to) => to.full.Success().resolve(({ output, target }) => target(new Success({ requestId: output }))) - }) + ) }, Success: { output: ({ state }) => state.requestId @@ -4809,7 +4801,7 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "parent" }))) }).handle({ Loading: { - invoke: Machine.invoke({ child: Child }) + invoke: (from) => from.child(Child) } }) @@ -4859,7 +4851,7 @@ describe("Machine", () => { events: Machine.events(), initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "parent" }))) }).handle({ - Loading: { invoke: Machine.invoke({ child: Child }) } + Loading: { invoke: (from) => from.child(Child) } }) const parent = yield* Machine.start(parentMachine) @@ -4898,7 +4890,7 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "parent" }))) }).handle({ Loading: { - invoke: Machine.invoke({ child: Child, input: { userId: "configured" } }) + invoke: (from) => from.child(Child, { input: { userId: "configured" } }) } }) @@ -4945,11 +4937,10 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "parent" }))) }).handle({ Loading: { - invoke: Machine.invoke({ - child: Child, - onDone: (to) => + invoke: (from) => + from.child(Child).onDone((to) => to.full.Success().resolve(({ output, target }) => target(new Success({ requestId: output }))) - }) + ) }, Success: { output: ({ state }) => state.requestId } }) @@ -5005,10 +4996,7 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "request-1" }))) }).handle({ Loading: { - invoke: [ - Machine.invoke({ child: Child }), - Machine.invoke({ child: Child }) - ] + invoke: (from) => [from.child(Child), from.child(Child)] } }) @@ -5034,17 +5022,11 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "request-1" }))) }).handle({ Loading: { - invoke: [ - Machine.invoke({ - id: "worker", - address: First, - logic: source - }), - Machine.invoke({ - id: "worker", - address: Second, - logic: source - }) + invoke: ( + from + ) => [ + from.logic("worker", { address: First, logic: source }), + from.logic("worker", { address: Second, logic: source }) ] } }) @@ -5072,12 +5054,10 @@ describe("Machine", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "request", - effect: () => Effect.fail(error), - onFailure: (to) => + invoke: (from) => + from.effect("request", () => Effect.fail(error)).onFailure((to) => to.full.Failed().resolve(({ error, target }) => target(new Failed({ message: error.message }))) - }) + ) }, Failed: { output: ({ state }) => state.message @@ -5114,12 +5094,10 @@ describe("Machine", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "request", - effect: () => Effect.succeed("loaded"), - onDone: (to) => + invoke: (from) => + from.effect("request", () => Effect.succeed("loaded")).onDone((to) => to.full.Success().resolve(({ output, target }) => target(new Success({ requestId: output }))) - }) + ) }, Success: { output: ({ state }) => state.requestId @@ -5141,21 +5119,20 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "request-1" }))) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("request-parent"), - logic: Machine.logic({ - initial: undefined, - run: ({ parent, sendTo }) => - parent === undefined ? - Effect.die("child expected an owning actor") : - Deferred.succeed(childStarted, void 0).pipe( - Effect.andThen(sendTo(parent, new RequestSucceeded({ value: "child" }))), - Effect.andThen(Effect.never) - ) - }), - onFailure: (to) => to.none - }), + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("request-parent"), + logic: Machine.logic({ + initial: undefined, + run: ({ parent, sendTo }) => + parent === undefined ? + Effect.die("child expected an owning actor") : + Deferred.succeed(childStarted, void 0).pipe( + Effect.andThen(sendTo(parent, new RequestSucceeded({ value: "child" }))), + Effect.andThen(Effect.never) + ) + }) + }).onFailure((to) => to.none), on: { RequestSucceeded: (to) => to.full.Success().resolve(({ event, target }) => target(new Success({ requestId: event.value }))) @@ -5187,21 +5164,20 @@ describe("Machine", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("stale-request"), - logic: Machine.logic({ - initial: undefined, - run: ({ parent, sendTo }) => - parent === undefined ? - Effect.die("child expected an owning actor") : - Deferred.succeed(childStarted, void 0).pipe( - Effect.andThen(Effect.never), - Effect.onInterrupt(() => sendTo(parent, new RequestSucceeded({ value: "stale" }))) - ) - }), - onFailure: (to) => to.none - }), + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("stale-request"), + logic: Machine.logic({ + initial: undefined, + run: ({ parent, sendTo }) => + parent === undefined ? + Effect.die("child expected an owning actor") : + Deferred.succeed(childStarted, void 0).pipe( + Effect.andThen(Effect.never), + Effect.onInterrupt(() => sendTo(parent, new RequestSucceeded({ value: "stale" }))) + ) + }) + }).onFailure((to) => to.none), on: { Resolve: (to) => to.full.Idle().resolve(({ target }) => target(new Idle({ userId: "resolved" }))) } @@ -5237,12 +5213,10 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "request-1" }))) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "request", - effect: () => Effect.fail(failure), - onFailure: (to) => + invoke: (from) => + from.effect("request", () => Effect.fail(failure)).onFailure((to) => to.full.Failed().resolve(({ error, target }) => target(new Failed({ message: error.message }))) - }) + ) }, Failed: { output: ({ state }) => state.message @@ -5266,12 +5240,10 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "request-1" }))) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "request", - effect: () => requiredMessage, - onDone: (to) => + invoke: (from) => + from.effect("request", () => requiredMessage).onDone((to) => to.full.Success().resolve(({ output, target }) => target(new Success({ requestId: output }))) - }) + ) }, Success: { output: ({ state }) => state.requestId @@ -5297,11 +5269,10 @@ describe("Machine", () => { initial: (to) => to.Loading().resolve(({ target }) => target(new Loading({ requestId: "request-1" }))) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "timeout", - after: "1 hour", - onDone: (to) => to.full.Success().resolve(({ target }) => target(new Success({ requestId: "timeout" }))) - }) + invoke: (from) => + from.timer("timeout", "1 hour").onDone((to) => + to.full.Success().resolve(({ target }) => target(new Success({ requestId: "timeout" }))) + ) }, Success: { output: ({ state }) => state.requestId @@ -5350,12 +5321,11 @@ describe("Machine", () => { } }, Loading: { - invoke: { - id: "request", - address: Machine.childAddress("progress-request"), - logic: Machine.logic({ initial: "pending", run: () => Effect.never }), - onSnapshot - } + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("progress-request"), + logic: Machine.logic({ initial: "pending", run: () => Effect.never }) + }).onSnapshot(onSnapshot) }, Success: { output: ({ state }) => state.requestId @@ -5393,10 +5363,7 @@ describe("Machine", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "request", - effect: () => Effect.die(error) - }) + invoke: (from) => from.effect("request", () => Effect.die(error)) } }) @@ -5457,20 +5424,19 @@ describe("Machine", () => { } }, Loading: { - invoke: { - id: "request", - address: Machine.childAddress("filtered-progress"), - logic: Machine.logic({ - initial: "pending", - run: ({ setState }) => - Deferred.succeed(started, void 0).pipe( - Effect.andThen(Deferred.await(release)), - Effect.andThen(setState("ready")), - Effect.andThen(Effect.never) - ) - }), - onSnapshot - } + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("filtered-progress"), + logic: Machine.logic({ + initial: "pending", + run: ({ setState }) => + Deferred.succeed(started, void 0).pipe( + Effect.andThen(Deferred.await(release)), + Effect.andThen(setState("ready")), + Effect.andThen(Effect.never) + ) + }) + }).onSnapshot(onSnapshot) }, Success: { output: ({ state }) => state.requestId @@ -5516,15 +5482,14 @@ describe("Machine", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("void-request"), - logic: Machine.logic({ - initial: "pending", - run: () => Effect.void - }), - onDone: (to) => to.none - }) + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("void-request"), + logic: Machine.logic({ + initial: "pending", + run: () => Effect.void + }) + }).onDone((to) => to.none) } }) @@ -5571,11 +5536,8 @@ describe("Machine", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("stopping-request"), - logic: childLogic - }), + invoke: (from) => + from.logic("request", { address: Machine.childAddress("stopping-request"), logic: childLogic }), on: { Resolve: (to) => to.full.Success().resolve(({ target }) => target(new Success({ requestId: "request-1" }))), RequestSucceeded: (to) => @@ -5658,22 +5620,22 @@ describe("Machine", () => { })) }).handle({ payment: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("payment-parent"), - logic: makeInvokeLogic("parent", parentStarted) - }), + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("payment-parent"), + logic: makeInvokeLogic("parent", parentStarted) + }), states: { entering: { entry: ({ ancestors, state }) => { assert.deepStrictEqual(state, entering) assert.deepStrictEqual(ancestors, { payment }) }, - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("payment-entering"), - logic: makeInvokeLogic("entering", enteringStarted) - }), + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("payment-entering"), + logic: makeInvokeLogic("entering", enteringStarted) + }), on: { Authorize: (to) => to.local.authorized().resolve(({ event, target }) => @@ -5682,11 +5644,11 @@ describe("Machine", () => { } }, authorized: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("payment-authorized"), - logic: makeInvokeLogic("authorized", authorizedStarted) - }) + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("payment-authorized"), + logic: makeInvokeLogic("authorized", authorizedStarted) + }) } } } @@ -5794,18 +5756,18 @@ describe("Machine", () => { })) }).handle({ fulfillment: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("fulfillment-parent"), - logic: makeInvokeLogic(parentStarted, parentStopping) - }), + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("fulfillment-parent"), + logic: makeInvokeLogic(parentStarted, parentStopping) + }), states: { inventory: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("fulfillment-inventory"), - logic: makeInvokeLogic(inventoryStarted, inventoryStopping) - }), + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("fulfillment-inventory"), + logic: makeInvokeLogic(inventoryStarted, inventoryStopping) + }), states: { checking: { on: { @@ -5816,11 +5778,11 @@ describe("Machine", () => { } }, shipping: { - invoke: Machine.invoke({ - id: "request", - address: Machine.childAddress("fulfillment-shipping"), - logic: makeInvokeLogic(shippingStarted, shippingStopping) - }) + invoke: (from) => + from.logic("request", { + address: Machine.childAddress("fulfillment-shipping"), + logic: makeInvokeLogic(shippingStarted, shippingStopping) + }) } } }, diff --git a/test/machine/MachineReferences.test.ts b/test/machine/MachineReferences.test.ts index 4da97e1..aa2faed 100644 --- a/test/machine/MachineReferences.test.ts +++ b/test/machine/MachineReferences.test.ts @@ -269,7 +269,7 @@ describe("machine reference event channels", () => { initial: (to) => to.Awaiting().resolve(({ target }) => target(new Awaiting({}))) }).handle({ Awaiting: { - invoke: Machine.invoke({ child: Child }), + invoke: (from) => from.child(Child), on: { ChildReported: (to) => to.full.Finished().resolve(({ target }) => target(new Finished({ source: "parent event" }))), @@ -289,7 +289,7 @@ describe("machine reference event channels", () => { assert.strictEqual(yield* parent.join, "parent event") })) - it.effect("types and delivers parent input from a Machine.invoke source", () => + it.effect("types and delivers parent input from an invocation source", () => Effect.gen(function*() { class ChildIdle extends Schema.TaggedClass("BoundInvokeChildIdle")("ChildIdle", {}) {} class ParentWaiting extends Schema.TaggedClass("BoundInvokeParentWaiting")( @@ -309,12 +309,9 @@ describe("machine reference event channels", () => { }) const childMachine = childDefinition.handle({ ChildIdle: { - invoke: Machine.invoke({ - id: "notify-ready", - effect: ({ parent }) => parent.send(ParentEvents.ChildReady()), - onDone: (to) => to.none, - onFailure: (to) => to.none - }) + invoke: (from) => + from.effect("notify-ready", ({ parent }) => parent.send(ParentEvents.ChildReady())).onDone((to) => to.none) + .onFailure((to) => to.none) } }) assert.strictEqual(childMachine.parent?.mode, "required") @@ -329,10 +326,7 @@ describe("machine reference event channels", () => { initial: (to) => to.ParentWaiting().resolve(({ target }) => target(new ParentWaiting({}))) }).handle({ ParentWaiting: { - invoke: Machine.invoke({ - child: Child, - onFailure: (to) => to.none - }), + invoke: (from) => from.child(Child).onFailure((to) => to.none), on: { ChildReady: (to) => to.full.ParentDone().resolve(({ target }) => target(new ParentDone({}))) } diff --git a/test/machine/Resume.test.ts b/test/machine/Resume.test.ts index 0a9b987..8d692ba 100644 --- a/test/machine/Resume.test.ts +++ b/test/machine/Resume.test.ts @@ -73,52 +73,39 @@ describe("Machine.resume", () => { initial: (to) => to.Inactive().resolve(({ target }) => target(new Inactive({}))) }).handle({ Root: { - invoke: Machine.invoke({ - id: "root", - address: Machine.childAddress("root"), - logic: restoredLogic("root") - }), + invoke: (from) => from.logic("root", { address: Machine.childAddress("root"), logic: restoredLogic("root") }), states: { left: { - invoke: Machine.invoke({ - id: "left", - address: Machine.childAddress("left"), - logic: restoredLogic("left") - }), + invoke: (from) => + from.logic("left", { address: Machine.childAddress("left"), logic: restoredLogic("left") }), states: { On: { - invoke: Machine.invoke({ - id: "left-leaf", - address: Machine.childAddress("left-leaf"), - logic: restoredLogic("left-leaf") - }) + invoke: (from) => + from.logic("left-leaf", { + address: Machine.childAddress("left-leaf"), + logic: restoredLogic("left-leaf") + }) } } }, right: { - invoke: Machine.invoke({ - id: "right", - address: Machine.childAddress("right"), - logic: restoredLogic("right") - }), + invoke: (from) => + from.logic("right", { address: Machine.childAddress("right"), logic: restoredLogic("right") }), states: { On: { - invoke: Machine.invoke({ - id: "right-leaf", - address: Machine.childAddress("right-leaf"), - logic: restoredLogic("right-leaf") - }) + invoke: (from) => + from.logic("right-leaf", { + address: Machine.childAddress("right-leaf"), + logic: restoredLogic("right-leaf") + }) } } } } }, Inactive: { - invoke: Machine.invoke({ - id: "inactive", - address: Machine.childAddress("inactive"), - logic: restoredLogic("inactive") - }) + invoke: (from) => + from.logic("inactive", { address: Machine.childAddress("inactive"), logic: restoredLogic("inactive") }) } }) const snapshot = { @@ -345,11 +332,10 @@ describe("Machine.resume", () => { initial: (to) => to.Cancelled().resolve(({ target }) => target(new Cancelled({}))) }).handle({ Waiting: { - invoke: Machine.invoke({ - id: "timeout", - after: "1 second", - onDone: (to) => to.full.TimedOut().resolve(({ target }) => target(new TimedOut({}))) - }), + invoke: (from) => + from.timer("timeout", "1 second").onDone((to) => + to.full.TimedOut().resolve(({ target }) => target(new TimedOut({}))) + ), on: { Cancel: (to) => to.full.Cancelled().resolve(({ target }) => target(new Cancelled({}))) } @@ -388,11 +374,10 @@ describe("Machine.resume", () => { initial: (to) => to.Loaded().resolve(({ target }) => target(new Loaded({ value: "initial" }))) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "load", - effect: () => Ref.updateAndGet(runs, (n) => n + 1).pipe(Effect.as("fresh")), - onDone: (to) => to.full.Loaded().resolve(({ output, target }) => target(new Loaded({ value: output }))) - }) + invoke: (from) => + from.effect("load", () => Ref.updateAndGet(runs, (n) => n + 1).pipe(Effect.as("fresh"))).onDone((to) => + to.full.Loaded().resolve(({ output, target }) => target(new Loaded({ value: output }))) + ) }, Loaded: {} }) @@ -423,15 +408,13 @@ describe("Machine.resume", () => { initial: (to) => to.Failed().resolve(({ target }) => target(new Failed({ message: "initial" }))) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "load", - effect: () => + invoke: (from) => + from.effect("load", () => Ref.update(runs, (n) => n + 1).pipe( Effect.andThen(Effect.fail(new LoadFailure({ message: "offline" }))) - ), - onFailure: (to) => - to.full.Failed().resolve(({ error, target }) => target(new Failed({ message: error.message }))) - }) + )).onFailure((to) => + to.full.Failed().resolve(({ error, target }) => target(new Failed({ message: error.message }))) + ) }, Failed: {} }) @@ -477,11 +460,10 @@ describe("Machine.resume", () => { initial: (to) => to.ChildOutput().resolve(({ target }) => target(new ChildOutput({ value: 0 }))) }).handle({ Parent: { - invoke: Machine.invoke({ - child: Child, - onDone: (to) => + invoke: (from) => + from.child(Child).onDone((to) => to.full.ChildOutput().resolve(({ output, target }) => target(new ChildOutput({ value: output }))) - }) + ) }, ChildOutput: {} }) diff --git a/test/testing/Probe.test.ts b/test/testing/Probe.test.ts index d29ce68..b8fee8b 100644 --- a/test/testing/Probe.test.ts +++ b/test/testing/Probe.test.ts @@ -162,13 +162,11 @@ describe("MachineTest probe", () => { } }, Loading: { - invoke: Machine.invoke({ - id: "loader", - effect: () => { + invoke: (from) => + from.effect("loader", () => { starts += 1 return Effect.never - } - }) + }) } }) const ref = yield* Machine.start(invokeMachine) diff --git a/test/testing/Runtime.test.ts b/test/testing/Runtime.test.ts index 190bf75..ff1b2f9 100644 --- a/test/testing/Runtime.test.ts +++ b/test/testing/Runtime.test.ts @@ -250,11 +250,10 @@ describe("MachineTest runtime commands", () => { initial: (to) => to.Waiting().resolve(({ target }) => target(new Waiting({}))) }).handle({ Waiting: { - invoke: Machine.invoke({ - id: "timeout", - after: "1 second", - onDone: (to) => to.full.TimedOut().resolve(({ target }) => target(new TimedOut({}))) - }) + invoke: (from) => + from.timer("timeout", "1 second").onDone((to) => + to.full.TimedOut().resolve(({ target }) => target(new TimedOut({}))) + ) }, TimedOut: {} }) @@ -731,11 +730,10 @@ describe("MachineTest causal runtime commands", () => { initial: (to) => to.Waiting().resolve(({ target }) => target(new Waiting({}))) }).handle({ Waiting: { - invoke: Machine.invoke({ - id: "timeout", - after: "1 second", - onDone: (to) => to.full.TimedOut().resolve(({ target }) => target(new TimedOut({}))) - }) + invoke: (from) => + from.timer("timeout", "1 second").onDone((to) => + to.full.TimedOut().resolve(({ target }) => target(new TimedOut({}))) + ) }, TimedOut: {} }) diff --git a/test/testing/Verification.test.ts b/test/testing/Verification.test.ts index 9e5e40a..0222215 100644 --- a/test/testing/Verification.test.ts +++ b/test/testing/Verification.test.ts @@ -101,17 +101,11 @@ const invokedMachine = Machine.make({ initial: (to) => to.counter().resolve(({ target }) => target(new Counter({ count: 0 }))) }).handle({ counter: { - invoke: [ - Machine.invoke({ - id: "first", - effect: () => Effect.succeed(1), - onDone: (to) => to.none - }), - Machine.invoke({ - id: "second", - effect: () => Effect.succeed(2), - onDone: (to) => to.none - }) + invoke: ( + from + ) => [ + from.effect("first", () => Effect.succeed(1)).onDone((to) => to.none), + from.effect("second", () => Effect.succeed(2)).onDone((to) => to.none) ] } }) diff --git a/test/unstable/cluster/ClusterMachine.test.ts b/test/unstable/cluster/ClusterMachine.test.ts index a986b8a..bb797b1 100644 --- a/test/unstable/cluster/ClusterMachine.test.ts +++ b/test/unstable/cluster/ClusterMachine.test.ts @@ -582,11 +582,7 @@ describe("ClusterMachine", () => { initial: (to) => to.Count().resolve(({ target }) => target(new Count({ value: 0 }))) }).handle({ Count: { - invoke: Machine.invoke({ - id: "child", - effect: () => Effect.void, - onDone: (to) => to.none - }) + invoke: (from) => from.effect("child", () => Effect.void).onDone((to) => to.none) } }) const bridge = ClusterMachine.make("InvokedCounter", invoked, { version: "1" }) diff --git a/test/unstable/reactivity/AtomMachine.test.ts b/test/unstable/reactivity/AtomMachine.test.ts index d8baf75..e3cda1d 100644 --- a/test/unstable/reactivity/AtomMachine.test.ts +++ b/test/unstable/reactivity/AtomMachine.test.ts @@ -152,14 +152,14 @@ describe("AtomMachine", () => { }) }).handle({ Count: { - invoke: Machine.invoke({ - id: "active", - address: Machine.childAddress("active"), - logic: Machine.logic({ - initial: () => Ref.update(invokeStarts, (n) => n + 1).pipe(Effect.as(undefined)), - run: () => Effect.never.pipe(Effect.onInterrupt(() => Deferred.succeed(invokeStopped, void 0))) - }) - }), + invoke: (from) => + from.logic("active", { + address: Machine.childAddress("active"), + logic: Machine.logic({ + initial: () => Ref.update(invokeStarts, (n) => n + 1).pipe(Effect.as(undefined)), + run: () => Effect.never.pipe(Effect.onInterrupt(() => Deferred.succeed(invokeStopped, void 0))) + }) + }), on: { Finish: (to) => to.full.Count().resolve(({ event, state, target }) => @@ -215,10 +215,7 @@ describe("AtomMachine", () => { } }, ValueRead: { - invoke: Machine.invoke({ - child: Child, - onDone: (to) => to.none - }), + invoke: (from) => from.child(Child).onDone((to) => to.none), on: { ReadValue: (to) => to.full.Count().resolve(({ target }) => target(new Count({ value: 0 }))) } diff --git a/typetest/machine/Activities.tst.ts b/typetest/machine/Activities.tst.ts index 5c07a6f..1e720b4 100644 --- a/typetest/machine/Activities.tst.ts +++ b/typetest/machine/Activities.tst.ts @@ -13,18 +13,10 @@ const machine = Machine.make({ initial: (to) => to.Loading().resolve(({ target }) => (target(new Loading({})))) }).handle({ Loading: { - invoke: Machine.invoke({ - id: "timeout", - after: "1 second", - onDone: (to) => to.none - }) + invoke: (from) => from.timer("timeout", "1 second").onDone((to) => to.none) }, Dynamic: { - invoke: Machine.invoke({ - id: "dynamic", - after: () => "2 seconds" as const, - onDone: (to) => to.none - }) + invoke: (from) => from.timer("dynamic", () => "2 seconds" as const).onDone((to) => to.none) } }) diff --git a/typetest/machine/EventConstructors.tst.ts b/typetest/machine/EventConstructors.tst.ts index 3a76e54..146386a 100644 --- a/typetest/machine/EventConstructors.tst.ts +++ b/typetest/machine/EventConstructors.tst.ts @@ -109,25 +109,21 @@ describe("Machine event constructor collections", () => { expect( machine.handle({ Idle: { - invoke: [ - Machine.invoke({ - id: "load", - effect: () => Effect.succeed("ready"), - onDone: (to) => - to.none.resolve(({ output }, enqueue) => { - enqueue.raise(internalEvents.Loaded({ value: output })) - return undefined - }) - }), - Machine.invoke({ - id: "timeout", - after: "1 second", - onDone: (to) => - to.none.resolve((_, enqueue) => { - enqueue.raise(internalEvents.Failed()) - return undefined - }) - }) + invoke: ( + from + ) => [ + from.effect("load", () => Effect.succeed("ready")).onDone((to) => + to.none.resolve(({ output }, enqueue) => { + enqueue.raise(internalEvents.Loaded({ value: output })) + return undefined + }) + ), + from.timer("timeout", "1 second").onDone((to) => + to.none.resolve((_, enqueue) => { + enqueue.raise(internalEvents.Failed()) + return undefined + }) + ) ] } }) diff --git a/typetest/machine/Machine.tst.ts b/typetest/machine/Machine.tst.ts index b91a4f4..b7c8f31 100644 --- a/typetest/machine/Machine.tst.ts +++ b/typetest/machine/Machine.tst.ts @@ -95,6 +95,18 @@ describe("Machine", () => { }, down: Down }) + type DownInvokeSelector = Machine.Machine.InvokeSelector< + typeof UpStates.states, + readonly [typeof SignIn], + readonly [], + "down" + > + type DownInvoke = Machine.Machine.InvokeBuilderInput< + typeof UpStates.states, + readonly [typeof SignIn], + readonly [], + "down" + > const NestedParallelStates = Machine.states({ root: { @@ -529,6 +541,7 @@ describe("Machine", () => { }) it("invoke infers one-shot outputs from factories in the owning state", () => { + expect(Machine).type.not.toHaveProperty("invoke") const machine = Machine.make({ states: UpStates.states, events: Machine.events(SignIn), @@ -537,25 +550,19 @@ describe("Machine", () => { machine.handle({ down: { - invoke: Machine.invoke({ - id: "valid", - effect: () => Effect.succeed(1), - onDone: (to) => to.full.down().resolve(({ target }) => target(new Down({}))) - }) + invoke: (from) => + from.effect("valid", () => Effect.succeed(1)).onDone((to) => + to.full.down().resolve(({ target }) => target(new Down({}))) + ) } }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "invalid", - effect: () => Effect.succeed(1) - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "direct-effect", - effect: Effect.succeed(1), - onDone: () => undefined - }) + const incomplete = (from: DownInvokeSelector) => from.effect("invalid", () => Effect.succeed(1)) + const from = null as unknown as DownInvokeSelector + expect(incomplete).type.not.toBeAssignableTo() + expect(from.effect).type.not.toBeCallableWith("direct-effect", Effect.succeed(1)) }) - it("contextually types dynamic Effect sources through Machine.invoke", () => { + it("contextually types dynamic Effect sources through the fluent invocation builder", () => { const machine = Machine.make({ states: UpStates.states, events: Machine.events(SignIn), @@ -564,19 +571,16 @@ describe("Machine", () => { machine.handle({ down: { - invoke: Machine.invoke({ - id: "dynamic", - effect: ({ state }) => { + invoke: (from) => + from.effect("dynamic", ({ state }) => { expect(state).type.toBe() return Effect.succeed(state._tag) - }, - onDone: (to) => to.full.down().resolve(({ target }) => target(new Down({}))) - }) + }).onDone((to) => to.full.down().resolve(({ target }) => target(new Down({})))) } }) }) - it("infers Stream elements, failures, and services through Machine.invoke", () => { + it("infers Stream elements, failures, and services through the fluent invocation builder", () => { class StreamFailure { readonly _tag = "StreamFailure" } @@ -590,22 +594,19 @@ describe("Machine", () => { }) const handled = machine.handle({ down: { - invoke: Machine.invoke({ - id: "updates", - stream: ({ state }) => { + invoke: (from) => + from.stream("updates", ({ state }) => { expect(state).type.toBe() return updates - }, - onElement: (to) => + }).onElement((to) => to.none.resolve(({ element }) => { expect(element).type.toBe<1>() - }), - onDone: (to) => to.none, - onFailure: (to) => + }) + ).onDone((to) => to.none).onFailure((to) => to.none.resolve(({ error }) => { expect(error).type.toBe() }) - }) + ) } }) @@ -614,22 +615,19 @@ describe("Machine", () => { const staticHandled = machine.handle({ down: { - invoke: Machine.invoke({ - id: "static-updates", - stream: ({ state }) => { + invoke: (from) => + from.stream("static-updates", ({ state }) => { expect(state).type.toBe() return updates - }, - onElement: (to) => + }).onElement((to) => to.none.resolve(({ element }) => { expect(element).type.toBe<1>() - }), - onDone: (to) => to.none, - onFailure: (to) => + }) + ).onDone((to) => to.none).onFailure((to) => to.none.resolve(({ error }) => { expect(error).type.toBe() }) - }) + ) } }) @@ -647,31 +645,18 @@ describe("Machine", () => { const values = (_: Context) => Stream.make(1) const failure = (_: Context) => Stream.fail("unavailable" as const) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "missing-element", - stream: values, - onDone: (to: any) => to.none - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "missing-done", - stream: values, - onElement: (to: any) => to.none - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "missing-failure", - stream: failure, - onDone: (to: any) => to.none - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "unreachable-element", - stream: failure, - onElement: (to: any) => to.none, - onDone: (to: any) => to.none, - onFailure: (to: any) => to.none - }) + const missingElement = (from: DownInvokeSelector) => from.stream("missing-element", values).onDone((to) => to.none) + const missingDone = (from: DownInvokeSelector) => from.stream("missing-done", values).onElement((to) => to.none) + const missingFailure = (from: DownInvokeSelector) => from.stream("missing-failure", failure).onDone((to) => to.none) + const from = null as unknown as DownInvokeSelector + + expect(missingElement).type.not.toBeAssignableTo() + expect(missingDone).type.not.toBeAssignableTo() + expect(missingFailure).type.not.toBeAssignableTo() + expect(from.stream("unreachable-element", failure)).type.not.toHaveProperty("onElement") }) - it("infers dynamic Machine.invoke Effect channels from the source return", () => { + it("infers dynamic Effect invocation channels from the source return", () => { class LoadFailure { readonly _tag = "LoadFailure" } @@ -684,20 +669,16 @@ describe("Machine", () => { machine.handle({ down: { - invoke: Machine.invoke({ - id: "dynamic", - effect: ({ state }) => { + invoke: (from) => + from.effect("dynamic", ({ state }) => { expect(state).type.toBe() return load(state._tag) - }, - onDone: (to) => to.none, - onFailure: (to) => to.none - }) + }).onDone((to) => to.none).onFailure((to) => to.none) } }) }) - it("requires only reachable handlers for dynamic Machine.invoke Effects", () => { + it("requires only reachable handlers for dynamic Effect invocations", () => { class LoadFailure { readonly _tag = "LoadFailure" } @@ -709,37 +690,26 @@ describe("Machine", () => { machine.handle({ down: { - invoke: [ - Machine.invoke({ - id: "success", - effect: ({ state }) => Effect.succeed(state._tag), - onDone: (to) => to.none - }), - Machine.invoke({ - id: "failure", - effect: ({ state }) => Effect.fail(new LoadFailure()).pipe(Effect.annotateLogs("state", state._tag)), - onFailure: (to) => to.none - }), - Machine.invoke({ - id: "never", - effect: ({ state }) => Effect.never.pipe(Effect.annotateLogs("state", state._tag)) - }), - Machine.invoke({ - id: "requirements", - effect: ({ state }) => Effect.as(EntryRequirement, state._tag), - onDone: (to) => to.none - }) + invoke: ( + from + ) => [ + from.effect("success", ({ state }) => Effect.succeed(state._tag)).onDone((to) => to.none), + from.effect( + "failure", + ({ state }) => Effect.fail(new LoadFailure()).pipe(Effect.annotateLogs("state", state._tag)) + ).onFailure((to) => to.none), + from.effect("never", ({ state }) => Effect.never.pipe(Effect.annotateLogs("state", state._tag))), + from.effect("requirements", ({ state }) => Effect.as(EntryRequirement, state._tag)).onDone((to) => to.none) ] } }) const requirementsHandled = machine.handle({ down: { - invoke: Machine.invoke({ - id: "requirements-only", - effect: ({ state }) => Effect.as(EntryRequirement, state._tag), - onDone: (to) => to.none - }) + invoke: (from) => + from.effect("requirements-only", ({ state }) => Effect.as(EntryRequirement, state._tag)).onDone((to) => + to.none + ) } }) @@ -748,7 +718,7 @@ describe("Machine", () => { expect().type.not.toBeAssignableTo>() }) - it("rejects unreachable and missing handlers for dynamic Machine.invoke Effects", () => { + it("rejects unreachable and missing handlers for dynamic Effect invocations", () => { type Context = Machine.Machine.InvokeContext< typeof UpStates.states, readonly [typeof SignIn], @@ -759,36 +729,19 @@ describe("Machine", () => { const failure = (_: Context) => Effect.fail("unavailable" as const) const pending = (_: Context) => Effect.never - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "missing-done", - effect: success - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "unreachable-failure", - effect: success, - onDone: () => undefined, - onFailure: () => undefined - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "missing-failure", - effect: failure - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "unreachable-done", - effect: failure, - onDone: () => undefined, - onFailure: () => undefined - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "pending-done", - effect: pending, - onDone: () => undefined - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "pending-failure", - effect: pending, - onFailure: () => undefined - }) + const missingDone = (from: DownInvokeSelector) => from.effect("missing-done", success) + const missingFailure = (from: DownInvokeSelector) => from.effect("missing-failure", failure) + const from = null as unknown as DownInvokeSelector + const successful = from.effect("unreachable-failure", success) + const failed = from.effect("unreachable-done", failure) + const never = from.effect("pending", pending) + + expect(missingDone).type.not.toBeAssignableTo() + expect(missingFailure).type.not.toBeAssignableTo() + expect(successful).type.not.toHaveProperty("onFailure") + expect(failed).type.not.toHaveProperty("onDone") + expect(never).type.not.toHaveProperty("onDone") + expect(never).type.not.toHaveProperty("onFailure") }) it("separates public input events from the complete internal protocol", () => { @@ -854,27 +807,30 @@ describe("Machine", () => { initial: (to) => to.down().resolve(({ target }) => (target(new Down({})))) }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "missing-failure", - effect: failure - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "unreachable-failure", - effect: () => Effect.succeed("user-1"), - onDone: () => undefined, - onFailure: () => undefined - }) - expect(Machine.invoke).type.not.toBeCallableWith({ - id: "erased-failure", - effect: erasedFailure - }) + type Selector = Machine.Machine.InvokeSelector< + typeof UpStates.states, + readonly [typeof SignIn, typeof SignInCompleted], + readonly [], + "down", + readonly [typeof SignIn] + > + type Invoke = Machine.Machine.InvokeBuilderInput< + typeof UpStates.states, + readonly [typeof SignIn, typeof SignInCompleted], + readonly [], + "down", + readonly [typeof SignIn] + > + const missingFailure = (from: Selector) => from.effect("missing-failure", failure) + const erased = (from: Selector) => from.effect("erased-failure", erasedFailure) + const from = null as unknown as Selector + + expect(missingFailure).type.not.toBeAssignableTo() + expect(erased).type.not.toBeAssignableTo() + expect(from.effect("unreachable-failure", () => Effect.succeed("user-1"))).type.not.toHaveProperty("onFailure") machine.handle({ down: { - invoke: Machine.invoke({ - id: "erased-failure", - effect: erasedFailure, - onFailure: (to) => to.none - }) + invoke: (from) => from.effect("erased-failure", erasedFailure).onFailure((to) => to.none) } }) }) @@ -929,80 +885,37 @@ describe("Machine", () => { events: Machine.events(SignIn), initial: (to) => to.down().resolve(({ target }) => (target(new Down({})))) }) - - type ChildArgs = Machine.Machine.ChildInvokeArgs< + type ParentInvokeSelector = Machine.Machine.InvokeSelector< typeof UpStates.states, readonly [typeof SignIn], readonly [], - "down", - typeof Child.machine, - typeof Child + "down" > - const onSnapshot: NonNullable = (to) => - to.none.resolve(({ snapshot }) => { - expect(snapshot.state).type.toBe>() - return undefined - }) - const onDone: ChildArgs["onDone"] = (to) => - to.none.resolve(({ output, state }) => { - expect(output).type.toBe() - expect(state).type.toBe() - return undefined - }) parent.handle({ down: { - invoke: Machine.invoke({ - child: Child, - input: { userId: "child" }, - onSnapshot, - onDone - }) + invoke: (from: ParentInvokeSelector) => + from.child(Child, { input: { userId: "child" } }).onSnapshot((to) => + to.none.resolve(({ snapshot }) => { + expect(snapshot.state).type.toBe>() + return undefined + }) + ).onDone((to) => + to.none.resolve(({ output, state }) => { + expect(output).type.toBe() + expect(state).type.toBe() + return undefined + }) + ) } }) expect(parent.handle).type.not.toBeCallableWith({ down: { invoke: { child: Child, onDone: () => undefined } } }) - const incompatibleEmits = Machine.make({ - states: childStates.states, - events: Machine.events(SignIn), - emittedEvents: Machine.emittedEvents(Down), - input: ChildInput, - initial: (to) => to.done().resolve(({ target }) => (target(new Down({})))) - }).handle({ - done: { - output: () => new SignIn({ userId: "child" }) - } - }) - expect(parent.handle).type.not.toBeCallableWith({ - down: { - invoke: { - child: Machine.child("incompatible", incompatibleEmits), - input: { userId: "child" }, - onDone: () => undefined - } - } - }) - expect(parent.handle).type.not.toBeCallableWith({ - down: { - invoke: { - child: Child, - input: { userId: "child" }, - onSnapshot: () => new Down({}), - onDone: () => undefined - } - } - }) - expect(parent.handle).type.not.toBeCallableWith({ - down: { - invoke: { - child: Child, - input: { userId: "child" }, - onDone: () => new Down({}) - } - } - }) + const childBuilder = (null as unknown as ParentInvokeSelector).child(Child, { input: { userId: "child" } }) + expect(childBuilder.onSnapshot).type.not.toBeCallableWith(() => new Down({})) + expect(childBuilder.onDone).type.not.toBeCallableWith(() => new Down({})) }) it("types nested invocation output handlers against their owning state", () => { @@ -1018,11 +931,7 @@ describe("Machine", () => { auth: { states: { signedOut: { - invoke: Machine.invoke({ - id: "nested", - effect: () => Effect.succeed(Option.some(1)), - onDone: (to) => to.none - }) + invoke: (from) => from.effect("nested", () => Effect.succeed(Option.some(1))).onDone((to) => to.none) } } }, @@ -1600,10 +1509,8 @@ describe("Machine", () => { }) expect(Machine.planInitial).type.not.toBeCallableWith(machine) expect(Machine.start).type.not.toBeCallableWith(machine) - expect(Machine.invoke).type.not.toBeCallableWith({ - child: Machine.child("incomplete", machine), - onDone: () => undefined - }) + const from = null as unknown as DownInvokeSelector + expect(from.child).type.not.toBeCallableWith(Machine.child("incomplete", machine)) expect(machine).type.not.toBeAssignableTo() const complete = machine.handle({ diff --git a/typetest/machine/MachineReferences.tst.ts b/typetest/machine/MachineReferences.tst.ts index 917caf4..5064ec4 100644 --- a/typetest/machine/MachineReferences.tst.ts +++ b/typetest/machine/MachineReferences.tst.ts @@ -113,7 +113,7 @@ describe("machine reference event channels", () => { initial: (to) => to.Idle().resolve(({ target }) => (target(new Idle({})))) }) compatible.handle({ - Idle: { invoke: Machine.invoke({ child: Child }) } + Idle: { invoke: (from) => from.child(Child) } }) const incompatible = Machine.make({ @@ -122,7 +122,16 @@ describe("machine reference event channels", () => { initial: (to) => to.Idle().resolve(({ target }) => (target(new Idle({})))) }) expect(incompatible.handle).type.not.toBeCallableWith({ - Idle: { invoke: Machine.invoke({ child: Child }) } + Idle: { + invoke: ( + from: Machine.Machine.InvokeSelector< + typeof states.states, + readonly [typeof Ping, typeof OtherParentEvent], + readonly [], + "Idle" + > + ) => from.child(Child) + } }) }) @@ -153,7 +162,7 @@ describe("machine reference event channels", () => { expect>().type.toBe() }) - it("contextually binds Machine.invoke self and parent references to the owning machine protocols", () => { + it("contextually binds invocation self and parent references to the owning machine protocols", () => { Machine.make({ states: states.states, events: Events, @@ -163,24 +172,22 @@ describe("machine reference event channels", () => { initial: (to) => to.Idle().resolve(({ target }) => (target(new Idle({})))) }).handle({ Idle: { - invoke: Machine.invoke({ - id: "notify-parent", - effect: ({ parent, self }) => { + invoke: (from) => + from.effect("notify-parent", ({ parent, self }) => { expect(self.send).type.toBeCallableWith(Events.Ping()) expect(self.send).type.not.toBeCallableWith(InternalEvents.Local()) expect(self.send).type.not.toBeCallableWith(ParentEvents.ParentNotice({ value: 1 })) expect(parent.send).type.toBeCallableWith(ParentEvents.ParentNotice({ value: 1 })) expect(parent.send).type.not.toBeCallableWith(Events.Ping()) return Effect.void - }, - onDone: (to) => + }).onDone((to) => to.none.resolve(({ parent, self }, enqueue) => { enqueue.sendTo(self, Events.Ping()) enqueue.sendTo(parent, ParentEvents.ParentNotice({ value: 1 })) expect(enqueue.sendTo).type.not.toBeCallableWith(parent, Events.Ping()) return undefined }) - }) + ) } }) @@ -191,17 +198,15 @@ describe("machine reference event channels", () => { initial: (to) => to.Idle().resolve(({ target }) => target.from()) }).handle({ Idle: { - invoke: Machine.invoke({ - id: "notify-parent-failure", - effect: () => Effect.fail("failed" as const), - onFailure: (to) => + invoke: (from) => + from.effect("notify-parent-failure", () => Effect.fail("failed" as const)).onFailure((to) => to.none.resolve(({ parent, self }, enqueue) => { enqueue.sendTo(self, Events.Ping()) enqueue.sendTo(parent, ParentEvents.ParentNotice({ value: 1 })) expect(enqueue.sendTo).type.not.toBeCallableWith(parent, Events.Ping()) return undefined }) - }) + ) } }) diff --git a/typetest/machine/Readiness.tst.ts b/typetest/machine/Readiness.tst.ts index 15e798c..2748578 100644 --- a/typetest/machine/Readiness.tst.ts +++ b/typetest/machine/Readiness.tst.ts @@ -64,8 +64,15 @@ const outputIncomplete = Machine.make({ initial: (to) => to.Ready().resolve(({ target }) => (target(new Ready({})))) }) const outputSnapshot = { path: "Ready" as const, value: new Ready({}) } +type InvokeSelector = Machine.Machine.InvokeSelector< + typeof outputStates.states, + readonly [typeof Tick], + readonly [], + "Ready" +> const bound = null as unknown as AtomMachine.Bound +const from = null as unknown as InvokeSelector describe("executable machine readiness", () => { it("rejects an unimplemented choice at every planning and execution boundary", () => { @@ -73,9 +80,7 @@ describe("executable machine readiness", () => { expect(Machine.plan).type.not.toBeCallableWith(choiceIncomplete, choiceSnapshot, new Tick({})) expect(Machine.start).type.not.toBeCallableWith(choiceIncomplete) expect(Machine.resume).type.not.toBeCallableWith(choiceIncomplete, choiceSnapshot) - expect(Machine.invoke).type.not.toBeCallableWith({ - child: Machine.child("choice", choiceIncomplete) - }) + expect(from.child).type.not.toBeCallableWith(Machine.child("choice", choiceIncomplete)) expect(MachineTest.run).type.not.toBeCallableWith(choiceIncomplete, { events: [] }) expect(AtomMachine.make).type.not.toBeCallableWith(choiceIncomplete) expect(AtomMachine.resume).type.not.toBeCallableWith(choiceIncomplete, choiceSnapshot) @@ -89,9 +94,7 @@ describe("executable machine readiness", () => { expect(Machine.plan).type.not.toBeCallableWith(historyIncomplete, historySnapshot, new Tick({})) expect(Machine.start).type.not.toBeCallableWith(historyIncomplete) expect(Machine.resume).type.not.toBeCallableWith(historyIncomplete, historySnapshot) - expect(Machine.invoke).type.not.toBeCallableWith({ - child: Machine.child("history", historyIncomplete) - }) + expect(from.child).type.not.toBeCallableWith(Machine.child("history", historyIncomplete)) expect(MachineTest.run).type.not.toBeCallableWith(historyIncomplete, { events: [] }) expect(AtomMachine.make).type.not.toBeCallableWith(historyIncomplete) expect(AtomMachine.resume).type.not.toBeCallableWith(historyIncomplete, historySnapshot) @@ -105,10 +108,7 @@ describe("executable machine readiness", () => { expect(Machine.plan).type.not.toBeCallableWith(outputIncomplete, outputSnapshot, new Tick({})) expect(Machine.start).type.not.toBeCallableWith(outputIncomplete) expect(Machine.resume).type.not.toBeCallableWith(outputIncomplete, outputSnapshot) - expect(Machine.invoke).type.not.toBeCallableWith({ - child: Machine.child("output", outputIncomplete), - onDone: () => undefined - }) + expect(from.child).type.not.toBeCallableWith(Machine.child("output", outputIncomplete)) expect(MachineTest.run).type.not.toBeCallableWith(outputIncomplete, { events: [] }) expect(AtomMachine.make).type.not.toBeCallableWith(outputIncomplete) expect(AtomMachine.resume).type.not.toBeCallableWith(outputIncomplete, outputSnapshot) diff --git a/typetest/testing/MachineTest.tst.ts b/typetest/testing/MachineTest.tst.ts index 407b9fb..c69fe14 100644 --- a/typetest/testing/MachineTest.tst.ts +++ b/typetest/testing/MachineTest.tst.ts @@ -100,14 +100,11 @@ describe("MachineTest", () => { initial: (to) => to.idle().resolve(({ target }) => (target(new Idle({})))) }).handle({ idle: { - invoke: Machine.invoke({ - id: "service-backed-invoke", - effect: () => + invoke: (from) => + from.effect("service-backed-invoke", () => Effect.gen(function*() { yield* InvokeRequirement - }), - onDone: (to) => to.none - }) + })).onDone((to) => to.none) } }) diff --git a/typetest/unstable/reactivity/AtomMachine.tst.ts b/typetest/unstable/reactivity/AtomMachine.tst.ts index d4b3ff0..c9bf48e 100644 --- a/typetest/unstable/reactivity/AtomMachine.tst.ts +++ b/typetest/unstable/reactivity/AtomMachine.tst.ts @@ -138,7 +138,7 @@ describe("AtomMachine", () => { initial: (to) => to.Idle().resolve(({ target }) => (target(new Idle({})))) }).handle({ Idle: { - invoke: Machine.invoke({ child: Child }) + invoke: (from) => from.child(Child) } }) const child = AtomMachine.make(parentMachine).child(Child) @@ -182,7 +182,7 @@ describe("AtomMachine", () => { initial: (to) => to.Idle().resolve(({ target }) => target(new Idle({}))) }).handle({ Idle: { - invoke: Machine.invoke({ child: Child }) + invoke: (from) => from.child(Child) } }) const parent = AtomMachine.make(parentMachine)