From 9f6f015d79e21ba6f1f7b17aeedd6044304e5bab Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 4 Jun 2024 08:07:27 -0400 Subject: [PATCH 01/80] Use defaultActionExecutor (temp) --- packages/core/src/StateMachine.ts | 17 ++++-- packages/core/src/stateUtils.ts | 89 +++++++++++++++++++++++-------- yarn.lock | 12 ----- 3 files changed, 79 insertions(+), 39 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index d204aff983..d3c385cca6 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -9,6 +9,7 @@ import { } from './State.ts'; import { StateNode } from './StateNode.ts'; import { + defaultActionExecutor, getAllStateNodes, getInitialStateNodes, getStateNodeByPath, @@ -294,7 +295,8 @@ export class StateMachine< TOutput, TMeta > { - return macrostep(snapshot, event, actorScope).snapshot as typeof snapshot; + return macrostep(snapshot, event, actorScope, [], defaultActionExecutor) + .snapshot as typeof snapshot; } /** @@ -327,7 +329,8 @@ export class StateMachine< TMeta > > { - return macrostep(snapshot, event, actorScope).microstates; + return macrostep(snapshot, event, actorScope, [], defaultActionExecutor) + .microstates; } public getTransitionData( @@ -383,7 +386,9 @@ export class StateMachine< initEvent, actorScope, [assign(assignment)], - internalQueue + internalQueue, + undefined, + defaultActionExecutor ) as SnapshotFrom; } @@ -440,14 +445,16 @@ export class StateMachine< actorScope, initEvent, true, - internalQueue + internalQueue, + defaultActionExecutor ); const { snapshot: macroState } = macrostep( nextState, initEvent as AnyEventObject, actorScope, - internalQueue + internalQueue, + defaultActionExecutor ); return macroState as SnapshotFrom; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 0b3052c939..16b684349e 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -38,7 +38,8 @@ import { ActionFunction, AnyTransitionConfig, ProvidedActor, - AnyActorScope + AnyActorScope, + NonReducibleUnknown } from './types.ts'; import { resolveOutput, @@ -50,6 +51,10 @@ import { } from './utils.ts'; import { ProcessingStatus } from './createActor.ts'; +export const defaultActionExecutor: ActionExecutor = (actionToExecute) => { + actionToExecute.execute(actionToExecute.stuff, actionToExecute.params); +}; + type StateNodeIterable< TContext extends MachineContext, TE extends EventObject @@ -998,7 +1003,8 @@ export function microstep< actorScope: AnyActorScope, event: AnyEventObject, isInitial: boolean, - internalQueue: Array + internalQueue: Array, + actionExecutor: ActionExecutor ): AnyMachineSnapshot { if (!transitions.length) { return currentSnapshot; @@ -1023,7 +1029,8 @@ export function microstep< filteredTransitions, mutStateNodeSet, historyValue, - internalQueue + internalQueue, + actionExecutor ); } @@ -1033,7 +1040,9 @@ export function microstep< event, actorScope, filteredTransitions.flatMap((t) => t.actions), - internalQueue + internalQueue, + undefined, + actionExecutor ); // Enter states @@ -1045,7 +1054,8 @@ export function microstep< mutStateNodeSet, internalQueue, historyValue, - isInitial + isInitial, + actionExecutor ); const nextStateNodes = [...mutStateNodeSet]; @@ -1058,7 +1068,9 @@ export function microstep< nextStateNodes .sort((a, b) => b.order - a.order) .flatMap((state) => state.exit), - internalQueue + internalQueue, + undefined, + actionExecutor ); } @@ -1117,7 +1129,8 @@ function enterStates( mutStateNodeSet: Set, internalQueue: AnyEventObject[], historyValue: HistoryValue, - isInitial: boolean + isInitial: boolean, + actionExecutor: ActionExecutor ) { let nextSnapshot = currentSnapshot; const statesToEnter = new Set(); @@ -1168,7 +1181,8 @@ function enterStates( actorScope, actions, internalQueue, - stateNodeToEnter.invoke.map((invokeDef) => invokeDef.id) + stateNodeToEnter.invoke.map((invokeDef) => invokeDef.id), + actionExecutor ); if (stateNodeToEnter.type === 'final') { @@ -1425,7 +1439,8 @@ function exitStates( transitions: AnyTransitionDefinition[], mutStateNodeSet: Set, historyValue: HistoryValue, - internalQueue: AnyEventObject[] + internalQueue: AnyEventObject[], + actionExecutor: ActionExecutor ) { let nextSnapshot = currentSnapshot; const statesToExit = computeExitSet( @@ -1462,7 +1477,9 @@ function exitStates( event, actorScope, [...s.exit, ...s.invoke.map((def) => stopChild(def.id))], - internalQueue + internalQueue, + undefined, + actionExecutor ); mutStateNodeSet.delete(s); } @@ -1495,6 +1512,18 @@ export let executingCustomAction: | ActionFunction | false = false; +interface ActionToExecute { + actionType: string; + stuff: ActionArgs; + params: NonReducibleUnknown; + execute: ( + stuff: ActionArgs, + params: NonReducibleUnknown + ) => void; +} + +type ActionExecutor = (actionToExecute: ActionToExecute) => void; + function resolveAndExecuteActionsWithContext( currentSnapshot: AnyMachineSnapshot, event: AnyEventObject, @@ -1504,7 +1533,8 @@ function resolveAndExecuteActionsWithContext( internalQueue: AnyEventObject[]; deferredActorIds: string[] | undefined; }, - retries: (readonly [BuiltinAction, unknown])[] | undefined + retries: (readonly [BuiltinAction, unknown])[] | undefined, + actionExecutor: ActionExecutor ): AnyMachineSnapshot { const { machine } = currentSnapshot; let intermediateSnapshot = currentSnapshot; @@ -1569,7 +1599,13 @@ function resolveAndExecuteActionsWithContext( }); try { executingCustomAction = resolvedAction; - resolvedAction(actionArgs, actionParams); + actionExecutor({ + actionType: typeof action === 'object' ? action.type : 'TODO', + stuff: actionArgs, + params: actionParams, + execute: resolvedAction + }); + // resolvedAction(actionArgs, actionParams); } finally { executingCustomAction = false; } @@ -1617,7 +1653,8 @@ function resolveAndExecuteActionsWithContext( actorScope, actions, extra, - retries + retries, + actionExecutor ); } } @@ -1631,7 +1668,8 @@ export function resolveActionsAndContext( actorScope: AnyActorScope, actions: UnknownAction[], internalQueue: AnyEventObject[], - deferredActorIds?: string[] + deferredActorIds: string[] | undefined, + actionExecutor: ActionExecutor ): AnyMachineSnapshot { const retries: (readonly [BuiltinAction, unknown])[] | undefined = deferredActorIds ? [] : undefined; @@ -1641,7 +1679,8 @@ export function resolveActionsAndContext( actorScope, actions, { internalQueue, deferredActorIds }, - retries + retries, + actionExecutor ); retries?.forEach(([builtinAction, params]) => { builtinAction.retryResolve(actorScope, nextState, params); @@ -1653,7 +1692,8 @@ export function macrostep( snapshot: AnyMachineSnapshot, event: EventObject, actorScope: AnyActorScope, - internalQueue: AnyEventObject[] = [] + internalQueue: AnyEventObject[], + actionExecutor: ActionExecutor ): { snapshot: typeof snapshot; microstates: Array; @@ -1683,7 +1723,7 @@ export function macrostep( // Handle stop event if (event.type === XSTATE_STOP) { nextSnapshot = cloneMachineSnapshot( - stopChildren(nextSnapshot, event, actorScope), + stopChildren(nextSnapshot, event, actorScope, actionExecutor), { status: 'stopped' } @@ -1726,7 +1766,8 @@ export function macrostep( actorScope, nextEvent, false, // isInitial - internalQueue + internalQueue, + actionExecutor ); addMicrostate(nextSnapshot, currentEvent, transitions); } @@ -1757,14 +1798,15 @@ export function macrostep( actorScope, nextEvent, false, - internalQueue + internalQueue, + actionExecutor ); shouldSelectEventlessTransitions = nextSnapshot !== previousState; addMicrostate(nextSnapshot, nextEvent, enabledTransitions); } if (nextSnapshot.status !== 'active') { - stopChildren(nextSnapshot, nextEvent, actorScope); + stopChildren(nextSnapshot, nextEvent, actorScope, actionExecutor); } return { @@ -1776,14 +1818,17 @@ export function macrostep( function stopChildren( nextState: AnyMachineSnapshot, event: AnyEventObject, - actorScope: AnyActorScope + actorScope: AnyActorScope, + actionExecutor: ActionExecutor ) { return resolveActionsAndContext( nextState, event, actorScope, Object.values(nextState.children).map((child: any) => stopChild(child)), - [] + [], + undefined, + actionExecutor ); } diff --git a/yarn.lock b/yarn.lock index 0385aada7b..ad9c8efefd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2484,11 +2484,6 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -9031,13 +9026,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" From abfe3580c32cdce69c082a5e60cbcac9c888d1cc Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 4 Jun 2024 09:03:05 -0400 Subject: [PATCH 02/80] Convert entry/exit events --- packages/core/src/StateNode.ts | 31 +++++++++++++++++++++++++----- packages/core/src/types.ts | 18 ++++++++++++++++- packages/core/test/actions.test.ts | 4 +++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index b001022f6a..485dd6ecec 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -30,7 +30,8 @@ import type { AnyStateNodeConfig, ProvidedActor, NonReducibleUnknown, - EventDescriptor + EventDescriptor, + UnknownActionObject } from './types.ts'; import { createInvokeId, @@ -105,11 +106,11 @@ export class StateNode< /** * The action(s) to be executed upon entering the state node. */ - public entry: UnknownAction[]; + public entry: UnknownActionObject[]; /** * The action(s) to be executed upon exiting the state node. */ - public exit: UnknownAction[]; + public exit: UnknownActionObject[]; /** * The parent state node. */ @@ -220,8 +221,28 @@ export class StateNode< this.history = this.config.history === true ? 'shallow' : this.config.history || false; - this.entry = toArray(this.config.entry).slice(); - this.exit = toArray(this.config.exit).slice(); + const convertAction = ( + action: UnknownAction, + kind: 'entry' | 'exit', + i: number + ): UnknownActionObject => { + if (typeof action === 'string') { + return { type: action }; + } + if (typeof action === 'function' && !('resolve' in action)) { + const type = `${this.id}|${kind}:${i}`; + this.machine.implementations.actions[type] = action as any; + return { type }; + } + return action as any; + }; + + this.entry = toArray(this.config.entry as UnknownAction).map((a, i) => + convertAction(a, 'entry', i) + ); + this.exit = toArray(this.config.exit as UnknownAction).map((a, i) => + convertAction(a, 'exit', i) + ); this.meta = this.config.meta; this.output = diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8675e56706..c4b036cd60 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -253,6 +253,22 @@ export type Action< TEmitted >; +export type ActionObject< + TContext extends MachineContext, + TExpressionEvent extends EventObject, + TAction extends ParameterizedObject +> = + // TODO: consider merging `NoRequiredParams` and `WithDynamicParams` into one + // this way we could iterate over `TAction` (and `TGuard` in the `Guard` type) once and not twice + | NoRequiredParams + | WithDynamicParams; + +export type UnknownActionObject = ActionObject< + MachineContext, + EventObject, + ParameterizedObject +>; + export type UnknownAction = Action< MachineContext, EventObject, @@ -1854,7 +1870,7 @@ export interface TransitionDefinition< > { target: ReadonlyArray> | undefined; source: StateNode; - actions: readonly UnknownAction[]; + actions: readonly UnknownActionObject[]; reenter: boolean; guard?: UnknownGuard; eventType: EventDescriptor; diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index c1caacbe3c..30ab9ac643 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3482,7 +3482,9 @@ describe('assign action order', () => { ] }); - createActor(machine).start(); + const actor = createActor(machine).start(); + + expect(actor.getSnapshot().context).toEqual({ count: 2 }); expect(captured).toEqual([0, 1, 2]); }); From 6e181ead5c2d884687ecf021e6d5c689d1ea6d06 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 4 Jun 2024 13:43:06 -0400 Subject: [PATCH 03/80] UnknownAction -> UnknownActionObject --- packages/core/src/StateMachine.ts | 5 +- packages/core/src/StateNode.ts | 31 +++------- packages/core/src/stateUtils.ts | 92 +++++++++++++++++++----------- packages/core/src/types.ts | 15 ++--- packages/core/test/inspect.test.ts | 80 +++++++++++++------------- packages/core/test/utils.ts | 14 +++-- 6 files changed, 128 insertions(+), 109 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index d3c385cca6..4896035c5f 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -47,7 +47,8 @@ import type { SnapshotFrom, StateMachineDefinition, StateValue, - TransitionDefinition + TransitionDefinition, + UnknownActionObject } from './types.ts'; import { resolveReferencedActor, toStatePath } from './utils.ts'; @@ -385,7 +386,7 @@ export class StateMachine< preInitial, initEvent, actorScope, - [assign(assignment)], + [assign(assignment) as unknown as UnknownActionObject], internalQueue, undefined, defaultActionExecutor diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 485dd6ecec..1a39769fbf 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -42,21 +42,6 @@ import { const EMPTY_OBJECT = {}; -const toSerializableAction = (action: UnknownAction) => { - if (typeof action === 'string') { - return { type: action }; - } - if (typeof action === 'function') { - if ('resolve' in action) { - return { type: (action as any).type }; - } - return { - type: action.name - }; - } - return action; -}; - interface StateNodeOptions< TContext extends MachineContext, TEvent extends EventObject @@ -254,8 +239,8 @@ export class StateNode< public _initialize() { this.transitions = formatTransitions(this); if (this.config.always) { - this.always = toTransitionConfigArray(this.config.always).map((t) => - formatTransition(this, NULL_EVENT, t) + this.always = toTransitionConfigArray(this.config.always).map((t, i) => + formatTransition(this, NULL_EVENT, t, i) ); } @@ -277,13 +262,13 @@ export class StateNode< ? { target: this.initial.target, source: this, - actions: this.initial.actions.map(toSerializableAction), + actions: this.initial.actions, eventType: null as any, reenter: false, toJSON: () => ({ target: this.initial!.target!.map((t) => `#${t.id}`), source: `#${this.id}`, - actions: this.initial!.actions.map(toSerializableAction), + actions: this.initial!.actions, eventType: null as any }) } @@ -295,10 +280,10 @@ export class StateNode< on: this.on, transitions: [...this.transitions.values()].flat().map((t) => ({ ...t, - actions: t.actions.map(toSerializableAction) + actions: t.actions })), - entry: this.entry.map(toSerializableAction), - exit: this.exit.map(toSerializableAction), + entry: this.entry, + exit: this.exit, meta: this.meta, order: this.order || -1, output: this.output, @@ -412,7 +397,7 @@ export class StateNode< event: TEvent ): TransitionDefinition[] | undefined { const eventType = event.type; - const actions: UnknownAction[] = []; + const actions: UnknownActionObject[] = []; let selectedTransition: TransitionDefinition | undefined; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 16b684349e..8244e1da1b 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -22,7 +22,6 @@ import { AnyMachineSnapshot, AnyStateNode, AnyTransitionDefinition, - DelayExpr, DelayedTransitionDefinition, EventObject, HistoryValue, @@ -39,7 +38,8 @@ import { AnyTransitionConfig, ProvidedActor, AnyActorScope, - NonReducibleUnknown + NonReducibleUnknown, + UnknownActionObject } from './types.ts'; import { resolveOutput, @@ -285,11 +285,17 @@ export function getDelayedTransitions( return []; } - const mutateEntryExit = (delay: string | number, i: number) => { + const mutateEntryExit = (delay: string | number) => { const afterEvent = createAfterEvent(delay, stateNode.id); const eventType = afterEvent.type; - stateNode.entry.push(raise(afterEvent, { id: eventType, delay })); - stateNode.exit.push(cancel(eventType)); + + stateNode.entry.push( + raise(afterEvent, { + id: eventType, + delay: delay as any // TODO: fix types + }) as unknown as UnknownActionObject + ); + stateNode.exit.push(cancel(eventType) as unknown as UnknownActionObject); return eventType; }; @@ -300,20 +306,21 @@ export function getDelayedTransitions( ? { target: configTransition } : configTransition; const resolvedDelay = Number.isNaN(+delay) ? delay : +delay; - const eventType = mutateEntryExit(resolvedDelay, i); + const eventType = mutateEntryExit(resolvedDelay); return toArray(resolvedTransition).map((transition) => ({ ...transition, event: eventType, delay: resolvedDelay })); }); - return delayedTransitions.map((delayedTransition) => { + return delayedTransitions.map((delayedTransition, i) => { const { delay } = delayedTransition; return { ...formatTransition( stateNode, delayedTransition.event, - delayedTransition + delayedTransition, + i ), delay }; @@ -326,7 +333,8 @@ export function formatTransition< >( stateNode: AnyStateNode, descriptor: string, - transitionConfig: AnyTransitionConfig + transitionConfig: AnyTransitionConfig, + index: number ): AnyTransitionDefinition { const normalizedTarget = normalizeTarget(transitionConfig.target); const reenter = transitionConfig.reenter ?? false; @@ -338,9 +346,25 @@ export function formatTransition< `State "${stateNode.id}" has declared \`cond\` for one of its transitions. This property has been renamed to \`guard\`. Please update your code.` ); } + + const convertAction = ( + action: UnknownAction, + i: number + ): UnknownActionObject => { + if (typeof action === 'string') { + return { type: action }; + } + if (typeof action === 'function' && !('resolve' in action)) { + const type = `${stateNode.id}|${descriptor}:${index}:${i}`; + stateNode.machine.implementations.actions[type] = action as any; + return { type }; + } + return action as any; + }; + const transition = { ...transitionConfig, - actions: toArray(transitionConfig.actions), + actions: toArray(transitionConfig.actions).map(convertAction), guard: transitionConfig.guard as never, target, source: stateNode, @@ -376,8 +400,8 @@ export function formatTransitions< const transitionsConfig = stateNode.config.on[descriptor]; transitions.set( descriptor, - toTransitionConfigArray(transitionsConfig).map((t) => - formatTransition(stateNode, descriptor, t) + toTransitionConfigArray(transitionsConfig).map((t, i) => + formatTransition(stateNode, descriptor, t, i) ) ); } @@ -386,8 +410,8 @@ export function formatTransitions< const descriptor = `xstate.done.state.${stateNode.id}`; transitions.set( descriptor, - toTransitionConfigArray(stateNode.config.onDone).map((t) => - formatTransition(stateNode, descriptor, t) + toTransitionConfigArray(stateNode.config.onDone).map((t, i) => + formatTransition(stateNode, descriptor, t, i) ) ); } @@ -396,8 +420,8 @@ export function formatTransitions< const descriptor = `xstate.done.actor.${invokeDef.id}`; transitions.set( descriptor, - toTransitionConfigArray(invokeDef.onDone).map((t) => - formatTransition(stateNode, descriptor, t) + toTransitionConfigArray(invokeDef.onDone).map((t, i) => + formatTransition(stateNode, descriptor, t, i) ) ); } @@ -405,8 +429,8 @@ export function formatTransitions< const descriptor = `xstate.error.actor.${invokeDef.id}`; transitions.set( descriptor, - toTransitionConfigArray(invokeDef.onError).map((t) => - formatTransition(stateNode, descriptor, t) + toTransitionConfigArray(invokeDef.onError).map((t, i) => + formatTransition(stateNode, descriptor, t, i) ) ); } @@ -414,8 +438,8 @@ export function formatTransitions< const descriptor = `xstate.snapshot.${invokeDef.id}`; transitions.set( descriptor, - toTransitionConfigArray(invokeDef.onSnapshot).map((t) => - formatTransition(stateNode, descriptor, t) + toTransitionConfigArray(invokeDef.onSnapshot).map((t, i) => + formatTransition(stateNode, descriptor, t, i) ) ); } @@ -1156,7 +1180,7 @@ function enterStates( (a, b) => a.order - b.order )) { mutStateNodeSet.add(stateNodeToEnter); - const actions: UnknownAction[] = []; + const actions: UnknownActionObject[] = []; // Add entry actions actions.push(...stateNodeToEnter.entry); @@ -1166,7 +1190,7 @@ function enterStates( spawnChild(invokeDef.src, { ...invokeDef, syncSnapshot: !!invokeDef.onSnapshot - }) + }) as unknown as UnknownActionObject ); } @@ -1476,7 +1500,12 @@ function exitStates( nextSnapshot, event, actorScope, - [...s.exit, ...s.invoke.map((def) => stopChild(def.id))], + [ + ...s.exit, + ...(s.invoke.map((def) => + stopChild(def.id) + ) as unknown as UnknownActionObject[]) + ], internalQueue, undefined, actionExecutor @@ -1498,7 +1527,7 @@ interface BuiltinAction { ) => [ newState: AnyMachineSnapshot, params: unknown, - actions?: UnknownAction[] + actions?: UnknownActionObject[] ]; retryResolve: ( actorScope: AnyActorScope, @@ -1528,7 +1557,7 @@ function resolveAndExecuteActionsWithContext( currentSnapshot: AnyMachineSnapshot, event: AnyEventObject, actorScope: AnyActorScope, - actions: UnknownAction[], + actions: UnknownActionObject[], extra: { internalQueue: AnyEventObject[]; deferredActorIds: string[] | undefined; @@ -1588,12 +1617,7 @@ function resolveAndExecuteActionsWithContext( type: '@xstate.action', actorRef: actorScope.self, action: { - type: - typeof action === 'string' - ? action - : typeof action === 'object' - ? action.type - : action.name || '(anonymous)', + type: action.type, params: actionParams } }); @@ -1666,7 +1690,7 @@ export function resolveActionsAndContext( currentSnapshot: AnyMachineSnapshot, event: AnyEventObject, actorScope: AnyActorScope, - actions: UnknownAction[], + actions: UnknownActionObject[], internalQueue: AnyEventObject[], deferredActorIds: string[] | undefined, actionExecutor: ActionExecutor @@ -1825,7 +1849,9 @@ function stopChildren( nextState, event, actorScope, - Object.values(nextState.children).map((child: any) => stopChild(child)), + Object.values(nextState.children).map( + (child: any) => stopChild(child) as unknown as UnknownActionObject + ), [], undefined, actionExecutor diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c4b036cd60..102b5b2483 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -257,11 +257,12 @@ export type ActionObject< TContext extends MachineContext, TExpressionEvent extends EventObject, TAction extends ParameterizedObject -> = - // TODO: consider merging `NoRequiredParams` and `WithDynamicParams` into one - // this way we could iterate over `TAction` (and `TGuard` in the `Guard` type) once and not twice +> = { + type: string; +} & ( // this way we could iterate over `TAction` (and `TGuard` in the `Guard` type) once and not twice // TODO: consider merging `NoRequiredParams` and `WithDynamicParams` into one | NoRequiredParams - | WithDynamicParams; + | WithDynamicParams +); export type UnknownActionObject = ActionObject< MachineContext, @@ -1082,8 +1083,8 @@ export interface StateNodeDefinition< on: TransitionDefinitionMap; transitions: Array>; // TODO: establish what a definition really is - entry: UnknownAction[]; - exit: UnknownAction[]; + entry: UnknownActionObject[]; + exit: UnknownActionObject[]; meta: any; order: number; output?: StateNodeConfig< @@ -1877,7 +1878,7 @@ export interface TransitionDefinition< toJSON: () => { target: string[] | undefined; source: string; - actions: readonly UnknownAction[]; + actions: readonly UnknownActionObject[]; guard?: UnknownGuard; eventType: EventDescriptor; meta?: Record; diff --git a/packages/core/test/inspect.test.ts b/packages/core/test/inspect.test.ts index 96702f23a5..0e6e2b9ec3 100644 --- a/packages/core/test/inspect.test.ts +++ b/packages/core/test/inspect.test.ts @@ -975,46 +975,46 @@ describe('inspect', () => { expect(simplifyEvents(events, (ev) => ev.type === '@xstate.action')) .toMatchInlineSnapshot(` - [ - { - "action": { - "params": undefined, - "type": "enter1", - }, - "type": "@xstate.action", - }, - { - "action": { - "params": undefined, - "type": "stringAction", - }, - "type": "@xstate.action", - }, - { - "action": { - "params": { - "foo": "bar", - }, - "type": "namedAction", - }, - "type": "@xstate.action", - }, - { - "action": { - "params": undefined, - "type": "(anonymous)", - }, - "type": "@xstate.action", - }, - { - "action": { - "params": undefined, - "type": "exit1", - }, - "type": "@xstate.action", - }, - ] - `); +[ + { + "action": { + "params": undefined, + "type": "enter1", + }, + "type": "@xstate.action", + }, + { + "action": { + "params": undefined, + "type": "stringAction", + }, + "type": "@xstate.action", + }, + { + "action": { + "params": { + "foo": "bar", + }, + "type": "namedAction", + }, + "type": "@xstate.action", + }, + { + "action": { + "params": undefined, + "type": "(machine).loading|event:0:2", + }, + "type": "@xstate.action", + }, + { + "action": { + "params": undefined, + "type": "exit1", + }, + "type": "@xstate.action", + }, +] +`); }); it('@xstate.microstep inspection events should report no transitions if an unknown event was sent', () => { diff --git a/packages/core/test/utils.ts b/packages/core/test/utils.ts index a97ce6cccd..63bc001402 100644 --- a/packages/core/test/utils.ts +++ b/packages/core/test/utils.ts @@ -81,15 +81,21 @@ export function trackEntries(machine: AnyStateMachine) { let logs: string[] = []; + machine.implementations.actions['__log'] = function __logAction(_, params) { + logs.push((params as any).msg); + }; + function addTrackingActions( state: StateNode, stateDescription: string ) { - state.entry.unshift(function __testEntryTracker() { - logs.push(`enter: ${stateDescription}`); + state.entry.unshift({ + type: '__log', + params: { msg: `enter: ${stateDescription}` } }); - state.exit.unshift(function __testExitTracker() { - logs.push(`exit: ${stateDescription}`); + state.exit.unshift({ + type: '__log', + params: { msg: `exit: ${stateDescription}` } }); } From f1100ab2df6ad13b1122d2df843b526d38ad8078 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 5 Jun 2024 07:09:43 -0400 Subject: [PATCH 04/80] Use defaultActionExecutor --- packages/core/src/StateMachine.ts | 26 ++++++++++++++++---------- packages/core/src/stateUtils.ts | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 4896035c5f..cdd74a4e03 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -9,6 +9,7 @@ import { } from './State.ts'; import { StateNode } from './StateNode.ts'; import { + ActionExecutor, defaultActionExecutor, getAllStateNodes, getInitialStateNodes, @@ -286,7 +287,8 @@ export class StateMachine< TMeta >, event: TEvent, - actorScope: ActorScope + actorScope: ActorScope, + actionExecutor: ActionExecutor = defaultActionExecutor ): MachineSnapshot< TContext, TEvent, @@ -296,7 +298,7 @@ export class StateMachine< TOutput, TMeta > { - return macrostep(snapshot, event, actorScope, [], defaultActionExecutor) + return macrostep(snapshot, event, actorScope, [], actionExecutor) .snapshot as typeof snapshot; } @@ -318,7 +320,8 @@ export class StateMachine< TMeta >, event: TEvent, - actorScope: AnyActorScope + actorScope: AnyActorScope, + actionExecutor: ActionExecutor = defaultActionExecutor ): Array< MachineSnapshot< TContext, @@ -330,7 +333,7 @@ export class StateMachine< TMeta > > { - return macrostep(snapshot, event, actorScope, [], defaultActionExecutor) + return macrostep(snapshot, event, actorScope, [], actionExecutor) .microstates; } @@ -356,7 +359,8 @@ export class StateMachine< private getPreInitialState( actorScope: AnyActorScope, initEvent: any, - internalQueue: AnyEventObject[] + internalQueue: AnyEventObject[], + actionExecutor: ActionExecutor ): MachineSnapshot< TContext, TEvent, @@ -389,7 +393,7 @@ export class StateMachine< [assign(assignment) as unknown as UnknownActionObject], internalQueue, undefined, - defaultActionExecutor + actionExecutor ) as SnapshotFrom; } @@ -414,7 +418,8 @@ export class StateMachine< AnyActorSystem, TEmitted >, - input?: TInput + input?: TInput, + actionExecutor: ActionExecutor = defaultActionExecutor ): MachineSnapshot< TContext, TEvent, @@ -429,7 +434,8 @@ export class StateMachine< const preInitialState = this.getPreInitialState( actorScope, initEvent, - internalQueue + internalQueue, + actionExecutor ); const nextState = microstep( [ @@ -447,7 +453,7 @@ export class StateMachine< initEvent, true, internalQueue, - defaultActionExecutor + actionExecutor ); const { snapshot: macroState } = macrostep( @@ -455,7 +461,7 @@ export class StateMachine< initEvent as AnyEventObject, actorScope, internalQueue, - defaultActionExecutor + actionExecutor ); return macroState as SnapshotFrom; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 8244e1da1b..368923b74b 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1551,7 +1551,7 @@ interface ActionToExecute { ) => void; } -type ActionExecutor = (actionToExecute: ActionToExecute) => void; +export type ActionExecutor = (actionToExecute: ActionToExecute) => void; function resolveAndExecuteActionsWithContext( currentSnapshot: AnyMachineSnapshot, From 8e25fa18d449e7c761e29298b45708290e133380 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 5 Jun 2024 07:28:29 -0400 Subject: [PATCH 05/80] Cleanup --- packages/core/src/StateMachine.ts | 20 +++--- packages/core/src/createActor.ts | 4 +- packages/core/src/getNextSnapshot.ts | 3 +- packages/core/src/stateUtils.ts | 84 +++++++++++-------------- packages/core/src/transition.ts | 20 ++++++ packages/core/src/types.ts | 8 ++- packages/xstate-graph/src/actorScope.ts | 3 +- 7 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 packages/core/src/transition.ts diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index cdd74a4e03..a010776637 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -287,8 +287,7 @@ export class StateMachine< TMeta >, event: TEvent, - actorScope: ActorScope, - actionExecutor: ActionExecutor = defaultActionExecutor + actorScope: ActorScope ): MachineSnapshot< TContext, TEvent, @@ -298,7 +297,7 @@ export class StateMachine< TOutput, TMeta > { - return macrostep(snapshot, event, actorScope, [], actionExecutor) + return macrostep(snapshot, event, actorScope, []) .snapshot as typeof snapshot; } @@ -320,8 +319,7 @@ export class StateMachine< TMeta >, event: TEvent, - actorScope: AnyActorScope, - actionExecutor: ActionExecutor = defaultActionExecutor + actorScope: AnyActorScope ): Array< MachineSnapshot< TContext, @@ -333,8 +331,7 @@ export class StateMachine< TMeta > > { - return macrostep(snapshot, event, actorScope, [], actionExecutor) - .microstates; + return macrostep(snapshot, event, actorScope, []).microstates; } public getTransitionData( @@ -392,8 +389,7 @@ export class StateMachine< actorScope, [assign(assignment) as unknown as UnknownActionObject], internalQueue, - undefined, - actionExecutor + undefined ) as SnapshotFrom; } @@ -452,16 +448,14 @@ export class StateMachine< actorScope, initEvent, true, - internalQueue, - actionExecutor + internalQueue ); const { snapshot: macroState } = macrostep( nextState, initEvent as AnyEventObject, actorScope, - internalQueue, - actionExecutor + internalQueue ); return macroState as SnapshotFrom; diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 5aca1a6263..e81effeac1 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -8,6 +8,7 @@ import { createInitEvent } from './eventUtils.ts'; import { reportUnhandledError } from './reportUnhandledError.ts'; +import { defaultActionExecutor } from './stateUtils.ts'; import { symbolObservable } from './symbolObservable.ts'; import { AnyActorSystem, Clock, createSystem } from './system.ts'; @@ -200,7 +201,8 @@ export class Actor for (const handler of Array.from(allListeners)) { handler(emittedEvent); } - } + }, + actionExecutor: defaultActionExecutor }; // Ensure that the send method is bound to this Actor instance diff --git a/packages/core/src/getNextSnapshot.ts b/packages/core/src/getNextSnapshot.ts index 611fcbaff0..855f42ad4c 100644 --- a/packages/core/src/getNextSnapshot.ts +++ b/packages/core/src/getNextSnapshot.ts @@ -27,7 +27,8 @@ export function createInertActorScope( sessionId: '', stopChild: () => {}, system: self.system, - emit: () => {} + emit: () => {}, + actionExecutor: () => {} }; return inertActorScope; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 368923b74b..cd243f3788 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -52,7 +52,7 @@ import { import { ProcessingStatus } from './createActor.ts'; export const defaultActionExecutor: ActionExecutor = (actionToExecute) => { - actionToExecute.execute(actionToExecute.stuff, actionToExecute.params); + actionToExecute.execute(); }; type StateNodeIterable< @@ -1027,8 +1027,7 @@ export function microstep< actorScope: AnyActorScope, event: AnyEventObject, isInitial: boolean, - internalQueue: Array, - actionExecutor: ActionExecutor + internalQueue: Array ): AnyMachineSnapshot { if (!transitions.length) { return currentSnapshot; @@ -1054,7 +1053,7 @@ export function microstep< mutStateNodeSet, historyValue, internalQueue, - actionExecutor + actorScope.actionExecutor ); } @@ -1065,8 +1064,7 @@ export function microstep< actorScope, filteredTransitions.flatMap((t) => t.actions), internalQueue, - undefined, - actionExecutor + undefined ); // Enter states @@ -1078,8 +1076,7 @@ export function microstep< mutStateNodeSet, internalQueue, historyValue, - isInitial, - actionExecutor + isInitial ); const nextStateNodes = [...mutStateNodeSet]; @@ -1093,8 +1090,7 @@ export function microstep< .sort((a, b) => b.order - a.order) .flatMap((state) => state.exit), internalQueue, - undefined, - actionExecutor + undefined ); } @@ -1153,8 +1149,7 @@ function enterStates( mutStateNodeSet: Set, internalQueue: AnyEventObject[], historyValue: HistoryValue, - isInitial: boolean, - actionExecutor: ActionExecutor + isInitial: boolean ) { let nextSnapshot = currentSnapshot; const statesToEnter = new Set(); @@ -1205,8 +1200,7 @@ function enterStates( actorScope, actions, internalQueue, - stateNodeToEnter.invoke.map((invokeDef) => invokeDef.id), - actionExecutor + stateNodeToEnter.invoke.map((invokeDef) => invokeDef.id) ); if (stateNodeToEnter.type === 'final') { @@ -1507,8 +1501,7 @@ function exitStates( ) as unknown as UnknownActionObject[]) ], internalQueue, - undefined, - actionExecutor + undefined ); mutStateNodeSet.delete(s); } @@ -1541,17 +1534,21 @@ export let executingCustomAction: | ActionFunction | false = false; -interface ActionToExecute { - actionType: string; - stuff: ActionArgs; +export interface ExecutableAction { + /** + * The action type + */ + type: string; + // TODO: don't know if we need this + _info: ActionArgs; params: NonReducibleUnknown; - execute: ( - stuff: ActionArgs, - params: NonReducibleUnknown - ) => void; + /** + * Executes the action with the params (and extra action info, like context/event, for now) + */ + execute: () => void; } -export type ActionExecutor = (actionToExecute: ActionToExecute) => void; +export type ActionExecutor = (actionToExecute: ExecutableAction) => void; function resolveAndExecuteActionsWithContext( currentSnapshot: AnyMachineSnapshot, @@ -1562,8 +1559,7 @@ function resolveAndExecuteActionsWithContext( internalQueue: AnyEventObject[]; deferredActorIds: string[] | undefined; }, - retries: (readonly [BuiltinAction, unknown])[] | undefined, - actionExecutor: ActionExecutor + retries: (readonly [BuiltinAction, unknown])[] | undefined ): AnyMachineSnapshot { const { machine } = currentSnapshot; let intermediateSnapshot = currentSnapshot; @@ -1623,11 +1619,11 @@ function resolveAndExecuteActionsWithContext( }); try { executingCustomAction = resolvedAction; - actionExecutor({ - actionType: typeof action === 'object' ? action.type : 'TODO', - stuff: actionArgs, + actorScope.actionExecutor({ + type: typeof action === 'object' ? action.type : 'TODO', + _info: actionArgs, params: actionParams, - execute: resolvedAction + execute: () => resolvedAction(actionArgs, actionParams) }); // resolvedAction(actionArgs, actionParams); } finally { @@ -1677,8 +1673,7 @@ function resolveAndExecuteActionsWithContext( actorScope, actions, extra, - retries, - actionExecutor + retries ); } } @@ -1692,8 +1687,7 @@ export function resolveActionsAndContext( actorScope: AnyActorScope, actions: UnknownActionObject[], internalQueue: AnyEventObject[], - deferredActorIds: string[] | undefined, - actionExecutor: ActionExecutor + deferredActorIds: string[] | undefined ): AnyMachineSnapshot { const retries: (readonly [BuiltinAction, unknown])[] | undefined = deferredActorIds ? [] : undefined; @@ -1703,8 +1697,7 @@ export function resolveActionsAndContext( actorScope, actions, { internalQueue, deferredActorIds }, - retries, - actionExecutor + retries ); retries?.forEach(([builtinAction, params]) => { builtinAction.retryResolve(actorScope, nextState, params); @@ -1716,8 +1709,7 @@ export function macrostep( snapshot: AnyMachineSnapshot, event: EventObject, actorScope: AnyActorScope, - internalQueue: AnyEventObject[], - actionExecutor: ActionExecutor + internalQueue: AnyEventObject[] ): { snapshot: typeof snapshot; microstates: Array; @@ -1747,7 +1739,7 @@ export function macrostep( // Handle stop event if (event.type === XSTATE_STOP) { nextSnapshot = cloneMachineSnapshot( - stopChildren(nextSnapshot, event, actorScope, actionExecutor), + stopChildren(nextSnapshot, event, actorScope), { status: 'stopped' } @@ -1790,8 +1782,7 @@ export function macrostep( actorScope, nextEvent, false, // isInitial - internalQueue, - actionExecutor + internalQueue ); addMicrostate(nextSnapshot, currentEvent, transitions); } @@ -1822,15 +1813,14 @@ export function macrostep( actorScope, nextEvent, false, - internalQueue, - actionExecutor + internalQueue ); shouldSelectEventlessTransitions = nextSnapshot !== previousState; addMicrostate(nextSnapshot, nextEvent, enabledTransitions); } if (nextSnapshot.status !== 'active') { - stopChildren(nextSnapshot, nextEvent, actorScope, actionExecutor); + stopChildren(nextSnapshot, nextEvent, actorScope); } return { @@ -1842,8 +1832,7 @@ export function macrostep( function stopChildren( nextState: AnyMachineSnapshot, event: AnyEventObject, - actorScope: AnyActorScope, - actionExecutor: ActionExecutor + actorScope: AnyActorScope ) { return resolveActionsAndContext( nextState, @@ -1853,8 +1842,7 @@ function stopChildren( (child: any) => stopChild(child) as unknown as UnknownActionObject ), [], - undefined, - actionExecutor + undefined ); } diff --git a/packages/core/src/transition.ts b/packages/core/src/transition.ts new file mode 100644 index 0000000000..569e4e8114 --- /dev/null +++ b/packages/core/src/transition.ts @@ -0,0 +1,20 @@ +import { createInertActorScope } from './getNextSnapshot'; +import { ExecutableAction } from './stateUtils'; +import { AnyActorLogic, EventFrom, SnapshotFrom } from './types'; + +export function transition( + logic: T, + snapshot: SnapshotFrom, + event: EventFrom +): [SnapshotFrom, ExecutableAction[]] { + const executableActions = [] as ExecutableAction[]; + + const actorScope = createInertActorScope(logic); + actorScope.actionExecutor = (action) => { + executableActions.push(action); + }; + + const nextSnapshot = logic.transition(snapshot, event, actorScope); + + return [nextSnapshot, executableActions]; +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 102b5b2483..3d6696e3cb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -13,6 +13,7 @@ import { TypegenConstraint, TypegenDisabled } from './typegenTypes.ts'; +import { ActionExecutor } from './stateUtils.ts'; export type Identity = { [K in keyof T]: T[K] }; @@ -2366,6 +2367,7 @@ export interface ActorScope< emit: (event: TEmitted) => void; system: TSystem; stopChild: (child: AnyActorRef) => void; + actionExecutor: ActionExecutor; } export type AnyActorScope = ActorScope< @@ -2415,16 +2417,16 @@ export interface ActorLogic< /** The initial setup/configuration used to create the actor logic. */ config?: unknown; /** - * Transition function that processes the current state and an incoming message + * Transition function that processes the current state and an incoming event * to produce a new state. * @param snapshot - The current state. - * @param message - The incoming message. + * @param event - The incoming event. * @param actorScope - The actor scope. * @returns The new state. */ transition: ( snapshot: TSnapshot, - message: TEvent, + event: TEvent, actorScope: ActorScope ) => TSnapshot; /** diff --git a/packages/xstate-graph/src/actorScope.ts b/packages/xstate-graph/src/actorScope.ts index a1783a3654..0168a2c498 100644 --- a/packages/xstate-graph/src/actorScope.ts +++ b/packages/xstate-graph/src/actorScope.ts @@ -10,6 +10,7 @@ export function createMockActorScope(): AnyActorScope { defer: () => {}, system: emptyActor.system, // TODO: mock system? stopChild: () => {}, - emit: () => {} + emit: () => {}, + actionExecutor: () => {} }; } From b2b974a1e50bd12087d5f944096951c18ce51173 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 5 Jun 2024 08:16:45 -0400 Subject: [PATCH 06/80] WIP --- packages/core/src/StateMachine.ts | 11 +-- packages/core/src/createActor.ts | 24 ++++++- packages/core/src/index.ts | 1 + packages/core/src/stateUtils.ts | 63 +++++++++-------- packages/core/src/transition.ts | 20 +++++- packages/core/test/deterministic.test.ts | 74 ++++++++------------ packages/core/test/getNextSnapshot.test.ts | 19 ++++- packages/core/test/transition.test.ts | 80 ++++++++++++++++++++++ 8 files changed, 206 insertions(+), 86 deletions(-) create mode 100644 packages/core/test/transition.test.ts diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index a010776637..dae332b18f 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -9,8 +9,6 @@ import { } from './State.ts'; import { StateNode } from './StateNode.ts'; import { - ActionExecutor, - defaultActionExecutor, getAllStateNodes, getInitialStateNodes, getStateNodeByPath, @@ -356,8 +354,7 @@ export class StateMachine< private getPreInitialState( actorScope: AnyActorScope, initEvent: any, - internalQueue: AnyEventObject[], - actionExecutor: ActionExecutor + internalQueue: AnyEventObject[] ): MachineSnapshot< TContext, TEvent, @@ -414,8 +411,7 @@ export class StateMachine< AnyActorSystem, TEmitted >, - input?: TInput, - actionExecutor: ActionExecutor = defaultActionExecutor + input?: TInput ): MachineSnapshot< TContext, TEvent, @@ -430,8 +426,7 @@ export class StateMachine< const preInitialState = this.getPreInitialState( actorScope, initEvent, - internalQueue, - actionExecutor + internalQueue ); const nextState = microstep( [ diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index e81effeac1..f59f4f168a 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -8,10 +8,11 @@ import { createInitEvent } from './eventUtils.ts'; import { reportUnhandledError } from './reportUnhandledError.ts'; -import { defaultActionExecutor } from './stateUtils.ts'; import { symbolObservable } from './symbolObservable.ts'; import { AnyActorSystem, Clock, createSystem } from './system.ts'; +export let executingCustomAction: ((...args: any[]) => void) | false = false; + import type { ActorScope, AnyActorLogic, @@ -202,7 +203,26 @@ export class Actor handler(emittedEvent); } }, - actionExecutor: defaultActionExecutor + actionExecutor: (action) => { + if (this._processingStatus === ProcessingStatus.Running) { + this._actorScope.system._sendInspectionEvent({ + type: '@xstate.action', + actorRef: this, + action: { + type: action.type, + params: action.params as any // TODO: fix types + } + }); + try { + executingCustomAction = action.execute; + action.execute(); + } finally { + executingCustomAction = false; + } + } else { + this._deferred.push(action.execute); + } + } }; // Ensure that the send method is bound to this Actor instance diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8412689310..680ca1903b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,6 +38,7 @@ export { type Interpreter }; export { assertEvent } from './assert.ts'; +export { transition } from './transition.ts'; declare global { interface SymbolConstructor { diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index cd243f3788..3a0f6cf380 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1530,10 +1530,6 @@ interface BuiltinAction { execute: (actorScope: AnyActorScope, params: unknown) => void; } -export let executingCustomAction: - | ActionFunction - | false = false; - export interface ExecutableAction { /** * The action type @@ -1609,36 +1605,43 @@ function resolveAndExecuteActionsWithContext( : undefined; function executeAction() { - actorScope.system._sendInspectionEvent({ - type: '@xstate.action', - actorRef: actorScope.self, - action: { - type: action.type, - params: actionParams - } + actorScope.actionExecutor({ + type: typeof action === 'object' ? action.type : 'TODO', + _info: actionArgs, + params: actionParams, + execute: () => resolvedAction(actionArgs, actionParams) }); - try { - executingCustomAction = resolvedAction; - actorScope.actionExecutor({ - type: typeof action === 'object' ? action.type : 'TODO', - _info: actionArgs, - params: actionParams, - execute: () => resolvedAction(actionArgs, actionParams) - }); - // resolvedAction(actionArgs, actionParams); - } finally { - executingCustomAction = false; - } + // actorScope.system._sendInspectionEvent({ + // type: '@xstate.action', + // actorRef: actorScope.self, + // action: { + // type: action.type, + // params: actionParams + // } + // }); + // try { + // executingCustomAction = resolvedAction; + // actorScope.actionExecutor({ + // type: typeof action === 'object' ? action.type : 'TODO', + // _info: actionArgs, + // params: actionParams, + // execute: () => resolvedAction(actionArgs, actionParams) + // }); + // // resolvedAction(actionArgs, actionParams); + // } finally { + // executingCustomAction = false; + // } } if (!('resolve' in resolvedAction)) { - if (actorScope.self._processingStatus === ProcessingStatus.Running) { - executeAction(); - } else { - actorScope.defer(() => { - executeAction(); - }); - } + executeAction(); + // if (actorScope.self._processingStatus === ProcessingStatus.Running) { + // executeAction(); + // } else { + // actorScope.defer(() => { + // executeAction(); + // }); + // } continue; } diff --git a/packages/core/src/transition.ts b/packages/core/src/transition.ts index 569e4e8114..a064000e09 100644 --- a/packages/core/src/transition.ts +++ b/packages/core/src/transition.ts @@ -1,6 +1,6 @@ import { createInertActorScope } from './getNextSnapshot'; import { ExecutableAction } from './stateUtils'; -import { AnyActorLogic, EventFrom, SnapshotFrom } from './types'; +import { AnyActorLogic, EventFrom, InputFrom, SnapshotFrom } from './types'; export function transition( logic: T, @@ -18,3 +18,21 @@ export function transition( return [nextSnapshot, executableActions]; } + +export function initialTransition( + logic: T, + ...[input]: undefined extends InputFrom + ? [input?: InputFrom] + : [input: InputFrom] +): [SnapshotFrom, ExecutableAction[]] { + const executableActions = [] as ExecutableAction[]; + + const actorScope = createInertActorScope(logic); + actorScope.actionExecutor = (action) => { + executableActions.push(action); + }; + + const nextSnapshot = logic.getInitialSnapshot(actorScope, input); + + return [nextSnapshot, executableActions]; +} diff --git a/packages/core/test/deterministic.test.ts b/packages/core/test/deterministic.test.ts index bdaafbaaa6..e7b4756925 100644 --- a/packages/core/test/deterministic.test.ts +++ b/packages/core/test/deterministic.test.ts @@ -1,7 +1,7 @@ import { fromCallback, createActor, - getNextSnapshot, + transition, createMachine, getInitialSnapshot } from '../src/index.ts'; @@ -69,13 +69,13 @@ describe('deterministic machine', () => { describe('machine transitions', () => { it('should properly transition states based on event-like object', () => { expect( - getNextSnapshot( + transition( lightMachine, lightMachine.resolveState({ value: 'green' }), { type: 'TIMER' } - ).value + )[0].value ).toEqual('yellow'); }); @@ -104,7 +104,7 @@ describe('deterministic machine', () => { it('should throw an error if not given an event', () => { expect(() => - getNextSnapshot( + transition( lightMachine, testMachine.resolveState({ value: 'red' }), undefined as any @@ -114,9 +114,9 @@ describe('deterministic machine', () => { it('should transition to nested states as target', () => { expect( - getNextSnapshot(testMachine, testMachine.resolveState({ value: 'a' }), { + transition(testMachine, testMachine.resolveState({ value: 'a' }), { type: 'T' - }).value + })[0].value ).toEqual({ b: 'b1' }); @@ -124,37 +124,31 @@ describe('deterministic machine', () => { it('should throw an error for transitions from invalid states', () => { expect(() => - getNextSnapshot( - testMachine, - testMachine.resolveState({ value: 'fake' }), - { type: 'T' } - ) + transition(testMachine, testMachine.resolveState({ value: 'fake' }), { + type: 'T' + }) ).toThrow(); }); it('should throw an error for transitions from invalid substates', () => { expect(() => - getNextSnapshot( - testMachine, - testMachine.resolveState({ value: 'a.fake' }), - { - type: 'T' - } - ) + transition(testMachine, testMachine.resolveState({ value: 'a.fake' }), { + type: 'T' + }) ).toThrow(); }); it('should use the machine.initialState when an undefined state is given', () => { const init = getInitialSnapshot(lightMachine, undefined); expect( - getNextSnapshot(lightMachine, init, { type: 'TIMER' }).value + transition(lightMachine, init, { type: 'TIMER' })[0].value ).toEqual('yellow'); }); it('should use the machine.initialState when an undefined state is given (unhandled event)', () => { const init = getInitialSnapshot(lightMachine, undefined); expect( - getNextSnapshot(lightMachine, init, { type: 'TIMER' }).value + transition(lightMachine, init, { type: 'TIMER' })[0].value ).toEqual('yellow'); }); }); @@ -162,23 +156,19 @@ describe('deterministic machine', () => { describe('machine transition with nested states', () => { it('should properly transition a nested state', () => { expect( - getNextSnapshot( + transition( lightMachine, lightMachine.resolveState({ value: { red: 'walk' } }), { type: 'PED_COUNTDOWN' } - ).value + )[0].value ).toEqual({ red: 'wait' }); }); it('should transition from initial nested states', () => { expect( - getNextSnapshot( - lightMachine, - lightMachine.resolveState({ value: 'red' }), - { - type: 'PED_COUNTDOWN' - } - ).value + transition(lightMachine, lightMachine.resolveState({ value: 'red' }), { + type: 'PED_COUNTDOWN' + })[0].value ).toEqual({ red: 'wait' }); @@ -186,13 +176,9 @@ describe('deterministic machine', () => { it('should transition from deep initial nested states', () => { expect( - getNextSnapshot( - lightMachine, - lightMachine.resolveState({ value: 'red' }), - { - type: 'PED_COUNTDOWN' - } - ).value + transition(lightMachine, lightMachine.resolveState({ value: 'red' }), { + type: 'PED_COUNTDOWN' + })[0].value ).toEqual({ red: 'wait' }); @@ -200,11 +186,11 @@ describe('deterministic machine', () => { it('should bubble up events that nested states cannot handle', () => { expect( - getNextSnapshot( + transition( lightMachine, lightMachine.resolveState({ value: { red: 'stop' } }), { type: 'TIMER' } - ).value + )[0].value ).toEqual('green'); }); @@ -238,13 +224,13 @@ describe('deterministic machine', () => { it('should transition to the deepest initial state', () => { expect( - getNextSnapshot( + transition( lightMachine, lightMachine.resolveState({ value: 'yellow' }), { type: 'TIMER' } - ).value + )[0].value ).toEqual({ red: 'walk' }); @@ -252,10 +238,10 @@ describe('deterministic machine', () => { it('should return the same state if no transition occurs', () => { const init = getInitialSnapshot(lightMachine, undefined); - const initialState = getNextSnapshot(lightMachine, init, { + const [initialState] = transition(lightMachine, init, { type: 'NOTHING' }); - const nextState = getNextSnapshot(lightMachine, initialState, { + const [nextState] = transition(lightMachine, initialState, { type: 'NOTHING' }); @@ -288,7 +274,7 @@ describe('deterministic machine', () => { it('should work with substate nodes that have the same key', () => { const init = getInitialSnapshot(machine, undefined); - expect(getNextSnapshot(machine, init, { type: 'NEXT' }).value).toEqual( + expect(transition(machine, init, { type: 'NEXT' })[0].value).toEqual( 'test' ); }); @@ -296,7 +282,7 @@ describe('deterministic machine', () => { describe('forbidden events', () => { it('undefined transitions should forbid events', () => { - const walkState = getNextSnapshot( + const [walkState] = transition( lightMachine, lightMachine.resolveState({ value: { red: 'walk' } }), { type: 'TIMER' } diff --git a/packages/core/test/getNextSnapshot.test.ts b/packages/core/test/getNextSnapshot.test.ts index 76759391d2..dcba56b9f2 100644 --- a/packages/core/test/getNextSnapshot.test.ts +++ b/packages/core/test/getNextSnapshot.test.ts @@ -51,7 +51,24 @@ describe('getNextSnapshot', () => { expect(s2.value).toEqual('c'); }); - it('should not execute actions', () => { + it('should not execute entry actions', () => { + const fn = jest.fn(); + + const machine = createMachine({ + initial: 'a', + entry: fn, + states: { + a: {}, + b: {} + } + }); + + getInitialSnapshot(machine, undefined); + + expect(fn).not.toHaveBeenCalled(); + }); + + it('should not execute transition actions', () => { const fn = jest.fn(); const machine = createMachine({ diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts new file mode 100644 index 0000000000..1ffd7a4d0c --- /dev/null +++ b/packages/core/test/transition.test.ts @@ -0,0 +1,80 @@ +import { assign, setup, transition } from '../src'; +import { initialTransition } from '../src/transition'; + +describe('transition function', () => { + it('should capture actions', () => { + const actionWithParams = jest.fn(); + const actionWithDynamicParams = jest.fn(); + const stringAction = jest.fn(); + + const machine = setup({ + types: { + context: {} as { count: number }, + events: {} as { type: 'event'; msg: string } + }, + actions: { + actionWithParams, + actionWithDynamicParams: (_, params: { msg: string }) => { + actionWithDynamicParams(params); + }, + stringAction + } + }).createMachine({ + entry: [ + { type: 'actionWithParams', params: { a: 1 } }, + 'stringAction', + assign({ count: 100 }) + ], + context: { count: 0 }, + on: { + event: { + actions: { + type: 'actionWithDynamicParams', + params: ({ event }) => { + return { msg: event.msg }; + } + } + } + } + }); + + const [state0, actions0] = initialTransition(machine); + + expect(state0.context.count).toBe(100); + expect(actions0).toEqual([ + expect.objectContaining({ type: 'actionWithParams', params: { a: 1 } }), + expect.objectContaining({ type: 'stringAction' }) + ]); + + expect(actionWithParams).not.toHaveBeenCalled(); + expect(stringAction).not.toHaveBeenCalled(); + + // Execute actions + actions0.forEach((a) => a.execute()); + + expect(actionWithParams).toHaveBeenCalledWith(expect.anything(), { a: 1 }); + expect(stringAction).toHaveBeenCalled(); + + const [state1, actions1] = transition(machine, state0, { + type: 'event', + msg: 'hello' + }); + + expect(state1.context.count).toBe(100); + expect(actions1).toEqual([ + expect.objectContaining({ + type: 'actionWithDynamicParams', + params: { msg: 'hello' } + }) + ]); + + expect(actionWithDynamicParams).not.toHaveBeenCalled(); + + // Execute actions + actions1.forEach((a) => a.execute()); + + expect(actionWithDynamicParams).toHaveBeenCalledWith({ + msg: 'hello' + }); + }); +}); From 7641ec51cf0fcbde55ba4e5659d6d7ad86212b63 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 19 Jun 2024 10:18:46 +0200 Subject: [PATCH 07/80] Fixing tests --- packages/core/src/actions/assign.ts | 2 +- packages/core/src/actions/emit.ts | 2 +- packages/core/src/actions/raise.ts | 2 +- packages/core/src/actions/send.ts | 2 +- packages/core/src/actions/spawnChild.ts | 2 +- packages/core/src/createActor.ts | 7 +++++-- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/core/src/actions/assign.ts b/packages/core/src/actions/assign.ts index 9332a3442b..2dcb66a68c 100644 --- a/packages/core/src/actions/assign.ts +++ b/packages/core/src/actions/assign.ts @@ -1,7 +1,7 @@ import isDevelopment from '#is-development'; import { cloneMachineSnapshot } from '../State.ts'; +import { executingCustomAction } from '../createActor.ts'; import { Spawner, createSpawner } from '../spawn.ts'; -import { executingCustomAction } from '../stateUtils.ts'; import type { ActionArgs, AnyActorScope, diff --git a/packages/core/src/actions/emit.ts b/packages/core/src/actions/emit.ts index 32efb9ed91..6eec19a136 100644 --- a/packages/core/src/actions/emit.ts +++ b/packages/core/src/actions/emit.ts @@ -1,5 +1,5 @@ import isDevelopment from '#is-development'; -import { executingCustomAction } from '../stateUtils.ts'; +import { executingCustomAction } from '../createActor.ts'; import { ActionArgs, AnyActorScope, diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index e1f964c4d6..28be6d994c 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -1,5 +1,5 @@ import isDevelopment from '#is-development'; -import { executingCustomAction } from '../stateUtils.ts'; +import { executingCustomAction } from '../createActor.ts'; import { ActionArgs, ActionFunction, diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 03250afd4a..71fa112fc0 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -1,7 +1,7 @@ import isDevelopment from '#is-development'; import { XSTATE_ERROR } from '../constants.ts'; import { createErrorActorEvent } from '../eventUtils.ts'; -import { executingCustomAction } from '../stateUtils.ts'; +import { executingCustomAction } from '../createActor.ts'; import { ActionArgs, ActionFunction, diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index ad93e2ce3b..551ffffcb9 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -1,7 +1,7 @@ import isDevelopment from '#is-development'; import { cloneMachineSnapshot } from '../State.ts'; import { ProcessingStatus, createActor } from '../createActor.ts'; -import { executingCustomAction } from '../stateUtils.ts'; +// import { executingCustomAction } from '../createActor.ts'; import { ActionArgs, ActionFunction, diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 62f2778383..e75c4b4250 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -209,7 +209,7 @@ export class Actor } }, actionExecutor: (action) => { - if (this._processingStatus === ProcessingStatus.Running) { + const exec = () => { this._actorScope.system._sendInspectionEvent({ type: '@xstate.action', actorRef: this, @@ -224,8 +224,11 @@ export class Actor } finally { executingCustomAction = false; } + }; + if (this._processingStatus === ProcessingStatus.Running) { + exec(); } else { - this._deferred.push(action.execute); + this._deferred.push(exec); } } }; From 99423822aa52330eb360a41dbc85ac69167dfe68 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 19 Jun 2024 10:32:15 +0200 Subject: [PATCH 08/80] Add toJSON to built-in actions --- packages/core/src/actions/assign.ts | 4 ++++ packages/core/src/actions/cancel.ts | 4 ++++ packages/core/src/actions/emit.ts | 4 ++++ packages/core/src/actions/enqueueActions.ts | 4 ++++ packages/core/src/actions/log.ts | 4 ++++ packages/core/src/actions/raise.ts | 4 ++++ packages/core/src/actions/send.ts | 4 ++++ packages/core/src/actions/spawnChild.ts | 4 ++++ packages/core/src/actions/stopChild.ts | 5 ++++- 9 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/core/src/actions/assign.ts b/packages/core/src/actions/assign.ts index 2dcb66a68c..eefc5048f8 100644 --- a/packages/core/src/actions/assign.ts +++ b/packages/core/src/actions/assign.ts @@ -177,5 +177,9 @@ export function assign< assign.resolve = resolveAssign; + assign.toJSON = () => ({ + ...assign + }); + return assign; } diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index 5a094b8d20..7b6d5f6636 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -97,5 +97,9 @@ export function cancel< cancel.resolve = resolveCancel; cancel.execute = executeCancel; + cancel.toJSON = () => ({ + ...cancel + }); + return cancel; } diff --git a/packages/core/src/actions/emit.ts b/packages/core/src/actions/emit.ts index 6eec19a136..43e3ad0dee 100644 --- a/packages/core/src/actions/emit.ts +++ b/packages/core/src/actions/emit.ts @@ -143,5 +143,9 @@ export function emit< emit.resolve = resolveEmit; emit.execute = executeEmit; + emit.toJSON = () => ({ + ...emit + }); + return emit; } diff --git a/packages/core/src/actions/enqueueActions.ts b/packages/core/src/actions/enqueueActions.ts index b90856a67c..f91dbed8d5 100644 --- a/packages/core/src/actions/enqueueActions.ts +++ b/packages/core/src/actions/enqueueActions.ts @@ -304,5 +304,9 @@ export function enqueueActions< enqueueActions.collect = collect; enqueueActions.resolve = resolveEnqueueActions; + enqueueActions.toJSON = () => ({ + ...enqueueActions + }); + return enqueueActions; } diff --git a/packages/core/src/actions/log.ts b/packages/core/src/actions/log.ts index b7c849d4c0..746bc6096b 100644 --- a/packages/core/src/actions/log.ts +++ b/packages/core/src/actions/log.ts @@ -95,5 +95,9 @@ export function log< log.resolve = resolveLog; log.execute = executeLog; + log.toJSON = () => ({ + ...log + }); + return log; } diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index 28be6d994c..f63c4bd1e4 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -165,5 +165,9 @@ export function raise< raise.resolve = resolveRaise; raise.execute = executeRaise; + raise.toJSON = () => ({ + ...raise + }); + return raise; } diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 71fa112fc0..7f0a50ce8c 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -258,6 +258,10 @@ export function sendTo< sendTo.retryResolve = retryResolveSendTo; sendTo.execute = executeSendTo; + sendTo.toJSON = () => ({ + ...sendTo + }); + return sendTo; } diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index 551ffffcb9..340e689098 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -225,5 +225,9 @@ export function spawnChild< spawnChild.resolve = resolveSpawn; spawnChild.execute = executeSpawn; + spawnChild.toJSON = () => ({ + ...spawnChild + }); + return spawnChild; } diff --git a/packages/core/src/actions/stopChild.ts b/packages/core/src/actions/stopChild.ts index 2f0f1b6917..ea709cfaf8 100644 --- a/packages/core/src/actions/stopChild.ts +++ b/packages/core/src/actions/stopChild.ts @@ -3,7 +3,6 @@ import { cloneMachineSnapshot } from '../State.ts'; import { ProcessingStatus } from '../createActor.ts'; import { ActionArgs, - ActorRef, AnyActorRef, AnyActorScope, AnyMachineSnapshot, @@ -116,6 +115,10 @@ export function stopChild< stop.resolve = resolveStop; stop.execute = executeStop; + stop.toJSON = () => ({ + ...stop + }); + return stop; } From efaac403b7ceebdc102c858cc61eebe47a87264e Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 25 Jun 2024 11:22:32 +0200 Subject: [PATCH 09/80] Add docs --- packages/core/src/transition.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/transition.ts b/packages/core/src/transition.ts index a064000e09..df361e00c0 100644 --- a/packages/core/src/transition.ts +++ b/packages/core/src/transition.ts @@ -2,11 +2,17 @@ import { createInertActorScope } from './getNextSnapshot'; import { ExecutableAction } from './stateUtils'; import { AnyActorLogic, EventFrom, InputFrom, SnapshotFrom } from './types'; +/** + * Given actor `logic`, a `snapshot`, and an `event`, returns a + * tuple of the `nextSnapshot` and `actions` to execute. + * + * This is a pure function that does not execute `actions`. + */ export function transition( logic: T, snapshot: SnapshotFrom, event: EventFrom -): [SnapshotFrom, ExecutableAction[]] { +): [nextSnapshot: SnapshotFrom, actions: ExecutableAction[]] { const executableActions = [] as ExecutableAction[]; const actorScope = createInertActorScope(logic); @@ -19,6 +25,13 @@ export function transition( return [nextSnapshot, executableActions]; } +/** + * Given actor `logic` and optional `input`, returns a + * tuple of the `nextSnapshot` and `actions` to execute from the + * initial transition (no previous state). + * + * This is a pure function that does not execute `actions`. + */ export function initialTransition( logic: T, ...[input]: undefined extends InputFrom From 8b936ab18dcb2bbf5881b9187d8519e2f9bb418b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 3 Jul 2024 11:15:41 -0400 Subject: [PATCH 10/80] Update packages/core/src/actions/spawnChild.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/core/src/actions/spawnChild.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index 340e689098..41ec26da2f 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -1,7 +1,6 @@ import isDevelopment from '#is-development'; import { cloneMachineSnapshot } from '../State.ts'; import { ProcessingStatus, createActor } from '../createActor.ts'; -// import { executingCustomAction } from '../createActor.ts'; import { ActionArgs, ActionFunction, From ae70cc6235b99c0caef0feb65969e2aca01e61d1 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 3 Jul 2024 11:15:49 -0400 Subject: [PATCH 11/80] Update packages/core/src/stateUtils.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/core/src/stateUtils.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 3a0f6cf380..e1cc06c5e4 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1611,26 +1611,6 @@ function resolveAndExecuteActionsWithContext( params: actionParams, execute: () => resolvedAction(actionArgs, actionParams) }); - // actorScope.system._sendInspectionEvent({ - // type: '@xstate.action', - // actorRef: actorScope.self, - // action: { - // type: action.type, - // params: actionParams - // } - // }); - // try { - // executingCustomAction = resolvedAction; - // actorScope.actionExecutor({ - // type: typeof action === 'object' ? action.type : 'TODO', - // _info: actionArgs, - // params: actionParams, - // execute: () => resolvedAction(actionArgs, actionParams) - // }); - // // resolvedAction(actionArgs, actionParams); - // } finally { - // executingCustomAction = false; - // } } if (!('resolve' in resolvedAction)) { From f3c360f43424bdf11b47c2916ce04c8f492bd19a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 4 Jul 2024 14:51:38 -0400 Subject: [PATCH 12/80] Unify convertAction --- packages/core/src/StateNode.ts | 43 +++++++++++++++------------ packages/core/src/stateUtils.ts | 51 ++++++++++++++++++++++----------- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 1a39769fbf..15a3a9d271 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -4,6 +4,7 @@ import { NULL_EVENT, STATE_DELIMITER } from './constants.ts'; import { evaluateGuard } from './guards.ts'; import { memo } from './memo.ts'; import { + convertAction, formatInitialTransition, formatTransition, formatTransitions, @@ -206,27 +207,31 @@ export class StateNode< this.history = this.config.history === true ? 'shallow' : this.config.history || false; - const convertAction = ( - action: UnknownAction, - kind: 'entry' | 'exit', - i: number - ): UnknownActionObject => { - if (typeof action === 'string') { - return { type: action }; - } - if (typeof action === 'function' && !('resolve' in action)) { - const type = `${this.id}|${kind}:${i}`; - this.machine.implementations.actions[type] = action as any; - return { type }; + this.entry = toArray(this.config.entry as UnknownAction).map( + (action, actionIndex) => { + const actionObject = convertAction( + action, + this, + undefined, + 'entry', + 0, + actionIndex + ); + return actionObject; } - return action as any; - }; - - this.entry = toArray(this.config.entry as UnknownAction).map((a, i) => - convertAction(a, 'entry', i) ); - this.exit = toArray(this.config.exit as UnknownAction).map((a, i) => - convertAction(a, 'exit', i) + this.exit = toArray(this.config.exit as UnknownAction).map( + (action, actionIndex) => { + const actionObject = convertAction( + action, + this, + undefined, + 'exit', + 0, + actionIndex + ); + return actionObject; + } ); this.meta = this.config.meta; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index e1cc06c5e4..02277d938c 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -334,7 +334,7 @@ export function formatTransition< stateNode: AnyStateNode, descriptor: string, transitionConfig: AnyTransitionConfig, - index: number + transitionIndex: number ): AnyTransitionDefinition { const normalizedTarget = normalizeTarget(transitionConfig.target); const reenter = transitionConfig.reenter ?? false; @@ -347,24 +347,18 @@ export function formatTransition< ); } - const convertAction = ( - action: UnknownAction, - i: number - ): UnknownActionObject => { - if (typeof action === 'string') { - return { type: action }; - } - if (typeof action === 'function' && !('resolve' in action)) { - const type = `${stateNode.id}|${descriptor}:${index}:${i}`; - stateNode.machine.implementations.actions[type] = action as any; - return { type }; - } - return action as any; - }; - const transition = { ...transitionConfig, - actions: toArray(transitionConfig.actions).map(convertAction), + actions: toArray(transitionConfig.actions).map((action, actionIndex) => { + return convertAction( + action, + stateNode, + descriptor, + undefined, + transitionIndex, + actionIndex + ); + }), guard: transitionConfig.guard as never, target, source: stateNode, @@ -1906,3 +1900,26 @@ export function stateValuesEqual( aKeys.every((key) => stateValuesEqual(a[key], b[key])) ); } + +export function convertAction( + action: UnknownAction, + stateNode: AnyStateNode, + descriptor: string | undefined, + kind: 'entry' | 'exit' | undefined, + transitionIndex: number, + actionIndex: number +): UnknownActionObject { + if (typeof action === 'string') { + return { type: action }; + } + if (typeof action === 'function' && !('resolve' in action)) { + const type = `${stateNode.id}|${ + descriptor ?? kind + }:${transitionIndex}:${actionIndex}`; + stateNode.machine.implementations.actions[type] = action as any; + return { + type + }; + } + return action as any; +} From 32b278c446658054173a0eb45530d6a210bf67da Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 4 Jul 2024 14:52:08 -0400 Subject: [PATCH 13/80] Update packages/core/src/stateUtils.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/core/src/stateUtils.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 02277d938c..2b66525f77 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1609,13 +1609,6 @@ function resolveAndExecuteActionsWithContext( if (!('resolve' in resolvedAction)) { executeAction(); - // if (actorScope.self._processingStatus === ProcessingStatus.Running) { - // executeAction(); - // } else { - // actorScope.defer(() => { - // executeAction(); - // }); - // } continue; } From b1d346ebf34334ebf8ce067e734afc6975f71dbe Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 4 Jul 2024 14:58:08 -0400 Subject: [PATCH 14/80] Remove TODO --- packages/core/src/stateUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 2b66525f77..1384ca4958 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1600,7 +1600,7 @@ function resolveAndExecuteActionsWithContext( function executeAction() { actorScope.actionExecutor({ - type: typeof action === 'object' ? action.type : 'TODO', + type: action.type, _info: actionArgs, params: actionParams, execute: () => resolvedAction(actionArgs, actionParams) From 4f61ac74cc70299cb2ed0f29a0233156ef9ee2d4 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 4 Jul 2024 15:42:41 -0400 Subject: [PATCH 15/80] Introduce `executeAction`, remove `action.execute()` --- packages/core/src/createActor.ts | 5 +++-- packages/core/src/stateUtils.ts | 24 +++++++++++++----------- packages/core/test/transition.test.ts | 5 +++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index e75c4b4250..b8a5a74e69 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -8,6 +8,7 @@ import { createInitEvent } from './eventUtils.ts'; import { reportUnhandledError } from './reportUnhandledError.ts'; +import { executeAction } from './stateUtils.ts'; import { symbolObservable } from './symbolObservable.ts'; import { AnyActorSystem, Clock, createSystem } from './system.ts'; @@ -219,8 +220,8 @@ export class Actor } }); try { - executingCustomAction = action.execute; - action.execute(); + executingCustomAction = action.function; + executeAction(action); } finally { executingCustomAction = false; } diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 1384ca4958..29cafa341d 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -51,9 +51,7 @@ import { } from './utils.ts'; import { ProcessingStatus } from './createActor.ts'; -export const defaultActionExecutor: ActionExecutor = (actionToExecute) => { - actionToExecute.execute(); -}; +export const defaultActionExecutor: ActionExecutor = executeAction; type StateNodeIterable< TContext extends MachineContext, @@ -1529,13 +1527,9 @@ export interface ExecutableAction { * The action type */ type: string; - // TODO: don't know if we need this - _info: ActionArgs; + info: ActionArgs; params: NonReducibleUnknown; - /** - * Executes the action with the params (and extra action info, like context/event, for now) - */ - execute: () => void; + function: (info: ActionArgs, params: unknown) => void; } export type ActionExecutor = (actionToExecute: ExecutableAction) => void; @@ -1601,9 +1595,9 @@ function resolveAndExecuteActionsWithContext( function executeAction() { actorScope.actionExecutor({ type: action.type, - _info: actionArgs, + info: actionArgs, params: actionParams, - execute: () => resolvedAction(actionArgs, actionParams) + function: resolvedAction }); } @@ -1916,3 +1910,11 @@ export function convertAction( } return action as any; } + +export function executeAction( + action: ExecutableAction, + info: ActionArgs = action.info, + params: unknown = action.params +) { + return action.function(info, params); +} diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 1ffd7a4d0c..943586cbf9 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -1,4 +1,5 @@ import { assign, setup, transition } from '../src'; +import { executeAction } from '../src/stateUtils'; import { initialTransition } from '../src/transition'; describe('transition function', () => { @@ -50,7 +51,7 @@ describe('transition function', () => { expect(stringAction).not.toHaveBeenCalled(); // Execute actions - actions0.forEach((a) => a.execute()); + actions0.forEach((a) => executeAction(a)); expect(actionWithParams).toHaveBeenCalledWith(expect.anything(), { a: 1 }); expect(stringAction).toHaveBeenCalled(); @@ -71,7 +72,7 @@ describe('transition function', () => { expect(actionWithDynamicParams).not.toHaveBeenCalled(); // Execute actions - actions1.forEach((a) => a.execute()); + actions1.forEach((a) => executeAction(a)); expect(actionWithDynamicParams).toHaveBeenCalledWith({ msg: 'hello' From bc431d1f1672334f9f619f8e770e7957b71e3aa2 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 4 Jul 2024 19:09:34 -0400 Subject: [PATCH 16/80] Enqueue actions test --- packages/core/src/actions/enqueueActions.ts | 12 +++++++++- packages/core/src/stateUtils.ts | 22 ++++++++++------- packages/core/test/transition.test.ts | 26 ++++++++++++++++++++- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/core/src/actions/enqueueActions.ts b/packages/core/src/actions/enqueueActions.ts index f91dbed8d5..8d846e2361 100644 --- a/packages/core/src/actions/enqueueActions.ts +++ b/packages/core/src/actions/enqueueActions.ts @@ -1,5 +1,6 @@ import isDevelopment from '#is-development'; import { Guard, evaluateGuard } from '../guards.ts'; +import { convertAction } from '../stateUtils.ts'; import { Action, ActionArgs, @@ -121,7 +122,16 @@ function resolveEnqueueActions( const enqueue: Parameters[0]['enqueue'] = function enqueue( action ) { - actions.push(action); + actions.push( + convertAction( + action as any, + snapshot.machine.root, + 'enqueue' + Math.random(), // TODO: this should come from state node ID which isn't provided + undefined, + 0, + actions.length + ) + ); }; enqueue.assign = (...args) => { actions.push(assign(...args)); diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 29cafa341d..f6599300ec 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -471,7 +471,11 @@ export function formatInitialTransition< const transition: InitialTransitionDefinition = { source: stateNode, actions: - !_target || typeof _target === 'string' ? [] : toArray(_target.actions), + !_target || typeof _target === 'string' + ? [] + : toArray(_target.actions).map((action) => + convertAction(action, stateNode, 'xstate.init', undefined, 0, 0) + ), eventType: null as any, reenter: false, target: resolvedTarget ? [resolvedTarget] : [], @@ -1529,7 +1533,9 @@ export interface ExecutableAction { type: string; info: ActionArgs; params: NonReducibleUnknown; - function: (info: ActionArgs, params: unknown) => void; + function: + | ((info: ActionArgs, params: unknown) => void) + | undefined; } export type ActionExecutor = (actionToExecute: ExecutableAction) => void; @@ -1572,10 +1578,6 @@ function resolveAndExecuteActionsWithContext( > )[typeof action === 'string' ? action : action.type]; - if (!resolvedAction) { - continue; - } - const actionArgs = { context: intermediateSnapshot.context, event, @@ -1601,11 +1603,15 @@ function resolveAndExecuteActionsWithContext( }); } - if (!('resolve' in resolvedAction)) { + if (!resolvedAction || !('resolve' in resolvedAction)) { executeAction(); continue; } + if (!resolvedAction) { + continue; + } + const builtinAction = resolvedAction as BuiltinAction; const [nextState, params, actions] = builtinAction.resolve( @@ -1916,5 +1922,5 @@ export function executeAction( info: ActionArgs = action.info, params: unknown = action.params ) { - return action.function(info, params); + return action.function?.(info, params); } diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 943586cbf9..0a8ca5266f 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -1,4 +1,10 @@ -import { assign, setup, transition } from '../src'; +import { + assign, + createMachine, + enqueueActions, + setup, + transition +} from '../src'; import { executeAction } from '../src/stateUtils'; import { initialTransition } from '../src/transition'; @@ -78,4 +84,22 @@ describe('transition function', () => { msg: 'hello' }); }); + + it('should capture enqueued actions', () => { + const machine = createMachine({ + entry: [ + enqueueActions((x) => { + x.enqueue('stringAction'); + x.enqueue({ type: 'objectAction' }); + }) + ] + }); + + const [_state, actions] = initialTransition(machine); + + expect(actions).toEqual([ + expect.objectContaining({ type: 'stringAction' }), + expect.objectContaining({ type: 'objectAction' }) + ]); + }); }); From 96ba826dcf06c4d310779e60efc96eec25ea03a2 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 4 Jul 2024 19:21:58 -0400 Subject: [PATCH 17/80] Fix type error --- packages/core/src/createActor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index b8a5a74e69..83b1ad9df2 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -219,6 +219,9 @@ export class Actor params: action.params as any // TODO: fix types } }); + if (!action.function) { + return; + } try { executingCustomAction = action.function; executeAction(action); From abf3a2cfde7e639d237db7ee74616bc00fb41d7a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 8 Jul 2024 08:47:19 -0400 Subject: [PATCH 18/80] =?UTF-8?q?Expose=20`executeAction(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/index.ts | 1 + packages/core/src/stateUtils.ts | 14 ++++++++++++++ packages/core/test/transition.test.ts | 4 ++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 680ca1903b..f0445783a4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,6 +12,7 @@ import { createMachine } from './createMachine.ts'; export { getInitialSnapshot, getNextSnapshot } from './getNextSnapshot.ts'; import { Actor, createActor, interpret, Interpreter } from './createActor.ts'; import { StateNode } from './StateNode.ts'; +export { executeAction } from './stateUtils.ts'; // TODO: decide from where those should be exported export { and, not, or, stateIn } from './guards.ts'; export { setup } from './setup.ts'; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index f6599300ec..bb10cafefd 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1917,6 +1917,20 @@ export function convertAction( return action as any; } +/** + * 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: ExecutableAction, info: ActionArgs = action.info, diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 0a8ca5266f..24db1fcdfe 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -3,9 +3,9 @@ import { createMachine, enqueueActions, setup, - transition + transition, + executeAction } from '../src'; -import { executeAction } from '../src/stateUtils'; import { initialTransition } from '../src/transition'; describe('transition function', () => { From d97cb8f035923150b7db80c83c0ef26fea81e26d Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 8 Jul 2024 09:05:43 -0400 Subject: [PATCH 19/80] Provide actor to action --- packages/core/src/createActor.ts | 2 +- packages/core/src/stateUtils.ts | 12 ++++++--- packages/core/test/transition.test.ts | 37 ++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 83b1ad9df2..f0ddcd2521 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -224,7 +224,7 @@ export class Actor } try { executingCustomAction = action.function; - executeAction(action); + executeAction(action, this); } finally { executingCustomAction = false; } diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index bb10cafefd..52b41d6a82 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -39,7 +39,8 @@ import { ProvidedActor, AnyActorScope, NonReducibleUnknown, - UnknownActionObject + UnknownActionObject, + AnyActorRef } from './types.ts'; import { resolveOutput, @@ -1933,8 +1934,13 @@ export function convertAction( */ export function executeAction( action: ExecutableAction, - info: ActionArgs = action.info, + actor: AnyActorRef, params: unknown = action.params ) { - return action.function?.(info, params); + const resolvedInfo = { + ...action.info, + self: actor, + system: actor.system + }; + return action.function?.(resolvedInfo, params); } diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 24db1fcdfe..ed2fc07a99 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -4,7 +4,9 @@ import { enqueueActions, setup, transition, - executeAction + executeAction, + raise, + createActor } from '../src'; import { initialTransition } from '../src/transition'; @@ -57,7 +59,7 @@ describe('transition function', () => { expect(stringAction).not.toHaveBeenCalled(); // Execute actions - actions0.forEach((a) => executeAction(a)); + actions0.forEach((a) => executeAction(a, {} as any)); expect(actionWithParams).toHaveBeenCalledWith(expect.anything(), { a: 1 }); expect(stringAction).toHaveBeenCalled(); @@ -78,7 +80,7 @@ describe('transition function', () => { expect(actionWithDynamicParams).not.toHaveBeenCalled(); // Execute actions - actions1.forEach((a) => executeAction(a)); + actions1.forEach((a) => executeAction(a, {} as any)); expect(actionWithDynamicParams).toHaveBeenCalledWith({ msg: 'hello' @@ -102,4 +104,33 @@ describe('transition function', () => { expect.objectContaining({ type: 'objectAction' }) ]); }); + + it('actor can be specified', () => { + const machine = createMachine({ + entry: (x) => { + x.self.send({ type: 'next' }); + }, + initial: 'a', + states: { + a: { + on: { next: 'b' } + }, + b: {} + } + }); + + const [state, actions] = initialTransition(machine); + + const actor = createActor(machine, { + snapshot: state + }).start(); + + expect(actor.getSnapshot().matches('a')).toBeTruthy(); + + actions.forEach((action) => { + executeAction(action, actor); + }); + + expect(actor.getSnapshot().matches('b')).toBeTruthy(); + }); }); From 5f6a00ba903f03b9dbf1cb2bf4d2d8ca53f3a2da Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 8 Jul 2024 09:07:23 -0400 Subject: [PATCH 20/80] Fix type issue --- packages/core/src/stateUtils.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 52b41d6a82..be53e54f14 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1539,7 +1539,10 @@ export interface ExecutableAction { | undefined; } -export type ActionExecutor = (actionToExecute: ExecutableAction) => void; +export type ActionExecutor = ( + actionToExecute: ExecutableAction, + actorRef: AnyActorRef +) => void; function resolveAndExecuteActionsWithContext( currentSnapshot: AnyMachineSnapshot, @@ -1596,12 +1599,15 @@ function resolveAndExecuteActionsWithContext( : undefined; function executeAction() { - actorScope.actionExecutor({ - type: action.type, - info: actionArgs, - params: actionParams, - function: resolvedAction - }); + actorScope.actionExecutor( + { + type: action.type, + info: actionArgs, + params: actionParams, + function: resolvedAction + }, + actorScope.self + ); } if (!resolvedAction || !('resolve' in resolvedAction)) { From a73a0db4e70c1fb981d759ecf0e1a711f3037042 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 12 Jul 2024 22:13:32 -0400 Subject: [PATCH 21/80] Changeset --- .changeset/lemon-needles-play.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .changeset/lemon-needles-play.md diff --git a/.changeset/lemon-needles-play.md b/.changeset/lemon-needles-play.md new file mode 100644 index 0000000000..7a4db02a9b --- /dev/null +++ b/.changeset/lemon-needles-play.md @@ -0,0 +1,23 @@ +--- +'xstate': minor +--- + +Added a new `transition` function that takes an actor logic, a snapshot, and an event, and returns a tuple containing the next snapshot and the actions to execute. This function is a pure function and does not execute the actions itself. It can be used like this: + +```ts +import { transition } from 'xstate'; + +const [nextState, actions] = transition(actorLogic, currentState, event); +// Execute actions as needed +``` + +Added a new `initialTransition` function that takes an actor logic and an optional input, and returns a tuple containing the initial snapshot and the actions to execute from the initial transition. This function is also a pure function and does not execute the actions itself. It can be used like this: + +```ts +import { initialTransition } from 'xstate'; + +const [initialState, actions] = initialTransition(actorLogic, input); +// Execute actions as needed +``` + +These new functions provide a way to separate the calculation of the next snapshot and actions from the execution of those actions, allowing for more control and flexibility in the transition process. From 4c8d4911c389eb500d9c7d0909a9b24062d73a8e Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 14 Jul 2024 09:18:20 -0400 Subject: [PATCH 22/80] Deprecate getNextSnapshot and getInitialSnapshot --- packages/core/src/getNextSnapshot.ts | 2 + packages/core/src/transition.ts | 20 +++-- packages/core/test/getNextSnapshot.test.ts | 7 +- packages/core/test/transition.test.ts | 93 +++++++++++++++++++++- 4 files changed, 111 insertions(+), 11 deletions(-) diff --git a/packages/core/src/getNextSnapshot.ts b/packages/core/src/getNextSnapshot.ts index a9ada526e2..3e30b1a8f9 100644 --- a/packages/core/src/getNextSnapshot.ts +++ b/packages/core/src/getNextSnapshot.ts @@ -34,6 +34,7 @@ export function createInertActorScope( return inertActorScope; } +/** @deprecated Use `initialTransition(…)` instead. */ export function getInitialSnapshot( actorLogic: T, ...[input]: undefined extends InputFrom @@ -51,6 +52,7 @@ export function getInitialSnapshot( * If the `snapshot` is `undefined`, the initial snapshot of the `actorLogic` is * used. * + * @deprecated Use `transition(…)` instead. * @example * * ```ts diff --git a/packages/core/src/transition.ts b/packages/core/src/transition.ts index df361e00c0..eeffec5871 100644 --- a/packages/core/src/transition.ts +++ b/packages/core/src/transition.ts @@ -1,17 +1,23 @@ import { createInertActorScope } from './getNextSnapshot'; import { ExecutableAction } from './stateUtils'; -import { AnyActorLogic, EventFrom, InputFrom, SnapshotFrom } from './types'; +import { + AnyActorLogic, + EventFrom, + EventFromLogic, + InputFrom, + SnapshotFrom +} from './types'; /** - * Given actor `logic`, a `snapshot`, and an `event`, returns a - * tuple of the `nextSnapshot` and `actions` to execute. + * Given actor `logic`, a `snapshot`, and an `event`, returns a tuple of the + * `nextSnapshot` and `actions` to execute. * * This is a pure function that does not execute `actions`. */ export function transition( logic: T, snapshot: SnapshotFrom, - event: EventFrom + event: EventFromLogic ): [nextSnapshot: SnapshotFrom, actions: ExecutableAction[]] { const executableActions = [] as ExecutableAction[]; @@ -26,9 +32,9 @@ export function transition( } /** - * Given actor `logic` and optional `input`, returns a - * tuple of the `nextSnapshot` and `actions` to execute from the - * initial transition (no previous state). + * Given actor `logic` and optional `input`, returns a tuple of the + * `nextSnapshot` and `actions` to execute from the initial transition (no + * previous state). * * This is a pure function that does not execute `actions`. */ diff --git a/packages/core/test/getNextSnapshot.test.ts b/packages/core/test/getNextSnapshot.test.ts index dcba56b9f2..ec4b85c530 100644 --- a/packages/core/test/getNextSnapshot.test.ts +++ b/packages/core/test/getNextSnapshot.test.ts @@ -4,6 +4,7 @@ import { getNextSnapshot, getInitialSnapshot } from '../src'; +import { initialTransition, transition } from '../src/transition'; describe('getNextSnapshot', () => { it('should calculate the next snapshot for transition logic', () => { @@ -18,10 +19,10 @@ describe('getNextSnapshot', () => { { count: 0 } ); - const init = getInitialSnapshot(logic, undefined); - const s1 = getNextSnapshot(logic, init, { type: 'next' }); + const [init] = initialTransition(logic); + const [s1] = transition(logic, init, { type: 'next' }); expect(s1.context.count).toEqual(1); - const s2 = getNextSnapshot(logic, s1, { type: 'next' }); + const [s2] = transition(logic, s1, { type: 'next' }); expect(s2.context.count).toEqual(2); }); it('should calculate the next snapshot for machine logic', () => { diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index ed2fc07a99..056d235c7d 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -6,7 +6,8 @@ import { transition, executeAction, raise, - createActor + createActor, + fromTransition } from '../src'; import { initialTransition } from '../src/transition'; @@ -133,4 +134,94 @@ describe('transition function', () => { expect(actor.getSnapshot().matches('b')).toBeTruthy(); }); + + // Copied from getSnapshot.test.ts + + it('should calculate the next snapshot for transition logic', () => { + const logic = fromTransition( + (state, event) => { + if (event.type === 'next') { + return { count: state.count + 1 }; + } else { + return state; + } + }, + { count: 0 } + ); + + const [init] = initialTransition(logic); + const [s1] = transition(logic, init, { type: 'next' }); + expect(s1.context.count).toEqual(1); + const [s2] = transition(logic, s1, { type: 'next' }); + expect(s2.context.count).toEqual(2); + }); + + it('should calculate the next snapshot for machine logic', () => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + NEXT: 'b' + } + }, + b: { + on: { + NEXT: 'c' + } + }, + c: {} + } + }); + + const [init] = initialTransition(machine); + const [s1] = transition(machine, init, { type: 'NEXT' }); + + expect(s1.value).toEqual('b'); + + const [s2] = transition(machine, s1, { type: 'NEXT' }); + + expect(s2.value).toEqual('c'); + }); + it('should not execute entry actions', () => { + const fn = jest.fn(); + + const machine = createMachine({ + initial: 'a', + entry: fn, + states: { + a: {}, + b: {} + } + }); + + initialTransition(machine); + + expect(fn).not.toHaveBeenCalled(); + }); + + it('should not execute transition actions', () => { + const fn = jest.fn(); + + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + event: { + target: 'b', + actions: fn + } + } + }, + b: {} + } + }); + + const [init] = initialTransition(machine); + const [nextSnapshot] = transition(machine, init, { type: 'event' }); + + expect(fn).not.toHaveBeenCalled(); + expect(nextSnapshot.value).toEqual('b'); + }); }); From cbb0a7fb8869cb252389214fec589c34d4ac57f3 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 18 Jul 2024 08:11:25 -0500 Subject: [PATCH 23/80] Update packages/core/src/stateUtils.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/core/src/stateUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index c5588cb424..be70d15205 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -52,7 +52,6 @@ import { } from './utils.ts'; import { ProcessingStatus } from './createActor.ts'; -export const defaultActionExecutor: ActionExecutor = executeAction; type StateNodeIterable< TContext extends MachineContext, From 18f0015f66ec7596e9a30ae981053f736b1fafd9 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 18 Jul 2024 08:15:05 -0500 Subject: [PATCH 24/80] Restore getNextSnapshot.test.ts file --- packages/core/test/getNextSnapshot.test.ts | 26 ++++------------------ 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/packages/core/test/getNextSnapshot.test.ts b/packages/core/test/getNextSnapshot.test.ts index ec4b85c530..76759391d2 100644 --- a/packages/core/test/getNextSnapshot.test.ts +++ b/packages/core/test/getNextSnapshot.test.ts @@ -4,7 +4,6 @@ import { getNextSnapshot, getInitialSnapshot } from '../src'; -import { initialTransition, transition } from '../src/transition'; describe('getNextSnapshot', () => { it('should calculate the next snapshot for transition logic', () => { @@ -19,10 +18,10 @@ describe('getNextSnapshot', () => { { count: 0 } ); - const [init] = initialTransition(logic); - const [s1] = transition(logic, init, { type: 'next' }); + const init = getInitialSnapshot(logic, undefined); + const s1 = getNextSnapshot(logic, init, { type: 'next' }); expect(s1.context.count).toEqual(1); - const [s2] = transition(logic, s1, { type: 'next' }); + const s2 = getNextSnapshot(logic, s1, { type: 'next' }); expect(s2.context.count).toEqual(2); }); it('should calculate the next snapshot for machine logic', () => { @@ -52,24 +51,7 @@ describe('getNextSnapshot', () => { expect(s2.value).toEqual('c'); }); - it('should not execute entry actions', () => { - const fn = jest.fn(); - - const machine = createMachine({ - initial: 'a', - entry: fn, - states: { - a: {}, - b: {} - } - }); - - getInitialSnapshot(machine, undefined); - - expect(fn).not.toHaveBeenCalled(); - }); - - it('should not execute transition actions', () => { + it('should not execute actions', () => { const fn = jest.fn(); const machine = createMachine({ From e2e821cb8e0b79974f5d5a5da4821bc9422f6802 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 18 Jul 2024 08:21:36 -0500 Subject: [PATCH 25/80] function -> exec --- packages/core/src/createActor.ts | 4 ++-- packages/core/src/stateUtils.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index ed94a475a5..2b6631270c 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -215,11 +215,11 @@ export class Actor params: action.params as any // TODO: fix types } }); - if (!action.function) { + if (!action.exec) { return; } try { - executingCustomAction = action.function; + executingCustomAction = action.exec; executeAction(action, this); } finally { executingCustomAction = false; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index be70d15205..590f76a524 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -52,7 +52,6 @@ import { } from './utils.ts'; import { ProcessingStatus } from './createActor.ts'; - type StateNodeIterable< TContext extends MachineContext, TE extends EventObject @@ -1525,7 +1524,7 @@ export interface ExecutableAction { type: string; info: ActionArgs; params: NonReducibleUnknown; - function: + exec: | ((info: ActionArgs, params: unknown) => void) | undefined; } @@ -1595,7 +1594,7 @@ function resolveAndExecuteActionsWithContext( type: action.type, info: actionArgs, params: actionParams, - function: resolvedAction + exec: resolvedAction }, actorScope.self ); @@ -1941,5 +1940,5 @@ export function executeAction( self: actor, system: actor.system }; - return action.function?.(resolvedInfo, params); + return action.exec?.(resolvedInfo, params); } From 0c29c2c4be9af9c7535e75b2aaf2a1b6e60d3209 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 6 Aug 2024 23:20:46 -0400 Subject: [PATCH 26/80] Delayed raise action test --- packages/core/src/stateUtils.ts | 20 ++++ packages/core/test/inspect.test.ts | 138 +++++++++++++++----------- packages/core/test/transition.test.ts | 68 ++++++++++++- 3 files changed, 169 insertions(+), 57 deletions(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 1742a780e3..8c32a478be 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1505,6 +1505,7 @@ function exitStates( interface BuiltinAction { (): void; + type: `xstate.${string}`; resolve: ( actorScope: AnyActorScope, snapshot: AnyMachineSnapshot, @@ -1632,6 +1633,15 @@ function resolveAndExecuteActionsWithContext( } if ('execute' in builtinAction) { + actorScope.actionExecutor( + { + type: builtinAction.type, + info: actionArgs, + params, + exec: () => {} // noop + }, + actorScope.self + ); if (actorScope.self._processingStatus === ProcessingStatus.Running) { builtinAction.execute(actorScope, params); } else { @@ -1946,5 +1956,15 @@ export function executeAction( self: actor, system: actor.system }; + if (action.type === 'xstate.raise' && (action.params as any).delay) { + actor.system.scheduler.schedule( + actor, + actor, + (action.params as any).event, + (action.params as any).delay, + (action.params as any).id + ); + return; + } return action.exec?.(resolvedInfo, params); } diff --git a/packages/core/test/inspect.test.ts b/packages/core/test/inspect.test.ts index 4dcd1d1202..fc1cca7b71 100644 --- a/packages/core/test/inspect.test.ts +++ b/packages/core/test/inspect.test.ts @@ -697,64 +697,90 @@ describe('inspect', () => { }).start(); expect(simplifyEvents(events)).toMatchInlineSnapshot(` - [ - { - "actorId": "x:5", - "type": "@xstate.actor", - }, - { - "event": { - "type": "to_b", - }, - "transitions": [ - { - "eventType": "to_b", - "target": [ - "(machine).b", - ], - }, - ], - "type": "@xstate.microstep", - "value": "b", - }, - { - "event": { - "type": "to_c", - }, - "transitions": [ - { - "eventType": "to_c", - "target": [ - "(machine).c", - ], - }, - ], - "type": "@xstate.microstep", - "value": "c", - }, - { - "event": { - "input": undefined, - "type": "xstate.init", - }, - "sourceId": undefined, - "targetId": "x:5", - "type": "@xstate.event", +[ + { + "actorId": "x:5", + "type": "@xstate.actor", + }, + { + "event": { + "type": "to_b", + }, + "transitions": [ + { + "eventType": "to_b", + "target": [ + "(machine).b", + ], + }, + ], + "type": "@xstate.microstep", + "value": "b", + }, + { + "event": { + "type": "to_c", + }, + "transitions": [ + { + "eventType": "to_c", + "target": [ + "(machine).c", + ], + }, + ], + "type": "@xstate.microstep", + "value": "c", + }, + { + "event": { + "input": undefined, + "type": "xstate.init", + }, + "sourceId": undefined, + "targetId": "x:5", + "type": "@xstate.event", + }, + { + "action": { + "params": { + "delay": undefined, + "event": { + "type": "to_b", }, - { - "actorId": "x:5", - "event": { - "input": undefined, - "type": "xstate.init", - }, - "snapshot": { - "value": "c", - }, - "status": "active", - "type": "@xstate.snapshot", + "id": undefined, + }, + "type": "xstate.raise", + }, + "type": "@xstate.action", + }, + { + "action": { + "params": { + "delay": undefined, + "event": { + "type": "to_c", }, - ] - `); + "id": undefined, + }, + "type": "xstate.raise", + }, + "type": "@xstate.action", + }, + { + "actorId": "x:5", + "event": { + "input": undefined, + "type": "xstate.init", + }, + "snapshot": { + "value": "c", + }, + "status": "active", + "type": "@xstate.snapshot", + }, +] +`); }); it('should inspect microsteps for normal transitions', () => { diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 056d235c7d..43f6188e28 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -7,7 +7,8 @@ import { executeAction, raise, createActor, - fromTransition + fromTransition, + waitFor } from '../src'; import { initialTransition } from '../src/transition'; @@ -135,6 +136,71 @@ describe('transition function', () => { expect(actor.getSnapshot().matches('b')).toBeTruthy(); }); + it('Delayed raise actions should be returned', async () => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + entry: raise({ type: 'NEXT' }, { delay: 10 }), + on: { + NEXT: 'b' + } + }, + b: {} + } + }); + + const [state, actions] = initialTransition(machine); + + expect(state.value).toEqual('a'); + + expect(actions[0]).toEqual( + expect.objectContaining({ + type: 'xstate.raise', + params: expect.objectContaining({ + delay: 10, + event: { type: 'NEXT' } + }) + }) + ); + + const actor = createActor(machine, { + snapshot: state + }).start(); + + actions.forEach((action) => { + executeAction(action, actor); + }); + + await waitFor(actor, (s) => s.matches('b')); + }); + + it('Delayed transitions should be returned', () => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + after: { 10: 'b' } + }, + b: {} + } + }); + + const [state, actions] = initialTransition(machine); + + expect(state.value).toEqual('a'); + + expect(actions[0]).toEqual( + expect.objectContaining({ + type: 'xstate.raise', + params: expect.objectContaining({ + delay: 10, + event: { type: 'xstate.after.10.(machine).a' } + }) + }) + ); + }); + // Copied from getSnapshot.test.ts it('should calculate the next snapshot for transition logic', () => { From 6e182960f2c6dc8140e45e1f20b7d7dadab0b217 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 8 Aug 2024 09:02:39 -0400 Subject: [PATCH 27/80] Getting close --- packages/core/src/actions/cancel.ts | 2 +- packages/core/src/actions/raise.ts | 8 ++++---- packages/core/src/stateUtils.ts | 7 ++++++- packages/core/test/interpreter.test.ts | 4 +++- packages/core/test/scxml.test.ts | 2 +- packages/core/test/transition.test.ts | 12 +++++++++++- 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index a1297e7b8e..dac4e6878d 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -35,7 +35,7 @@ function resolveCancel( function executeCancel(actorScope: AnyActorScope, resolvedSendId: string) { actorScope.defer(() => { - actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); + // actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); }); } diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index f63c4bd1e4..30d766c12e 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -87,10 +87,10 @@ function executeRaise( ) { const { event, delay, id } = params; if (typeof delay === 'number') { - actorScope.defer(() => { - const self = actorScope.self; - actorScope.system.scheduler.schedule(self, self, event, delay, id); - }); + // actorScope.defer(() => { + // const self = actorScope.self; + // actorScope.system.scheduler.schedule(self, self, event, delay, id); + // }); return; } } diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 8c32a478be..626560376a 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1956,7 +1956,10 @@ export function executeAction( self: actor, system: actor.system }; - if (action.type === 'xstate.raise' && (action.params as any).delay) { + if ( + action.type === 'xstate.raise' && + (action.params as any).delay !== undefined + ) { actor.system.scheduler.schedule( actor, actor, @@ -1965,6 +1968,8 @@ export function executeAction( (action.params as any).id ); return; + } else if (action.type === 'xstate.cancel') { + actor.system.scheduler.cancel(actor, action.params as string); } return action.exec?.(resolvedInfo, params); } diff --git a/packages/core/test/interpreter.test.ts b/packages/core/test/interpreter.test.ts index eacb255e31..4f2d62ddd9 100644 --- a/packages/core/test/interpreter.test.ts +++ b/packages/core/test/interpreter.test.ts @@ -1316,7 +1316,9 @@ describe('interpreter', () => { expect(typeof intervalService.subscribe === 'function').toBeTruthy(); intervalService.subscribe( - (state) => (count = state.context.count), + (state) => { + count = state.context.count; + }, undefined, () => { expect(count).toEqual(5); diff --git a/packages/core/test/scxml.test.ts b/packages/core/test/scxml.test.ts index a939ce35b4..2366ebaf10 100644 --- a/packages/core/test/scxml.test.ts +++ b/packages/core/test/scxml.test.ts @@ -437,7 +437,7 @@ async function runTestToCompletion( describe('scxml', () => { const onlyTests: string[] = [ // e.g., 'test399.txml' - // 'test175.txml' + 'test208.txml' ]; const testGroupKeys = Object.keys(testGroups); diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 43f6188e28..2c51e5b5fe 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -175,7 +175,7 @@ describe('transition function', () => { await waitFor(actor, (s) => s.matches('b')); }); - it('Delayed transitions should be returned', () => { + it('Delayed transitions should be returned', async () => { const machine = createMachine({ initial: 'a', states: { @@ -199,6 +199,16 @@ describe('transition function', () => { }) }) ); + + const actor = createActor(machine, { + snapshot: state + }).start(); + + actions.forEach((action) => { + executeAction(action, actor); + }); + + await waitFor(actor, (s) => s.matches('b')); }); // Copied from getSnapshot.test.ts From 7146bdc1334927d8131db375ac745ed90f144971 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 9 Aug 2024 01:00:23 -0400 Subject: [PATCH 28/80] Update scheduler to handle delayed sendTo actions without an initially resolved target --- packages/core/src/actions/send.ts | 9 +-------- packages/core/src/stateUtils.ts | 13 +++++++++++++ packages/core/src/system.ts | 13 +++++++++++-- packages/core/test/invoke.test.ts | 14 +++++++++----- packages/core/test/scxml.test.ts | 2 +- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index d2ea9a91a1..9a943c1bae 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -149,13 +149,6 @@ function executeSendTo( actorScope.defer(() => { const { to, event, delay, id } = params; if (typeof delay === 'number') { - actorScope.system.scheduler.schedule( - actorScope.self, - to, - event, - delay, - id - ); return; } actorScope.system._relay( @@ -250,7 +243,7 @@ export function sendTo< } } - sendTo.type = 'xsnapshot.sendTo'; + sendTo.type = 'xstate.sendTo'; sendTo.to = to; sendTo.event = eventOrExpr; sendTo.id = options?.id; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 626560376a..b7ece08452 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1970,6 +1970,19 @@ export function executeAction( return; } else if (action.type === 'xstate.cancel') { actor.system.scheduler.cancel(actor, action.params as string); + return; + } else if ( + action.type === 'xstate.sendTo' && + (action.params as any).delay !== undefined + ) { + actor.system.scheduler.schedule( + actor, + (action.params as any).to, + (action.params as any).event, + (action.params as any).delay, + (action.params as any).id + ); + return; } return action.exec?.(resolvedInfo, params); } diff --git a/packages/core/src/system.ts b/packages/core/src/system.ts index 763c5c5e1e..8e53c7b58c 100644 --- a/packages/core/src/system.ts +++ b/packages/core/src/system.ts @@ -29,7 +29,7 @@ export interface Clock { interface Scheduler { schedule( source: AnyActorRef, - target: AnyActorRef, + target: AnyActorRef | string, event: EventObject, delay: number, id: string | undefined @@ -127,7 +127,16 @@ export function createSystem( delete timerMap[scheduledEventId]; delete system._snapshot._scheduledEvents[scheduledEventId]; - system._relay(source, target, event); + const resolvedTarget = + typeof target === 'string' + ? source.getSnapshot().children[target] + : target; + + if (!resolvedTarget) { + throw new Error(`Actor with id ${target} not found in the system.`); + } + + system._relay(source, resolvedTarget, event); }, delay); timerMap[scheduledEventId] = timeout; diff --git a/packages/core/test/invoke.test.ts b/packages/core/test/invoke.test.ts index 344b67cfe2..880e655ad2 100644 --- a/packages/core/test/invoke.test.ts +++ b/packages/core/test/invoke.test.ts @@ -21,7 +21,8 @@ import { sendParent, Snapshot, ActorRef, - AnyEventObject + AnyEventObject, + waitFor } from '../src/index.ts'; import { sleep } from '@xstate-repo/jest-utils'; @@ -3344,7 +3345,7 @@ describe('invoke', () => { src: child }, entry: sendTo('foo', ({ self }) => ({ type: 'PING', origin: self }), { - delay: 1 + delay: 10 }), on: { PONG: 'c' @@ -3357,10 +3358,13 @@ describe('invoke', () => { }); const actorRef = createActor(machine).start(); + actorRef.system.inspect((e) => { + e; + }); actorRef.send({ type: 'NEXT' }); - await sleep(3); - expect(actorRef.getSnapshot().status).toBe('done'); - }); + + await waitFor(actorRef, (s) => s.status === 'done'); + }, 100000); }); describe('invoke input', () => { diff --git a/packages/core/test/scxml.test.ts b/packages/core/test/scxml.test.ts index 2366ebaf10..6393799714 100644 --- a/packages/core/test/scxml.test.ts +++ b/packages/core/test/scxml.test.ts @@ -437,7 +437,7 @@ async function runTestToCompletion( describe('scxml', () => { const onlyTests: string[] = [ // e.g., 'test399.txml' - 'test208.txml' + // 'test208.txml' ]; const testGroupKeys = Object.keys(testGroups); From 3d76b58ef50d0c3993e73d81ba4a000bf24aa289 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 10 Aug 2024 10:08:27 -0400 Subject: [PATCH 29/80] Fix types --- packages/core/src/stateUtils.ts | 70 ++++++++++++++++++++------------- packages/core/src/system.ts | 7 +++- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index b7ece08452..de862e975f 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1951,38 +1951,52 @@ export function executeAction( actor: AnyActorRef, params: unknown = action.params ) { + const resolvedAction = resolveSpecialAction(action); const resolvedInfo = { ...action.info, self: actor, system: actor.system }; - if ( - action.type === 'xstate.raise' && - (action.params as any).delay !== undefined - ) { - actor.system.scheduler.schedule( - actor, - actor, - (action.params as any).event, - (action.params as any).delay, - (action.params as any).id - ); - return; - } else if (action.type === 'xstate.cancel') { - actor.system.scheduler.cancel(actor, action.params as string); - return; - } else if ( - action.type === 'xstate.sendTo' && - (action.params as any).delay !== undefined - ) { - actor.system.scheduler.schedule( - actor, - (action.params as any).to, - (action.params as any).event, - (action.params as any).delay, - (action.params as any).id - ); - return; + return resolvedAction.exec?.(resolvedInfo, params); +} + +function resolveSpecialAction(action: ExecutableAction): ExecutableAction { + const resolvedAction = { ...action }; + switch (action.type) { + case 'xstate.raise': + if ((action.params as any).delay !== undefined) { + resolvedAction.exec = (info, params) => { + info.system.scheduler.schedule( + info.self, + info.self, + (action.params as any).event, + (action.params as any).delay, + (action.params as any).id + ); + }; + return resolvedAction; + } + break; + case 'xstate.cancel': + resolvedAction.exec = (info, params) => { + info.system.scheduler.cancel(info.self, action.params as string); + }; + return resolvedAction; + case 'xstate.sendTo': + if ((action.params as any).delay !== undefined) { + resolvedAction.exec = (info, params) => { + info.system.scheduler.schedule( + info.self, + (action.params as any).to, + (action.params as any).event, + (action.params as any).delay, + (action.params as any).id + ); + }; + return resolvedAction; + } + break; } - return action.exec?.(resolvedInfo, params); + + return action; } diff --git a/packages/core/src/system.ts b/packages/core/src/system.ts index 8e53c7b58c..03549ca03c 100644 --- a/packages/core/src/system.ts +++ b/packages/core/src/system.ts @@ -18,7 +18,12 @@ interface ScheduledEvent { startedAt: number; // timestamp delay: number; source: AnyActorRef; - target: AnyActorRef; + /** + * The target `ActorRef` of the event. + * + * Can be a `string` (references `snapshot.children[target]`) or an `ActorRef` + */ + target: AnyActorRef | string; } export interface Clock { From 857c96cc226f0dfa1a64fdcc45bb8ebcca2f03c9 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 17 Aug 2024 15:43:10 -0400 Subject: [PATCH 30/80] WIP --- packages/core/src/actions/spawnChild.ts | 4 +- packages/core/src/stateUtils.ts | 8 +-- packages/core/test/transition.test.ts | 90 +++++++++++++++++++++++-- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index 41ec26da2f..fe6f5b8ced 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -102,6 +102,8 @@ function executeSpawn( } actorScope.defer(() => { + actorRef._parent = actorScope.self; + actorRef.system = actorScope.system; if (actorRef._processingStatus === ProcessingStatus.Stopped) { return; } @@ -214,7 +216,7 @@ export function spawnChild< } } - spawnChild.type = 'snapshot.spawnChild'; + spawnChild.type = 'xstate.spawn'; spawnChild.id = id; spawnChild.systemId = systemId; spawnChild.src = src; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index de862e975f..6248fdbce1 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1946,18 +1946,14 @@ export function convertAction( * } * ``` */ -export function executeAction( - action: ExecutableAction, - actor: AnyActorRef, - params: unknown = action.params -) { +export function executeAction(action: ExecutableAction, actor: AnyActorRef) { const resolvedAction = resolveSpecialAction(action); const resolvedInfo = { ...action.info, self: actor, system: actor.system }; - return resolvedAction.exec?.(resolvedInfo, params); + return resolvedAction.exec?.(resolvedInfo, action.params); } function resolveSpecialAction(action: ExecutableAction): ExecutableAction { diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 2c51e5b5fe..bb98ecbdb5 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -8,7 +8,10 @@ import { raise, createActor, fromTransition, - waitFor + waitFor, + EventObject, + fromCallback, + fromPromise } from '../src'; import { initialTransition } from '../src/transition'; @@ -284,13 +287,30 @@ describe('transition function', () => { states: { a: { on: { - event: { - target: 'b', - actions: fn + event: ({ context, check }, enq) => { + enq.action(alwaysDoThisAction); + + if (check({ type: 'blah' })) { + enq.action(doThisOtherAction); + return 'otherState'; + } + + return 'someState'; } } }, - b: {} + b: { + on: { + someEvent: { + guard: ({ context }): context is User => + context.user !== undefined, + actions: ({ context }) => { + context.user; // undefined | User + }, + target: 'a' + } + } + } } }); @@ -300,4 +320,64 @@ describe('transition function', () => { expect(fn).not.toHaveBeenCalled(); expect(nextSnapshot.value).toEqual('b'); }); + + it.only('serverless workflow example', (done) => { + expect.assertions(1); + const db = { + state: undefined as any + }; + + const machine = createMachine({ + initial: 'sendingWelcomeEmail', + states: { + sendingWelcomeEmail: { + invoke: { + src: fromPromise(async () => { + return { id: 1 }; + }), + onDone: 'finish' + } + }, + finish: {} + } + }); + + const logic = fromTransition((_, ev) => { + ev; + }, {}); + + function createProxyActor() { + const actor = createActor(logic).start(); + + return actor; + } + + // POST /workflow + function postStart() { + const [state, actions] = initialTransition(machine); + + // execute actions + actions.forEach((action) => { + executeAction(action, createProxyActor()); + }); + + db.state = state; + } + + // POST /workflow/{sessionId} + async function postEvent(event: EventObject) { + const [nextState, actions] = transition(machine, db.state, event); + + // "sync" built-in actions: assign, raise, cancel, stop + // "external" built-in actions: sendTo, raise w/delay, log + actions.forEach((action) => { + executeAction(action, createProxyActor()); + }); + + db.state = nextState; + done(); + } + + postStart(); + }); }); From 59eedc2aa71e70083ddc6839747842b2f3f9c332 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 28 Aug 2024 17:34:40 -0400 Subject: [PATCH 31/80] Remove test code --- packages/core/test/transition.test.ts | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index bb98ecbdb5..4fe94ca939 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -287,30 +287,13 @@ describe('transition function', () => { states: { a: { on: { - event: ({ context, check }, enq) => { - enq.action(alwaysDoThisAction); - - if (check({ type: 'blah' })) { - enq.action(doThisOtherAction); - return 'otherState'; - } - - return 'someState'; + event: { + target: 'b', + actions: fn } } }, - b: { - on: { - someEvent: { - guard: ({ context }): context is User => - context.user !== undefined, - actions: ({ context }) => { - context.user; // undefined | User - }, - target: 'a' - } - } - } + b: {} } }); From 4cdd545b912f578412cf478919a05f8c8e8cd51d Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 1 Sep 2024 10:50:59 -0400 Subject: [PATCH 32/80] Default actor --- .vscode/launch.json | 2 ++ packages/core/src/stateUtils.ts | 6 +++- packages/core/test/transition.test.ts | 50 ++++++++++++--------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 087b07217d..99afef04ea 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,6 +9,7 @@ "args": ["${file}", "--config", "jest.config.js", "--no-cache"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", + "runtimeExecutable": "node", "windows": { "program": "${workspaceFolder}/node_modules/jest/bin/jest" } @@ -27,6 +28,7 @@ ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", + "runtimeExecutable": "node", "windows": { "program": "${workspaceFolder}/node_modules/jest/bin/jest" } diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 6248fdbce1..5662b53df4 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -51,6 +51,7 @@ import { isErrorActorEvent } from './utils.ts'; import { ProcessingStatus } from './createActor.ts'; +import { createEmptyActor } from './actors/index.ts'; type StateNodeIterable< TContext extends MachineContext, @@ -1946,7 +1947,10 @@ export function convertAction( * } * ``` */ -export function executeAction(action: ExecutableAction, actor: AnyActorRef) { +export function executeAction( + action: ExecutableAction, + actor: AnyActorRef = createEmptyActor() +) { const resolvedAction = resolveSpecialAction(action); const resolvedInfo = { ...action.info, diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 4fe94ca939..848b3a005b 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -11,7 +11,8 @@ import { waitFor, EventObject, fromCallback, - fromPromise + fromPromise, + EventFrom } from '../src'; import { initialTransition } from '../src/transition'; @@ -304,7 +305,7 @@ describe('transition function', () => { expect(nextSnapshot.value).toEqual('b'); }); - it.only('serverless workflow example', (done) => { + it('serverless workflow example', async () => { expect.assertions(1); const db = { state: undefined as any @@ -314,53 +315,46 @@ describe('transition function', () => { initial: 'sendingWelcomeEmail', states: { sendingWelcomeEmail: { - invoke: { - src: fromPromise(async () => { - return { id: 1 }; - }), - onDone: 'finish' - } + entry: async () => { + // send welcome email + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + }, + on: { sent: 'finish' } }, finish: {} } }); - const logic = fromTransition((_, ev) => { - ev; - }, {}); - - function createProxyActor() { - const actor = createActor(logic).start(); - - return actor; - } - // POST /workflow - function postStart() { + async function postStart() { const [state, actions] = initialTransition(machine); // execute actions - actions.forEach((action) => { - executeAction(action, createProxyActor()); - }); + for (const action of actions) { + await executeAction(action); + } db.state = state; } // POST /workflow/{sessionId} - async function postEvent(event: EventObject) { + async function postEvent(event: EventFrom) { const [nextState, actions] = transition(machine, db.state, event); // "sync" built-in actions: assign, raise, cancel, stop // "external" built-in actions: sendTo, raise w/delay, log - actions.forEach((action) => { - executeAction(action, createProxyActor()); - }); + for (const action of actions) { + await executeAction(action); + } db.state = nextState; - done(); } - postStart(); + await postStart(); + postEvent({ type: 'sent' }); + + expect(db.state.value).toBe('finish'); }); }); From d2e583fe11991d4e2bf693c53e8b627df9732ec5 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 1 Sep 2024 10:54:38 -0400 Subject: [PATCH 33/80] Serialization in test --- packages/core/test/transition.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 848b3a005b..986a2490a6 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -336,12 +336,16 @@ describe('transition function', () => { await executeAction(action); } - db.state = state; + db.state = JSON.stringify(state); } // POST /workflow/{sessionId} async function postEvent(event: EventFrom) { - const [nextState, actions] = transition(machine, db.state, event); + const [nextState, actions] = transition( + machine, + machine.resolveState(JSON.parse(db.state)), + event + ); // "sync" built-in actions: assign, raise, cancel, stop // "external" built-in actions: sendTo, raise w/delay, log @@ -349,12 +353,12 @@ describe('transition function', () => { await executeAction(action); } - db.state = nextState; + db.state = JSON.stringify(nextState); } await postStart(); postEvent({ type: 'sent' }); - expect(db.state.value).toBe('finish'); + expect(JSON.parse(db.state).value).toBe('finish'); }); }); From fea607b4ef1ca92df61403ea375660d435ac6929 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 5 Sep 2024 07:39:01 -0400 Subject: [PATCH 34/80] Revert invoke.test.ts --- packages/core/test/invoke.test.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/core/test/invoke.test.ts b/packages/core/test/invoke.test.ts index 880e655ad2..344b67cfe2 100644 --- a/packages/core/test/invoke.test.ts +++ b/packages/core/test/invoke.test.ts @@ -21,8 +21,7 @@ import { sendParent, Snapshot, ActorRef, - AnyEventObject, - waitFor + AnyEventObject } from '../src/index.ts'; import { sleep } from '@xstate-repo/jest-utils'; @@ -3345,7 +3344,7 @@ describe('invoke', () => { src: child }, entry: sendTo('foo', ({ self }) => ({ type: 'PING', origin: self }), { - delay: 10 + delay: 1 }), on: { PONG: 'c' @@ -3358,13 +3357,10 @@ describe('invoke', () => { }); const actorRef = createActor(machine).start(); - actorRef.system.inspect((e) => { - e; - }); actorRef.send({ type: 'NEXT' }); - - await waitFor(actorRef, (s) => s.status === 'done'); - }, 100000); + await sleep(3); + expect(actorRef.getSnapshot().status).toBe('done'); + }); }); describe('invoke input', () => { From 7a33175598af2550be958ac0785d28a6b43cce98 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 5 Sep 2024 07:42:36 -0400 Subject: [PATCH 35/80] Cancel action execution --- packages/core/src/actions/cancel.ts | 5 +---- packages/core/src/stateUtils.ts | 5 ----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index dac4e6878d..4251d90f0f 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -1,7 +1,6 @@ import isDevelopment from '#is-development'; import { AnyActorScope, - AnyActor, AnyMachineSnapshot, EventObject, MachineContext, @@ -34,9 +33,7 @@ function resolveCancel( } function executeCancel(actorScope: AnyActorScope, resolvedSendId: string) { - actorScope.defer(() => { - // actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); - }); + actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); } export interface CancelAction< diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 5662b53df4..4d93a76589 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1977,11 +1977,6 @@ function resolveSpecialAction(action: ExecutableAction): ExecutableAction { return resolvedAction; } break; - case 'xstate.cancel': - resolvedAction.exec = (info, params) => { - info.system.scheduler.cancel(info.self, action.params as string); - }; - return resolvedAction; case 'xstate.sendTo': if ((action.params as any).delay !== undefined) { resolvedAction.exec = (info, params) => { From 0c80c660c743004a6a51c1c15cc0e7c990893231 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 5 Sep 2024 09:25:02 -0400 Subject: [PATCH 36/80] WIP --- packages/core/test/transition.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 986a2490a6..81c8b17911 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -327,13 +327,25 @@ describe('transition function', () => { } }); + // TODO: assigns and raises without timers should not be in the actions + // TODO: example with delayed event + + const proxyActor = createProxyActorForThisServer({ + sendEndpoint: `/postEvent` + }); + // POST /workflow async function postStart() { + const system = createPostgresSystem({ + connectionString: '...', + scheduler: {} + }); + const [state, actions] = initialTransition(machine); // execute actions for (const action of actions) { - await executeAction(action); + await system.executeAction(action); } db.state = JSON.stringify(state); @@ -350,7 +362,7 @@ describe('transition function', () => { // "sync" built-in actions: assign, raise, cancel, stop // "external" built-in actions: sendTo, raise w/delay, log for (const action of actions) { - await executeAction(action); + await machine.executeAction(action); } db.state = JSON.stringify(nextState); From 062676c698af7923b2f6000e99032e7a885e607a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 14 Sep 2024 11:25:14 -0400 Subject: [PATCH 37/80] Update launch.json and jest.config.js --- .vscode/launch.json | 8 ++++---- jest.config.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 99afef04ea..9ad1cb6268 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,20 +5,20 @@ "type": "node", "request": "launch", "name": "Jest Current File", - "program": "${workspaceFolder}/node_modules/.bin/jest", + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", "args": ["${file}", "--config", "jest.config.js", "--no-cache"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "runtimeExecutable": "node", "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest" + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js" } }, { "type": "node", "request": "launch", "name": "Jest Current File (no timeout)", - "program": "${workspaceFolder}/node_modules/.bin/jest", + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", "args": [ "${file}", "--config", @@ -30,7 +30,7 @@ "internalConsoleOptions": "neverOpen", "runtimeExecutable": "node", "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest" + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js" } } ] diff --git a/jest.config.js b/jest.config.js index daef5f79a9..8f17310f08 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,7 @@ const { constants } = require('jest-config'); /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { prettierPath: null, - setupFilesAfterEnv: ['@xstate-repo/jest-utils/setup'], + setupFilesAfterEnv: ['/scripts/jest-utils/setup'], transform: { [constants.DEFAULT_JS_PATTERN]: 'babel-jest', '^.+\\.vue$': '@vue/vue3-jest', From 2d480e1b2dccc694049e297932cf671fb63d3e19 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 14 Sep 2024 11:25:29 -0400 Subject: [PATCH 38/80] Proof of concept for invoked actions --- packages/core/src/StateMachine.ts | 6 ++ packages/core/src/StateNode.ts | 10 +++- packages/core/src/actions/spawnChild.ts | 26 ++++---- packages/core/src/index.ts | 1 - packages/core/src/stateUtils.ts | 12 +++- packages/core/test/transition.test.ts | 79 +++++++++++++++---------- 6 files changed, 87 insertions(+), 47 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index a7247de832..ea38796ba7 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -9,6 +9,8 @@ import { } from './State.ts'; import { StateNode } from './StateNode.ts'; import { + ExecutableAction, + executeAction, getAllStateNodes, getInitialStateNodes, getStateNodeByPath, @@ -633,4 +635,8 @@ export class StateMachine< return restoredSnapshot; } + + public executeAction(action: ExecutableAction, actor?: AnyActorRef) { + return executeAction(action, actor); + } } diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 35f2719afb..0ef635ef85 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -307,13 +307,17 @@ export class StateNode< toArray(this.config.invoke).map((invokeConfig, i) => { const { src, systemId } = invokeConfig; const resolvedId = invokeConfig.id ?? createInvokeId(this.id, i); - const resolvedSrc = + const sourceName = typeof src === 'string' ? src : `xstate.invoke.${createInvokeId(this.id, i)}`; + if (typeof src !== 'string') { + this.machine.implementations.actors[sourceName] = src; + } + return { ...invokeConfig, - src: resolvedSrc, + src: sourceName, id: resolvedId, systemId: systemId, toJSON() { @@ -321,7 +325,7 @@ export class StateNode< return { ...invokeDefValues, type: 'xstate.invoke', - src: resolvedSrc, + src: sourceName, id: resolvedId }; } diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index fe6f5b8ced..b344d6e7cb 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -88,7 +88,8 @@ function resolveSpawn( }), { id, - actorRef + actorRef, + src } ]; } @@ -173,17 +174,18 @@ type SpawnArguments< TExpressionEvent extends EventObject, TEvent extends EventObject, TActor extends ProvidedActor -> = IsLiteralString extends true - ? DistributeActors - : [ - src: string | AnyActorLogic, - options?: { - id?: ResolvableActorId; - systemId?: string; - input?: unknown; - syncSnapshot?: boolean; - } - ]; +> = + IsLiteralString extends true + ? DistributeActors + : [ + src: string | AnyActorLogic, + options?: { + id?: ResolvableActorId; + systemId?: string; + input?: unknown; + syncSnapshot?: boolean; + } + ]; export function spawnChild< TContext extends MachineContext, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 326b8d6dba..104d4f5c93 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,6 @@ export { type Interpreter, type RequiredActorOptionsKeys as RequiredActorOptionsKeys } from './createActor.ts'; -export { executeAction } from './stateUtils.ts'; // TODO: decide from where those should be exported export { createMachine } from './createMachine.ts'; export { getInitialSnapshot, getNextSnapshot } from './getNextSnapshot.ts'; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 4d93a76589..04ce48e4e1 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1529,7 +1529,7 @@ interface BuiltinAction { export interface ExecutableAction { /** The action type */ - type: string; + type: unknown; info: ActionArgs; params: NonReducibleUnknown; exec: @@ -1537,6 +1537,16 @@ export interface ExecutableAction { | undefined; } +export type MachineExecutableActions = { + type: 'xstate.spawn'; + info: ActionArgs; + params: { + id: string; + actorRef: AnyActorRef | undefined; + src: string; + }; +}; + export type ActionExecutor = ( actionToExecute: ExecutableAction, actorRef: AnyActorRef diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 81c8b17911..d9c318ebbf 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -4,7 +4,6 @@ import { enqueueActions, setup, transition, - executeAction, raise, createActor, fromTransition, @@ -12,8 +11,11 @@ import { EventObject, fromCallback, fromPromise, - EventFrom + EventFrom, + AnyActorRef } from '../src'; +import { createDoneActorEvent } from '../src/eventUtils'; +import { ExecutableAction, MachineExecutableActions } from '../src/stateUtils'; import { initialTransition } from '../src/transition'; describe('transition function', () => { @@ -65,7 +67,7 @@ describe('transition function', () => { expect(stringAction).not.toHaveBeenCalled(); // Execute actions - actions0.forEach((a) => executeAction(a, {} as any)); + actions0.forEach((a) => machine.executeAction(a, {} as any)); expect(actionWithParams).toHaveBeenCalledWith(expect.anything(), { a: 1 }); expect(stringAction).toHaveBeenCalled(); @@ -86,7 +88,7 @@ describe('transition function', () => { expect(actionWithDynamicParams).not.toHaveBeenCalled(); // Execute actions - actions1.forEach((a) => executeAction(a, {} as any)); + actions1.forEach((a) => machine.executeAction(a, {} as any)); expect(actionWithDynamicParams).toHaveBeenCalledWith({ msg: 'hello' @@ -134,7 +136,7 @@ describe('transition function', () => { expect(actor.getSnapshot().matches('a')).toBeTruthy(); actions.forEach((action) => { - executeAction(action, actor); + machine.executeAction(action, actor); }); expect(actor.getSnapshot().matches('b')).toBeTruthy(); @@ -173,7 +175,7 @@ describe('transition function', () => { }).start(); actions.forEach((action) => { - executeAction(action, actor); + machine.executeAction(action, actor); }); await waitFor(actor, (s) => s.matches('b')); @@ -209,7 +211,7 @@ describe('transition function', () => { }).start(); actions.forEach((action) => { - executeAction(action, actor); + machine.executeAction(action, actor); }); await waitFor(actor, (s) => s.matches('b')); @@ -306,22 +308,25 @@ describe('transition function', () => { }); it('serverless workflow example', async () => { - expect.assertions(1); const db = { state: undefined as any }; - const machine = createMachine({ + const machine = setup({ + actors: { + sendWelcomeEmail: fromPromise(async () => { + calls.push('sendWelcomeEmail'); + return {}; + }) + } + }).createMachine({ initial: 'sendingWelcomeEmail', states: { sendingWelcomeEmail: { - entry: async () => { - // send welcome email - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - }, - on: { sent: 'finish' } + invoke: { + src: 'sendWelcomeEmail', + onDone: 'finish' + } }, finish: {} } @@ -330,25 +335,37 @@ describe('transition function', () => { // TODO: assigns and raises without timers should not be in the actions // TODO: example with delayed event - const proxyActor = createProxyActorForThisServer({ - sendEndpoint: `/postEvent` - }); + const calls: string[] = []; + + async function execute(action: MachineExecutableActions) { + switch (action.type) { + case 'xstate.spawn': { + if (action.params.src === 'sendWelcomeEmail') { + await new Promise((resolve) => { + setTimeout(() => { + calls.push('sendWelcomeEmail'); + resolve(null); + }, 100); + }); + + postEvent(createDoneActorEvent(action.params.id)); + } + } + default: + break; + } + } // POST /workflow async function postStart() { - const system = createPostgresSystem({ - connectionString: '...', - scheduler: {} - }); - const [state, actions] = initialTransition(machine); + db.state = JSON.stringify(state); + // execute actions for (const action of actions) { - await system.executeAction(action); + await execute(action); } - - db.state = JSON.stringify(state); } // POST /workflow/{sessionId} @@ -359,18 +376,20 @@ describe('transition function', () => { event ); + db.state = JSON.stringify(nextState); + // "sync" built-in actions: assign, raise, cancel, stop // "external" built-in actions: sendTo, raise w/delay, log for (const action of actions) { - await machine.executeAction(action); + await execute(action); } - - db.state = JSON.stringify(nextState); } await postStart(); postEvent({ type: 'sent' }); + expect(calls).toEqual(['sendWelcomeEmail']); + expect(JSON.parse(db.state).value).toBe('finish'); }); }); From a75ceb91db732463844cbd4401bfea28861a2f4c Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 23 Sep 2024 07:15:04 -0400 Subject: [PATCH 39/80] Include resolved input & systemId in spawnChild action --- packages/core/src/actions/spawnChild.ts | 23 ++++++++------- packages/core/test/transition.test.ts | 39 +++++++++++++++---------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index b344d6e7cb..f8a06182e2 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -53,24 +53,25 @@ function resolveSpawn( ? resolveReferencedActor(snapshot.machine, src) : src; const resolvedId = typeof id === 'function' ? id(actionArgs) : id; - let actorRef: AnyActorRef | undefined; + let resolvedInput: unknown | undefined = undefined; if (logic) { + resolvedInput = + typeof input === 'function' + ? input({ + context: snapshot.context, + event: actionArgs.event, + self: actorScope.self + }) + : input; actorRef = createActor(logic, { id: resolvedId, src, parent: actorScope.self, syncSnapshot, systemId, - input: - typeof input === 'function' - ? input({ - context: snapshot.context, - event: actionArgs.event, - self: actorScope.self - }) - : input + input: resolvedInput }); } @@ -88,8 +89,10 @@ function resolveSpawn( }), { id, + systemId, actorRef, - src + src, + input: resolvedInput } ]; } diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index d9c318ebbf..d2eed667e6 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -8,14 +8,12 @@ import { createActor, fromTransition, waitFor, - EventObject, - fromCallback, fromPromise, EventFrom, - AnyActorRef + toPromise } from '../src'; import { createDoneActorEvent } from '../src/eventUtils'; -import { ExecutableAction, MachineExecutableActions } from '../src/stateUtils'; +import { MachineExecutableActions } from '../src/stateUtils'; import { initialTransition } from '../src/transition'; describe('transition function', () => { @@ -316,7 +314,9 @@ describe('transition function', () => { actors: { sendWelcomeEmail: fromPromise(async () => { calls.push('sendWelcomeEmail'); - return {}; + return { + status: 'sent' + }; }) } }).createMachine({ @@ -325,6 +325,13 @@ describe('transition function', () => { sendingWelcomeEmail: { invoke: { src: 'sendWelcomeEmail', + input: () => ({ message: 'hello world', subject: 'hi' }), + onDone: 'logSent' + } + }, + logSent: { + invoke: { + src: fromPromise(async () => {}), onDone: 'finish' } }, @@ -340,16 +347,11 @@ describe('transition function', () => { async function execute(action: MachineExecutableActions) { switch (action.type) { case 'xstate.spawn': { - if (action.params.src === 'sendWelcomeEmail') { - await new Promise((resolve) => { - setTimeout(() => { - calls.push('sendWelcomeEmail'); - resolve(null); - }, 100); - }); - - postEvent(createDoneActorEvent(action.params.id)); - } + const logic = machine.implementations.actors[action.params.src]; + const output = await toPromise( + createActor(logic as any, action.params).start() + ); + postEvent(createDoneActorEvent(action.params.id, output)); } default: break; @@ -390,6 +392,11 @@ describe('transition function', () => { expect(calls).toEqual(['sendWelcomeEmail']); - expect(JSON.parse(db.state).value).toBe('finish'); + await new Promise((res) => { + setTimeout(() => { + expect(JSON.parse(db.state).value).toBe('finish'); + }, 10); + res(); + }); }); }); From 200fc12cad5b66d0dff4ae0b49a97c127589dfb9 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 24 Sep 2024 09:24:27 -0400 Subject: [PATCH 40/80] Refactor action types to use ExecutableActionObject and add startedAt timestamp to raise and send actions --- packages/core/src/StateMachine.ts | 4 +- packages/core/src/actions/raise.ts | 23 ++++++- packages/core/src/actions/send.ts | 8 ++- packages/core/src/stateUtils.ts | 28 +++++--- packages/core/src/transition.ts | 10 +-- packages/core/test/inspect.test.ts | 2 + packages/core/test/transition.test.ts | 94 +++++++++++++++++++++++++-- 7 files changed, 145 insertions(+), 24 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index ea38796ba7..4a16ff6763 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -9,7 +9,7 @@ import { } from './State.ts'; import { StateNode } from './StateNode.ts'; import { - ExecutableAction, + ExecutableActionObject, executeAction, getAllStateNodes, getInitialStateNodes, @@ -636,7 +636,7 @@ export class StateMachine< return restoredSnapshot; } - public executeAction(action: ExecutableAction, actor?: AnyActorRef) { + public executeAction(action: ExecutableActionObject, actor?: AnyActorRef) { return executeAction(action, actor); } } diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index 30d766c12e..8e26320ca1 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -1,5 +1,6 @@ import isDevelopment from '#is-development'; import { executingCustomAction } from '../createActor.ts'; +import { ExecutableActionObject } from '../stateUtils.ts'; import { ActionArgs, ActionFunction, @@ -74,7 +75,15 @@ function resolveRaise( if (typeof resolvedDelay !== 'number') { internalQueue.push(resolvedEvent); } - return [snapshot, { event: resolvedEvent, id, delay: resolvedDelay }]; + return [ + snapshot, + { + event: resolvedEvent, + id, + delay: resolvedDelay, + startedAt: resolvedDelay === undefined ? undefined : Date.now() + } + ]; } function executeRaise( @@ -83,6 +92,7 @@ function executeRaise( event: EventObject; id: string | undefined; delay: number | undefined; + startedAt: number; // timestamp } ) { const { event, delay, id } = params; @@ -161,6 +171,7 @@ export function raise< raise.event = eventOrExpr; raise.id = options?.id; raise.delay = options?.delay; + raise.startedAt = options?.delay === undefined ? undefined : Date.now(); raise.resolve = resolveRaise; raise.execute = executeRaise; @@ -171,3 +182,13 @@ export function raise< return raise; } + +export interface ExecutableRaiseAction extends ExecutableActionObject { + type: 'xstate.raise'; + params: { + event: EventObject; + id: string | undefined; + delay: number | undefined; + startedAt: number | undefined; + }; +} diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 9a943c1bae..e8bfea22ce 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -116,7 +116,13 @@ function resolveSendTo( return [ snapshot, - { to: targetActorRef, event: resolvedEvent, id, delay: resolvedDelay } + { + to: targetActorRef, + event: resolvedEvent, + id, + delay: resolvedDelay, + startedAt: resolvedDelay === undefined ? undefined : Date.now() + } ]; } diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 04ce48e4e1..f1456f8072 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -52,6 +52,7 @@ import { } from './utils.ts'; import { ProcessingStatus } from './createActor.ts'; import { createEmptyActor } from './actors/index.ts'; +import { ExecutableRaiseAction } from './actions/raise.ts'; type StateNodeIterable< TContext extends MachineContext, @@ -1527,9 +1528,8 @@ interface BuiltinAction { execute: (actorScope: AnyActorScope, params: unknown) => void; } -export interface ExecutableAction { - /** The action type */ - type: unknown; +export interface ExecutableActionObject { + type: string; info: ActionArgs; params: NonReducibleUnknown; exec: @@ -1537,7 +1537,7 @@ export interface ExecutableAction { | undefined; } -export type MachineExecutableActions = { +export interface ExecutableSpawnAction extends ExecutableActionObject { type: 'xstate.spawn'; info: ActionArgs; params: { @@ -1545,10 +1545,20 @@ export type MachineExecutableActions = { actorRef: AnyActorRef | undefined; src: string; }; -}; +} + +export type ExecutableAction = ExecutableSpawnAction | ExecutableRaiseAction; +// | { +// type: string & { _: unknown }; +// info: ActionArgs; +// params: NonReducibleUnknown; +// exec: +// | ((info: ActionArgs, params: unknown) => void) +// | undefined; +// }; export type ActionExecutor = ( - actionToExecute: ExecutableAction, + actionToExecute: ExecutableActionObject, actorRef: AnyActorRef ) => void; @@ -1958,7 +1968,7 @@ export function convertAction( * ``` */ export function executeAction( - action: ExecutableAction, + action: ExecutableActionObject, actor: AnyActorRef = createEmptyActor() ) { const resolvedAction = resolveSpecialAction(action); @@ -1970,7 +1980,9 @@ export function executeAction( return resolvedAction.exec?.(resolvedInfo, action.params); } -function resolveSpecialAction(action: ExecutableAction): ExecutableAction { +function resolveSpecialAction( + action: ExecutableActionObject +): ExecutableActionObject { const resolvedAction = { ...action }; switch (action.type) { case 'xstate.raise': diff --git a/packages/core/src/transition.ts b/packages/core/src/transition.ts index eeffec5871..ecdbd2c39b 100644 --- a/packages/core/src/transition.ts +++ b/packages/core/src/transition.ts @@ -1,5 +1,5 @@ import { createInertActorScope } from './getNextSnapshot'; -import { ExecutableAction } from './stateUtils'; +import { ExecutableActionObject } from './stateUtils'; import { AnyActorLogic, EventFrom, @@ -18,8 +18,8 @@ export function transition( logic: T, snapshot: SnapshotFrom, event: EventFromLogic -): [nextSnapshot: SnapshotFrom, actions: ExecutableAction[]] { - const executableActions = [] as ExecutableAction[]; +): [nextSnapshot: SnapshotFrom, actions: ExecutableActionObject[]] { + const executableActions = [] as ExecutableActionObject[]; const actorScope = createInertActorScope(logic); actorScope.actionExecutor = (action) => { @@ -43,8 +43,8 @@ export function initialTransition( ...[input]: undefined extends InputFrom ? [input?: InputFrom] : [input: InputFrom] -): [SnapshotFrom, ExecutableAction[]] { - const executableActions = [] as ExecutableAction[]; +): [SnapshotFrom, ExecutableActionObject[]] { + const executableActions = [] as ExecutableActionObject[]; const actorScope = createInertActorScope(logic); actorScope.actionExecutor = (action) => { diff --git a/packages/core/test/inspect.test.ts b/packages/core/test/inspect.test.ts index fc1cca7b71..070c050b11 100644 --- a/packages/core/test/inspect.test.ts +++ b/packages/core/test/inspect.test.ts @@ -749,6 +749,7 @@ describe('inspect', () => { "type": "to_b", }, "id": undefined, + "startedAt": undefined, }, "type": "xstate.raise", }, @@ -762,6 +763,7 @@ describe('inspect', () => { "type": "to_c", }, "id": undefined, + "startedAt": undefined, }, "type": "xstate.raise", }, diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index d2eed667e6..6c653de6ba 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -10,10 +10,15 @@ import { waitFor, fromPromise, EventFrom, - toPromise + toPromise, + SpawnAction } from '../src'; import { createDoneActorEvent } from '../src/eventUtils'; -import { MachineExecutableActions } from '../src/stateUtils'; +import { + ExecutableAction, + ExecutableActionObject, + ExecutableSpawnAction +} from '../src/stateUtils'; import { initialTransition } from '../src/transition'; describe('transition function', () => { @@ -305,7 +310,81 @@ describe('transition function', () => { expect(nextSnapshot.value).toEqual('b'); }); - it('serverless workflow example', async () => { + it('delayed events example (experimental)', async () => { + const db = { + state: undefined as any + }; + + const machine = createMachine({ + initial: 'start', + states: { + start: { + on: { + next: 'waiting' + } + }, + waiting: { + after: { + 10: 'done' + } + }, + done: { + type: 'final' + } + } + }); + + async function execute(action: ExecutableAction) { + if (action.type === 'xstate.raise' && action.params.delay) { + const currentTime = Date.now(); + const startedAt = action.params.startedAt ?? currentTime; + const elapsed = currentTime - startedAt; + const timeRemaining = Math.max(0, action.params.delay - elapsed); + + await new Promise((res) => setTimeout(res, timeRemaining)); + postEvent(action.params.event); + } + } + + // POST /workflow + async function postStart() { + const [state, actions] = initialTransition(machine); + + db.state = JSON.stringify(state); + + // execute actions + for (const action of actions) { + await execute(action); + } + } + + // POST /workflow/{sessionId} + async function postEvent(event: EventFrom) { + const [nextState, actions] = transition( + machine, + machine.resolveState(JSON.parse(db.state)), + event + ); + + db.state = JSON.stringify(nextState); + + for (const action of actions) { + await execute(action); + } + } + + await postStart(); + postEvent({ type: 'next' }); + + await new Promise((res) => { + setTimeout(() => { + expect(JSON.parse(db.state).status).toBe('done'); + }, 15); + res(); + }); + }); + + it('serverless workflow example (experimental)', async () => { const db = { state: undefined as any }; @@ -344,14 +423,15 @@ describe('transition function', () => { const calls: string[] = []; - async function execute(action: MachineExecutableActions) { + async function execute(action: ExecutableActionObject) { switch (action.type) { case 'xstate.spawn': { - const logic = machine.implementations.actors[action.params.src]; + const spawnAction = action as ExecutableSpawnAction; + const logic = machine.implementations.actors[spawnAction.params.src]; const output = await toPromise( - createActor(logic as any, action.params).start() + createActor(logic as any, spawnAction.params).start() ); - postEvent(createDoneActorEvent(action.params.id, output)); + postEvent(createDoneActorEvent(spawnAction.params.id, output)); } default: break; From 96f4ac42e47d032f78e48bb6edf3943e5b5f36eb Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 26 Sep 2024 09:01:52 -0400 Subject: [PATCH 41/80] Add ExecutableActionsFrom --- packages/core/src/stateUtils.ts | 39 +- packages/core/src/transition.ts | 5 +- packages/core/src/types.ts | 729 ++++++++++++++------------ packages/core/test/transition.test.ts | 11 +- 4 files changed, 403 insertions(+), 381 deletions(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index f1456f8072..2cef3bc561 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -38,9 +38,10 @@ import { AnyTransitionConfig, ProvidedActor, AnyActorScope, - NonReducibleUnknown, UnknownActionObject, - AnyActorRef + AnyActorRef, + ActionExecutor, + ExecutableActionObject } from './types.ts'; import { resolveOutput, @@ -1528,40 +1529,6 @@ interface BuiltinAction { execute: (actorScope: AnyActorScope, params: unknown) => void; } -export interface ExecutableActionObject { - type: string; - info: ActionArgs; - params: NonReducibleUnknown; - exec: - | ((info: ActionArgs, params: unknown) => void) - | undefined; -} - -export interface ExecutableSpawnAction extends ExecutableActionObject { - type: 'xstate.spawn'; - info: ActionArgs; - params: { - id: string; - actorRef: AnyActorRef | undefined; - src: string; - }; -} - -export type ExecutableAction = ExecutableSpawnAction | ExecutableRaiseAction; -// | { -// type: string & { _: unknown }; -// info: ActionArgs; -// params: NonReducibleUnknown; -// exec: -// | ((info: ActionArgs, params: unknown) => void) -// | undefined; -// }; - -export type ActionExecutor = ( - actionToExecute: ExecutableActionObject, - actorRef: AnyActorRef -) => void; - function resolveAndExecuteActionsWithContext( currentSnapshot: AnyMachineSnapshot, event: AnyEventObject, diff --git a/packages/core/src/transition.ts b/packages/core/src/transition.ts index ecdbd2c39b..76106ffd50 100644 --- a/packages/core/src/transition.ts +++ b/packages/core/src/transition.ts @@ -1,11 +1,10 @@ import { createInertActorScope } from './getNextSnapshot'; -import { ExecutableActionObject } from './stateUtils'; import { AnyActorLogic, - EventFrom, EventFromLogic, InputFrom, - SnapshotFrom + SnapshotFrom, + ExecutableActionObject } from './types'; /** diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a1f3e1b88e..b6e41f08e3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -8,7 +8,7 @@ import type { Actor, ProcessingStatus } from './createActor.ts'; import { Spawner } from './spawn.ts'; import { AnyActorSystem, Clock } from './system.js'; import { InspectionEvent } from './inspection.ts'; -import { ActionExecutor } from './stateUtils.ts'; +import { ExecutableRaiseAction } from './actions/raise.ts'; export type Identity = { [K in keyof T]: T[K] }; @@ -62,11 +62,12 @@ export type IndexByProp, P extends keyof T> = { export type IndexByType = IndexByProp; -export type Equals = (() => A extends A2 - ? true - : false) extends () => A extends A1 ? true : false - ? true - : false; +export type Equals = + (() => A extends A2 ? true : false) extends () => A extends A1 + ? true + : false + ? true + : false; export type IsAny = Equals; export type Cast = A extends B ? A : B; // @TODO: Replace with native `NoInfer` when TS issue gets fixed: @@ -128,44 +129,46 @@ export interface ActionArgs< TEvent extends EventObject > extends UnifiedArg {} -export type InputFrom = T extends StateMachine< - infer _TContext, - infer _TEvent, - infer _TChildren, - infer _TActor, - infer _TAction, - infer _TGuard, - infer _TDelay, - infer _TStateValue, - infer _TTag, - infer TInput, - infer _TOutput, - infer _TEmitted, - infer _TMeta, - infer _TStateSchema -> - ? TInput - : T extends ActorLogic< - infer _TSnapshot, - infer _TEvent, - infer TInput, - infer _TSystem, - infer _TEmitted - > +export type InputFrom = + T extends StateMachine< + infer _TContext, + infer _TEvent, + infer _TChildren, + infer _TActor, + infer _TAction, + infer _TGuard, + infer _TDelay, + infer _TStateValue, + infer _TTag, + infer TInput, + infer _TOutput, + infer _TEmitted, + infer _TMeta, + infer _TStateSchema + > ? TInput - : never; + : T extends ActorLogic< + infer _TSnapshot, + infer _TEvent, + infer TInput, + infer _TSystem, + infer _TEmitted + > + ? TInput + : never; -export type OutputFrom = T extends ActorLogic< - infer TSnapshot, - infer _TEvent, - infer _TInput, - infer _TSystem, - infer _TEmitted -> - ? (TSnapshot & { status: 'done' })['output'] - : T extends ActorRef +export type OutputFrom = + T extends ActorLogic< + infer TSnapshot, + infer _TEvent, + infer _TInput, + infer _TSystem, + infer _TEmitted + > ? (TSnapshot & { status: 'done' })['output'] - : never; + : T extends ActorRef + ? (TSnapshot & { status: 'done' })['output'] + : never; export type ActionFunction< TContext extends MachineContext, @@ -771,87 +774,88 @@ export type InvokeConfig< TDelay extends string, TEmitted extends EventObject, TMeta extends MetaObject -> = IsLiteralString extends true - ? DistributeActors< - TContext, - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TEmitted, - TMeta, - TActor - > - : { - /** - * The unique identifier for the invoked machine. If not specified, this - * will be the machine's own `id`, or the URL (from `src`). - */ - id?: string; - - systemId?: string; - /** The source of the machine to be invoked, or the machine itself. */ - src: AnyActorLogic | string; // TODO: fix types - - input?: - | Mapper - | NonReducibleUnknown; - /** - * The transition to take upon the invoked child machine reaching its - * final top-level state. - */ - onDone?: - | string - | SingleOrArray< - TransitionConfigOrTarget< - TContext, - DoneActorEvent, // TODO: consider replacing with `unknown` - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TEmitted, - TMeta - > - >; - /** - * The transition to take upon the invoked child machine sending an error - * event. - */ - onError?: - | string - | SingleOrArray< - TransitionConfigOrTarget< - TContext, - ErrorActorEvent, - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TEmitted, - TMeta - > - >; - - onSnapshot?: - | string - | SingleOrArray< - TransitionConfigOrTarget< - TContext, - SnapshotEvent, - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TEmitted, - TMeta - > - >; - }; +> = + IsLiteralString extends true + ? DistributeActors< + TContext, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted, + TMeta, + TActor + > + : { + /** + * The unique identifier for the invoked machine. If not specified, this + * will be the machine's own `id`, or the URL (from `src`). + */ + id?: string; + + systemId?: string; + /** The source of the machine to be invoked, or the machine itself. */ + src: AnyActorLogic | string; // TODO: fix types + + input?: + | Mapper + | NonReducibleUnknown; + /** + * The transition to take upon the invoked child machine reaching its + * final top-level state. + */ + onDone?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + DoneActorEvent, // TODO: consider replacing with `unknown` + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted, + TMeta + > + >; + /** + * The transition to take upon the invoked child machine sending an + * error event. + */ + onError?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + ErrorActorEvent, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted, + TMeta + > + >; + + onSnapshot?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + SnapshotEvent, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted, + TMeta + > + >; + }; export type AnyInvokeConfig = InvokeConfig< any, @@ -2006,73 +2010,75 @@ export type ActorRefLike = Pick< export type UnknownActorRef = ActorRef, EventObject>; -export type ActorLogicFrom = ReturnTypeOrValue extends infer R - ? R extends StateMachine< - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, // TMeta - any // TStateSchema - > - ? R - : R extends Promise - ? PromiseActorLogic - : never - : never; +export type ActorLogicFrom = + ReturnTypeOrValue extends infer R + ? R extends StateMachine< + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, // TMeta + any // TStateSchema + > + ? R + : R extends Promise + ? PromiseActorLogic + : never + : never; // TODO: in v6, this should only accept AnyActorLogic, like ActorRefFromLogic -export type ActorRefFrom = ReturnTypeOrValue extends infer R - ? R extends StateMachine< - infer TContext, - infer TEvent, - infer TChildren, - infer _TActor, - infer _TAction, - infer _TGuard, - infer _TDelay, - infer TStateValue, - infer TTag, - infer _TInput, - infer TOutput, - infer TEmitted, - infer TMeta, - infer TStateSchema - > - ? ActorRef< - MachineSnapshot< - TContext, - TEvent, - TChildren, - TStateValue, - TTag, - TOutput, - TMeta, - TStateSchema - >, - TEvent, - TEmitted +export type ActorRefFrom = + ReturnTypeOrValue extends infer R + ? R extends StateMachine< + infer TContext, + infer TEvent, + infer TChildren, + infer _TActor, + infer _TAction, + infer _TGuard, + infer _TDelay, + infer TStateValue, + infer TTag, + infer _TInput, + infer TOutput, + infer TEmitted, + infer TMeta, + infer TStateSchema > - : R extends Promise - ? ActorRefFrom> - : R extends ActorLogic< - infer TSnapshot, - infer TEvent, - infer _TInput, - infer _TSystem, - infer TEmitted - > - ? ActorRef - : never - : never; + ? ActorRef< + MachineSnapshot< + TContext, + TEvent, + TChildren, + TStateValue, + TTag, + TOutput, + TMeta, + TStateSchema + >, + TEvent, + TEmitted + > + : R extends Promise + ? ActorRefFrom> + : R extends ActorLogic< + infer TSnapshot, + infer TEvent, + infer _TInput, + infer _TSystem, + infer TEmitted + > + ? ActorRef + : never + : never; export type ActorRefFromLogic = ActorRef< SnapshotFrom, @@ -2085,73 +2091,75 @@ export type DevToolsAdapter = (service: AnyActor) => void; /** @deprecated Use `Actor` instead. */ export type InterpreterFrom< T extends AnyStateMachine | ((...args: any[]) => AnyStateMachine) -> = ReturnTypeOrValue extends StateMachine< - infer TContext, - infer TEvent, - infer TChildren, - infer _TActor, - infer _TAction, - infer _TGuard, - infer _TDelay, - infer TStateValue, - infer TTag, - infer TInput, - infer TOutput, - infer TEmitted, - infer TMeta, - infer TStateSchema -> - ? Actor< - ActorLogic< - MachineSnapshot< - TContext, +> = + ReturnTypeOrValue extends StateMachine< + infer TContext, + infer TEvent, + infer TChildren, + infer _TActor, + infer _TAction, + infer _TGuard, + infer _TDelay, + infer TStateValue, + infer TTag, + infer TInput, + infer TOutput, + infer TEmitted, + infer TMeta, + infer TStateSchema + > + ? Actor< + ActorLogic< + MachineSnapshot< + TContext, + TEvent, + TChildren, + TStateValue, + TTag, + TOutput, + TMeta, + TStateSchema + >, TEvent, - TChildren, - TStateValue, - TTag, - TOutput, - TMeta, - TStateSchema - >, - TEvent, - TInput, - AnyActorSystem, - TEmitted + TInput, + AnyActorSystem, + TEmitted + > > - > - : never; + : never; export type MachineImplementationsFrom< T extends AnyStateMachine | ((...args: any[]) => AnyStateMachine) -> = ReturnTypeOrValue extends StateMachine< - infer TContext, - infer TEvent, - infer _TChildren, - infer TActor, - infer TAction, - infer TGuard, - infer TDelay, - infer _TStateValue, - infer TTag, - infer _TInput, - infer _TOutput, - infer TEmitted, - infer _TMeta, - infer _TStateSchema -> - ? InternalMachineImplementations< - ResolvedStateMachineTypes< - TContext, - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TTag, - TEmitted +> = + ReturnTypeOrValue extends StateMachine< + infer TContext, + infer TEvent, + infer _TChildren, + infer TActor, + infer TAction, + infer TGuard, + infer TDelay, + infer _TStateValue, + infer TTag, + infer _TInput, + infer _TOutput, + infer TEmitted, + infer _TMeta, + infer _TStateSchema + > + ? InternalMachineImplementations< + ResolvedStateMachineTypes< + TContext, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TTag, + TEmitted + > > - > - : never; + : never; export interface ActorScope< TSnapshot extends Snapshot, @@ -2295,28 +2303,29 @@ export type UnknownActorLogic = ActorLogic< any // emitted >; -export type SnapshotFrom = ReturnTypeOrValue extends infer R - ? R extends ActorRef - ? TSnapshot - : R extends Actor - ? SnapshotFrom - : R extends ActorLogic< - infer _TSnapshot, - infer _TEvent, - infer _TInput, - infer _TEmitted, - infer _TSystem - > - ? ReturnType - : R extends ActorScope< - infer TSnapshot, +export type SnapshotFrom = + ReturnTypeOrValue extends infer R + ? R extends ActorRef + ? TSnapshot + : R extends Actor + ? SnapshotFrom + : R extends ActorLogic< + infer _TSnapshot, infer _TEvent, + infer _TInput, infer _TEmitted, infer _TSystem > - ? TSnapshot - : never - : never; + ? ReturnType + : R extends ActorScope< + infer TSnapshot, + infer _TEvent, + infer _TEmitted, + infer _TSystem + > + ? TSnapshot + : never + : never; export type EventFromLogic = TLogic extends ActorLogic< @@ -2340,39 +2349,40 @@ export type EmittedFrom = ? TEmitted : never; -type ResolveEventType = ReturnTypeOrValue extends infer R - ? R extends StateMachine< - infer _TContext, - infer TEvent, - infer _TChildren, - infer _TActor, - infer _TAction, - infer _TGuard, - infer _TDelay, - infer _TStateValue, - infer _TTag, - infer _TInput, - infer _TOutput, - infer _TEmitted, - infer _TMeta, - infer _TStateSchema - > - ? TEvent - : R extends MachineSnapshot< - infer _TContext, - infer TEvent, - infer _TChildren, - infer _TStateValue, - infer _TTag, - infer _TOutput, - infer _TMeta, - infer _TStateSchema - > +type ResolveEventType = + ReturnTypeOrValue extends infer R + ? R extends StateMachine< + infer _TContext, + infer TEvent, + infer _TChildren, + infer _TActor, + infer _TAction, + infer _TGuard, + infer _TDelay, + infer _TStateValue, + infer _TTag, + infer _TInput, + infer _TOutput, + infer _TEmitted, + infer _TMeta, + infer _TStateSchema + > ? TEvent - : R extends ActorRef + : R extends MachineSnapshot< + infer _TContext, + infer TEvent, + infer _TChildren, + infer _TStateValue, + infer _TTag, + infer _TOutput, + infer _TMeta, + infer _TStateSchema + > ? TEvent - : never - : never; + : R extends ActorRef + ? TEvent + : never + : never; export type EventFrom< T, @@ -2380,56 +2390,57 @@ export type EventFrom< TEvent extends EventObject = ResolveEventType > = IsNever extends true ? TEvent : ExtractEvent; -export type ContextFrom = ReturnTypeOrValue extends infer R - ? R extends StateMachine< - infer TContext, - infer _TEvent, - infer _TChildren, - infer _TActor, - infer _TAction, - infer _TGuard, - infer _TDelay, - infer _TStateValue, - infer _TTag, - infer _TInput, - infer _TOutput, - infer _TEmitted, - infer _TMeta, - infer _TStateSchema - > - ? TContext - : R extends MachineSnapshot< - infer TContext, - infer _TEvent, - infer _TChildren, - infer _TStateValue, - infer _TTag, - infer _TOutput, - infer _TMeta, - infer _TStateSchema - > +export type ContextFrom = + ReturnTypeOrValue extends infer R + ? R extends StateMachine< + infer TContext, + infer _TEvent, + infer _TChildren, + infer _TActor, + infer _TAction, + infer _TGuard, + infer _TDelay, + infer _TStateValue, + infer _TTag, + infer _TInput, + infer _TOutput, + infer _TEmitted, + infer _TMeta, + infer _TStateSchema + > ? TContext - : R extends Actor - ? TActorLogic extends StateMachine< + : R extends MachineSnapshot< infer TContext, infer _TEvent, infer _TChildren, - infer _TActor, - infer _TAction, - infer _TGuard, - infer _TDelay, infer _TStateValue, infer _TTag, - infer _TInput, infer _TOutput, - infer _TEmitted, infer _TMeta, infer _TStateSchema > - ? TContext + ? TContext + : R extends Actor + ? TActorLogic extends StateMachine< + infer TContext, + infer _TEvent, + infer _TChildren, + infer _TActor, + infer _TAction, + infer _TGuard, + infer _TDelay, + infer _TStateValue, + infer _TTag, + infer _TInput, + infer _TOutput, + infer _TEmitted, + infer _TMeta, + infer _TStateSchema + > + ? TContext + : never : never - : never - : never; + : never; export type InferEvent = { [T in E['type']]: { type: T } & Extract; @@ -2612,3 +2623,51 @@ export type ToStateValue = T extends { > : never) : {}; + +export interface ExecutableActionObject { + type: string; + info: ActionArgs; + params: NonReducibleUnknown; + exec: + | ((info: ActionArgs, params: unknown) => void) + | undefined; +} + +export interface ExecutableSpawnAction extends ExecutableActionObject { + type: 'xstate.spawn'; + info: ActionArgs; + params: { + id: string; + actorRef: AnyActorRef | undefined; + src: string; + }; +} + +export type SpecialExecutableAction = + | ExecutableSpawnAction + | ExecutableRaiseAction; + +export type ExecutableActionsFrom = + T extends StateMachine< + infer _TContext, + infer _TEvent, + infer _TChildren, + infer _TActor, + infer TAction, + infer _TGuard, + infer _TDelay, + infer _TStateValue, + infer _TTag, + infer _TInput, + infer _TOutput, + infer _TEmitted, + infer _TMeta, + infer _TConfig + > + ? SpecialExecutableAction | { type: unknown; params: unknown } + : never; + +export type ActionExecutor = ( + actionToExecute: ExecutableActionObject, + actorRef: AnyActorRef +) => void; diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 6c653de6ba..014fe14fc8 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -11,14 +11,11 @@ import { fromPromise, EventFrom, toPromise, - SpawnAction + ExecutableActionObject, + ExecutableSpawnAction, + ExecutableActionsFrom } from '../src'; import { createDoneActorEvent } from '../src/eventUtils'; -import { - ExecutableAction, - ExecutableActionObject, - ExecutableSpawnAction -} from '../src/stateUtils'; import { initialTransition } from '../src/transition'; describe('transition function', () => { @@ -334,7 +331,7 @@ describe('transition function', () => { } }); - async function execute(action: ExecutableAction) { + async function execute(action: ExecutableActionsFrom) { if (action.type === 'xstate.raise' && action.params.delay) { const currentTime = Date.now(); const startedAt = action.params.startedAt ?? currentTime; From 9c077d6aaf46262192c886a5c943a92862c6aadc Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 28 Sep 2024 10:54:31 -0400 Subject: [PATCH 42/80] Clean up types --- packages/core/src/transition.ts | 21 +++++++++++++-------- packages/core/src/types.ts | 13 +++++++++++-- packages/core/test/transition.test.ts | 2 +- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/core/src/transition.ts b/packages/core/src/transition.ts index 76106ffd50..6ebce5e9ad 100644 --- a/packages/core/src/transition.ts +++ b/packages/core/src/transition.ts @@ -4,7 +4,9 @@ import { EventFromLogic, InputFrom, SnapshotFrom, - ExecutableActionObject + ExecutableActionObject, + ExecutableActionsFrom, + AnyStateMachine } from './types'; /** @@ -17,12 +19,12 @@ export function transition( logic: T, snapshot: SnapshotFrom, event: EventFromLogic -): [nextSnapshot: SnapshotFrom, actions: ExecutableActionObject[]] { - const executableActions = [] as ExecutableActionObject[]; +): [nextSnapshot: SnapshotFrom, actions: ExecutableActionsFrom[]] { + const executableActions = [] as ExecutableActionsFrom[]; const actorScope = createInertActorScope(logic); actorScope.actionExecutor = (action) => { - executableActions.push(action); + executableActions.push(action as ExecutableActionsFrom); }; const nextSnapshot = logic.transition(snapshot, event, actorScope); @@ -42,15 +44,18 @@ export function initialTransition( ...[input]: undefined extends InputFrom ? [input?: InputFrom] : [input: InputFrom] -): [SnapshotFrom, ExecutableActionObject[]] { - const executableActions = [] as ExecutableActionObject[]; +): [SnapshotFrom, ExecutableActionsFrom[]] { + const executableActions = [] as ExecutableActionsFrom[]; const actorScope = createInertActorScope(logic); actorScope.actionExecutor = (action) => { - executableActions.push(action); + executableActions.push(action as ExecutableActionsFrom); }; - const nextSnapshot = logic.getInitialSnapshot(actorScope, input); + const nextSnapshot = logic.getInitialSnapshot( + actorScope, + input + ) as SnapshotFrom; return [nextSnapshot, executableActions]; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b6e41f08e3..0605b5c2d2 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2633,6 +2633,13 @@ export interface ExecutableActionObject { | undefined; } +export interface ToExecutableAction + extends ExecutableActionObject { + type: T['type']; + params: T['params']; + exec: undefined; +} + export interface ExecutableSpawnAction extends ExecutableActionObject { type: 'xstate.spawn'; info: ActionArgs; @@ -2647,7 +2654,7 @@ export type SpecialExecutableAction = | ExecutableSpawnAction | ExecutableRaiseAction; -export type ExecutableActionsFrom = +export type ExecutableActionsFrom = T extends StateMachine< infer _TContext, infer _TEvent, @@ -2664,7 +2671,9 @@ export type ExecutableActionsFrom = infer _TMeta, infer _TConfig > - ? SpecialExecutableAction | { type: unknown; params: unknown } + ? + | SpecialExecutableAction + | (string extends TAction['type'] ? never : ToExecutableAction) : never; export type ActionExecutor = ( diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 014fe14fc8..d389a4c7d6 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -420,7 +420,7 @@ describe('transition function', () => { const calls: string[] = []; - async function execute(action: ExecutableActionObject) { + async function execute(action: ExecutableActionsFrom) { switch (action.type) { case 'xstate.spawn': { const spawnAction = action as ExecutableSpawnAction; From cf9d549fe2bc9a84dbd148dff9e6a0043596d169 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 9 Oct 2024 08:12:32 -0400 Subject: [PATCH 43/80] Lint --- packages/core/src/StateMachine.ts | 4 ++-- packages/core/src/actions/raise.ts | 15 ++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 355335644f..20036efae5 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -9,7 +9,6 @@ import { } from './State.ts'; import { StateNode } from './StateNode.ts'; import { - ExecutableActionObject, executeAction, getAllStateNodes, getInitialStateNodes, @@ -51,7 +50,8 @@ import type { UnknownActionObject, ResolvedStateMachineTypes, StateSchema, - SnapshotStatus + SnapshotStatus, + ExecutableActionObject } from './types.ts'; import { resolveReferencedActor, toStatePath } from './utils.ts'; diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index 4389e98d80..6499db86e0 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -1,6 +1,5 @@ import isDevelopment from '#is-development'; import { executingCustomAction } from '../createActor.ts'; -import { ExecutableActionObject } from '../stateUtils.ts'; import { ActionArgs, ActionFunction, @@ -10,6 +9,7 @@ import { DelayExpr, DoNotInfer, EventObject, + ExecutableActionObject, MachineContext, ParameterizedObject, RaiseActionOptions, @@ -88,22 +88,15 @@ function resolveRaise( } function executeRaise( - actorScope: AnyActorScope, - params: { + _clock: AnyActorScope, + _params: { event: EventObject; id: string | undefined; delay: number | undefined; startedAt: number; // timestamp } ) { - const { event, delay, id } = params; - if (typeof delay === 'number') { - // actorScope.defer(() => { - // const self = actorScope.self; - // actorScope.system.scheduler.schedule(self, self, event, delay, id); - // }); - return; - } + return; } export interface RaiseAction< From 68654bede4dd65041c702def47bb6cbd90cb3d14 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 9 Oct 2024 08:13:59 -0400 Subject: [PATCH 44/80] Lint for real --- packages/core/src/actions/send.ts | 2 +- packages/core/src/transition.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index bf3ee9751f..c4e9754d2e 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -157,7 +157,7 @@ function executeSendTo( // this forms an outgoing events queue // thanks to that the recipient actors are able to read the *updated* snapshot value of the sender actorScope.defer(() => { - const { to, event, delay, id } = params; + const { to, event, delay, id: _id } = params; if (typeof delay === 'number') { return; } diff --git a/packages/core/src/transition.ts b/packages/core/src/transition.ts index 6ebce5e9ad..6466c9436a 100644 --- a/packages/core/src/transition.ts +++ b/packages/core/src/transition.ts @@ -4,9 +4,7 @@ import { EventFromLogic, InputFrom, SnapshotFrom, - ExecutableActionObject, - ExecutableActionsFrom, - AnyStateMachine + ExecutableActionsFrom } from './types'; /** From 7622a7940d5d1f33118a02e8bae664c1a08b1522 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 9 Oct 2024 08:17:03 -0400 Subject: [PATCH 45/80] Lint lint --- packages/core/src/system.ts | 9 ++++++--- packages/core/src/types.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/system.ts b/packages/core/src/system.ts index 4753f266ef..06d8417772 100644 --- a/packages/core/src/system.ts +++ b/packages/core/src/system.ts @@ -6,7 +6,8 @@ import { Observer, HomomorphicOmit, EventObject, - Subscription + Subscription, + AnyMachineSnapshot } from './types.ts'; import { toObserver } from './utils.ts'; @@ -132,11 +133,13 @@ export function createSystem( const resolvedTarget = typeof target === 'string' - ? source.getSnapshot().children[target] + ? (source.getSnapshot() as AnyMachineSnapshot).children[target] : target; if (!resolvedTarget) { - throw new Error(`Actor with id ${target} not found in the system.`); + throw new Error( + `Actor with id ${typeof target === 'string' ? target : target.sessionId} not found in the system.` + ); } system._relay(source, resolvedTarget, event); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8898d31c30..1fc5f7ca11 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1122,7 +1122,7 @@ export type AnyStateNodeDefinition = StateNodeDefinition; export type AnyMachineSnapshot = MachineSnapshot< any, any, - any, + Record, any, any, any, From 4f61f398e7df8b01bd8dab75665fcea58de7c3d3 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 9 Oct 2024 08:30:45 -0400 Subject: [PATCH 46/80] Back to any --- packages/core/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1fc5f7ca11..8898d31c30 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1122,7 +1122,7 @@ export type AnyStateNodeDefinition = StateNodeDefinition; export type AnyMachineSnapshot = MachineSnapshot< any, any, - Record, + any, any, any, any, From a365418cd0a65fbc95cd3b7f8d1088416bcb31fc Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 9 Oct 2024 10:48:10 -0400 Subject: [PATCH 47/80] Update packages/core/test/transition.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/core/test/transition.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index d389a4c7d6..31bc488733 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -181,7 +181,7 @@ describe('transition function', () => { await waitFor(actor, (s) => s.matches('b')); }); - it('Delayed transitions should be returned', async () => { + it('raise actions related to delayed transitions should be returned', async () => { const machine = createMachine({ initial: 'a', states: { From f6de7682de97d3452d6b247b37125b851996ca91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 12 Oct 2024 00:16:26 +0200 Subject: [PATCH 48/80] use sleep --- packages/core/test/transition.test.ts | 36 +++++++++++---------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 31bc488733..945db22efe 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -1,19 +1,19 @@ +import { sleep } from '@xstate-repo/jest-utils'; import { assign, + createActor, createMachine, enqueueActions, - setup, - transition, - raise, - createActor, - fromTransition, - waitFor, - fromPromise, EventFrom, - toPromise, - ExecutableActionObject, + ExecutableActionsFrom, ExecutableSpawnAction, - ExecutableActionsFrom + fromPromise, + fromTransition, + raise, + setup, + toPromise, + transition, + waitFor } from '../src'; import { createDoneActorEvent } from '../src/eventUtils'; import { initialTransition } from '../src/transition'; @@ -373,12 +373,8 @@ describe('transition function', () => { await postStart(); postEvent({ type: 'next' }); - await new Promise((res) => { - setTimeout(() => { - expect(JSON.parse(db.state).status).toBe('done'); - }, 15); - res(); - }); + await sleep(15); + expect(JSON.parse(db.state).status).toBe('done'); }); it('serverless workflow example (experimental)', async () => { @@ -469,11 +465,7 @@ describe('transition function', () => { expect(calls).toEqual(['sendWelcomeEmail']); - await new Promise((res) => { - setTimeout(() => { - expect(JSON.parse(db.state).value).toBe('finish'); - }, 10); - res(); - }); + await sleep(10); + expect(JSON.parse(db.state).value).toBe('finish'); }); }); From 1977a9d55c4b4ae5357c216f20832c5d1456eea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 12 Oct 2024 00:16:43 +0200 Subject: [PATCH 49/80] remove outdated comment --- packages/core/test/transition.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 945db22efe..9ae11c1865 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -411,9 +411,6 @@ describe('transition function', () => { } }); - // TODO: assigns and raises without timers should not be in the actions - // TODO: example with delayed event - const calls: string[] = []; async function execute(action: ExecutableActionsFrom) { From d202b9321460b3754b6651a30f959847c7becea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 12 Oct 2024 00:19:59 +0200 Subject: [PATCH 50/80] add `ExecutableSendToAction` to `SpecialExecutableAction` --- packages/core/src/actions/send.ts | 12 ++++++++++++ packages/core/src/types.ts | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index c4e9754d2e..6ae2d4957b 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -14,6 +14,7 @@ import { DoNotInfer, EventFrom, EventObject, + ExecutableActionObject, InferEvent, MachineContext, ParameterizedObject, @@ -368,3 +369,14 @@ export function forwardTo< TUsedDelay >(target, ({ event }: any) => event, options); } + +export interface ExecutableSendToAction extends ExecutableActionObject { + type: 'xstate.sendTo'; + params: { + event: EventObject; + id: string | undefined; + delay: number | undefined; + to: AnyActorRef; + startedAt: number | undefined; + }; +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8898d31c30..dc2a955121 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -9,6 +9,7 @@ import { Spawner } from './spawn.ts'; import { AnyActorSystem, Clock } from './system.js'; import { InspectionEvent } from './inspection.ts'; import { ExecutableRaiseAction } from './actions/raise.ts'; +import { ExecutableSendToAction } from './actions/send.ts'; export type Identity = { [K in keyof T]: T[K] }; @@ -2652,7 +2653,8 @@ export interface ExecutableSpawnAction extends ExecutableActionObject { export type SpecialExecutableAction = | ExecutableSpawnAction - | ExecutableRaiseAction; + | ExecutableRaiseAction + | ExecutableSendToAction; export type ExecutableActionsFrom = T extends StateMachine< From 0c344dadfa8d1bf6e7ad6df097d6544fcd5e2d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 12 Oct 2024 00:23:55 +0200 Subject: [PATCH 51/80] remove `startedAt` --- packages/core/src/actions/raise.ts | 6 +----- packages/core/src/actions/send.ts | 4 +--- packages/core/test/inspect.test.ts | 2 -- packages/core/test/transition.test.ts | 2 +- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index 6499db86e0..e98759efa7 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -81,8 +81,7 @@ function resolveRaise( { event: resolvedEvent, id, - delay: resolvedDelay, - startedAt: resolvedDelay === undefined ? undefined : Date.now() + delay: resolvedDelay } ]; } @@ -93,7 +92,6 @@ function executeRaise( event: EventObject; id: string | undefined; delay: number | undefined; - startedAt: number; // timestamp } ) { return; @@ -165,7 +163,6 @@ export function raise< raise.event = eventOrExpr; raise.id = options?.id; raise.delay = options?.delay; - raise.startedAt = options?.delay === undefined ? undefined : Date.now(); raise.resolve = resolveRaise; raise.execute = executeRaise; @@ -183,6 +180,5 @@ export interface ExecutableRaiseAction extends ExecutableActionObject { event: EventObject; id: string | undefined; delay: number | undefined; - startedAt: number | undefined; }; } diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 6ae2d4957b..351cc89174 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -125,8 +125,7 @@ function resolveSendTo( to: targetActorRef, event: resolvedEvent, id, - delay: resolvedDelay, - startedAt: resolvedDelay === undefined ? undefined : Date.now() + delay: resolvedDelay } ]; } @@ -377,6 +376,5 @@ export interface ExecutableSendToAction extends ExecutableActionObject { id: string | undefined; delay: number | undefined; to: AnyActorRef; - startedAt: number | undefined; }; } diff --git a/packages/core/test/inspect.test.ts b/packages/core/test/inspect.test.ts index 070c050b11..fc1cca7b71 100644 --- a/packages/core/test/inspect.test.ts +++ b/packages/core/test/inspect.test.ts @@ -749,7 +749,6 @@ describe('inspect', () => { "type": "to_b", }, "id": undefined, - "startedAt": undefined, }, "type": "xstate.raise", }, @@ -763,7 +762,6 @@ describe('inspect', () => { "type": "to_c", }, "id": undefined, - "startedAt": undefined, }, "type": "xstate.raise", }, diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 9ae11c1865..55e32012f5 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -334,7 +334,7 @@ describe('transition function', () => { async function execute(action: ExecutableActionsFrom) { if (action.type === 'xstate.raise' && action.params.delay) { const currentTime = Date.now(); - const startedAt = action.params.startedAt ?? currentTime; + const startedAt = currentTime; const elapsed = currentTime - startedAt; const timeRemaining = Math.max(0, action.params.delay - elapsed); From ece423d68c449eeb0ae5dee8392e4e10a989ce63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 12 Oct 2024 11:24:33 +0200 Subject: [PATCH 52/80] add failing raise test case --- packages/core/src/actions/raise.ts | 2 +- packages/core/test/actions.test.ts | 93 ++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index e98759efa7..ed5b660dde 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -87,7 +87,7 @@ function resolveRaise( } function executeRaise( - _clock: AnyActorScope, + _actorScope: AnyActorScope, _params: { event: EventObject; id: string | undefined; diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 2976745405..a1ed80b486 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3178,6 +3178,49 @@ describe('sendTo', () => { ] `); }); + + it('a self-event "handler" sent using sendTo should be able to read updated snapshot of self', () => { + const spy = jest.fn(); + const machine = createMachine({ + context: { + counter: 0 + }, + initial: 'a', + states: { + a: { + on: { NEXT: 'b' } + }, + b: { + entry: [ + assign({ counter: 1 }), + sendTo(({ self }) => self, { type: 'EVENT' }) + ], + on: { + EVENT: { + actions: ({ self }) => spy(self.getSnapshot().context), + target: 'c' + } + } + }, + c: {} + } + }); + + const actorRef = createActor(machine).start(); + + actorRef.send({ type: 'NEXT' }); + actorRef.send({ type: 'EVENT' }); + + expect(spy).toMatchMockCallsInlineSnapshot(` +[ + [ + { + "counter": 1, + }, + ], +] +`); + }); }); describe('raise', () => { @@ -3215,7 +3258,7 @@ describe('raise', () => { service.send({ type: 'TO_B' }); }); - it('should be able to send a delayed event to itself with delay = 0', (done) => { + it('should be able to send a delayed event to itself with delay = 0', async () => { const machine = createMachine({ initial: 'a', states: { @@ -3239,11 +3282,9 @@ describe('raise', () => { // The state should not be changed yet; `delay: 0` is equivalent to `setTimeout(..., 0)` expect(service.getSnapshot().value).toEqual('a'); - setTimeout(() => { - // The state should be changed now - expect(service.getSnapshot().value).toEqual('b'); - done(); - }); + await sleep(0); + // The state should be changed now + expect(service.getSnapshot().value).toEqual('b'); }); it('should be able to raise an event and respond to it in the same state', () => { @@ -3384,6 +3425,46 @@ describe('raise', () => { ] `); }); + + it('a raised event "handler" should be able to read updated snapshot of self', () => { + const spy = jest.fn(); + const machine = createMachine({ + context: { + counter: 0 + }, + initial: 'a', + states: { + a: { + on: { NEXT: 'b' } + }, + b: { + entry: [assign({ counter: 1 }), raise({ type: 'EVENT' })], + on: { + EVENT: { + actions: ({ self }) => spy(self.getSnapshot().context), + target: 'c' + } + } + }, + c: {} + } + }); + + const actorRef = createActor(machine).start(); + + actorRef.send({ type: 'NEXT' }); + actorRef.send({ type: 'EVENT' }); + + expect(spy).toMatchMockCallsInlineSnapshot(` +[ + [ + { + "counter": 1, + }, + ], +] +`); + }); }); describe('cancel', () => { From 95766162d3e44473d035f0e04244a6bd11637eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 12 Oct 2024 11:25:32 +0200 Subject: [PATCH 53/80] tweak test title --- packages/core/test/actions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index a1ed80b486..ffcb6f4f0d 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3179,7 +3179,7 @@ describe('sendTo', () => { `); }); - it('a self-event "handler" sent using sendTo should be able to read updated snapshot of self', () => { + it('a self-event "handler" of an event sent using sendTo should be able to read updated snapshot of self', () => { const spy = jest.fn(); const machine = createMachine({ context: { From 85b1f671cc0fe22eac86b4c815885496549f3ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 12 Oct 2024 11:43:59 +0200 Subject: [PATCH 54/80] add extra cancel tests --- packages/core/test/actions.test.ts | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index ffcb6f4f0d..02abe7e32f 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3623,6 +3623,97 @@ describe('cancel', () => { expect(spy.mock.calls.length).toBe(0); }); + + it('should be able to cancel a just scheduled delayed event to a just invoked child', async () => { + const spy = jest.fn(); + + const child = createMachine({ + on: { + PING: { + actions: spy + } + } + }); + + const machine = setup({ + actors: { + child + } + }).createMachine({ + initial: 'a', + states: { + a: { + on: { + START: 'b' + } + }, + b: { + entry: [ + sendTo('myChild', { type: 'PING' }, { id: 'myEvent', delay: 0 }), + cancel('myEvent') + ], + invoke: { + src: 'child', + id: 'myChild' + } + } + } + }); + + const actorRef = createActor(machine).start(); + + actorRef.send({ + type: 'START' + }); + + await sleep(10); + expect(spy.mock.calls.length).toBe(0); + }); + + it('should not be able to cancel a just scheduled non-delayed event to a just invoked child', async () => { + const spy = jest.fn(); + + const child = createMachine({ + on: { + PING: { + actions: spy + } + } + }); + + const machine = setup({ + actors: { + child + } + }).createMachine({ + initial: 'a', + states: { + a: { + on: { + START: 'b' + } + }, + b: { + entry: [ + sendTo('myChild', { type: 'PING' }, { id: 'myEvent' }), + cancel('myEvent') + ], + invoke: { + src: 'child', + id: 'myChild' + } + } + } + }); + + const actorRef = createActor(machine).start(); + + actorRef.send({ + type: 'START' + }); + + expect(spy.mock.calls.length).toBe(1); + }); }); describe('assign action order', () => { From 9d3f2a0c34af696f68b7f1745f280e2a756a6359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 12 Oct 2024 11:55:02 +0200 Subject: [PATCH 55/80] add failing test for invalid event delivery --- packages/core/test/actions.test.ts | 124 +++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 02abe7e32f..662794d894 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -7,6 +7,7 @@ import { raise, sendParent, sendTo, + spawnChild, stopChild } from '../src/actions.ts'; import { CallbackActorRef, fromCallback } from '../src/actors/callback.ts'; @@ -3221,6 +3222,129 @@ describe('sendTo', () => { ] `); }); + + it("should not attempt to deliver a delayed event to the spawned actor's ID that was stopped since the event was scheduled", async () => { + const spy1 = jest.fn(); + + const child1 = createMachine({ + on: { + PING: { + actions: spy1 + } + } + }); + + const spy2 = jest.fn(); + + const child2 = createMachine({ + on: { + PING: { + actions: spy2 + } + } + }); + + const machine = setup({ + actors: { + child1, + child2 + } + }).createMachine({ + initial: 'a', + states: { + a: { + on: { + START: 'b' + } + }, + b: { + entry: [ + spawnChild('child1', { + id: 'myChild' + }), + sendTo('myChild', { type: 'PING' }, { delay: 1 }), + stopChild('myChild'), + spawnChild('child2', { + id: 'myChild' + }) + ] + } + } + }); + + const actorRef = createActor(machine).start(); + + actorRef.send({ type: 'START' }); + + await sleep(10); + + expect(spy1).toHaveBeenCalledTimes(0); + expect(spy2).toHaveBeenCalledTimes(0); + }); + + it("should not attempt to deliver a delayed event to the invoked actor's ID that was stopped since the event was scheduled", async () => { + const spy1 = jest.fn(); + + const child1 = createMachine({ + on: { + PING: { + actions: spy1 + } + } + }); + + const spy2 = jest.fn(); + + const child2 = createMachine({ + on: { + PING: { + actions: spy2 + } + } + }); + + const machine = setup({ + actors: { + child1, + child2 + } + }).createMachine({ + initial: 'a', + states: { + a: { + on: { + START: 'b' + } + }, + b: { + entry: sendTo('myChild', { type: 'PING' }, { delay: 1 }), + invoke: { + src: 'child1', + id: 'myChild' + }, + on: { + NEXT: 'c' + } + }, + c: { + invoke: { + src: 'child2', + id: 'myChild' + } + } + } + }); + + const actorRef = createActor(machine).start(); + + actorRef.send({ type: 'START' }); + actorRef.send({ type: 'NEXT' }); + + await sleep(10); + + expect(spy1).toHaveBeenCalledTimes(0); + expect(spy2).toHaveBeenCalledTimes(0); + }); }); describe('raise', () => { From 45168336221422bb03c7098caaec5628add12cc3 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 13 Oct 2024 12:44:34 -0400 Subject: [PATCH 56/80] Revert test (happens in main) --- packages/core/test/actions.test.ts | 40 ------------------------------ 1 file changed, 40 deletions(-) diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 662794d894..0fda021ccc 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3549,46 +3549,6 @@ describe('raise', () => { ] `); }); - - it('a raised event "handler" should be able to read updated snapshot of self', () => { - const spy = jest.fn(); - const machine = createMachine({ - context: { - counter: 0 - }, - initial: 'a', - states: { - a: { - on: { NEXT: 'b' } - }, - b: { - entry: [assign({ counter: 1 }), raise({ type: 'EVENT' })], - on: { - EVENT: { - actions: ({ self }) => spy(self.getSnapshot().context), - target: 'c' - } - } - }, - c: {} - } - }); - - const actorRef = createActor(machine).start(); - - actorRef.send({ type: 'NEXT' }); - actorRef.send({ type: 'EVENT' }); - - expect(spy).toMatchMockCallsInlineSnapshot(` -[ - [ - { - "counter": 1, - }, - ], -] -`); - }); }); describe('cancel', () => { From ab4bad0d3e32efe93d9cb48e013f8e64472820b1 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 20 Oct 2024 09:50:20 -0400 Subject: [PATCH 57/80] Remove invalid test: The element is used to cancel a delayed event. --- packages/core/test/actions.test.ts | 45 ------------------------------ 1 file changed, 45 deletions(-) diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 0fda021ccc..bbd77fa36b 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3753,51 +3753,6 @@ describe('cancel', () => { await sleep(10); expect(spy.mock.calls.length).toBe(0); }); - - it('should not be able to cancel a just scheduled non-delayed event to a just invoked child', async () => { - const spy = jest.fn(); - - const child = createMachine({ - on: { - PING: { - actions: spy - } - } - }); - - const machine = setup({ - actors: { - child - } - }).createMachine({ - initial: 'a', - states: { - a: { - on: { - START: 'b' - } - }, - b: { - entry: [ - sendTo('myChild', { type: 'PING' }, { id: 'myEvent' }), - cancel('myEvent') - ], - invoke: { - src: 'child', - id: 'myChild' - } - } - } - }); - - const actorRef = createActor(machine).start(); - - actorRef.send({ - type: 'START' - }); - - expect(spy.mock.calls.length).toBe(1); - }); }); describe('assign action order', () => { From 73c6b2281acca689abbd0e6bb6bc853f8c22725a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 21 Oct 2024 13:42:34 +0200 Subject: [PATCH 58/80] Fixed tests --- packages/core/src/actions/cancel.ts | 4 ++- packages/core/src/actions/raise.ts | 13 +++++++-- packages/core/src/actions/send.ts | 11 ++++++-- packages/core/src/createActor.ts | 16 +++++------ packages/core/src/stateUtils.ts | 43 ++++++++--------------------- packages/core/src/system.ts | 25 +++-------------- packages/core/src/types.ts | 5 +--- packages/core/test/actions.test.ts | 2 +- 8 files changed, 48 insertions(+), 71 deletions(-) diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index 703fea2c4f..1af923ecfe 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -33,7 +33,9 @@ function resolveCancel( } function executeCancel(actorScope: AnyActorScope, resolvedSendId: string) { - actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); + actorScope.defer(() => { + actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); + }); } export interface CancelAction< diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index ed5b660dde..74ed0d05ae 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -87,14 +87,21 @@ function resolveRaise( } function executeRaise( - _actorScope: AnyActorScope, - _params: { + actorScope: AnyActorScope, + params: { event: EventObject; id: string | undefined; delay: number | undefined; } ) { - return; + const { event, delay, id } = params; + if (typeof delay === 'number') { + actorScope.defer(() => { + const self = actorScope.self; + actorScope.system.scheduler.schedule(self, self, event, delay, id); + }); + return; + } } export interface RaiseAction< diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 351cc89174..bb38ed803e 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -157,8 +157,15 @@ function executeSendTo( // this forms an outgoing events queue // thanks to that the recipient actors are able to read the *updated* snapshot value of the sender actorScope.defer(() => { - const { to, event, delay, id: _id } = params; + const { to, event, delay, id } = params; if (typeof delay === 'number') { + actorScope.system.scheduler.schedule( + actorScope.self, + to, + event, + delay, + id + ); return; } actorScope.system._relay( @@ -240,7 +247,7 @@ export function sendTo< > { if (isDevelopment && executingCustomAction) { console.warn( - 'Custom actions should not call `raise()` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details.' + 'Custom actions should not call `sendTo()` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details.' ); } diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 24d37a4e6d..67c5356f01 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -8,11 +8,10 @@ import { createInitEvent } from './eventUtils.ts'; import { reportUnhandledError } from './reportUnhandledError.ts'; -import { executeAction } from './stateUtils.ts'; import { symbolObservable } from './symbolObservable.ts'; import { AnyActorSystem, Clock, createSystem } from './system.ts'; -export let executingCustomAction: ((...args: any[]) => void) | false = false; +export let executingCustomAction: boolean = false; import type { ActorScope, @@ -186,11 +185,11 @@ export class Actor if (!listeners && !wildcardListener) { return; } - const allListeners = new Set([ + const allListeners = [ ...(listeners ? listeners.values() : []), ...(wildcardListener ? wildcardListener.values() : []) - ]); - for (const handler of Array.from(allListeners)) { + ]; + for (const handler of allListeners) { handler(emittedEvent); } }, @@ -207,11 +206,12 @@ export class Actor if (!action.exec) { return; } + const saveExecutingCustomAction = executingCustomAction; try { - executingCustomAction = action.exec; - executeAction(action, this); + executingCustomAction = true; + action.exec(action.info, action.params); } finally { - executingCustomAction = false; + executingCustomAction = saveExecutingCustomAction; } }; if (this._processingStatus === ProcessingStatus.Running) { diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index b841d767d6..e33fd8ee06 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1566,24 +1566,13 @@ function resolveAndExecuteActionsWithContext( : action.params : undefined; - function executeAction() { - actorScope.actionExecutor( - { - type: action.type, - info: actionArgs, - params: actionParams, - exec: resolvedAction - }, - actorScope.self - ); - } - if (!resolvedAction || !('resolve' in resolvedAction)) { - executeAction(); - continue; - } - - if (!resolvedAction) { + actorScope.actionExecutor({ + type: action.type, + info: actionArgs, + params: actionParams, + exec: resolvedAction + }); continue; } @@ -1604,20 +1593,12 @@ function resolveAndExecuteActionsWithContext( } if ('execute' in builtinAction) { - actorScope.actionExecutor( - { - type: builtinAction.type, - info: actionArgs, - params, - exec: () => {} // noop - }, - actorScope.self - ); - if (actorScope.self._processingStatus === ProcessingStatus.Running) { - builtinAction.execute(actorScope, params); - } else { - actorScope.defer(builtinAction.execute.bind(null, actorScope, params)); - } + actorScope.actionExecutor({ + type: builtinAction.type, + info: actionArgs, + params, + exec: builtinAction.execute.bind(null, actorScope, params) + }); } if (actions) { diff --git a/packages/core/src/system.ts b/packages/core/src/system.ts index 06d8417772..1ff355cc05 100644 --- a/packages/core/src/system.ts +++ b/packages/core/src/system.ts @@ -6,8 +6,7 @@ import { Observer, HomomorphicOmit, EventObject, - Subscription, - AnyMachineSnapshot + Subscription } from './types.ts'; import { toObserver } from './utils.ts'; @@ -17,12 +16,7 @@ interface ScheduledEvent { startedAt: number; // timestamp delay: number; source: AnyActorRef; - /** - * The target `ActorRef` of the event. - * - * Can be a `string` (references `snapshot.children[target]`) or an `ActorRef` - */ - target: AnyActorRef | string; + target: AnyActorRef; } export interface Clock { @@ -33,7 +27,7 @@ export interface Clock { interface Scheduler { schedule( source: AnyActorRef, - target: AnyActorRef | string, + target: AnyActorRef, event: EventObject, delay: number, id: string | undefined @@ -131,18 +125,7 @@ export function createSystem( delete timerMap[scheduledEventId]; delete system._snapshot._scheduledEvents[scheduledEventId]; - const resolvedTarget = - typeof target === 'string' - ? (source.getSnapshot() as AnyMachineSnapshot).children[target] - : target; - - if (!resolvedTarget) { - throw new Error( - `Actor with id ${typeof target === 'string' ? target : target.sessionId} not found in the system.` - ); - } - - system._relay(source, resolvedTarget, event); + system._relay(source, target, event); }, delay); timerMap[scheduledEventId] = timeout; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index dc2a955121..b410608a64 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2678,7 +2678,4 @@ export type ExecutableActionsFrom = | (string extends TAction['type'] ? never : ToExecutableAction) : never; -export type ActionExecutor = ( - actionToExecute: ExecutableActionObject, - actorRef: AnyActorRef -) => void; +export type ActionExecutor = (actionToExecute: ExecutableActionObject) => void; diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index bbd77fa36b..60c41fcfe6 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -4252,7 +4252,7 @@ describe('actions', () => { "Custom actions should not call \`raise()\` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details.", ], [ - "Custom actions should not call \`raise()\` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details.", + "Custom actions should not call \`sendTo()\` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details.", ], [ "Custom actions should not call \`emit()\` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details.", From 07707e756a1a0c9b93db5c34c3bde9fef002f536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 21 Oct 2024 13:44:29 +0200 Subject: [PATCH 59/80] remove unused import --- packages/core/src/stateUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index e33fd8ee06..4c95bf6a0c 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -51,7 +51,6 @@ import { toTransitionConfigArray, isErrorActorEvent } from './utils.ts'; -import { ProcessingStatus } from './createActor.ts'; import { createEmptyActor } from './actors/index.ts'; type StateNodeIterable< From 898fa9918d4767300051154062bf4b8ae86687b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 21 Oct 2024 14:07:21 +0200 Subject: [PATCH 60/80] add warn assertions --- packages/core/test/actions.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 60c41fcfe6..56c1a044ff 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3280,6 +3280,15 @@ describe('sendTo', () => { expect(spy1).toHaveBeenCalledTimes(0); expect(spy2).toHaveBeenCalledTimes(0); + + expect(console.warn).toMatchMockCallsInlineSnapshot(` +[ + [ + "Event "PING" was sent to stopped actor "myChild (x:113)". This actor has already reached its final state, and will not transition. +Event: {"type":"PING"}", + ], +] +`); }); it("should not attempt to deliver a delayed event to the invoked actor's ID that was stopped since the event was scheduled", async () => { @@ -3344,6 +3353,15 @@ describe('sendTo', () => { expect(spy1).toHaveBeenCalledTimes(0); expect(spy2).toHaveBeenCalledTimes(0); + + expect(console.warn).toMatchMockCallsInlineSnapshot(` +[ + [ + "Event "PING" was sent to stopped actor "myChild (x:116)". This actor has already reached its final state, and will not transition. +Event: {"type":"PING"}", + ], +] +`); }); }); From 71324adeb1397579c189833789474b6bdcbefd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 23 Oct 2024 13:03:00 +0200 Subject: [PATCH 61/80] Revert "Remove invalid test: The element is used to cancel a delayed event." This reverts commit ab4bad0d3e32efe93d9cb48e013f8e64472820b1. --- packages/core/test/actions.test.ts | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 56c1a044ff..11a7c6eacf 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3771,6 +3771,51 @@ describe('cancel', () => { await sleep(10); expect(spy.mock.calls.length).toBe(0); }); + + it('should not be able to cancel a just scheduled non-delayed event to a just invoked child', async () => { + const spy = jest.fn(); + + const child = createMachine({ + on: { + PING: { + actions: spy + } + } + }); + + const machine = setup({ + actors: { + child + } + }).createMachine({ + initial: 'a', + states: { + a: { + on: { + START: 'b' + } + }, + b: { + entry: [ + sendTo('myChild', { type: 'PING' }, { id: 'myEvent' }), + cancel('myEvent') + ], + invoke: { + src: 'child', + id: 'myChild' + } + } + } + }); + + const actorRef = createActor(machine).start(); + + actorRef.send({ + type: 'START' + }); + + expect(spy.mock.calls.length).toBe(1); + }); }); describe('assign action order', () => { From ca2977fb87c847676755d12bd39293e9b353ef9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 25 Oct 2024 22:34:21 +0200 Subject: [PATCH 62/80] make it green, make it green --- packages/core/src/StateMachine.ts | 3 +- packages/core/src/StateNode.ts | 69 ++++---- packages/core/src/actions/assign.ts | 4 - packages/core/src/actions/cancel.ts | 4 - packages/core/src/actions/emit.ts | 4 - packages/core/src/actions/enqueueActions.ts | 16 +- packages/core/src/actions/log.ts | 4 - packages/core/src/actions/raise.ts | 6 +- packages/core/src/actions/send.ts | 6 +- packages/core/src/actions/spawnChild.ts | 8 +- packages/core/src/actions/stopChild.ts | 4 - packages/core/src/index.ts | 3 +- packages/core/src/stateUtils.ts | 175 +++++++------------- packages/core/src/types.ts | 29 +--- packages/core/test/actions.test.ts | 15 ++ packages/core/test/actor.test.ts | 49 ++++++ packages/core/test/guards.test.ts | 42 +++++ packages/core/test/inspect.test.ts | 2 +- packages/core/test/transition.test.ts | 16 +- packages/core/test/utils.ts | 14 +- 20 files changed, 221 insertions(+), 252 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 20036efae5..2cfa601666 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -47,7 +47,6 @@ import type { StateMachineDefinition, StateValue, TransitionDefinition, - UnknownActionObject, ResolvedStateMachineTypes, StateSchema, SnapshotStatus, @@ -389,7 +388,7 @@ export class StateMachine< preInitial, initEvent, actorScope, - [assign(assignment) as unknown as UnknownActionObject], + [assign(assignment)], internalQueue, undefined ) as SnapshotFrom; diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 6948c08de0..5abff6f96a 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -4,7 +4,7 @@ import { NULL_EVENT, STATE_DELIMITER } from './constants.ts'; import { evaluateGuard } from './guards.ts'; import { memo } from './memo.ts'; import { - convertAction, + BuiltinAction, formatInitialTransition, formatTransition, formatTransitions, @@ -31,8 +31,7 @@ import type { AnyStateNodeConfig, ProvidedActor, NonReducibleUnknown, - EventDescriptor, - UnknownActionObject + EventDescriptor } from './types.ts'; import { createInvokeId, @@ -43,6 +42,21 @@ import { const EMPTY_OBJECT = {}; +const toSerializableAction = (action: UnknownAction) => { + if (typeof action === 'string') { + return { type: action }; + } + if (typeof action === 'function') { + if ('resolve' in action) { + return { type: (action as BuiltinAction).type }; + } + return { + type: action.name + }; + } + return action; +}; + interface StateNodeOptions< TContext extends MachineContext, TEvent extends EventObject @@ -85,9 +99,9 @@ export class StateNode< */ public history: false | 'shallow' | 'deep'; /** The action(s) to be executed upon entering the state node. */ - public entry: UnknownActionObject[]; + public entry: UnknownAction[]; /** The action(s) to be executed upon exiting the state node. */ - public exit: UnknownActionObject[]; + public exit: UnknownAction[]; /** The parent state node. */ public parent?: StateNode; /** The root machine node. */ @@ -196,32 +210,8 @@ export class StateNode< this.history = this.config.history === true ? 'shallow' : this.config.history || false; - this.entry = toArray(this.config.entry as UnknownAction).map( - (action, actionIndex) => { - const actionObject = convertAction( - action, - this, - undefined, - 'entry', - 0, - actionIndex - ); - return actionObject; - } - ); - this.exit = toArray(this.config.exit as UnknownAction).map( - (action, actionIndex) => { - const actionObject = convertAction( - action, - this, - undefined, - 'exit', - 0, - actionIndex - ); - return actionObject; - } - ); + this.entry = toArray(this.config.entry).slice(); + this.exit = toArray(this.config.exit).slice(); this.meta = this.config.meta; this.output = @@ -233,8 +223,8 @@ export class StateNode< public _initialize() { this.transitions = formatTransitions(this); if (this.config.always) { - this.always = toTransitionConfigArray(this.config.always).map((t, i) => - formatTransition(this, NULL_EVENT, t, i) + this.always = toTransitionConfigArray(this.config.always).map((t) => + formatTransition(this, NULL_EVENT, t) ); } @@ -254,13 +244,13 @@ export class StateNode< ? { target: this.initial.target, source: this, - actions: this.initial.actions, + actions: this.initial.actions.map(toSerializableAction), eventType: null as any, reenter: false, toJSON: () => ({ target: this.initial.target.map((t) => `#${t.id}`), source: `#${this.id}`, - actions: this.initial.actions, + actions: this.initial.actions.map(toSerializableAction), eventType: null as any }) } @@ -274,8 +264,8 @@ export class StateNode< ...t, actions: t.actions })), - entry: this.entry, - exit: this.exit, + entry: this.entry.map(toSerializableAction), + exit: this.exit.map(toSerializableAction), meta: this.meta, order: this.order || -1, output: this.output, @@ -311,9 +301,6 @@ export class StateNode< typeof src === 'string' ? src : `xstate.invoke.${createInvokeId(this.id, i)}`; - if (typeof src !== 'string') { - this.machine.implementations.actors[sourceName] = src; - } return { ...invokeConfig, @@ -390,7 +377,7 @@ export class StateNode< event: TEvent ): TransitionDefinition[] | undefined { const eventType = event.type; - const actions: UnknownActionObject[] = []; + const actions: UnknownAction[] = []; let selectedTransition: TransitionDefinition | undefined; diff --git a/packages/core/src/actions/assign.ts b/packages/core/src/actions/assign.ts index 515e3c04ae..e4d9bd45dc 100644 --- a/packages/core/src/actions/assign.ts +++ b/packages/core/src/actions/assign.ts @@ -179,9 +179,5 @@ export function assign< assign.resolve = resolveAssign; - assign.toJSON = () => ({ - ...assign - }); - return assign; } diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index 1af923ecfe..96d36993ad 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -102,9 +102,5 @@ export function cancel< cancel.resolve = resolveCancel; cancel.execute = executeCancel; - cancel.toJSON = () => ({ - ...cancel - }); - return cancel; } diff --git a/packages/core/src/actions/emit.ts b/packages/core/src/actions/emit.ts index 4c7ca4b2b5..1fad95846e 100644 --- a/packages/core/src/actions/emit.ts +++ b/packages/core/src/actions/emit.ts @@ -146,9 +146,5 @@ export function emit< emit.resolve = resolveEmit; emit.execute = executeEmit; - emit.toJSON = () => ({ - ...emit - }); - return emit; } diff --git a/packages/core/src/actions/enqueueActions.ts b/packages/core/src/actions/enqueueActions.ts index 8188095d0a..a1d2c83993 100644 --- a/packages/core/src/actions/enqueueActions.ts +++ b/packages/core/src/actions/enqueueActions.ts @@ -1,6 +1,5 @@ import isDevelopment from '#is-development'; import { Guard, evaluateGuard } from '../guards.ts'; -import { convertAction } from '../stateUtils.ts'; import { Action, ActionArgs, @@ -136,16 +135,7 @@ function resolveEnqueueActions( const enqueue: Parameters[0]['enqueue'] = function enqueue( action ) { - actions.push( - convertAction( - action as any, - snapshot.machine.root, - 'enqueue' + Math.random(), // TODO: this should come from state node ID which isn't provided - undefined, - 0, - actions.length - ) - ); + actions.push(action); }; enqueue.assign = (...args) => { actions.push(assign(...args)); @@ -333,9 +323,5 @@ export function enqueueActions< enqueueActions.collect = collect; enqueueActions.resolve = resolveEnqueueActions; - enqueueActions.toJSON = () => ({ - ...enqueueActions - }); - return enqueueActions; } diff --git a/packages/core/src/actions/log.ts b/packages/core/src/actions/log.ts index 5add904afc..a4552a2e49 100644 --- a/packages/core/src/actions/log.ts +++ b/packages/core/src/actions/log.ts @@ -96,9 +96,5 @@ export function log< log.resolve = resolveLog; log.execute = executeLog; - log.toJSON = () => ({ - ...log - }); - return log; } diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index 74ed0d05ae..6f8c76462e 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -86,7 +86,7 @@ function resolveRaise( ]; } -function executeRaise( +export function executeRaise( actorScope: AnyActorScope, params: { event: EventObject; @@ -174,10 +174,6 @@ export function raise< raise.resolve = resolveRaise; raise.execute = executeRaise; - raise.toJSON = () => ({ - ...raise - }); - return raise; } diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index bb38ed803e..4e5060ba41 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -145,7 +145,7 @@ function retryResolveSendTo( } } -function executeSendTo( +export function executeSendTo( actorScope: AnyActorScope, params: { to: AnyActorRef; @@ -270,10 +270,6 @@ export function sendTo< sendTo.retryResolve = retryResolveSendTo; sendTo.execute = executeSendTo; - sendTo.toJSON = () => ({ - ...sendTo - }); - return sendTo; } diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index 3df5f033da..b6f91455c5 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -107,8 +107,6 @@ function executeSpawn( } actorScope.defer(() => { - actorRef._parent = actorScope.self; - actorRef.system = actorScope.system; if (actorRef._processingStatus === ProcessingStatus.Stopped) { return; } @@ -222,7 +220,7 @@ export function spawnChild< } } - spawnChild.type = 'xstate.spawn'; + spawnChild.type = 'xstate.spawnChild'; spawnChild.id = id; spawnChild.systemId = systemId; spawnChild.src = src; @@ -232,9 +230,5 @@ export function spawnChild< spawnChild.resolve = resolveSpawn; spawnChild.execute = executeSpawn; - spawnChild.toJSON = () => ({ - ...spawnChild - }); - return spawnChild; } diff --git a/packages/core/src/actions/stopChild.ts b/packages/core/src/actions/stopChild.ts index 0f764ff813..18b7f48c31 100644 --- a/packages/core/src/actions/stopChild.ts +++ b/packages/core/src/actions/stopChild.ts @@ -115,10 +115,6 @@ export function stopChild< stop.resolve = resolveStop; stop.execute = executeStop; - stop.toJSON = () => ({ - ...stop - }); - return stop; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 104d4f5c93..3690d330e6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,6 @@ export { type Interpreter, type RequiredActorOptionsKeys as RequiredActorOptionsKeys } from './createActor.ts'; -// TODO: decide from where those should be exported export { createMachine } from './createMachine.ts'; export { getInitialSnapshot, getNextSnapshot } from './getNextSnapshot.ts'; export { and, not, or, stateIn } from './guards.ts'; @@ -34,7 +33,7 @@ export { pathToStateValue, toObserver } from './utils.ts'; -export { transition } from './transition.ts'; +export { transition, initialTransition } from './transition.ts'; export { waitFor } from './waitFor.ts'; declare global { diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 4c95bf6a0c..651b9aaaf5 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -38,7 +38,6 @@ import { AnyTransitionConfig, ProvidedActor, AnyActorScope, - UnknownActionObject, AnyActorRef, ActionExecutor, ExecutableActionObject @@ -52,6 +51,8 @@ import { 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, @@ -287,10 +288,10 @@ export function getDelayedTransitions( stateNode.entry.push( raise(afterEvent, { id: eventType, - delay: delay as any // TODO: fix types - }) as unknown as UnknownActionObject + delay + }) ); - stateNode.exit.push(cancel(eventType) as unknown as UnknownActionObject); + stateNode.exit.push(cancel(eventType)); return eventType; }; @@ -308,14 +309,13 @@ export function getDelayedTransitions( delay: resolvedDelay })); }); - return delayedTransitions.map((delayedTransition, i) => { + return delayedTransitions.map((delayedTransition) => { const { delay } = delayedTransition; return { ...formatTransition( stateNode, delayedTransition.event, - delayedTransition, - i + delayedTransition ), delay }; @@ -325,8 +325,7 @@ export function getDelayedTransitions( export function formatTransition( stateNode: AnyStateNode, descriptor: string, - transitionConfig: AnyTransitionConfig, - transitionIndex: number + transitionConfig: AnyTransitionConfig ): AnyTransitionDefinition { const normalizedTarget = normalizeTarget(transitionConfig.target); const reenter = transitionConfig.reenter ?? false; @@ -341,16 +340,7 @@ export function formatTransition( const transition = { ...transitionConfig, - actions: toArray(transitionConfig.actions).map((action, actionIndex) => { - return convertAction( - action, - stateNode, - descriptor, - undefined, - transitionIndex, - actionIndex - ); - }), + actions: toArray(transitionConfig.actions), guard: transitionConfig.guard as never, target, source: stateNode, @@ -386,8 +376,8 @@ export function formatTransitions< const transitionsConfig = stateNode.config.on[descriptor]; transitions.set( descriptor, - toTransitionConfigArray(transitionsConfig).map((t, i) => - formatTransition(stateNode, descriptor, t, i) + toTransitionConfigArray(transitionsConfig).map((t) => + formatTransition(stateNode, descriptor, t) ) ); } @@ -396,8 +386,8 @@ export function formatTransitions< const descriptor = `xstate.done.state.${stateNode.id}`; transitions.set( descriptor, - toTransitionConfigArray(stateNode.config.onDone).map((t, i) => - formatTransition(stateNode, descriptor, t, i) + toTransitionConfigArray(stateNode.config.onDone).map((t) => + formatTransition(stateNode, descriptor, t) ) ); } @@ -406,8 +396,8 @@ export function formatTransitions< const descriptor = `xstate.done.actor.${invokeDef.id}`; transitions.set( descriptor, - toTransitionConfigArray(invokeDef.onDone).map((t, i) => - formatTransition(stateNode, descriptor, t, i) + toTransitionConfigArray(invokeDef.onDone).map((t) => + formatTransition(stateNode, descriptor, t) ) ); } @@ -415,8 +405,8 @@ export function formatTransitions< const descriptor = `xstate.error.actor.${invokeDef.id}`; transitions.set( descriptor, - toTransitionConfigArray(invokeDef.onError).map((t, i) => - formatTransition(stateNode, descriptor, t, i) + toTransitionConfigArray(invokeDef.onError).map((t) => + formatTransition(stateNode, descriptor, t) ) ); } @@ -424,8 +414,8 @@ export function formatTransitions< const descriptor = `xstate.snapshot.${invokeDef.id}`; transitions.set( descriptor, - toTransitionConfigArray(invokeDef.onSnapshot).map((t, i) => - formatTransition(stateNode, descriptor, t, i) + toTransitionConfigArray(invokeDef.onSnapshot).map((t) => + formatTransition(stateNode, descriptor, t) ) ); } @@ -466,11 +456,7 @@ export function formatInitialTransition< const transition: InitialTransitionDefinition = { source: stateNode, actions: - !_target || typeof _target === 'string' - ? [] - : toArray(_target.actions).map((action) => - convertAction(action, stateNode, 'xstate.init', undefined, 0, 0) - ), + !_target || typeof _target === 'string' ? [] : toArray(_target.actions), eventType: null as any, reenter: false, target: resolvedTarget ? [resolvedTarget] : [], @@ -1155,7 +1141,7 @@ function enterStates( (a, b) => a.order - b.order )) { mutStateNodeSet.add(stateNodeToEnter); - const actions: UnknownActionObject[] = []; + const actions: UnknownAction[] = []; // Add entry actions actions.push(...stateNodeToEnter.entry); @@ -1165,7 +1151,7 @@ function enterStates( spawnChild(invokeDef.src, { ...invokeDef, syncSnapshot: !!invokeDef.onSnapshot - }) as unknown as UnknownActionObject + }) ); } @@ -1474,12 +1460,7 @@ function exitStates( nextSnapshot, event, actorScope, - [ - ...s.exit, - ...(s.invoke.map((def) => - stopChild(def.id) - ) as unknown as UnknownActionObject[]) - ], + [...s.exit, ...s.invoke.map((def) => stopChild(def.id))], internalQueue, undefined ); @@ -1488,7 +1469,7 @@ function exitStates( return [nextSnapshot, changedHistory || historyValue] as const; } -interface BuiltinAction { +export interface BuiltinAction { (): void; type: `xstate.${string}`; resolve: ( @@ -1501,7 +1482,7 @@ interface BuiltinAction { ) => [ newState: AnyMachineSnapshot, params: unknown, - actions?: UnknownActionObject[] + actions?: UnknownAction[] ]; retryResolve: ( actorScope: AnyActorScope, @@ -1515,7 +1496,7 @@ function resolveAndExecuteActionsWithContext( currentSnapshot: AnyMachineSnapshot, event: AnyEventObject, actorScope: AnyActorScope, - actions: UnknownActionObject[], + actions: UnknownAction[], extra: { internalQueue: AnyEventObject[]; deferredActorIds: string[] | undefined; @@ -1567,7 +1548,12 @@ function resolveAndExecuteActionsWithContext( if (!resolvedAction || !('resolve' in resolvedAction)) { actorScope.actionExecutor({ - type: action.type, + type: + typeof action === 'string' + ? action + : typeof action === 'object' + ? action.type + : action.name || '(anonymous)', info: actionArgs, params: actionParams, exec: resolvedAction @@ -1619,7 +1605,7 @@ export function resolveActionsAndContext( currentSnapshot: AnyMachineSnapshot, event: AnyEventObject, actorScope: AnyActorScope, - actions: UnknownActionObject[], + actions: UnknownAction[], internalQueue: AnyEventObject[], deferredActorIds: string[] | undefined ): AnyMachineSnapshot { @@ -1772,9 +1758,7 @@ function stopChildren( nextState, event, actorScope, - Object.values(nextState.children).map( - (child: any) => stopChild(child) as unknown as UnknownActionObject - ), + Object.values(nextState.children).map((child: any) => stopChild(child)), [], undefined ); @@ -1834,29 +1818,6 @@ export function resolveStateValue( return getStateValue(rootNode, [...allStateNodes]); } -export function convertAction( - action: UnknownAction, - stateNode: AnyStateNode, - descriptor: string | undefined, - kind: 'entry' | 'exit' | undefined, - transitionIndex: number, - actionIndex: number -): UnknownActionObject { - if (typeof action === 'string') { - return { type: action }; - } - if (typeof action === 'function' && !('resolve' in action)) { - const type = `${stateNode.id}|${ - descriptor ?? kind - }:${transitionIndex}:${actionIndex}`; - stateNode.machine.implementations.actions[type] = action as any; - return { - type - }; - } - return action as any; -} - /** * Runs an executable action. Executable actions are returned from the * `transition(…)` function. @@ -1876,49 +1837,31 @@ export function executeAction( action: ExecutableActionObject, actor: AnyActorRef = createEmptyActor() ) { - const resolvedAction = resolveSpecialAction(action); - const resolvedInfo = { - ...action.info, - self: actor, - system: actor.system - }; - return resolvedAction.exec?.(resolvedInfo, action.params); -} + 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; + } -function resolveSpecialAction( - action: ExecutableActionObject -): ExecutableActionObject { - const resolvedAction = { ...action }; - switch (action.type) { - case 'xstate.raise': - if ((action.params as any).delay !== undefined) { - resolvedAction.exec = (info, _params) => { - info.system.scheduler.schedule( - info.self, - info.self, - (action.params as any).event, - (action.params as any).delay, - (action.params as any).id - ); - }; - return resolvedAction; - } - break; - case 'xstate.sendTo': - if ((action.params as any).delay !== undefined) { - resolvedAction.exec = (info, _params) => { - info.system.scheduler.schedule( - info.self, - (action.params as any).to, - (action.params as any).event, - (action.params as any).delay, - (action.params as any).id - ); - }; - return resolvedAction; - } - break; + action.exec?.( + { + ...action.info, + self: actor, + system: actor.system + }, + action.params + ); + } finally { + actorScope.defer = defer; } - - return action; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b410608a64..f7433276d8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -251,23 +251,6 @@ export type Action< TEmitted >; -export type ActionObject< - TContext extends MachineContext, - TExpressionEvent extends EventObject, - TAction extends ParameterizedObject -> = { - type: string; -} & ( // this way we could iterate over `TAction` (and `TGuard` in the `Guard` type) once and not twice // TODO: consider merging `NoRequiredParams` and `WithDynamicParams` into one - | NoRequiredParams - | WithDynamicParams -); - -export type UnknownActionObject = ActionObject< - MachineContext, - EventObject, - ParameterizedObject ->; - export type UnknownAction = Action< MachineContext, EventObject, @@ -1079,8 +1062,8 @@ export interface StateNodeDefinition< on: TransitionDefinitionMap; transitions: Array>; // TODO: establish what a definition really is - entry: UnknownActionObject[]; - exit: UnknownActionObject[]; + entry: UnknownAction[]; + exit: UnknownAction[]; meta: any; order: number; output?: StateNodeConfig< @@ -1685,14 +1668,14 @@ export interface TransitionDefinition< > { target: ReadonlyArray> | undefined; source: StateNode; - actions: readonly UnknownActionObject[]; + actions: readonly UnknownAction[]; reenter: boolean; guard?: UnknownGuard; eventType: EventDescriptor; toJSON: () => { target: string[] | undefined; source: string; - actions: readonly UnknownActionObject[]; + actions: readonly UnknownAction[]; guard?: UnknownGuard; eventType: EventDescriptor; meta?: Record; @@ -2642,12 +2625,12 @@ export interface ToExecutableAction } export interface ExecutableSpawnAction extends ExecutableActionObject { - type: 'xstate.spawn'; + type: 'xstate.spawnChild'; info: ActionArgs; params: { id: string; actorRef: AnyActorRef | undefined; - src: string; + src: string | AnyActorLogic; }; } diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 11a7c6eacf..3a8213337b 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -4323,4 +4323,19 @@ describe('actions', () => { ] `); }); + + it('inline actions should not leak into provided actions object', async () => { + const actions = {}; + + const machine = createMachine( + { + entry: () => {} + }, + { actions } + ); + + createActor(machine).start(); + + expect(actions).toEqual({}); + }); }); diff --git a/packages/core/test/actor.test.ts b/packages/core/test/actor.test.ts index d21488fc00..65ff942554 100644 --- a/packages/core/test/actor.test.ts +++ b/packages/core/test/actor.test.ts @@ -1753,4 +1753,53 @@ describe('actors', () => { actor.start(); actor.send({ type: 'event' }); }); + + it('same-position invokes should not leak between machines', async () => { + const spy = jest.fn(); + + const sharedActors = {}; + + const m1 = createMachine( + { + invoke: { + src: fromPromise(async () => 'foo'), + onDone: { + actions: ({ event }) => spy(event.output) + } + } + }, + { actors: sharedActors } + ); + + createMachine( + { + invoke: { src: fromPromise(async () => 100) } + }, + { actors: sharedActors } + ); + + createActor(m1).start(); + + await sleep(1); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('foo'); + }); + + it('inline invokes should not leak into provided actors object', async () => { + const actors = {}; + + const machine = createMachine( + { + invoke: { + src: fromPromise(async () => 'foo') + } + }, + { actors } + ); + + createActor(machine).start(); + + expect(actors).toEqual({}); + }); }); diff --git a/packages/core/test/guards.test.ts b/packages/core/test/guards.test.ts index 1fdadf4e5f..099b6a6063 100644 --- a/packages/core/test/guards.test.ts +++ b/packages/core/test/guards.test.ts @@ -1010,6 +1010,48 @@ describe('guards - other', () => { expect(service.getSnapshot().value).toBe('c'); }); + + it('inline function guard should not leak into provided guards object', async () => { + const guards = {}; + + const machine = createMachine( + { + on: { + FOO: { + guard: () => false, + actions: () => {} + } + } + }, + { guards } + ); + + const actorRef = createActor(machine).start(); + actorRef.send({ type: 'FOO' }); + + expect(guards).toEqual({}); + }); + + it('inline builtin guard should not leak into provided guards object', async () => { + const guards = {}; + + const machine = createMachine( + { + on: { + FOO: { + guard: not(() => false), + actions: () => {} + } + } + }, + { guards } + ); + + const actorRef = createActor(machine).start(); + actorRef.send({ type: 'FOO' }); + + expect(guards).toEqual({}); + }); }); describe('not() guard', () => { diff --git a/packages/core/test/inspect.test.ts b/packages/core/test/inspect.test.ts index fc1cca7b71..87ec10e6eb 100644 --- a/packages/core/test/inspect.test.ts +++ b/packages/core/test/inspect.test.ts @@ -1032,7 +1032,7 @@ describe('inspect', () => { { "action": { "params": undefined, - "type": "(machine).loading|event:0:2", + "type": "(anonymous)", }, "type": "@xstate.action", }, diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 55e32012f5..1790078b93 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -17,6 +17,8 @@ import { } from '../src'; import { createDoneActorEvent } from '../src/eventUtils'; import { initialTransition } from '../src/transition'; +import assert from 'node:assert'; +import { resolveReferencedActor } from '../src/utils'; describe('transition function', () => { it('should capture actions', () => { @@ -67,7 +69,7 @@ describe('transition function', () => { expect(stringAction).not.toHaveBeenCalled(); // Execute actions - actions0.forEach((a) => machine.executeAction(a, {} as any)); + actions0.forEach((a) => machine.executeAction(a)); expect(actionWithParams).toHaveBeenCalledWith(expect.anything(), { a: 1 }); expect(stringAction).toHaveBeenCalled(); @@ -88,7 +90,7 @@ describe('transition function', () => { expect(actionWithDynamicParams).not.toHaveBeenCalled(); // Execute actions - actions1.forEach((a) => machine.executeAction(a, {} as any)); + actions1.forEach((a) => machine.executeAction(a)); expect(actionWithDynamicParams).toHaveBeenCalledWith({ msg: 'hello' @@ -415,11 +417,15 @@ describe('transition function', () => { async function execute(action: ExecutableActionsFrom) { switch (action.type) { - case 'xstate.spawn': { + case 'xstate.spawnChild': { const spawnAction = action as ExecutableSpawnAction; - const logic = machine.implementations.actors[spawnAction.params.src]; + const logic = + typeof spawnAction.params.src === 'string' + ? resolveReferencedActor(machine, spawnAction.params.src) + : spawnAction.params.src; + assert('transition' in logic); const output = await toPromise( - createActor(logic as any, spawnAction.params).start() + createActor(logic, spawnAction.params).start() ); postEvent(createDoneActorEvent(spawnAction.params.id, output)); } diff --git a/packages/core/test/utils.ts b/packages/core/test/utils.ts index 63bc001402..a97ce6cccd 100644 --- a/packages/core/test/utils.ts +++ b/packages/core/test/utils.ts @@ -81,21 +81,15 @@ export function trackEntries(machine: AnyStateMachine) { let logs: string[] = []; - machine.implementations.actions['__log'] = function __logAction(_, params) { - logs.push((params as any).msg); - }; - function addTrackingActions( state: StateNode, stateDescription: string ) { - state.entry.unshift({ - type: '__log', - params: { msg: `enter: ${stateDescription}` } + state.entry.unshift(function __testEntryTracker() { + logs.push(`enter: ${stateDescription}`); }); - state.exit.unshift({ - type: '__log', - params: { msg: `exit: ${stateDescription}` } + state.exit.unshift(function __testExitTracker() { + logs.push(`exit: ${stateDescription}`); }); } 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 63/80] 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(); From a7e83cec850a5fe1859bac1274ecdff8c9bcf885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 26 Oct 2024 11:00:19 +0200 Subject: [PATCH 64/80] share `resolvedInfo` between branches --- packages/core/src/StateMachine.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 2cf4c4dbd3..e5dfc6cdca 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -670,27 +670,17 @@ export class StateMachine< case 'xstate.sendTo': executeSendTo(actorScope, action.params as any); return; - default: } + const resolvedInfo = { + ...action.info, + self: actor, + system: actor.system + }; if (action.exec) { - action.exec?.( - { - ...action.info, - self: actor, - system: actor.system - }, - action.params - ); + action.exec?.(resolvedInfo, action.params); } else { const resolvedAction = resolveReferencedAction(this, action.type)!; - resolvedAction( - { - ...action.info, - self: actor, - system: actor.system - }, - action.params - ); + resolvedAction(resolvedInfo, action.params); } } finally { actorScope.defer = defer; From 3474c5c6f5444117d74a155cc84a624ea9848c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 26 Oct 2024 11:02:08 +0200 Subject: [PATCH 65/80] bring back `ExecutableActionObject['exec']` --- packages/core/src/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6ec27da8f3..f7433276d8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2612,6 +2612,9 @@ export interface ExecutableActionObject { type: string; info: ActionArgs; params: NonReducibleUnknown; + exec: + | ((info: ActionArgs, params: unknown) => void) + | undefined; } export interface ToExecutableAction From 94d44f018da0801b45949255256e0cfdbd0a47ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 30 Oct 2024 14:51:31 +0100 Subject: [PATCH 66/80] add a failing boilerplate for `cancel` execution --- packages/core/src/StateMachine.ts | 4 ++++ packages/core/src/actions/cancel.ts | 5 ++++- packages/core/src/types.ts | 1 + packages/core/test/transition.test.ts | 32 +++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index e5dfc6cdca..299bacb8da 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -1,5 +1,6 @@ import isDevelopment from '#is-development'; import { assign } from './actions.ts'; +import { executeCancel } from './actions/cancel.ts'; import { executeRaise } from './actions/raise.ts'; import { executeSendTo } from './actions/send.ts'; import { createEmptyActor } from './actors/index.ts'; @@ -661,6 +662,9 @@ export class StateMachine< actorScope.defer = (fn) => fn(); try { switch (action.type) { + case 'xstate.cancel': + executeCancel(actorScope, action.params as any); + return; case 'xstate.raise': if (typeof (action as any).params.delay !== 'number') { return; diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index 96d36993ad..d0bcc79cb5 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -32,7 +32,10 @@ function resolveCancel( return [snapshot, resolvedSendId]; } -function executeCancel(actorScope: AnyActorScope, resolvedSendId: string) { +export function executeCancel( + actorScope: AnyActorScope, + resolvedSendId: string +) { actorScope.defer(() => { actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); }); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f7433276d8..1c13e74dc0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2634,6 +2634,7 @@ export interface ExecutableSpawnAction extends ExecutableActionObject { }; } +// TODO: cover all that can be actually returned export type SpecialExecutableAction = | ExecutableSpawnAction | ExecutableRaiseAction diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 522b7ace68..72e775423e 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -1,6 +1,7 @@ import { sleep } from '@xstate-repo/jest-utils'; import { assign, + cancel, createActor, createMachine, enqueueActions, @@ -244,6 +245,37 @@ describe('transition function', () => { await waitFor(actor, (s) => s.matches('b')); }); + it('cancel action should be returned and can be executed', async () => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + entry: raise({ type: 'NEXT' }, { delay: 10, id: 'myRaise' }), + on: { + NEXT: { + target: 'b', + actions: cancel('myRaise') + } + } + }, + b: {} + } + }); + + const [state] = initialTransition(machine); + + expect(state.value).toEqual('a'); + + const [, actions] = transition(machine, state, { type: 'NEXT' }); + + actions.forEach((action) => { + machine.executeAction(action); + }); + + // TODO: tweak the assertion + expect(actions.map((a) => a.type)).toEqual({}); + }); + // Copied from getSnapshot.test.ts it('should calculate the next snapshot for transition logic', () => { From 6242d5de51577fa57cc12418c09adbad3395c0fb Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 2 Nov 2024 10:25:09 -0400 Subject: [PATCH 67/80] Fix cancel action --- packages/core/src/actions/cancel.ts | 11 ++++++----- packages/core/src/types.ts | 11 +++++++++++ packages/core/test/transition.test.ts | 9 ++++++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index d0bcc79cb5..1ef3507f92 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -5,7 +5,8 @@ import { EventObject, MachineContext, ActionArgs, - ParameterizedObject + ParameterizedObject, + SpecialActionResolution } from '../types.ts'; type ResolvableSendId< @@ -26,18 +27,18 @@ function resolveCancel( actionArgs: ActionArgs, actionParams: ParameterizedObject['params'] | undefined, { sendId }: { sendId: ResolvableSendId } -) { +): SpecialActionResolution { const resolvedSendId = typeof sendId === 'function' ? sendId(actionArgs, actionParams) : sendId; - return [snapshot, resolvedSendId]; + return [snapshot, { sendId: resolvedSendId }]; } export function executeCancel( actorScope: AnyActorScope, - resolvedSendId: string + params: { sendId: string } ) { actorScope.defer(() => { - actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); + actorScope.system.scheduler.cancel(actorScope.self, params.sendId); }); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1c13e74dc0..faebd830ee 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2663,3 +2663,14 @@ export type ExecutableActionsFrom = : never; export type ActionExecutor = (actionToExecute: ExecutableActionObject) => void; + +export type SpecialActionResolution = + | [ + AnyMachineSnapshot, + NonReducibleUnknown // params + ] + | [ + AnyMachineSnapshot, + NonReducibleUnknown, // params + UnknownAction[] | undefined + ]; diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 72e775423e..26515d863d 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -273,7 +273,14 @@ describe('transition function', () => { }); // TODO: tweak the assertion - expect(actions.map((a) => a.type)).toEqual({}); + expect(actions).toContainEqual( + expect.objectContaining({ + type: 'xstate.cancel', + params: expect.objectContaining({ + sendId: 'myRaise' + }) + }) + ); }); // Copied from getSnapshot.test.ts From 2a573fbfe14d7938c8c932cf7b74a6890d7ab9d6 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 2 Nov 2024 10:28:55 -0400 Subject: [PATCH 68/80] Add SpecialActionResolution type --- packages/core/src/actions/assign.ts | 9 ++++++--- packages/core/src/actions/cancel.ts | 2 +- packages/core/src/actions/emit.ts | 7 ++++--- packages/core/src/actions/enqueueActions.ts | 3 ++- packages/core/src/actions/log.ts | 8 +++++--- packages/core/src/actions/raise.ts | 8 +++++--- packages/core/src/actions/send.ts | 6 ++++-- packages/core/src/actions/spawnChild.ts | 6 ++++-- packages/core/src/actions/stopChild.ts | 8 +++++--- packages/core/src/types.ts | 15 +++++---------- 10 files changed, 41 insertions(+), 31 deletions(-) diff --git a/packages/core/src/actions/assign.ts b/packages/core/src/actions/assign.ts index e4d9bd45dc..2ecee91aaa 100644 --- a/packages/core/src/actions/assign.ts +++ b/packages/core/src/actions/assign.ts @@ -15,7 +15,8 @@ import type { ParameterizedObject, PropertyAssigner, ProvidedActor, - ActionFunction + ActionFunction, + SpecialActionResolution } from '../types.ts'; export interface AssignArgs< @@ -39,7 +40,7 @@ function resolveAssign( | Assigner | PropertyAssigner; } -) { +): SpecialActionResolution { if (!snapshot.context) { throw new Error( 'Cannot assign to undefined `context`. Ensure that `context` is defined in the machine config.' @@ -83,7 +84,9 @@ function resolveAssign( ...spawnedChildren } : snapshot.children - }) + }), + undefined, + undefined ]; } diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index 1ef3507f92..fb7b4a399e 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -30,7 +30,7 @@ function resolveCancel( ): SpecialActionResolution { const resolvedSendId = typeof sendId === 'function' ? sendId(actionArgs, actionParams) : sendId; - return [snapshot, { sendId: resolvedSendId }]; + return [snapshot, { sendId: resolvedSendId }, undefined]; } export function executeCancel( diff --git a/packages/core/src/actions/emit.ts b/packages/core/src/actions/emit.ts index 1fad95846e..c4404c5ecd 100644 --- a/packages/core/src/actions/emit.ts +++ b/packages/core/src/actions/emit.ts @@ -10,7 +10,8 @@ import { EventObject, MachineContext, ParameterizedObject, - SendExpr + SendExpr, + SpecialActionResolution } from '../types.ts'; function resolveEmit( @@ -31,12 +32,12 @@ function resolveEmit( EventObject >; } -) { +): SpecialActionResolution { const resolvedEvent = typeof eventOrExpr === 'function' ? eventOrExpr(args, actionParams) : eventOrExpr; - return [snapshot, { event: resolvedEvent }]; + return [snapshot, { event: resolvedEvent }, undefined]; } function executeEmit( diff --git a/packages/core/src/actions/enqueueActions.ts b/packages/core/src/actions/enqueueActions.ts index a1d2c83993..df7339e506 100644 --- a/packages/core/src/actions/enqueueActions.ts +++ b/packages/core/src/actions/enqueueActions.ts @@ -12,6 +12,7 @@ import { MachineContext, ParameterizedObject, ProvidedActor, + SpecialActionResolution, UnifiedArg } from '../types.ts'; import { assign } from './assign.ts'; @@ -130,7 +131,7 @@ function resolveEnqueueActions( EventObject >; } -) { +): SpecialActionResolution { const actions: any[] = []; const enqueue: Parameters[0]['enqueue'] = function enqueue( action diff --git a/packages/core/src/actions/log.ts b/packages/core/src/actions/log.ts index a4552a2e49..e1d77aec45 100644 --- a/packages/core/src/actions/log.ts +++ b/packages/core/src/actions/log.ts @@ -6,7 +6,8 @@ import { EventObject, LogExpr, MachineContext, - ParameterizedObject + ParameterizedObject, + SpecialActionResolution } from '../types.ts'; type ResolvableLogValue< @@ -28,14 +29,15 @@ function resolveLog( value: ResolvableLogValue; label: string | undefined; } -) { +): SpecialActionResolution { return [ snapshot, { value: typeof value === 'function' ? value(actionArgs, actionParams) : value, label - } + }, + undefined ]; } diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index 6f8c76462e..c2ca417a8b 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -13,7 +13,8 @@ import { MachineContext, ParameterizedObject, RaiseActionOptions, - SendExpr + SendExpr, + SpecialActionResolution } from '../types.ts'; function resolveRaise( @@ -48,7 +49,7 @@ function resolveRaise( | undefined; }, { internalQueue }: { internalQueue: AnyEventObject[] } -) { +): SpecialActionResolution { const delaysMap = snapshot.machine.implementations.delays; if (typeof eventOrExpr === 'string') { @@ -82,7 +83,8 @@ function resolveRaise( event: resolvedEvent, id, delay: resolvedDelay - } + }, + undefined ]; } diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 4e5060ba41..fe8a09edc1 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -20,6 +20,7 @@ import { ParameterizedObject, SendExpr, SendToActionOptions, + SpecialActionResolution, SpecialTargets, UnifiedArg } from '../types.ts'; @@ -64,7 +65,7 @@ function resolveSendTo( | undefined; }, extra: { deferredActorIds: string[] | undefined } -) { +): SpecialActionResolution { const delaysMap = snapshot.machine.implementations.delays; if (typeof eventOrExpr === 'string') { @@ -126,7 +127,8 @@ function resolveSendTo( event: resolvedEvent, id, delay: resolvedDelay - } + }, + undefined ]; } diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index b6f91455c5..6ec4ce8070 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -18,6 +18,7 @@ import { ParameterizedObject, ProvidedActor, RequiredActorOptions, + SpecialActionResolution, UnifiedArg } from '../types.ts'; import { resolveReferencedActor } from '../utils.ts'; @@ -47,7 +48,7 @@ function resolveSpawn( input?: unknown; syncSnapshot: boolean; } -) { +): SpecialActionResolution { const logic = typeof src === 'string' ? resolveReferencedActor(snapshot.machine, src) @@ -94,7 +95,8 @@ function resolveSpawn( actorRef, src, input: resolvedInput - } + }, + undefined ]; } diff --git a/packages/core/src/actions/stopChild.ts b/packages/core/src/actions/stopChild.ts index 18b7f48c31..88556f68a6 100644 --- a/packages/core/src/actions/stopChild.ts +++ b/packages/core/src/actions/stopChild.ts @@ -8,7 +8,8 @@ import { AnyMachineSnapshot, EventObject, MachineContext, - ParameterizedObject + ParameterizedObject, + SpecialActionResolution } from '../types.ts'; type ResolvableActorRef< @@ -30,7 +31,7 @@ function resolveStop( args: ActionArgs, actionParams: ParameterizedObject['params'] | undefined, { actorRef }: { actorRef: ResolvableActorRef } -) { +): SpecialActionResolution { const actorRefOrString = typeof actorRef === 'function' ? actorRef(args, actionParams) : actorRef; const resolvedActorRef: AnyActorRef | undefined = @@ -47,7 +48,8 @@ function resolveStop( cloneMachineSnapshot(snapshot, { children }), - resolvedActorRef + resolvedActorRef, + undefined ]; } function executeStop( diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index faebd830ee..8f1a3d37bf 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2664,13 +2664,8 @@ export type ExecutableActionsFrom = export type ActionExecutor = (actionToExecute: ExecutableActionObject) => void; -export type SpecialActionResolution = - | [ - AnyMachineSnapshot, - NonReducibleUnknown // params - ] - | [ - AnyMachineSnapshot, - NonReducibleUnknown, // params - UnknownAction[] | undefined - ]; +export type SpecialActionResolution = [ + AnyMachineSnapshot, + NonReducibleUnknown, // params + UnknownAction[] | undefined +]; From 91128d87a6306eba0ac8f2bddaab19f7fdeacf38 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 3 Nov 2024 09:17:23 -0500 Subject: [PATCH 69/80] Add test for sendTo action --- packages/core/src/actions/send.ts | 1 + packages/core/src/types.ts | 6 +++- packages/core/test/transition.test.ts | 48 ++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index fe8a09edc1..d6ce20fff5 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -124,6 +124,7 @@ function resolveSendTo( snapshot, { to: targetActorRef, + actorId: typeof resolvedTarget === 'string' ? resolvedTarget : undefined, event: resolvedEvent, id, delay: resolvedDelay diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8f1a3d37bf..e2f218f796 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1985,7 +1985,11 @@ export interface ActorRef< ) => Subscription; } -export type AnyActorRef = ActorRef; +export type AnyActorRef = ActorRef< + any, + any, // TODO: shouldn't this be AnyEventObject? + any +>; export type ActorRefLike = Pick< AnyActorRef, diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 26515d863d..ebd6bfd20d 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -11,6 +11,7 @@ import { fromPromise, fromTransition, raise, + sendTo, setup, toPromise, transition, @@ -20,6 +21,7 @@ import { createDoneActorEvent } from '../src/eventUtils'; import { initialTransition } from '../src/transition'; import assert from 'node:assert'; import { resolveReferencedActor } from '../src/utils'; +import { parseArgs } from 'node:util'; describe('transition function', () => { it('should capture actions', () => { @@ -268,11 +270,12 @@ describe('transition function', () => { const [, actions] = transition(machine, state, { type: 'NEXT' }); + // This does nothing, since a delayed raise action should be handled + // by an external scheduler actions.forEach((action) => { machine.executeAction(action); }); - // TODO: tweak the assertion expect(actions).toContainEqual( expect.objectContaining({ type: 'xstate.cancel', @@ -283,6 +286,49 @@ describe('transition function', () => { ); }); + it('sendTo action should be returned and can be executed', async () => { + const machine = createMachine({ + initial: 'a', + invoke: { + src: createMachine({}), + id: 'someActor' + }, + states: { + a: { + on: { + NEXT: { + actions: sendTo('someActor', { type: 'someEvent' }) + } + } + } + } + }); + + const [state, actions0] = initialTransition(machine); + + expect(state.value).toEqual('a'); + + expect(actions0).toContainEqual( + expect.objectContaining({ + type: 'xstate.spawnChild', + params: expect.objectContaining({ + id: 'someActor' + }) + }) + ); + + const [, actions] = transition(machine, state, { type: 'NEXT' }); + + expect(actions).toContainEqual( + expect.objectContaining({ + type: 'xstate.sendTo', + params: expect.objectContaining({ + actorId: 'someActor' + }) + }) + ); + }); + // Copied from getSnapshot.test.ts it('should calculate the next snapshot for transition logic', () => { From 002670ff3cde49cf4223a0486d5a1fa843151225 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 4 Nov 2024 07:51:00 -0500 Subject: [PATCH 70/80] actorId -> targetId --- packages/core/src/actions/send.ts | 2 +- packages/core/test/transition.test.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index d6ce20fff5..472313379a 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -124,7 +124,7 @@ function resolveSendTo( snapshot, { to: targetActorRef, - actorId: typeof resolvedTarget === 'string' ? resolvedTarget : undefined, + targetId: typeof resolvedTarget === 'string' ? resolvedTarget : undefined, event: resolvedEvent, id, delay: resolvedDelay diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index ebd6bfd20d..0a49aac983 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -21,7 +21,6 @@ import { createDoneActorEvent } from '../src/eventUtils'; import { initialTransition } from '../src/transition'; import assert from 'node:assert'; import { resolveReferencedActor } from '../src/utils'; -import { parseArgs } from 'node:util'; describe('transition function', () => { it('should capture actions', () => { @@ -323,7 +322,7 @@ describe('transition function', () => { expect.objectContaining({ type: 'xstate.sendTo', params: expect.objectContaining({ - actorId: 'someActor' + targetId: 'someActor' }) }) ); From a2d8fb0ade5f30d948d2e7ed5ec8a0d514c20c1c Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 4 Nov 2024 07:58:42 -0500 Subject: [PATCH 71/80] Rename --- packages/core/src/actions/assign.ts | 4 ++-- packages/core/src/actions/cancel.ts | 4 ++-- packages/core/src/actions/emit.ts | 4 ++-- packages/core/src/actions/enqueueActions.ts | 4 ++-- packages/core/src/actions/log.ts | 4 ++-- packages/core/src/actions/raise.ts | 4 ++-- packages/core/src/actions/send.ts | 4 ++-- packages/core/src/actions/spawnChild.ts | 4 ++-- packages/core/src/actions/stopChild.ts | 4 ++-- packages/core/src/types.ts | 2 +- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/core/src/actions/assign.ts b/packages/core/src/actions/assign.ts index 2ecee91aaa..7144d6e22f 100644 --- a/packages/core/src/actions/assign.ts +++ b/packages/core/src/actions/assign.ts @@ -16,7 +16,7 @@ import type { PropertyAssigner, ProvidedActor, ActionFunction, - SpecialActionResolution + BuiltinActionResolution } from '../types.ts'; export interface AssignArgs< @@ -40,7 +40,7 @@ function resolveAssign( | Assigner | PropertyAssigner; } -): SpecialActionResolution { +): BuiltinActionResolution { if (!snapshot.context) { throw new Error( 'Cannot assign to undefined `context`. Ensure that `context` is defined in the machine config.' diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index fb7b4a399e..1f1d055b5c 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -6,7 +6,7 @@ import { MachineContext, ActionArgs, ParameterizedObject, - SpecialActionResolution + BuiltinActionResolution } from '../types.ts'; type ResolvableSendId< @@ -27,7 +27,7 @@ function resolveCancel( actionArgs: ActionArgs, actionParams: ParameterizedObject['params'] | undefined, { sendId }: { sendId: ResolvableSendId } -): SpecialActionResolution { +): BuiltinActionResolution { const resolvedSendId = typeof sendId === 'function' ? sendId(actionArgs, actionParams) : sendId; return [snapshot, { sendId: resolvedSendId }, undefined]; diff --git a/packages/core/src/actions/emit.ts b/packages/core/src/actions/emit.ts index c4404c5ecd..e0e56f2868 100644 --- a/packages/core/src/actions/emit.ts +++ b/packages/core/src/actions/emit.ts @@ -11,7 +11,7 @@ import { MachineContext, ParameterizedObject, SendExpr, - SpecialActionResolution + BuiltinActionResolution } from '../types.ts'; function resolveEmit( @@ -32,7 +32,7 @@ function resolveEmit( EventObject >; } -): SpecialActionResolution { +): BuiltinActionResolution { const resolvedEvent = typeof eventOrExpr === 'function' ? eventOrExpr(args, actionParams) diff --git a/packages/core/src/actions/enqueueActions.ts b/packages/core/src/actions/enqueueActions.ts index df7339e506..4757f59627 100644 --- a/packages/core/src/actions/enqueueActions.ts +++ b/packages/core/src/actions/enqueueActions.ts @@ -12,7 +12,7 @@ import { MachineContext, ParameterizedObject, ProvidedActor, - SpecialActionResolution, + BuiltinActionResolution, UnifiedArg } from '../types.ts'; import { assign } from './assign.ts'; @@ -131,7 +131,7 @@ function resolveEnqueueActions( EventObject >; } -): SpecialActionResolution { +): BuiltinActionResolution { const actions: any[] = []; const enqueue: Parameters[0]['enqueue'] = function enqueue( action diff --git a/packages/core/src/actions/log.ts b/packages/core/src/actions/log.ts index e1d77aec45..e762a79574 100644 --- a/packages/core/src/actions/log.ts +++ b/packages/core/src/actions/log.ts @@ -7,7 +7,7 @@ import { LogExpr, MachineContext, ParameterizedObject, - SpecialActionResolution + BuiltinActionResolution } from '../types.ts'; type ResolvableLogValue< @@ -29,7 +29,7 @@ function resolveLog( value: ResolvableLogValue; label: string | undefined; } -): SpecialActionResolution { +): BuiltinActionResolution { return [ snapshot, { diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index c2ca417a8b..d2c46fd75d 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -14,7 +14,7 @@ import { ParameterizedObject, RaiseActionOptions, SendExpr, - SpecialActionResolution + BuiltinActionResolution } from '../types.ts'; function resolveRaise( @@ -49,7 +49,7 @@ function resolveRaise( | undefined; }, { internalQueue }: { internalQueue: AnyEventObject[] } -): SpecialActionResolution { +): BuiltinActionResolution { const delaysMap = snapshot.machine.implementations.delays; if (typeof eventOrExpr === 'string') { diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 472313379a..e60d81c7b5 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -20,7 +20,7 @@ import { ParameterizedObject, SendExpr, SendToActionOptions, - SpecialActionResolution, + BuiltinActionResolution, SpecialTargets, UnifiedArg } from '../types.ts'; @@ -65,7 +65,7 @@ function resolveSendTo( | undefined; }, extra: { deferredActorIds: string[] | undefined } -): SpecialActionResolution { +): BuiltinActionResolution { const delaysMap = snapshot.machine.implementations.delays; if (typeof eventOrExpr === 'string') { diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index 6ec4ce8070..6070a8cc7f 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -18,7 +18,7 @@ import { ParameterizedObject, ProvidedActor, RequiredActorOptions, - SpecialActionResolution, + BuiltinActionResolution, UnifiedArg } from '../types.ts'; import { resolveReferencedActor } from '../utils.ts'; @@ -48,7 +48,7 @@ function resolveSpawn( input?: unknown; syncSnapshot: boolean; } -): SpecialActionResolution { +): BuiltinActionResolution { const logic = typeof src === 'string' ? resolveReferencedActor(snapshot.machine, src) diff --git a/packages/core/src/actions/stopChild.ts b/packages/core/src/actions/stopChild.ts index 88556f68a6..3fe268b8fe 100644 --- a/packages/core/src/actions/stopChild.ts +++ b/packages/core/src/actions/stopChild.ts @@ -9,7 +9,7 @@ import { EventObject, MachineContext, ParameterizedObject, - SpecialActionResolution + BuiltinActionResolution } from '../types.ts'; type ResolvableActorRef< @@ -31,7 +31,7 @@ function resolveStop( args: ActionArgs, actionParams: ParameterizedObject['params'] | undefined, { actorRef }: { actorRef: ResolvableActorRef } -): SpecialActionResolution { +): BuiltinActionResolution { const actorRefOrString = typeof actorRef === 'function' ? actorRef(args, actionParams) : actorRef; const resolvedActorRef: AnyActorRef | undefined = diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e2f218f796..1ec3f3a2e0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2668,7 +2668,7 @@ export type ExecutableActionsFrom = export type ActionExecutor = (actionToExecute: ExecutableActionObject) => void; -export type SpecialActionResolution = [ +export type BuiltinActionResolution = [ AnyMachineSnapshot, NonReducibleUnknown, // params UnknownAction[] | undefined From b47f77f0be0037af9592574138e9f75abf55f9f5 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 4 Nov 2024 08:04:21 -0500 Subject: [PATCH 72/80] Add tests for emit and log --- packages/core/test/transition.test.ts | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 0a49aac983..b652bc986c 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -4,12 +4,14 @@ import { cancel, createActor, createMachine, + emit, enqueueActions, EventFrom, ExecutableActionsFrom, ExecutableSpawnAction, fromPromise, fromTransition, + log, raise, sendTo, setup, @@ -328,6 +330,71 @@ describe('transition function', () => { ); }); + it('emit actions should be returned', async () => { + const machine = createMachine({ + initial: 'a', + context: { count: 10 }, + states: { + a: { + on: { + NEXT: { + actions: emit(({ context }) => ({ + type: 'counted', + count: context.count + })) + } + } + } + } + }); + + const [state] = initialTransition(machine); + + expect(state.value).toEqual('a'); + + const [, nextActions] = transition(machine, state, { type: 'NEXT' }); + + expect(nextActions).toContainEqual( + expect.objectContaining({ + type: 'xstate.emit', + params: expect.objectContaining({ + event: { type: 'counted', count: 10 } + }) + }) + ); + }); + + it('log actions should be returned', async () => { + const machine = createMachine({ + initial: 'a', + context: { count: 10 }, + states: { + a: { + on: { + NEXT: { + actions: log(({ context }) => `count: ${context.count}`) + } + } + } + } + }); + + const [state] = initialTransition(machine); + + expect(state.value).toEqual('a'); + + const [, nextActions] = transition(machine, state, { type: 'NEXT' }); + + expect(nextActions).toContainEqual( + expect.objectContaining({ + type: 'xstate.log', + params: expect.objectContaining({ + value: 'count: 10' + }) + }) + ); + }); + // Copied from getSnapshot.test.ts it('should calculate the next snapshot for transition logic', () => { From c04c12c62d1835a5554ebb4aab0000784e3ce9a1 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 11 Nov 2024 23:04:56 -0500 Subject: [PATCH 73/80] Remove switch statement in executeAction --- packages/core/src/StateMachine.ts | 46 +++++++-------------------- packages/core/src/actions/cancel.ts | 5 +-- packages/core/src/actions/raise.ts | 2 +- packages/core/src/actions/send.ts | 2 +- packages/core/src/stateUtils.ts | 10 ++---- packages/core/test/transition.test.ts | 20 ------------ 6 files changed, 16 insertions(+), 69 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 299bacb8da..5de555f4c7 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -1,8 +1,5 @@ import isDevelopment from '#is-development'; import { assign } from './actions.ts'; -import { executeCancel } from './actions/cancel.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'; @@ -22,7 +19,7 @@ import { macrostep, microstep, resolveActionsAndContext, - resolveReferencedAction, + getAction, resolveStateValue, transitionNode } from './stateUtils.ts'; @@ -657,37 +654,16 @@ export class StateMachine< 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.cancel': - executeCancel(actorScope, action.params as any); - return; - 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; - } - const resolvedInfo = { - ...action.info, - self: actor, - system: actor.system - }; - if (action.exec) { - action.exec?.(resolvedInfo, action.params); - } else { - const resolvedAction = resolveReferencedAction(this, action.type)!; - resolvedAction(resolvedInfo, action.params); - } - } finally { - actorScope.defer = defer; + const resolvedInfo = { + ...action.info, + self: actor, + system: actor.system + }; + if (action.exec) { + action.exec?.(resolvedInfo, action.params); + } else { + const resolvedAction = getAction(this, action.type)!; + resolvedAction(resolvedInfo, action.params); } } } diff --git a/packages/core/src/actions/cancel.ts b/packages/core/src/actions/cancel.ts index 1f1d055b5c..2447f757c4 100644 --- a/packages/core/src/actions/cancel.ts +++ b/packages/core/src/actions/cancel.ts @@ -33,10 +33,7 @@ function resolveCancel( return [snapshot, { sendId: resolvedSendId }, undefined]; } -export function executeCancel( - actorScope: AnyActorScope, - params: { sendId: string } -) { +function executeCancel(actorScope: AnyActorScope, params: { sendId: string }) { actorScope.defer(() => { actorScope.system.scheduler.cancel(actorScope.self, params.sendId); }); diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index d2c46fd75d..5d8e699cdf 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -88,7 +88,7 @@ function resolveRaise( ]; } -export function executeRaise( +function executeRaise( actorScope: AnyActorScope, params: { event: EventObject; diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index e60d81c7b5..3d5c3917a7 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -148,7 +148,7 @@ function retryResolveSendTo( } } -export function executeSendTo( +function executeSendTo( actorScope: AnyActorScope, params: { to: AnyActorRef; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index eb712ca0f8..a97948e317 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1486,10 +1486,7 @@ export interface BuiltinAction { execute: (actorScope: AnyActorScope, params: unknown) => void; } -export function resolveReferencedAction( - machine: AnyStateMachine, - actionType: string -) { +export function getAction(machine: AnyStateMachine, actionType: string) { return machine.implementations.actions[actionType]; } @@ -1515,10 +1512,7 @@ function resolveAndExecuteActionsWithContext( // 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 - resolveReferencedAction( - machine, - typeof action === 'string' ? action : action.type - ); + getAction(machine, typeof action === 'string' ? action : action.type); const actionArgs = { context: intermediateSnapshot.context, event, diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index b652bc986c..96bbff23e3 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -200,16 +200,6 @@ describe('transition function', () => { }) }) ); - - const actor = createActor(machine, { - snapshot: state - }).start(); - - actions.forEach((action) => { - machine.executeAction(action, actor); - }); - - await waitFor(actor, (s) => s.matches('b')); }); it('raise actions related to delayed transitions should be returned', async () => { @@ -236,16 +226,6 @@ describe('transition function', () => { }) }) ); - - const actor = createActor(machine, { - snapshot: state - }).start(); - - actions.forEach((action) => { - machine.executeAction(action, actor); - }); - - await waitFor(actor, (s) => s.matches('b')); }); it('cancel action should be returned and can be executed', async () => { From e87d8475f2bbff7fd3a06e52af1f9b388d32c773 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 11 Nov 2024 23:07:48 -0500 Subject: [PATCH 74/80] Undo From a58a3f2189a976d84e4308bd019d67ee1fa6f049 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 11 Nov 2024 23:15:08 -0500 Subject: [PATCH 75/80] Remove executeAction for now --- packages/core/src/StateMachine.ts | 37 +-------------------------- packages/core/test/transition.test.ts | 35 +------------------------ 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 5de555f4c7..171872c4fb 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -1,6 +1,5 @@ import isDevelopment from '#is-development'; import { assign } from './actions.ts'; -import { createEmptyActor } from './actors/index.ts'; import { $$ACTOR_TYPE, createActor } from './createActor.ts'; import { createInitEvent } from './eventUtils.ts'; import { @@ -19,7 +18,6 @@ import { macrostep, microstep, resolveActionsAndContext, - getAction, resolveStateValue, transitionNode } from './stateUtils.ts'; @@ -50,8 +48,7 @@ import type { TransitionDefinition, ResolvedStateMachineTypes, StateSchema, - SnapshotStatus, - ExecutableActionObject + SnapshotStatus } from './types.ts'; import { resolveReferencedActor, toStatePath } from './utils.ts'; @@ -634,36 +631,4 @@ export class StateMachine< return restoredSnapshot; } - - /** - * 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 resolvedInfo = { - ...action.info, - self: actor, - system: actor.system - }; - if (action.exec) { - action.exec?.(resolvedInfo, action.params); - } else { - const resolvedAction = getAction(this, action.type)!; - resolvedAction(resolvedInfo, action.params); - } - } } diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 96bbff23e3..12a8d7cb19 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -72,12 +72,6 @@ describe('transition function', () => { expect(actionWithParams).not.toHaveBeenCalled(); expect(stringAction).not.toHaveBeenCalled(); - // Execute actions - actions0.forEach((a) => machine.executeAction(a)); - - expect(actionWithParams).toHaveBeenCalledWith(expect.anything(), { a: 1 }); - expect(stringAction).toHaveBeenCalled(); - const [state1, actions1] = transition(machine, state0, { type: 'event', msg: 'hello' @@ -92,13 +86,6 @@ describe('transition function', () => { ]); expect(actionWithDynamicParams).not.toHaveBeenCalled(); - - // Execute actions - actions1.forEach((a) => machine.executeAction(a)); - - expect(actionWithDynamicParams).toHaveBeenCalledWith({ - msg: 'hello' - }); }); it('should be able to execute a referenced serialized action', () => { @@ -116,14 +103,6 @@ describe('transition function', () => { 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', () => { @@ -158,19 +137,13 @@ describe('transition function', () => { } }); - const [state, actions] = initialTransition(machine); + const [state] = initialTransition(machine); const actor = createActor(machine, { snapshot: state }).start(); expect(actor.getSnapshot().matches('a')).toBeTruthy(); - - actions.forEach((action) => { - machine.executeAction(action, actor); - }); - - expect(actor.getSnapshot().matches('b')).toBeTruthy(); }); it('delayed raise actions should be returned', async () => { @@ -251,12 +224,6 @@ describe('transition function', () => { const [, actions] = transition(machine, state, { type: 'NEXT' }); - // This does nothing, since a delayed raise action should be handled - // by an external scheduler - actions.forEach((action) => { - machine.executeAction(action); - }); - expect(actions).toContainEqual( expect.objectContaining({ type: 'xstate.cancel', From b0a7992980317f2df7db8255c65c7f135a67c382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 12 Nov 2024 09:28:25 +0100 Subject: [PATCH 76/80] bring back one `toSerializableAction` call --- packages/core/src/StateNode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 5abff6f96a..9c7c764cdc 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -262,7 +262,7 @@ export class StateNode< on: this.on, transitions: [...this.transitions.values()].flat().map((t) => ({ ...t, - actions: t.actions + actions: t.actions.map(toSerializableAction) })), entry: this.entry.map(toSerializableAction), exit: this.exit.map(toSerializableAction), From c894ad409e238be15cc35ad1e2ca54d5b1254e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 12 Nov 2024 09:31:48 +0100 Subject: [PATCH 77/80] fix one type issue --- packages/core/src/createActor.ts | 2 +- packages/core/src/inspection.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 67c5356f01..0b0467fef2 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -200,7 +200,7 @@ export class Actor actorRef: this, action: { type: action.type, - params: action.params as any // TODO: fix types + params: action.params } }); if (!action.exec) { diff --git a/packages/core/src/inspection.ts b/packages/core/src/inspection.ts index f8b81381e1..8a31484eb7 100644 --- a/packages/core/src/inspection.ts +++ b/packages/core/src/inspection.ts @@ -41,7 +41,7 @@ export interface InspectedActionEvent extends BaseInspectionEventProperties { type: '@xstate.action'; action: { type: string; - params: Record; + params: unknown; }; } From 03dac75c5e6e62f2383e13aeb25a9d87da0294a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 12 Nov 2024 09:35:30 +0100 Subject: [PATCH 78/80] tweak test titles --- packages/core/test/transition.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 12a8d7cb19..139e03bce1 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -88,7 +88,7 @@ describe('transition function', () => { expect(actionWithDynamicParams).not.toHaveBeenCalled(); }); - it('should be able to execute a referenced serialized action', () => { + it('should not execute a referenced serialized action', () => { const foo = jest.fn(); const machine = setup({ @@ -201,7 +201,7 @@ describe('transition function', () => { ); }); - it('cancel action should be returned and can be executed', async () => { + it('cancel action should be returned', async () => { const machine = createMachine({ initial: 'a', states: { @@ -234,7 +234,7 @@ describe('transition function', () => { ); }); - it('sendTo action should be returned and can be executed', async () => { + it('sendTo action should be returned', async () => { const machine = createMachine({ initial: 'a', invoke: { @@ -342,8 +342,6 @@ describe('transition function', () => { ); }); - // Copied from getSnapshot.test.ts - it('should calculate the next snapshot for transition logic', () => { const logic = fromTransition( (state, event) => { From fa47906a693db9b4ae619a059a91ed17ac87d947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 12 Nov 2024 09:37:11 +0100 Subject: [PATCH 79/80] remove redundant test --- packages/core/test/transition.test.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/packages/core/test/transition.test.ts b/packages/core/test/transition.test.ts index 139e03bce1..dfbf13ff00 100644 --- a/packages/core/test/transition.test.ts +++ b/packages/core/test/transition.test.ts @@ -123,29 +123,6 @@ describe('transition function', () => { ]); }); - it('actor can be specified', () => { - const machine = createMachine({ - entry: (x) => { - x.self.send({ type: 'next' }); - }, - initial: 'a', - states: { - a: { - on: { next: 'b' } - }, - b: {} - } - }); - - const [state] = initialTransition(machine); - - const actor = createActor(machine, { - snapshot: state - }).start(); - - expect(actor.getSnapshot().matches('a')).toBeTruthy(); - }); - it('delayed raise actions should be returned', async () => { const machine = createMachine({ initial: 'a', From 9ee75649fd6e7683879af354b86d125b3ad01aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 12 Nov 2024 09:38:06 +0100 Subject: [PATCH 80/80] dont export `getAction` --- packages/core/src/stateUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index a97948e317..3b01a64baf 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1486,7 +1486,7 @@ export interface BuiltinAction { execute: (actorScope: AnyActorScope, params: unknown) => void; } -export function getAction(machine: AnyStateMachine, actionType: string) { +function getAction(machine: AnyStateMachine, actionType: string) { return machine.implementations.actions[actionType]; }