Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plenty-parks-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': minor
---

Add `@xstate.guard` inspection events for debugging guard evaluations during transitions. When using the `inspect` option, guard evaluation events are now emitted showing the guard name, parameters, and result (true/false). This makes it easier to debug why transitions didn't happen as expected.
5 changes: 0 additions & 5 deletions .changeset/six-trees-hide.md

This file was deleted.

8 changes: 6 additions & 2 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,13 @@ export class StateMachine<
TMeta,
TStateSchema
>,
event: TEvent
event: TEvent,
actorScope?: AnyActorScope
): Array<TransitionDefinition<TContext, TEvent>> {
return transitionNode(this.root, snapshot.value, snapshot, event) || [];
return (
transitionNode(this.root, snapshot.value, snapshot, event, actorScope) ||
[]
);
}

/**
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import type {
AnyStateNodeConfig,
ProvidedActor,
NonReducibleUnknown,
EventDescriptor
EventDescriptor,
AnyActorScope
} from './types.ts';
import {
createInvokeId,
Expand Down Expand Up @@ -374,7 +375,8 @@ export class StateNode<
any, // TMeta
any // TStateSchema
>,
event: TEvent
event: TEvent,
actorScope?: AnyActorScope
): TransitionDefinition<TContext, TEvent>[] | undefined {
const eventType = event.type;
const actions: UnknownAction[] = [];
Expand All @@ -400,7 +402,8 @@ export class StateNode<
guard,
resolvedContext,
event,
snapshot
snapshot,
actorScope
);
} catch (err: any) {
const guardType =
Expand Down
47 changes: 37 additions & 10 deletions packages/core/src/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import type {
WithDynamicParams,
Identity,
Elements,
DoNotInfer
DoNotInfer,
AnyActorScope
} from './types.ts';
import { isStateId } from './stateUtils.ts';

Expand Down Expand Up @@ -341,7 +342,8 @@ export function evaluateGuard<
guard: UnknownGuard | UnknownInlineGuard,
context: TContext,
event: TExpressionEvent,
snapshot: AnyMachineSnapshot
snapshot: AnyMachineSnapshot,
actorScope?: AnyActorScope
): boolean {
const { machine } = snapshot;
const isInline = typeof guard === 'function';
Expand All @@ -361,7 +363,7 @@ export function evaluateGuard<
}

if (typeof resolved !== 'function') {
return evaluateGuard(resolved!, context, event, snapshot);
return evaluateGuard(resolved!, context, event, snapshot, actorScope);
}

const guardArgs = {
Expand All @@ -378,18 +380,43 @@ export function evaluateGuard<
: guard.params
: undefined;

let result: boolean;

if (!('check' in resolved)) {
// the existing type of `.guards` assumes non-nullable `TExpressionGuard`
// inline guards expect `TExpressionGuard` to be set to `undefined`
// it's fine to cast this here, our logic makes sure that we call those 2 "variants" correctly
return resolved(guardArgs, guardParams as never);
result = resolved(guardArgs, guardParams as never);
} else {
const builtinGuard = resolved as unknown as BuiltinGuard;
result = builtinGuard.check(
snapshot,
guardArgs,
resolved // this holds all params
);
}

const builtinGuard = resolved as unknown as BuiltinGuard;
// Emit guard inspection event if actorScope is provided
if (actorScope) {
const guardType =
typeof guard === 'string'
? guard
: typeof guard === 'object' && 'type' in guard
? guard.type
: isInline
? '<inline>'
: '<unknown>';

actorScope.system._sendInspectionEvent({
type: '@xstate.guard',
actorRef: actorScope.self,
guard: {
type: guardType,
params: guardParams
},
result
});
}

return builtinGuard.check(
snapshot,
guardArgs,
resolved // this holds all params
);
return result;
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type {
InspectedActionEvent,
InspectedActorEvent,
InspectedEventEvent,
InspectedGuardEvent,
InspectedMicrostepEvent,
InspectedSnapshotEvent,
InspectionEvent
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export type InspectionEvent =
| InspectedEventEvent
| InspectedActorEvent
| InspectedMicrostepEvent
| InspectedActionEvent;
| InspectedActionEvent
| InspectedGuardEvent;

interface BaseInspectionEventProperties {
rootId: string; // the session ID of the root
Expand Down Expand Up @@ -57,3 +58,12 @@ export interface InspectedEventEvent extends BaseInspectionEventProperties {
export interface InspectedActorEvent extends BaseInspectionEventProperties {
type: '@xstate.actor';
}

export interface InspectedGuardEvent extends BaseInspectionEventProperties {
type: '@xstate.guard';
guard: {
type: string;
params: unknown;
};
result: boolean;
}
84 changes: 64 additions & 20 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,13 +623,14 @@ function transitionAtomicNode<
any, // TMeta
any // TStateSchema
>,
event: TEvent
event: TEvent,
actorScope?: AnyActorScope
): Array<TransitionDefinition<TContext, TEvent>> | undefined {
const childStateNode = getStateNode(stateNode, stateValue);
const next = childStateNode.next(snapshot, event);
const next = childStateNode.next(snapshot, event, actorScope);

if (!next || !next.length) {
return stateNode.next(snapshot, event);
return stateNode.next(snapshot, event, actorScope);
}

return next;
Expand All @@ -651,7 +652,8 @@ function transitionCompoundNode<
any, // TMeta
any // TStateSchema
>,
event: TEvent
event: TEvent,
actorScope?: AnyActorScope
): Array<TransitionDefinition<TContext, TEvent>> | undefined {
const subStateKeys = Object.keys(stateValue);

Expand All @@ -660,11 +662,12 @@ function transitionCompoundNode<
childStateNode,
stateValue[subStateKeys[0]]!,
snapshot,
event
event,
actorScope
);

if (!next || !next.length) {
return stateNode.next(snapshot, event);
return stateNode.next(snapshot, event, actorScope);
}

return next;
Expand All @@ -686,7 +689,8 @@ function transitionParallelNode<
any, // TMeta
any // TStateSchema
>,
event: TEvent
event: TEvent,
actorScope?: AnyActorScope
): Array<TransitionDefinition<TContext, TEvent>> | undefined {
const allInnerTransitions: Array<TransitionDefinition<TContext, TEvent>> = [];

Expand All @@ -702,14 +706,15 @@ function transitionParallelNode<
subStateNode,
subStateValue,
snapshot,
event
event,
actorScope
);
if (innerTransitions) {
allInnerTransitions.push(...innerTransitions);
}
}
if (!allInnerTransitions.length) {
return stateNode.next(snapshot, event);
return stateNode.next(snapshot, event, actorScope);
}

return allInnerTransitions;
Expand All @@ -731,20 +736,39 @@ export function transitionNode<
any,
any // TStateSchema
>,
event: TEvent
event: TEvent,
actorScope?: AnyActorScope
): Array<TransitionDefinition<TContext, TEvent>> | undefined {
// leaf node
if (typeof stateValue === 'string') {
return transitionAtomicNode(stateNode, stateValue, snapshot, event);
return transitionAtomicNode(
stateNode,
stateValue,
snapshot,
event,
actorScope
);
}

// compound node
if (Object.keys(stateValue).length === 1) {
return transitionCompoundNode(stateNode, stateValue, snapshot, event);
return transitionCompoundNode(
stateNode,
stateValue,
snapshot,
event,
actorScope
);
}

// parallel node
return transitionParallelNode(stateNode, stateValue, snapshot, event);
return transitionParallelNode(
stateNode,
stateValue,
snapshot,
event,
actorScope
);
}

function getHistoryNodes(stateNode: AnyStateNode): Array<AnyStateNode> {
Expand Down Expand Up @@ -1620,7 +1644,11 @@ export function macrostep(
const currentEvent = nextEvent;
const isErr = isErrorActorEvent(currentEvent);

const transitions = selectTransitions(currentEvent, nextSnapshot);
const transitions = selectTransitions(
currentEvent,
nextSnapshot,
actorScope
);

if (isErr && !transitions.length) {
// TODO: we should likely only allow transitions selected by very explicit descriptors
Expand Down Expand Up @@ -1652,7 +1680,7 @@ export function macrostep(
while (nextSnapshot.status === 'active') {
let enabledTransitions: AnyTransitionDefinition[] =
shouldSelectEventlessTransitions
? selectEventlessTransitions(nextSnapshot, nextEvent)
? selectEventlessTransitions(nextSnapshot, nextEvent, actorScope)
: [];

// eventless transitions should always be selected after selecting *regular* transitions
Expand All @@ -1664,7 +1692,11 @@ export function macrostep(
break;
}
nextEvent = internalQueue.shift()!;
enabledTransitions = selectTransitions(nextEvent, nextSnapshot);
enabledTransitions = selectTransitions(
nextEvent,
nextSnapshot,
actorScope
);
}

nextSnapshot = microstep(
Expand Down Expand Up @@ -1706,14 +1738,20 @@ function stopChildren(

function selectTransitions(
event: AnyEventObject,
nextState: AnyMachineSnapshot
nextState: AnyMachineSnapshot,
actorScope: AnyActorScope
): AnyTransitionDefinition[] {
return nextState.machine.getTransitionData(nextState as any, event);
return nextState.machine.getTransitionData(
nextState as any,
event,
actorScope
);
}

function selectEventlessTransitions(
nextState: AnyMachineSnapshot,
event: AnyEventObject
event: AnyEventObject,
actorScope: AnyActorScope
): AnyTransitionDefinition[] {
const enabledTransitionSet: Set<AnyTransitionDefinition> = new Set();
const atomicStates = nextState._nodes.filter(isAtomicStateNode);
Expand All @@ -1728,7 +1766,13 @@ function selectEventlessTransitions(
for (const transition of s.always) {
if (
transition.guard === undefined ||
evaluateGuard(transition.guard, nextState.context, event, nextState)
evaluateGuard(
transition.guard,
nextState.context,
event,
nextState,
actorScope
)
) {
enabledTransitionSet.add(transition);
break loop;
Expand Down
Loading