tstate-machine
Advanced tools
Comparing version 1.1.0 to 1.1.1
@@ -0,8 +1,12 @@ | ||
1.1.1 | ||
- Translated readme, comments and changelog | ||
- `@StateMachine.hide` become universal - for properties and methods | ||
# 1.1.0 | ||
- написаны тесты | ||
- убрал lodash из сборки | ||
- .hide теперь не требует вызова и не имеет обратной совместимости со старой версией | ||
- приделал TravisCI | ||
- tests complete | ||
- remove lodash from bundle | ||
- .hide now not able to call | ||
- attach TravisCI | ||
# 1.0.7 | ||
- убрал `reflect-metadata` из сборки | ||
- remove `reflect-metadata` from bundle |
@@ -5,3 +5,3 @@ // Generated by dts-bundle v0.7.2 | ||
/** | ||
* @description константа с названием initial-состояния. | ||
* @description constant to store initial state name | ||
* @type {string} | ||
@@ -11,59 +11,36 @@ */ | ||
/** | ||
* @description Служебный статический декоратор, прячет декорированный метод от перебора в цикле for-in | ||
* @description Static service decorator for hiding property/method in for-in | ||
*/ | ||
static hide(_target: object, _key: string, descriptor: PropertyDescriptor): PropertyDescriptor; | ||
/** | ||
* @description Служебный статичный декоратор, делает наследование состояния. | ||
* Название декорируемого свойства класса будет названием регистрируемого сосотояния | ||
* @param parentState - имя родительского сосотояния(от которого наследуемся) | ||
* @param to - массив/строка состояний/состояния, в которые/которое можно перейти из данного состояния. | ||
* @description Static service decorator - make state inheritance | ||
* Name of decorated property becomes as state name | ||
* @param parentState - name of parent state | ||
* @param to - states in which we can transit from them state | ||
*/ | ||
static extend(parentState: string, to?: string | Array<string>): (target: object, stateName: string) => void; | ||
/** | ||
* @description Массив состояний, в которые можно перейти из 'initial' | ||
* @description Array of states in which machine can transit from initial | ||
*/ | ||
protected readonly $next: Array<string>; | ||
/** | ||
* @description Метод для смены состояния StateMachine в targetState. | ||
* Проверяет что оно зарегистрировано, что в него можно перейти из текущего состояния и если ок - переходит. | ||
* @param targetState - название состояния, в которое нужно перейти | ||
* @param args - любые данные, которые будут проброшены в onEnter-callback при входе в состояние | ||
* @description Method for transit machine to another state | ||
* Check the target state is registered, check transition is possible | ||
* @param targetState - name of state to transit | ||
* @param args - any data for pass to onEnter callback | ||
*/ | ||
transitTo(targetState: string, ...args: Array<any>): void; | ||
/** | ||
* @description Служебный метод, который обязательно вызывать в конструкторе класса-потомка | ||
* для создания слепка начального состояния StateMachine. | ||
* @description Service method. Required to call in constructor of child-class | ||
* for create a snapshot of initial state | ||
*/ | ||
protected rememberInitState(): void; | ||
/** | ||
* @description Метод, регистрирующий коллбэк cb для ВХОДА в состяоние stateName | ||
* @param stateName - название состояния | ||
* @param cb - коллбэк | ||
*/ | ||
onEnter(stateName: string, cb: (...args: Array<any>) => void): () => void; | ||
/** | ||
* @description Метод, регистрирующий коллбэк cb для ВЫХОДА из состояния stateName | ||
* @param stateName - название состояния | ||
* @param cb - коллбэк | ||
*/ | ||
onLeave(stateName: string, cb: () => void): () => void; | ||
/** | ||
* @description Название текущего состояния StateMachine | ||
* @description getter for current state name | ||
*/ | ||
readonly currentState: string; | ||
/** | ||
* @description Проверка, находится-ли машина в состоянии stateName | ||
* @param stateName - название проверяемого состояния | ||
*/ | ||
is(stateName: string): boolean; | ||
/** | ||
* @description Проверка что машина может перейти в состояние stateName из текущего | ||
* @param stateName - название целевого состояния | ||
* @returns {boolean} | ||
*/ | ||
can(stateName: string): boolean; | ||
/** | ||
* @description Получить список состояний, в которые машина может перейти из текущего | ||
* @return {Array<string>} | ||
*/ | ||
transitions(): Array<string>; | ||
@@ -70,0 +47,0 @@ } |
150
index.js
@@ -164,3 +164,3 @@ (function webpackUniversalModuleDefinition(root, factory) { | ||
/** | ||
* @description изолированное хранилище внутренней информации конкретной StateMachine | ||
* @description isolated store for meta-information of concrete StateMachine | ||
*/ | ||
@@ -172,6 +172,6 @@ var StateMachineWeakMap = new WeakMap(); | ||
/** | ||
* @description Служебный статический метод, генерирующий текст ошибки, сообщающей о невозможности перейти в состояние | ||
* @param currentState - из какого состояния не смогли перейти | ||
* @param stateName - в какой состяоние не смогли перейти | ||
* @returns string - сообщение об ошибке | ||
* @description static service method for generate error text about unable transit to | ||
* @param currentState - from what state cant transit | ||
* @param stateName - to what state cant transit | ||
* @returns string - message | ||
*/ | ||
@@ -182,13 +182,18 @@ StateMachine.NEXT_STATE_RESTRICTED = function (currentState, stateName) { | ||
/** | ||
* @description Служебный статический декоратор, прячет декорированный метод от перебора в цикле for-in | ||
* @description Static service decorator for hiding property/method in for-in | ||
*/ | ||
StateMachine.hide = function (_target, _key, descriptor) { | ||
descriptor.enumerable = false; | ||
if (descriptor) { | ||
descriptor.enumerable = false; | ||
} | ||
else { | ||
descriptor = { enumerable: false, configurable: true }; | ||
} | ||
return descriptor; | ||
}; | ||
/** | ||
* @description Служебный статичный декоратор, делает наследование состояния. | ||
* Название декорируемого свойства класса будет названием регистрируемого сосотояния | ||
* @param parentState - имя родительского сосотояния(от которого наследуемся) | ||
* @param to - массив/строка состояний/состояния, в которые/которое можно перейти из данного состояния. | ||
* @description Static service decorator - make state inheritance | ||
* Name of decorated property becomes as state name | ||
* @param parentState - name of parent state | ||
* @param to - states in which we can transit from them state | ||
*/ | ||
@@ -201,3 +206,3 @@ StateMachine.extend = function (parentState, to) { | ||
/** | ||
* @description Получить хранилище внутренней информации для данного экземпляра StateMachine | ||
* @description Receive store of inner information for this instance of StateMachine | ||
*/ | ||
@@ -218,3 +223,3 @@ get: function () { | ||
/** | ||
* @description Массив состояний, в которые можно перейти из 'initial' | ||
* @description Array of states in which machine can transit from initial | ||
*/ | ||
@@ -229,3 +234,3 @@ get: function () { | ||
/** | ||
* @description Служебный метод для получения прототипа текущего экземпляра StateMachine. Нужен для извлечения метаданных | ||
* @description Service method for get prototype of current instance | ||
*/ | ||
@@ -239,4 +244,3 @@ get: function () { | ||
/** | ||
* @description Служебный метод для получения метаданных о состоянии stateName | ||
* @param stateName - название состояния | ||
* @description Service method for get metadata for state | ||
*/ | ||
@@ -247,6 +251,6 @@ StateMachine.prototype.getMetadataByName = function (stateName) { | ||
/** | ||
* @description Метод для смены состояния StateMachine в targetState. | ||
* Проверяет что оно зарегистрировано, что в него можно перейти из текущего состояния и если ок - переходит. | ||
* @param targetState - название состояния, в которое нужно перейти | ||
* @param args - любые данные, которые будут проброшены в onEnter-callback при входе в состояние | ||
* @description Method for transit machine to another state | ||
* Check the target state is registered, check transition is possible | ||
* @param targetState - name of state to transit | ||
* @param args - any data for pass to onEnter callback | ||
*/ | ||
@@ -258,14 +262,12 @@ StateMachine.prototype.transitTo = function (targetState) { | ||
} | ||
// Проверить, что нужное состояние зарегистрировано | ||
// Check target state is registered | ||
var stateToApply = targetState !== 'initial' ? this[targetState] : this.$store.initialState; | ||
if (!stateToApply) { | ||
// Здесь и далее - просто напишу ошибку в консоль и выйду из метода. | ||
// Сделано это затем, что если метод вызвать внутри коллбэков промиса - промис словит эту ошибку и промолчит. | ||
// А ошибка по сути служебная, только для разработчика, что он забыл что-то описать или опечатался | ||
// Here and next - simply write error to console and return | ||
console.error("No state '" + targetState + "' for navigation registered"); | ||
return; | ||
} | ||
// Проверим, что можем совершить переход в нужное состояние | ||
// Check transition is possible | ||
if (this.$store.isInitialState) { | ||
// У initial-состояния допуски для перехода хранятся в $next | ||
// initial state store next on $next | ||
if (!this.$next.includes(targetState)) { | ||
@@ -277,3 +279,3 @@ console.error(StateMachine.NEXT_STATE_RESTRICTED(this.$store.currentState, targetState)); | ||
else { | ||
// У других состояний допуски хранятся в их метаданных | ||
// another states store next in them metadata | ||
var currentStateProps = this.getMetadataByName(this.$store.currentState); | ||
@@ -286,3 +288,3 @@ var to = currentStateProps.to; | ||
} | ||
// Т.к. состояния не "чистые", а наследуемые - применять любое состояние буду от initial до требуемого. Использую стек, LIFO | ||
// Make chain of states | ||
var stateChain = [stateToApply]; | ||
@@ -298,5 +300,5 @@ if (targetState !== 'initial') { | ||
} | ||
// Вызввать все коллбэки при выходе из состояния | ||
// Call onLeave callbacks | ||
this.$store.callLeaveCbs(); | ||
// Применяем стек состояний | ||
// Apply states chain | ||
merge_1.merge(this, this.$store.initialState); | ||
@@ -307,10 +309,9 @@ while (stateChain.length) { | ||
} | ||
// Вызвать все коллбэки при входе в состояние | ||
// Call all onEnter callbacks | ||
this.$store.callEnterCbs(targetState, args); | ||
// Записываем, что пришли в состояние | ||
this.$store.currentState = targetState; | ||
}; | ||
/** | ||
* @description Служебный метод, который обязательно вызывать в конструкторе класса-потомка | ||
* для создания слепка начального состояния StateMachine. | ||
* @description Service method. Required to call in constructor of child-class | ||
* for create a snapshot of initial state | ||
*/ | ||
@@ -324,15 +325,5 @@ StateMachine.prototype.rememberInitState = function () { | ||
}; | ||
/** | ||
* @description Метод, регистрирующий коллбэк cb для ВХОДА в состяоние stateName | ||
* @param stateName - название состояния | ||
* @param cb - коллбэк | ||
*/ | ||
StateMachine.prototype.onEnter = function (stateName, cb) { | ||
return this.$store.registerEnterCallback(stateName, cb); | ||
}; | ||
/** | ||
* @description Метод, регистрирующий коллбэк cb для ВЫХОДА из состояния stateName | ||
* @param stateName - название состояния | ||
* @param cb - коллбэк | ||
*/ | ||
StateMachine.prototype.onLeave = function (stateName, cb) { | ||
@@ -343,3 +334,3 @@ return this.$store.registerLeaveCallback(stateName, cb); | ||
/** | ||
* @description Название текущего состояния StateMachine | ||
* @description getter for current state name | ||
*/ | ||
@@ -352,14 +343,5 @@ get: function () { | ||
}); | ||
/** | ||
* @description Проверка, находится-ли машина в состоянии stateName | ||
* @param stateName - название проверяемого состояния | ||
*/ | ||
StateMachine.prototype.is = function (stateName) { | ||
return this.currentState === stateName; | ||
}; | ||
/** | ||
* @description Проверка что машина может перейти в состояние stateName из текущего | ||
* @param stateName - название целевого состояния | ||
* @returns {boolean} | ||
*/ | ||
StateMachine.prototype.can = function (stateName) { | ||
@@ -372,6 +354,2 @@ if (this.$store.isInitialState) { | ||
}; | ||
/** | ||
* @description Получить список состояний, в которые машина может перейти из текущего | ||
* @return {Array<string>} | ||
*/ | ||
StateMachine.prototype.transitions = function () { | ||
@@ -381,3 +359,3 @@ return this.$store.isInitialState ? this.$next : this.getMetadataByName(this.currentState).to; | ||
/** | ||
* @description константа с названием initial-состояния. | ||
* @description constant to store initial state name | ||
* @type {string} | ||
@@ -468,4 +446,4 @@ */ | ||
/** | ||
* Хранилище внутренней информации для конкретной StateMachine. | ||
* Все методы и свойства этого класса используются только в родительском классе StateMachine и никакие потомки сюда доступа не имеют | ||
* Store for inner meta-information for concrete StateMachine. | ||
* All methods and properties of this class used only in parent StateMachine class and no one child statemachine no access to it | ||
*/ | ||
@@ -475,17 +453,17 @@ var StateMachineInnerStore = (function () { | ||
/** | ||
* @description Хранит начальное состояние машины | ||
* @description Store initial state | ||
*/ | ||
this.$initialState = {}; | ||
/** | ||
* @description Название текущего состояния машины. Начальное - initial | ||
* @description name of current state | ||
*/ | ||
this.currentState = 'initial'; | ||
/** | ||
* @description - key-value-хранилище коллбэков, которые будут работать при ВХОДЕ в состояние. | ||
* Ключ - название состояния. Значение - массив с функциями | ||
* @description - key-value-store for onEnter callbacks | ||
* key - state name, value - array with callbacks | ||
*/ | ||
this.onEnterCbs = {}; | ||
/** | ||
* @description - key-value-хранилище коллбэков, которые будут работать при ВЫХОДЕ из состояния. | ||
* Ключ - название состояния. Значение - массив с функциями | ||
* @description - key-value-store for onLeave callbacks | ||
* key - state name, value - array with callbacks | ||
*/ | ||
@@ -495,12 +473,8 @@ this.onLeaveCbs = {}; | ||
/** | ||
* @description метод, сохраняющий начальное ключа во внутренний объект $initialState. | ||
* Вызовы этого метода собирают чистое initial-состояние | ||
* @param key - ключ | ||
* @param value - значение | ||
* @description store initial value of property to $initialState | ||
*/ | ||
StateMachineInnerStore.prototype.rememberInitialKey = function (key, value) { | ||
// Здесь важно порвать ссылки с полями машины. | ||
// Было this.$initialState[key] = value. Если value не был примитивом - он сохранялся, очевидно, по ссылке. | ||
// И в случае его изменения - он изменялся и в initialState, что влекло за собой бардак. | ||
// Можно было использовать _.cloneDeep после запоминания всех ключей, но разницы имхо никакой кроме порождения лишнего метода | ||
// Here is important to break links with statemachine properties. | ||
// If value wasn`t primitive type - they save by link | ||
// And if we change them - initialState change too. | ||
var assignable = {}; | ||
@@ -512,5 +486,2 @@ assignable[key] = value; | ||
Object.defineProperty(StateMachineInnerStore.prototype, "initialState", { | ||
/** | ||
* @description Начальное состояние машины | ||
*/ | ||
get: function () { | ||
@@ -523,5 +494,2 @@ return this.$initialState; | ||
Object.defineProperty(StateMachineInnerStore.prototype, "isInitialState", { | ||
/** | ||
* @description Находится-ли машина в начальном состоянии? | ||
*/ | ||
get: function () { | ||
@@ -534,6 +502,3 @@ return this.currentState === 'initial'; | ||
/** | ||
* @description Регистрирует коллбэк cb, который вызовется при входе в состояние stateName | ||
* @param stateName - состояние, при входе в которое вызвать коллбэк | ||
* @param cb - коллбэк | ||
* @returns {()=>void} - функция удаления созданного коллбэка | ||
* @description register onEnter callback, return function for drop callback | ||
*/ | ||
@@ -549,6 +514,3 @@ StateMachineInnerStore.prototype.registerEnterCallback = function (stateName, cb) { | ||
/** | ||
* @description Регистрирует коллбэк cb, который вызовется при выходе из состояния stateName. | ||
* @param stateName - состояние, при выходе из которого вызвать коллбэк | ||
* @param cb - коллбэк | ||
* @returns {()=>void} - функция удаления созданного коллбэка | ||
* @description register onLeave callback, return function for drop callback | ||
*/ | ||
@@ -563,7 +525,2 @@ StateMachineInnerStore.prototype.registerLeaveCallback = function (stateName, cb) { | ||
}; | ||
/** | ||
* @description Вызвать все коллбэки, зарегистрированные на вход в состояние stateName | ||
* @param stateName - имя состояния | ||
* @param args - возможные аргументы, переданные при переходе в состояние. Они попадут в коллбэк | ||
*/ | ||
StateMachineInnerStore.prototype.callEnterCbs = function (stateName, args) { | ||
@@ -574,5 +531,2 @@ if (this.onEnterCbs[stateName]) { | ||
}; | ||
/** | ||
* @description Вызывать все коллбэки по выходу из текущего состояния | ||
*/ | ||
StateMachineInnerStore.prototype.callLeaveCbs = function () { | ||
@@ -596,3 +550,3 @@ var stateName = this.currentState; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var StateMachineMetadataKey = 'Tochka_StateMachineMetadata'; | ||
var StateMachineMetadataKey = 'TStateMachineMetadata'; | ||
/** | ||
@@ -599,0 +553,0 @@ * Хранилище метаданных для состояний машины. Хранит родительское состояние и названия состояний куда можно перейти |
{ | ||
"name": "tstate-machine", | ||
"version": "1.1.0", | ||
"version": "1.1.1", | ||
"description": "TypeScript implementation of StateMachine", | ||
@@ -5,0 +5,0 @@ "types": "index.d.ts", |
309
README.md
# tstate-machine | ||
[![Build Status](https://travis-ci.org/SoEasy/tstate-machine.svg?branch=master)](https://travis-ci.org/SoEasy/tstate-machine) | ||
Реализация StateMachine на TypeScript | ||
StateMachine implementation on TypeScript. Works fine with ES6 | ||
## Attention | ||
Модуль работоспособен, но разработка еще не закончена. API менять не собираюсь | ||
## Overview | ||
Class-based, declarative, strongly typed state machine with hard declared transitions and without autocomplete problems. | ||
## Установка | ||
Пакет лежит в npm | ||
## Example | ||
```javascript | ||
import { IStateDeclaration, StateMachine } from 'tstate-machine'; | ||
class ButtonStateMachine extends StateMachine { | ||
// initial state | ||
text: string = 'do request'; | ||
diisabled: boolean = false; | ||
// state declarations | ||
// From what state we inherit and in what states we can transit | ||
@StateMachine.extend(StateMachine.INTIAL, ['requestState']) | ||
mainState: IStateDeclaration<ButtonStateMachine> = {}; // no changes relative to parent(initial) state | ||
@StateMachine.extend('mainState', ['doneState']) | ||
requestState: IStateDeclaration<ButtonStateMachine> = { | ||
text: 'sending...', | ||
disabled: true | ||
}; | ||
@StateMachine.extend('requestState') | ||
doneState: IStateDeclaration<ButtonStateMachine> = { | ||
text: 'done' | ||
// no change disabled - property inherited from requestState and has `false` value | ||
}; | ||
// common but important actions | ||
// states in which one we can transit from initial | ||
@StateMachine.hide | ||
protected get $next(): Array<string> { | ||
return ['mainState']; | ||
} | ||
// remember initial state | ||
constructor() { | ||
super(); | ||
this.rememberInitState(); | ||
} | ||
} | ||
const machine = new TextStateMachine(); | ||
machine.transitTo('maintState'); | ||
machine.transitTo('requestState'); | ||
console.log(machine.text); // autocomplete works fine! | ||
``` | ||
## Installation | ||
From npm | ||
>npm install --save tstate-machine | ||
Установка с гитхаба | ||
From github | ||
>npm install https://github.com/SoEasy/tstate-machine/tarball/master | ||
## Основная информация | ||
Интерфейс машины состояний подсмотрен тут: | ||
[JS FSM](https://github.com/jakesgordon/javascript-state-machine). | ||
## How to use | ||
StateMachine представляет собой класс, от которого стоит наследовать свои классы конкретных машин. | ||
Все поля, которые будут описаны в вашем потомке - это начальное состояние машины. | ||
### Create your own StateMachine | ||
To create your own state machine you must create class and inherit it from `StateMachine` class. | ||
```javascript | ||
class ButtonStateMachine extends StateMachine {} | ||
``` | ||
Главный бонус - с типизацией проблем не возникнет. | ||
> Важно! Всем полям класса необходимо задать начальное значение - хоть null, хоть undefined. В пртивном случае машина не запомнит эти поля из-за особенностей компиляции TypeScript | ||
### Fill initial state | ||
All declared fields in your class with their initial values will be called `StateMachine.INITIAL`. | ||
> Important! All of your state fields must contain any initial value: null/undefined/something. | ||
Otherwise your state machine will not work correctly due to the features of typescript compilation. | ||
Данная реализация StateMachine не предполагает создания независимых состояний. | ||
Т.е. все состояния машины либо наследуются от initial, либо от других состояний. | ||
Объявлять в состоянии новые поля с данными - технически можно, но не нужно. | ||
Объявление нового состояния должно содержать только те поля, которые отличаются от родительского. | ||
```javascript | ||
class ButtonStateMachine extends StateMachine { | ||
text: string = 'do request'; | ||
disabled: boolean = false; | ||
} | ||
``` | ||
Because StateMachine is made by inheritance - to remember initial values you must call method `.rememberInitState` in constructor. | ||
```javascript | ||
constructor() { | ||
super(); // call constructor of StateMachine | ||
this.rememberInitState(); // remember own properties as initial state | ||
} | ||
``` | ||
StateMachine берет на себя контроль за переходами из состояния в состояние. | ||
Для этого при описании состояния в декларативном стиле описывается массив других состояний, в которые можно перейти из описываемого. | ||
При попытке перейти в недозволенное состояние вылетит Error с описанием откуда-куда не получилось перейти | ||
StateMachine позволяет регистрировать коллбэки, которые будут вызваны при входе в нужное состояние и при выходе из него. | ||
Коллбэки, зарегистрированные для входа в состояние способны получать данные, переданные в метод перехода между состояниями. | ||
Коллбэки выхода из состояния, очевидно, никаких данных не принимают. | ||
### Declare states | ||
There are not independent states - every state must be inherited from the initial state or from other declared state. | ||
Simply, if we represent statemachine as graph - we can travel to each state from initial state by transitions. | ||
## Объяснение работы | ||
Основа реализации машины - метаданные, дескрипторы доступа декораторов и цикл for-in по полям объекта. | ||
Also with state declaration we can describe the states in which we can go. | ||
Главная идея: | ||
1. В конструкторе класса-потомка вызвать родительский метод `this.rememberInitState();` Который пройдет циклом по всем полям класса и запомнит их как начальное состояние | ||
2. В классе-потомке описать protected-геттер $next, возвращающий массив возможных состояний для перехода из начального | ||
3. С помощью специального декоратора `@StateMachine.extend` зарегистрировать новые состояния, описанные как объекты с изменениями. Хранить их в метаданных класса | ||
4. При переходе из состояния в состояние собрать цепочку наследования состояний, привести объект машины в начальное состояние и накатить на него всю цепочку изменений. *Почему так? Потому что ветвей наследования может быть несколько, и если переходить вдруг из одной в другую - чтобы не описывать все различия - проще поехать от корня дерева состояний. Опыт показывает, что развесистых графов состояния у нас не было - можно не бояться за производительность* | ||
5. Все методы класса оборачиваются декоратором `StateMachine.hide` - он прячет метод от попадания в итератор for-in по объекту. Это важно, чтобы методы не попадали в хранилище initial-состояния и не перезаписывались каждый раз при переходе. | ||
To declare the states there is static method `StateMachine.extend(parentState: string, to: Array<string>|string)` with two arguments - from what state to be inherited and in what states can go. | ||
Properties names becomes as state names. | ||
```javascript | ||
// declare mainState, inherit from initial state, can transit to requestState | ||
@StateMachine.extend(StateMachine.INITIAL, ['requestState']) | ||
mainState: IStateDeclaration<ButtonStateMachine> = {}; | ||
// declare requestState, inherit from mainState, can transit to doneState | ||
@StateMachine.extend('mainState', ['doneState']) | ||
requestState: IStateDeclaration<ButtonStateMachine> = { | ||
// override initial properties | ||
text: 'sending...', | ||
disabled: true | ||
} | ||
// declare doneState, inherit from requestState, cant transit to anything - its final state | ||
@StateMachine.extend('requestState') | ||
doneState: IStateDeclaration<ButtonStateMachine> = { | ||
text: 'done' | ||
// no change disabled - property inherited from requestState and has `false` value | ||
}; | ||
``` | ||
> Hint: Declaration of new state should contains only changed fields relative to parent state. | ||
What is `IStateDeclaration`? It`s a optional simple type to avoid typos. | ||
```javascript | ||
export type IStateDeclaration<T> = { | ||
[F in keyof T]?: T[F]; | ||
} | ||
``` | ||
### Declare initial transitions | ||
StateMachine can`t transit to random state. Transitions between states must be declared. | ||
You can imagine that as directed graph. | ||
After creating an instance of your machine they will be in initial state. | ||
To tell machine in which states we can transit from initial state we must declare getter `$next`: | ||
```javascript | ||
@StateMachine.hide // special decorator to avoid properties and methods from for..in iterator | ||
protected get $next(): Array<string> { | ||
return ['mainState']; | ||
} | ||
``` | ||
Ok, now we can start changing states. | ||
### Transitions between states | ||
To transit your machine from one state to another simply call `.transitTo(targetState: string, ...args: Array<any>): void` method of your instance. | ||
``` | ||
const machine = new ButtonStateMachine(); | ||
machine.transitTo('mainState'); // first transition from initial to main state | ||
machine.transitTo('requestState'); // We can transit to declared state | ||
``` | ||
StateMachine restrict the transition to undescribed states: | ||
``` | ||
const machine = new ButtonStateMachine(); | ||
machine.transitTo('doneState'); // cant transit from intial to doneState | ||
// throw error: Navigate to doneState restircted by 'to' argument of state initial | ||
``` | ||
if you try to navigate in unregistered state - machine throw error `No state '%NAME%' for navigation registered`. | ||
### onEnter and onLeave events | ||
StateMachine supports register callbacks to enter and leave states. | ||
``` | ||
const machine = new ButtonStateMachine(); | ||
// register callbacks | ||
const onEnterDoneHandler = machine.onEnter('mainState', (message) => { alert(`main! ${message}`); }); | ||
// Add another onEnter-callback to same state | ||
const onOneMoreEnterDoneHandler = machine.onEnter('mainState', () => { /* do anything */ }); | ||
const onLeaveDoneHandler = machine.onLeave('doneState, () => { alert('...'); }); | ||
machine.transitTo('mainState', 'hello'); | ||
// unregister callbacks | ||
onEnterDoneHandler(); | ||
onLeaveDoneHandler(); | ||
``` | ||
Method `.transitTo` can receive many arguments which passed to onEnter callback. | ||
`onEnter` and `onLeave` methods returns functions - call them and callback will be destroyed. | ||
## How it works | ||
The StateMachine based on several things: metadata, descriptors, for..in iterator over object properties. | ||
Scheme: | ||
1. In a child-class constructor call inherited `this.rememberInitState()` method which iterate over object properties and remember them values as initial state. | ||
2. In a child class define protected getter calling `$next` which return array of possible states to transit in one of them from initial state. | ||
3. With help of special decorator `@StateMachine.extend` register new states which look like diff-objects. Decorator save them into metadata. | ||
4. When transition happens - we build chain of transitions from initial to target state, bring the object to initial state and one-by-one apply states from chain to them. | ||
5. All class methods wrapped by `@StateMachine.hide` decorator to avoid them falling into for..in cycle under the hood of StateMachine. It`s important to each transition does not override them. | ||
## API | ||
- Поля состояния описывать просто как поля класса. | ||
- `@StateMachine.hide()` - декоратор, которым оборачивать все методы класса-потомка | ||
- `StateMachine.extend(parentState, to)` - объявление состояния, наследованного от parentState и с возможными переходами в to | ||
- `transitTo(targetState, ...args)` - переход в состояние targetState. Опционально - аргументы, которые попадут в коллбэк | ||
- `currentState` - название текущего состояния | ||
- `is(stateName)` - текущее состояние == stateName | ||
- `can(stateName)` - возможно-ли перейти из текущего состояния в stateName | ||
- `transitions()` - получить список состояний, в которые можно перейти из текущего | ||
- `onEnter(stateName: string, cb: (...args: Array<any>) => void): () => void` - повесить коллбэк на вход в состояние | ||
- `onLeave(stateName: string, cb: () => void): () => void` - повесить коллбэк на выход из состояния | ||
## Пример использования | ||
### Описание StateMachine | ||
export class ChildStateMachine extends StateMachine { | ||
// Описываем начальное состояние машины | ||
loading: boolean = false; | ||
mainWindowVisible: boolean = false; | ||
paymentWindowVisible: boolean = false; | ||
successMessageVisible: boolean = false; | ||
errorMessageVisible: boolean = false; | ||
// Описываем состояния, в которые можно пойти из начального | ||
@StateMachine.hide() | ||
protected get $next(): Array<string> { return ['loadingState']; } | ||
// Регистрация состояния loadingState, унаследованного от initial, возможно перейти в mainState | ||
@StateMachine.extend('initial', ['mainState']) | ||
private loadingState = { | ||
loading: true | ||
}; | ||
@StateMachine.extend('initial', ['paymentState']) | ||
private mainState = { | ||
mainWindowVisible: true | ||
}; | ||
@StateMachine.extend('mainState', ['mainState', 'successState', 'errorState']) | ||
private paymentState = { | ||
paymentWindowVisible: true | ||
}; | ||
@StateMachine.extend('paymentState', ['paymentState', 'mainState']) | ||
private successState = { | ||
successMessageVisible: true | ||
}; | ||
@StateMachine.extend('paymentState', ['paymentState', 'mainState']) | ||
private errorState = { | ||
errorMessageVisible: true | ||
}; | ||
constructor() { | ||
super(); | ||
this.rememberInitState(); | ||
} | ||
} | ||
### Использование StateMachine | ||
import { ChildFMS } from './child.fsm'; | ||
// Создание экземпляра машины | ||
const f = new ChildStateMachine(); | ||
// Регистрируем коллбэк на вход в состояние paymentState | ||
f.onEnter('paymentState', (paymentSum) => { | ||
console.log('on enter paymentState', paymentSum); | ||
}); | ||
- `@StateMachine.hide()` - decorator for wrapping fields/methods that are not related to the state | ||
- `StateMachine.extend(parentState, to)` - declaring new state, inherited from parentState, possible to transit `to` states | ||
- `transitTo(targetState, ...args)` - transit machine to targetState. Optional - args for `onEnter` callback | ||
- `currentState: string` - name of current state | ||
- `is(stateName): boolean` - current state == stateName | ||
- `can(stateName): boolean` - is it possible to transit to stateName? | ||
- `transitions(): Array<string>` - possible transitions from current state | ||
- `onEnter(stateName: string, cb: (...args: Array<any>) => void): () => void` - add onEnter callback | ||
- `onLeave(stateName: string, cb: () => void): () => void` - add onLeave callback | ||
f.transitTo('loadingState'); | ||
console.log(f); | ||
f.transitTo('mainState'); | ||
console.log(f); | ||
// Переход в состояние с коллбэком, передаем сумму, например | ||
f.transitTo('paymentState', 1000); | ||
console.log(f); | ||
f.transitTo('successState'); | ||
console.log(f.transitions()); | ||
f.transitTo('mainState'); | ||
console.log(f); | ||
## Рекомендации | ||
- Не забывать описывать в своих классах-потомках конструктор и $next. | ||
- Делать адекватную цепочку состояний. | ||
Т.к. создание состояний реализовано с помощью наследования - результат применения какого-либо состояния является цепочкой последовательных мержей предыдущих состояний на родительское состояние машины. | ||
Проще говоря - в initial-состоянии опишите вообще все, что может меняться в контексте состояний и задайте этому дефолтные значения, а каждое новое состояние пусть что-то меняет в предыдущем. | ||
- Не надо делать параллельных ветвей состояний - между ними будет неудобно переходить. См картинку https://monosnap.com/file/KcASX734C1vNcibCd0pDUgpMLymSZo | ||
В этом примере есть три вкладки - release, attach и remove. Так вот для каждой лучше созать свою StateMachine, чем городить такое дерево, где нарисованы далеко не все переходы. | ||
## Recommendations | ||
- dont forget to call `rememberInitState` and declare `get $next` | ||
- Make an adequate chain of states. | ||
- New state can define only changed fields relative to parent state | ||
## TODO | ||
- Написать тесты | ||
- Написать examples | ||
- initial-состояние хранить так же как все остальные - избавиться от проверок на isInitial внутри реализации | ||
- Добавить в callback входа информацию о состоянии, из которого перешли | ||
- При сборке цепочки родительских состояний приделать проверку на циклы | ||
## Thanks | ||
The interface is peeked here: | ||
[JS FSM](https://github.com/jakesgordon/javascript-state-machine). | ||
## LICENCE | ||
MIT |
@@ -6,3 +6,3 @@ import { merge } from './utils/merge'; | ||
/** | ||
* @description изолированное хранилище внутренней информации конкретной StateMachine | ||
* @description isolated store for meta-information of concrete StateMachine | ||
*/ | ||
@@ -13,3 +13,3 @@ const StateMachineWeakMap: WeakMap<StateMachine, StateMachineInnerStore> = new WeakMap<StateMachine, StateMachineInnerStore>(); | ||
/** | ||
* @description константа с названием initial-состояния. | ||
* @description constant to store initial state name | ||
* @type {string} | ||
@@ -20,6 +20,6 @@ */ | ||
/** | ||
* @description Служебный статический метод, генерирующий текст ошибки, сообщающей о невозможности перейти в состояние | ||
* @param currentState - из какого состояния не смогли перейти | ||
* @param stateName - в какой состяоние не смогли перейти | ||
* @returns string - сообщение об ошибке | ||
* @description static service method for generate error text about unable transit to | ||
* @param currentState - from what state cant transit | ||
* @param stateName - to what state cant transit | ||
* @returns string - message | ||
*/ | ||
@@ -31,6 +31,10 @@ private static NEXT_STATE_RESTRICTED(currentState: string, stateName: string): string { | ||
/** | ||
* @description Служебный статический декоратор, прячет декорированный метод от перебора в цикле for-in | ||
* @description Static service decorator for hiding property/method in for-in | ||
*/ | ||
static hide(_target: object, _key: string, descriptor: PropertyDescriptor): PropertyDescriptor { | ||
descriptor.enumerable = false; | ||
if (descriptor) { | ||
descriptor.enumerable = false; | ||
} else { | ||
descriptor = { enumerable: false, configurable: true }; | ||
} | ||
return descriptor; | ||
@@ -40,6 +44,6 @@ } | ||
/** | ||
* @description Служебный статичный декоратор, делает наследование состояния. | ||
* Название декорируемого свойства класса будет названием регистрируемого сосотояния | ||
* @param parentState - имя родительского сосотояния(от которого наследуемся) | ||
* @param to - массив/строка состояний/состояния, в которые/которое можно перейти из данного состояния. | ||
* @description Static service decorator - make state inheritance | ||
* Name of decorated property becomes as state name | ||
* @param parentState - name of parent state | ||
* @param to - states in which we can transit from them state | ||
*/ | ||
@@ -51,3 +55,3 @@ static extend(parentState: string, to: string | Array<string> = []): (target: object, stateName: string) => void { | ||
/** | ||
* @description Получить хранилище внутренней информации для данного экземпляра StateMachine | ||
* @description Receive store of inner information for this instance of StateMachine | ||
*/ | ||
@@ -66,3 +70,3 @@ @StateMachine.hide | ||
/** | ||
* @description Массив состояний, в которые можно перейти из 'initial' | ||
* @description Array of states in which machine can transit from initial | ||
*/ | ||
@@ -75,3 +79,3 @@ @StateMachine.hide | ||
/** | ||
* @description Служебный метод для получения прототипа текущего экземпляра StateMachine. Нужен для извлечения метаданных | ||
* @description Service method for get prototype of current instance | ||
*/ | ||
@@ -84,4 +88,3 @@ @StateMachine.hide | ||
/** | ||
* @description Служебный метод для получения метаданных о состоянии stateName | ||
* @param stateName - название состояния | ||
* @description Service method for get metadata for state | ||
*/ | ||
@@ -94,15 +97,13 @@ @StateMachine.hide | ||
/** | ||
* @description Метод для смены состояния StateMachine в targetState. | ||
* Проверяет что оно зарегистрировано, что в него можно перейти из текущего состояния и если ок - переходит. | ||
* @param targetState - название состояния, в которое нужно перейти | ||
* @param args - любые данные, которые будут проброшены в onEnter-callback при входе в состояние | ||
* @description Method for transit machine to another state | ||
* Check the target state is registered, check transition is possible | ||
* @param targetState - name of state to transit | ||
* @param args - any data for pass to onEnter callback | ||
*/ | ||
@StateMachine.hide | ||
transitTo(targetState: string, ...args: Array<any>): void { | ||
// Проверить, что нужное состояние зарегистрировано | ||
// Check target state is registered | ||
const stateToApply = targetState !== 'initial' ? this[targetState] : this.$store.initialState; | ||
if (!stateToApply) { | ||
// Здесь и далее - просто напишу ошибку в консоль и выйду из метода. | ||
// Сделано это затем, что если метод вызвать внутри коллбэков промиса - промис словит эту ошибку и промолчит. | ||
// А ошибка по сути служебная, только для разработчика, что он забыл что-то описать или опечатался | ||
// Here and next - simply write error to console and return | ||
console.error(`No state '${targetState}' for navigation registered`); | ||
@@ -112,5 +113,5 @@ return; | ||
// Проверим, что можем совершить переход в нужное состояние | ||
// Check transition is possible | ||
if (this.$store.isInitialState) { | ||
// У initial-состояния допуски для перехода хранятся в $next | ||
// initial state store next on $next | ||
if (!this.$next.includes(targetState)) { | ||
@@ -121,3 +122,3 @@ console.error(StateMachine.NEXT_STATE_RESTRICTED(this.$store.currentState, targetState)); | ||
} else { | ||
// У других состояний допуски хранятся в их метаданных | ||
// another states store next in them metadata | ||
const currentStateProps: StateMachineMetadata = this.getMetadataByName(this.$store.currentState); | ||
@@ -131,3 +132,3 @@ const to: Array<string> = currentStateProps.to; | ||
// Т.к. состояния не "чистые", а наследуемые - применять любое состояние буду от initial до требуемого. Использую стек, LIFO | ||
// Make chain of states | ||
const stateChain: Array<any> = [stateToApply]; | ||
@@ -146,6 +147,6 @@ | ||
// Вызввать все коллбэки при выходе из состояния | ||
// Call onLeave callbacks | ||
this.$store.callLeaveCbs(); | ||
// Применяем стек состояний | ||
// Apply states chain | ||
merge(this, this.$store.initialState); | ||
@@ -157,6 +158,5 @@ while (stateChain.length) { | ||
// Вызвать все коллбэки при входе в состояние | ||
// Call all onEnter callbacks | ||
this.$store.callEnterCbs(targetState, args); | ||
// Записываем, что пришли в состояние | ||
this.$store.currentState = targetState; | ||
@@ -166,4 +166,4 @@ } | ||
/** | ||
* @description Служебный метод, который обязательно вызывать в конструкторе класса-потомка | ||
* для создания слепка начального состояния StateMachine. | ||
* @description Service method. Required to call in constructor of child-class | ||
* for create a snapshot of initial state | ||
*/ | ||
@@ -179,7 +179,2 @@ @StateMachine.hide | ||
/** | ||
* @description Метод, регистрирующий коллбэк cb для ВХОДА в состяоние stateName | ||
* @param stateName - название состояния | ||
* @param cb - коллбэк | ||
*/ | ||
@StateMachine.hide | ||
@@ -190,7 +185,2 @@ onEnter(stateName: string, cb: (...args: Array<any>) => void): () => void { | ||
/** | ||
* @description Метод, регистрирующий коллбэк cb для ВЫХОДА из состояния stateName | ||
* @param stateName - название состояния | ||
* @param cb - коллбэк | ||
*/ | ||
@StateMachine.hide | ||
@@ -202,3 +192,3 @@ onLeave(stateName: string, cb: () => void): () => void { | ||
/** | ||
* @description Название текущего состояния StateMachine | ||
* @description getter for current state name | ||
*/ | ||
@@ -210,6 +200,2 @@ @StateMachine.hide | ||
/** | ||
* @description Проверка, находится-ли машина в состоянии stateName | ||
* @param stateName - название проверяемого состояния | ||
*/ | ||
@StateMachine.hide | ||
@@ -220,7 +206,2 @@ is(stateName: string): boolean { | ||
/** | ||
* @description Проверка что машина может перейти в состояние stateName из текущего | ||
* @param stateName - название целевого состояния | ||
* @returns {boolean} | ||
*/ | ||
@StateMachine.hide | ||
@@ -235,6 +216,2 @@ can(stateName: string): boolean { | ||
/** | ||
* @description Получить список состояний, в которые машина может перейти из текущего | ||
* @return {Array<string>} | ||
*/ | ||
@StateMachine.hide | ||
@@ -241,0 +218,0 @@ transitions(): Array<string> { |
import { merge } from './utils/merge'; | ||
/** | ||
* Хранилище внутренней информации для конкретной StateMachine. | ||
* Все методы и свойства этого класса используются только в родительском классе StateMachine и никакие потомки сюда доступа не имеют | ||
* Store for inner meta-information for concrete StateMachine. | ||
* All methods and properties of this class used only in parent StateMachine class and no one child statemachine no access to it | ||
*/ | ||
export class StateMachineInnerStore { | ||
/** | ||
* @description Хранит начальное состояние машины | ||
* @description Store initial state | ||
*/ | ||
@@ -14,3 +14,3 @@ private $initialState: Record<string, any> = {}; | ||
/** | ||
* @description Название текущего состояния машины. Начальное - initial | ||
* @description name of current state | ||
*/ | ||
@@ -20,4 +20,4 @@ public currentState: string = 'initial'; | ||
/** | ||
* @description - key-value-хранилище коллбэков, которые будут работать при ВХОДЕ в состояние. | ||
* Ключ - название состояния. Значение - массив с функциями | ||
* @description - key-value-store for onEnter callbacks | ||
* key - state name, value - array with callbacks | ||
*/ | ||
@@ -27,4 +27,4 @@ private onEnterCbs: Record<string, Array<(...args: Array<any>) => void>> = {}; | ||
/** | ||
* @description - key-value-хранилище коллбэков, которые будут работать при ВЫХОДЕ из состояния. | ||
* Ключ - название состояния. Значение - массив с функциями | ||
* @description - key-value-store for onLeave callbacks | ||
* key - state name, value - array with callbacks | ||
*/ | ||
@@ -34,12 +34,8 @@ private onLeaveCbs: Record<string, Array<() => void>> = {}; | ||
/** | ||
* @description метод, сохраняющий начальное ключа во внутренний объект $initialState. | ||
* Вызовы этого метода собирают чистое initial-состояние | ||
* @param key - ключ | ||
* @param value - значение | ||
* @description store initial value of property to $initialState | ||
*/ | ||
rememberInitialKey(key: string, value: any): void { | ||
// Здесь важно порвать ссылки с полями машины. | ||
// Было this.$initialState[key] = value. Если value не был примитивом - он сохранялся, очевидно, по ссылке. | ||
// И в случае его изменения - он изменялся и в initialState, что влекло за собой бардак. | ||
// Можно было использовать _.cloneDeep после запоминания всех ключей, но разницы имхо никакой кроме порождения лишнего метода | ||
// Here is important to break links with statemachine properties. | ||
// If value wasn`t primitive type - they save by link | ||
// And if we change them - initialState change too. | ||
const assignable: Record<string, any> = {}; | ||
@@ -51,5 +47,2 @@ assignable[key] = value; | ||
/** | ||
* @description Начальное состояние машины | ||
*/ | ||
get initialState(): object { | ||
@@ -59,5 +52,2 @@ return this.$initialState; | ||
/** | ||
* @description Находится-ли машина в начальном состоянии? | ||
*/ | ||
get isInitialState(): boolean { | ||
@@ -68,6 +58,3 @@ return this.currentState === 'initial'; | ||
/** | ||
* @description Регистрирует коллбэк cb, который вызовется при входе в состояние stateName | ||
* @param stateName - состояние, при входе в которое вызвать коллбэк | ||
* @param cb - коллбэк | ||
* @returns {()=>void} - функция удаления созданного коллбэка | ||
* @description register onEnter callback, return function for drop callback | ||
*/ | ||
@@ -84,6 +71,3 @@ registerEnterCallback(stateName: string, cb: () => void): () => void { | ||
/** | ||
* @description Регистрирует коллбэк cb, который вызовется при выходе из состояния stateName. | ||
* @param stateName - состояние, при выходе из которого вызвать коллбэк | ||
* @param cb - коллбэк | ||
* @returns {()=>void} - функция удаления созданного коллбэка | ||
* @description register onLeave callback, return function for drop callback | ||
*/ | ||
@@ -99,7 +83,2 @@ registerLeaveCallback(stateName: string, cb: () => void): () => void { | ||
/** | ||
* @description Вызвать все коллбэки, зарегистрированные на вход в состояние stateName | ||
* @param stateName - имя состояния | ||
* @param args - возможные аргументы, переданные при переходе в состояние. Они попадут в коллбэк | ||
*/ | ||
callEnterCbs(stateName: string, args?: Array<any>): void { | ||
@@ -111,5 +90,2 @@ if (this.onEnterCbs[stateName]) { | ||
/** | ||
* @description Вызывать все коллбэки по выходу из текущего состояния | ||
*/ | ||
callLeaveCbs(): void { | ||
@@ -116,0 +92,0 @@ const stateName = this.currentState; |
@@ -1,2 +0,2 @@ | ||
const StateMachineMetadataKey = 'Tochka_StateMachineMetadata'; | ||
const StateMachineMetadataKey = 'TStateMachineMetadata'; | ||
@@ -3,0 +3,0 @@ /** |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
208
52755
16
1093