Skip to content

Instantly share code, notes, and snippets.

@aronanda
Last active April 16, 2020 05:18
Show Gist options
  • Save aronanda/f593d683070fa20816bbe78419489d1a to your computer and use it in GitHub Desktop.
Save aronanda/f593d683070fa20816bbe78419489d1a to your computer and use it in GitHub Desktop.
Fine State Machine
const EventEmitter = require('events').EventEmitter
class PrivateStateMachine {
constructor(sm, opts = {}) {
this.state = opts.state
Object.defineProperty(this, 'sm', { value: sm })
Object.defineProperty(this, 'stateKey', { value: opts.stateKey || 'state' })
this._setData(opts.data)
this._setStates(opts)
Object.defineProperty(this.sm, this.stateKey, { value: this.state, enumerable: true, configurable: true })
this.goto(this.state)
}
_setData(data) {
data = data || {}
if (isFn(data))
data = data(this.sm)
if (this.sm instanceof EventEmitter) {
const self = this
data = new Proxy(data, {
get (obj, prop) {
if (!_.isString(prop) || [ 'inspect', 'valueOf' ].includes(prop) || _.isFunction(obj[prop]))
return Reflect.get(...arguments)
if (prop !== self.stateKey)
self.sm.emit('get', { key: prop })
return Reflect.get(...arguments)
},
set (obj, prop, value) {
if (!isFn(value) && prop !== self.stateKey)
self.sm.emit('set', { key: prop, prev: obj[prop], next: value })
return Reflect.set(...arguments)
}
})
}
Object.defineProperty(this, 'data', { value: data, enumerable: true })
}
_setStates(opts = {}) {
const isEvent = this.sm instanceof EventEmitter
const states = _.assign(new Map, opts.states)
// create public context (don't allow actions to set state)
const publicCtx = {
state: new Proxy(this.data, {
set (obj, prop, value) {
log('you cannot do that')
return {}
}
})
}
Object.defineProperty(publicCtx, 'dispatch', { value: this.dispatch.bind(this), enumerable: true })
Object.defineProperty(publicCtx, 'goto', { value: this.goto.bind(this), enumerable: true })
// private context can only commit or set state directly
const privateCtx = { state: this.data }
Object.defineProperty(privateCtx, 'commit', { value: this.commit.bind(this), enumerable: true })
// bind contexts to states
for (let state in states) {
let obj = states[state]
// mutators
let mutators = _.assign({}, opts.mutators, obj.mutators)
obj.mutators = {}
for (let name in mutators) {
obj.mutators[name] = (...args) => {
if (isEvent)
this.sm.emit('commit', { name, args })
mutators[name].call(privateCtx, privateCtx, ...args)
}
}
// actions
let actions = _.assign({}, opts.actions, obj.actions)
obj.actions = {}
for (let name in actions) {
obj.actions[name] = (...args) => {
if (isEvent)
this.sm.emit('dispatch', { name, args })
actions[name].call(publicCtx, publicCtx, ...args)
}
}
// methods
let methods = _.assign({}, opts.methods, obj.methods)
obj.methods = {}
for (let name in methods) {
obj.methods[name] = (...args) => {
if (isEvent)
this.sm.emit('method', { name, args })
methods[name].call(privateCtx, privateCtx, ...args)
}
}
// enters
if (obj.enter)
obj.enter = this._parseTerminator(obj.enter, privateCtx)
if (obj.exit)
obj.exit = this._parseTerminator(obj.exit, privateCtx)
}
Object.defineProperty(this, 'states', { value: states, enumerable: true })
// set initial state as first key of states object
if (!this.state)
this.state = _.keys(this.states)[0]
}
_parseTerminator(input, ctx) {
let fn
if (isFn(input)) {
fn = input.bind(ctx, ctx)
} else if (isObj(input)) {
fn = () => {
for (const name in input) {
const _fn = input[name]
if (!isFn(_fn))
continue
_fn.call(ctx, ctx)
}
}
} else if (_.isArray(input)) {
fn = () => {
for (const _fn of input) {
if (!isFn(_fn))
continue
_fn.call(ctx, ctx)
}
}
}
return fn
}
goto(state) {
if (!this.states[state])
return false
const previous = this.state
const prev = this.states[previous]
const curr = this.states[state]
this.state = state
Object.defineProperty(this.sm, this.stateKey, { writable: true })
this.sm[this.stateKey] = state
Object.defineProperty(this.sm, this.stateKey, { writable: false })
for (let key in this.data) {
if (!this.data[key])
Object.defineProperty(this.sm, key, { configurable: true, enumerable: true, writable: true })
Object.defineProperty(this.sm, key, { writable: true })
Object.defineProperty(this.sm, key, { value: this.data[key] })
}
const prevActions = prev.actions || {}
for (let name in prevActions)
delete this.sm[name]
const currActions = curr.actions || {}
for (let name in currActions)
Object.defineProperty(this.sm, name, { value: currActions[name], configurable: true, enumerable: true })
if (prev.exit && previous !== state)
prev.exit()
if (curr.enter)
curr.enter()
for (let key in this.data) {
Object.defineProperty(this.sm, key, { writable: true })
Object.defineProperty(this.sm, key, { value: this.data[key] })
Object.defineProperty(this.sm, key, { writable: false })
}
}
commit(mutator, payload) {
let obj = this.states[this.state]
if (!obj)
return false
let fn = obj.mutators[mutator]
if (!isFn(fn))
return false
return fn(payload)
}
dispatch(action, payload) {
let obj = this.states[this.state]
if (!obj)
return false
let fn = obj.actions[action]
if (!isFn(fn))
return false
return fn(payload)
}
static spawn(sm, opts) {
new this(sm, opts)
return sm
}
}
module.exports = new Proxy(class StateMachine extends EventEmitter {}, {
construct (target, args, self) {
return PrivateStateMachine.spawn(Reflect.construct(target, args, self), args[0])
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment