-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsmallfsm.js
172 lines (155 loc) · 4.54 KB
/
smallfsm.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/**
Copyright 2011 by Greg Reimer
Released under the MIT license.
https://github.com/greim/smallfsm
=================================
JavaScript finite state machine (FSM) library that uses an event-driven pattern
for state transitions.
@param startingState - Which state the machine starts in.
*/
var SmallFSM = (function(){
/**
@constructor - Returns an FSM instance.
*/
return function(startingState){
var FSM = {};
if (!startingState){
throw new Error('no starting state provided');
}
var stateHist = []; // private var which tracks current state
var states = {};
var transitions = {}; // stores valid state transition
var custEvents = {}; // stores custom events
var beginFunc; // may contain a func that runs at startup
var begun = false;
var sep = '!';
function emitCustom(eventName, eventObj){
var evq = custEvents[eventName];
if (evq) {
for (var i=0; i<evq.length; i++) {
evq[i](eventObj);
}
}
};
// external api
/**
@method - Optionally function to set a callback to run when the
machione starts.
*/
FSM.onBegin = function(f){
beginFunc = f;
return FSM;
};
/**
@method - Starts the machine.
*/
FSM.begin = function(){
if (!begun) {
states[startingState]=true;
stateHist.push(startingState);
if (beginFunc) {beginFunc();}
begun = true;
}
return FSM;
};
/**
@method - Gets the current state of the machine.
*/
FSM.getState = function(){
return stateHist[stateHist.length-1];
};
/**
@method - Sets a handler for for a given transition. Also causes the machine
to remember this as an allowed transition.
@param transition - A string like this "stateA => stateB" where stateA and
stateB are states given in the constructor.
@param action - The function to be executed.
@param events - String containing space-separated list of custom events
to be emitted when this transition occurrs.
*/
FSM.onTransit = FSM.allowTransit = function(transition, action, events, clobberable){
if (transition.indexOf(sep)!=-1){
throw new Error('transition string cannot contain "'+sep+'" character');
}
var tr = transition.split(/\s*=>\s*/);
// clean up transition list and store the state
for (var i=0;i<tr.length;i++){
tr[i] = tr[i].replace(/^\s\s*/,'').replace(/\s\s*$/,'');
states[tr[i]]=true;
}
// build string to internally store representation of transition
var trStr = tr.join(sep);
// populate all two-length transitions comprising transitions
// of three or more but make them clobberable
if (tr.length > 2) {
for (var i=1;i<tr.length;i++){
var newTransition = tr[i-1]+' => '+tr[i];
FSM.onTransit(newTransition, null, null, true);
}
}
var exst = !!transitions[trStr], // this transition already exists
clb = !!clobberable, // whether transition being added wants to be clobberable
write = !exst || (exst && !clb), // whether to write/overwrite this transition
err = exst && !clb && !transitions[trStr].clb; // whether to throw an error
if (write) {
transitions[trStr] = {
pattern: new RegExp('(^|'+sep+')'+trStr+'$'),
events: events?events.split(/\s+/):[],
action: action,
clb:!!clobberable
};
}
if (err) {
var errMsg = 'cannot overwrite "'+transition
+'" transition, already exists'
throw new Error(errMsg);
}
return FSM;
};
/**
@method - Sets a handler for a custom event.
*/
FSM.on = function(event, action){
var evq = custEvents[event];
if (!evq) { custEvents[event] = evq = []; }
evq.push(action);
return FSM;
};
/**
@method - Pushes the machine into a new state.
@throws Error - If the state wasn't declared in the constructor or if
it isn't an allowed transition.
*/
FSM.transit = function(toState, event){
if (!begun) { FSM.begin(); }
event = event || {};
var matchFound=false;
hStr = stateHist.join(sep)+sep+toState;
for (var trs in transitions) {
if(!transitions.hasOwnProperty(trs)){continue;}
var tr=transitions[trs];
if (tr.pattern.test(hStr)){
matchFound=true;
if (tr.action) { tr.action(event); }
for (var i=0;i<tr.events.length;i++){
emitCustom(tr.events[i],event);
}
}
}
if(!matchFound){
if(!states[toState]) {
throw new Error('"'+toState+'" is not a valid state');
} else {
throw new Error('"'+FSM.getState()+' => '+toState+'" is not a valid transition');
}
} else {
stateHist.push(toState);
if (stateHist.length > 64) {
stateHist.splice(0,8);
}
}
return FSM;
};
return FSM;
};
})();