diff --git a/README.md b/README.md index 913633fd8f..5ce99ad5e6 100644 --- a/README.md +++ b/README.md @@ -181,15 +181,15 @@ Read [📽 the slides](http://slides.com/davidkhourshid/finite-state-machines) ( ## Packages -| Package | Description | -| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| 🤖 `xstate` | Core finite state machine and statecharts library + interpreter | -| [📉 `@xstate/graph`](https://github.com/statelyai/xstate/tree/main/packages/xstate-graph) | Graph traversal and model-based testing utilities using XState | -| [⚛️ `@xstate/react`](https://github.com/statelyai/xstate/tree/main/packages/xstate-react) | React hooks and utilities for using XState in React applications | -| [💚 `@xstate/vue`](https://github.com/statelyai/xstate/tree/main/packages/xstate-vue) | Vue composition functions and utilities for using XState in Vue applications | -| [🎷 `@xstate/svelte`](https://github.com/statelyai/xstate/tree/main/packages/xstate-svelte) | Svelte utilities for using XState in Svelte applications | -| [🥏 `@xstate/solid`](https://github.com/statelyai/xstate/tree/main/packages/xstate-solid) | Solid hooks and utilities for using XState in Solid applications | -| [🔍 `@statelyai/inspect`](https://github.com/statelyai/inspect) | Inspection utilities for XState | +| Package | Description | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| 🤖 `xstate` | Core finite state machine and statecharts library + interpreter | +| [📉 `@xstate/graph`](https://github.com/statelyai/xstate/tree/main/packages/xstate-graph) | Graph traversal and model-based testing utilities using XState | +| [⚛️ `@xstate/react`](https://github.com/statelyai/xstate/tree/main/packages/xstate-react) | React hooks and utilities for using XState in React applications | +| [💚 `@xstate/vue`](https://github.com/statelyai/xstate/tree/main/packages/xstate-vue) | Vue composition functions and utilities for using XState in Vue applications | +| [🎷 `@xstate/svelte`](https://github.com/statelyai/xstate/tree/main/packages/xstate-svelte) | Svelte utilities for using XState in Svelte applications | +| [🥏 `@xstate/solid`](https://github.com/statelyai/xstate/tree/main/packages/xstate-solid) | Solid hooks and utilities for using XState in Solid applications | +| [🔍 `@statelyai/inspect`](https://github.com/statelyai/inspect) | Inspection utilities for XState | ## Finite State Machines diff --git a/packages/core/src/actors/callback.ts b/packages/core/src/actors/callback.ts index 1dcc84a371..c1e2d70aa6 100644 --- a/packages/core/src/actors/callback.ts +++ b/packages/core/src/actors/callback.ts @@ -2,6 +2,8 @@ import { XSTATE_STOP } from '../constants.ts'; import { AnyActorSystem } from '../system.ts'; import { ActorLogic, + ActorRefFrom, + ActorScope, ActorRefFromLogic, AnyActorRef, AnyEventObject, @@ -92,6 +94,7 @@ type InvokeCallback< self, sendBack, receive, + spawn, emit }: { /** @@ -111,6 +114,7 @@ type InvokeCallback< * listener is then called whenever events are received by the callback actor */ receive: Receiver; + spawn: ActorScope['spawnChild']; emit: (emitted: TEmitted) => void; }) => (() => void) | void; @@ -216,6 +220,7 @@ export function fromCallback< callbackState.receivers ??= new Set(); callbackState.receivers.add(listener); }, + spawn: actorScope.spawnChild, emit }); }, @@ -241,6 +246,7 @@ export function fromCallback< }, getInitialSnapshot: (_, input) => { return { + context: undefined, status: 'active', output: undefined, error: undefined, diff --git a/packages/core/src/actors/observable.ts b/packages/core/src/actors/observable.ts index 0dafa0813c..804ac34654 100644 --- a/packages/core/src/actors/observable.ts +++ b/packages/core/src/actors/observable.ts @@ -2,6 +2,7 @@ import { XSTATE_STOP } from '../constants'; import { AnyActorSystem } from '../system.ts'; import { ActorLogic, + ActorScope, ActorRefFromLogic, EventObject, NonReducibleUnknown, @@ -128,6 +129,7 @@ export function fromObservable< input: TInput; system: AnyActorSystem; self: ObservableActorRef; + spawnChild: ActorScope['spawnChild']; emit: (emitted: TEmitted) => void; }) => Subscribable ): ObservableActorLogic { @@ -184,7 +186,7 @@ export function fromObservable< _subscription: undefined }; }, - start: (state, { self, system, emit }) => { + start: (state, { self, system, spawnChild, emit }) => { if (state.status === 'done') { // Do not restart a completed observable return; @@ -193,6 +195,7 @@ export function fromObservable< input: state.input!, system, self, + spawnChild, emit }).subscribe({ next: (value) => { diff --git a/packages/core/src/actors/promise.ts b/packages/core/src/actors/promise.ts index ccc920bc1d..effd9ea749 100644 --- a/packages/core/src/actors/promise.ts +++ b/packages/core/src/actors/promise.ts @@ -2,6 +2,7 @@ import { XSTATE_STOP } from '../constants.ts'; import { AnyActorSystem } from '../system.ts'; import { ActorLogic, + ActorScope, ActorRefFromLogic, AnyActorRef, EventObject, @@ -11,10 +12,12 @@ import { export type PromiseSnapshot = Snapshot & { input: TInput | undefined; + children: Record; }; const XSTATE_PROMISE_RESOLVE = 'xstate.promise.resolve'; const XSTATE_PROMISE_REJECT = 'xstate.promise.reject'; +const XSTATE_SPAWN_CHILD = 'xstate.spawn.child'; export type PromiseActorLogic< TOutput, @@ -135,19 +138,27 @@ export function fromPromise< system: AnyActorSystem; /** The parent actor of the promise actor */ self: PromiseActorRef; + spawnChild: ActorScope['spawnChild']; signal: AbortSignal; emit: (emitted: TEmitted) => void; }) => PromiseLike ): PromiseActorLogic { const logic: PromiseActorLogic = { config: promiseCreator, - transition: (state, event, scope) => { + transition: (state, event, actorScope) => { if (state.status !== 'active') { return state; } + const stopChildren = () => { + for (const child of Object.values(state.children)) { + actorScope.stopChild?.(child); + } + }; + switch (event.type) { case XSTATE_PROMISE_RESOLVE: { + stopChildren(); const resolvedValue = (event as any).data; return { ...state, @@ -157,30 +168,53 @@ export function fromPromise< }; } case XSTATE_PROMISE_REJECT: + stopChildren(); return { ...state, status: 'error', error: (event as any).data, input: undefined }; - case XSTATE_STOP: { - controllerMap.get(scope.self)?.abort(); + case XSTATE_STOP: + stopChildren(); + controllerMap.get(actorScope.self)?.abort(); + return { ...state, status: 'stopped', input: undefined }; + case XSTATE_SPAWN_CHILD: { + return { + ...state, + children: { + ...state.children, + [(event as any).child.id]: (event as any).child + } + }; } default: return state; } }, - start: (state, { self, system, emit }) => { + start: (state, { self, system, spawnChild, emit }) => { // TODO: determine how to allow customizing this so that promises // can be restarted if necessary if (state.status !== 'active') { return; } + + const innerSpawnChild: typeof spawnChild = (logic, actorOptions) => { + const child = spawnChild?.(logic, actorOptions) as AnyActorRef; + + self.send({ + type: XSTATE_SPAWN_CHILD, + child + }); + + return child; + }; + const controller = new AbortController(); controllerMap.set(self, controller); const resolvedPromise = Promise.resolve( @@ -188,6 +222,7 @@ export function fromPromise< input: state.input!, system, self, + spawnChild: innerSpawnChild as any, signal: controller.signal, emit }) @@ -218,10 +253,12 @@ export function fromPromise< }, getInitialSnapshot: (_, input) => { return { + context: undefined, status: 'active', output: undefined, error: undefined, - input + input, + children: {} }; }, getPersistedSnapshot: (snapshot) => snapshot, diff --git a/packages/core/src/actors/transition.ts b/packages/core/src/actors/transition.ts index 4c92b2c168..db41fe1c20 100644 --- a/packages/core/src/actors/transition.ts +++ b/packages/core/src/actors/transition.ts @@ -185,10 +185,12 @@ export function fromTransition< | TContext | (({ input, - self + self, + spawnChild }: { input: TInput; self: TransitionActorRef; + spawnChild: ActorScope['spawnChild']; }) => TContext) // TODO: type ): TransitionActorLogic { return { @@ -203,14 +205,14 @@ export function fromTransition< ) }; }, - getInitialSnapshot: (_, input) => { + getInitialSnapshot: ({ self, spawnChild }, input) => { return { status: 'active', output: undefined, error: undefined, context: typeof initialContext === 'function' - ? (initialContext as any)({ input }) + ? (initialContext as any)({ input, self, spawnChild }) : initialContext }; }, diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 3ad250edcc..2752aa1f11 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -12,6 +12,7 @@ import { symbolObservable } from './symbolObservable.ts'; import { AnyActorSystem, Clock, createSystem } from './system.ts'; import type { + ActorRefFrom, ActorScope, AnyActorLogic, AnyActorRef, @@ -177,6 +178,21 @@ export class Actor } (child as any)._stop(); }, + spawnChild: ( + logic: T, + actorOptions?: ActorOptions + ) => { + const actor = createActor(logic, { + parent: this, + ...actorOptions + }); + + if (this._processingStatus === ProcessingStatus.Running) { + actor.start(); + } + + return actor as ActorRefFrom; + }, emit: (emittedEvent) => { const listeners = this.eventListeners.get(emittedEvent.type); const wildcardListener = this.eventListeners.get('*'); diff --git a/packages/core/src/getNextSnapshot.ts b/packages/core/src/getNextSnapshot.ts index 7f57aaab0d..d6fcbafdeb 100644 --- a/packages/core/src/getNextSnapshot.ts +++ b/packages/core/src/getNextSnapshot.ts @@ -27,6 +27,11 @@ export function createInertActorScope( sessionId: '', stopChild: () => {}, system: self.system, + spawnChild: (logic) => { + const child = createActor(logic) as any; + + return child; + }, emit: () => {} }; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 13fbbcf924..7751c655b8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2149,6 +2149,10 @@ export interface ActorScope< emit: (event: TEmitted) => void; system: TSystem; stopChild: (child: AnyActorRef) => void; + spawnChild: ( + logic: T, + actorOptions?: ActorOptions + ) => ActorRefFrom; } export type AnyActorScope = ActorScope< @@ -2200,17 +2204,17 @@ export interface ActorLogic< /** The initial setup/configuration used to create the actor logic. */ config?: unknown; /** - * Transition function that processes the current state and an incoming - * message to produce a new state. + * Transition function that processes the current state and an incoming event + * to produce a new state. * * @param snapshot - The current state. - * @param message - The incoming message. + * @param event - The incoming event. * @param actorScope - The actor scope. * @returns The new state. */ transition: ( snapshot: TSnapshot, - message: TEvent, + event: TEvent, actorScope: ActorScope ) => TSnapshot; /** diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index eafc3271c3..9d1d6ab152 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -3,7 +3,6 @@ import { isMachineSnapshot } from './State.ts'; import type { StateNode } from './StateNode.ts'; import { TARGETLESS_KEY } from './constants.ts'; import type { - AnyActorLogic, AnyActorRef, AnyEventObject, AnyMachineSnapshot, @@ -283,3 +282,7 @@ export function resolveReferencedActor(machine: AnyStateMachine, src: string) { export function getAllOwnEventDescriptors(snapshot: AnyMachineSnapshot) { return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])]; } + +export function isActorRef(actorRef: unknown): actorRef is AnyActorRef { + return !!actorRef && typeof actorRef === 'object' && 'send' in actorRef; +} diff --git a/packages/core/test/actorLogic.test.ts b/packages/core/test/actorLogic.test.ts index 7f37a7f145..e1230d96e5 100644 --- a/packages/core/test/actorLogic.test.ts +++ b/packages/core/test/actorLogic.test.ts @@ -6,7 +6,8 @@ import { createActor, AnyActorLogic, Snapshot, - ActorLogic + ActorLogic, + toPromise } from '../src/index.ts'; import { fromCallback, @@ -17,6 +18,7 @@ import { } from '../src/actors/index.ts'; import { waitFor } from '../src/waitFor.ts'; import { raise, sendTo } from '../src/actions.ts'; +import { isActorRef } from '../src/utils.ts'; describe('promise logic (fromPromise)', () => { it('should interpret a promise', async () => { @@ -134,13 +136,15 @@ describe('promise logic (fromPromise)', () => { const resolvedPersistedState = actor.getPersistedSnapshot(); expect(resolvedPersistedState).toMatchInlineSnapshot(` - { - "error": undefined, - "input": undefined, - "output": 42, - "status": "done", - } - `); +{ + "children": {}, + "context": undefined, + "error": undefined, + "input": undefined, + "output": 42, + "status": "done", +} +`); const restoredActor = createActor(promiseLogic, { snapshot: resolvedPersistedState @@ -163,13 +167,15 @@ describe('promise logic (fromPromise)', () => { const resolvedPersistedState = actor.getPersistedSnapshot(); expect(resolvedPersistedState).toMatchInlineSnapshot(` - { - "error": undefined, - "input": undefined, - "output": 1, - "status": "done", - } - `); +{ + "children": {}, + "context": undefined, + "error": undefined, + "input": undefined, + "output": 1, + "status": "done", +} +`); expect(createdPromises).toBe(1); const restoredActor = createActor(promiseLogic, { @@ -194,13 +200,15 @@ describe('promise logic (fromPromise)', () => { const rejectedPersistedState = actorRef.getPersistedSnapshot(); expect(rejectedPersistedState).toMatchInlineSnapshot(` - { - "error": 1, - "input": undefined, - "output": undefined, - "status": "error", - } - `); +{ + "children": {}, + "context": undefined, + "error": 1, + "input": undefined, + "output": undefined, + "status": "error", +} +`); expect(createdPromises).toBe(1); const actorRef2 = createActor(promiseLogic, { @@ -464,6 +472,125 @@ describe('promise logic (fromPromise)', () => { const fn2 = signalListenerList[1]; expect(fn2).toHaveBeenCalled(); }); + + it('can spawn an actor', () => { + expect.assertions(3); + const promiseLogic = fromPromise(({ spawnChild }) => { + const childActor = spawnChild( + fromPromise(() => Promise.resolve(42)), + { + id: 'child' + } + ); + return Promise.resolve(childActor); + }); + + const actor = createActor(promiseLogic).start(); + + toPromise(actor).then((res) => { + expect(isActorRef(res)).toBeTruthy(); + expect((res as AnyActorRef)._parent).toBe(actor); + + expect(actor.getSnapshot().children.child).toBe(res); + }); + }); + + it('stops spawned actors when it is stopped', async () => { + const promiseLogic = fromPromise(async ({ spawnChild }) => { + spawnChild( + fromPromise( + () => + new Promise((_res, _rej) => { + // ... + }) + ), + { + id: 'child' + } + ); + await new Promise((_res, _rej) => { + // ... + }); + }); + + const actor = createActor(promiseLogic).start(); + + const snapshot = await waitFor( + actor, + (s) => Object.keys(s.children).length > 0 + ); + + const child = snapshot.children.child; + + expect(isActorRef(child)).toBeTruthy(); + expect((child as AnyActorRef)._parent).toBe(actor); + + expect(actor.getSnapshot().children.child).toBe(child); + + expect(child.getSnapshot().status).toEqual('active'); + + actor.stop(); + + expect(child.getSnapshot().status).toEqual('stopped'); + }); + + it('stops spawned actors when it is done', async () => { + const promiseLogic = fromPromise(async ({ spawnChild }) => { + spawnChild( + fromPromise( + () => + new Promise((_res, _rej) => { + // ... + }) + ), + { + id: 'child' + } + ); + return 42; + }); + + const actor = createActor(promiseLogic).start(); + + await toPromise(actor); + + const snapshot = actor.getSnapshot(); + const child = snapshot.children.child; + + expect(isActorRef(child)).toBeTruthy(); + expect((child as AnyActorRef)._parent).toBe(actor); + expect(actor.getSnapshot().children.child).toBe(child); + expect(child.getSnapshot().status).toEqual('stopped'); + }); + + it('stops spawned actors when it errors', async () => { + const promiseLogic = fromPromise(async ({ spawnChild }) => { + spawnChild( + fromPromise( + () => + new Promise((_res, _rej) => { + // ... + }) + ), + { + id: 'child' + } + ); + await Promise.reject('uh oh'); + }); + + const actor = createActor(promiseLogic).start(); + + try { + await toPromise(actor); + } catch { + const snapshot = actor.getSnapshot(); + const child = snapshot.children.child; + + expect(isActorRef(child)).toBeTruthy(); + expect(child.getSnapshot().status).toEqual('stopped'); + } + }); }); describe('transition function logic (fromTransition)', () => { @@ -546,6 +673,105 @@ describe('transition function logic (fromTransition)', () => { actor.send({ type: 'a' }); }); + + it('can spawn an actor when receiving an event', () => { + expect.assertions(1); + const transitionLogic = fromTransition< + AnyActorRef | undefined, + any, + any, + any + >((state, _event, { spawnChild }) => { + if (state) { + return state; + } + const childActor = spawnChild(fromPromise(() => Promise.resolve(42))); + return childActor; + }, undefined); + + const actor = createActor(transitionLogic); + actor.subscribe({ + error: (err) => { + console.error(err); + } + }); + actor.start(); + actor.send({ type: 'anyEvent' }); + + expect(isActorRef(actor.getSnapshot().context)).toBeTruthy(); + }); + + it('can spawn an actor upon start', () => { + expect.assertions(1); + const transitionLogic = fromTransition< + AnyActorRef | undefined, + any, + any, + any + >( + (state) => { + return state; + }, + ({ spawnChild }) => { + const childActor = spawnChild(fromPromise(() => Promise.resolve(42))); + return childActor; + } + ); + + const actor = createActor(transitionLogic).start(); + actor.send({ type: 'anyEvent' }); + + expect(isActorRef(actor.getSnapshot().context)).toBeTruthy(); + }); + it('can spawn an actor when receiving an event', () => { + expect.assertions(1); + const transitionLogic = fromTransition< + AnyActorRef | undefined, + any, + any, + any + >((state, _event, { spawnChild }) => { + if (state) { + return state; + } + const childActor = spawnChild(fromPromise(() => Promise.resolve(42))); + return childActor; + }, undefined); + + const actor = createActor(transitionLogic); + actor.subscribe({ + error: (err) => { + console.error(err); + } + }); + actor.start(); + actor.send({ type: 'anyEvent' }); + + expect(isActorRef(actor.getSnapshot().context)).toBeTruthy(); + }); + + it('can spawn an actor upon start', () => { + expect.assertions(1); + const transitionLogic = fromTransition< + AnyActorRef | undefined, + any, + any, + any + >( + (state) => { + return state; + }, + ({ spawnChild }) => { + const childActor = spawnChild(fromPromise(() => Promise.resolve(42))); + return childActor; + } + ); + + const actor = createActor(transitionLogic).start(); + actor.send({ type: 'anyEvent' }); + + expect(isActorRef(actor.getSnapshot().context)).toBeTruthy(); + }); }); describe('observable logic (fromObservable)', () => { @@ -648,6 +874,17 @@ describe('observable logic (fromObservable)', () => { createActor(observableLogic).start(); }); + + it('can spawn an actor', () => { + expect.assertions(1); + const observableLogic = fromObservable(({ spawnChild }) => { + const actorRef = spawnChild(fromCallback(() => {})); + expect(isActorRef(actorRef)).toBe(true); + return of(actorRef); + }); + + createActor(observableLogic).start(); + }); }); describe('eventObservable logic (fromEventObservable)', () => { @@ -670,6 +907,17 @@ describe('eventObservable logic (fromEventObservable)', () => { createActor(observableLogic).start(); }); + + it('can spawn an actor', () => { + expect.assertions(1); + const observableLogic = fromObservable(({ spawnChild }) => { + const actorRef = spawnChild(fromCallback(() => {})); + expect(isActorRef(actorRef)).toBe(true); + return of({ type: 'a', payload: actorRef }); + }); + + createActor(observableLogic).start(); + }); }); describe('callback logic (fromCallback)', () => { @@ -788,6 +1036,16 @@ describe('callback logic (fromCallback)', () => { expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith(13); }); + + it('can spawn an actor', () => { + expect.assertions(1); + const callbackLogic = fromCallback(({ spawn }) => { + const actorRef = spawn(fromPromise(() => Promise.resolve(42))); + expect(isActorRef(actorRef)).toBe(true); + }); + + createActor(callbackLogic).start(); + }); }); describe('machine logic', () => { @@ -839,13 +1097,15 @@ describe('machine logic', () => { const persistedState = actor.getPersistedSnapshot()!; expect((persistedState as any).children.a.snapshot).toMatchInlineSnapshot(` - { - "error": undefined, - "input": undefined, - "output": 42, - "status": "done", - } - `); +{ + "children": {}, + "context": undefined, + "error": undefined, + "input": undefined, + "output": 42, + "status": "done", +} +`); expect((persistedState as any).children.b.snapshot).toEqual( expect.objectContaining({ @@ -987,7 +1247,7 @@ describe('machine logic', () => { id: 'child', src: createMachine({ context: ({ input }) => ({ - // this is only meant to showcase why we can't invoke this actor when it's missing in the persisted state + // this is meant to showcase why we can't invoke this actor when it's missing in the persisted state // because we don't have access to the right input as it depends on the event that was used to enter state `b` value: input.deep.prop }) diff --git a/packages/core/test/inspect.test.ts b/packages/core/test/inspect.test.ts index f199f9d7df..e473c961ab 100644 --- a/packages/core/test/inspect.test.ts +++ b/packages/core/test/inspect.test.ts @@ -240,214 +240,218 @@ describe('inspect', () => { ['@xstate.actor', '@xstate.event', '@xstate.snapshot'].includes(ev.type) ) ).toMatchInlineSnapshot(` - [ - { - "actorId": "x:1", - "type": "@xstate.actor", - }, - { - "actorId": "x:2", - "type": "@xstate.actor", - }, - { - "event": { - "input": undefined, - "type": "xstate.init", - }, - "sourceId": undefined, - "targetId": "x:1", - "type": "@xstate.event", - }, - { - "event": { - "input": undefined, - "type": "xstate.init", - }, - "sourceId": "x:1", - "targetId": "x:2", - "type": "@xstate.event", - }, - { - "actorId": "x:2", - "event": { - "input": undefined, - "type": "xstate.init", - }, - "snapshot": { - "value": "start", - }, - "status": "active", - "type": "@xstate.snapshot", - }, - { - "actorId": "x:1", - "event": { - "input": undefined, - "type": "xstate.init", - }, - "snapshot": { - "value": "waiting", - }, - "status": "active", - "type": "@xstate.snapshot", - }, - { - "event": { - "type": "load", - }, - "sourceId": undefined, - "targetId": "x:1", - "type": "@xstate.event", - }, - { - "event": { - "type": "loadChild", - }, - "sourceId": "x:1", - "targetId": "x:2", - "type": "@xstate.event", - }, - { - "actorId": "x:3", - "type": "@xstate.actor", - }, - { - "event": { - "input": undefined, - "type": "xstate.init", - }, - "sourceId": "x:2", - "targetId": "x:3", - "type": "@xstate.event", - }, - { - "actorId": "x:3", - "event": { - "input": undefined, - "type": "xstate.init", - }, - "snapshot": { - "error": undefined, - "input": undefined, - "output": undefined, - "status": "active", - }, - "status": "active", - "type": "@xstate.snapshot", - }, - { - "actorId": "x:2", - "event": { - "type": "loadChild", - }, - "snapshot": { - "value": "loading", - }, - "status": "active", - "type": "@xstate.snapshot", - }, - { - "actorId": "x:1", - "event": { - "type": "load", - }, - "snapshot": { - "value": "waiting", - }, - "status": "active", - "type": "@xstate.snapshot", - }, - { - "event": { - "data": 42, - "type": "xstate.promise.resolve", - }, - "sourceId": "x:3", - "targetId": "x:3", - "type": "@xstate.event", - }, - { - "event": { - "actorId": "0.(machine).loading", - "output": 42, - "type": "xstate.done.actor.0.(machine).loading", - }, - "sourceId": "x:3", - "targetId": "x:2", - "type": "@xstate.event", - }, - { - "event": { - "type": "toParent", - }, - "sourceId": "x:2", - "targetId": "x:1", - "type": "@xstate.event", - }, - { - "actorId": "x:1", - "event": { - "type": "toParent", - }, - "snapshot": { - "value": "waiting", - }, - "status": "active", - "type": "@xstate.snapshot", - }, - { - "event": { - "actorId": "child", - "output": undefined, - "type": "xstate.done.actor.child", - }, - "sourceId": "x:2", - "targetId": "x:1", - "type": "@xstate.event", - }, - { - "actorId": "x:1", - "event": { - "actorId": "child", - "output": undefined, - "type": "xstate.done.actor.child", - }, - "snapshot": { - "value": "success", - }, - "status": "active", - "type": "@xstate.snapshot", - }, - { - "actorId": "x:2", - "event": { - "actorId": "0.(machine).loading", - "output": 42, - "type": "xstate.done.actor.0.(machine).loading", - }, - "snapshot": { - "value": "loaded", - }, - "status": "done", - "type": "@xstate.snapshot", - }, - { - "actorId": "x:3", - "event": { - "data": 42, - "type": "xstate.promise.resolve", - }, - "snapshot": { - "error": undefined, - "input": undefined, - "output": 42, - "status": "done", - }, - "status": "done", - "type": "@xstate.snapshot", - }, - ] - `); +[ + { + "actorId": "x:1", + "type": "@xstate.actor", + }, + { + "actorId": "x:2", + "type": "@xstate.actor", + }, + { + "event": { + "input": undefined, + "type": "xstate.init", + }, + "sourceId": undefined, + "targetId": "x:1", + "type": "@xstate.event", + }, + { + "event": { + "input": undefined, + "type": "xstate.init", + }, + "sourceId": "x:1", + "targetId": "x:2", + "type": "@xstate.event", + }, + { + "actorId": "x:2", + "event": { + "input": undefined, + "type": "xstate.init", + }, + "snapshot": { + "value": "start", + }, + "status": "active", + "type": "@xstate.snapshot", + }, + { + "actorId": "x:1", + "event": { + "input": undefined, + "type": "xstate.init", + }, + "snapshot": { + "value": "waiting", + }, + "status": "active", + "type": "@xstate.snapshot", + }, + { + "event": { + "type": "load", + }, + "sourceId": undefined, + "targetId": "x:1", + "type": "@xstate.event", + }, + { + "event": { + "type": "loadChild", + }, + "sourceId": "x:1", + "targetId": "x:2", + "type": "@xstate.event", + }, + { + "actorId": "x:3", + "type": "@xstate.actor", + }, + { + "event": { + "input": undefined, + "type": "xstate.init", + }, + "sourceId": "x:2", + "targetId": "x:3", + "type": "@xstate.event", + }, + { + "actorId": "x:3", + "event": { + "input": undefined, + "type": "xstate.init", + }, + "snapshot": { + "children": {}, + "context": undefined, + "error": undefined, + "input": undefined, + "output": undefined, + "status": "active", + }, + "status": "active", + "type": "@xstate.snapshot", + }, + { + "actorId": "x:2", + "event": { + "type": "loadChild", + }, + "snapshot": { + "value": "loading", + }, + "status": "active", + "type": "@xstate.snapshot", + }, + { + "actorId": "x:1", + "event": { + "type": "load", + }, + "snapshot": { + "value": "waiting", + }, + "status": "active", + "type": "@xstate.snapshot", + }, + { + "event": { + "data": 42, + "type": "xstate.promise.resolve", + }, + "sourceId": "x:3", + "targetId": "x:3", + "type": "@xstate.event", + }, + { + "event": { + "actorId": "0.(machine).loading", + "output": 42, + "type": "xstate.done.actor.0.(machine).loading", + }, + "sourceId": "x:3", + "targetId": "x:2", + "type": "@xstate.event", + }, + { + "event": { + "type": "toParent", + }, + "sourceId": "x:2", + "targetId": "x:1", + "type": "@xstate.event", + }, + { + "actorId": "x:1", + "event": { + "type": "toParent", + }, + "snapshot": { + "value": "waiting", + }, + "status": "active", + "type": "@xstate.snapshot", + }, + { + "event": { + "actorId": "child", + "output": undefined, + "type": "xstate.done.actor.child", + }, + "sourceId": "x:2", + "targetId": "x:1", + "type": "@xstate.event", + }, + { + "actorId": "x:1", + "event": { + "actorId": "child", + "output": undefined, + "type": "xstate.done.actor.child", + }, + "snapshot": { + "value": "success", + }, + "status": "active", + "type": "@xstate.snapshot", + }, + { + "actorId": "x:2", + "event": { + "actorId": "0.(machine).loading", + "output": 42, + "type": "xstate.done.actor.0.(machine).loading", + }, + "snapshot": { + "value": "loaded", + }, + "status": "done", + "type": "@xstate.snapshot", + }, + { + "actorId": "x:3", + "event": { + "data": 42, + "type": "xstate.promise.resolve", + }, + "snapshot": { + "children": {}, + "context": undefined, + "error": undefined, + "input": undefined, + "output": 42, + "status": "done", + }, + "status": "done", + "type": "@xstate.snapshot", + }, +] +`); }); it('can inspect microsteps from always events', async () => { diff --git a/packages/xstate-graph/src/actorScope.ts b/packages/xstate-graph/src/actorScope.ts index a1783a3654..b7080a66ba 100644 --- a/packages/xstate-graph/src/actorScope.ts +++ b/packages/xstate-graph/src/actorScope.ts @@ -10,6 +10,7 @@ export function createMockActorScope(): AnyActorScope { defer: () => {}, system: emptyActor.system, // TODO: mock system? stopChild: () => {}, + spawnChild: () => emptyActor as any, emit: () => {} }; }