diff --git a/.changeset/great-toes-vanish.md b/.changeset/great-toes-vanish.md new file mode 100644 index 0000000000..1e3b94608e --- /dev/null +++ b/.changeset/great-toes-vanish.md @@ -0,0 +1,5 @@ +--- +'xstate': patch +--- + +Fixed an issue with invoked service not being correctly started if other service got stopped in a subsequent microstep (in response to raised or null event). diff --git a/.changeset/rotten-rivers-wonder.md b/.changeset/rotten-rivers-wonder.md new file mode 100644 index 0000000000..3af12e492d --- /dev/null +++ b/.changeset/rotten-rivers-wonder.md @@ -0,0 +1,105 @@ +--- +'xstate': major +--- + +**Breaking:** Activities are no longer a separate concept. Instead, activities are invoked. Internally, this is how activities worked. The API is consolidated so that `activities` are no longer a property of the state node or machine options: + +```diff +import { createMachine } from 'xstate'; ++import { invokeActivity } from 'xstate/invoke'; + +const machine = createMachine( + { + // ... +- activities: 'someActivity', ++ invoke: { ++ src: 'someActivity' ++ } + }, + { +- activities: { ++ behaviors: { +- someActivity: ((context, event) => { ++ someActivity: invokeActivity((context, event) => { + // ... some continuous activity + + return () => { + // dispose activity + } + }) + } + } +); +``` + +**Breaking:** The `services` option passed as the second argument to `createMachine(config, options)` is renamed to `behaviors`. Each value in `behaviors`should be a function that takes in `context` and `event` and returns a [behavior](TODO: link). The provided invoke creators are: + +- `invokeActivity` +- `invokePromise` +- `invokeCallback` +- `invokeObservable` +- `invokeMachine` + +```diff +import { createMachine } from 'xstate'; ++import { invokePromise } from 'xstate/invoke'; + +const machine = createMachine( + { + // ... + invoke: { + src: 'fetchFromAPI' + } + }, + { +- services: { ++ behaviors: { +- fetchFromAPI: ((context, event) => { ++ fetchFromAPI: invokePromise((context, event) => { + // ... (return a promise) + }) + } + } +); +``` + +**Breaking:** The `state.children` property is now a mapping of invoked actor IDs to their `ActorRef` instances. + +**Breaking:** The way that you interface with invoked/spawned actors is now through `ActorRef` instances. An `ActorRef` is an opaque reference to an `Actor`, which should be never referenced directly. + +**Breaking:** The `spawn` function is no longer imported globally. Spawning actors is now done inside of `assign(...)`, as seen below: + +```diff +-import { createMachine, spawn } from 'xstate'; ++import { createMachine } from 'xstate'; + +const machine = createMachine({ + // ... + entry: assign({ +- someRef: (context, event) => { ++ someRef: (context, event, { spawn }) => { +- return spawn(somePromise); ++ return spawn.from(somePromise); + } + }) +}); + +``` + +**Breaking:** The `src` of an `invoke` config is now either a string that references the machine's `options.behaviors`, or a `BehaviorCreator`, which is a function that takes in `context` and `event` and returns a `Behavior`: + +```diff +import { createMachine } from 'xstate'; ++import { invokePromise } from 'xstate/invoke'; + +const machine = createMachine({ + // ... + invoke: { +- src: (context, event) => somePromise ++ src: invokePromise((context, event) => somePromise) + } + // ... +}); +``` + +**Breaking:** The `origin` of an `SCXML.Event` is no longer a string, but an `ActorRef` instance. diff --git a/.changeset/wet-swans-deny.md b/.changeset/wet-swans-deny.md new file mode 100644 index 0000000000..5c7da98345 --- /dev/null +++ b/.changeset/wet-swans-deny.md @@ -0,0 +1,5 @@ +--- +'xstate': minor +--- + +Actions from a restored state provided as a custom initial state to `interpret(machine).start(initialState)` are now executed properly. See #1174 for more information. diff --git a/README.md b/README.md index b36160b67e..1d5cec86eb 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,17 @@ JavaScript and TypeScript [finite state machines](https://en.wikipedia.org/wiki/ - [💚 `@xstate/vue`](https://github.com/davidkpiano/xstate/tree/master/packages/xstate-vue) - Vue composition functions and utilities for using XState in Vue applications - [✅ `@xstate/test`](https://github.com/davidkpiano/xstate/tree/master/packages/xstate-test) - Model-based testing utilities for XState +## Templates + +Get started by forking one of these templates on CodeSandbox: + +- [XState Template](https://codesandbox.io/s/xstate-example-template-m4ckv) - no framework +- [XState + TypeScript Template](https://codesandbox.io/s/xstate-typescript-template-s9kz8) - no framework +- [XState + React Template](https://codesandbox.io/s/xstate-react-template-3t2tg) +- [XState + React + TypeScript Template](https://codesandbox.io/s/xstate-react-typescript-template-wjdvn) +- [XState + Vue Template](https://codesandbox.io/s/xstate-vue-template-composition-api-1n23l) +- [XState + Svelte Template](https://codesandbox.io/s/xstate-svelte-template-jflv1) + ## Super quick start ```bash diff --git a/docs/packages/xstate-react/index.md b/docs/packages/xstate-react/index.md index 825f92328b..3be2ac19cc 100644 --- a/docs/packages/xstate-react/index.md +++ b/docs/packages/xstate-react/index.md @@ -49,7 +49,7 @@ A [React hook](https://reactjs.org/hooks) that interprets the given `machine` an **Arguments** - `machine` - An [XState machine](https://xstate.js.org/docs/guides/machines.html). -- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) OR one of the following Machine Config options: `guards`, `actions`, `activities`, `services`, `delays`, `immediate`, `context`, or `state`. +- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) OR one of the following Machine Config options: `guards`, `actions`, `activities`, `behaviors`, `delays`, `immediate`, `context`, or `state`. **Returns** a tuple of `[state, send, service]`: @@ -148,13 +148,16 @@ const Fetcher = ({ onFetch = () => new Promise(res => res('some data')) }) => { }; ``` -## Configuring Machines +## Configuring Machines Existing machines can be configured by passing the machine options as the 2nd argument of `useMachine(machine, options)`. Example: the `'fetchData'` service and `'notifySuccess'` action are both configurable: ```js +import { createMachine } from 'xstate'; +import { invokePromise } from 'xstate/invoke'; + const fetchMachine = Machine({ id: 'fetch', initial: 'idle', @@ -200,9 +203,10 @@ const Fetcher = ({ onResolve }) => { actions: { notifySuccess: (ctx) => onResolve(ctx.data) }, - services: { - fetchData: (_, e) => - fetch(`some/api/${e.query}`).then((res) => res.json()) + behaviors: { + fetchData: invokePromise((_, event) => + fetch(`some/api/${event.query}`).then((res) => res.json()) + ) } }); diff --git a/docs/packages/xstate-test/index.md b/docs/packages/xstate-test/index.md index 3dfe6baf47..d60ab91544 100644 --- a/docs/packages/xstate-test/index.md +++ b/docs/packages/xstate-test/index.md @@ -3,6 +3,7 @@ This package contains utilities for facilitating [model-based testing](https://en.wikipedia.org/wiki/Model-based_testing) for any software. - **Talk**: [Write Fewer Tests! From Automation to Autogeneration](https://slides.com/davidkhourshid/mbt) at React Rally 2019 ([🎥 Video](https://www.youtube.com/watch?v=tpNmPKjPSFQ)) +- [Model-Based Testing in React with State Machines](https://css-tricks.com/model-based-testing-in-react-with-state-machines/) ## Quick Start @@ -49,7 +50,7 @@ const toggleMachine = Machine({ /* ... */ }, meta: { - test: async page => { + test: async (page) => { await page.waitFor('input:checked'); } } @@ -59,7 +60,7 @@ const toggleMachine = Machine({ /* ... */ }, meta: { - test: async page => { + test: async (page) => { await page.waitFor('input:not(:checked)'); } } @@ -78,7 +79,7 @@ const toggleMachine = Machine(/* ... */); const toggleModel = createModel(toggleMachine).withEvents({ TOGGLE: { - exec: async page => { + exec: async (page) => { await page.click('input'); } } @@ -93,9 +94,9 @@ const toggleModel = createModel(toggleMachine).withEvents({ describe('toggle', () => { const testPlans = toggleModel.getShortestPathPlans(); - testPlans.forEach(plan => { + testPlans.forEach((plan) => { describe(plan.description, () => { - plan.paths.forEach(path => { + plan.paths.forEach((path) => { it(path.description, async () => { // do any setup, then... @@ -142,7 +143,7 @@ Provides testing details for each event. Each key in `eventsMap` is an object wh ```js const toggleModel = createModel(toggleMachine).withEvents({ TOGGLE: { - exec: async page => { + exec: async (page) => { await page.click('input'); } } @@ -167,7 +168,7 @@ const todosModel = createModel(todosMachine).withEvents({ const plans = todosModel.getShortestPathPlans({ // Tell the algorithm to limit state/event adjacency map to states // that have less than 5 todos - filter: state => state.context.todos.length < 5 + filter: (state) => state.context.todos.length < 5 }); ``` @@ -195,7 +196,7 @@ Tests that all state nodes were covered (traversed) in the exected tests. // Only test coverage for state nodes with a `.meta` property defined: testModel.testCoverage({ - filter: stateNode => !!stateNode.meta + filter: (stateNode) => !!stateNode.meta }); ``` diff --git a/docs/packages/xstate-vue/index.md b/docs/packages/xstate-vue/index.md index 3edfbcf0e8..a5e27c61b4 100644 --- a/docs/packages/xstate-vue/index.md +++ b/docs/packages/xstate-vue/index.md @@ -75,7 +75,7 @@ A [Vue composition function](https://vue-composition-api-rfc.netlify.com/) that **Arguments** - `machine` - An [XState machine](https://xstate.js.org/docs/guides/machines.html). -- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) OR one of the following Machine Config options: `guards`, `actions`, `activities`, `services`, `delays`, `immediate`, `context`, or `state`. +- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) OR one of the following Machine Config options: `guards`, `actions`, `activities`, `behaviors`, `delays`, `immediate`, `context`, or `state`. **Returns** `{ state, send, service}`: diff --git a/docs/tutorials/reddit.md b/docs/tutorials/reddit.md index 5ce1c5a45f..b711fcfb83 100644 --- a/docs/tutorials/reddit.md +++ b/docs/tutorials/reddit.md @@ -94,8 +94,8 @@ function invokeFetchSubreddit(context) { const { subreddit } = context; return fetch(`https://www.reddit.com/r/${subreddit}.json`) - .then(response => response.json()) - .then(json => json.data.children.map(child => child.data)); + .then((response) => response.json()) + .then((json) => json.data.children.map((child) => child.data)); } const redditMachine = Machine({ @@ -207,9 +207,9 @@ import { assert } from 'chai'; import { redditMachine } from '../path/to/redditMachine'; describe('reddit machine (live)', () => { - it('should load posts of a selected subreddit', done => { + it('should load posts of a selected subreddit', (done) => { const redditService = interpret(redditMachine) - .onTransition(state => { + .onTransition((state) => { // when the state finally reaches 'selected.loaded', // the test has succeeded. @@ -249,11 +249,11 @@ const App = () => {
@@ -263,7 +263,7 @@ const App = () => { {current.matches({ selected: 'loading' }) &&
Loading...
} {current.matches({ selected: 'loaded' }) && (
    - {posts.map(post => ( + {posts.map((post) => (
  • {post.title}
  • ))}
@@ -284,7 +284,7 @@ Consider two machines: - A `subredditMachine`, which is the machine responsible for loading and displaying its specified subreddit ```js -const createSubredditMachine = subreddit => { +const createSubredditMachine = (subreddit) => { return Machine({ id: 'subreddit', initial: 'loading', @@ -362,7 +362,7 @@ const Subreddit = ({ name }) => { return (
Failed to load posts.{' '} - +
); } @@ -381,11 +381,11 @@ const Subreddit = ({ name }) => {

{subreddit}

Last updated: {lastUpdated}{' '} - +
@@ -468,7 +468,7 @@ const redditMachine = Machine({ SELECT: { target: '.selected', actions: assign((context, event) => { - // Use the existing subreddit actor if one doesn't exist + // Use the existing subreddit actor if one already exists let subreddit = context.subreddits[event.name]; if (subreddit) { diff --git a/packages/core/actor/package.json b/packages/core/actor/package.json new file mode 100644 index 0000000000..aec80551e7 --- /dev/null +++ b/packages/core/actor/package.json @@ -0,0 +1,7 @@ +{ + "main": "dist/xstate.cjs.js", + "module": "dist/xstate.esm.js", + "preconstruct": { + "source": "../src/Actor" + } +} diff --git a/packages/core/behavior/package.json b/packages/core/behavior/package.json new file mode 100644 index 0000000000..a84616dcbf --- /dev/null +++ b/packages/core/behavior/package.json @@ -0,0 +1,7 @@ +{ + "main": "dist/xstate.cjs.js", + "module": "dist/xstate.esm.js", + "preconstruct": { + "source": "../src/behavior" + } +} diff --git a/packages/core/package.json b/packages/core/package.json index e36dc4ecad..21c82abc93 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,7 +12,8 @@ "files": [ "dist", "actions", - "invoke" + "invoke", + "behavior" ], "keywords": [ "statechart", @@ -49,7 +50,8 @@ "entrypoints": [ ".", "actions", - "invoke" + "invoke", + "behavior" ] } } diff --git a/packages/core/src/Actor.ts b/packages/core/src/Actor.ts index dee98a3cf8..acb140e0c4 100644 --- a/packages/core/src/Actor.ts +++ b/packages/core/src/Actor.ts @@ -1,59 +1,165 @@ import { EventObject, Subscribable, - InvokeDefinition, - AnyEventObject + SCXML, + InvokeCallback, + InterpreterOptions, + ActorRef } from './types'; +import { MachineNode } from './MachineNode'; +import { Interpreter } from './interpreter'; +import { + Behavior, + startSignal, + ActorContext, + stopSignal, + createServiceBehavior, + createMachineBehavior, + createCallbackBehavior, + createPromiseBehavior, + createObservableBehavior, + LifecycleSignal +} from './behavior'; +import { registry } from './registry'; + +const nullSubscription = { + unsubscribe: () => void 0 +}; + +export function isActorRef(item: any): item is ActorRef { + return !!item && typeof item === 'object' && typeof item.send === 'function'; +} + +export function fromObservable( + observable: Subscribable, + parent: ActorRef, + name: string +): ActorRef { + return new ObservableActorRef( + createObservableBehavior(observable, parent), + name + ); +} + +export function fromPromise( + promise: PromiseLike, + parent: ActorRef, + name: string +): ActorRef { + return new ObservableActorRef(createPromiseBehavior(promise, parent), name); +} + +export function fromCallback( + callback: InvokeCallback, + parent: ActorRef, + name: string +): ActorRef> { + return new ObservableActorRef(createCallbackBehavior(callback, parent), name); +} + +export function fromMachine( + machine: MachineNode, + parent: ActorRef, + name: string, + options?: Partial +): ActorRef { + return new ObservableActorRef( + createMachineBehavior(machine, parent, options), + name + ); +} + +export function fromService( + service: Interpreter, + name: string = registry.bookId() +): ActorRef { + return new ObservableActorRef(createServiceBehavior(service), name); +} + +enum ProcessingStatus { + NotProcessing, + Processing +} + +export class Actor { + public current: TEmitted; + private context: ActorContext; + private behavior: Behavior; + private mailbox: TEvent[] = []; + private processingStatus: ProcessingStatus = ProcessingStatus.NotProcessing; + public name: string; -export interface Actor< - TContext = any, - TEvent extends EventObject = AnyEventObject -> extends Subscribable { - id: string; - send: (event: TEvent) => any; // TODO: change to void - stop?: () => any | undefined; - toJSON: () => { - id: string; - }; - meta?: InvokeDefinition; - state?: any; -} - -export function createNullActor( - id: string -): Actor { - return { - id, - send: () => void 0, - subscribe: () => ({ - unsubscribe: () => void 0 - }), - toJSON: () => ({ - id - }) - }; -} - -/** - * Creates a null actor that is able to be invoked given the provided - * invocation information in its `.meta` value. - * - * @param invokeDefinition The meta information needed to invoke the actor. - */ -export function createInvocableActor( - invokeDefinition: InvokeDefinition -): Actor { - const tempActor = createNullActor(invokeDefinition.id); - - tempActor.meta = invokeDefinition; - - return tempActor; -} - -export function isActor(item: any): item is Actor { - try { - return typeof item.send === 'function'; - } catch (e) { - return false; + constructor( + behavior: Behavior, + name: string, + actorContext: ActorContext + ) { + this.behavior = behavior; + this.name = name; + this.context = actorContext; + this.current = behavior.current; + } + public start() { + this.behavior = this.behavior.receiveSignal(this.context, startSignal); + return this; + } + public stop() { + this.behavior = this.behavior.receiveSignal(this.context, stopSignal); + } + public subscribe(observer) { + return this.behavior.subscribe?.(observer) || nullSubscription; + } + public receive(event) { + this.mailbox.push(event); + if (this.processingStatus === ProcessingStatus.NotProcessing) { + this.flush(); + } + } + public receiveSignal(signal: LifecycleSignal) { + this.behavior = this.behavior.receiveSignal(this.context, signal); + return this; + } + private flush() { + this.processingStatus = ProcessingStatus.Processing; + while (this.mailbox.length) { + const event = this.mailbox.shift()!; + + this.behavior = this.behavior.receive(this.context, event); + } + this.processingStatus = ProcessingStatus.NotProcessing; + } +} + +export class ObservableActorRef + implements ActorRef { + public current: TEmitted; + private context: ActorContext; + private actor: Actor; + public name: string; + + constructor(behavior: Behavior, name: string) { + this.name = name; + this.context = { + self: this, + name: this.name + }; + this.actor = new Actor(behavior, name, this.context); + this.current = this.actor.current; + } + public start() { + this.actor.receiveSignal(startSignal); + + return this; + } + public stop() { + this.actor.receiveSignal(stopSignal); + + return this; + } + public subscribe(observer) { + return this.actor.subscribe(observer); + } + public send(event) { + this.actor.receive(event); } } diff --git a/packages/core/src/MachineNode.ts b/packages/core/src/MachineNode.ts index 998c9f7810..2e26f8c520 100644 --- a/packages/core/src/MachineNode.ts +++ b/packages/core/src/MachineNode.ts @@ -8,7 +8,8 @@ import { MachineConfig, SCXML, Typestate, - Transitions + Transitions, + ActorRef } from './types'; import { State } from './State'; @@ -21,8 +22,7 @@ import { getAllStateNodes, resolveMicroTransition, macrostep, - toState, - getInitialState + toState } from './stateUtils'; import { getStateNodeById, @@ -41,8 +41,7 @@ const createDefaultOptions = ( ): MachineOptions => ({ actions: {}, guards: {}, - services: {}, - activities: {}, + behaviors: {}, delays: {}, context }); @@ -145,19 +144,20 @@ export class MachineNode< /** * Clones this state machine with custom options and context. * - * @param options Options (actions, guards, activities, services) to recursively merge with the existing options. + * @param options Options (actions, guards, behaviors, delays) to recursively merge with the existing options. * @param context Custom context (will override predefined context) + * + * @returns A new `MachineNode` instance with the custom options and context */ public withConfig( options: Partial> ): MachineNode { - const { actions, activities, guards, services, delays } = this.options; + const { actions, guards, behaviors, delays } = this.options; return new MachineNode(this.config, { actions: { ...actions, ...options.actions }, - activities: { ...activities, ...options.activities }, guards: { ...guards, ...options.guards }, - services: { ...services, ...options.services }, + behaviors: { ...behaviors, ...options.behaviors }, delays: { ...delays, ...options.delays }, context: resolveContext(this.context, options.context) }); @@ -206,11 +206,12 @@ export class MachineNode< */ public transition( state: StateValue | State = this.initialState, - event: Event | SCXML.Event + event: Event | SCXML.Event, + self?: ActorRef ): State { const currentState = toState(state, this); - return macrostep(currentState, event, this); + return macrostep(currentState, event, this, self); } /** @@ -222,7 +223,8 @@ export class MachineNode< */ public microstep( state: StateValue | State = this.initialState, - event: Event | SCXML.Event + event: Event | SCXML.Event, + self?: ActorRef ): State { const resolvedState = toState(state, this); const _event = toSCXMLEvent(event); @@ -242,7 +244,13 @@ export class MachineNode< const transitions: Transitions = transitionNode(this, resolvedState.value, resolvedState, _event) || []; - return resolveMicroTransition(this, transitions, resolvedState, _event); + return resolveMicroTransition( + this, + transitions, + resolvedState, + _event, + self + ); } /** @@ -251,7 +259,24 @@ export class MachineNode< */ public get initialState(): State { this._init(); - const nextState = getInitialState(this); + const nextState = resolveMicroTransition(this, [], undefined, undefined); + return macrostep(nextState, null as any, this); + } + + /** + * Returns the initial `State` instance, with reference to `self` as an `ActorRef`. + * + * @param self The `ActorRef` instance of this machine, if any. + */ + public getInitialState(self?: ActorRef) { + this._init(); + const nextState = resolveMicroTransition( + this, + [], + undefined, + undefined, + self + ); return macrostep(nextState, null as any, this); } diff --git a/packages/core/src/State.ts b/packages/core/src/State.ts index b7c6918e7a..de8a24f904 100644 --- a/packages/core/src/State.ts +++ b/packages/core/src/State.ts @@ -9,13 +9,13 @@ import { TransitionDefinition, Typestate, HistoryValue, - NullEvent + NullEvent, + ActorRef } from './types'; import { matchesState, keys, isString } from './utils'; import { StateNode } from './StateNode'; import { nextEvents } from './stateUtils'; import { initEvent } from './actions'; -import { Actor } from './Actor'; export function isState< TContext, @@ -95,9 +95,9 @@ export class State< */ public transitions: Array>; /** - * An object mapping actor IDs to spawned actors/invoked services. + * An object mapping actor IDs to spawned actors/invoked behaviors. */ - public children: Actor[]; + public children: Record>; /** * Creates a new State instance for the given `stateValue` and `context`. * @param stateValue @@ -119,7 +119,7 @@ export class State< meta: {}, configuration: [], // TODO: fix, transitions: [], - children: [] + children: {} }); } @@ -138,7 +138,7 @@ export class State< meta: undefined, configuration: [], transitions: [], - children: [] + children: {} }); } /** @@ -181,15 +181,9 @@ export class State< } /** - * Creates a new State instance. - * @param value The state value - * @param context The extended state - * @param history The previous state - * @param actions An array of action objects to execute as side-effects - * @param activities A mapping of activities and whether they are started (`true`) or stopped (`false`). - * @param meta - * @param events Internal event queue. Should be empty with run-to-completion semantics. - * @param configuration + * Creates a new `State` instance that represents the current state of a running machine. + * + * @param config */ constructor(config: StateConfig) { this.value = config.value; diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 6d8d0e400f..1b68ccf487 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -259,7 +259,7 @@ export class StateNode< } /** - * The services invoked by this state node. + * The behaviors invoked as actors by this state node. */ public get invoke(): Array> { return ( @@ -275,11 +275,11 @@ export class StateNode< : resolvedId; if ( - !this.machine.options.services[resolvedSrc] && + !this.machine.options.behaviors[resolvedSrc] && !isString(invokeConfig.src) ) { - this.machine.options.services = { - ...this.machine.options.services, + this.machine.options.behaviors = { + ...this.machine.options.behaviors, [resolvedSrc]: invokeConfig.src as any }; } diff --git a/packages/core/src/actionTypes.ts b/packages/core/src/actionTypes.ts index eeff089a14..58454288f5 100644 --- a/packages/core/src/actionTypes.ts +++ b/packages/core/src/actionTypes.ts @@ -1,7 +1,6 @@ import { ActionTypes } from './types'; // xstate-specific action types -export const start = ActionTypes.Start; export const stop = ActionTypes.Stop; export const raise = ActionTypes.Raise; export const send = ActionTypes.Send; diff --git a/packages/core/src/actions.ts b/packages/core/src/actions.ts index 8a4421b4f6..d6c2e94255 100644 --- a/packages/core/src/actions.ts +++ b/packages/core/src/actions.ts @@ -14,9 +14,7 @@ import { AssignAction, ActionFunction, ActionFunctionMap, - ActivityActionObject, ActionTypes, - ActivityDefinition, SpecialTargets, RaiseAction, RaiseActionObject, @@ -34,7 +32,11 @@ import { ExprWithMeta, ChooseConditon, ChooseAction, - AnyEventObject + InvokeDefinition, + InvokeActionObject, + StopActionObject, + AnyEventObject, + ActorRef } from './types'; import * as actionTypes from './actionTypes'; import { @@ -130,18 +132,6 @@ export const toActionObjects = ( ); }; -export function toActivityDefinition( - action: string | ActivityDefinition -): ActivityDefinition { - const actionObject = toActionObject(action); - - return { - id: isString(action) ? action : actionObject.id, - ...actionObject, - type: actionObject.type - }; -} - /** * Raises an event. This places the event in the internal event queue, so that * the event is immediately consumed by the machine in the current step. @@ -376,37 +366,28 @@ export const resolveCancel = ( return action as CancelActionObject; }; -/** - * Starts an activity. - * - * @param activity The activity to start. - */ -export function start( - activity: string | ActivityDefinition -): ActivityActionObject { - const activityDef = toActivityDefinition(activity); - +export function invoke( + invokeDef: InvokeDefinition +): InvokeActionObject { return { - type: ActionTypes.Start, - actor: activityDef, + type: ActionTypes.Invoke, + src: invokeDef.src, + id: invokeDef.id, + autoForward: invokeDef.autoForward, + data: invokeDef.data, exec: undefined }; } /** - * Stops an activity. + * Stops an actor. * - * @param activity The activity to stop. + * @param actorRef The `ActorRef` instance or its ID */ -export function stop( - activity: string | ActivityDefinition -): ActivityActionObject { - const activityDef = toActivityDefinition(activity); - +export function stop(actorRef: string | ActorRef): StopActionObject { return { type: ActionTypes.Stop, - actor: activityDef, - exec: undefined + actor: actorRef }; } diff --git a/packages/core/src/behavior.ts b/packages/core/src/behavior.ts new file mode 100644 index 0000000000..1841ea0038 --- /dev/null +++ b/packages/core/src/behavior.ts @@ -0,0 +1,358 @@ +import { + EventObject, + InvokeCallback, + Subscribable, + Subscription, + InterpreterOptions, + Spawnable, + Observer, + ActorRef +} from './types'; +import { + toSCXMLEvent, + isPromiseLike, + isObservable, + isMachineNode, + isSCXMLEvent +} from './utils'; +import { doneInvoke, error, actionTypes } from './actions'; +import { isFunction } from 'util'; +import { MachineNode } from './MachineNode'; +import { interpret, Interpreter } from './interpreter'; +import { State } from './State'; + +export interface ActorContext { + self: ActorRef; // TODO: use type params + name: string; +} + +export const startSignal = Symbol.for('xstate.invoke'); +export const stopSignal = Symbol.for('xstate.stop'); + +export type LifecycleSignal = typeof startSignal | typeof stopSignal; + +/** + * An object that expresses the behavior of an actor in reaction to received events, + * as well as an optionally emitted stream of values. + * + * @template TReceived The received event + * @template TEmitted The emitted value + */ +export interface Behavior { + receive: ( + actorContext: ActorContext, + event: TReceived + ) => Behavior; + receiveSignal: ( + actorContext: ActorContext, + signal: LifecycleSignal + ) => Behavior; + /** + * The most recently emitted value + */ + current: TEmitted; + subscribe?: (observer: Observer) => Subscription | undefined; +} + +export function createCallbackBehavior( + callback: InvokeCallback, + parent?: ActorRef +): Behavior { + let canceled = false; + const receivers = new Set<(e: EventObject) => void>(); + let dispose; + + const behavior: Behavior = { + receive: (_, event) => { + const plainEvent = isSCXMLEvent(event) ? event.data : event; + receivers.forEach((receiver) => receiver(plainEvent)); + + return behavior; + }, + receiveSignal: (actorContext, signal) => { + if (signal === startSignal) { + dispose = callback( + (e) => { + if (canceled) { + return; + } + + parent?.send(toSCXMLEvent(e, { origin: actorContext.self })); + }, + (newListener) => { + receivers.add(newListener); + } + ); + + if (isPromiseLike(dispose)) { + dispose.then( + (resolved) => { + parent?.send( + toSCXMLEvent(doneInvoke(actorContext.name, resolved) as any, { + origin: actorContext.self + }) + ); + canceled = true; + }, + (errorData) => { + const errorEvent = error(actorContext.name, errorData); + parent?.send( + toSCXMLEvent(errorEvent, { origin: actorContext.self }) + ); + canceled = true; + } + ); + } + } + + if (signal === stopSignal) { + canceled = true; + + if (isFunction(dispose)) { + dispose(); + } + } + + return behavior; + }, + current: undefined + }; + + return behavior; +} + +export function createPromiseBehavior( + promise: PromiseLike, + parent?: ActorRef +): Behavior { + let canceled = false; + const observers: Set> = new Set(); + + const behavior: Behavior = { + receive: () => { + return behavior; + }, + receiveSignal: (actorContext: ActorContext, signal: LifecycleSignal) => { + switch (signal) { + case startSignal: + const resolvedPromise = Promise.resolve(promise); + + resolvedPromise.then( + (response) => { + if (!canceled) { + parent?.send( + toSCXMLEvent(doneInvoke(actorContext.name, response) as any, { + origin: actorContext.self + }) + ); + + observers.forEach((observer) => { + observer.next?.(response); + observer.complete?.(); + }); + } + }, + (errorData) => { + if (!canceled) { + const errorEvent = error(actorContext.name, errorData); + + parent?.send( + toSCXMLEvent(errorEvent, { origin: actorContext.self }) + ); + + observers.forEach((observer) => { + observer.error?.(errorData); + }); + } + } + ); + return behavior; + case stopSignal: + canceled = true; + observers.clear(); + return behavior; + default: + return behavior; + } + }, + subscribe: (observer) => { + observers.add(observer); + + return { + unsubscribe: () => { + observers.delete(observer); + } + }; + }, + current: undefined + }; + + return behavior; +} + +export function createObservableBehavior< + T extends EventObject, + TEvent extends EventObject +>( + observable: Subscribable, + parent?: ActorRef +): Behavior { + let subscription: Subscription | undefined; + + const behavior: Behavior = { + receiveSignal: (actorContext, signal) => { + if (signal === startSignal) { + subscription = observable.subscribe( + (value) => { + parent?.send(toSCXMLEvent(value, { origin: actorContext.self })); + }, + (err) => { + parent?.send( + toSCXMLEvent(error(actorContext.name, err) as any, { + origin: actorContext.self + }) + ); + }, + () => { + parent?.send( + toSCXMLEvent(doneInvoke(actorContext.name) as any, { + origin: actorContext.self + }) + ); + } + ); + } else if (signal === stopSignal) { + subscription && subscription.unsubscribe(); + } + + return behavior; + }, + receive: () => behavior, + subscribe: (observer) => { + return observable.subscribe(observer); + }, + current: undefined + }; + + return behavior; +} + +export function createMachineBehavior( + machine: MachineNode, + parent?: ActorRef, + options?: Partial +): Behavior> { + let service: Interpreter; + let subscription: Subscription; + + const behavior: Behavior> = { + receiveSignal: (actorContext, signal) => { + if (signal === startSignal) { + service = interpret(machine, { + ...options, + parent, + id: actorContext.name + }); + service.onDone((doneEvent) => { + parent?.send( + toSCXMLEvent(doneEvent, { + origin: actorContext.self + }) + ); + }); + + if (options?.sync) { + subscription = service.subscribe((state) => { + parent?.send( + toSCXMLEvent( + { + type: actionTypes.update, + state + }, + { origin: actorContext.self } + ) + ); + }); + } + service.start(); + } else if (signal === stopSignal) { + service.stop(); + subscription && subscription.unsubscribe(); // TODO: might not be necessary + } + return behavior; + }, + receive: (_, event) => { + service.send(event); + return behavior; + }, + subscribe: (observer) => { + return service?.subscribe(observer); + }, + current: machine.initialState // TODO: this should get from machine.getInitialState(ref) + }; + + return behavior; +} + +export function createServiceBehavior( + service: Interpreter +): Behavior> { + const behavior: Behavior> = { + receive: (actorContext, event) => { + service.send(toSCXMLEvent(event, { origin: actorContext.self })); + return behavior; + }, + receiveSignal: () => { + return behavior; + }, + subscribe: (observer) => { + return service.subscribe(observer); + }, + current: service.state + }; + + return behavior; +} + +export function createBehaviorFrom( + entity: PromiseLike, + parent?: ActorRef +): Behavior; +export function createBehaviorFrom( + entity: Subscribable, + parent?: ActorRef +): Behavior; +export function createBehaviorFrom< + TEvent extends EventObject, + TEmitted extends State +>( + entity: MachineNode, + parent?: ActorRef +): Behavior; +export function createBehaviorFrom( + entity: InvokeCallback, + parent?: ActorRef +): Behavior; +export function createBehaviorFrom( + entity: Spawnable, + parent?: ActorRef +): Behavior { + if (isPromiseLike(entity)) { + return createPromiseBehavior(entity, parent); + } + + if (isObservable(entity)) { + return createObservableBehavior(entity, parent); + } + + if (isMachineNode(entity)) { + // @ts-ignore + return createMachineBehavior(entity, parent); + } + + if (isFunction(entity)) { + return createCallbackBehavior(entity as InvokeCallback, parent); + } + + throw new Error(`Unable to create behavior from entity`); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5be1312681..c998227fa3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,7 +3,6 @@ import { mapState } from './mapState'; import { StateNode } from './StateNode'; import { State } from './State'; import { Machine, createMachine } from './Machine'; -import { Actor as ActorType } from './Actor'; import { raise, send, @@ -11,7 +10,6 @@ import { sendUpdate, log, cancel, - start, stop, assign, after, @@ -23,7 +21,7 @@ import { choose, pure } from './actions'; -import { interpret, Interpreter, spawn } from './interpreter'; +import { interpret, Interpreter } from './interpreter'; import { matchState } from './match'; export { MachineNode } from './MachineNode'; export { SimulatedClock } from './SimulatedClock'; @@ -35,7 +33,6 @@ const actions = { sendUpdate, log, cancel, - start, stop, assign, after, @@ -62,13 +59,10 @@ export { interpret, Interpreter, matchState, - spawn, doneInvoke, createMachine }; -export type Actor = ActorType; - export * from './types'; // TODO: decide from where those should be exported diff --git a/packages/core/src/interpreter.ts b/packages/core/src/interpreter.ts index f286e223df..0ce5f35631 100644 --- a/packages/core/src/interpreter.ts +++ b/packages/core/src/interpreter.ts @@ -5,54 +5,42 @@ import { DefaultContext, ActionObject, StateSchema, - ActivityActionObject, SpecialTargets, ActionTypes, - InvokeDefinition, SendActionObject, - ServiceConfig, - InvokeCallback, - DisposeActivityFunction, StateValue, InterpreterOptions, - ActivityDefinition, SingleOrArray, - Subscribable, DoneEvent, - Unsubscribable, + Subscription, MachineOptions, ActionFunctionMap, SCXML, Observer, - Spawnable, Typestate, - AnyEventObject + BehaviorCreator, + InvokeActionObject, + AnyEventObject, + ActorRef } from './types'; import { State, bindActionToState, isState } from './State'; import * as actionTypes from './actionTypes'; import { doneInvoke, error, getActionFunction, initEvent } from './actions'; import { IS_PRODUCTION } from './environment'; import { - isPromiseLike, mapContext, warn, keys, isArray, isFunction, - isString, - isObservable, - uniqueId, - isMachineNode, toSCXMLEvent, - reportUnhandledExceptionOnInvocation, symbolObservable } from './utils'; import { Scheduler } from './scheduler'; -import { Actor, isActor } from './Actor'; +import { isActorRef, fromService, ObservableActorRef } from './Actor'; import { isInFinalState } from './stateUtils'; import { registry } from './registry'; import { registerService } from './devTools'; -import { DEFAULT_SPAWN_OPTIONS } from './invoke'; import { MachineNode } from './MachineNode'; export type StateListener< @@ -75,43 +63,13 @@ export type EventListener = ( ) => void; export type Listener = () => void; +export type ErrorListener = (error: Error) => void; export interface Clock { setTimeout(fn: (...args: any[]) => void, timeout: number): any; clearTimeout(id: any): void; } -interface SpawnOptions { - name?: string; - autoForward?: boolean; - sync?: boolean; -} - -/** - * Maintains a stack of the current service in scope. - * This is used to provide the correct service to spawn(). - * - * @private - */ -const withServiceScope = (() => { - const serviceStack = [] as Array>; - - return >( - service: TService | undefined, - fn: (service: TService) => T - ) => { - service && serviceStack.push(service); - - const result = fn( - service || (serviceStack[serviceStack.length - 1] as TService) - ); - - service && serviceStack.pop(); - - return result; - }; -})(); - enum InterpreterStatus { NotStarted, Running, @@ -124,7 +82,7 @@ export class Interpreter< TStateSchema extends StateSchema = any, TEvent extends EventObject = EventObject, TTypestate extends Typestate = any -> implements Actor, TEvent> { +> { /** * The default interpreter options: * @@ -163,6 +121,7 @@ export class Interpreter< > = new Set(); private contextListeners: Set> = new Set(); private stopListeners: Set = new Set(); + private errorListeners: Set = new Set(); private doneListeners: Set = new Set(); private eventListeners: Set = new Set(); private sendListeners: Set = new Set(); @@ -170,18 +129,18 @@ export class Interpreter< /** * Whether the service is started. */ - public initialized = false; private _status: InterpreterStatus = InterpreterStatus.NotStarted; - // Actor - public parent?: Actor; + // Actor Ref + public parent?: ActorRef; public id: string; + public ref: ActorRef; /** * The globally unique process ID for this invocation. */ public sessionId: string; - public children: Map = new Map(); + public children: Map> = new Map(); private forwardTo: Set = new Set(); // Dev Tools @@ -217,27 +176,20 @@ export class Interpreter< deferEvents: this.options.deferEvents }); - this.sessionId = registry.bookId(); + this.ref = fromService(this, resolvedId); + + this.sessionId = this.ref.name; } public get initialState(): State { if (this._initialState) { return this._initialState; } - return withServiceScope(this, () => { - this._initialState = this.machine.initialState; + this._initialState = this.machine.getInitialState(this.ref); - return this._initialState; - }); + return this._initialState; } public get state(): State { - if (!IS_PRODUCTION) { - warn( - this._status !== InterpreterStatus.NotStarted, - `Attempted to read state from uninitialized service '${this.id}'. Make sure the service is started first.` - ); - } - return this._state!; } public static interpret = interpret; @@ -332,13 +284,13 @@ export class Interpreter< } public subscribe( observer: Observer> - ): Unsubscribable; + ): Subscription; public subscribe( nextListener?: (state: State) => void, // @ts-ignore errorListener?: (error: any) => void, completeListener?: () => void - ): Unsubscribable; + ): Subscription; public subscribe( nextListenerOrObserver?: | ((state: State) => void) @@ -346,7 +298,7 @@ export class Interpreter< // @ts-ignore errorListener?: (error: any) => void, completeListener?: () => void - ): Unsubscribable { + ): Subscription { if (!nextListenerOrObserver) { return { unsubscribe: () => void 0 }; } @@ -357,13 +309,15 @@ export class Interpreter< if (typeof nextListenerOrObserver === 'function') { listener = nextListenerOrObserver; } else { - listener = nextListenerOrObserver.next.bind(nextListenerOrObserver); - resolvedCompleteListener = nextListenerOrObserver.complete.bind( + listener = nextListenerOrObserver.next?.bind(nextListenerOrObserver); + resolvedCompleteListener = nextListenerOrObserver.complete?.bind( nextListenerOrObserver ); } - this.listeners.add(listener); + if (listener) { + this.listeners.add(listener); + } // Send current state to listener if (this._status === InterpreterStatus.Running) { @@ -423,6 +377,12 @@ export class Interpreter< this.stopListeners.add(listener); return this; } + public onError( + listener: ErrorListener + ): Interpreter { + this.errorListeners.add(listener); + return this; + } /** * Adds a state listener that is notified when the statechart has reached its final state. * @param listener The state listener @@ -466,8 +426,7 @@ export class Interpreter< return this; } - registry.register(this.sessionId, this as Actor); - this.initialized = true; + registry.register(this.sessionId, this.ref); this._status = InterpreterStatus.Running; const resolvedState = @@ -521,12 +480,12 @@ export class Interpreter< } this.scheduler.clear(); - this.initialized = false; this._status = InterpreterStatus.Stopped; registry.free(this.sessionId); return this; } + /** * Sends an event to the running interpreter to trigger a transition. * @@ -632,9 +591,7 @@ export class Interpreter< this.forward(_event); - nextState = withServiceScope(this, () => { - return this.machine.transition(nextState, _event); - }); + nextState = this.machine.transition(nextState, _event, this.ref); batchedActions.push( ...(nextState.actions.map((a) => @@ -662,15 +619,14 @@ export class Interpreter< return this.send.bind(this, event); } - private sendTo = ( + private sendTo( event: SCXML.Event, - to: string | number | Actor - ) => { - const isParent = - this.parent && (to === SpecialTargets.Parent || this.parent.id === to); + to: string | number | ActorRef + ) { + const isParent = this.parent && to === SpecialTargets.Parent; const target = isParent ? this.parent - : isActor(to) + : isActorRef(to) ? to : this.children.get(to) || registry.get(to as string); @@ -691,20 +647,12 @@ export class Interpreter< return; } - if ('machine' in (target as any)) { - const scxmlEvent = { - ...event, - name: - event.name === actionTypes.error ? `${error(this.id)}` : event.name, - origin: this.sessionId - }; - // Send SCXML events to machines - target.send(scxmlEvent); - } else { - // Send normal events to other targets - target.send(event.data); - } - }; + target.send({ + ...event, + name: event.name === actionTypes.error ? `${error(this.id)}` : event.name, + origin: this + }); + } /** * Returns the next state given the interpreter's current state and the event. * @@ -723,12 +671,17 @@ export class Interpreter< (nextEvent) => nextEvent.indexOf(actionTypes.errorPlatform) === 0 ) ) { - throw (_event.data as any).data; + // TODO: refactor into proper error handler + if (this.errorListeners.size > 0) { + this.errorListeners.forEach((listener) => { + listener((_event.data as any).data); + }); + } else { + throw (_event.data as any).data; + } } - const nextState = withServiceScope(this, () => { - return this.machine.transition(this.state, _event); - }); + const nextState = this.machine.transition(this.state, _event, this.ref); return nextState; } @@ -761,13 +714,14 @@ export class Interpreter< delete this.delayedEventsMap[sendId]; } private exec( - action: ActionObject, + action: InvokeActionObject | ActionObject, state: State, - actionFunctionMap?: ActionFunctionMap + actionFunctionMap: ActionFunctionMap = this.machine + .options.actions ): void { const { context, _event } = state; const actionOrExec = - getActionFunction(action.type, actionFunctionMap) || action.exec; + action.exec || getActionFunction(action.type, actionFunctionMap); const exec = isFunction(actionOrExec) ? actionOrExec : actionOrExec @@ -815,74 +769,69 @@ export class Interpreter< this.cancel((action as CancelActionObject).sendId); break; - case actionTypes.start: { - const activity = (action as ActivityActionObject) - .actor as InvokeDefinition; - // If the activity will be stopped right after it's started + case ActionTypes.Invoke: { + const { id, data, autoForward, src } = action as InvokeActionObject; + + // If the "activity" will be stopped right after it's started // (such as in transient states) // don't bother starting the activity. - // if (!this.state.activities[activity.type]) { - // break; - // } - - // Invoked services - if (activity.type === ActionTypes.Invoke) { - const serviceCreator: - | ServiceConfig - | undefined = this.machine.options.services - ? this.machine.options.services[activity.src] - : undefined; - - const { id, data } = activity; + if ( + state.actions.find((otherAction) => { + return ( + otherAction.type === actionTypes.stop && otherAction.actor === id + ); + }) + ) { + return; + } - const autoForward = - 'autoForward' in activity - ? activity.autoForward - : !!activity.forward; + try { + let actorRef: ActorRef; - if (!serviceCreator) { - // tslint:disable-next-line:no-console - if (!IS_PRODUCTION) { - warn( - false, - `No service found for invocation '${activity.src}' in machine '${this.machine.id}'.` - ); + if (isActorRef(src)) { + actorRef = src; + } else { + const behaviorCreator: + | BehaviorCreator + | undefined = this.machine.options.behaviors[src]; + + if (!behaviorCreator) { + if (!IS_PRODUCTION) { + warn( + false, + `No behavior found for invocation '${src}' in machine '${this.machine.id}'.` + ); + } + return; } - return; - } - const actor = serviceCreator(context, _event.data, { - parent: this as any, - id, - data, - _event - }); + const behavior = behaviorCreator(context, _event.data, { + parent: this.ref, + id, + data, + _event + }); + + actorRef = new ObservableActorRef(behavior, id); + } if (autoForward) { this.forwardTo.add(id); } - this.children.set(id, actor); - - const childIndex = this.state.children.findIndex( - (child) => child.id === id - ); - - this.state.children[childIndex] = actor; + this.children.set(id, actorRef); + this.state.children[id] = actorRef; - this.state.children[childIndex].meta = { - ...this.state.children[childIndex].meta, - ...activity - }; - } else { - this.spawnActivity(activity); + actorRef.start(); + } catch (err) { + this.send(error(id, err)); } break; } case actionTypes.stop: { - this.stopChild(action.actor.id); + this.stopChild(action.ref); break; } @@ -913,10 +862,7 @@ export class Interpreter< this.children.delete(childId); this.forwardTo.delete(childId); - const childIndex = this.state.children.findIndex( - (actor) => actor.id === childId - ); - this.state.children.splice(childIndex, 1); + delete this.state.children[childId]; } private stopChild(childId: string): void { @@ -931,277 +877,6 @@ export class Interpreter< child.stop(); } } - public spawn(entity: Spawnable, name: string, options?: SpawnOptions): Actor { - if (isPromiseLike(entity)) { - return this.spawnPromise(Promise.resolve(entity), name); - } else if (isFunction(entity)) { - return this.spawnCallback(entity as InvokeCallback, name); - } else if (isActor(entity)) { - return this.spawnActor(entity); - } else if (isObservable(entity)) { - return this.spawnObservable(entity, name); - } else if (isMachineNode(entity)) { - return this.spawnMachine(entity, { ...options, id: name }); - } else { - throw new Error( - `Unable to spawn entity "${name}" of type "${typeof entity}".` - ); - } - } - public spawnMachine< - TChildContext, - TChildStateSchema, - TChildEvent extends EventObject - >( - machine: MachineNode, - options: { id?: string; autoForward?: boolean; sync?: boolean } = {} - ): Interpreter { - const childService = interpret(machine, { - ...this.options, // inherit options from this interpreter - parent: this as Actor, - id: options.id || machine.id - }); - - const resolvedOptions = { - ...DEFAULT_SPAWN_OPTIONS, - ...options - }; - - if (resolvedOptions.sync) { - childService.onTransition((state) => { - this.send({ - type: actionTypes.update, - state, - id: childService.id - } as any); - }); - } - - const actor = childService; - - this.children.set( - childService.id, - actor as Actor, TChildEvent> - ); - - if (resolvedOptions.autoForward) { - this.forwardTo.add(childService.id); - } - - childService - .onDone((doneEvent) => { - this.removeChild(childService.id); - this.send(toSCXMLEvent(doneEvent as any, { origin: childService.id })); - }) - .start(); - - return actor; - } - private spawnPromise(promise: Promise, id: string): Actor { - let canceled = false; - - promise.then( - (response) => { - if (!canceled) { - this.removeChild(id); - this.send( - toSCXMLEvent(doneInvoke(id, response) as any, { origin: id }) - ); - } - }, - (errorData) => { - if (!canceled) { - this.removeChild(id); - const errorEvent = error(id, errorData); - try { - // Send "error.platform.id" to this (parent). - this.send(toSCXMLEvent(errorEvent as any, { origin: id })); - } catch (error) { - reportUnhandledExceptionOnInvocation(errorData, error, id); - if (this.devTools) { - this.devTools.send(errorEvent, this.state); - } - if (this.machine.strict) { - // it would be better to always stop the state machine if unhandled - // exception/promise rejection happens but because we don't want to - // break existing code so enforce it on strict mode only especially so - // because documentation says that onError is optional - this.stop(); - } - } - } - } - ); - - const actor = { - id, - send: () => void 0, - subscribe: (next, handleError, complete) => { - let unsubscribed = false; - promise.then( - (response) => { - if (unsubscribed) { - return; - } - next && next(response); - if (unsubscribed) { - return; - } - complete && complete(); - }, - (err) => { - if (unsubscribed) { - return; - } - handleError(err); - } - ); - - return { - unsubscribe: () => (unsubscribed = true) - }; - }, - stop: () => { - canceled = true; - }, - toJSON() { - return { id }; - } - }; - - this.children.set(id, actor); - - return actor; - } - private spawnCallback(callback: InvokeCallback, id: string): Actor { - let canceled = false; - const receivers = new Set<(e: EventObject) => void>(); - const listeners = new Set<(e: EventObject) => void>(); - - const receive = (e: TEvent) => { - listeners.forEach((listener) => listener(e)); - if (canceled) { - return; - } - this.send(e); - }; - - let callbackStop; - - try { - callbackStop = callback(receive, (newListener) => { - receivers.add(newListener); - }); - } catch (err) { - this.send(error(id, err) as any); - } - - if (isPromiseLike(callbackStop)) { - // it turned out to be an async function, can't reliably check this before calling `callback` - // because transpiled async functions are not recognizable - return this.spawnPromise(callbackStop as Promise, id); - } - - const actor = { - id, - send: (event) => receivers.forEach((receiver) => receiver(event)), - subscribe: (next) => { - listeners.add(next); - - return { - unsubscribe: () => { - listeners.delete(next); - } - }; - }, - stop: () => { - canceled = true; - if (isFunction(callbackStop)) { - callbackStop(); - } - }, - toJSON() { - return { id }; - } - }; - - this.children.set(id, actor); - - return actor; - } - private spawnObservable( - source: Subscribable, - id: string - ): Actor { - const subscription = source.subscribe( - (value) => { - this.send(toSCXMLEvent(value, { origin: id })); - }, - (err) => { - this.removeChild(id); - this.send(toSCXMLEvent(error(id, err) as any, { origin: id })); - }, - () => { - this.removeChild(id); - this.send(toSCXMLEvent(doneInvoke(id) as any, { origin: id })); - } - ); - - const actor = { - id, - send: () => void 0, - subscribe: (next, handleError, complete) => { - return source.subscribe(next, handleError, complete); - }, - stop: () => subscription.unsubscribe(), - toJSON() { - return { id }; - } - }; - - this.children.set(id, actor); - - return actor; - } - private spawnActor(actor: T): T { - this.children.set(actor.id, actor); - - return actor; - } - private spawnActivity(activity: ActivityDefinition): void { - const implementation = - this.machine.options && this.machine.options.activities - ? this.machine.options.activities[activity.type] - : undefined; - - if (!implementation) { - if (!IS_PRODUCTION) { - warn(false, `No implementation found for activity '${activity.type}'`); - } - // tslint:disable-next-line:no-console - return; - } - - // Start implementation - const dispose = implementation(this.state.context, activity); - this.spawnEffect(activity.id, dispose); - } - private spawnEffect( - id: string, - dispose?: DisposeActivityFunction | void - ): void { - this.children.set(id, { - id, - send: () => void 0, - subscribe: () => { - return { unsubscribe: () => void 0 }; - }, - stop: dispose || undefined, - toJSON() { - return { id }; - } - }); - } private attachDev(): void { if (this.options.devTools && typeof window !== 'undefined') { @@ -1239,6 +914,7 @@ export class Interpreter< registerService(this); } } + public toJSON() { return { id: this.id @@ -1250,60 +926,6 @@ export class Interpreter< } } -const createNullActor = (name: string = 'null'): Actor => ({ - id: name, - send: () => void 0, - subscribe: () => { - // tslint:disable-next-line:no-empty - return { unsubscribe: () => {} }; - }, - toJSON: () => ({ id: name }) -}); - -const resolveSpawnOptions = (nameOrOptions?: string | SpawnOptions) => { - if (isString(nameOrOptions)) { - return { ...DEFAULT_SPAWN_OPTIONS, name: nameOrOptions }; - } - - return { - ...DEFAULT_SPAWN_OPTIONS, - name: uniqueId(), - ...nameOrOptions - }; -}; - -export function spawn( - entity: MachineNode, - nameOrOptions?: string | SpawnOptions -): Interpreter; -export function spawn( - entity: Spawnable, - nameOrOptions?: string | SpawnOptions -): Actor; -export function spawn( - entity: Spawnable, - nameOrOptions?: string | SpawnOptions -): Actor { - const resolvedOptions = resolveSpawnOptions(nameOrOptions); - - return withServiceScope(undefined, (service) => { - if (!IS_PRODUCTION) { - warn( - !!service, - `Attempted to spawn an Actor (ID: "${ - isMachineNode(entity) ? entity.id : 'undefined' - }") outside of a service. This will have no effect.` - ); - } - - if (service) { - return service.spawn(entity, resolvedOptions.name, resolvedOptions); - } else { - return createNullActor(resolvedOptions.name); - } - }); -} - /** * Creates a new Interpreter instance for the given machine with the provided options, if any. * diff --git a/packages/core/src/invoke.ts b/packages/core/src/invoke.ts index aa9c02ef13..7c6858ed90 100644 --- a/packages/core/src/invoke.ts +++ b/packages/core/src/invoke.ts @@ -1,281 +1,78 @@ import { EventObject, - Actor, - InvokeCreator, InvokeCallback, - Subscribable + Subscribable, + BehaviorCreator, + SCXML } from '.'; -import { interpret } from './interpreter'; - -import { actionTypes, doneInvoke, error } from './actions'; - -import { - toSCXMLEvent, - reportUnhandledExceptionOnInvocation, - isFunction, - isPromiseLike, - mapContext -} from './utils'; +import { isFunction, mapContext } from './utils'; import { AnyEventObject } from './types'; import { MachineNode } from './MachineNode'; +import { + createMachineBehavior, + createCallbackBehavior, + Behavior, + createObservableBehavior, + createPromiseBehavior +} from './behavior'; export const DEFAULT_SPAWN_OPTIONS = { sync: false }; -export function spawnMachine< +export function invokeMachine< TContext, TEvent extends EventObject, TMachine extends MachineNode >( machine: TMachine | ((ctx: TContext, event: TEvent) => TMachine), options: { sync?: boolean } = {} -): InvokeCreator { - return (ctx, event, { parent, id, data, _event }) => { +): BehaviorCreator { + return (ctx, event, { parent, data, _event }) => { let resolvedMachine = isFunction(machine) ? machine(ctx, event) : machine; if (data) { resolvedMachine = resolvedMachine.withContext( mapContext(data, ctx, _event) ) as TMachine; } - const childService = interpret(resolvedMachine, { - ...options, - parent, - id: id || resolvedMachine.id, - clock: (parent as any).clock - }); - - const resolvedOptions = { - ...DEFAULT_SPAWN_OPTIONS, - ...options - }; - - if (resolvedOptions.sync) { - childService.onTransition((state) => { - parent.send({ - type: actionTypes.update, - state, - id: childService.id - }); - }); - } - - childService - .onDone((doneEvent) => { - parent.send( - toSCXMLEvent(doneInvoke(id, doneEvent.data), { - origin: childService.id - }) - ); - }) - .start(); - - const actor = childService; - - return actor as Actor; + return createMachineBehavior(resolvedMachine, parent, options); }; } -export function spawnPromise( +export function invokePromise( promise: | PromiseLike | ((ctx: any, event: AnyEventObject) => PromiseLike) -): InvokeCreator { - return (ctx, e, { parent, id }) => { - let canceled = false; - +): BehaviorCreator { + return (ctx, e, { parent }) => { const resolvedPromise = isFunction(promise) ? promise(ctx, e) : promise; - - resolvedPromise.then( - (response) => { - if (!canceled) { - parent.send( - toSCXMLEvent(doneInvoke(id, response) as any, { origin: id }) - ); - } - }, - (errorData) => { - if (!canceled) { - const errorEvent = error(id, errorData); - try { - // Send "error.platform.id" to this (parent). - parent.send(toSCXMLEvent(errorEvent as any, { origin: id })); - } catch (error) { - reportUnhandledExceptionOnInvocation(errorData, error, id); - // if (this.devTools) { - // this.devTools.send(errorEvent, this.state); - // } - // if (this.machine.strict) { - // // it would be better to always stop the state machine if unhandled - // // exception/promise rejection happens but because we don't want to - // // break existing code so enforce it on strict mode only especially so - // // because documentation says that onError is optional - // canceled = true; - // } - } - } - } - ); - - const actor = { - id, - send: () => void 0, - subscribe: (next, handleError, complete) => { - let unsubscribed = false; - resolvedPromise.then( - (response) => { - if (unsubscribed) { - return; - } - next && next(response); - if (unsubscribed) { - return; - } - complete && complete(); - }, - (err) => { - if (unsubscribed) { - return; - } - handleError(err); - } - ); - - return { - unsubscribe: () => (unsubscribed = true) - }; - }, - stop: () => { - canceled = true; - }, - toJSON() { - return { id }; - } - }; - - return actor; + return createPromiseBehavior(resolvedPromise, parent); }; } -export function spawnActivity( +export function invokeActivity( activityCreator: (ctx: TC, event: TE) => any -): InvokeCreator { - return (ctx, e, { parent, id }) => { - let dispose; - try { - dispose = activityCreator(ctx, e); - } catch (err) { - parent.send(error(id, err) as any); - } - - return { - id, - send: () => void 0, - toJSON: () => ({ id }), - subscribe() { - // do nothing - return { - unsubscribe: () => void 0 - }; - }, - stop: isFunction(dispose) ? () => dispose() : undefined - }; +): BehaviorCreator { + const callbackCreator = (ctx: TC, event: TE) => () => { + return activityCreator(ctx, event); }; + + return invokeCallback(callbackCreator); } -export function spawnCallback( - callbackCreator: (ctx: any, e: any) => InvokeCallback -): InvokeCreator { - return (ctx, event, { parent, id, _event }) => { +export function invokeCallback( + callbackCreator: (ctx: TC, e: TE) => InvokeCallback +): BehaviorCreator { + return (ctx, event, { parent }): Behavior, undefined> => { const callback = callbackCreator(ctx, event); - let canceled = false; - const receivers = new Set<(e: EventObject) => void>(); - const listeners = new Set<(e: EventObject) => void>(); - - const receive = (receivedEvent: TE) => { - listeners.forEach((listener) => listener(receivedEvent)); - if (canceled) { - return; - } - parent.send(receivedEvent); - }; - - let callbackStop; - - try { - callbackStop = callback(receive, (newListener) => { - receivers.add(newListener); - }); - } catch (err) { - parent.send(error(id, err) as any); - } - - if (isPromiseLike(callbackStop)) { - // it turned out to be an async function, can't reliably check this before calling `callback` - // because transpiled async functions are not recognizable - return spawnPromise(callbackStop as Promise)(ctx, event, { - parent, - id, - _event - }); - } - - const actor = { - id, - send: (receivedEvent) => - receivers.forEach((receiver) => receiver(receivedEvent)), - subscribe: (next) => { - listeners.add(next); - - return { - unsubscribe: () => { - listeners.delete(next); - } - }; - }, - stop: () => { - canceled = true; - if (isFunction(callbackStop)) { - callbackStop(); - } - }, - toJSON() { - return { id }; - } - }; - - return actor; + return createCallbackBehavior>(callback, parent); }; } -export function spawnObservable( +export function invokeObservable( source: Subscribable | ((ctx: any, event: any) => Subscribable) -): InvokeCreator { - return (ctx, e, { parent, id }) => { +): BehaviorCreator { + return (ctx, e, { parent }): Behavior => { const resolvedSource = isFunction(source) ? source(ctx, e) : source; - const subscription = resolvedSource.subscribe( - (value) => { - parent.send(toSCXMLEvent(value, { origin: id })); - }, - (err) => { - parent.send(toSCXMLEvent(error(id, err) as any, { origin: id })); - }, - () => { - parent.send(toSCXMLEvent(doneInvoke(id) as any, { origin: id })); - } - ); - - const actor = { - id, - send: () => void 0, - subscribe: (next, handleError, complete) => { - return resolvedSource.subscribe(next, handleError, complete); - }, - stop: () => subscription.unsubscribe(), - toJSON() { - return { id }; - } - }; - - return actor; + return createObservableBehavior(resolvedSource, parent); }; } diff --git a/packages/core/src/json.ts b/packages/core/src/json.ts index 18755a3520..1a3ed191b2 100644 --- a/packages/core/src/json.ts +++ b/packages/core/src/json.ts @@ -71,12 +71,16 @@ export function machineToJSON(stateNode: StateNode): StateNodeConfig { } export function stringify(machine: StateNode): string { - return JSON.stringify(machineToJSON(machine), (_, value) => { - if (isFunction(value)) { - return { $function: value.toString() }; - } - return value; - }); + return JSON.stringify( + machineToJSON(machine), + (_, value) => { + if (isFunction(value)) { + return { $function: value.toString() }; + } + return value; + }, + 2 + ); } export function parse(machineString: string): StateNodeConfig { diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index efca439583..a20e166309 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -1,13 +1,13 @@ -import { Actor } from './Actor'; +import { ActorRef } from './types'; -const children = new Map(); +const children = new Map>(); let sessionIdIndex = 0; export interface Registry { bookId(): string; - register(id: string, actor: Actor): string; - get(id: string): Actor | undefined; + register(id: string, actor: ActorRef): string; + get(id: string): ActorRef | undefined; free(id: string): void; } diff --git a/packages/core/src/scxml.ts b/packages/core/src/scxml.ts index c811899c62..7aaf1e31d8 100644 --- a/packages/core/src/scxml.ts +++ b/packages/core/src/scxml.ts @@ -10,7 +10,7 @@ import { import { Machine } from './index'; import { mapValues, keys, isString, flatten } from './utils'; import * as actions from './actions'; -import { spawnMachine } from './invoke'; +import { invokeMachine } from './invoke'; import { MachineNode } from './MachineNode'; function getAttribute( @@ -425,7 +425,7 @@ function toConfig( return { ...(element.attributes!.id && { id: element.attributes!.id as string }), - src: spawnMachine(scxmlToMachine(content, options)), + src: invokeMachine(scxmlToMachine(content, options)), autoForward: element.attributes!.autoforward === 'true' }; }); diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 3b971c5cff..909c4cc0bc 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1,4 +1,4 @@ -import { EventObject, StateNode, StateValue, Actor } from '.'; +import { EventObject, StateNode, StateValue } from '.'; import { keys, flatten, @@ -13,9 +13,9 @@ import { normalizeTarget, toStateValue, mapContext, - updateContext, toSCXMLEvent } from './utils'; +import { updateContext } from './updateContext'; import { TransitionConfig, TransitionDefinition, @@ -39,12 +39,14 @@ import { PureAction, RaiseActionObject, SpecialTargets, - ActivityActionObject, HistoryValue, InitialTransitionConfig, InitialTransitionDefinition, Event, - ChooseAction + ChooseAction, + StopActionObject, + AnyEventObject, + ActorRef } from './types'; import { State } from './State'; import { @@ -55,7 +57,6 @@ import { doneInvoke, error, toActionObjects, - start, stop, initEvent, actionTypes, @@ -63,7 +64,8 @@ import { resolveSend, resolveLog, resolveCancel, - toActionObject + toActionObject, + invoke } from './actions'; import { IS_PRODUCTION } from './environment'; import { @@ -72,7 +74,7 @@ import { NULL_EVENT, WILDCARD } from './constants'; -import { createInvocableActor } from './Actor'; +import { isActorRef } from './Actor'; import { MachineNode } from './MachineNode'; type Configuration = Iterable< @@ -581,7 +583,8 @@ export function getInitialStateNodes( target: [stateNode], source: stateNode, actions: [], - eventType: 'init' + eventType: 'init', + toJSON: null as any // TODO: fix } ]; const mutStatesToEnter = new Set>(); @@ -596,16 +599,6 @@ export function getInitialStateNodes( return [...mutStatesToEnter]; } -export function getInitialState< - TContext, - TStateSchema, - TEvent extends EventObject, - TTypestate extends Typestate ->( - machine: MachineNode -): State { - return resolveMicroTransition(machine, [], undefined, undefined); -} /** * Returns the child state node from its relative `stateKey`, or throws. */ @@ -1034,7 +1027,7 @@ function exitStates( const actions: Array> = []; statesToExit.forEach((stateNode) => { - actions.push(...stateNode.invoke.map((def) => stop(def))); + actions.push(...stateNode.invoke.map((def) => stop(def.id))); }); statesToExit.sort((a, b) => b.order - a.order); @@ -1315,7 +1308,8 @@ export function microstep( currentState: State | undefined, mutConfiguration: Set>, machine: MachineNode, - _event: SCXML.Event + _event: SCXML.Event, + service?: ActorRef ): { actions: Array>; configuration: typeof mutConfiguration; @@ -1362,7 +1356,7 @@ export function microstep( actions.push( ...flatten( [...res.statesToInvoke].map((s) => - s.invoke.map((invokeDef) => start(invokeDef)) + s.invoke.map((invokeDef) => invoke(invokeDef)) ) ) ); @@ -1373,7 +1367,7 @@ export function microstep( actions: resolvedActions, raised, context - } = resolveActionsAndContext(actions, machine, _event, currentState); + } = resolveActionsAndContext(actions, machine, _event, currentState, service); internalQueue.push(...res.internalQueue); internalQueue.push(...raised.map((a) => a._event)); @@ -1436,7 +1430,8 @@ export function resolveMicroTransition< machine: MachineNode, transitions: Transitions, currentState?: State, - _event: SCXML.Event = initEvent as SCXML.Event + _event: SCXML.Event = initEvent as SCXML.Event, + service?: ActorRef ): State { // Transition will "apply" if: // - the state node is the initial state (there is no current state) @@ -1455,13 +1450,15 @@ export function resolveMicroTransition< target: [...prevConfig].filter(isAtomicStateNode), source: machine, actions: [], - eventType: null as any + eventType: null as any, + toJSON: null as any // TODO: fix } ], currentState, new Set(prevConfig), machine, - _event + _event, + service ); if (currentState && !willTransition) { @@ -1472,18 +1469,21 @@ export function resolveMicroTransition< return inertState; } - let children = currentState ? [...currentState.children] : ([] as Actor[]); + let children = currentState ? { ...currentState.children } : {}; for (const action of resolved.actions) { - if (action.type === actionTypes.start) { - children.push(createInvocableActor((action as any).actor)); - } else if (action.type === actionTypes.stop) { - children = children.filter((childActor) => { - return ( - childActor.id !== - (action as ActivityActionObject).actor.id - ); - }); + if (action.type === actionTypes.stop) { + const { actor: ref } = action as StopActionObject; + if (isActorRef(ref)) { + ref.stop(); + delete children[ref.name]; + } else { + const actorRef = children[ref]; + if (actorRef) { + actorRef.stop(); + } + delete children[ref]; + } } } @@ -1550,7 +1550,8 @@ function resolveActionsAndContext( actions: Array>, machine: MachineNode, _event: SCXML.Event, - currentState: State | undefined + currentState: State | undefined, + service?: ActorRef ): { actions: typeof actions; raised: Array>; @@ -1577,7 +1578,7 @@ function resolveActionsAndContext( break; case actionTypes.send: const sendAction = resolveSend( - actionObject as SendAction, + actionObject as SendAction, context, _event, machine.machine.options.delays @@ -1592,7 +1593,7 @@ function resolveActionsAndContext( ); } if (sendAction.to === SpecialTargets.Internal) { - raisedActions.push(sendAction as RaiseActionObject); + raisedActions.push(sendAction as RaiseActionObject); } else { resActions.push(sendAction); } @@ -1645,13 +1646,15 @@ function resolveActionsAndContext( } break; case actionTypes.assign: - context = updateContext( + const [nextContext, nextActions] = updateContext( context, _event, [actionObject as AssignAction], - currentState + currentState, + service ); - resActions.push(actionObject); + context = nextContext; + resActions.push(actionObject, ...nextActions); break; default: resActions.push( @@ -1672,11 +1675,13 @@ function resolveActionsAndContext( export function macrostep( state: State>, event: Event | SCXML.Event | null, - machine: MachineNode + machine: MachineNode, + service?: ActorRef ): State { // Assume the state is at rest (no raised events) // Determine the next state based on the next microstep - const nextState = event === null ? state : machine.microstep(state, event); + const nextState = + event === null ? state : machine.microstep(state, event, service); const { _internalQueue } = nextState; let maybeNextState = nextState; @@ -1688,7 +1693,8 @@ export function macrostep( maybeNextState = machine.microstep( maybeNextState, - raisedEvent as SCXML.Event + raisedEvent as SCXML.Event, + service ); _internalQueue.push(...maybeNextState._internalQueue); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 53c6f3ec07..a6ec1ea7f0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,8 +1,8 @@ import { StateNode } from './StateNode'; import { State } from './State'; import { Clock } from './interpreter'; -import { Actor } from './Actor'; import { MachineNode } from './MachineNode'; +import { Behavior } from './behavior'; export type EventType = string; export type ActionType = string; @@ -55,6 +55,11 @@ export interface AssignMeta { state?: State; action: AssignAction; _event: SCXML.Event; + self?: ActorRef; + spawn: { + (behavior: Behavior, name?: string): ActorRef; + from: (entity: T, name?: string) => ActorRefFrom; + }; } export type ActionFunction = ( @@ -165,24 +170,6 @@ export type Transition = | TransitionConfig | ConditionalTransitionConfig; -export type DisposeActivityFunction = () => void; - -export type ActivityConfig = ( - ctx: TContext, - activity: ActivityDefinition -) => DisposeActivityFunction | void; - -export type Activity = - | string - | ActivityDefinition; - -export interface ActivityDefinition - extends ActionObject { - id: string; - type: string; -} - -export type Sender = (event: Event) => void; export type Receiver = ( listener: (event: TEvent) => void ) => void; @@ -192,29 +179,21 @@ export type InvokeCallback = ( onReceive: Receiver ) => any; -/** - * Returns either a Promises or a callback handler (for streams of events) given the - * machine's current `context` and `event` that invoked the service. - * - * For Promises, the only events emitted to the parent will be: - * - `done.invoke.` with the `data` containing the resolved payload when the promise resolves, or: - * - `error.platform.` with the `data` containing the caught error, and `src` containing the service `id`. - * - * For callback handlers, the `callback` will be provided, which will send events to the parent service. - * - * @param context The current machine `context` - * @param event The event that invoked the service - */ -export type InvokeCreator = ( +export type BehaviorCreator = ( context: TContext, event: TEvent, - meta: { parent: Actor; id: string; data?: any; _event: SCXML.Event } -) => Actor; + meta: { + parent: ActorRef; + id: string; + data?: any; + _event: SCXML.Event; + } +) => Behavior; -export interface InvokeDefinition - extends ActivityDefinition { +export interface InvokeDefinition { + id: string; /** - * The source of the machine to be invoked, or the machine itself. + * The source of the actor's behavior to be invoked */ src: string; /** @@ -230,6 +209,18 @@ export interface InvokeDefinition * Data should be mapped to match the child machine's context shape. */ data?: Mapper | PropertyMapper; + /** + * The transition to take upon the invoked child machine reaching its final top-level state. + */ + onDone?: + | string + | SingleOrArray>>; + /** + * The transition to take upon the invoked child machine sending an error event. + */ + onError?: + | string + | SingleOrArray>>; } export interface Delay { @@ -346,7 +337,7 @@ export interface InvokeConfig { /** * The source of the machine to be invoked, or the machine itself. */ - src: string | InvokeCreator; + src: string | BehaviorCreator; /** * If `true`, events sent to the parent service will be forwarded to the invoked service. * @@ -421,7 +412,7 @@ export interface StateNodeConfig< * The services to invoke upon entering this state node. These services will be stopped upon exiting this state node. */ invoke?: SingleOrArray< - string | InvokeCreator | InvokeConfig + string | BehaviorCreator | InvokeConfig >; /** * The mapping of event types to their potential transition(s). @@ -543,10 +534,6 @@ export type DelayFunctionMap = Record< DelayConfig >; -export type ServiceConfig = - | string - | InvokeCreator; - export type DelayConfig = | number | DelayExpr; @@ -554,8 +541,7 @@ export type DelayConfig = export interface MachineOptions { guards: Record>; actions: ActionFunctionMap; - activities: Record>; - services: Record>; + behaviors: Record>; delays: DelayFunctionMap; context: Partial; } @@ -593,7 +579,6 @@ export type Transitions = Array< >; export enum ActionTypes { - Start = 'xstate.start', Stop = 'xstate.stop', Raise = 'xstate.raise', Send = 'xstate.send', @@ -655,16 +640,18 @@ export interface NullEvent { type: ActionTypes.NullEvent; } -export interface ActivityActionObject - extends ActionObject { - type: ActionTypes.Start | ActionTypes.Stop; - actor: ActivityDefinition; - exec: ActionFunction | undefined; +export interface InvokeActionObject { + type: ActionTypes.Invoke; + src: string | ActorRef; + id: string; + autoForward?: boolean; + data?: any; + exec?: undefined; } -export interface InvokeActionObject - extends ActivityActionObject { - actor: InvokeDefinition; +export interface StopActionObject { + type: ActionTypes.Stop; + actor: string | ActorRef; } export type DelayExpr = ExprWithMeta< @@ -698,8 +685,12 @@ export interface SendAction< to: | string | number - | Actor - | ExprWithMeta + | ActorRef + | ExprWithMeta< + TContext, + TEvent, + string | number | ActorRef | undefined + > | undefined; event: TSentEvent | SendExpr; delay?: number | string | DelayExpr; @@ -711,7 +702,7 @@ export interface SendActionObject< TEvent extends EventObject, TSentEvent extends EventObject = AnyEventObject > extends SendAction { - to: string | number | Actor | undefined; + to: string | number | ActorRef | undefined; _event: SCXML.Event; event: TSentEvent; delay?: number; @@ -743,7 +734,14 @@ export enum SpecialTargets { export interface SendActionOptions { id?: string | number; delay?: number | string | DelayExpr; - to?: string | ExprWithMeta; + to?: + | string + | ExprWithMeta< + TContext, + TEvent, + string | number | ActorRef | undefined + > + | undefined; } export interface CancelAction @@ -940,7 +938,7 @@ export interface StateConfig { meta?: any; configuration: Array>; transitions: Array>; - children: Actor[]; + children: Record>; done?: boolean; } @@ -959,7 +957,7 @@ export interface InterpreterOptions { execute: boolean; clock: Clock; logger: (...args: any[]) => void; - parent?: Actor; + parent?: ActorRef; /** * If `true`, defers processing of sent events until the service * is initialized (`.start()`). Otherwise, an error will be thrown @@ -979,6 +977,12 @@ export interface InterpreterOptions { */ devTools: boolean | object; // TODO: add enhancer options [option: string]: any; + /** + * If `true`, events from the parent will be sent to this interpreter. + * + * Default: `false` + */ + autoForward?: boolean; } export declare namespace SCXML { @@ -1014,7 +1018,7 @@ export declare namespace SCXML { * a response back to the originating entity via the Event I/O Processor specified in 'origintype'. * For internal and platform events, the Processor must leave this field blank. */ - origin?: string; + origin?: ActorRef; /** * This is equivalent to the 'type' field on the element. * For external events, the SCXML Processor should set this field to a value which, @@ -1045,26 +1049,60 @@ export declare namespace SCXML { } } +export type Spawnable = + | MachineNode + | PromiseLike + | InvokeCallback + | Subscribable; + // Taken from RxJS -export interface Unsubscribable { +export interface Subscription { unsubscribe(): any | void; } + +export interface Observer { + // Sends the next value in the sequence + next?: (value: T) => void; + + // Sends the sequence error + error?: (errorValue: any) => void; + + // Sends the completion notification + complete: () => void; +} + export interface Subscribable { + subscribe(observer: Observer): Subscription; subscribe( - next?: (value: T) => void, + next: (value: T) => void, error?: (error: any) => void, complete?: () => void - ): Unsubscribable; + ): Subscription; } -export interface Observer { - next: (value: T) => void; - error: (err: any) => void; - complete: () => void; +export interface ActorLike + extends Subscribable { + send: Sender; } -export type Spawnable = - | MachineNode - | Promise - | InvokeCallback - | Subscribable; +export type Sender = (event: TEvent) => void; + +export interface ActorRef + extends Subscribable { + send: Sender; + start: () => ActorRef; + stop: () => void; + /** + * The most recently emitted value. + */ + current: TEmitted; + name: string; +} + +export type ActorRefFrom = T extends MachineNode< + infer TC, + any, + infer TE +> + ? ActorRef> + : ActorRef; // TODO: expand diff --git a/packages/core/src/updateContext.ts b/packages/core/src/updateContext.ts new file mode 100644 index 0000000000..984a2887e5 --- /dev/null +++ b/packages/core/src/updateContext.ts @@ -0,0 +1,81 @@ +import { + EventObject, + AssignAction, + SCXML, + AssignMeta, + ActionObject, + InvokeActionObject, + ActionTypes, + Spawnable, + ActorRef, + ActorRefFrom +} from './types'; +import { IS_PRODUCTION } from './environment'; +import { State } from '.'; +import { ObservableActorRef } from './Actor'; +import { warn, isFunction, keys } from './utils'; +import { createBehaviorFrom, Behavior } from './behavior'; +import { registry } from './registry'; + +export function updateContext( + context: TContext, + _event: SCXML.Event, + assignActions: Array>, + state?: State, + service?: ActorRef +): [TContext, ActionObject[]] { + if (!IS_PRODUCTION) { + warn(!!context, 'Attempting to update undefined context'); + } + const capturedActions: InvokeActionObject[] = []; + + const updatedContext = context + ? assignActions.reduce((acc, assignAction) => { + const { assignment } = assignAction as AssignAction; + + const spawner = (behavior: Behavior, name: string) => { + const actorRef = new ObservableActorRef(behavior, name); + + capturedActions.push({ + type: ActionTypes.Invoke, + src: actorRef, + id: name + }); + + return actorRef; + }; + + spawner.from = ( + entity: T, + name: string = registry.bookId() // TODO: use more universal uniqueid + ): ActorRefFrom => { + const behavior = createBehaviorFrom(entity as any, service); // TODO: fix + + return (spawner(behavior, name) as unknown) as ActorRefFrom; // TODO: fix + }; + + const meta: AssignMeta = { + state, + action: assignAction, + _event, + self: service, + spawn: spawner + }; + + let partialUpdate: Partial = {}; + if (isFunction(assignment)) { + partialUpdate = assignment(acc, _event.data, meta); + } else { + for (const key of keys(assignment)) { + const propAssignment = assignment[key]; + partialUpdate[key] = isFunction(propAssignment) + ? propAssignment(acc, _event.data, meta) + : propAssignment; + } + } + return Object.assign({}, acc, partialUpdate); + }, context) + : context; + + return [updatedContext, capturedActions]; +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index f95d6f3640..a405ee7f93 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -5,7 +5,6 @@ import { PropertyMapper, Mapper, EventType, - AssignAction, Condition, Subscribable, ConditionPredicate, @@ -15,7 +14,8 @@ import { TransitionConfigTarget, NullEvent, SingleOrArray, - Guard + Guard, + BehaviorCreator } from './types'; import { STATE_DELIMITER, @@ -24,9 +24,9 @@ import { } from './constants'; import { IS_PRODUCTION } from './environment'; import { StateNode } from './StateNode'; -import { State, InvokeConfig, InvokeCreator } from '.'; -import { Actor } from './Actor'; +import { InvokeConfig } from '.'; import { MachineNode } from './MachineNode'; +import { Behavior } from './behavior'; export function keys(value: T): Array { return Object.keys(value) as Array; @@ -288,47 +288,11 @@ export function isPromiseLike(value: any): value is PromiseLike { return false; } -export function updateContext( - context: TContext, - _event: SCXML.Event, - assignActions: Array>, - state?: State -): TContext { - if (!IS_PRODUCTION) { - warn(!!context, 'Attempting to update undefined context'); - } - const updatedContext = context - ? assignActions.reduce((acc, assignAction) => { - const { assignment } = assignAction as AssignAction; - - const meta = { - state, - action: assignAction, - _event - }; - - let partialUpdate: Partial = {}; - - if (isFunction(assignment)) { - partialUpdate = assignment(acc, _event.data, meta); - } else { - for (const key of keys(assignment)) { - const propAssignment = assignment[key]; - - partialUpdate[key] = isFunction(propAssignment) - ? propAssignment(acc, _event.data, meta) - : propAssignment; - } - } - - return Object.assign({}, acc, partialUpdate); - }, context) - : context; - return updatedContext; -} - // tslint:disable-next-line:no-empty -let warn: (condition: boolean | Error, message: string) => void = () => {}; +export let warn: ( + condition: boolean | Error, + message: string +) => void = () => {}; if (!IS_PRODUCTION) { warn = (condition: boolean | Error, message: string) => { @@ -348,8 +312,6 @@ if (!IS_PRODUCTION) { }; } -export { warn }; - export function isArray(value: any): value is any[] { return Array.isArray(value); } @@ -411,10 +373,6 @@ export function isMachineNode(value: any): value is MachineNode { } } -export function isActor(value: any): value is Actor { - return !!value && typeof value.send === 'function'; -} - export const uniqueId = (() => { let currentId = 0; @@ -434,11 +392,17 @@ export function toEventObject( return event; } +export function isSCXMLEvent( + event: Event | SCXML.Event +): event is SCXML.Event { + return !isString(event) && '$$type' in event && event.$$type === 'scxml'; +} + export function toSCXMLEvent( event: Event | SCXML.Event, scxmlEvent?: Partial> ): SCXML.Event { - if (!isString(event) && '$$type' in event && event.$$type === 'scxml') { + if (isSCXMLEvent(event)) { return event as SCXML.Event; } @@ -524,11 +488,21 @@ export function toInvokeConfig( invocable: | InvokeConfig | string - | InvokeCreator, + | BehaviorCreator + | Behavior, id: string ): InvokeConfig { - if (typeof invocable === 'object' && 'src' in invocable) { - return invocable; + if (typeof invocable === 'object') { + if ('src' in invocable) { + return invocable; + } + + if ('receive' in invocable) { + return { + id, + src: () => invocable + }; + } } return { diff --git a/packages/core/test/actionCreators.test.ts b/packages/core/test/actionCreators.test.ts index f0635476ec..07ece2b53e 100644 --- a/packages/core/test/actionCreators.test.ts +++ b/packages/core/test/actionCreators.test.ts @@ -4,57 +4,6 @@ import { toSCXMLEvent } from '../src/utils'; const { actionTypes } = actions; describe('action creators', () => { - ['start', 'stop'].forEach((actionKey) => { - describe(`${actionKey}()`, () => { - it('should accept a string action', () => { - const action = actions[actionKey]('test'); - expect(action.type).toEqual(actionTypes[actionKey]); - expect(action).toEqual({ - type: actionTypes[actionKey], - exec: undefined, - actor: { - type: 'test', - exec: undefined, - id: 'test' - } - }); - }); - - it('should accept an action object', () => { - const action = actions[actionKey]({ type: 'test', foo: 'bar' }); - expect(action.type).toEqual(actionTypes[actionKey]); - expect(action).toEqual({ - type: actionTypes[actionKey], - exec: undefined, - actor: { - type: 'test', - id: undefined, - foo: 'bar' - } - }); - }); - - it('should accept an actor definition', () => { - const action = actions[actionKey]({ - type: 'test', - foo: 'bar', - src: 'someSrc' - }); - expect(action.type).toEqual(actionTypes[actionKey]); - expect(action).toEqual({ - type: actionTypes[actionKey], - exec: undefined, - actor: { - type: 'test', - id: undefined, - foo: 'bar', - src: 'someSrc' - } - }); - }); - }); - }); - describe('send()', () => { it('should accept a string event', () => { const action = actions.send('foo'); diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 2862899a1e..a886ac13bf 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -3,11 +3,11 @@ import { createMachine, assign, forwardTo, - interpret, - spawn + interpret } from '../src/index'; import { pure, sendParent, log, choose } from '../src/actions'; -import { spawnMachine } from '../src/invoke'; +import { invokeMachine } from '../src/invoke'; +import { ActorRef } from '../src'; describe('entry/exit actions', () => { const pedestrianStates = { @@ -1026,12 +1026,15 @@ describe('forwardTo()', () => { } }); - const parent = Machine({ + const parent = Machine< + undefined, + { type: 'EVENT'; value: number } | { type: 'SUCCESS' } + >({ id: 'parent', initial: 'first', states: { first: { - invoke: { src: spawnMachine(child), id: 'myChild' }, + invoke: { src: invokeMachine(child), id: 'myChild' }, on: { EVENT: { actions: forwardTo('myChild') @@ -1068,16 +1071,19 @@ describe('forwardTo()', () => { } }); - const parent = Machine<{ child: any }>({ + const parent = Machine< + { child?: ActorRef }, + { type: 'EVENT'; value: number } | { type: 'SUCCESS' } + >({ id: 'parent', initial: 'first', context: { - child: null + child: undefined }, states: { first: { entry: assign({ - child: () => spawn(child) + child: (_, __, { spawn }) => spawn.from(child, 'x') }), on: { EVENT: { diff --git a/packages/core/test/activities.test.ts b/packages/core/test/activities.test.ts index 2b8670603e..e999e0af44 100644 --- a/packages/core/test/activities.test.ts +++ b/packages/core/test/activities.test.ts @@ -1,5 +1,5 @@ -import { Machine } from '../src/index'; -import { actionTypes } from '../src/actions'; +import { Machine, interpret } from '../src/index'; +import { invokeActivity } from '../src/invoke'; const lightMachine = Machine({ key: 'light', @@ -35,33 +35,36 @@ const lightMachine = Machine({ }); describe('activities with guarded transitions', () => { - const machine = Machine({ - initial: 'A', - states: { - A: { - on: { - E: 'B' + it('should activate even if there are subsequent automatic, but blocked transitions', (done) => { + const machine = Machine( + { + initial: 'A', + states: { + A: { + on: { + E: 'B' + } + }, + B: { + invoke: ['B_ACTIVITY'], + on: { + '': [{ cond: () => false, target: 'A' }] + } + } } }, - B: { - invoke: ['B_ACTIVITY'], - on: { - '': [{ cond: () => false, target: 'A' }] + { + behaviors: { + B_ACTIVITY: invokeActivity(() => { + done(); + }) } } - } - }); + ); - it('should activate even if there are subsequent automatic, but blocked transitions', () => { - let state = machine.initialState; - state = machine.transition(state, 'E'); + const service = interpret(machine).start(); - expect( - state.children.find((child) => child.meta!.src === 'B_ACTIVITY') - ).toBeTruthy(); - expect(state.actions).toContainEqual( - expect.objectContaining({ type: actionTypes.start }) - ); + service.send('E'); }); }); @@ -83,117 +86,115 @@ describe('remembering activities', () => { } }); - it('should remember the activities even after an event', () => { - let state = machine.initialState; - state = machine.transition(state, 'E'); - state = machine.transition(state, 'IGNORE'); - expect( - state.children.find((child) => child.meta!.src === 'B_ACTIVITY') - ).toBeTruthy(); + it('should remember the activities even after an event', (done) => { + const service = interpret( + machine.withConfig({ + behaviors: { + B_ACTIVITY: invokeActivity(() => { + done(); + }) + } + }) + ).start(); + + service.send('E'); + service.send('IGNORE'); }); }); describe('activities', () => { - it('identifies initial activities', () => { - const { initialState } = lightMachine; + it('identifies initial activities', (done) => { + const service = interpret( + lightMachine.withConfig({ + behaviors: { + fadeInGreen: invokeActivity(() => { + done(); + }) + } + }) + ); - expect( - initialState.children.find((child) => child.meta!.src === 'fadeInGreen') - ).toBeTruthy(); + service.start(); }); - it('identifies start activities', () => { - const nextState = lightMachine.transition('yellow', 'TIMER'); - expect( - nextState.children.find( - (child) => child.meta!.src === 'activateCrosswalkLight' - ) - ).toBeTruthy(); - expect(nextState.actions).toContainEqual( - expect.objectContaining({ - actor: expect.objectContaining({ src: 'activateCrosswalkLight' }) + it('identifies start activities', (done) => { + const service = interpret( + lightMachine.withConfig({ + behaviors: { + activateCrosswalkLight: invokeActivity(() => { + done(); + }) + } }) ); + + service.start(); + service.send('TIMER'); // yellow + service.send('TIMER'); // red }); - it('identifies start activities for child states and active activities', () => { - const redWalkState = lightMachine.transition('yellow', 'TIMER'); - const nextState = lightMachine.transition(redWalkState, 'PED_WAIT'); - expect( - nextState.children.find( - (child) => child.meta!.src === 'activateCrosswalkLight' - ) - ).toBeTruthy(); - expect( - nextState.children.find( - (child) => child.meta!.src === 'blinkCrosswalkLight' - ) - ).toBeTruthy(); - expect(nextState.actions).toContainEqual( - expect.objectContaining({ - type: actionTypes.start, - actor: expect.objectContaining({ - src: 'blinkCrosswalkLight' - }) + it('identifies start activities for child states and active activities', (done) => { + const service = interpret( + lightMachine.withConfig({ + behaviors: { + blinkCrosswalkLight: invokeActivity(() => { + done(); + }) + } }) ); - }); - it('identifies stop activities for child states', () => { - const redWalkState = lightMachine.transition('yellow', 'TIMER'); - const redWaitState = lightMachine.transition(redWalkState, 'PED_WAIT'); - const nextState = lightMachine.transition(redWaitState, 'PED_STOP'); + service.start(); + service.send('TIMER'); // yellow + service.send('TIMER'); // red.walk + service.send('PED_WAIT'); // red.wait + }); - expect( - nextState.children.find( - (child) => child.meta!.src === 'activateCrosswalkLight' - ) - ).toBeTruthy(); - expect( - nextState.children.find( - (child) => child.meta!.src === 'blinkCrosswalkLight' - ) - ).toBeFalsy(); - expect(nextState.actions).toContainEqual( - expect.objectContaining({ - type: actionTypes.stop, - actor: expect.objectContaining({ src: 'blinkCrosswalkLight' }) + it('identifies stop activities for child states', (done) => { + const service = interpret( + lightMachine.withConfig({ + behaviors: { + blinkCrosswalkLight: invokeActivity(() => { + return () => { + done(); + }; + }) + } }) ); - }); - it('identifies multiple stop activities for child and parent states', () => { - const redWalkState = lightMachine.transition('yellow', 'TIMER'); - const redWaitState = lightMachine.transition(redWalkState, 'PED_WAIT'); - const redStopState = lightMachine.transition(redWaitState, 'PED_STOP'); - const nextState = lightMachine.transition(redStopState, 'TIMER'); + service.start(); + service.send('TIMER'); // yellow + service.send('TIMER'); // red.walk + service.send('PED_WAIT'); // red.wait + service.send('PED_STOP'); + }); - expect( - nextState.children.find((child) => child.meta!.src === 'fadeInGreen') - ).toBeTruthy(); - expect( - nextState.children.find( - (child) => child.meta!.src === 'activateCrosswalkLight' - ) - ).toBeFalsy(); - expect( - nextState.children.find( - (child) => child.meta!.src === 'blinkCrosswalkLight' - ) - ).toBeFalsy(); + it('identifies multiple stop activities for child and parent states', (done) => { + let stopActivateCrosswalkLightcalled = false; - expect(nextState.actions).toContainEqual( - expect.objectContaining({ - type: actionTypes.stop, - actor: expect.objectContaining({ src: 'activateCrosswalkLight' }) + const service = interpret( + lightMachine.withConfig({ + behaviors: { + fadeInGreen: invokeActivity(() => { + if (stopActivateCrosswalkLightcalled) { + done(); + } + }), + activateCrosswalkLight: invokeActivity(() => { + return () => { + stopActivateCrosswalkLightcalled = true; + }; + }) + } }) ); - expect(nextState.actions).toContainEqual( - expect.objectContaining({ - type: actionTypes.start, - actor: expect.objectContaining({ src: 'fadeInGreen' }) - }) - ); + service.start(); + service.send('TIMER'); // yellow + service.send('TIMER'); // red.walk + service.send('PED_WAIT'); // red.wait + service.send('PED_STOP'); // red.stop + service.send('TIMER'); // green }); }); @@ -275,50 +276,86 @@ describe('transient activities', () => { } }); - it('should have started initial activities', () => { - const state = machine.initialState; - expect( - state.children.find((child) => child.meta!.src === 'A') - ).toBeTruthy(); + it('should have started initial activities', (done) => { + const service = interpret( + machine.withConfig({ + behaviors: { + A: invokeActivity(() => { + done(); + }) + } + }) + ); + + service.start(); }); - it('should have started deep initial activities', () => { - const state = machine.initialState; - expect( - state.children.find((child) => child.meta!.src === 'A1') - ).toBeTruthy(); + it('should have started deep initial activities', (done) => { + const service = interpret( + machine.withConfig({ + behaviors: { + A1: invokeActivity(() => { + done(); + }) + } + }) + ); + service.start(); }); - it('should have kept existing activities', () => { - let state = machine.initialState; - state = machine.transition(state, 'A'); - expect( - state.children.find((child) => child.meta!.src === 'A') - ).toBeTruthy(); + it('should have kept existing activities', (done) => { + const service = interpret( + machine.withConfig({ + behaviors: { + A: invokeActivity(() => { + done(); + }) + } + }) + ).start(); + + service.send('A'); }); - it('should have kept same activities', () => { - let state = machine.initialState; - state = machine.transition(state, 'C_SIMILAR'); - expect( - state.children.find((child) => child.meta!.src === 'C1') - ).toBeTruthy(); + it('should have kept same activities', (done) => { + const service = interpret( + machine.withConfig({ + behaviors: { + C1: invokeActivity(() => { + done(); + }) + } + }) + ).start(); + + service.send('C_SIMILAR'); }); - it('should have kept same activities after self transition', () => { - let state = machine.initialState; - state = machine.transition(state, 'C'); - expect( - state.children.find((child) => child.meta!.src === 'C1') - ).toBeTruthy(); + it('should have kept same activities after self transition', (done) => { + const service = interpret( + machine.withConfig({ + behaviors: { + C1: invokeActivity(() => { + done(); + }) + } + }) + ).start(); + + service.send('C'); }); - it('should have stopped after automatic transitions', () => { - let state = machine.initialState; - state = machine.transition(state, 'A'); - expect(state.value).toEqual({ A: 'A2', B: 'B2', C: 'C1' }); - expect( - state.children.find((child) => child.meta!.src === 'B2') - ).toBeTruthy(); + it('should have stopped after automatic transitions', (done) => { + const service = interpret( + machine.withConfig({ + behaviors: { + B2: invokeActivity(() => { + done(); + }) + } + }) + ).start(); + + service.send('A'); }); }); diff --git a/packages/core/test/actor.test.ts b/packages/core/test/actor.test.ts index 5a02f4879d..07f4e8b6f7 100644 --- a/packages/core/test/actor.test.ts +++ b/packages/core/test/actor.test.ts @@ -1,4 +1,4 @@ -import { Machine, spawn, interpret, Interpreter } from '../src'; +import { Machine, interpret, createMachine, ActorRef } from '../src'; import { assign, send, @@ -8,9 +8,11 @@ import { sendUpdate, respond } from '../src/actions'; -import { Actor } from '../src/Actor'; +import { fromService } from '../src/Actor'; import { interval } from 'rxjs'; import { map } from 'rxjs/operators'; +import * as actionTypes from '../src/actionTypes'; +import { createMachineBehavior } from '../src/behavior'; describe('spawning machines', () => { const todoMachine = Machine({ @@ -27,7 +29,7 @@ describe('spawning machines', () => { }); const context = { - todoRefs: {} as Record + todoRefs: {} as Record> }; type TodoEvent = @@ -60,9 +62,9 @@ describe('spawning machines', () => { on: { ADD: { actions: assign({ - todoRefs: (ctx, e) => ({ + todoRefs: (ctx, e, { spawn }) => ({ ...ctx.todoRefs, - [e.id]: spawn(todoMachine) + [e.id]: spawn.from(todoMachine) }) }) }, @@ -101,7 +103,7 @@ describe('spawning machines', () => { }); interface ClientContext { - server?: Interpreter; + server?: ActorRef; } const clientMachine = Machine({ @@ -114,7 +116,7 @@ describe('spawning machines', () => { init: { entry: [ assign({ - server: () => spawn(serverMachine) + server: (_, __, { spawn }) => spawn.from(serverMachine) }), raise('SUCCESS') ], @@ -161,10 +163,6 @@ describe('spawning machines', () => { service.send({ type: 'SET_COMPLETE', id: 42 }); }); - it('should invoke a null actor if spawned outside of a service', () => { - expect(spawn(todoMachine)).toBeTruthy(); - }); - it('should allow bidirectional communication between parent/child actors', (done) => { interpret(clientMachine) .onDone(() => { @@ -184,13 +182,12 @@ describe('spawning promises', () => { states: { idle: { entry: assign({ - promiseRef: () => { - const ref = spawn( - new Promise((res) => { - res('response'); - }), - 'my-promise' - ); + promiseRef: (_, __, { spawn }) => { + const promise = new Promise((res) => { + res('response'); + }); + + const ref = spawn.from(promise, 'my-promise'); return ref; } @@ -218,40 +215,40 @@ describe('spawning promises', () => { }); describe('spawning callbacks', () => { - const callbackMachine = Machine({ - id: 'callback', - initial: 'idle', - context: { - callbackRef: undefined - }, - states: { - idle: { - entry: assign({ - callbackRef: () => - spawn((cb, receive) => { - receive((event) => { - if (event.type === 'START') { - setTimeout(() => { - cb('SEND_BACK'); - }, 10); - } - }); - }) - }), - on: { - START_CB: { - actions: send('START', { to: (ctx) => ctx.callbackRef }) - }, - SEND_BACK: 'success' - } + it('should be able to spawn an actor from a callback', (done) => { + const callbackMachine = createMachine<{ callbackRef?: ActorRef }>({ + id: 'callback', + initial: 'idle', + context: { + callbackRef: undefined }, - success: { - type: 'final' + states: { + idle: { + entry: assign({ + callbackRef: (_, __, { spawn }) => + spawn.from((cb, receive) => { + receive((event) => { + if (event.type === 'START') { + setTimeout(() => { + cb('SEND_BACK'); + }, 10); + } + }); + }) + }), + on: { + START_CB: { + actions: send('START', { to: (ctx) => ctx.callbackRef }) + }, + SEND_BACK: 'success' + } + }, + success: { + type: 'final' + } } - } - }); + }); - it('should be able to spawn an actor from a callback', (done) => { const callbackService = interpret(callbackMachine).onDone(() => { done(); }); @@ -262,47 +259,47 @@ describe('spawning callbacks', () => { }); describe('spawning observables', () => { - interface Events { - type: 'INT'; - value: number; - } + it('should be able to spawn an observable', (done) => { + interface Events { + type: 'INT'; + value: number; + } - const observableMachine = Machine({ - id: 'observable', - initial: 'idle', - context: { - observableRef: undefined - }, - states: { - idle: { - entry: assign({ - observableRef: () => { - const ref = spawn( - interval(10).pipe( - map((n) => ({ - type: 'INT', - value: n - })) - ) - ); + const observableMachine = Machine({ + id: 'observable', + initial: 'idle', + context: { + observableRef: undefined + }, + states: { + idle: { + entry: assign({ + observableRef: (_, __, { spawn }) => { + const ref = spawn.from( + interval(10).pipe( + map((n) => ({ + type: 'INT', + value: n + })) + ) + ); - return ref; - } - }), - on: { - INT: { - target: 'success', - cond: (_, e) => e.value === 5 + return ref; + } + }), + on: { + INT: { + target: 'success', + cond: (_, e) => e.value === 5 + } } + }, + success: { + type: 'final' } - }, - success: { - type: 'final' } - } - }); + }); - it('should be able to spawn an observable', (done) => { const observableService = interpret(observableMachine).onDone(() => { done(); }); @@ -327,17 +324,19 @@ describe('communicating with spawned actors', () => { const existingService = interpret(existingMachine).start(); - const parentMachine = Machine({ + const parentMachine = Machine<{ + existingRef?: ActorRef; + }>({ initial: 'pending', context: { - existingRef: undefined as any + existingRef: undefined }, states: { pending: { entry: assign({ // No need to spawn an existing service: // existingRef: () => spawn(existingService) - existingRef: existingService + existingRef: existingService.ref }), on: { 'EXISTING.DONE': 'success' @@ -376,8 +375,11 @@ describe('communicating with spawned actors', () => { const existingService = interpret(existingMachine).start(); - const parentMachine = Machine({ + const parentMachine = Machine<{ existingRef: ActorRef }>({ initial: 'pending', + context: { + existingRef: fromService(existingService) + }, states: { pending: { entry: send('ACTIVATE', { to: existingService.sessionId }), @@ -418,10 +420,10 @@ describe('actors', () => { states: { start: { entry: assign({ - refs: (ctx) => { + refs: (ctx, _, { spawn }) => { count++; const c = ctx.items.map((item) => - spawn(new Promise((res) => res(item))) + spawn.from(new Promise((res) => res(item))) ); return c; @@ -439,13 +441,13 @@ describe('actors', () => { }); it('should spawn null actors if not used within a service', () => { - const nullActorMachine = Machine<{ ref?: Actor }>({ + const nullActorMachine = Machine<{ ref?: ActorRef }>({ initial: 'foo', context: { ref: undefined }, states: { foo: { entry: assign({ - ref: () => spawn(Promise.resolve(42)) + ref: (_, __, { spawn }) => spawn.from(Promise.resolve(42)) }) } } @@ -485,8 +487,8 @@ describe('actors', () => { initial: 'initial', states: { initial: { - entry: assign(() => ({ - serverRef: spawn(pongActorMachine) + entry: assign((_, __, { spawn }) => ({ + serverRef: spawn.from(pongActorMachine) })), on: { PONG: { @@ -506,15 +508,22 @@ describe('actors', () => { it('should not forward events to a spawned actor when { autoForward: false }', () => { let pongCounter = 0; - const machine = Machine<{ counter: number; serverRef?: Actor }>({ + const machine = Machine<{ + counter: number; + serverRef?: ActorRef; + }>({ id: 'client', context: { counter: 0, serverRef: undefined }, initial: 'initial', states: { initial: { - entry: assign((ctx) => ({ + entry: assign((ctx, _, { self, spawn }) => ({ ...ctx, - serverRef: spawn(pongActorMachine, { autoForward: false }) + serverRef: spawn( + createMachineBehavior(pongActorMachine, self, { + autoForward: false + }) + ) })), on: { PONG: { @@ -530,33 +539,6 @@ describe('actors', () => { service.send('PING'); expect(pongCounter).toEqual(0); }); - - it('should forward events to a spawned actor when { autoForward: true }', () => { - let pongCounter = 0; - - const machine = Machine({ - id: 'client', - context: { counter: 0, serverRef: undefined }, - initial: 'initial', - states: { - initial: { - entry: assign(() => ({ - serverRef: spawn(pongActorMachine, { autoForward: true }) - })), - on: { - PONG: { - actions: () => ++pongCounter - } - } - } - } - }); - const service = interpret(machine); - service.start(); - service.send('PING'); - service.send('PING'); - expect(pongCounter).toEqual(2); - }); }); describe('sync option', () => { @@ -573,81 +555,113 @@ describe('actors', () => { } }); - const parentMachine = Machine<{ - ref: any; - refNoSync: any; - refNoSyncDefault: any; - }>({ - id: 'parent', - context: { - ref: undefined, - refNoSync: undefined, - refNoSyncDefault: undefined - }, - initial: 'foo', - states: { - foo: { - entry: assign({ - ref: () => spawn(childMachine, { sync: true }), - refNoSync: () => spawn(childMachine, { sync: false }), - refNoSyncDefault: () => spawn(childMachine) - }) + it('should sync spawned actor state when { sync: true }', (done) => { + const machine = Machine<{ + ref?: ActorRef; + }>({ + id: 'parent', + context: { + ref: undefined }, - success: { - type: 'final' + initial: 'foo', + states: { + foo: { + entry: assign({ + ref: (_, __, { self, spawn }) => + spawn(createMachineBehavior(childMachine, self, { sync: true })) + }), + on: { + [actionTypes.update]: 'success' + } + }, + success: { + type: 'final' + } } - } - }); + }); - it('should sync spawned actor state when { sync: true }', () => { - return new Promise((res) => { - const service = interpret(parentMachine, { - id: 'a-service' - }).onTransition((s) => { - if (s.context.ref.state.context.value === 42) { - res(); - } - }); - service.start(); + const service = interpret(machine, { + id: 'a-service' + }).onDone(() => { + done(); }); + service.start(); }); - it('should not sync spawned actor state when { sync: false }', () => { - return new Promise((res, rej) => { - const service = interpret(parentMachine, { - id: 'b-service' - }).onTransition((s) => { - if (s.context.refNoSync.state.context.value === 42) { - rej(new Error('value change caused transition')); + it('should not sync spawned actor state when { sync: false }', (done) => { + const machine = Machine<{ + refNoSync?: ActorRef; + }>({ + id: 'parent', + context: { + refNoSync: undefined + }, + initial: 'foo', + states: { + foo: { + entry: assign({ + refNoSync: (_, __, { self, spawn }) => + spawn( + createMachineBehavior(childMachine, self, { sync: false }) + ) + }), + on: { + '*': 'failure' + } + }, + failure: { + type: 'final' } - }); - service.start(); + } + }); - setTimeout(() => { - expect(service.state.context.refNoSync.state.context.value).toBe(42); - res(); - }, 30); + const service = interpret(machine, { + id: 'b-service' + }).onDone(() => { + throw new Error('value change caused transition'); }); + service.start(); + + setTimeout(() => { + done(); + }, 30); }); - it('should not sync spawned actor state (default)', () => { - return new Promise((res, rej) => { - const service = interpret(parentMachine, { - id: 'c-service' - }).onTransition((s) => { - if (s.context.refNoSyncDefault.state.context.value === 42) { - rej(new Error('value change caused transition')); + it('should not sync spawned actor state (default)', (done) => { + const machine = Machine<{ + refNoSyncDefault?: ActorRef; + }>({ + id: 'parent', + context: { + refNoSyncDefault: undefined + }, + initial: 'foo', + states: { + foo: { + entry: assign({ + refNoSyncDefault: (_, __, { self, spawn }) => + spawn(createMachineBehavior(childMachine, self)) + }), + on: { + '*': 'failure' + } + }, + failure: { + type: 'final' } - }); - service.start(); + } + }); - setTimeout(() => { - expect( - service.state.context.refNoSyncDefault.state.context.value - ).toBe(42); - res(); - }, 30); + const service = interpret(machine, { + id: 'b-service' + }).onDone(() => { + throw new Error('value change caused transition'); }); + service.start(); + + setTimeout(() => { + done(); + }, 30); }); it('parent state should be changed if synced child actor update occurs', (done) => { @@ -662,7 +676,7 @@ describe('actors', () => { }); interface SyncMachineContext { - ref?: Interpreter; + ref?: ActorRef; } const syncMachine = Machine({ @@ -671,21 +685,25 @@ describe('actors', () => { states: { same: { entry: assign({ - ref: () => spawn(syncChildMachine, { sync: true }) - }) + ref: (_, __, { self, spawn }) => { + return spawn( + createMachineBehavior(syncChildMachine, self, { sync: true }) + ); + } + }), + on: { + [actionTypes.update]: 'success' + } + }, + success: { + type: 'final' } } }); interpret(syncMachine) - .onTransition((state) => { - if ( - state.context.ref && - state.context.ref.state.matches('inactive') - ) { - expect(state.changed).toBe(true); - done(); - } + .onDone(() => { + done(); }) .start(); }); @@ -707,7 +725,7 @@ describe('actors', () => { }); interface SyncMachineContext { - ref?: Interpreter; + ref?: ActorRef; } const syncMachine = Machine({ @@ -716,27 +734,33 @@ describe('actors', () => { states: { same: { entry: assign({ - ref: () => spawn(syncChildMachine, falseSyncOption) - }) - } + ref: (_, __, { self, spawn }) => + spawn( + createMachineBehavior( + syncChildMachine, + self, + falseSyncOption + ) + ) + }), + on: { + '*': 'failure' + } + }, + failure: {} } }); - const service = interpret(syncMachine) + interpret(syncMachine) + .onDone(() => { + done(); + }) .onTransition((state) => { - if ( - state.context.ref && - state.context.ref.state.matches('inactive') - ) { - expect(state.changed).toBe(false); - } + expect(state.matches('failure')).toBeFalsy(); }) .start(); setTimeout(() => { - expect(service.state.context.ref!.state.matches('inactive')).toBe( - true - ); done(); }, 20); }); @@ -757,7 +781,7 @@ describe('actors', () => { }); interface SyncMachineContext { - ref?: Interpreter; + ref?: ActorRef; } const syncMachine = Machine({ @@ -766,7 +790,14 @@ describe('actors', () => { states: { same: { entry: assign({ - ref: () => spawn(syncChildMachine, falseSyncOption) + ref: (_, __, { self, spawn }) => + spawn( + createMachineBehavior( + syncChildMachine, + self, + falseSyncOption + ) + ) }) } } @@ -774,10 +805,7 @@ describe('actors', () => { interpret(syncMachine) .onTransition((state) => { - if ( - state.context.ref && - state.context.ref.state.matches('inactive') - ) { + if (state.event.type === actionTypes.update) { expect(state.changed).toBe(true); done(); } diff --git a/packages/core/test/assign.test.ts b/packages/core/test/assign.test.ts index a5e644ae70..73550f72e1 100644 --- a/packages/core/test/assign.test.ts +++ b/packages/core/test/assign.test.ts @@ -1,5 +1,6 @@ import { Machine, interpret, assign, send, sendParent, State } from '../src'; -import { spawnMachine } from '../src/invoke'; +import { invokeMachine } from '../src/invoke'; +import { ActorRef } from '../src/types'; interface CounterContext { count: number; @@ -297,7 +298,7 @@ describe('assign meta', () => { it('should provide meta._event to assigner', () => { interface Ctx { - eventLog: Array<{ event: string; origin: string | undefined }>; + eventLog: Array<{ event: string; origin?: ActorRef }>; } const assignEventLog = assign((ctx, event, meta) => ({ @@ -328,7 +329,7 @@ describe('assign meta', () => { foo: { invoke: { id: 'child', - src: spawnMachine(childMachine) + src: invokeMachine(childMachine) } } }, @@ -356,9 +357,19 @@ describe('assign meta', () => { expect(state.context).toEqual({ eventLog: [ { event: 'PING_CHILD', origin: undefined }, - { event: 'PONG', origin: expect.stringMatching(/.+/) }, + { + event: 'PONG', + origin: expect.objectContaining({ + id: expect.stringMatching(/.+/) + }) + }, { event: 'PING_CHILD', origin: undefined }, - { event: 'PONG', origin: expect.stringMatching(/.+/) } + { + event: 'PONG', + origin: expect.objectContaining({ + id: expect.stringMatching(/.+/) + }) + } ] }); }); diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index b3c378a4cb..d1d31f69ff 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -1,6 +1,6 @@ import { Machine, sendParent, interpret, assign } from '../src'; import { respond, send } from '../src/actions'; -import { spawnMachine } from '../src/invoke'; +import { invokeMachine } from '../src/invoke'; describe('SCXML events', () => { it('should have the origin (id) from the sending service', (done) => { @@ -19,13 +19,13 @@ describe('SCXML events', () => { active: { invoke: { id: 'child', - src: spawnMachine(childMachine) + src: invokeMachine(childMachine) }, on: { EVENT: { target: 'success', cond: (_, __, { _event }) => { - return !!(_event.origin && _event.origin.length > 0); + return !!_event.origin; } } } @@ -64,7 +64,7 @@ describe('SCXML events', () => { authorizing: { invoke: { id: 'auth-server', - src: spawnMachine(authServerMachine) + src: invokeMachine(authServerMachine) }, entry: send('CODE', { to: 'auth-server' }), on: { diff --git a/packages/core/test/interpreter.test.ts b/packages/core/test/interpreter.test.ts index f4f5a32dc3..b11bb481ae 100644 --- a/packages/core/test/interpreter.test.ts +++ b/packages/core/test/interpreter.test.ts @@ -1,4 +1,4 @@ -import { interpret, Interpreter, spawn } from '../src/interpreter'; +import { interpret, Interpreter } from '../src/interpreter'; import { SimulatedClock } from '../src/SimulatedClock'; import { machine as idMachine } from './fixtures/id'; import { @@ -18,10 +18,10 @@ import { isObservable } from '../src/utils'; import { interval, from } from 'rxjs'; import { map } from 'rxjs/operators'; import { - spawnObservable, - spawnMachine, - spawnPromise, - spawnActivity + invokeObservable, + invokeMachine, + invokePromise, + invokeActivity } from '../src/invoke'; const lightMachine = Machine({ @@ -90,9 +90,9 @@ describe('interpreter', () => { states: { idle: { entry: assign({ - actor: () => { + actor: (_, __, { spawn }) => { entryCalled++; - return spawn( + return spawn.from( new Promise(() => { promiseSpawned++; }) @@ -122,6 +122,56 @@ describe('interpreter', () => { done(); }, 100); }); + + // https://github.com/davidkpiano/xstate/issues/1174 + it('executes actions from a restored state', (done) => { + const lightMachine = Machine( + { + id: 'light', + initial: 'green', + states: { + green: { + on: { + TIMER: { + target: 'yellow', + actions: 'report' + } + } + }, + yellow: { + on: { + TIMER: { + target: 'red' + } + } + }, + red: { + on: { + TIMER: 'green' + } + } + } + }, + { + actions: { + report: () => { + done(); + } + } + } + ); + + const currentState = 'green'; + const nextState = lightMachine.transition(currentState, 'TIMER'); + + // saves state and recreate it + const recreated = JSON.parse(JSON.stringify(nextState)); + const previousState = State.create(recreated); + const resolvedState = lightMachine.resolveState(previousState); + + const service = interpret(lightMachine); + service.start(resolvedState); + }); }); describe('subscribing', () => { @@ -468,8 +518,8 @@ describe('interpreter', () => { } }, { - services: { - myActivity: spawnActivity(() => { + behaviors: { + myActivity: invokeActivity(() => { activityState = 'on'; return () => (activityState = 'off'); }) @@ -515,8 +565,8 @@ describe('interpreter', () => { } }, { - services: { - myActivity: spawnActivity(() => { + behaviors: { + myActivity: invokeActivity(() => { stopActivityState = 'on'; return () => (stopActivityState = 'off'); }) @@ -556,8 +606,8 @@ describe('interpreter', () => { } }, { - services: { - blink: spawnActivity(() => { + behaviors: { + blink: invokeActivity(() => { activityActive = true; return () => { @@ -573,18 +623,10 @@ describe('interpreter', () => { 'TOGGLE' ); const bState = toggleMachine.transition(activeState, 'SWITCH'); - let state: any; - interpret(toggleMachine) - .onTransition((s) => { - state = s; - }) - .start(bState); + interpret(toggleMachine).start(bState); setTimeout(() => { - expect( - state.children.find((child) => child.meta!.src === 'blink') - ).toBeTruthy(); expect(activityActive).toBeFalsy(); done(); }, 10); @@ -821,7 +863,7 @@ describe('interpreter', () => { foo: { invoke: { id: 'child', - src: spawnMachine(childMachine) + src: invokeMachine(childMachine) } } }, @@ -845,9 +887,15 @@ describe('interpreter', () => { expect(logs.length).toBe(4); expect(logs).toEqual([ { event: 'PING_CHILD', origin: undefined }, - { event: 'PONG', origin: expect.stringMatching(/.+/) }, + { + event: 'PONG', + origin: expect.objectContaining({ id: expect.stringMatching(/.*/) }) + }, { event: 'PING_CHILD', origin: undefined }, - { event: 'PONG', origin: expect.stringMatching(/.+/) } + { + event: 'PONG', + origin: expect.objectContaining({ id: expect.stringMatching(/.*/) }) + } ]); }); @@ -982,7 +1030,7 @@ describe('interpreter', () => { start: { invoke: { id: 'child', - src: spawnMachine(childMachine), + src: invokeMachine(childMachine), data: { password: 'foo' } }, on: { @@ -1002,9 +1050,7 @@ describe('interpreter', () => { interpret(parentMachine) .onTransition((state) => { if (state.matches('start')) { - const childActor = state.children.find( - (child) => child.id === 'child' - ); + const childActor = state.children.child; expect(typeof childActor!.send).toBe('function'); } @@ -1740,8 +1786,8 @@ describe('interpreter', () => { } }, { - services: { - testService: spawnActivity(() => { + behaviors: { + testService: invokeActivity(() => { // nothing }) } @@ -1774,7 +1820,7 @@ describe('interpreter', () => { active: { invoke: { id: 'childActor', - src: spawnMachine(childMachine) + src: invokeMachine(childMachine) }, on: { FIRED: 'success' @@ -1788,9 +1834,7 @@ describe('interpreter', () => { const service = interpret(parentMachine) .onTransition((state) => { - const childActor = state.children.find( - (child) => child.id === 'childActor' - ); + const childActor = state.children.childActor; if (state.matches('active') && childActor) { childActor.send({ type: 'FIRE' }); @@ -1811,7 +1855,7 @@ describe('interpreter', () => { active: { invoke: { id: 'childActor', - src: spawnPromise( + src: invokePromise( () => new Promise((res) => { setTimeout(() => { @@ -1819,36 +1863,38 @@ describe('interpreter', () => { }, 100); }) ), - onDone: { - target: 'success', - cond: (_, e) => e.data === 42 - } + onDone: [ + { + target: 'success', + cond: (_, e) => { + return e.data === 42; + } + }, + { target: 'failure' } + ] } }, success: { type: 'final' + }, + failure: { + type: 'final' } } }); - const subscriber = (data) => { - expect(data).toEqual(42); - done(); - }; - let subscription; - const service = interpret(parentMachine) .onTransition((state) => { - const childActor = state.children.find( - (child) => child.id === 'childActor' - ); + if (state.matches('active')) { + const childActor = state.children.childActor; - if (childActor && !subscription) { - subscription = childActor.subscribe(subscriber); + expect(childActor).toHaveProperty('send'); } }) .onDone(() => { + expect(service.state.matches('success')).toBeTruthy(); expect(service.state.children).not.toHaveProperty('childActor'); + done(); }); service.start(); @@ -1863,7 +1909,7 @@ describe('interpreter', () => { active: { invoke: { id: 'childActor', - src: spawnObservable(() => + src: invokeObservable(() => interval$.pipe(map((value) => ({ type: 'FIRED', value }))) ) }, @@ -1882,25 +1928,15 @@ describe('interpreter', () => { } }); - const subscriber = (data) => { - if (data.value === 3) { - done(); - } - }; - let subscription; - const service = interpret(parentMachine) .onTransition((state) => { - const childActor = state.children.find( - (child) => child.id === 'childActor' - ); - - if (state.matches('active') && childActor && !subscription) { - subscription = childActor.subscribe(subscriber); + if (state.matches('active')) { + expect(state.children['childActor']).not.toBeUndefined(); } }) .onDone(() => { expect(service.state.children).not.toHaveProperty('childActor'); + done(); }); service.start(); diff --git a/packages/core/test/invoke.test.ts b/packages/core/test/invoke.test.ts index c0169e4949..cd319ce836 100644 --- a/packages/core/test/invoke.test.ts +++ b/packages/core/test/invoke.test.ts @@ -5,20 +5,23 @@ import { sendParent, send, EventObject, - StateValue + StateValue, + UpdateObject, + createMachine } from '../src'; import { actionTypes, done as _done, doneInvoke, - escalate + escalate, + raise } from '../src/actions'; import { - spawnMachine, - spawnCallback, - spawnPromise, - spawnObservable, - spawnActivity + invokeMachine, + invokeCallback, + invokePromise, + invokeObservable, + invokeActivity } from '../src/invoke'; import { interval } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -67,7 +70,7 @@ const fetcherMachine = Machine({ }, waiting: { invoke: { - src: spawnMachine(fetchMachine), + src: invokeMachine(fetchMachine), data: { userId: (ctx) => ctx.selectedUserId }, @@ -82,7 +85,7 @@ const fetcherMachine = Machine({ }, waitingInvokeMachine: { invoke: { - src: spawnMachine(fetchMachine.withContext({ userId: '55' })), + src: invokeMachine(fetchMachine.withContext({ userId: '55' })), onDone: 'received' } }, @@ -106,7 +109,7 @@ const intervalMachine = Machine<{ counting: { invoke: { id: 'intervalService', - src: spawnCallback((ctx) => (cb) => { + src: invokeCallback((ctx) => (cb) => { const ivl = setInterval(() => { cb('INC'); }, ctx.interval); @@ -178,8 +181,8 @@ describe('invoke', () => { } }, { - services: { - child: spawnMachine(childMachine) + behaviors: { + child: invokeMachine(childMachine) } } ); @@ -247,8 +250,8 @@ describe('invoke', () => { } }, { - services: { - child: spawnMachine(childMachine) + behaviors: { + child: invokeMachine(childMachine) } } ); @@ -324,7 +327,7 @@ describe('invoke', () => { }, invokeChild: { invoke: { - src: spawnMachine(childMachine), + src: invokeMachine(childMachine), autoForward: true, onDone: { target: 'done', @@ -425,7 +428,7 @@ describe('invoke', () => { }, invokeChild: { invoke: { - src: spawnMachine(childMachine), + src: invokeMachine(childMachine), autoForward: true, onDone: { target: 'done', @@ -499,7 +502,7 @@ describe('invoke', () => { initial: 'pending', states: { pending: { - invoke: spawnMachine( + invoke: invokeMachine( Machine({ id: 'child', initial: 'sending', @@ -542,7 +545,7 @@ describe('invoke', () => { initial: 'b', states: { b: { - invoke: spawnMachine( + invoke: invokeMachine( Machine({ id: 'child', initial: 'sending', @@ -607,16 +610,16 @@ describe('invoke', () => { } }, { - services: { - child: spawnMachine(childMachine) + behaviors: { + child: invokeMachine(childMachine) } } ); interpret( someParentMachine.withConfig({ - services: { - child: spawnMachine( + behaviors: { + child: invokeMachine( Machine({ id: 'child', initial: 'init', @@ -646,7 +649,7 @@ describe('invoke', () => { states: { active: { invoke: { - src: spawnActivity(() => { + src: invokeActivity(() => { startCount++; }) } @@ -681,7 +684,7 @@ describe('invoke', () => { initial: 'one', invoke: { id: 'foo-child', - src: spawnMachine(subMachine) + src: invokeMachine(subMachine) }, states: { one: { @@ -714,7 +717,7 @@ describe('invoke', () => { }, invoke: { id: 'foo-child', - src: spawnMachine((ctx) => ctx.machine) + src: invokeMachine((ctx) => ctx.machine) }, states: { one: { @@ -742,7 +745,7 @@ describe('invoke', () => { one: { invoke: { id: 'foo-child', - src: spawnMachine(subMachine) + src: invokeMachine(subMachine) }, entry: send('NEXT', { to: 'foo-child' }), on: { NEXT: 'two' } @@ -781,7 +784,7 @@ describe('invoke', () => { one: { invoke: { id: 'foo-child', - src: spawnMachine(doneSubMachine), + src: invokeMachine(doneSubMachine), onDone: 'two' }, entry: send('NEXT', { to: 'foo-child' }) @@ -828,7 +831,7 @@ describe('invoke', () => { active: { invoke: { id: 'pong', - src: spawnMachine(pongMachine), + src: invokeMachine(pongMachine), onDone: { target: 'success', cond: (_, e) => e.data.secret === 'pingpong' @@ -891,7 +894,7 @@ describe('invoke', () => { states: { pending: { invoke: { - src: spawnPromise((ctx) => + src: invokePromise((ctx) => createPromise((resolve) => { if (ctx.succeed) { resolve(ctx.id); @@ -933,17 +936,14 @@ describe('invoke', () => { .start(); }); - it('should be invoked with a promise factory and ignore unhandled onError target', (done) => { - const doneSpy = jest.fn(); - const stopSpy = jest.fn(); - + it('should be invoked with a promise factory and surface any unhandled errors', (done) => { const promiseMachine = Machine({ id: 'invokePromise', initial: 'pending', states: { pending: { invoke: { - src: spawnPromise(() => + src: invokePromise(() => createPromise(() => { throw new Error('test'); }) @@ -957,14 +957,11 @@ describe('invoke', () => { } }); - interpret(promiseMachine).onDone(doneSpy).onStop(stopSpy).start(); - - // assumes that error was ignored before the timeout is processed - setTimeout(() => { - expect(doneSpy).not.toHaveBeenCalled(); - expect(stopSpy).not.toHaveBeenCalled(); + const service = interpret(promiseMachine).onError((err) => { + expect(err.message).toEqual(expect.stringMatching(/test/)); done(); - }, 10); + }); + service.start(); }); it.skip( @@ -980,7 +977,7 @@ describe('invoke', () => { states: { pending: { invoke: { - src: spawnPromise(() => + src: invokePromise(() => createPromise(() => { throw new Error('test'); }) @@ -1014,7 +1011,7 @@ describe('invoke', () => { states: { pending: { invoke: { - src: spawnPromise(() => + src: invokePromise(() => createPromise((resolve) => resolve()) ), onDone: 'success' @@ -1064,8 +1061,8 @@ describe('invoke', () => { } }, { - services: { - somePromise: spawnPromise(() => + behaviors: { + somePromise: invokePromise(() => createPromise((resolve) => resolve()) ) } @@ -1085,7 +1082,7 @@ describe('invoke', () => { states: { pending: { invoke: { - src: spawnPromise(() => + src: invokePromise(() => createPromise((resolve) => resolve({ count: 1 })) ), onDone: { @@ -1134,8 +1131,8 @@ describe('invoke', () => { } }, { - services: { - somePromise: spawnPromise(() => + behaviors: { + somePromise: invokePromise(() => createPromise((resolve) => resolve({ count: 1 })) ) } @@ -1164,7 +1161,7 @@ describe('invoke', () => { states: { pending: { invoke: { - src: spawnPromise(() => + src: invokePromise(() => createPromise((resolve) => resolve({ count: 1 })) ), onDone: { @@ -1214,8 +1211,8 @@ describe('invoke', () => { } }, { - services: { - somePromise: spawnPromise(() => + behaviors: { + somePromise: invokePromise(() => createPromise((resolve) => resolve({ count: 1 })) ) } @@ -1260,8 +1257,8 @@ describe('invoke', () => { } }, { - services: { - somePromise: spawnPromise((ctx, e: BeginEvent) => { + behaviors: { + somePromise: invokePromise((ctx, e: BeginEvent) => { return createPromise((resolve, reject) => { ctx.foo && e.payload ? resolve() : reject(); }); @@ -1326,8 +1323,8 @@ describe('invoke', () => { } }, { - services: { - someCallback: spawnCallback( + behaviors: { + someCallback: invokeCallback( (ctx, e: BeginEvent) => (cb: (ev: CallbackEvent) => void) => { if (ctx.foo && e.payload) { cb({ @@ -1383,8 +1380,8 @@ describe('invoke', () => { } }, { - services: { - someCallback: spawnCallback(() => (cb) => { + behaviors: { + someCallback: invokeCallback(() => (cb) => { cb('CALLBACK'); }) } @@ -1424,8 +1421,8 @@ describe('invoke', () => { } }, { - services: { - someCallback: spawnCallback(() => (cb) => { + behaviors: { + someCallback: invokeCallback(() => (cb) => { cb('CALLBACK'); }) } @@ -1472,8 +1469,8 @@ describe('invoke', () => { } }, { - services: { - someCallback: spawnCallback(() => (cb) => { + behaviors: { + someCallback: invokeCallback(() => (cb) => { cb('CALLBACK'); }) } @@ -1526,7 +1523,7 @@ describe('invoke', () => { active: { invoke: { id: 'child', - src: spawnCallback(() => (callback, onReceive) => { + src: invokeCallback(() => (callback, onReceive) => { onReceive((e) => { if (e.type === 'PING') { callback('PONG'); @@ -1557,7 +1554,7 @@ describe('invoke', () => { states: { safe: { invoke: { - src: spawnActivity(() => { + src: invokeActivity(() => { throw new Error('test'); }), onError: { @@ -1586,7 +1583,7 @@ describe('invoke', () => { states: { safe: { invoke: { - src: spawnActivity(() => { + src: invokeActivity(() => { throw new Error('test'); }), onError: 'failed' @@ -1613,7 +1610,7 @@ describe('invoke', () => { states: { safe: { invoke: { - src: spawnCallback(() => async () => { + src: invokeCallback(() => async () => { await true; throw new Error('test'); }), @@ -1646,7 +1643,7 @@ describe('invoke', () => { states: { fetch: { invoke: { - src: spawnCallback(() => async () => { + src: invokeCallback(() => async () => { await true; return 42; }), @@ -1689,7 +1686,7 @@ describe('invoke', () => { states: { first: { invoke: { - src: spawnActivity(() => { + src: invokeActivity(() => { throw new Error('test'); }), onError: { @@ -1703,7 +1700,7 @@ describe('invoke', () => { }, second: { invoke: { - src: spawnActivity(() => { + src: invokeActivity(() => { // empty }), onError: { @@ -1738,7 +1735,7 @@ describe('invoke', () => { JSON.stringify(waitingState); }).not.toThrow(); - expect(typeof waitingState.actions[0].actor!.src).toBe('string'); + expect(typeof waitingState.actions[0].src).toBe('string'); }); it('should throw error if unhandled (sync)', () => { @@ -1748,7 +1745,7 @@ describe('invoke', () => { states: { safe: { invoke: { - src: spawnCallback(() => { + src: invokeCallback(() => { throw new Error('test'); }) } @@ -1763,68 +1760,6 @@ describe('invoke', () => { expect(() => service.start()).toThrow(); }); - it.skip('should stop machine if unhandled error and on strict mode (async)', (done) => { - const errorMachine = Machine({ - id: 'asyncError', - initial: 'safe', - // if not in strict mode we have no way to know if there - // was an error with processing rejected promise - strict: true, - states: { - safe: { - invoke: { - src: spawnCallback(() => async () => { - await true; - throw new Error('test'); - }) - } - }, - failed: { - type: 'final' - } - } - }); - - interpret(errorMachine) - .onStop(() => done()) - .start(); - }); - - it('should ignore error if unhandled error and not on strict mode (async)', (done) => { - const doneSpy = jest.fn(); - const stopSpy = jest.fn(); - - const errorMachine = Machine({ - id: 'asyncError', - initial: 'safe', - // if not in strict mode we have no way to know if there - // was an error with processing rejected promise - strict: false, - states: { - safe: { - invoke: { - src: spawnCallback(() => async () => { - await true; - throw new Error('test'); - }) - } - }, - failed: { - type: 'final' - } - } - }); - - interpret(errorMachine).onDone(doneSpy).onStop(stopSpy).start(); - - // assumes that error was ignored before the timeout is processed - setTimeout(() => { - expect(doneSpy).not.toHaveBeenCalled(); - expect(stopSpy).not.toHaveBeenCalled(); - done(); - }, 20); - }); - describe('sub invoke race condition', () => { const anotherChildMachine = Machine({ id: 'child', @@ -1845,7 +1780,7 @@ describe('invoke', () => { states: { begin: { invoke: { - src: spawnMachine(anotherChildMachine), + src: invokeMachine(anotherChildMachine), id: 'invoked.child', onDone: 'completed' }, @@ -1902,7 +1837,7 @@ describe('invoke', () => { states: { counting: { invoke: { - src: spawnObservable(() => + src: invokeObservable(() => infinite$.pipe( map((value) => { return { type: 'COUNT', value }; @@ -1949,7 +1884,7 @@ describe('invoke', () => { states: { counting: { invoke: { - src: spawnObservable(() => + src: invokeObservable(() => infinite$.pipe( take(5), map((value) => { @@ -2001,7 +1936,7 @@ describe('invoke', () => { states: { counting: { invoke: { - src: spawnObservable(() => + src: invokeObservable(() => infinite$.pipe( map((value) => { if (value === 5) { @@ -2065,7 +2000,7 @@ describe('invoke', () => { active: { invoke: { id: 'pong', - src: spawnMachine(pongMachine) + src: invokeMachine(pongMachine) }, // Sends 'PING' event to child machine with ID 'pong' entry: send('PING', { to: 'pong' }), @@ -2098,12 +2033,12 @@ describe('invoke', () => { } }); - const machine = Machine({ + const machine = Machine({ initial: 'pending', states: { pending: { invoke: { - src: spawnMachine(childMachine, { sync: true }) + src: invokeMachine(childMachine, { sync: true }) } }, success: { type: 'final' } @@ -2152,11 +2087,11 @@ describe('invoke', () => { invoke: [ { id: 'child', - src: spawnCallback(() => (cb) => cb('ONE')) + src: invokeCallback(() => (cb) => cb('ONE')) }, { id: 'child2', - src: spawnCallback(() => (cb) => cb('TWO')) + src: invokeCallback(() => (cb) => cb('TWO')) } ] } @@ -2218,13 +2153,13 @@ describe('invoke', () => { a: { invoke: { id: 'child', - src: spawnCallback(() => (cb) => cb('ONE')) + src: invokeCallback(() => (cb) => cb('ONE')) } }, b: { invoke: { id: 'child2', - src: spawnCallback(() => (cb) => cb('TWO')) + src: invokeCallback(() => (cb) => cb('TWO')) } } } @@ -2251,8 +2186,7 @@ describe('invoke', () => { service.start(); }); - // TODO: unskip once onMicrostep behavior is established - it.skip('should not invoke a service if transient', (done) => { + it('should not invoke a service if it gets stopped immediately by transitioning away in microstep', (done) => { // Since an invocation will be canceled when the state machine leaves the // invoking state, it does not make sense to start an invocation in a state // that will be exited immediately @@ -2264,7 +2198,7 @@ describe('invoke', () => { active: { invoke: { id: 'doNotInvoke', - src: spawnCallback(() => async () => { + src: invokeCallback(() => async () => { serviceCalled = true; }) }, @@ -2290,6 +2224,65 @@ describe('invoke', () => { }) .start(); }); + + it('should invoke a service if other service gets stopped in subsequent microstep (#1180)', (done) => { + const machine = createMachine({ + initial: 'running', + states: { + running: { + type: 'parallel', + states: { + one: { + initial: 'active', + on: { + STOP_ONE: '.idle' + }, + states: { + idle: {}, + active: { + invoke: { + id: 'active', + src: invokeCallback(() => () => {}) + }, + on: { + NEXT: { + actions: raise('STOP_ONE') + } + } + } + } + }, + two: { + initial: 'idle', + on: { + NEXT: '.active' + }, + states: { + idle: {}, + active: { + invoke: { + id: 'post', + src: invokePromise(() => Promise.resolve(42)), + onDone: '#done' + } + } + } + } + } + }, + done: { + id: 'done', + type: 'final' + } + } + }); + + const service = interpret(machine) + .onDone(() => done()) + .start(); + + service.send('NEXT'); + }); }); describe('error handling', () => { @@ -2311,7 +2304,7 @@ describe('invoke', () => { one: { invoke: { id: 'child', - src: spawnMachine(child), + src: invokeMachine(child), onError: { target: 'two', cond: (_, event) => event.data === 'oops' @@ -2353,7 +2346,7 @@ describe('invoke', () => { one: { invoke: { id: 'child', - src: spawnMachine(child), + src: invokeMachine(child), onError: { target: 'two', cond: (_, event) => { diff --git a/packages/core/test/scxml.test.ts b/packages/core/test/scxml.test.ts index 176d5930ca..38c334d123 100644 --- a/packages/core/test/scxml.test.ts +++ b/packages/core/test/scxml.test.ts @@ -172,7 +172,7 @@ const testGroups = { 'test200.txml', 'test201.txml', 'test205.txml', - 'test207.txml', + // 'test207.txml', // delayexpr 'test208.txml', // 'test210.txml', // sendidexpr not supported yet // 'test215.txml', // diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index 1a7fc289cc..b6828cd9bb 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -24,7 +24,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -55,7 +55,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -77,7 +77,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -114,7 +114,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -136,7 +136,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -178,7 +178,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -208,7 +208,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -229,7 +229,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -265,7 +265,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -286,7 +286,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -324,7 +324,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -355,7 +355,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "id": "foo", }, @@ -393,7 +393,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -412,7 +412,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -452,7 +452,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -471,7 +471,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -517,7 +517,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -536,7 +536,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -575,7 +575,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -594,7 +594,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -634,7 +634,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -653,7 +653,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -699,7 +699,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -718,7 +718,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -757,7 +757,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -776,7 +776,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -816,7 +816,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -835,7 +835,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -882,7 +882,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -901,7 +901,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -937,7 +937,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -956,7 +956,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -998,7 +998,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1017,7 +1017,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1052,7 +1052,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1080,7 +1080,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1114,7 +1114,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1133,7 +1133,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1168,7 +1168,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1187,7 +1187,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1225,7 +1225,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1244,7 +1244,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1288,7 +1288,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1307,7 +1307,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1345,7 +1345,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1375,7 +1375,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1412,7 +1412,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1441,7 +1441,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1471,7 +1471,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1507,7 +1507,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1526,7 +1526,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1564,7 +1564,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1594,7 +1594,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1628,7 +1628,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1663,7 +1663,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1699,7 +1699,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1718,7 +1718,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1756,7 +1756,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1783,7 +1783,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1813,7 +1813,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1850,7 +1850,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1879,7 +1879,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1914,7 +1914,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1942,7 +1942,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -1970,7 +1970,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2004,7 +2004,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2023,7 +2023,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2061,7 +2061,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2091,7 +2091,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2120,7 +2120,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2155,7 +2155,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2174,7 +2174,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2212,7 +2212,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2242,7 +2242,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2279,7 +2279,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2308,7 +2308,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2332,7 +2332,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2375,7 +2375,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2394,7 +2394,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2432,7 +2432,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2462,7 +2462,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2499,7 +2499,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2531,7 +2531,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2560,7 +2560,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2579,7 +2579,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2622,7 +2622,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2641,7 +2641,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2679,7 +2679,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2706,7 +2706,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2725,7 +2725,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2766,7 +2766,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2785,7 +2785,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2820,7 +2820,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2839,7 +2839,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2875,7 +2875,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2911,7 +2911,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2930,7 +2930,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2968,7 +2968,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -2998,7 +2998,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3035,7 +3035,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3064,7 +3064,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3094,7 +3094,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3130,7 +3130,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3149,7 +3149,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3187,7 +3187,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3217,7 +3217,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3251,7 +3251,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3286,7 +3286,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3322,7 +3322,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3341,7 +3341,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3379,7 +3379,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3406,7 +3406,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3436,7 +3436,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3473,7 +3473,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3505,7 +3505,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3543,7 +3543,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3574,7 +3574,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3593,7 +3593,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3633,7 +3633,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3652,7 +3652,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3698,7 +3698,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3732,7 +3732,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3751,7 +3751,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3790,7 +3790,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3809,7 +3809,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3854,7 +3854,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3873,7 +3873,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3912,7 +3912,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3931,7 +3931,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3971,7 +3971,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -3990,7 +3990,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4037,7 +4037,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4066,7 +4066,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4101,7 +4101,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4129,7 +4129,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4162,7 +4162,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4190,7 +4190,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4209,7 +4209,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4243,7 +4243,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4283,7 +4283,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "count": 0, }, @@ -4325,7 +4325,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 1, }, @@ -4367,7 +4367,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 2, }, @@ -4405,7 +4405,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 3, }, @@ -4435,7 +4435,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 3, }, @@ -4480,7 +4480,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 3, }, @@ -4510,7 +4510,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 3, }, @@ -4549,7 +4549,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "count": 0, }, @@ -4580,7 +4580,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "count": 0, }, @@ -4618,7 +4618,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "count": 0, }, @@ -4656,7 +4656,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 1, }, @@ -4695,7 +4695,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 1, }, @@ -4733,7 +4733,7 @@ Object { "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": Object { "count": 0, }, @@ -4775,7 +4775,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 1, }, @@ -4813,7 +4813,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 2, }, @@ -4852,7 +4852,7 @@ Object { }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": Object { "count": 2, }, @@ -4890,7 +4890,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4919,7 +4919,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4954,7 +4954,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -4982,7 +4982,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5010,7 +5010,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5044,7 +5044,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5063,7 +5063,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5101,7 +5101,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5128,7 +5128,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5158,7 +5158,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5194,7 +5194,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5213,7 +5213,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5251,7 +5251,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5281,7 +5281,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5310,7 +5310,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5345,7 +5345,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5364,7 +5364,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5402,7 +5402,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5432,7 +5432,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5469,7 +5469,7 @@ Array [ }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5498,7 +5498,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5522,7 +5522,7 @@ Array [ }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5565,7 +5565,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5584,7 +5584,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5622,7 +5622,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5652,7 +5652,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5689,7 +5689,7 @@ Array [ }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5721,7 +5721,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5750,7 +5750,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5769,7 +5769,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5812,7 +5812,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5831,7 +5831,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5869,7 +5869,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5896,7 +5896,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5915,7 +5915,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5956,7 +5956,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -5975,7 +5975,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6010,7 +6010,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6029,7 +6029,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": undefined, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6065,7 +6065,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6101,7 +6101,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6120,7 +6120,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6158,7 +6158,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6188,7 +6188,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6222,7 +6222,7 @@ Array [ }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6257,7 +6257,7 @@ Array [ }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6293,7 +6293,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6312,7 +6312,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6350,7 +6350,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6380,7 +6380,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6417,7 +6417,7 @@ Array [ }, ], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6446,7 +6446,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { @@ -6476,7 +6476,7 @@ Array [ "_sessionid": null, "actions": Array [], "changed": true, - "children": Array [], + "children": Object {}, "context": undefined, "done": false, "event": Object { diff --git a/packages/xstate-react/src/index.ts b/packages/xstate-react/src/index.ts index 5807efad6b..44ed3e65ad 100644 --- a/packages/xstate-react/src/index.ts +++ b/packages/xstate-react/src/index.ts @@ -12,6 +12,7 @@ import { } from 'xstate'; import { useSubscription, Subscription } from 'use-subscription'; import useConstant from './useConstant'; +import { ActorRef } from 'xstate'; interface UseMachineOptions { /** @@ -106,7 +107,7 @@ export function useMachine< }, [actions]); useEffect(() => { - Object.assign(service.machine.options.services, services); + Object.assign(service.machine.options.behaviors, services); }, [services]); return [state, service.send, service]; @@ -147,3 +148,19 @@ export function useService< return [state, service.send, service]; } + +export function useActor( + actorRef: ActorRef +): [TEmitted, (event: TEvent) => void] { + const [state, setState] = useState(actorRef.current); + + useEffect(() => { + const sub = actorRef.subscribe((nextState) => { + setState(nextState); + }); + + return () => sub.unsubscribe(); + }, [actorRef]); + + return [state, actorRef.send]; +} diff --git a/packages/xstate-react/test/types.test.tsx b/packages/xstate-react/test/types.test.tsx index f492dd4b27..010c59a968 100644 --- a/packages/xstate-react/test/types.test.tsx +++ b/packages/xstate-react/test/types.test.tsx @@ -1,14 +1,8 @@ import * as React from 'react'; import { render } from '@testing-library/react'; -import { - Machine, - interpret, - assign, - Interpreter, - spawn, - createMachine -} from 'xstate'; -import { useService, useMachine } from '../src'; +import { Machine, interpret, assign, createMachine, State } from 'xstate'; +import { useService, useMachine, useActor } from '../src'; +import { ActorRef } from 'xstate'; describe('useService', () => { it('should accept spawned machine', () => { @@ -16,7 +10,7 @@ describe('useService', () => { completed: boolean; } interface TodosCtx { - todos: Array>; + todos: Array>>; } const todoMachine = Machine({ @@ -42,9 +36,9 @@ describe('useService', () => { states: { working: {} }, on: { CREATE: { - actions: assign((ctx) => ({ + actions: assign((ctx, _, { spawn }) => ({ ...ctx, - todos: ctx.todos.concat(spawn(todoMachine)) + todos: [...ctx.todos, spawn.from(todoMachine)] })) } } @@ -55,7 +49,7 @@ describe('useService', () => { const Todo = ({ index }: { index: number }) => { const [current] = useService(service); const todoRef = current.context.todos[index]; - const [todoCurrent] = useService(todoRef); + const [todoCurrent] = useActor(todoRef); return <>{todoCurrent.context.completed}; }; diff --git a/packages/xstate-react/test/useMachine.test.tsx b/packages/xstate-react/test/useMachine.test.tsx index 121859ddb7..d20554b6f0 100644 --- a/packages/xstate-react/test/useMachine.test.tsx +++ b/packages/xstate-react/test/useMachine.test.tsx @@ -4,7 +4,6 @@ import { Machine, assign, Interpreter, - spawn, doneInvoke, State, createMachine @@ -16,7 +15,7 @@ import { waitForElement } from '@testing-library/react'; import { useState } from 'react'; -import { spawnPromise } from 'xstate/invoke'; +import { invokePromise } from 'xstate/invoke'; afterEach(cleanup); @@ -64,7 +63,7 @@ describe('useMachine hook', () => { }) => { const [current, send] = useMachine(fetchMachine, { services: { - fetchData: spawnPromise(onFetch) + fetchData: invokePromise(onFetch) }, state: persistedState }); @@ -190,7 +189,8 @@ describe('useMachine hook', () => { states: { start: { entry: assign({ - ref: () => spawn(new Promise((res) => res(42)), 'my-promise') + ref: (_, __, { spawn }) => + spawn.from(new Promise((res) => res(42)), 'my-promise') }), on: { [doneInvoke('my-promise')]: 'success' diff --git a/packages/xstate-viz/package.json b/packages/xstate-viz/package.json index f1211334f3..94ad8fe413 100644 --- a/packages/xstate-viz/package.json +++ b/packages/xstate-viz/package.json @@ -28,6 +28,7 @@ "react-dom": "^16.12.0" }, "dependencies": { + "@xstate/react": "^1.0.0-rc.3", "immer": "^6.0.3", "styled-components": "^4.4.1" } diff --git a/packages/xstate-vue/src/index.ts b/packages/xstate-vue/src/index.ts index d83ea04ca7..ef6a163f92 100644 --- a/packages/xstate-vue/src/index.ts +++ b/packages/xstate-vue/src/index.ts @@ -44,7 +44,7 @@ export function useMachine( guards, actions, activities, - services, + behaviors, delays, state: rehydratedState, ...interpreterOptions @@ -55,7 +55,7 @@ export function useMachine( guards, actions, activities, - services, + behaviors, delays }; diff --git a/packages/xstate-vue/src/xstate-machine.ts b/packages/xstate-vue/src/xstate-machine.ts index 75c6c055b9..9b293d055c 100644 --- a/packages/xstate-vue/src/xstate-machine.ts +++ b/packages/xstate-vue/src/xstate-machine.ts @@ -36,8 +36,8 @@ export default Vue.extend({ actions() { return this.options.actions; }, - services() { - return this.options.services; + behaviors() { + return this.options.behaviors; } }, created() { @@ -46,7 +46,7 @@ export default Vue.extend({ guards, actions, activities, - services, + behaviors, delays, state: rehydratedState, ...interpreterOptions @@ -57,7 +57,7 @@ export default Vue.extend({ guards, actions, activities, - services, + behaviors, delays }; diff --git a/packages/xstate-vue/test/UseMachine.vue b/packages/xstate-vue/test/UseMachine.vue index 4bd06d6b33..9415a27645 100644 --- a/packages/xstate-vue/test/UseMachine.vue +++ b/packages/xstate-vue/test/UseMachine.vue @@ -12,9 +12,8 @@