Skip to content

Commit

Permalink
add action resolution capabilities to machine.executeAction
Browse files Browse the repository at this point in the history
  • Loading branch information
Andarist committed Oct 26, 2024
1 parent ca2977f commit c50d849
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 78 deletions.
65 changes: 62 additions & 3 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,7 +12,6 @@ import {
} from './State.ts';
import { StateNode } from './StateNode.ts';
import {
executeAction,
getAllStateNodes,
getInitialStateNodes,
getStateNodeByPath,
Expand All @@ -19,6 +21,7 @@ import {
macrostep,
microstep,
resolveActionsAndContext,
resolveReferencedAction,
resolveStateValue,
transitionNode
} from './stateUtils.ts';
Expand Down Expand Up @@ -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) {

Check failure on line 675 in packages/core/src/StateMachine.ts

View workflow job for this annotation

GitHub Actions / build

Property 'exec' does not exist on type 'ExecutableActionObject'.
action.exec?.(

Check failure on line 676 in packages/core/src/StateMachine.ts

View workflow job for this annotation

GitHub Actions / build

Property 'exec' does not exist on type 'ExecutableActionObject'.
{
...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;
}
}
}
83 changes: 12 additions & 71 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,10 @@ import {
TODO,
UnknownAction,
ParameterizedObject,
ActionFunction,
AnyTransitionConfig,
ProvidedActor,
AnyActorScope,
AnyActorRef,
ActionExecutor,
ExecutableActionObject
AnyStateMachine
} from './types.ts';
import {
resolveOutput,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
}
3 changes: 0 additions & 3 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2612,9 +2612,6 @@ export interface ExecutableActionObject {
type: string;
info: ActionArgs<MachineContext, EventObject, EventObject>;
params: NonReducibleUnknown;
exec:
| ((info: ActionArgs<any, any, any>, params: unknown) => void)
| undefined;
}

export interface ToExecutableAction<T extends ParameterizedObject>
Expand Down
28 changes: 27 additions & 1 deletion packages/core/test/transition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -267,6 +292,7 @@ describe('transition function', () => {

expect(s2.value).toEqual('c');
});

it('should not execute entry actions', () => {
const fn = jest.fn();

Expand Down

0 comments on commit c50d849

Please sign in to comment.