Skip to content
Open
Show file tree
Hide file tree
Changes from all 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/giant-seals-show.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': minor
---

Add evaluated guards to microstep event 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
46 changes: 36 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,42 @@ 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;
// Collect guard evaluation for microstep inspection event
if (actorScope) {
const guardType =
typeof guard === 'string'
? guard
: typeof guard === 'object' && 'type' in guard
? guard.type
: isInline
? '<inline>'
: '<unknown>';

if (!actorScope._collectedGuards) {
actorScope._collectedGuards = [];
}
actorScope._collectedGuards.push({
type: guardType,
params: guardParams,
result
});
}

return builtinGuard.check(
snapshot,
guardArgs,
resolved // this holds all params
);
return result;
}
5 changes: 5 additions & 0 deletions packages/core/src/inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export interface InspectedMicrostepEvent extends BaseInspectionEventProperties {
event: AnyEventObject; // { type: string, ... }
snapshot: Snapshot<unknown>;
_transitions: AnyTransitionDefinition[];
_guards: Array<{
type: string;
params: unknown;
result: boolean;
}>;
}

export interface InspectedActionEvent extends BaseInspectionEventProperties {
Expand Down
Loading