Merge pull request #5 from icetemple/task-4

feat: change object to map events and states
This commit is contained in:
Dmitriy Pleshevskiy 2020-11-08 04:51:26 +02:00 committed by GitHub
commit ef2fa353c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 45 additions and 21 deletions

View file

@ -8,15 +8,15 @@ export type ActionEvent = (event: string, fromState: StateType, toState: StateTy
export interface IConfig { export interface IConfig {
[key: string]: undefined | ActionEvent | ActionConfigMap [key: string]: undefined | ActionEvent | ActionConfigMap;
onEnter?: ActionEvent onEnter?: ActionEvent;
onLeave?: ActionEvent onLeave?: ActionEvent;
} }
export interface IActionConfig { export interface IActionConfig {
state: StateType state: StateType;
onBeforeChange?: ActionEvent onBeforeChange?: ActionEvent;
onChange?: ActionEvent onChange?: ActionEvent;
} }
@ -26,8 +26,8 @@ export class StateMachine {
private _currentState: StateType; private _currentState: StateType;
private _onEnter?: ActionEvent; private _onEnter?: ActionEvent;
private _onLeave?: ActionEvent; private _onLeave?: ActionEvent;
private _eventsByState: Record<string, Record<string, (payload: Payload) => any>> = {}; private _eventsByState = new Map<StateType, Record<string, (payload: Payload) => any>>();
private _statesByState: Record<string, StateType[]> = {}; private _statesByState = new Map<StateType, StateType[]>();
constructor(initial: StateType, config: IConfig) { constructor(initial: StateType, config: IConfig) {
this._currentState = initial; this._currentState = initial;
@ -38,7 +38,11 @@ export class StateMachine {
continue continue
} }
this._statesByState[fromStateKey] = []; const fromState: StateType = /^\d+$/.test(fromStateKey) ?
parseInt(fromStateKey, 10)
: fromStateKey;
const statesOfState: StateType[] = [];
let actions = config[fromStateKey] as ActionConfigMap; let actions = config[fromStateKey] as ActionConfigMap;
for (let actionName in actions) { for (let actionName in actions) {
@ -47,9 +51,9 @@ export class StateMachine {
action as IActionConfig action as IActionConfig
: { state: action as StateType }; : { state: action as StateType };
this._statesByState[fromStateKey].push(actionConfig.state); statesOfState.push(actionConfig.state);
this._statesByState.set(fromState, statesOfState);
let fromState: StateType = /^\d+$/.test(fromStateKey) ? parseInt(fromStateKey, 10) : fromStateKey;
this._initChangeState(actionName, fromState, actionConfig.state, actionConfig); this._initChangeState(actionName, fromState, actionConfig.state, actionConfig);
} }
} }
@ -57,10 +61,6 @@ export class StateMachine {
private _initChangeState(eventName: string, fromState: StateType, toState: StateType, actionConfig: IActionConfig): void { private _initChangeState(eventName: string, fromState: StateType, toState: StateType, actionConfig: IActionConfig): void {
if (!this._eventsByState[fromState]) {
this._eventsByState[fromState] = {};
}
const { onBeforeChange, onChange } = actionConfig; const { onBeforeChange, onChange } = actionConfig;
const _runEvent = async (method?: ActionEvent, payload: Payload = {}): Promise<void> => { const _runEvent = async (method?: ActionEvent, payload: Payload = {}): Promise<void> => {
if (method) { if (method) {
@ -68,7 +68,8 @@ export class StateMachine {
} }
}; };
this._eventsByState[fromState][eventName] = async (sourcePayload: Payload = {}) => { const events = this._eventsByState.get(fromState) ?? {};
events[eventName] = async (sourcePayload: Payload = {}) => {
const payload = cloneDeep(sourcePayload); const payload = cloneDeep(sourcePayload);
await _runEvent(this._onEnter, payload); await _runEvent(this._onEnter, payload);
await _runEvent(onBeforeChange, payload); await _runEvent(onBeforeChange, payload);
@ -79,11 +80,13 @@ export class StateMachine {
return this; return this;
}; };
this._eventsByState.set(fromState, events);
if (!this[eventName]) { if (!this[eventName]) {
this[eventName] = async (payload: Payload = {}) => { this[eventName] = async (payload: Payload = {}) => {
if (this._eventsByState[this._currentState] const events = this._eventsByState.get(this._currentState);
&& this._eventsByState[this._currentState][eventName]) { if (events && events[eventName]) {
return this._eventsByState[this._currentState][eventName](payload); return events[eventName](payload);
} }
} }
} }
@ -102,10 +105,10 @@ export class StateMachine {
} }
public getAvailableStates(): StateType[] { public getAvailableStates(): StateType[] {
return this._statesByState[this._currentState] || [] return this._statesByState.get(this._currentState) ?? []
} }
public getAvailableActions(): string[] { public getAvailableActions(): string[] {
return Object.keys(this._eventsByState[this._currentState] || {}); return Object.keys(this._eventsByState.get(this._currentState) ?? {});
} }
} }

View file

@ -129,6 +129,27 @@ describe('StateMachine', () => {
objectStrFSM._currentState = StrStatus.PENDING; objectStrFSM._currentState = StrStatus.PENDING;
}); });
describe('::new', () => {
it('should init fsm model successfully', () => {
const simpleIntFSM = new StateMachine(IntStatus.PENDING, {
[IntStatus.PENDING]: {
active: IntStatus.ACTIVE,
delete: IntStatus.DELETED,
},
[IntStatus.ACTIVE]: {
toDraft: IntStatus.PENDING,
archive: IntStatus.ARCHIVED,
doNothing: IntStatus.ACTIVE,
}
});
expect(simpleIntFSM).toBeDefined();
expect(simpleIntFSM.getCurrentState()).toBe(IntStatus.PENDING);
})
})
describe('.getCurrentState', () => { describe('.getCurrentState', () => {
describe('<IntStatus>', () => { describe('<IntStatus>', () => {
it('should return initial int state for simple fsm model', () => { it('should return initial int state for simple fsm model', () => {