Skip to content

Instantly share code, notes, and snippets.

@GoodNovember
Created March 4, 2020 23:29
Show Gist options
  • Save GoodNovember/dd5fa109bc6a4bdd621216a30bc00754 to your computer and use it in GitHub Desktop.
Save GoodNovember/dd5fa109bc6a4bdd621216a30bc00754 to your computer and use it in GitHub Desktop.
Custom Finite State Machine Stuff
// see: https://kentcdodds.com/blog/implementing-a-simple-state-machine-library-in-javascript
// mine is different, but that's where I got the original logic.
// is this an object?
const isObject = something => {
if(typeof something === 'object' && something){
return true
}else{
return false
}
}
// is this a function?
const isFunction = something => {
if(typeof something === 'function'){
return true
}else{
return false
}
}
// transform a regular old object into a Map
const objectToMap = plainObject => {
const internalMap = new Map()
if(isObject(plainObject)){
const keys = Object.keys(plainObject)
for(const key of keys){
internalMap.set(key, plainObject[key])
}
}
return internalMap
}
// create a potential machine state.
const createState = ({ actions, transitions }) => {
const possibleTransitionsMap = objectToMap(transitions)
const possibleActionsMap = objectToMap(actions)
const canTransitionTo = transitionName => {
return possibleTransitionsMap.has(transitionName)
}
const canPerformAction = actionName => {
return possibleActionsMap.has(actionName)
}
return {
actions,
transitions,
canTransitionTo,
canPerformAction
}
}
// create a transition from a state.
const createTransition = ({ target, action }) => {
return {
target,
action
}
}
// create a finite state machine
const createMachine = ({
initialState,
...otherStates
}) => {
let possibleStateMap = objectToMap(otherStates)
let internalStateName = null
if(possibleStateMap.has(initialState)){
internalStateName = initialState
}else{
console.error(`createMachine Error: The initialState provided: "${initialState}" does not match any of the possible states in this machine.`)
}
const transition = (currentStateName, transitionName) => {
if(possibleStateMap.has(currentStateName)){
const currentStateInstance = possibleStateMap.get(currentStateName)
if(currentStateInstance.canTransitionTo(transitionName)){
const { target, action } = currentStateInstance.transitions[transitionName]
if (possibleStateMap.has(target)){
const destinationState = possibleStateMap.get(target)
if(isFunction(action)){
action()
}
if(currentStateInstance.canPerformAction('onExit')){
currentStateInstance.actions.onExit()
}
if(destinationState.canPerformAction('onEnter')){
destinationState.actions.onEnter()
}
internalStateName = target
}else{
console.error(`Transition Error: Failed to transition as there is no state with the name "${target}".`)
}
}else{
console.error(`Transition Error: The State: "${currentStateName}" cannot perform transition: "${transitionName}".`)
}
} else {
console.error(`Transition Error: There is not a state in this machine with the name: "${currentStateName}".`)
}
return internalStateName
}
return {
get state(){
return internalStateName
},
transition
}
}
// an example state machine.
const machine = createMachine({
initialState: 'off',
off: createState({
actions:{
onEnter(){
console.log('off: onEnter')
},
onExit(){
console.log('off: onExit')
}
},
transitions:{
toggle: createTransition({
target: 'on',
action(){
console.log('Transition action for "toggle" while in the "off" state moving to the "on" state.')
}
})
}
}),
on: createState({
actions:{
onEnter(){
console.log('on: onEnter')
},
onExit(){
console.log('on: onExit')
}
},
transitions:{
toggle:createTransition({
target: 'off',
action(){
console.log('Transition action for "toggle" while in the "on" state moving to the "off" state.')
}
})
}
})
})
let state = machine.state
console.log(`current state: ${state}`)
state = machine.transition(state, 'toggle')
console.log(`current state: ${state}`)
state = machine.transition(state, 'toggle')
console.log(`current state: ${state}`)
console.log('--- --- ---')
const machine2 = createMachine({
initialState: 'unplugged',
unplugged: createState({
actions:{
onEnter(){
console.log('The device is now unplugged. Try "plugIn" to make this device more useful.')
}
},
transitions:{
plugIn: createTransition({
target: 'pluggedIn',
action(){
console.log('Plugging in the device...')
}
})
}
}),
pluggedIn: createState({
actions:{
onEnter(){
console.log('The device is now plugged in and waiting try "startShow".')
}
},
transitions:{
unplug: createTransition({
target: 'unplugged',
action(){
console.log('Yanking the power cable.')
}
}),
startShow: createTransition({
target: 'showStarted',
action(){
console.log('Starting the Light Show!')
}
})
}
}),
showStarted: createState({
actions:{
onEnter(){
console.log('You see a beautiful lightshow begin to dance around the stage.')
}
},
transitions:{
unplug: createTransition({
target: 'unplugged',
action(){
console.log('Yanking the power cable.')
console.log('The light show suddenly shuts off abruptly.')
}
}),
stopShow: createTransition({
target: 'pluggedIn',
action(){
console.log('The light show gracefully fades out.')
}
})
}
})
})
let state2 = machine2.state
console.log('m2: ', state2)
state2 = machine2.transition(state2, 'plugIn')
console.log('m2: ', state2)
state2 = machine2.transition(state2, 'unplug')
console.log('m2: ', state2)
state2 = machine2.transition(state2, 'plugIn')
console.log('m2: ', state2)
state2 = machine2.transition(state2, 'startShow')
console.log('m2: ', state2)
state2 = machine2.transition(state2, 'unplug')
console.log('m2: ', state2)
state2 = machine2.transition(state2, 'plugIn')
console.log('m2: ', state2)
state2 = machine2.transition(state2, 'startShow')
console.log('m2: ', state2)
state2 = machine2.transition(state2, 'stopShow')
console.log('m2: ', state2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment