Skip to content

Commit

Permalink
[v5] Inspect API (#4082)
Browse files Browse the repository at this point in the history
* Add inspect API POC

* Move around some things

* Add invoke example

* Fix test

* More inspect work

* Make this public

* Add websocket handler

* POC

* Test updates

* Refactor xstate.transition event

* Add action inspection

* Inspector

* Use actorSystemId

* Remove name for now

* This one too

* Refactor

* Change communication event ot be xstate.event

* Oops

* Rename

* There we go

* Remove action stuff for now

* Fix test

* Tweak

* Update packages/core/test/inspect.test.ts

* Fix tests

* Revert changes to this file

* Clean up

* Update packages/core/src/interpreter.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/interpreter.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/interpreter.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/types.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/types.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/system.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/types.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Avoid mutating the received event (#4282)

* Avoid mutating the received event

* always go through the system when receiving an external event

* Rename types

* actorSystemId -> rootId

* Un-expose actor options

* private -> internal

* system.sendTo -> system.relay

* Move types

* Avoid adding `.root` to the system object (#4284)

* Update snapshots

* Update tests

* Simplify events

* Add tests for composable logic

* Revert "Add tests for composable logic"

This reverts commit b44bfde.

* Ugh types

* Required target

* Update packages/core/src/types.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* small tweaks

* remove redundant error

* slim down inspection events

* Changeset + toObserver

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist committed Oct 3, 2023
1 parent ddafb83 commit 13480c3
Show file tree
Hide file tree
Showing 20 changed files with 692 additions and 92 deletions.
37 changes: 37 additions & 0 deletions .changeset/sharp-spiders-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
'xstate': minor
---

You can now inspect actor system updates using the `inspect` option in `createActor(logic, { inspect })`. The types of **inspection events** you can observe include:

- `@xstate.actor` - An actor ref has been created in the system
- `@xstate.event` - An event was sent from a source actor ref to a target actor ref in the system
- `@xstate.snapshot` - An actor ref emitted a snapshot due to a received event

```ts
import { createMachine } from 'xstate';

const machine = createMachine({
// ...
});

const actor = createActor(machine, {
inspect: (inspectionEvent) => {
if (inspectionEvent.type === '@xstate.actor') {
console.log(inspectionEvent.actorRef);
}

if (inspectionEvent.type === '@xstate.event') {
console.log(inspectionEvent.sourceRef);
console.log(inspectionEvent.targetRef);
console.log(inspectionEvent.event);
}

if (inspectionEvent.type === '@xstate.snapshot') {
console.log(inspectionEvent.actorRef);
console.log(inspectionEvent.event);
console.log(inspectionEvent.snapshot);
}
}
});
```
2 changes: 1 addition & 1 deletion examples/workflow-applicant-request/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface Applicant {
lname: string;
age: number;
email: string;
};
}

// https://github.com/serverlessworkflow/specification/tree/main/examples#applicant-request-decision-example
export const workflow = createMachine(
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/actions/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ function executeSendTo(
const { to, event } = params;

actorContext.defer(() => {
to.send(
actorContext?.system._relay(
actorContext.self,
to,
event.type === XSTATE_ERROR
? createErrorActorEvent(actorContext.self.id, (event as any).data)
: event
Expand Down
14 changes: 9 additions & 5 deletions packages/core/src/actors/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,21 @@ export type InvokeCallback<
export function fromCallback<TEvent extends EventObject, TInput = unknown>(
invokeCallback: InvokeCallback<TEvent, AnyEventObject, TInput>
): CallbackActorLogic<TEvent, TInput> {
return {
const logic: CallbackActorLogic<TEvent, TInput> = {
config: invokeCallback,
start: (_state, { self }) => {
self.send({ type: XSTATE_INIT } as TEvent);
start: (_state, { self, system }) => {
system._relay(self, self, { type: XSTATE_INIT });
},
transition: (state, event, { self, id, system }) => {
transition: (state, event, { self, system }) => {
if (event.type === XSTATE_INIT) {
const sendBack = (eventForParent: AnyEventObject) => {
if (state.status === 'stopped') {
return;
}

self._parent?.send(eventForParent);
if (self._parent) {
system._relay(self, self._parent, eventForParent);
}
};

const receive: Receiver<TEvent> = (newListener) => {
Expand Down Expand Up @@ -124,4 +126,6 @@ export function fromCallback<TEvent extends EventObject, TInput = unknown>(
...state
})
};

return logic;
}
28 changes: 18 additions & 10 deletions packages/core/src/actors/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ export function fromObservable<TContext, TInput>(
const errorEventType = '$$xstate.error';
const completeEventType = '$$xstate.complete';

return {
// TODO: add event types
const logic: ObservableActorLogic<TContext, TInput> = {
config: observableCreator,
transition: (snapshot, event, { self, id, defer }) => {
transition: (snapshot, event, { self, id, defer, system }) => {
if (snapshot.status !== 'active') {
return snapshot;
}

switch (event.type) {
case nextEventType: {
return {
const newSnapshot = {
...snapshot,
context: event.data as TContext
};
return newSnapshot;
}
case errorEventType:
return {
Expand Down Expand Up @@ -109,13 +111,13 @@ export function fromObservable<TContext, TInput>(
self
}).subscribe({
next: (value) => {
self.send({ type: nextEventType, data: value });
system._relay(self, self, { type: nextEventType, data: value });
},
error: (err) => {
self.send({ type: errorEventType, data: err });
system._relay(self, self, { type: errorEventType, data: err });
},
complete: () => {
self.send({ type: completeEventType });
system._relay(self, self, { type: completeEventType });
}
});
},
Expand All @@ -125,6 +127,8 @@ export function fromObservable<TContext, TInput>(
_subscription: undefined
})
};

return logic;
}

/**
Expand All @@ -150,7 +154,7 @@ export function fromEventObservable<T extends EventObject, TInput>(
const completeEventType = '$$xstate.complete';

// TODO: event types
return {
const logic: ObservableActorLogic<T, TInput> = {
config: lazyObservable,
transition: (state, event) => {
if (state.status !== 'active') {
Expand Down Expand Up @@ -207,13 +211,15 @@ export function fromEventObservable<T extends EventObject, TInput>(
self
}).subscribe({
next: (value) => {
self._parent?.send(value);
if (self._parent) {
system._relay(self, self._parent, value);
}
},
error: (err) => {
self.send({ type: errorEventType, data: err });
system._relay(self, self, { type: errorEventType, data: err });
},
complete: () => {
self.send({ type: completeEventType });
system._relay(self, self, { type: completeEventType });
}
});
},
Expand All @@ -223,4 +229,6 @@ export function fromEventObservable<T extends EventObject, TInput>(
_subscription: undefined
})
};

return logic;
}
4 changes: 2 additions & 2 deletions packages/core/src/actors/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,13 @@ export function fromPromise<TOutput, TInput = unknown>(
if (self.getSnapshot().status !== 'active') {
return;
}
self.send({ type: resolveEventType, data: response });
system._relay(self, self, { type: resolveEventType, data: response });
},
(errorData) => {
if (self.getSnapshot().status !== 'active') {
return;
}
self.send({ type: rejectEventType, data: errorData });
system._relay(self, self, { type: rejectEventType, data: errorData });
}
);
},
Expand Down
98 changes: 68 additions & 30 deletions packages/core/src/interpreter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import isDevelopment from '#is-development';
import { Mailbox } from './Mailbox.ts';
import { createDoneActorEvent, createErrorActorEvent } from './eventUtils.ts';
import {
createDoneActorEvent,
createErrorActorEvent,
createInitEvent
} from './eventUtils.ts';
import { XSTATE_STOP } from './constants.ts';
import { devToolsAdapter } from './dev/index.ts';
import { reportUnhandledError } from './reportUnhandledError.ts';
Expand Down Expand Up @@ -32,7 +36,6 @@ import {
Subscription
} from './types.ts';
import { toObserver } from './utils.ts';
import { TlsOptions } from 'tls';

export type SnapshotListener<TLogic extends AnyActorLogic> = (
state: SnapshotFrom<TLogic>
Expand Down Expand Up @@ -137,12 +140,16 @@ export class Actor<TLogic extends AnyActorLogic>
const resolvedOptions = {
...defaultOptions,
...options
};
} as ActorOptions<TLogic> & typeof defaultOptions;

const { clock, logger, parent, id, systemId } = resolvedOptions;
const self = this;
const { clock, logger, parent, id, systemId, inspect } = resolvedOptions;

this.system = parent?.system ?? createSystem();
this.system = parent?.system ?? createSystem(this);

if (inspect && !parent) {
// Always inspect at the system-level
this.system.inspect(toObserver(inspect));
}

if (systemId) {
this._systemId = systemId;
Expand All @@ -158,7 +165,7 @@ export class Actor<TLogic extends AnyActorLogic>
this.src = resolvedOptions.src;
this.ref = this;
this._actorContext = {
self,
self: this,
id: this.id,
sessionId: this.sessionId,
logger: this.logger,
Expand All @@ -179,6 +186,10 @@ export class Actor<TLogic extends AnyActorLogic>
// Ensure that the send method is bound to this Actor instance
// if destructured
this.send = this.send.bind(this);
this.system._sendInspectionEvent({
type: '@xstate.actor',
actorRef: this
});
this._initState();
}

Expand All @@ -193,7 +204,7 @@ export class Actor<TLogic extends AnyActorLogic>
// array of functions to defer
private _deferred: Array<() => void> = [];

private update(snapshot: SnapshotFrom<TLogic>): void {
private update(snapshot: SnapshotFrom<TLogic>, event: EventObject): void {
// Update state
this._state = snapshot;

Expand Down Expand Up @@ -221,16 +232,29 @@ export class Actor<TLogic extends AnyActorLogic>
this.id,
(this._state as any).output
);
this._parent?.send(this._doneEvent as any);
if (this._parent) {
this.system._relay(this, this._parent, this._doneEvent);
}

break;
case 'error':
this._stopProcedure();
this._error((this._state as any).error);
this._parent?.send(
createErrorActorEvent(this.id, (this._state as any).error)
);
if (this._parent) {
this.system._relay(
this,
this._parent,
createErrorActorEvent(this.id, (this._state as any).error)
);
}
break;
}
this.system._sendInspectionEvent({
type: '@xstate.snapshot',
actorRef: this,
event,
snapshot
});
}

public subscribe(observer: Observer<SnapshotFrom<TLogic>>): Subscription;
Expand Down Expand Up @@ -284,13 +308,25 @@ export class Actor<TLogic extends AnyActorLogic>
}
this.status = ActorStatus.Running;

const initEvent = createInitEvent(this.options.input);

this.system._sendInspectionEvent({
type: '@xstate.event',
sourceRef: this._parent,
targetRef: this,
event: initEvent
});

const status = (this._state as any).status;

switch (status) {
case 'done':
// a state machine can be "done" upon intialization (it could reach a final state using initial microsteps)
// we still need to complete observers, flush deferreds etc
this.update(this._state);
this.update(
this._state,
initEvent as unknown as EventFromLogic<TLogic>
);
// fallthrough
case 'error':
// TODO: rethink cleanup of observers, mailbox, etc
Expand All @@ -311,7 +347,7 @@ export class Actor<TLogic extends AnyActorLogic>
// TODO: this notifies all subscribers but usually this is redundant
// there is no real change happening here
// we need to rethink if this needs to be refactored
this.update(this._state);
this.update(this._state, initEvent as unknown as EventFromLogic<TLogic>);

if (this.options.devTools) {
this.attachDevTools();
Expand Down Expand Up @@ -342,7 +378,7 @@ export class Actor<TLogic extends AnyActorLogic>
return;
}

this.update(nextState);
this.update(nextState, event);
if (event.type === XSTATE_STOP) {
this._stopProcedure();
this._complete();
Expand Down Expand Up @@ -431,17 +467,9 @@ export class Actor<TLogic extends AnyActorLogic>
}

/**
* Sends an event to the running Actor to trigger a transition.
*
* @param event The event to send
* @internal
*/
public send(event: EventFromLogic<TLogic>) {
if (typeof event === 'string') {
throw new Error(
`Only event objects may be sent to actors; use .send({ type: "${event}" }) instead`
);
}

public _send(event: EventFromLogic<TLogic>) {
if (this.status === ActorStatus.Stopped) {
// do nothing
if (isDevelopment) {
Expand All @@ -457,6 +485,20 @@ export class Actor<TLogic extends AnyActorLogic>
this.mailbox.enqueue(event);
}

/**
* Sends an event to the running Actor to trigger a transition.
*
* @param event The event to send
*/
public send(event: EventFromLogic<TLogic>) {
if (isDevelopment && typeof event === 'string') {
throw new Error(
`Only event objects may be sent to actors; use .send({ type: "${event}" }) instead`
);
}
this.system._relay(undefined, this, event);
}

// TODO: make private (and figure out a way to do this within the machine)
public delaySend({
event,
Expand All @@ -470,11 +512,7 @@ export class Actor<TLogic extends AnyActorLogic>
to?: AnyActorRef;
}): void {
const timerId = this.clock.setTimeout(() => {
if (to) {
to.send(event);
} else {
this.send(event as EventFromLogic<TLogic>);
}
this.system._relay(this, to ?? this, event as EventFromLogic<TLogic>);
}, delay);

// TODO: consider the rehydration story here
Expand Down
Loading

0 comments on commit 13480c3

Please sign in to comment.