Skip to content

Commit

Permalink
Add inspection event for microsteps (#4693)
Browse files Browse the repository at this point in the history
* Add @xstate/microstep inspection event + tests

* Optimization: make _sendInspectionEvent "lazy-ish"

* Revert StateMachine.ts

* Improve types

* Update packages/core/src/stateUtils.ts

Co-authored-by: Mateusz Burzyński <[email protected]>

* Update packages/core/src/stateUtils.ts

Co-authored-by: Mateusz Burzyński <[email protected]>

* Add tests

* Reusable util

* Add inspection.ts

* Add @xstate.action inspection event

* Update test

* Make sendInspectionEvent non-optional

* Changeset

* Rename .transitions to ._transitions

* Changeset

* Update packages/core/src/stateUtils.ts

Co-authored-by: Mateusz Burzyński <[email protected]>

* Update packages/core/src/stateUtils.ts

Co-authored-by: Mateusz Burzyński <[email protected]>

* Update

* Update test snapshots

---------

Co-authored-by: Mateusz Burzyński <[email protected]>
  • Loading branch information
davidkpiano and Andarist authored Feb 15, 2024
1 parent 7a8796f commit 11b6a1a
Show file tree
Hide file tree
Showing 16 changed files with 838 additions and 115 deletions.
47 changes: 47 additions & 0 deletions .changeset/witty-panthers-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
'xstate': minor
---

You can now inspect microsteps (`@xstate.microstep`) and actions (`@xstate.action`):

```ts
const machine = createMachine({
initial: 'a',
states: {
a: {
on: {
event: 'b'
}
},
b: {
entry: 'someAction',
always: 'c'
},
c: {}
}
});

const actor = createActor(machine, {
inspect: (inspEvent) => {
if (inspEvent.type === '@xstate.microstep') {
console.log(inspEvent.snapshot);
// logs:
// { value: 'a', … }
// { value: 'b', … }
// { value: 'c', … }

console.log(inspEvent.event);
// logs:
// { type: 'event', … }
} else if (inspEvent.type === '@xstate.action') {
console.log(inspEvent.action);
// logs:
// { type: 'someAction', … }
}
}
});

actor.start();

actor.send({ type: 'event' });
```
2 changes: 1 addition & 1 deletion examples/mongodb-credit-check-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ app.post("/workflows/:workflowId", async (req, res) => {
res
.status(200)
.send(
"Event received. Issue a GET request to see the current workflow state"
"Event received. Issue a GET request to see the current workflow state",
);
});

Expand Down
16 changes: 8 additions & 8 deletions examples/mongodb-credit-check-api/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,28 @@ export const creditCheckMachine = setup({
actors: {
checkBureau: fromPromise(
async ({ input }: { input: { ssn: string; bureauName: string } }) =>
await checkBureauService(input)
await checkBureauService(input),
),
checkReportsTable: fromPromise(
async ({ input }: { input: { ssn: string; bureauName: string } }) =>
await checkReportsTable(input)
await checkReportsTable(input),
),
verifyCredentials: fromPromise(
async ({ input }: { input: userCredential }) =>
await verifyCredentials(input)
await verifyCredentials(input),
),
determineMiddleScore: fromPromise(
async ({ input }: { input: number[] }) =>
await determineMiddleScore(input)
await determineMiddleScore(input),
),
generateInterestRates: fromPromise(
async ({ input }: { input: number }) => await generateInterestRate(input)
async ({ input }: { input: number }) => await generateInterestRate(input),
),
},
actions: {
saveReport: (
{ context }: { context: CreditProfile },
params: { bureauName: string }
params: { bureauName: string },
) => {
console.log("saving report to the database...");
saveCreditReport({
Expand All @@ -58,7 +58,7 @@ export const creditCheckMachine = setup({
emailUser: function ({ context }) {
console.log(
"emailing user with their interest rate options: ",
context.InterestRateOptions
context.InterestRateOptions,
);
},
saveCreditProfile: async function ({ context }) {
Expand All @@ -71,7 +71,7 @@ export const creditCheckMachine = setup({
context.FirstName,
context.LastName,
context.InterestRateOptions,
context.MiddleScore
context.MiddleScore,
);
},
},
Expand Down
2 changes: 1 addition & 1 deletion examples/mongodb-credit-check-api/models/creditProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export default class CreditProfile {
public ErrorMessage: string,
public MiddleScore: number,
public InterestRateOptions: number[],
public _id?: ObjectId
public _id?: ObjectId,
) {}
}
2 changes: 1 addition & 1 deletion examples/mongodb-credit-check-api/models/creditReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export default class CreditReport {
public ssn: string,
public bureauName: string,
public creditScore: number,
public _id?: ObjectId
public _id?: ObjectId,
) {}
}
4 changes: 2 additions & 2 deletions examples/mongodb-credit-check-api/services/actorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ export async function getDurableActor({
workflowId,
persistedState,
},
{ upsert: true }
{ upsert: true },
);

if (!result?.acknowledged) {
throw new Error(
"Error persisting actor state. Verify db connection is configured correctly."
"Error persisting actor state. Verify db connection is configured correctly.",
);
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export async function saveCreditReport(report: CreditReport) {
bureauName: report.bureauName,
},
report,
{ upsert: true }
{ upsert: true },
);
} catch (err) {
console.log("Error saving credit report", err);
Expand All @@ -114,7 +114,7 @@ export async function saveCreditProfile(profile: CreditProfile) {
ssn: profile.SSN,
},
profile,
{ upsert: true }
{ upsert: true },
);
} catch (err) {
console.log("Error saving credit profile", err);
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,7 @@ export class StateMachine<
): Array<
MachineSnapshot<TContext, TEvent, TChildren, TStateValue, TTag, TOutput>
> {
return macrostep(snapshot, event, actorScope)
.microstates as (typeof snapshot)[];
return macrostep(snapshot, event, actorScope).microstates;
}

public getTransitionData(
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ export * from './typegenTypes.ts';
export * from './types.ts';
export { waitFor } from './waitFor.ts';
import { createMachine } from './createMachine.ts';
export { getNextSnapshot, getInitialSnapshot } from './getNextSnapshot.ts';
export { getInitialSnapshot, getNextSnapshot } from './getNextSnapshot.ts';
import { Actor, createActor, interpret, Interpreter } from './createActor.ts';
import { StateNode } from './StateNode.ts';
// TODO: decide from where those should be exported
export { and, not, or, stateIn } from './guards.ts';
export { setup } from './setup.ts';
export type { ActorSystem } from './system.ts';
export type {
ActorSystem,
InspectedActorEvent,
InspectedEventEvent,
InspectedSnapshotEvent,
InspectionEvent
} from './system.ts';
} from './inspection.ts';
export { toPromise } from './toPromise.ts';
export {
getAllOwnEventDescriptors as __unsafe_getAllOwnEventDescriptors,
Expand Down
58 changes: 58 additions & 0 deletions packages/core/src/inspection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
AnyActorRef,
AnyEventObject,
AnyTransitionDefinition,
Snapshot
} from './types.ts';

export type InspectionEvent =
| InspectedSnapshotEvent
| InspectedEventEvent
| InspectedActorEvent
| InspectedMicrostepEvent
| InspectedActionEvent;

export interface BaseInspectionEventProperties {
rootId: string; // the session ID of the root
/**
* The relevant actorRef for the inspection event.
* - For snapshot events, this is the `actorRef` of the snapshot.
* - For event events, this is the target `actorRef` (recipient of event).
* - For actor events, this is the `actorRef` of the registered actor.
*/
actorRef: AnyActorRef;
}

export interface InspectedSnapshotEvent extends BaseInspectionEventProperties {
type: '@xstate.snapshot';
event: AnyEventObject; // { type: string, ... }
snapshot: Snapshot<unknown>;
}

export interface InspectedMicrostepEvent extends BaseInspectionEventProperties {
type: '@xstate.microstep';
event: AnyEventObject; // { type: string, ... }
snapshot: Snapshot<unknown>;
_transitions: AnyTransitionDefinition[];
}

export interface InspectedActionEvent extends BaseInspectionEventProperties {
type: '@xstate.action';
action: {
type: string;
params: Record<string, unknown>;
};
}

export interface InspectedEventEvent extends BaseInspectionEventProperties {
type: '@xstate.event';
// The source might not exist, e.g. when:
// - root init events
// - events sent from external (non-actor) sources
sourceRef: AnyActorRef | undefined;
event: AnyEventObject; // { type: string, ... }
}

export interface InspectedActorEvent extends BaseInspectionEventProperties {
type: '@xstate.actor';
}
58 changes: 45 additions & 13 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,7 @@ interface BuiltinAction {
execute: (actorScope: AnyActorScope, params: unknown) => void;
}

function resolveActionsAndContextWorker(
function resolveAndExecuteActionsWithContext(
currentSnapshot: AnyMachineSnapshot,
event: AnyEventObject,
actorScope: AnyActorScope,
Expand Down Expand Up @@ -1524,12 +1524,29 @@ function resolveActionsAndContextWorker(
: action.params
: undefined;

function executeAction() {
actorScope.system._sendInspectionEvent({
type: '@xstate.action',
actorRef: actorScope.self,
action: {
type:
typeof action === 'string'
? action
: typeof action === 'object'
? action.type
: action.name || '(anonymous)',
params: actionParams
}
});
resolvedAction(actionArgs, actionParams);
}

if (!('resolve' in resolvedAction)) {
if (actorScope.self._processingStatus === ProcessingStatus.Running) {
resolvedAction(actionArgs, actionParams);
executeAction();
} else {
actorScope.defer(() => {
resolvedAction(actionArgs, actionParams);
executeAction();
});
}
continue;
Expand Down Expand Up @@ -1560,7 +1577,7 @@ function resolveActionsAndContextWorker(
}

if (actions) {
intermediateSnapshot = resolveActionsAndContextWorker(
intermediateSnapshot = resolveAndExecuteActionsWithContext(
intermediateSnapshot,
event,
actorScope,
Expand All @@ -1584,7 +1601,7 @@ export function resolveActionsAndContext(
): AnyMachineSnapshot {
const retries: (readonly [BuiltinAction, unknown])[] | undefined =
deferredActorIds ? [] : undefined;
const nextState = resolveActionsAndContextWorker(
const nextState = resolveAndExecuteActionsWithContext(
currentSnapshot,
event,
actorScope,
Expand Down Expand Up @@ -1612,7 +1629,22 @@ export function macrostep(
}

let nextSnapshot = snapshot;
const states: AnyMachineSnapshot[] = [];
const microstates: AnyMachineSnapshot[] = [];

function addMicrostate(
microstate: AnyMachineSnapshot,
event: AnyEventObject,
transitions: AnyTransitionDefinition[]
) {
actorScope.system._sendInspectionEvent({
type: '@xstate.microstep',
actorRef: actorScope.self,
event,
snapshot: microstate,
_transitions: transitions
});
microstates.push(microstate);
}

// Handle stop event
if (event.type === XSTATE_STOP) {
Expand All @@ -1622,11 +1654,11 @@ export function macrostep(
status: 'stopped'
}
);
states.push(nextSnapshot);
addMicrostate(nextSnapshot, event, []);

return {
snapshot: nextSnapshot,
microstates: states
microstates
};
}

Expand All @@ -1648,10 +1680,10 @@ export function macrostep(
status: 'error',
error: currentEvent.error
});
states.push(nextSnapshot);
addMicrostate(nextSnapshot, currentEvent, []);
return {
snapshot: nextSnapshot,
microstates: states
microstates
};
}
nextSnapshot = microstep(
Expand All @@ -1662,7 +1694,7 @@ export function macrostep(
false, // isInitial
internalQueue
);
states.push(nextSnapshot);
addMicrostate(nextSnapshot, currentEvent, transitions);
}

let shouldSelectEventlessTransitions = true;
Expand Down Expand Up @@ -1694,7 +1726,7 @@ export function macrostep(
internalQueue
);
shouldSelectEventlessTransitions = nextSnapshot !== previousState;
states.push(nextSnapshot);
addMicrostate(nextSnapshot, nextEvent, enabledTransitions);
}

if (nextSnapshot.status !== 'active') {
Expand All @@ -1703,7 +1735,7 @@ export function macrostep(

return {
snapshot: nextSnapshot,
microstates: states
microstates
};
}

Expand Down
Loading

0 comments on commit 11b6a1a

Please sign in to comment.