diff --git a/.changeset/giant-moons-glow.md b/.changeset/giant-moons-glow.md new file mode 100644 index 0000000000..3283732569 --- /dev/null +++ b/.changeset/giant-moons-glow.md @@ -0,0 +1,26 @@ +--- +'xstate': major +--- + +There is now support for higher-level guards, which are guards that can compose other guards: + +- `and([guard1, guard2, /* ... */])` returns `true` if _all_ guards evaluate to truthy, otherwise `false` +- `or([guard1, guard2, /* ... */])` returns `true` if _any_ guard evaluates to truthy, otherwise `false` +- `not(guard1)` returns `true` if a single guard evaluates to `false`, otherwise `true` + +```js +import { and, or, not } from 'xstate/guards'; + +const someMachine = createMachine({ + // ... + on: { + EVENT: { + target: 'somewhere', + guard: and([ + 'stringGuard', + or([{ type: 'anotherGuard' }, not(() => false)]) + ]) + } + } +}); +``` diff --git a/.changeset/orange-gorillas-share.md b/.changeset/orange-gorillas-share.md new file mode 100644 index 0000000000..ced5a6eb04 --- /dev/null +++ b/.changeset/orange-gorillas-share.md @@ -0,0 +1,17 @@ +--- +'xstate': major +--- + +BREAKING: The `cond` property in transition config objects has been renamed to `guard`. This unifies terminology for guarded transitions and guard predicates (previously called "cond", or "conditional", predicates): + +```diff +someState: { + on: { + EVENT: { + target: 'anotherState', +- cond: 'isValid' ++ guard: 'isValid' + } + } +} +``` diff --git a/.changeset/thin-dryers-argue.md b/.changeset/thin-dryers-argue.md new file mode 100644 index 0000000000..6172848e0a --- /dev/null +++ b/.changeset/thin-dryers-argue.md @@ -0,0 +1,19 @@ +--- +'xstate': major +--- + +The interface for guard objects has changed. Notably, all guard parameters should be placed in the `params` property of the guard object: + +Example taken from [Custom Guards](https://xstate.js.org/docs/guides/guards.html#custom-guards): + +```diff +-cond: { ++guard: { +- name: 'searchValid', // `name` property no longer used + type: 'searchValid', +- minQueryLength: 3 ++ params: { ++ minQueryLength: 3 ++ } +} +``` diff --git a/docs/guides/context.md b/docs/guides/context.md index 7f6ef28d1f..2a8b8ffe9c 100644 --- a/docs/guides/context.md +++ b/docs/guides/context.md @@ -39,8 +39,8 @@ const glassMachine = Machine( filling: { // Transient transition always: { - target: 'full', - cond: 'glassIsFull' + target: 'full', + cond: 'glassIsFull' }, on: { FILL: { diff --git a/docs/guides/final.md b/docs/guides/final.md index 8ea3d5f270..76d895cd89 100644 --- a/docs/guides/final.md +++ b/docs/guides/final.md @@ -130,6 +130,6 @@ Final states correspond to the SCXML spec: [https://www.w3.org/TR/scxml/#final]( ## Notes - A final state node only indicates that its immediate parent is _done_. It does not affect the _done_ status of any higher parents, except with parallel state nodes, which are _done_ when all of its child compound state nodes are _done_. -- A parallel state that reaches a final substate does not stop receiving events until all its siblings are done. The final substate can still be exited with an event. +- A parallel state that reaches a final substate does not stop receiving events until all its siblings are done. The final substate can still be exited with an event. - Final state nodes cannot have any children. They are atomic state nodes. - You can specify `entry` and `exit` actions on final state nodes. diff --git a/packages/core/package.json b/packages/core/package.json index d6a9897c40..3ba56921f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,7 +13,8 @@ "dist", "actions", "invoke", - "behavior" + "behavior", + "guards" ], "keywords": [ "statechart", @@ -51,7 +52,8 @@ ".", "actions", "invoke", - "behavior" + "behavior", + "guards" ] } } diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index e7a0c16a77..4ef03bbeb3 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -38,9 +38,9 @@ import { formatInitialTransition } from './stateUtils'; import { getDelayedTransitions, formatTransitions, - getCandidates, - evaluateGuard + getCandidates } from './stateUtils'; +import { evaluateGuard } from './guards'; import { MachineNode } from './MachineNode'; import { STATE_DELIMITER } from './constants'; @@ -378,19 +378,24 @@ export class StateNode< (this.__cache.candidates[eventName] = getCandidates(this, eventName)); for (const candidate of candidates) { - const { cond } = candidate; + const { guard } = candidate; const resolvedContext = state.context; let guardPassed = false; try { guardPassed = - !cond || - evaluateGuard(this.machine, cond, resolvedContext, _event, state); + !guard || + evaluateGuard( + guard, + resolvedContext, + _event, + state + ); } catch (err) { throw new Error( `Unable to evaluate guard '${ - cond!.name || cond!.type + guard!.type }' in transition for event '${eventName}' in state node '${ this.id }':\n${err.message}` diff --git a/packages/core/src/actions.ts b/packages/core/src/actions.ts index 61b6b57fc4..95f2afef64 100644 --- a/packages/core/src/actions.ts +++ b/packages/core/src/actions.ts @@ -555,10 +555,10 @@ export function escalate< } export function choose( - conds: Array> + guards: Array> ): ChooseAction { return { type: ActionTypes.Choose, - conds + guards }; } diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index bc96cb58ed..0c1e7e5f6f 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,7 +1,4 @@ -import { DefaultGuardType } from './types'; - export const STATE_DELIMITER = '.'; -export const DEFAULT_GUARD_TYPE: DefaultGuardType = 'xstate.guard'; export const TARGETLESS_KEY = ''; export const NULL_EVENT = ''; export const STATE_IDENTIFIER = '#'; diff --git a/packages/core/src/guards.ts b/packages/core/src/guards.ts index f942228b45..2065a90396 100644 --- a/packages/core/src/guards.ts +++ b/packages/core/src/guards.ts @@ -1,13 +1,23 @@ -import { EventObject, StateValue, GuardPredicate } from './types'; +import { + EventObject, + StateValue, + BooleanGuardDefinition, + GuardConfig, + GuardDefinition, + GuardMeta, + SCXML, + GuardPredicate +} from './types'; import { isStateId } from './stateUtils'; -import { isString } from './utils'; +import { isFunction, isString } from './utils'; +import { State } from './State'; export function stateIn( stateValue: StateValue -): GuardPredicate { +): GuardDefinition { return { - type: 'xstate.guard', - name: 'In', + type: 'xstate.guard:in', + params: { stateValue }, predicate: (_, __, { state }) => { if (isString(stateValue) && isStateId(stateValue)) { return state.configuration.some((sn) => sn.id === stateValue.slice(1)); @@ -18,18 +28,106 @@ export function stateIn( }; } -export function stateNotIn( - stateValue: StateValue -): GuardPredicate { +export function not( + guard: GuardConfig +): BooleanGuardDefinition { return { - type: 'xstate.guard', - name: '!In', - predicate: (_, __, { state }) => { - if (isString(stateValue) && isStateId(stateValue)) { - return state.configuration.every((sn) => sn.id !== stateValue.slice(1)); - } + type: 'xstate.boolean', + params: { op: 'not' }, + children: [toGuardDefinition(guard)], + predicate: (ctx, _, meta) => { + return !meta.evaluate( + meta.guard.children![0], + ctx, + meta._event, + meta.state + ); + } + }; +} - return !state.matches(stateValue); +export function and( + guards: Array> +): BooleanGuardDefinition { + return { + type: 'xstate.boolean', + params: { op: 'and' }, + children: guards.map((guard) => toGuardDefinition(guard)), + predicate: (ctx, _, meta) => { + return meta.guard.children!.every((childGuard) => { + return meta.evaluate(childGuard, ctx, meta._event, meta.state); + }); + } + }; +} + +export function or( + guards: Array> +): BooleanGuardDefinition { + return { + type: 'xstate.boolean', + params: { op: 'or' }, + children: guards.map((guard) => toGuardDefinition(guard)), + predicate: (ctx, _, meta) => { + return meta.guard.children!.some((childGuard) => { + return meta.evaluate(childGuard, ctx, meta._event, meta.state); + }); } }; } + +export function evaluateGuard( + guard: GuardDefinition, + context: TContext, + _event: SCXML.Event, + state: State +): boolean { + const guardMeta: GuardMeta = { + state, + guard, + _event, + evaluate: evaluateGuard + }; + + const predicate = guard.predicate; + + if (!predicate) { + throw new Error(`Guard '${guard.type}' is not implemented.'.`); + } + + return predicate(context, _event.data, guardMeta); +} + +export function toGuardDefinition( + guardConfig: GuardConfig, + getPredicate?: (guardType: string) => GuardPredicate +): GuardDefinition { + if (isString(guardConfig)) { + return { + type: guardConfig, + predicate: getPredicate?.(guardConfig) || undefined, + params: { type: guardConfig } + }; + } + + if (isFunction(guardConfig)) { + return { + type: guardConfig.name, + predicate: guardConfig, + params: { + type: guardConfig.name, + name: guardConfig.name + } + }; + } + + return { + type: guardConfig.type, + params: guardConfig.params || guardConfig, + children: (guardConfig.children as Array< + GuardConfig + >)?.map((childGuard) => toGuardDefinition(childGuard, getPredicate)), + predicate: + getPredicate?.(guardConfig.type) || (guardConfig as any).predicate + }; +} diff --git a/packages/core/src/json.ts b/packages/core/src/json.ts index 1a3ed191b2..381579a04c 100644 --- a/packages/core/src/json.ts +++ b/packages/core/src/json.ts @@ -1,4 +1,4 @@ -import { StateNode, ActionObject, Guard, InvokeDefinition } from './'; +import { StateNode, ActionObject, GuardObject, InvokeDefinition } from './'; import { mapValues, isFunction } from './utils'; interface JSONFunction { @@ -20,7 +20,7 @@ interface TransitionConfig { target: string[]; source: string; actions: Array>; - cond: Guard | undefined; + guard: GuardObject | undefined; eventType: string; } @@ -54,7 +54,7 @@ export function machineToJSON(stateNode: StateNode): StateNodeConfig { target: t.target ? t.target.map(getStateNodeId) : [], source: getStateNodeId(t.source), actions: t.actions, - cond: t.cond, + guard: t.guard, eventType: t.eventType }; }); diff --git a/packages/core/src/scxml.ts b/packages/core/src/scxml.ts index 7e4c587a5b..6162eacbee 100644 --- a/packages/core/src/scxml.ts +++ b/packages/core/src/scxml.ts @@ -12,7 +12,7 @@ import { mapValues, keys, isString, flatten } from './utils'; import * as actions from './actions'; import { invokeMachine } from './invoke'; import { MachineNode } from './MachineNode'; -import { stateIn, stateNotIn } from './guards'; +import { not, stateIn } from './guards'; function getAttribute( element: XMLElement, @@ -125,12 +125,12 @@ const evaluateExecutableContent = < return fn(context, meta._event); }; -function createCond< +function createGuard< TContext extends object, TEvent extends EventObject = EventObject ->(cond: string) { +>(guard: string) { return (context: TContext, _event: TEvent, meta) => { - return evaluateExecutableContent(context, _event, meta, `return ${cond};`); + return evaluateExecutableContent(context, _event, meta, `return ${guard};`); }; } @@ -230,10 +230,10 @@ function mapAction< ); } case 'if': { - const conds: ChooseConditon[] = []; + const conds: Array> = []; let current: ChooseConditon = { - cond: createCond(element.attributes!.cond as string), + guard: createGuard(element.attributes!.cond as string), actions: [] }; @@ -246,7 +246,7 @@ function mapAction< case 'elseif': conds.push(current); current = { - cond: createCond(el.attributes!.cond as string), + guard: createGuard(el.attributes!.cond as string), actions: [] }; break; @@ -387,31 +387,31 @@ function toConfig( const targets = getAttribute(value, 'target'); const internal = getAttribute(value, 'type') === 'internal'; - let condObject = {}; + let guardObject = {}; if (value.attributes?.cond) { - const cond = value.attributes!.cond; - if ((cond as string).startsWith('In')) { - const inMatch = (cond as string).trim().match(/^In\('(.*)'\)/); + const guard = value.attributes!.cond; + if ((guard as string).startsWith('In')) { + const inMatch = (guard as string).trim().match(/^In\('(.*)'\)/); if (inMatch) { - condObject = { - cond: stateIn(`#${inMatch[1]}`) + guardObject = { + guard: stateIn(`#${inMatch[1]}`) }; } - } else if ((cond as string).startsWith('!In')) { - const notInMatch = (cond as string) + } else if ((guard as string).startsWith('!In')) { + const notInMatch = (guard as string) .trim() .match(/^!In\('(.*)'\)/); if (notInMatch) { - condObject = { - cond: stateNotIn(`#${notInMatch[1]}`) + guardObject = { + guard: not(stateIn(`#${notInMatch[1]}`)) }; } } else { - condObject = { - cond: createCond(value.attributes!.cond as string) + guardObject = { + guard: createGuard(value.attributes!.cond as string) }; } } @@ -420,7 +420,7 @@ function toConfig( event, target: getTargets(targets), ...(value.elements ? executableContent(value.elements) : undefined), - ...condObject, + ...guardObject, internal }; }); diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index a8a3a8c86c..1edd48e73c 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -8,7 +8,6 @@ import { isArray, isFunction, isString, - toGuard, toTransitionConfigArray, normalizeTarget, toStateValue, @@ -24,10 +23,7 @@ import { SingleOrArray, Typestate, DelayExpr, - Guard, SCXML, - GuardMeta, - GuardPredicate, Transitions, ActionObject, StateValueMap, @@ -69,15 +65,11 @@ import { resolveStop } from './actions'; import { IS_PRODUCTION } from './environment'; -import { - DEFAULT_GUARD_TYPE, - STATE_IDENTIFIER, - NULL_EVENT, - WILDCARD -} from './constants'; +import { STATE_IDENTIFIER, NULL_EVENT, WILDCARD } from './constants'; import { isSpawnedActorRef } from './Actor'; import { MachineNode } from './MachineNode'; import { createActorRefFromInvokeAction } from './invoke'; +import { evaluateGuard, toGuardDefinition } from './guards'; type Configuration = Iterable>; @@ -350,7 +342,12 @@ export function formatTransition( const transition = { ...transitionConfig, actions: toActionObjects(toArray(transitionConfig.actions)), - cond: toGuard(transitionConfig.cond, guards), + guard: transitionConfig.guard + ? toGuardDefinition( + transitionConfig.guard, + (guardType) => guards[guardType] + ) + : undefined, target, source: stateNode, internal, @@ -722,39 +719,6 @@ export function getStateNodes( ); } -export function evaluateGuard( - machine: MachineNode, - guard: Guard, - context: TContext, - _event: SCXML.Event, - state: State -): boolean { - const { guards } = machine.options; - const guardMeta: GuardMeta = { - state, - cond: guard, - _event - }; - - if (guard.type === DEFAULT_GUARD_TYPE) { - return (guard as GuardPredicate).predicate( - context, - _event.data, - guardMeta - ); - } - - const condFn = guards[guard.type]; - - if (!condFn) { - throw new Error( - `Guard '${guard.type}' is not implemented on machine '${machine.id}'.` - ); - } - - return condFn(context, _event.data, guardMeta); -} - export function transitionLeafNode( stateNode: StateNode, stateValue: string, @@ -1388,8 +1352,7 @@ export function microstep( } function selectEventlessTransitions( - state: State, - machine: MachineNode + state: State ): Transitions { const enabledTransitions: Set( for (const t of s.transitions) { if ( t.eventType === NULL_EVENT && - (t.cond === undefined || + (t.guard === undefined || evaluateGuard( - machine, - t.cond, + t.guard, state.context, toSCXMLEvent(NULL_EVENT as Event), state @@ -1535,7 +1497,7 @@ export function resolveMicroTransition< context !== currentContext; nextState._internalQueue = resolved.internalQueue; - const isTransient = selectEventlessTransitions(nextState, machine).length; + const isTransient = selectEventlessTransitions(nextState).length; if (isTransient) { nextState._internalQueue.unshift({ @@ -1629,18 +1591,17 @@ function resolveActionsAndContext( ); break; case actionTypes.choose: { - const ChooseAction = actionObject as ChooseAction; - const matchedActions = ChooseAction.conds.find((condition) => { - const guard = toGuard(condition.cond, machine.options.guards); + const chooseAction = actionObject as ChooseAction; + const matchedActions = chooseAction.guards.find((condition) => { + const guard = + condition.guard && + toGuardDefinition( + condition.guard, + (guardType) => machine.options.guards[guardType] + ); return ( !guard || - evaluateGuard( - machine, - guard, - context, - _event, - currentState as any - ) + evaluateGuard(guard, context, _event, currentState as any) ); })?.actions; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 34864aff39..d0630aa95b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -75,7 +75,7 @@ export type ActionFunction = ( ) => void; export interface ChooseConditon { - cond?: Condition; + guard?: GuardConfig; actions: Actions; } @@ -122,35 +122,67 @@ export type ExtractStateValue< >; }); -export type ConditionPredicate = ( +export type GuardPredicate = ( context: TContext, event: TEvent, meta: GuardMeta ) => boolean; -export type DefaultGuardType = 'xstate.guard'; - -export interface GuardPredicate { - type: DefaultGuardType; - name: string | undefined; - predicate: ConditionPredicate; +export interface DefaultGuardObject { + type: string; + params?: { [key: string]: any }; + /** + * Nested guards + */ + children?: Array>; + predicate?: GuardPredicate; } -export type Guard = - | GuardPredicate - | (Record & { - type: string; - }); +export type GuardEvaluator = ( + guard: GuardDefinition, + context: TContext, + _event: SCXML.Event, + state: State +) => boolean; export interface GuardMeta extends StateMeta { - cond: Guard; + guard: GuardDefinition; + evaluate: GuardEvaluator; } -export type Condition = +export type GuardConfig = | string - | ConditionPredicate - | Guard; + | GuardPredicate + | GuardObject; + +export type GuardObject = + | BooleanGuardObject + | DefaultGuardObject; + +export interface GuardDefinition { + type: string; + children?: Array>; + predicate?: GuardPredicate; + params: { [key: string]: any }; +} + +export interface BooleanGuardObject { + type: 'xstate.boolean'; + children: Array>; + params: { + op: 'and' | 'or' | 'not'; + }; + predicate: undefined; +} + +export interface BooleanGuardDefinition + extends GuardDefinition { + type: 'xstate.boolean'; + params: { + op: 'and' | 'or' | 'not'; + }; +} export type TransitionTarget< TContext, @@ -162,7 +194,7 @@ export type TransitionTargets = Array< >; export interface TransitionConfig { - cond?: Condition; + guard?: GuardConfig; actions?: Actions; internal?: boolean; target?: TransitionTarget; @@ -181,7 +213,7 @@ export type ConditionalTransitionConfig< export interface InitialTransitionConfig extends TransitionConfig { - cond?: never; + guard?: never; target: TransitionTarget; } @@ -605,7 +637,7 @@ export type DelayConfig = | DelayExpr; export interface MachineOptions { - guards: Record>; + guards: Record>; actions: ActionFunctionMap; behaviors: Record>; delays: DelayFunctionMap; @@ -887,7 +919,7 @@ export interface PureAction export interface ChooseAction extends ActionObject { type: ActionTypes.Choose; - conds: Array>; + guards: Array>; } export interface TransitionDefinition @@ -895,13 +927,13 @@ export interface TransitionDefinition target: Array> | undefined; source: StateNode; actions: Array>; - cond?: Guard; + guard?: GuardDefinition; eventType: TEvent['type'] | NullEvent['type'] | '*'; toJSON: () => { target: string[] | undefined; source: string; actions: Array>; - cond?: Guard; + guard?: GuardDefinition; eventType: TEvent['type'] | NullEvent['type'] | '*'; meta?: Record; }; @@ -912,7 +944,7 @@ export interface InitialTransitionDefinition< TEvent extends EventObject > extends TransitionDefinition { target: Array>; - cond?: never; + guard?: never; } export type TransitionDefinitionMap = { @@ -939,7 +971,7 @@ export interface Edge< event: TEventType; source: StateNode; target: StateNode; - cond?: Condition; + cond?: GuardConfig; actions: Array>; meta?: MetaObject; transition: TransitionDefinition; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 9276b73049..4e3b708f3b 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -5,25 +5,18 @@ import { PropertyMapper, Mapper, EventType, - Condition, Subscribable, - ConditionPredicate, SCXML, StateLike, TransitionConfig, TransitionConfigTarget, NullEvent, SingleOrArray, - Guard, BehaviorCreator, InvokeSourceDefinition, Observer } from './types'; -import { - STATE_DELIMITER, - DEFAULT_GUARD_TYPE, - TARGETLESS_KEY -} from './constants'; +import { STATE_DELIMITER, TARGETLESS_KEY } from './constants'; import { IS_PRODUCTION } from './environment'; import { StateNode } from './StateNode'; import { InvokeConfig, SCXMLErrorEvent } from '.'; @@ -328,33 +321,6 @@ export function isString(value: any): value is string { return typeof value === 'string'; } -export function toGuard( - condition?: Condition, - guardMap?: Record> -): Guard | undefined { - if (!condition) { - return undefined; - } - - if (isString(condition)) { - return { - type: DEFAULT_GUARD_TYPE, - name: condition, - predicate: guardMap ? guardMap[condition] : undefined - }; - } - - if (isFunction(condition)) { - return { - type: DEFAULT_GUARD_TYPE, - name: condition.name, - predicate: condition - }; - } - - return condition; -} - export function isObservable(value: any): value is Subscribable { try { return 'subscribe' in value && isFunction(value.subscribe); diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index dbecf8a058..c634c7e7d3 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -1017,7 +1017,7 @@ describe('forwardTo()', () => { on: { EVENT: { actions: sendParent('SUCCESS'), - cond: (_, e) => e.value === 42 + guard: (_, e) => e.value === 42 } } } @@ -1062,7 +1062,7 @@ describe('forwardTo()', () => { on: { EVENT: { actions: sendParent('SUCCESS'), - cond: (_, e) => e.value === 42 + guard: (_, e) => e.value === 42 } } } @@ -1159,7 +1159,7 @@ describe('choose', () => { states: { foo: { entry: choose([ - { cond: () => true, actions: assign({ answer: 42 }) } + { guard: () => true, actions: assign({ answer: 42 }) } ]) } } @@ -1184,7 +1184,7 @@ describe('choose', () => { foo: { entry: choose([ { - cond: () => true, + guard: () => true, actions: [() => (executed = true), assign({ answer: 42 })] } ]) @@ -1211,10 +1211,10 @@ describe('choose', () => { foo: { entry: choose([ { - cond: () => false, + guard: () => false, actions: assign({ shouldNotAppear: true }) }, - { cond: () => true, actions: assign({ answer: 42 }) } + { guard: () => true, actions: assign({ answer: 42 }) } ]) } } @@ -1238,7 +1238,7 @@ describe('choose', () => { foo: { entry: choose([ { - cond: () => false, + guard: () => false, actions: assign({ shouldNotAppear: true }) }, { actions: assign({ answer: 42 }) } @@ -1270,17 +1270,17 @@ describe('choose', () => { foo: { entry: choose([ { - cond: () => true, + guard: () => true, actions: [ assign({ firstLevel: true }), choose([ { - cond: () => true, + guard: () => true, actions: [ assign({ secondLevel: true }), choose([ { - cond: () => true, + guard: () => true, actions: [assign({ thirdLevel: true })] } ]) @@ -1317,7 +1317,7 @@ describe('choose', () => { foo: { entry: choose([ { - cond: (ctx) => ctx.counter > 100, + guard: (ctx) => ctx.counter > 100, actions: assign({ answer: 42 }) } ]) @@ -1349,7 +1349,7 @@ describe('choose', () => { target: 'bar', actions: choose([ { - cond: (_, event) => event.counter > 100, + guard: (_, event) => event.counter > 100, actions: assign({ answer: 42 }) } ]) @@ -1384,7 +1384,7 @@ describe('choose', () => { answering: { entry: choose([ { - cond: (_, __, { state }) => state.matches('bar'), + guard: (_, __, { state }) => state.matches('bar'), actions: assign({ answer: 42 }) } ]) @@ -1412,7 +1412,7 @@ describe('choose', () => { initial: 'foo', states: { foo: { - entry: choose([{ cond: 'worstGuard', actions: 'revealAnswer' }]) + entry: choose([{ guard: 'worstGuard', actions: 'revealAnswer' }]) } } }, @@ -1453,7 +1453,7 @@ describe('choose', () => { actions: { revealAnswer: assign({ answer: 42 }), conditionallyRevealAnswer: choose([ - { cond: 'worstGuard', actions: 'revealAnswer' } + { guard: 'worstGuard', actions: 'revealAnswer' } ]) } } diff --git a/packages/core/test/activities.test.ts b/packages/core/test/activities.test.ts index 67fa195261..9f17c7e0f7 100644 --- a/packages/core/test/activities.test.ts +++ b/packages/core/test/activities.test.ts @@ -48,9 +48,7 @@ describe('activities with guarded transitions', () => { }, B: { invoke: ['B_ACTIVITY'], - on: { - '': [{ cond: () => false, target: 'A' }] - } + always: [{ guard: () => false, target: 'A' }] } } }, @@ -240,7 +238,7 @@ describe('transient activities', () => { invoke: ['B1'], always: { target: 'B2', - cond: stateIn('#AWAIT') + guard: stateIn('#AWAIT') }, on: { B: 'B2' diff --git a/packages/core/test/actor.test.ts b/packages/core/test/actor.test.ts index 313e521afc..7b15cad6fb 100644 --- a/packages/core/test/actor.test.ts +++ b/packages/core/test/actor.test.ts @@ -203,7 +203,7 @@ describe('spawning promises', () => { on: { [doneInvoke('my-promise')]: { target: 'success', - cond: (_, e) => e.data === 'response' + guard: (_, e) => e.data === 'response' } } }, @@ -297,7 +297,7 @@ describe('spawning observables', () => { on: { INT: { target: 'success', - cond: (_, e) => e.value === 5 + guard: (_, e) => e.value === 5 } } }, @@ -367,8 +367,7 @@ describe('communicating with spawned actors', () => { parentService.start(); }); - // TODO: This is an invalid use-case; consider removing - it.skip('should be able to communicate with arbitrary actors if sessionId is known', (done) => { + it('should be able to communicate with arbitrary actors', (done) => { const existingMachine = Machine({ initial: 'inactive', states: { @@ -390,7 +389,7 @@ describe('communicating with spawned actors', () => { }, states: { pending: { - entry: send('ACTIVATE', { to: existingService.sessionId }), + entry: send('ACTIVATE', { to: () => existingService }), on: { 'EXISTING.DONE': 'success' }, diff --git a/packages/core/test/after.test.ts b/packages/core/test/after.test.ts index 7a8cd5cf32..07e7149316 100644 --- a/packages/core/test/after.test.ts +++ b/packages/core/test/after.test.ts @@ -130,7 +130,7 @@ describe('delayed transitions', () => { 1500: [ { target: 'Y', - cond: () => true + guard: () => true }, { target: 'Z' diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index d1d31f69ff..6d4dfc588d 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -24,7 +24,7 @@ describe('SCXML events', () => { on: { EVENT: { target: 'success', - cond: (_, __, { _event }) => { + guard: (_, __, { _event }) => { return !!_event.origin; } } @@ -118,7 +118,7 @@ const authMachine = Machine( on: { changePassword: [ { - cond: (_, event: ChangePassword) => event.password.length >= 10, + guard: (_, event: ChangePassword) => event.password.length >= 10, target: '.invalid', actions: ['assignPassword'] }, diff --git a/packages/core/test/examples/6.16.test.ts b/packages/core/test/examples/6.16.test.ts index 322575cdc2..357a322080 100644 --- a/packages/core/test/examples/6.16.test.ts +++ b/packages/core/test/examples/6.16.test.ts @@ -13,7 +13,7 @@ describe('Example 6.16', () => { on: { 2: { target: 'D', - cond: stateIn('#E') + guard: stateIn('#E') } } }, diff --git a/packages/core/test/final.test.ts b/packages/core/test/final.test.ts index 88abef7078..94f7baf084 100644 --- a/packages/core/test/final.test.ts +++ b/packages/core/test/final.test.ts @@ -28,7 +28,7 @@ const finalMachine = Machine({ } }, onDone: { - cond: (_, e) => e.data.signal === 'stop', + guard: (_, e) => e.data.signal === 'stop', actions: 'stopCrosswalk1' } }, diff --git a/packages/core/test/fixtures/factorial.ts b/packages/core/test/fixtures/factorial.ts index b64e3b0176..12d218dbba 100644 --- a/packages/core/test/fixtures/factorial.ts +++ b/packages/core/test/fixtures/factorial.ts @@ -11,7 +11,7 @@ const factorialMachine = Machine<{ n: number; fac: number }>({ ITERATE: [ { target: 'iteration', - cond: (xs) => xs.n > 0, + guard: (xs) => xs.n > 0, actions: [ assign({ fac: (xs) => xs.n * xs.fac, @@ -39,11 +39,11 @@ const testMachine = Machine<{ count: number }>({ ADD: [ { target: 'one', - cond: (xs) => xs.count === 1 + guard: (xs) => xs.count === 1 }, { target: 'init', - cond: (xs) => xs.count % 2 === 0, + guard: (xs) => xs.count % 2 === 0, actions: [ assign({ count: (xs) => xs.count / 2 diff --git a/packages/core/test/fixtures/omni.ts b/packages/core/test/fixtures/omni.ts index aa344e1483..4108ec2e0b 100644 --- a/packages/core/test/fixtures/omni.ts +++ b/packages/core/test/fixtures/omni.ts @@ -63,15 +63,15 @@ const omniMachine = Machine({ two: { after: { 2000: [ - { target: 'three', cond: (ctx) => ctx.count === 3 }, - { target: 'four', cond: (ctx) => ctx.count === 4 }, + { target: 'three', guard: (ctx) => ctx.count === 3 }, + { target: 'four', guard: (ctx) => ctx.count === 4 }, { target: 'one' } ] } }, three: { after: { - 1000: [{ target: 'one', cond: (ctx) => ctx.count === -1 }], + 1000: [{ target: 'one', guard: (ctx) => ctx.count === -1 }], 2000: 'four' } }, @@ -86,8 +86,8 @@ const omniMachine = Machine({ }, transientCond: { always: [ - { target: 'two', cond: (ctx) => ctx.count === 2 }, - { target: 'three', cond: (ctx) => ctx.count === 3 }, + { target: 'two', guard: (ctx) => ctx.count === 2 }, + { target: 'three', guard: (ctx) => ctx.count === 3 }, { target: 'one' } ] }, diff --git a/packages/core/test/guards.test.ts b/packages/core/test/guards.test.ts index b0b54de5cb..86a8b032a1 100644 --- a/packages/core/test/guards.test.ts +++ b/packages/core/test/guards.test.ts @@ -1,4 +1,5 @@ -import { Machine, interpret, State } from '../src'; +import { Machine, interpret, State, createMachine } from '../src'; +import { and, not, or } from '../src/guards'; describe('guard conditions', () => { interface LightMachineCtx { @@ -23,16 +24,16 @@ describe('guard conditions', () => { TIMER: [ { target: 'green', - cond: ({ elapsed }) => elapsed < 100 + guard: ({ elapsed }) => elapsed < 100 }, { target: 'yellow', - cond: ({ elapsed }) => elapsed >= 100 && elapsed < 200 + guard: ({ elapsed }) => elapsed >= 100 && elapsed < 200 } ], EMERGENCY: { target: 'red', - cond: (_, event) => !!event.isEmergency + guard: (_, event) => !!event.isEmergency } } }, @@ -40,11 +41,11 @@ describe('guard conditions', () => { on: { TIMER: { target: 'red', - cond: 'minTimeElapsed' + guard: 'minTimeElapsed' }, TIMER_COND_OBJ: { target: 'red', - cond: { + guard: { type: 'minTimeElapsed' } } @@ -54,7 +55,7 @@ describe('guard conditions', () => { on: { BAD_COND: { target: 'red', - cond: 'doesNotExist' + guard: 'doesNotExist' } } } @@ -189,26 +190,26 @@ describe('guard conditions', () => { always: [ { target: 'B4', - cond: (_state, _event, { state: s }) => s.matches('A.A4') + guard: (_state, _event, { state: s }) => s.matches('A.A4') } ], on: { T1: [ { target: 'B1', - cond: (_state, _event, { state: s }) => s.matches('A.A1') + guard: (_state, _event, { state: s }) => s.matches('A.A1') } ], T2: [ { target: 'B2', - cond: (_state, _event, { state: s }) => s.matches('A.A2') + guard: (_state, _event, { state: s }) => s.matches('A.A2') } ], T3: [ { target: 'B3', - cond: (_state, _event, { state: s }) => s.matches('A.A3') + guard: (_state, _event, { state: s }) => s.matches('A.A3') } ] } @@ -264,11 +265,9 @@ describe('custom guards', () => { on: { EVENT: { target: 'active', - cond: { + guard: { type: 'custom', - prop: 'count', - op: 'greaterThan', - compare: 3 + params: { prop: 'count', op: 'greaterThan', compare: 3 } } } } @@ -279,7 +278,7 @@ describe('custom guards', () => { { guards: { custom: (ctx, e: Extract, meta) => { - const { prop, compare, op } = meta.cond as any; // TODO: fix + const { prop, compare, op } = meta.guard.params; if (op === 'greaterThan') { return ctx[prop] + e.value > compare; } @@ -317,16 +316,16 @@ describe('referencing guards', () => { active: { on: { EVENT: [ - { cond: 'string' }, + { guard: 'string' }, { - cond: function guardFn() { + guard: function guardFn() { return true; } }, { - cond: { + guard: { type: 'object', - foo: 'bar' + params: { foo: 'bar' } } } ] @@ -345,21 +344,23 @@ describe('referencing guards', () => { const [stringGuard, functionGuard, objectGuard] = def.states.active.on.EVENT; it('guard predicates should be able to be referenced from a string', () => { - expect(stringGuard.cond!.predicate).toBeDefined(); - expect(stringGuard.cond!.name).toEqual('string'); + expect(stringGuard.guard!.predicate).toBeDefined(); + expect(stringGuard.guard!.type).toEqual('string'); }); it('guard predicates should be able to be referenced from a function', () => { - expect(functionGuard.cond!.predicate).toBeDefined(); - expect(functionGuard.cond!.name).toEqual('guardFn'); + expect(functionGuard.guard!.predicate).toBeDefined(); + expect(functionGuard.guard!.type).toEqual('guardFn'); }); it('guard predicates should be able to be referenced from an object', () => { - expect(objectGuard.cond).toBeDefined(); - expect(objectGuard.cond).toEqual({ - type: 'object', - foo: 'bar' - }); + expect(objectGuard.guard).toBeDefined(); + expect(objectGuard.guard).toEqual( + expect.objectContaining({ + type: 'object', + params: expect.objectContaining({ foo: 'bar' }) + }) + ); }); it('should throw for guards with missing predicates', () => { @@ -369,7 +370,7 @@ describe('referencing guards', () => { states: { active: { on: { - EVENT: { target: 'inactive', cond: 'missing-predicate' } + EVENT: { target: 'inactive', guard: 'missing-predicate' } } }, inactive: {} @@ -389,7 +390,7 @@ describe('guards - other', () => { states: { a: { on: { - EVENT: [{ target: 'b', cond: () => false }, 'c'] + EVENT: [{ target: 'b', guard: () => false }, 'c'] } }, b: {}, @@ -403,3 +404,398 @@ describe('guards - other', () => { expect(service.state.value).toBe('c'); }); }); + +describe('guards with child guards', () => { + it('guards can contain child guards', () => { + expect.assertions(3); + + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: { + type: 'testGuard', + children: [ + { + type: 'customGuard', + predicate: () => true + }, + { type: 'customGuard' } + ], + predicate: (_, __, { guard }) => { + expect(guard.children).toHaveLength(2); + expect( + guard.children?.find( + (childGuard) => childGuard.type === 'customGuard' + )?.predicate + ).toBeInstanceOf(Function); + + return true; + } + } + } + } + }, + b: {} + } + }, + { + guards: { + customGuard: () => true + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + expect(nextState.matches('b')).toBeTruthy(); + }); +}); + +describe('not() guard', () => { + it('should guard with inline function', () => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: not(() => false) + } + } + }, + b: {} + } + }); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with string', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: not('falsy') + } + } + }, + b: {} + } + }, + { + guards: { + falsy: () => false + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with object', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: not({ type: 'greaterThan10', params: { value: 5 } }) + } + } + }, + b: {} + } + }, + { + guards: { + greaterThan10: (_, __, { guard }) => { + return guard.params.value > 10; + } + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with nested built-in guards', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: not(and([not('truthy'), 'truthy'])) + } + } + }, + b: {} + } + }, + { + guards: { + truthy: () => true, + falsy: () => false + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); +}); + +describe('and() guard', () => { + it('should guard with inline function', () => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: and([() => true, () => 1 + 1 === 2]) + } + } + }, + b: {} + } + }); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with string', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: and(['truthy', 'truthy']) + } + } + }, + b: {} + } + }, + { + guards: { + truthy: () => true + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with object', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: and([ + { type: 'greaterThan10', params: { value: 11 } }, + { type: 'greaterThan10', params: { value: 50 } } + ]) + } + } + }, + b: {} + } + }, + { + guards: { + greaterThan10: (_, __, { guard }) => { + return guard.params.value > 10; + } + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with nested built-in guards', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: and([ + () => true, + not('falsy'), + and([not('falsy'), 'truthy']) + ]) + } + } + }, + b: {} + } + }, + { + guards: { + truthy: () => true, + falsy: () => false + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); +}); + +describe('or() guard', () => { + it('should guard with inline function', () => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: or([() => false, () => 1 + 1 === 2]) + } + } + }, + b: {} + } + }); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with string', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: or(['falsy', 'truthy']) + } + } + }, + b: {} + } + }, + { + guards: { + falsy: () => false, + truthy: () => true + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with object', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: or([ + { type: 'greaterThan10', params: { value: 4 } }, + { type: 'greaterThan10', params: { value: 50 } } + ]) + } + } + }, + b: {} + } + }, + { + guards: { + greaterThan10: (_, __, { guard }) => { + return guard.params.value > 10; + } + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); + + it('should guard with nested built-in guards', () => { + const machine = createMachine( + { + initial: 'a', + states: { + a: { + on: { + EVENT: { + target: 'b', + guard: or([ + () => false, + not('truthy'), + and([not('falsy'), 'truthy']) + ]) + } + } + }, + b: {} + } + }, + { + guards: { + truthy: () => true, + falsy: () => false + } + } + ); + + const nextState = machine.transition(undefined, 'EVENT'); + + expect(nextState.matches('b')).toBeTruthy(); + }); +}); diff --git a/packages/core/test/interpreter.test.ts b/packages/core/test/interpreter.test.ts index 0aa59d9eda..87fb8ba551 100644 --- a/packages/core/test/interpreter.test.ts +++ b/packages/core/test/interpreter.test.ts @@ -951,7 +951,7 @@ describe('interpreter', () => { on: { NEXT: { target: 'finish', - cond: (_, e) => e.password === 'foo' + guard: (_, e) => e.password === 'foo' } } }, @@ -1036,7 +1036,7 @@ describe('interpreter', () => { on: { NEXT: { target: 'finish', - cond: (_, e) => e.password === 'foo' + guard: (_, e) => e.password === 'foo' } } }, @@ -1218,7 +1218,7 @@ describe('interpreter', () => { on: { EVENT: { target: 'active', - cond: (_, e: any) => e.id === 42 // TODO: fix unknown event type + guard: (_, e: any) => e.id === 42 // TODO: fix unknown event type }, ACTIVATE: 'active' } @@ -1495,7 +1495,7 @@ describe('interpreter', () => { idle: { on: { START: 'transient' } }, transient: { always: [ - { target: 'end', cond: 'alwaysFalse' }, + { target: 'end', guard: 'alwaysFalse' }, { target: 'next' } ] }, @@ -1540,7 +1540,7 @@ describe('interpreter', () => { }, always: { target: 'finished', - cond: (ctx) => ctx.count >= 5 + guard: (ctx) => ctx.count >= 5 } }, finished: { @@ -1594,7 +1594,7 @@ describe('interpreter', () => { active: { always: { target: 'finished', - cond: (ctx) => ctx.count >= 5 + guard: (ctx) => ctx.count >= 5 }, on: { INC: { @@ -1723,7 +1723,7 @@ describe('interpreter', () => { onDone: [ { target: 'success', - cond: (_, e) => { + guard: (_, e) => { return e.data === 42; } }, @@ -1773,7 +1773,7 @@ describe('interpreter', () => { on: { FIRED: { target: 'success', - cond: (_, e: AnyEventObject) => { + guard: (_, e: AnyEventObject) => { return e.value === 3; } } diff --git a/packages/core/test/invoke.test.ts b/packages/core/test/invoke.test.ts index cfca4ee95e..127720ec7f 100644 --- a/packages/core/test/invoke.test.ts +++ b/packages/core/test/invoke.test.ts @@ -40,7 +40,7 @@ const fetchMachine = Machine<{ userId: string | undefined }>({ on: { RESOLVE: { target: 'success', - cond: (ctx) => ctx.userId !== undefined + guard: (ctx) => ctx.userId !== undefined } } }, @@ -76,7 +76,7 @@ const fetcherMachine = Machine({ }, onDone: { target: 'received', - cond: (_, e) => { + guard: (_, e) => { // Should receive { user: { name: 'David' } } as event data return e.data.user.name === 'David'; } @@ -119,7 +119,7 @@ const intervalMachine = Machine<{ }, always: { target: 'finished', - cond: (ctx) => ctx.count === 3 + guard: (ctx) => ctx.count === 3 }, on: { INC: { actions: assign({ count: (ctx) => ctx.count + 1 }) }, @@ -167,7 +167,7 @@ describe('invoke', () => { }, always: { target: 'stop', - cond: (ctx) => ctx.count === 2 + guard: (ctx) => ctx.count === 2 }, on: { INC: { @@ -235,7 +235,7 @@ describe('invoke', () => { }, always: { target: 'stop', - cond: (ctx) => ctx.count === -3 + guard: (ctx) => ctx.count === -3 }, on: { DEC: { actions: assign({ count: (ctx) => ctx.count - 1 }) }, @@ -285,7 +285,7 @@ describe('invoke', () => { INCREMENT: [ { target: 'done', - cond: (ctx) => { + guard: (ctx) => { actual.push('child got INCREMENT'); return ctx.count >= 2; } @@ -386,7 +386,7 @@ describe('invoke', () => { INCREMENT: [ { target: 'done', - cond: (ctx) => { + guard: (ctx) => { actual.push('child got INCREMENT'); return ctx.count >= 2; } @@ -514,7 +514,7 @@ describe('invoke', () => { on: { SUCCESS: { target: 'success', - cond: (_, e) => { + guard: (_, e) => { return e.data === 42; } } @@ -565,7 +565,7 @@ describe('invoke', () => { on: { SUCCESS: { target: 'success', - cond: (_, e) => { + guard: (_, e) => { return e.data === 42; } } @@ -832,7 +832,7 @@ describe('invoke', () => { src: invokeMachine(pongMachine), onDone: { target: 'success', - cond: (_, e) => e.data.secret === 'pingpong' + guard: (_, e) => e.data.secret === 'pingpong' } } }, @@ -903,7 +903,7 @@ describe('invoke', () => { ), onDone: { target: 'success', - cond: (ctx, e) => { + guard: (ctx, e) => { return e.data === ctx.id; } }, @@ -1313,7 +1313,7 @@ describe('invoke', () => { on: { CALLBACK: { target: 'last', - cond: (_, e) => e.data === 42 + guard: (_, e) => e.data === 42 } } }, @@ -1559,7 +1559,7 @@ describe('invoke', () => { }), onError: { target: 'failed', - cond: (_, e) => { + guard: (_, e) => { return e.data instanceof Error && e.data.message === 'test'; } } @@ -1616,7 +1616,7 @@ describe('invoke', () => { }), onError: { target: 'failed', - cond: (_, e) => { + guard: (_, e) => { return e.data instanceof Error && e.data.message === 'test'; } } @@ -1691,7 +1691,7 @@ describe('invoke', () => { }), onError: { target: 'failed', - cond: () => { + guard: () => { errorHandlersCalled++; return false; } @@ -1705,7 +1705,7 @@ describe('invoke', () => { }), onError: { target: 'failed', - cond: () => { + guard: () => { errorHandlersCalled++; return false; } @@ -1845,7 +1845,7 @@ describe('invoke', () => { }, always: { target: 'counted', - cond: (ctx) => ctx.count === 5 + guard: (ctx) => ctx.count === 5 }, on: { COUNT: { actions: assign({ count: (_, e) => e.value }) } @@ -1895,7 +1895,7 @@ describe('invoke', () => { ), onDone: { target: 'counted', - cond: (ctx) => ctx.count === 4 + guard: (ctx) => ctx.count === 4 } }, on: { @@ -1947,7 +1947,7 @@ describe('invoke', () => { ), onError: { target: 'success', - cond: (ctx, e) => { + guard: (ctx, e) => { expect(e.data.message).toEqual('some error'); return ctx.count === 4 && e.data.message === 'some error'; } @@ -2304,7 +2304,7 @@ describe('invoke', () => { src: invokeMachine(child), onError: { target: 'two', - cond: (_, event) => event.data === 'oops' + guard: (_, event) => event.data === 'oops' } } }, @@ -2346,7 +2346,7 @@ describe('invoke', () => { src: invokeMachine(child), onError: { target: 'two', - cond: (_, event) => { + guard: (_, event) => { expect(event.data).toEqual(42); return true; } diff --git a/packages/core/test/json.test.ts b/packages/core/test/json.test.ts index 0cb2bc02b3..4ea3c4eaec 100644 --- a/packages/core/test/json.test.ts +++ b/packages/core/test/json.test.ts @@ -51,7 +51,7 @@ describe('json', () => { on: { TO_FOO: { target: ['foo', 'bar'], - cond: (ctx) => !!ctx.string + guard: (ctx) => !!ctx.string } }, after: { diff --git a/packages/core/test/machine.test.ts b/packages/core/test/machine.test.ts index 44ae7cdabd..b93554e257 100644 --- a/packages/core/test/machine.test.ts +++ b/packages/core/test/machine.test.ts @@ -66,7 +66,7 @@ const configMachine = Machine( on: { EVENT: { target: 'bar', - cond: 'someCondition' + guard: 'someCondition' } } }, diff --git a/packages/core/test/state.test.ts b/packages/core/test/state.test.ts index 97a4d93dfb..5748f9c2fa 100644 --- a/packages/core/test/state.test.ts +++ b/packages/core/test/state.test.ts @@ -36,7 +36,7 @@ const machine = Machine({ TO_TWO: 'two', TO_TWO_MAYBE: { target: 'two', - cond: function maybe() { + guard: function maybe() { return true; } }, @@ -227,7 +227,7 @@ describe('State', () => { CHANGE: [ { target: '.valid', - cond: () => true + guard: () => true }, { target: '.invalid' @@ -521,7 +521,7 @@ describe('State', () => { ).toContainEqual( expect.objectContaining({ eventType: 'TO_TWO_MAYBE', - cond: expect.objectContaining({ name: 'maybe' }) + guard: expect.objectContaining({ type: 'maybe' }) }) ); }); diff --git a/packages/core/test/stateIn.test.ts b/packages/core/test/stateIn.test.ts index 02b9e87df7..360a457aa1 100644 --- a/packages/core/test/stateIn.test.ts +++ b/packages/core/test/stateIn.test.ts @@ -11,11 +11,11 @@ const machine = Machine({ on: { EVENT2: { target: 'a2', - cond: stateIn({ b: 'b2' }) + guard: stateIn({ b: 'b2' }) }, EVENT3: { target: 'a2', - cond: stateIn('#b_b2') + guard: stateIn('#b_b2') } } }, @@ -31,7 +31,7 @@ const machine = Machine({ on: { EVENT: { target: 'b2', - cond: stateIn('#a_a2') + guard: stateIn('#a_a2') } } }, @@ -44,7 +44,7 @@ const machine = Machine({ states: { foo1: { on: { - EVENT_DEEP: { target: 'foo2', cond: stateIn('#bar1') } + EVENT_DEEP: { target: 'foo2', guard: stateIn('#bar1') } } }, foo2: {} @@ -83,7 +83,7 @@ const lightMachine = Machine({ TIMER: [ { target: 'green', - cond: stateIn({ red: 'stop' }) + guard: stateIn({ red: 'stop' }) } ] } diff --git a/packages/core/test/transient.test.ts b/packages/core/test/transient.test.ts index 39f9caa667..aa0bc94274 100644 --- a/packages/core/test/transient.test.ts +++ b/packages/core/test/transient.test.ts @@ -12,8 +12,8 @@ const greetingMachine = Machine({ pending: { on: { '': [ - { target: 'morning', cond: (ctx) => ctx.hour < 12 }, - { target: 'afternoon', cond: (ctx) => ctx.hour < 18 }, + { target: 'morning', guard: (ctx) => ctx.hour < 12 }, + { target: 'afternoon', guard: (ctx) => ctx.hour < 18 }, { target: 'evening' } ] } @@ -39,9 +39,9 @@ describe('transient states (eventless transitions)', () => { on: { // eventless transition '': [ - { target: 'D', cond: ({ data }) => !data }, // no data returned - { target: 'B', cond: ({ status }) => status === 'Y' }, - { target: 'C', cond: ({ status }) => status === 'X' }, + { target: 'D', guard: ({ data }) => !data }, // no data returned + { target: 'B', guard: ({ status }) => status === 'Y' }, + { target: 'C', guard: ({ status }) => status === 'X' }, { target: 'F' } // default, or just the string 'F' ] } @@ -212,7 +212,7 @@ describe('transient states (eventless transitions)', () => { on: { '': { target: 'A4', - cond: stateIn({ B: 'B3' }) + guard: stateIn({ B: 'B3' }) } } }, @@ -232,7 +232,7 @@ describe('transient states (eventless transitions)', () => { on: { '': { target: 'B3', - cond: stateIn({ A: 'A2' }) + guard: stateIn({ A: 'A2' }) } } }, @@ -240,7 +240,7 @@ describe('transient states (eventless transitions)', () => { on: { '': { target: 'B4', - cond: stateIn({ A: 'A3' }) + guard: stateIn({ A: 'A3' }) } } }, @@ -273,7 +273,7 @@ describe('transient states (eventless transitions)', () => { A3: { always: { target: 'A4', - cond: stateIn({ B: 'B3' }) + guard: stateIn({ B: 'B3' }) } }, A4: {} @@ -291,13 +291,13 @@ describe('transient states (eventless transitions)', () => { B2: { always: { target: 'B3', - cond: stateIn({ A: 'A2' }) + guard: stateIn({ A: 'A2' }) } }, B3: { always: { target: 'B4', - cond: stateIn({ A: 'A3' }) + guard: stateIn({ A: 'A3' }) } }, B4: {} @@ -333,7 +333,7 @@ describe('transient states (eventless transitions)', () => { on: { '': { target: 'B2', - cond: stateIn({ A: 'A2' }) + guard: stateIn({ A: 'A2' }) } } }, @@ -347,7 +347,7 @@ describe('transient states (eventless transitions)', () => { on: { '': { target: 'C2', - cond: stateIn({ A: 'A2' }) + guard: stateIn({ A: 'A2' }) } } }, @@ -383,7 +383,7 @@ describe('transient states (eventless transitions)', () => { B1: { always: { target: 'B2', - cond: stateIn({ A: 'A2' }) + guard: stateIn({ A: 'A2' }) } }, B2: {} @@ -395,7 +395,7 @@ describe('transient states (eventless transitions)', () => { C1: { always: { target: 'C2', - cond: stateIn('#A2') + guard: stateIn('#A2') } }, C2: {} @@ -587,7 +587,7 @@ describe('transient states (eventless transitions)', () => { '': [ { target: '.success', - cond: (ctx) => { + guard: (ctx) => { return ctx.count > 0; } } @@ -625,7 +625,7 @@ describe('transient states (eventless transitions)', () => { always: [ { target: '.success', - cond: (ctx) => { + guard: (ctx) => { return ctx.count > 0; } } @@ -652,7 +652,7 @@ describe('transient states (eventless transitions)', () => { always: [ { target: `finished`, - cond: (ctx) => ctx.duration < 1000 + guard: (ctx) => ctx.duration < 1000 }, { target: `active` diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index 39388a8acc..487fb33684 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -70,7 +70,10 @@ describe('StateSchema', () => { on: { PED_COUNTDOWN: { target: 'stop', - cond: (ctx, e: { type: 'PED_COUNTDOWN'; duration: number }) => { + guard: ( + ctx, + e: { type: 'PED_COUNTDOWN'; duration: number } + ) => { return e.duration === 0 && ctx.elapsed > 0; } } @@ -244,23 +247,23 @@ describe('Raise events', () => { actions: raise({ type: 'ALOHA' }), - cond: (_ctx, ev) => !!ev.aloha + guard: (_ctx, ev) => !!ev.aloha }, { actions: raise({ type: 'MORNING' }), - cond: (ctx) => ctx.hour < 12 + guard: (ctx) => ctx.hour < 12 }, { actions: raise({ type: 'AFTERNOON' }), - cond: (ctx) => ctx.hour < 18 + guard: (ctx) => ctx.hour < 18 }, { actions: raise({ type: 'EVENING' }), - cond: (ctx) => ctx.hour < 22 + guard: (ctx) => ctx.hour < 22 } ] } diff --git a/packages/xstate-fsm/src/index.ts b/packages/xstate-fsm/src/index.ts index a41125b646..1bd792c87b 100644 --- a/packages/xstate-fsm/src/index.ts +++ b/packages/xstate-fsm/src/index.ts @@ -156,7 +156,7 @@ export function createMachine< typeof state === 'string' ? { value: state, context: fsmConfig.context! } : state; - const eventObject = toEventObject(event); + const eventObject = toEventObject(event) as TEvent; const stateConfig = fsmConfig.states[value]; if (!IS_PRODUCTION) { @@ -177,12 +177,16 @@ export function createMachine< return createUnchangedState(value, context); } - const { target = value, actions = [], cond = () => true } = + const { + target = value, + actions = [], + guard = () => true + }: StateMachine.TransitionObject = typeof transition === 'string' ? { target: transition } : transition; - if (cond(context, eventObject)) { + if (guard(context, eventObject)) { const nextStateConfig = fsmConfig.states[target]; const allActions = ([] as any[]) .concat(stateConfig.exit, actions, nextStateConfig.entry) diff --git a/packages/xstate-fsm/src/types.ts b/packages/xstate-fsm/src/types.ts index fb4bf912ba..da75c6ce29 100644 --- a/packages/xstate-fsm/src/types.ts +++ b/packages/xstate-fsm/src/types.ts @@ -47,13 +47,18 @@ export declare namespace StateMachine { assignment: Assigner | PropertyAssigner; } + export interface TransitionObject< + TContext extends object, + TEvent extends EventObject + > { + target?: string; + actions?: SingleOrArray>; + guard?: (context: TContext, event: TEvent) => boolean; + } + export type Transition = | string - | { - target?: string; - actions?: SingleOrArray>; - cond?: (context: TContext, event: TEvent) => boolean; - }; + | TransitionObject; export interface State< TContext extends object, TEvent extends EventObject, diff --git a/packages/xstate-fsm/test/fsm.test.ts b/packages/xstate-fsm/test/fsm.test.ts index 9d339c1f81..235b53520e 100644 --- a/packages/xstate-fsm/test/fsm.test.ts +++ b/packages/xstate-fsm/test/fsm.test.ts @@ -57,7 +57,7 @@ describe('@xstate/fsm', () => { INC: { actions: assign({ count: (ctx) => ctx.count + 1 }) }, EMERGENCY: { target: 'red', - cond: (ctx, e) => ctx.count + e.value === 2 + guard: (ctx, e) => ctx.count + e.value === 2 } } }, diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index fc325aaadc..8e023d385a 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -77,14 +77,14 @@ describe('@xstate/graph', () => { EVENT: [ { target: 'foo', - cond: (_, e) => e.id === 'foo' + guard: (_, e) => e.id === 'foo' }, { target: 'bar' } ], STATE: [ { target: 'foo', - cond: (s) => s.id === 'foo' + guard: (s) => s.id === 'foo' }, { target: 'bar' } ] @@ -262,7 +262,7 @@ describe('@xstate/graph', () => { start: { always: { target: 'finish', - cond: (ctx) => ctx.count === 3 + guard: (ctx) => ctx.count === 3 }, on: { INC: { @@ -314,7 +314,7 @@ describe('@xstate/graph', () => { empty: { always: { target: 'full', - cond: (ctx) => ctx.count === 5 + guard: (ctx) => ctx.count === 5 }, on: { INC: { diff --git a/packages/xstate-immer/test/immer.test.ts b/packages/xstate-immer/test/immer.test.ts index 0a065535b6..15c09f4d39 100644 --- a/packages/xstate-immer/test/immer.test.ts +++ b/packages/xstate-immer/test/immer.test.ts @@ -187,7 +187,7 @@ describe('@xstate/immer', () => { on: { '': { target: 'success', - cond: (ctx) => { + guard: (ctx) => { return ctx.name === 'David' && ctx.age === 0; } } diff --git a/packages/xstate-react/test/useMachine.test.tsx b/packages/xstate-react/test/useMachine.test.tsx index 755a43634e..dfd5d8425d 100644 --- a/packages/xstate-react/test/useMachine.test.tsx +++ b/packages/xstate-react/test/useMachine.test.tsx @@ -45,7 +45,7 @@ describe('useMachine hook', () => { actions: assign({ data: (_, e) => e.data }), - cond: (_, e) => e.data.length + guard: (_, e) => e.data.length } } }, diff --git a/packages/xstate-react/test/useService.test.tsx b/packages/xstate-react/test/useService.test.tsx index fcfff21fba..4f0009c3bc 100644 --- a/packages/xstate-react/test/useService.test.tsx +++ b/packages/xstate-react/test/useService.test.tsx @@ -306,7 +306,7 @@ describe('useService hook', () => { on: { EVENT: { target: 'second', - cond: (_, e) => e.value === 42 + guard: (_, e) => e.value === 42 } } }, diff --git a/packages/xstate-scxml/src/index.ts b/packages/xstate-scxml/src/index.ts index c138b573e9..6f023c57b4 100644 --- a/packages/xstate-scxml/src/index.ts +++ b/packages/xstate-scxml/src/index.ts @@ -62,8 +62,8 @@ export function transitionToSCXML( name: 'transition', attributes: cleanAttributes({ event: transition.eventType, - cond: transition.cond - ? functionToExpr(transition.cond.predicate) + guard: transition.guard?.predicate + ? functionToExpr(transition.guard.predicate) : undefined, target: (transition.target || []) .map((stateNode) => stateNode.id) diff --git a/packages/xstate-scxml/test/fixtures/assign-current-small-step/test0.ts b/packages/xstate-scxml/test/fixtures/assign-current-small-step/test0.ts index b1c69a80b4..105e35f406 100644 --- a/packages/xstate-scxml/test/fixtures/assign-current-small-step/test0.ts +++ b/packages/xstate-scxml/test/fixtures/assign-current-small-step/test0.ts @@ -12,7 +12,7 @@ export default Machine({ on: { t: { target: 'b', - cond: (ctx) => { + guard: (ctx) => { return ctx.x === 99; }, actions: assign({ @@ -32,7 +32,7 @@ export default Machine({ '': [ { target: 'c', - cond: (ctx) => ctx.x === 200 + guard: (ctx) => ctx.x === 200 }, { target: 'f' } ] diff --git a/packages/xstate-scxml/test/fixtures/assign-current-small-step/test1.ts b/packages/xstate-scxml/test/fixtures/assign-current-small-step/test1.ts index 5ec2c2313d..e89e9be531 100644 --- a/packages/xstate-scxml/test/fixtures/assign-current-small-step/test1.ts +++ b/packages/xstate-scxml/test/fixtures/assign-current-small-step/test1.ts @@ -21,7 +21,7 @@ export default Machine({ '': [ { target: 'b', - cond: (ctx) => { + guard: (ctx) => { return ctx.i < 10; }, actions: [ @@ -33,7 +33,7 @@ export default Machine({ }, { target: '#c', - cond: (ctx) => ctx.i === 10 + guard: (ctx) => ctx.i === 10 } ] } diff --git a/packages/xstate-scxml/test/scxml.test.ts b/packages/xstate-scxml/test/scxml.test.ts index b5e7232c0c..65cc1a487e 100644 --- a/packages/xstate-scxml/test/scxml.test.ts +++ b/packages/xstate-scxml/test/scxml.test.ts @@ -239,7 +239,7 @@ xdescribe('transition to SCXML', () => { SOME_EVENT: { target: 'next', internal: true, - cond: () => true, + guard: () => true, actions: ['foo', 'bar'] } } diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 21c43619fe..e6719a90f6 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -41,7 +41,7 @@ const dieHardMachine = Machine( on: { '': { target: 'success', - cond: 'weHave4Gallons' + guard: 'weHave4Gallons' }, POUR_3_TO_5: { actions: pour3to5 @@ -417,7 +417,7 @@ describe('events', () => { SUBMIT: [ { target: 'thanks', - cond: (_, e) => !!e.value.length + guard: (_, e) => !!e.value.length }, { target: '.invalid' diff --git a/packages/xstate-vue/test/useMachine.test.ts b/packages/xstate-vue/test/useMachine.test.ts index e57c9d2642..90b63cf8a3 100644 --- a/packages/xstate-vue/test/useMachine.test.ts +++ b/packages/xstate-vue/test/useMachine.test.ts @@ -37,7 +37,7 @@ describe('useMachine composition function', () => { actions: assign({ data: (_, e) => e.data }), - cond: (_, e) => e.data.length + guard: (_, e) => e.data.length } } },