From c50d849ad032e2665ef0ba641602bd30873c6127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 26 Oct 2024 10:56:34 +0200 Subject: [PATCH] add action resolution capabilities to machine.executeAction --- packages/core/src/StateMachine.ts | 65 ++++++++++++++++++++- packages/core/src/stateUtils.ts | 83 ++++----------------------- packages/core/src/types.ts | 3 - packages/core/test/transition.test.ts | 28 ++++++++- 4 files changed, 101 insertions(+), 78 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 2cfa601666..2cf4c4dbd3 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -1,5 +1,8 @@ import isDevelopment from '#is-development'; import { assign } from './actions.ts'; +import { executeRaise } from './actions/raise.ts'; +import { executeSendTo } from './actions/send.ts'; +import { createEmptyActor } from './actors/index.ts'; import { $$ACTOR_TYPE, createActor } from './createActor.ts'; import { createInitEvent } from './eventUtils.ts'; import { @@ -9,7 +12,6 @@ import { } from './State.ts'; import { StateNode } from './StateNode.ts'; import { - executeAction, getAllStateNodes, getInitialStateNodes, getStateNodeByPath, @@ -19,6 +21,7 @@ import { macrostep, microstep, resolveActionsAndContext, + resolveReferencedAction, resolveStateValue, transitionNode } from './stateUtils.ts'; @@ -634,7 +637,63 @@ export class StateMachine< return restoredSnapshot; } - public executeAction(action: ExecutableActionObject, actor?: AnyActorRef) { - return executeAction(action, actor); + /** + * Runs an executable action. Executable actions are returned from the + * `transition(…)` function. + * + * @example + * + * ```ts + * const [state, actions] = transition(someMachine, someState, someEvent); + * + * for (const action of actions) { + * // Executes the action + * someMachine.executeAction(action); + * } + * ``` + */ + public executeAction( + action: ExecutableActionObject, + actor: AnyActorRef = createEmptyActor() + ) { + const actorScope = (actor as any)._actorScope as AnyActorScope; + const defer = actorScope.defer; + actorScope.defer = (fn) => fn(); + try { + switch (action.type) { + case 'xstate.raise': + if (typeof (action as any).params.delay !== 'number') { + return; + } + executeRaise(actorScope, action.params as any); + return; + case 'xstate.sendTo': + executeSendTo(actorScope, action.params as any); + return; + default: + } + if (action.exec) { + action.exec?.( + { + ...action.info, + self: actor, + system: actor.system + }, + action.params + ); + } else { + const resolvedAction = resolveReferencedAction(this, action.type)!; + resolvedAction( + { + ...action.info, + self: actor, + system: actor.system + }, + action.params + ); + } + } finally { + actorScope.defer = defer; + } } } diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 651b9aaaf5..eb712ca0f8 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -34,13 +34,10 @@ import { TODO, UnknownAction, ParameterizedObject, - ActionFunction, AnyTransitionConfig, - ProvidedActor, AnyActorScope, - AnyActorRef, ActionExecutor, - ExecutableActionObject + AnyStateMachine } from './types.ts'; import { resolveOutput, @@ -50,9 +47,6 @@ import { toTransitionConfigArray, isErrorActorEvent } from './utils.ts'; -import { createEmptyActor } from './actors/index.ts'; -import { executeRaise } from './actions/raise.ts'; -import { executeSendTo } from './actions/send.ts'; type StateNodeIterable< TContext extends MachineContext, @@ -1492,6 +1486,13 @@ export interface BuiltinAction { execute: (actorScope: AnyActorScope, params: unknown) => void; } +export function resolveReferencedAction( + machine: AnyStateMachine, + actionType: string +) { + return machine.implementations.actions[actionType]; +} + function resolveAndExecuteActionsWithContext( currentSnapshot: AnyMachineSnapshot, event: AnyEventObject, @@ -1513,23 +1514,11 @@ function resolveAndExecuteActionsWithContext( : // the existing type of `.actions` assumes non-nullable `TExpressionAction` // it's fine to cast this here to get a common type and lack of errors in the rest of the code // our logic below makes sure that we call those 2 "variants" correctly - ( - machine.implementations.actions as Record< - string, - ActionFunction< - MachineContext, - EventObject, - EventObject, - ParameterizedObject['params'] | undefined, - ProvidedActor, - ParameterizedObject, - ParameterizedObject, - string, - EventObject - > - > - )[typeof action === 'string' ? action : action.type]; + resolveReferencedAction( + machine, + typeof action === 'string' ? action : action.type + ); const actionArgs = { context: intermediateSnapshot.context, event, @@ -1817,51 +1806,3 @@ export function resolveStateValue( const allStateNodes = getAllStateNodes(getStateNodes(rootNode, stateValue)); return getStateValue(rootNode, [...allStateNodes]); } - -/** - * Runs an executable action. Executable actions are returned from the - * `transition(…)` function. - * - * @example - * - * ```ts - * const [state, actions] = transition(someMachine, someState, someEvent); - * - * for (const action of actions) { - * // Executes the action - * executeAction(action); - * } - * ``` - */ -export function executeAction( - action: ExecutableActionObject, - actor: AnyActorRef = createEmptyActor() -) { - const actorScope = (actor as any)._actorScope as AnyActorScope; - const defer = actorScope.defer; - actorScope.defer = (fn) => fn(); - try { - switch (action.type) { - case 'xstate.raise': - if (typeof (action as any).params.delay !== 'number') { - return; - } - executeRaise(actorScope, action.params as any); - return; - case 'xstate.sendTo': - executeSendTo(actorScope, action.params as any); - return; - } - - action.exec?.( - { - ...action.info, - self: actor, - system: actor.system - }, - action.params - ); - } finally { - actorScope.defer = defer; - } -} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f7433276d8..6ec27da8f3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2612,9 +2612,6 @@ export interface ExecutableActionObject { type: string; info: ActionArgs; params: NonReducibleUnknown; - exec: - | ((info: ActionArgs, params: unknown) => void) - | undefined; } export interface ToExecutableAction diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 1790078b93..522b7ace68 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -97,6 +97,31 @@ describe('transition function', () => { }); }); + it('should be able to execute a referenced serialized action', () => { + const foo = jest.fn(); + + const machine = setup({ + actions: { + foo + } + }).createMachine({ + entry: 'foo', + context: { count: 0 } + }); + + const [, actions] = initialTransition(machine); + + expect(foo).not.toHaveBeenCalled(); + + actions + .map((a) => JSON.stringify(a)) + .forEach((a) => machine.executeAction(JSON.parse(a))); + + expect(foo).toHaveBeenCalledTimes(1); + expect(foo.mock.calls[0][0].context).toEqual({ count: 0 }); + expect(foo.mock.calls[0][0].event).toEqual({ type: 'xstate.init' }); + }); + it('should capture enqueued actions', () => { const machine = createMachine({ entry: [ @@ -144,7 +169,7 @@ describe('transition function', () => { expect(actor.getSnapshot().matches('b')).toBeTruthy(); }); - it('Delayed raise actions should be returned', async () => { + it('delayed raise actions should be returned', async () => { const machine = createMachine({ initial: 'a', states: { @@ -267,6 +292,7 @@ describe('transition function', () => { expect(s2.value).toEqual('c'); }); + it('should not execute entry actions', () => { const fn = jest.fn();