Last active
April 16, 2020 05:18
-
-
Save aronanda/f593d683070fa20816bbe78419489d1a to your computer and use it in GitHub Desktop.
Fine State Machine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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