Skip to content

Commit

Permalink
add observable events
Browse files Browse the repository at this point in the history
  • Loading branch information
brianzinn committed Feb 23, 2021
1 parent 973964a commit 8721262
Show file tree
Hide file tree
Showing 17 changed files with 905 additions and 18 deletions.
6 changes: 6 additions & 0 deletions .nycrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"branches": 100,
"lines": 100,
"functions": 100,
"statements": 100
}
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ const machina = createMachina<LightState, LightTransition>(LightState.On)
}, async () => console.log('light turned off'))
.build();

// before starting you can register for events in a strongly typed manner.
// subscribe to ALL events
const observeEveryting = machina.subscribe((eventData: EventData<LightState | LightTransition>) => console.log('all', eventData));
// subscribe only for the Light is turned on:
machina.subscribe((eventData: EventData<LightState | LightTransition>) => console.log('all', eventData), NotificationType.StateEnter, LightState.On);
// start machine initiates "StateEnter" for the initial configured state.
machina.start();
assert.strictEqual(LightState.On, machina.state.current);
assert.deepStrictEqual([LightTransition.TurnOff], machina.state.possibleTransitions.map(t => t.edge));

Expand Down Expand Up @@ -74,5 +81,5 @@ const machina = new Machina(LightState.On, new Map<LightState, NodeState<LightSt
Name inspired from the movie ex-machina, but a tribute to popular library xstate (did not find machina.js till after - it does not look to be actively maintained).

## TODO:
* add observables for events
* add hierarchy for state machines
* add events when leaving State and navigating transitions
* add hierarchy example for state machines
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"watch": "tsc --watch",
"unit-test": "cross-env TS_NODE_PROJECT=\"tsconfig.test.json\" mocha --require ts-node/register test/**/*.spec.ts --timeout=8000 --exit",
"test": "npm run unit-test",
"test:coverage": "nyc --reporter=lcov --reporter=text-summary npm run test"
"test:coverage": "nyc --check-coverage=true --reporter=lcov --reporter=text-summary npm run test"
},
"repository": {
"type": "git",
Expand Down
12 changes: 9 additions & 3 deletions src/Builder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Machina, NodeState, Transition } from "./Machina";
import { IMachina, Machina, NodeState, Transition } from "./Machina";

export interface IMachinaBuilder<S, E, T extends Transition<S, E>> {
/**
Expand All @@ -10,7 +10,9 @@ export interface IMachinaBuilder<S, E, T extends Transition<S, E>> {
/**
* Return the state machine based on all states and transitions added.
*/
build(): Machina<S, E, T>
build(): IMachina<S, E, T>

buildAndStart(): IMachina<S, E, T>
}

/**
Expand Down Expand Up @@ -46,9 +48,13 @@ export class MachinaBuilder<S, E, T extends Transition<S, E>> implements IMachin
return this;
};

build = () => {
build = (): IMachina<S, E, T> => {
return new Machina(this.initialState, this.stateMap);
}

buildAndStart = (): IMachina<S, E, T> => {
return this.build().start();
}
}

/**
Expand Down
70 changes: 68 additions & 2 deletions src/Machina.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Nullable } from "./index";

import { Observable } from "./subscriptions/Observable";
import { NotificationType } from './subscriptions/NotificationType';
import { Observer } from "./subscriptions/Observer";
import { EventData } from "./subscriptions/EventData";
import { EventState } from "./subscriptions/EventState";
/**
* Current state and transitions to other states.
*/
Expand Down Expand Up @@ -34,15 +38,40 @@ export type Transition<S, E> = {
}

export interface IMachina<S, E, T extends Transition<S, E>> {

start(): IMachina<S, E, T>
/**
* Edge to follow from current state to another state (edge is the input that triggers a transition).
* Return the result of the transition (or null) instead of IMachine, so cannot be used fluently.
*/
transition: (edge: E) => Nullable<MachinaState<S, E, T>>
transition(edge: E): Nullable<MachinaState<S, E, T>>

/**
* Current machine state (includes transitions out of current state, if any)
*/
readonly state: MachinaState<S, E, T>

/**
* Subscribe a callback with optional filtering by NotificationType and value for notifications.
*
* @param callback Will be called when events occur and can optionally include filtering.
* @param notificationType Only notify when this specific notification occurs (ie: State change, Transition followed) (defaults to All notificatons)
* @param valueFilter Should not be used with All notifications, only makes sense when already filtering by Scene/Transition (defaults to null - not filtering)
* @param insertFirst Should be inserted first and be notified before other already registered observers. (defaults false)
* @param nextNotificationOnly Only a single notification should occur. Observer will automatically unsubscribe after first notification (default false)
*/
subscribe(callback: (eventData: EventData<S | E>, eventState?: EventState) => void, notificationType?: NotificationType, valueFilter?: S | E, insertFirst?: boolean, nextNotificationOnly?: boolean): Nullable<Observer<S, E>>
/**
* Unregister an existing observer.
* @param observer observer to unsubscribe from receiving further notifications.
*/
unsubscribe(observer: Nullable<Observer<S, E>>): boolean

/**
* Unregister by the callback method from receiving further notifications.
* @param callback callback method to unsubscribe
*/
unsubscribeCallback(callback: (eventData: EventData<S | E>, eventState: EventState) => void): boolean
}

export type NodeState<S, E, T extends Transition<S, E>> = {
Expand All @@ -53,13 +82,41 @@ export type NodeState<S, E, T extends Transition<S, E>> = {

export class Machina<S, E, T extends Transition<S, E>> implements IMachina<S, E, T> {
private currentState: S;
private started = false;

/**
* Observable event triggered each time a transition or state change occurs.
*/
private onEventObservable = new Observable<S, E>();

constructor(initialState: S, private stateMap: Map<S, NodeState<S, E, T>>) {
this.currentState = initialState;
return this;
}

start(): IMachina<S, E, T> {
const currentNodeState: NodeState<S, E, T> | undefined = this.stateMap.get(this.currentState);
this.onEventObservable.notifyObservers({
notificationType: NotificationType.StateEnter,
value: this.currentState
});
if (currentNodeState !== undefined && currentNodeState.onEnter) {
currentNodeState.onEnter();
}
this.started = true;
return this;
}

subscribe(callback: (eventData: EventData<S | E>, eventState: EventState) => void, notificationType: NotificationType = NotificationType.All, valueFilter: Nullable<S | E> = null, insertFirst: boolean = false, nextNotificationOnly: boolean = false): Nullable<Observer<S, E>> {
return this.onEventObservable.add(callback, notificationType, valueFilter, insertFirst, nextNotificationOnly);
}

unsubscribe(observer: Nullable<Observer<S, E>>): boolean {
return this.onEventObservable.remove(observer);
}

unsubscribeCallback(callback: (eventData: EventData<S | E>, eventState: EventState) => void): boolean {
return this.onEventObservable.removeCallback(callback);
}

get state(): MachinaState<S, E, T> {
Expand All @@ -70,6 +127,9 @@ export class Machina<S, E, T extends Transition<S, E>> implements IMachina<S, E,
}

transition = (edge: E) => {
if (!this.started) {
throw new Error('Must start() Machine before transition(...).')
}
const nodeState: NodeState<S, E, T> = (this.stateMap.get(this.currentState))!;
const match: T | undefined = nodeState.outEdges.find(t => t.edge === edge);
if (match === undefined) {
Expand All @@ -86,6 +146,12 @@ export class Machina<S, E, T extends Transition<S, E>> implements IMachina<S, E,
current: match.nextState,
possibleTransitions: nextState.outEdges
}

this.onEventObservable.notifyObservers({
notificationType: NotificationType.StateEnter,
value: match.nextState
});

return result;
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/subscriptions/EventData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NotificationType } from "./NotificationType";

export type EventData<U> = {
/**
* ie: State on Enter/Leave or Transition
*/
notificationType: NotificationType
/**
* For the state or transition the value
*/
value: U
}
10 changes: 10 additions & 0 deletions src/subscriptions/EventState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Right now only allows to notify that future observers should be skipped, but we likely want to allow an observer to block some events (ie: Transition)
*/
export type EventState = {
/**
* An Observer can set this property to true to prevent subsequent observers of being notified.
* TODO: consider a cancellation with same functionality
*/
skipNextObservers: boolean;
}
29 changes: 29 additions & 0 deletions src/subscriptions/NotificationType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export enum NotificationType {
/**
* Should not be used
*/
None = 0,
/**
* When a state is entered
* NOTE: 0x001
*/
StateEnter = 1,
/**
* When a state is left
* NOTE: 0x010
*/
// tslint:disable-next-line:no-bitwise
StateLeave = 1 << 1,
/**
* When a transition occurs
* NOTE: 0x0100
*/
// tslint:disable-next-line:no-bitwise
Transition = 1 << 2,
/**
* Includes state enter/leave/transition. You need to use the event to determine what has occured.
* NOTE: 0x0111
*/
// tslint:disable-next-line:no-bitwise
All = ~(~0 << 3)
}
Loading

0 comments on commit 8721262

Please sign in to comment.