- {posts.map(post => {
+ {posts.map((post) => {
return - {post.title}
;
})}
@@ -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