UNPKG

st8flo

Version:
1,953 lines (1,540 loc) 60.5 kB
(function (root, factory) { //Environment Detection if (typeof define === 'function' && define.amd) { //AMD define([], factory); } else if (typeof exports === 'object') { //CommonJS module.exports = factory(); } else { // Script tag import i.e., IIFE root.St8 = factory(); } }(this, function () { 'use strict'; /*------------- Class St8Collection ------------------ Used to store and manipulate over a bunch of parallel children states. Must be passed an array of St8 objects --------------------------------------------------*/ function St8Collection(p) { if (p && !Array.isArray(p)) { throw Error('input must be an array'); } if (!this.pipeColl) this.pipeColl = {}; if (p) { var _self = this; p.forEach(function (item) { if (item instanceof FlowSt8) { _self.pipeColl[item.__st8.name] = item; } }); } this.type = St8.types.COLLECTION; return this; } St8Collection.prototype.toArray = function () { var keys = Object.keys(this.pipeColl); var array = Array(keys.length); for (var i = 0; i < keys.length; i++) { array[i] = this.pipeColl[keys[i]]; } return array; }; St8Collection.prototype.__ = function (name) { return this.pipeColl[name] || null; }; St8Collection.prototype.flush = function () { this.pipeColl = {}; return this; }; St8Collection.prototype.parent = function () { var obj = this.pipeColl; return obj[Object.keys(obj)[0]].parent(); }; St8Collection.prototype.root = function () { return FlowSt8.prototype.root.call(this); }; St8Collection.prototype.end = function () { return FlowSt8.prototype.end.call(this); }; // terminal name is optional St8Collection.prototype.terminal = function () { throw Error('Flow must end with a single St8, not a collection. Please select one first and then .terminal()'); }; St8Collection.prototype.exportStructure = function (options) { options = options || {}; var o = {}; var _self = this; Object.keys(_self.pipeColl).forEach(function (key) { o[key] = _self.pipeColl[key].exportStructure(options); }); return o; }; /*-------------- Class St8Flo --------------------- Represents a collection of interlinked states Has 1 root, but many terminals, one of which needs to be selected, to continue linking to the flow. */ function St8Flo(fName, options) { if (St8.st8Data.flows[fName]) { return St8.st8Data.flows[fName]; } this.parents = {}; this.type = St8.types.FLOW; this.name = fName; this.setRoot(new FlowSt8(fName)); // Must be a St8Obj type this.activeParent = null; this.terminals = null; // Must be a St8Collection type var _self = this; this.trigger = function (triggerData, options) { // console.log('st8flo() - Triggering Flow [%s]. this - ', _self.name); if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOW_TRIGGERED, { state: { flow: _self.exportState() } }); } if (_self._root && _self._root instanceof FlowSt8) { if (options && options.st8Obj) _self.activeParent = options.st8Obj; _self._root.__st8.dinnerServed = true; return _self._root.trigger(St8.Data(triggerData), options); } else { throw Error('Root St8 is not set on Flow'); } }; St8.st8Data.flows[fName] = this; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOW_CREATE, { structure: { flow: _self.exportStructure({ noChildren: true }) } }); } return this; } St8Flo.prototype.start = function () { return this._root; }; St8Flo.prototype.setDefaultFeed = function () { this._root.__st8.feedFn = FlowSt8.prototype.feedFn.bind(this); }; St8Flo.prototype.setRoot = function (st8Obj) { if (st8Obj instanceof FlowSt8) { this._root = st8Obj; this._root.__st8.flow = this; this._root.__st8.config.isFlowRoot = true; this.__st8 = this._root.__st8; this.setDefaultFeed(); } return this; }; St8Flo.prototype.setAsTerminal = function (st8Obj, tName) { if (st8Obj instanceof FlowSt8) { if (!this.terminals) this.terminals = new St8Collection(); this.terminals.pipeColl[tName || st8Obj.__st8.name] = st8Obj; } return this; }; St8Flo.prototype.__ = function (name) { if (this.terminals && this.terminals instanceof St8Collection) { return this.terminals.__(name); } else { throw Error('Flow has no terminals of type St8Collection, to select from'); } }; St8Flo.prototype.setFeed = function (fn) { console.log('setFeed called for this = ', this); if (typeof fn == 'function') { this._root.__st8.feedCB = fn; this._root.feedFn = function () { var st8Obj = this; this.__st8.feeding = true; return Promise.resolve(st8Obj.__st8.feedCB()) .then(function (res) { st8Obj.__st8.input = St8.Data(res); st8Obj.__st8.feeding = false; st8Obj.setState(St8.es.TASTING); setTimeout(st8Obj._trigger, 0); }); }; } }; St8Flo.prototype.setSkip = function (fn) { if (typeof fn == 'function') { this._root.__st8.skipCB = fn; } }; St8Flo.prototype.setReject = function (fn) { if (typeof fn == 'function') { this._root.__st8.rejectCB = fn; } }; St8Flo.prototype.setAccept = function (fn) { if (typeof fn == 'function') { this._root.__st8.acceptCB = fn; } }; St8Flo.prototype.setNext = function (fn) { if (typeof fn == 'function') { this._root.__st8.nextCB = fn; } }; St8Flo.prototype.setDone = function (terminalName, fn) { if (terminalName && typeof fn == 'function') { this.__(terminalName).__st8.doneCB = fn; } }; St8Flo.prototype.autoFeedWhile = function (fn) { var flow = this; flow._root.__st8.doneCB = function () { // flow._root.__st8.dinnerServed = true; if (fn()) { setTimeout(flow.trigger, 0); } }; }; St8Flo.prototype.autoRepeat = function (initialData) { var flow = this; flow.setFeed(function(){ return flow._root.__st8.results; }); flow._root.__st8.doneCB = function(){ flow._root.__st8.dinnerServed = true; if(flow._root.__st8.config.oneShot){ flow._root.__st8.template.execState = St8.c.HOLD; } }; flow._root.__st8.results = initialData; setTimeout(flow.trigger, 0); flow._root.__st8.config.autoRepeat = true; this.oneShot = function(){ this._root.__st8.config.oneShot= true; }; this.continuous = function(){ this._root.__st8.config.oneShot= false; }; return this; }; // St8Flo.prototype.setPoop = function(terminalName, fn){ // // } St8Flo.prototype.delete = function (parentSt8) { this._root.detachParents(parentSt8); if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOW_PARENTDETACHED, { structure: { flow: this.name, parent: parentSt8.__st8.id } }); } if (Object.keys(this.parents).length === 0) { this._root.deleteChain(); this._root.delete(); delete St8.st8Data.flows[this.name]; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOW_DELETED, { structure: { flow: this.name } }); } } }; St8Flo.prototype.clear = function () { this._root.clearChain(); }; St8Flo.prototype.clone = function (name) { var cloned = new St8Flo(name); this._root.cloneChain(cloned); }; St8Flo.prototype.exportStructure = function (options) { options = options || {}; var _self = this; var r; r = { name: _self.name, type: _self.type, _root: _self._root.exportStructure(options), parents: (function () { var o = {}; Object.keys(_self.parents).forEach(function (key) { o[key] = _self.parents[key].__st8.id; }); return o; })(), activeParent: _self.activeParent ? _self.activeParent.__st8.id : null, terminals: _self.terminals ? _self.terminals.exportStructure(options) : null }; return r; }; St8Flo.prototype.exportState = function (options) { options = options || {}; var _self = this; var r; r = { name: _self.name, type: _self.type, activeParent: _self.activeParent ? _self.activeParent.__st8.id : null, }; return r; }; /*------------- Class St8Template ------------------ Common template defining behaviour across all states of the same name --------------------------------------------------*/ function St8Template(p) { this.name = p; this.conditions = { common: {}, input: {} }; this.conditionValues = { common: {}, input: {} }; this.type = St8.types.TEMPLATE; this.states = []; this.pendingStates = []; this.execState = St8.c.EXEC; this.fn = null; this.clearFn = null; this.registered = false; this.debouncedTrigger = null; var _self = this; this._trigger = function () { var marker, exec = true; _self.conditionValues.common = {}; Object.keys(_self.conditions.common).some(function (k) { marker = _.get(St8.st8Data.appMarkers, k); if(!marker.state[_self.name]) marker.state[_self.name] = {}; if (!marker || !marker.triggered || !(_self.conditions.common[k] === '*' || (typeof _self.conditions.common[k] === 'function' ? _self.conditions.common[k].call(marker.state[_self.name], marker) : marker.value === _self.conditions.common[k]))) { exec = false; _self.conditionValues.common[k] = false; return true; // exit loop prematurely. } else { _self.conditionValues.common[k] = true; } }); if (exec === true) { // trigger parent, move on to next execState, // st8Obj.__st8.execState = St8.es.DIGESTING; _self.execState = St8.c.EXEC; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.TEMPLATE_TRIGGERED, { state: { stateTemplate: _self.exportState() } }); } _self.pendingStates.forEach(function (st8Obj) { // setTimeout(st8Obj.trigger, 0, _self.triggerData); setTimeout(st8Obj.trigger, 0); }); _self.pendingStates = []; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.TEMPLATE_PENDINGCLEAR, { state: { stateTemplate: _self.name } }); } } else{ _self.execState = St8.c.HOLD; } }; this.debouncedTrigger = _.debounce(this._trigger, St8.config.constants.TRIGGER_DEBOUNCE_DELAY); this.trigger = function (triggerData) { // _self.triggerData = triggerData; _self.debouncedTrigger(); }; St8.st8Data.stateTemplates[p] = this; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.TEMPLATE_CREATE, { structure: { stateTemplate: _self.exportStructure({ noChildren: true }) } }); } return this; } St8Template.prototype.addState = function (st8Obj) { if (st8Obj && st8Obj.__st8 && this.states.findIndex(function (check) { return check.__st8.id == st8Obj.__st8.id; }) < 0) { this.states.push(st8Obj); } }; St8Template.prototype.removeState = function (st8Obj) { if (st8Obj && st8Obj.__st8) { var index = this.states.findIndex(function (check) { return check.__st8.id == st8Obj.__st8.id; }); if (index > -1) { this.states.splice(index, 1); } } }; St8Template.prototype.addPending = function (st8Obj) { if (st8Obj && st8Obj.__st8 && this.pendingStates.findIndex(function (check) { return check.__st8.id == st8Obj.__st8.id; }) < 0) { this.pendingStates.push(st8Obj); if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.TEMPLATE_PENDINGADDED, { state: { stateTemplate: this.name, st8Obj: st8Obj.__st8.id } }); } } }; St8Template.prototype.removePending = function (st8Obj) { if (st8Obj && st8Obj.__st8) { var index = this.pendingStates.findIndex(function (check) { return check.__st8.id == st8Obj.__st8.id; }); if(index > -1){ this.pendingStates.splice(index,1); } } }; St8Template.prototype.setFn = function (pFn, options) { if (typeof pFn == 'function' && ((options && options.replace) || !this.fn)) { this.fn = pFn; } }; // St8Template.prototype.setClearFn = function (pFn, options) { // if (typeof pFn == 'function' && ((options && options.replace) || !this.clearFn)) { // this.clearFn = pFn; // } // }; /* Sorts supplied condition markers, into common & input. Common ones are regular markers, while, input - have the structure "input<seperator><suffix>" - are conditions based on dataPipe input. if the separator is a '.', the suffix is meant to be treated as a path within the input and copied to the conditions . otherwise suffix is prepended with a "*" to represent an ignorable id. In this case, the RHS is evaluated against the entire input data, and the suffix is simply meant to keep LHS keys unique for tests conditions on the RHS. */ St8Template.prototype.setConditions = function (conditions, options) { var _self = this; if (!this.registered || (options && options.replace)) { if (typeof conditions == 'object') { Object.keys(conditions).forEach(function (cKey, i) { if (cKey.substring(0, 5) == 'input') { if (cKey.charAt(5) == '.') { _self.conditions.input[cKey.substring(6)] = conditions[cKey]; } else { _self.conditions.input['*' + i + cKey.substring(5)] = conditions[cKey]; } } else { _self.conditions.common[cKey] = conditions[cKey]; } }); } var a; // if (!options || !options.silent) console.log('\tRegistering markers for state[%s] ', _self.name); Object.keys(_self.conditions.common).forEach(function (k) { // if (!options || !options.silent) console.log('\t\t markers key ', k); a = St8.Marker(St8.resolveMarkerPath(_self, k)); // if (!options || !options.silent) console.log('\t\t resolved as ', a); if (a.deps.findIndex(function (check) { return check.name == _self.name; }) < 0) { a.deps.push(_self); } }); _self.registered = true; _self.execState = St8.c.HOLD; _self.trigger(); } }; St8Template.prototype.oneShot = function(){ this.config.oneShot = true; }; St8Template.prototype.continuous = function(){ this.config.oneShot = false; }; St8Template.prototype.exportStructure = function (options) { options = options || {}; var o = {}; var _self = this; o = { name: _self.name, type: _self.type, registered: _self.registered }; if (!options.short) { Object.assign(o, { conditions: _self.conditions, fn: _self.fn ? _self.fn.toString() : 'not yet set', states: (function () { return _self.states.map(function (e) { return e.exportStructure({ short: true, noChildren: true }); }); })() }); } return o; }; St8Template.prototype.exportState = function (options) { options = options || {}; var o = {}; var _self = this; o = { name: _self.name, type: _self.type, execState: _self.execState, conditionValues: _self.conditionValues, pendingStates: (function () { return _self.pendingStates.map(function (e) { return e.exportStructure({ short: true, noChildren: true }); }); })() }; return o; }; /*----------- Class St8 - Local state container for a path ------------ behaves as per template on local data contained in a data pipe calls the flowFn in template through the data pipe, not directly. */ function FlowSt8(p, options) { options = options || {}; if (!options || !options.silent) console.log('---> ST8FLO CONSTRUCTOR --> constructor called for p[%s] with options.parent [%s] ', p, options && options.parent ? options.parent.__st8.id : 'none'); /* If instructions are explicity set to create, create one using p as usual. */ if (options.createSt8) { this.__st8 = { name: '', id: '', type: St8.types.FLOWST8, parent: null, children: {}, input: St8.Data(), results: St8.Data(), triggered: false, registered: false, activated: false, disabled: false, stateIsSetup: false, digestingFn: null, execState: St8.es.HUNGRY, counts: { triggered: 0, activated: 0, }, config: { isFlowRoot: false } }; this.state = {}; switch (typeof p) { case 'string': // retrieve a template, or create a blank new one on the spot. // conditions & flow will be set on subsequent calls to .when() & .flo() var template = St8.st8Data.stateTemplates[p]; if (!template) { template = new St8Template(p); } var st8Obj = this; break; default: throw Error('First param is an id and must be a string'); } /*------------------------------------ Create "this" as a fresh st8Obj ------------------------------------*/ this.__st8.name = p; // this.__st8.id = id; this.__st8.template = template; this.__st8.dinnerServed = false; this._trigger = function () { // console.log('St8Obj [%s] _trigger called. Current state is [%s]', st8Obj.__st8.id, st8Obj.getState()); if(!st8Obj.__st8.disabled){ st8Obj.setupState(); var exec = true, cKeys; switch (st8Obj.__st8.execState) { case St8.es.HUNGRY: if(!st8Obj.__st8.config.bypassFeeding){ // GET INPUT if (!st8Obj.__st8.feeding && st8Obj.__st8.dinnerServed === true) { // .feeding check is reqd because feedFn could be async & currently executing from a previous trigger. st8Obj.feedFn(); } break; } case St8.es.TASTING: if(!st8Obj.__st8.config.bypassTasting){ // EVALUATE INPUT if (st8Obj.__st8.input === 'st8-EOD' || !st8Obj.evalInput()) { // Signal rejection if (st8Obj.__st8.rejectCB) { st8Obj.__st8.rejectCB.call(st8Obj, st8Obj.__st8.input.getData(), st8Obj.__st8.input); } // & break to wait for the next meal. st8Obj.__st8.dinnerServed = false; st8Obj.setState(St8.es.HUNGRY); if(st8Obj.__st8.nextCB && st8Obj.__st8.input!=='st8-EOD'){ st8Obj.__st8.nextCB.call(st8Obj); } break; } else { // continue processing the current meal... st8Obj.setState(St8.es.WFC); if (st8Obj.__st8.acceptCB) { st8Obj.__st8.acceptCB.call(st8Obj, st8Obj.__st8.input.getData(), st8Obj.__st8.input); } // & also signal that we're ready to accept the next st8Obj.__st8.dinnerServed = false; if (st8Obj.__st8.config.isFlowRoot && st8Obj.__st8.flow.activeParent) { setTimeout(st8Obj.__st8.flow.activeParent._trigger, 0); } else if (st8Obj.__st8.parent) { setTimeout(st8Obj.__st8.parent._trigger, 0); } if(st8Obj.__st8.nextCB && st8Obj.__st8.input!=='st8-EOD'){ st8Obj.__st8.nextCB.call(st8Obj); } } } case St8.es.WFC: // console.log('\t--> Evaluating state WFC for [%s]', st8Obj.__st8.id); // Check common conditions // If valid move to next, else break & wait here var markerKey, marker; exec = true; if (st8Obj.__st8.template.execState === St8.c.EXEC || st8Obj.__st8.input.getSystemSignal(St8.signals.WFC_BYPASS_ONCE) || st8Obj.__st8.input.getSystemSignal(St8.signals.WFC_BYPASS)) { // trigger parent, move on to next execState, // st8Obj.__st8.execState = St8.es.DIGESTING; st8Obj.setState(St8.es.DIGESTING); } // else register and break & wait here till conditions are right. else { st8Obj.__st8.template.addPending(st8Obj); break; } case St8.es.DIGESTING: // console.log('\t--> Evaluating state DIGESTING for [%s]', st8Obj.__st8.id); // Execute fn with supplied data. // Store promise into __st8.digestingFn - so that its not called again till execution is done. // Once done, move to next step OR if reject, ++ the error count, break & move to hungry state. if (!st8Obj.__st8.digestingFn) { if (typeof st8Obj.__st8.template.fn == 'function') { st8Obj.__st8.digestingFn = Promise.resolve(st8Obj.__st8.template.fn.call(st8Obj, st8Obj.__st8.input.getData(), st8Obj.__st8.input)) .then(function (res) { // st8Obj.__st8.results = res; /* expected behaviour - 1> should create a base St8Data template using input. 2> Overwrite it with data or signals (whichever present in res). */ st8Obj.__st8.results = st8Obj.__st8.input.assimilate(res); st8Obj.__st8.results.deleteSystemSignal(St8.signals.WFC_BYPASS_ONCE); // St8.Marker(st8Obj.__st8.name).set(st8Obj.__st8.results); Object.keys(st8Obj.__st8.children).forEach(function (cKey) { st8Obj.__st8.children[cKey].__st8.dinnerServed = true; setTimeout(st8Obj.__st8.children[cKey].trigger, 0, st8Obj.__st8.results, { retrigger: true, st8Obj: st8Obj }); }); // st8Obj.__st8.execState = St8.es.POOPING; st8Obj.setState(St8.es.POOPING); st8Obj.__st8.digestingFn = null; /* NOTE - Edge case. When there is a function & no children, there's nothing to _trigger the POOPING state check & transition to HUNGRY state, leaving this st8Obj in POOPING state, when the next data & _trigger call comes in, causing a miss. */ if (Object.keys(st8Obj.__st8.children).length === 0) { st8Obj._trigger(); } }) .catch(function (e) { st8Obj.__st8.digestingFn = null; if (typeof e != 'undefined' && e instanceof Error) { console.log('ST8FLO ERROR - while digesting St8[%s]. e = ', st8Obj.__st8.id, e); if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOWST8_DIGESTERROR, { state: { st8Obj: st8Obj.exportState({ noChildren: true }) }, message: e }); } } else { if (st8Obj.__st8.flowRoot) { if (st8Obj.__st8.flowRoot.__st8.skipCB) st8Obj.__st8.flowRoot.__st8.skipCB.call(st8Obj, st8Obj.__st8.input.getData(), st8Obj.__st8.input); } else if (st8Obj.__st8.skipCB) { st8Obj.__st8.skipCB.call(st8Obj, st8Obj.__st8.input.getData(), st8Obj.__st8.input); } st8Obj.setState(St8.es.HUNGRY); st8Obj.signalHungry(true); } }); break; } else { // console.log('--> St8flo - no St8 function found. forwarding parent input data as', st8Obj.__st8.input); st8Obj.__st8.results = st8Obj.__st8.input; st8Obj.__st8.results.deleteSystemSignal(St8.signals.WFC_BYPASS_ONCE); // St8.Marker(st8Obj.__st8.name).set(st8Obj.__st8.input); Object.keys(st8Obj.__st8.children).forEach(function (cKey) { st8Obj.__st8.children[cKey].__st8.dinnerServed = true; setTimeout(st8Obj.__st8.children[cKey].trigger, 0, st8Obj.__st8.results, { retrigger: true, st8Obj: st8Obj }); }); // st8Obj.__st8.execState = St8.es.POOPING; st8Obj.setState(St8.es.POOPING); st8Obj.__st8.digestingFn = null; // /* // NOTE - Edge case. // When there is a function & no children, there's nothing to _trigger the POOPING state check & transition to HUNGRY state, // leaving this st8Obj in POOPING state, when the next data & _trigger call comes in, causing a miss. // */ // if (Object.keys(st8Obj.__st8.children).length === 0) { // st8Obj._trigger(); // } // Above is not required in this case, because it automatically flows to pooping state as part of current exeution, since its not a promise.then() // Not so when the fn promise.then resolves/rejects though. Hence required above, but not here. } } else { break; } case St8.es.POOPING: // console.log('\t--> Evaluating state POOPING for [%s]', st8Obj.__st8.id); // if children exist, children should trigger this eval, else skip // Once all children are hungry again, move back to hungry state cKeys = Object.keys(st8Obj.__st8.children); if (cKeys.length > 0) { exec = true; cKeys.some(function (c) { if (st8Obj.__st8.children[c].__st8.dinnerServed === true) { exec = false; return true; } }); if (exec === true) { // st8Obj.__st8.execState = St8.es.HUNGRY; st8Obj.setState(St8.es.HUNGRY); st8Obj.signalHungry(true); } } else { // st8Obj.__st8.execState = St8.es.HUNGRY; st8Obj.setState(St8.es.HUNGRY); st8Obj.signalHungry(true); // signal back to parent, since this is a leaf child. } } } }; // st8Obj._trigger(); if (!st8Obj.__st8.debouncedTrigger) st8Obj.__st8.debouncedTrigger = _.debounce(st8Obj._trigger, St8.config.constants.TRIGGER_DEBOUNCE_DELAY); this.trigger = function (triggerData, pOptions) { st8Obj.__st8.triggerData = St8.Data(triggerData); st8Obj.__st8.debouncedTrigger(); }; if (options && options.parent && options.parent.__st8) { this.__st8.parent = options.parent; } if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOWST8_CREATE, { structure: { st8Obj: this.exportStructure({ noChildren: true }) } }); } return this; } /* Attempt to find & return the FlowSt8, or recursively create a new one if not found. */ else { var templateName, template, st8Obj; var lastDot = p.lastIndexOf('.'); if (lastDot > -1) { templateName = p.substring(lastDot + 1); } else { templateName = p; } template = St8.st8Data.stateTemplates[templateName]; if (template) { st8Obj = template.states.find(function (check) { return check.__st8.id == p; }); if (st8Obj) { if (st8Obj.__st8.stateIsSetup) { if (st8Obj.__st8.parent || Object.keys(st8Obj.__st8.children).length > 0) { console.log('St8flo - WARNING - retrieved St8Obj [%s], is already setup as a flow. Attempting to re-configure may have unexpected results', st8Obj.__st8.id); } else { console.log('St8flo - WARNING - retrieved St8Obj [%s], is already setup. Attempting to re-configure may have unexpected results', st8Obj.__st8.id); } } return st8Obj; } } // console.warn('St8flo - WARNING - St8Obj [%s] could not be found. \n\tAttempting to create a new [%s] St8Obj...', p, templateName); options.createSt8 = true; return new FlowSt8(templateName, options); } } /*---------- METHODS ---------*/ FlowSt8.prototype.evalInput = function (pSt8Obj) { var st8Obj = pSt8Obj || this; st8Obj.__st8.template.conditionValues.input = {}; var derived; var exec = true; // Check against input conditions. Object.keys(st8Obj.__st8.template.conditions.input).some(function (k) { if (k.charAt(0) == '*') { // k = Ignorable LHS, RHS test is on full input variable derived = st8Obj.__st8.input.getData(); } else { // k = path structure within input, that leads to the variable to test derived = _.get(st8Obj.__st8.input.getData(), k); } if (!derived || !(typeof st8Obj.__st8.template.conditions.input[k] === 'function' ? st8Obj.__st8.template.conditions.input[k].call(st8Obj, derived, st8Obj.__st8.input) : st8Obj.__st8.template.conditions.input[k] == derived)) { exec = false; st8Obj.__st8.template.conditionValues.input[k] = false; return true; // exit loop prematurely. } else { st8Obj.__st8.template.conditionValues.input[k] = true; } }); // // if input ok, continue to next // if (exec === true) { // // set next execState, and continue with the next switch statement case, // // st8Obj.__st8.execState = St8.es.WFC; // } // // // else break & stay at hungry, for hopefully, a new meal. // else { // // } return exec; }; FlowSt8.prototype.feedFn = function (pSt8Obj) { var st8Obj = pSt8Obj || this; st8Obj.__st8.feeding = true; if (st8Obj.__st8.parent) { // Grab parent Data. st8Obj.__st8.input = St8.Data(st8Obj.__st8.parent.__st8.results); } else { st8Obj.__st8.input = St8.Data(st8Obj.__st8.triggerData); st8Obj.__st8.triggerData = null; } st8Obj.__st8.feeding = false; st8Obj.setState(St8.es.TASTING); setTimeout(st8Obj._trigger, 0); // if (st8Obj.__st8.config.isFlowRoot) { // if (st8Obj.__st8.flow.activeParent) { // st8Obj.__st8.input = st8Obj.__st8.flow.activeParent.__st8.results; // // // signal flow active parent that we've received the serving. // st8Obj.__st8.dinnerServed = false; // setTimeout(st8Obj.__st8.flow.activeParent._trigger, 0); // } else { // st8Obj.__st8.input = st8Obj.__st8.triggerData; // st8Obj.__st8.triggerData = null; // // st8Obj.__st8.dinnerServed = false; // } // } else { // if (st8Obj.__st8.parent) { // // Grab parent Data. // st8Obj.__st8.input = st8Obj.__st8.parent.__st8.results; // // // signal parent that we've received the serving. // st8Obj.__st8.dinnerServed = false; // setTimeout(st8Obj.__st8.parent._trigger, 0); // } else { // st8Obj.__st8.input = st8Obj.__st8.triggerData; // st8Obj.__st8.triggerData = null; // // st8Obj.__st8.dinnerServed = false; // } // } }; FlowSt8.prototype.getState = function () { switch (this.__st8.execState) { case St8.es.HUNGRY: return "HUNGRY"; case St8.es.TASTING: return "TASTING"; case St8.es.WFC: return "WFC"; case St8.es.DIGESTING: return "DIGESTING"; case St8.es.POOPING: return "POOPING"; } }; FlowSt8.prototype.signalHungry = function (selfSignal) { console.log('St8Flo - signalling hungry for st8Obj[%s]', this.__st8.id); // this.__st8.input = null; if (this.__st8.doneCB) this.__st8.doneCB.call(this, this.__st8.results.getData(), this.__st8.results); // if (this.__st8.feedCB) // this.__st8.feedCB.call(this); // if(sigParent && this.__st8.parent){ // this.__st8.parent._trigger(); // } if (selfSignal) { var _self = this; setTimeout(_self._trigger, 0); } }; FlowSt8.prototype.bypassFeeding = function(){ this.__st8.config.bypassFeeding = true; this.__st8.config.bypassTasting = true; this.__st8.input = St8.Data({}); this.signalHungry = function(){}; }; FlowSt8.prototype.setState = function (state) { this.__st8.execState = state; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOWST8_STATECHANGE, { state: { st8Obj: this.exportState(), stateTemplate: this.__st8.template.exportState() } }); } // console.log('\t [%s] changing state to ', this.__st8.id, this.getState()); }; FlowSt8.prototype.resetState = function () { this.__st8.digestingFn = null; this.__st8.dinnerServed = false; this.__st8.setState(St8.es.HUNGRY); }; /* Till this method is run, the object is just a shell without any state related bindings Once run, the shell becomes a proper state container. Why ? - The shell should be used to define stateTemplates without adding new states. */ FlowSt8.prototype.setupState = function () { if (!this.__st8.stateIsSetup) { if (!this.__st8.id) { if (this.__st8.parent && this.__st8.parent.__st8) { this.__st8.id = this.__st8.parent.__st8.id + '.' + this.__st8.name; if (this.__st8.parent.__st8.config.isFlowRoot) this.__st8.flowRoot = this.__st8.parent; else if (this.__st8.parent.__st8.flowRoot) { this.__st8.flowRoot = this.__st8.parent.__st8.flowRoot; } } else { this.__st8.id = this.__st8.name; } } St8.st8Data.stateTemplates[this.__st8.name].addState(this); _.set(St8.st8Data.states, this.__st8.id, this); this.__st8.stateIsSetup = true; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOWST8_SETUP, { structure: { st8Obj: this.exportStructure({ noChildren: true }) } }); } } return this; }; FlowSt8.prototype.pipe = function (p, pOptions) { this.setupState(); var options = pOptions || {}; var _self = this; function assignChild(par, c) { if (c instanceof St8Flo) { if (!par.__st8.children[c._root.__st8.id]) { par.__st8.children[c._root.__st8.id] = c; } else { throw new Error('Cannot assign child. Parent FlowSt8 [' + par.__st8.id + '], already contains a child with id [' + c._root.__st8.id + ']'); } } if (c instanceof FlowSt8) { if (!par.__st8.children[c.__st8.id]) { par.__st8.children[c.__st8.id] = c; } else { throw new Error('Cannot assign child. Parent FlowSt8 [' + par.__st8.id + '], already contains a child with id [' + c.__st8.id + ']'); } } } function setupChild(q) { if (q instanceof St8Flo) { q.parents[_self.__st8.id] = _self; if (_self.__st8.flowRoot && _self.__st8.flowRoot.__st8.skipCB) q._root.__st8.skipCB = _self.__st8.flowRoot.__st8.skipCB; assignChild(_self, q); return q; } if (q instanceof FlowSt8) { if (!q.__st8.stateIsSetup) { q.__st8.parent = _self; q.setupState(); assignChild(_self, q); return q; } else { console.warn('FlowSt8 [%s] was already setup before piping to [%s]... Creating a copy, instead.', q.__st8.id, _self.__st8.id); options.parent = _self; options.createSt8 = true; var newSt8 = (new FlowSt8(q.__st8.name, options)).setupState(); assignChild(_self, newSt8); return newSt8; } } else if (typeof q == 'string') { options.parent = _self; options.createSt8 = true; var newSt8 = (new FlowSt8(q, options)).setupState(); assignChild(_self, newSt8); return newSt8; } } if (p && Array.isArray(p)) { var arr = []; p.forEach(function (item) { arr.push(setupChild(item)); }); return new St8Collection(arr); } else { return setupChild(p); } }; FlowSt8.prototype.parent = function () { return this.__st8.parent; }; /* Travereses up the flow chain and returns its root. Use this as a terminal at the end of the flow configurations and the variable will hold the root FlowSt8, which can be plugged into other flows. */ FlowSt8.prototype.root = function () { var node = this; var stop = false; while (!stop) { node = node.parent(); if ((node instanceof FlowSt8) && !node.parent()) stop = true; } return node; }; // terminal name is optional FlowSt8.prototype.terminal = function (tName) { var root = this.root(); if (root.__st8.flow) { root.__st8.flow.setAsTerminal(this, tName); return this; } else throw Error('root of this St8 chain is not a St8Flo Object. Cannot have a terminal'); }; FlowSt8.prototype.end = function () { var root = this.root(); if (root.__st8.flow) { return root.__st8.flow; } else throw Error('root of this St8 chain is not a St8Flo Object. Cannot have an "end" '); }; // Set condition markers to template, if not already set. FlowSt8.prototype.when = function (markers, options) { if (typeof markers == 'string') { var parsed; try { parsed = JSON.parse(markers); } catch (e) { throw ("Condition markers string passed to FlowSt8[" + this.__st8.id + "] isn't a valid JSON parsable string - " + e); } markers = parsed; } if (this.__st8.template) { this.__st8.template.setConditions(markers, options); } return this; }; // // if sets conditions, but does not accept any new data if conditions aren't right // // & therefore doesn't hold onto any new data, while waiting for conditions to become right. // FlowSt8.prototype.if = function (markers, options) { // if (typeof markers == 'string') { // var parsed; // // try { // parsed = JSON.parse(markers); // } catch (e) { // throw ("Condition markers string passed to FlowSt8[" + this.__st8.id + "] isn't a valid JSON parsable string - " + e); // } // // markers = parsed; // } // if (this.__st8.template) { // this.__st8.template.setConditions(markers, options); // this.__st8.config.routerMode = true; // } // // // return this; // }; FlowSt8.prototype.do = function (pFn, options) { if (this.__st8.template) { this.__st8.template.setFn(pFn); } return this; }; // FlowSt8.prototype.autoRepeat = function (initialData) { // var st8Obj = this; // // st8Obj.__st8.parent = st8Obj; // st8Obj.feedFn = function(){ // st8Obj.__st8.feeding = true; // st8Obj.__st8.input = St8.Data(st8Obj.__st8.results); // st8Obj.__st8.feeding = st8Obj.__st8.dinnerServed = false; // st8Obj.setState(St8.es.TASTING); // setTimeout(st8Obj._trigger, 0); // }; // st8Obj.__st8.doneCB = function(){ // st8Obj.__st8.dinnerServed = true; // }; // // st8Obj.__st8.dinnerServed = true; // st8Obj.__st8.results = initialData; // setTimeout(st8Obj._trigger, 0); // // return this; // }; FlowSt8.prototype.onClear = function (pFn, options) { if (typeof pFn == 'function') { this.__st8.clearFn = pFn; } return this; }; FlowSt8.prototype.log = function (message, payload) { var index; index = St8.st8Data.userLogMessages.indexOf(message); if (!index) index = St8.st8Data.userLogMessages.push(message) - 1; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOWST8_USERLOG, { index: index, state: { st8Obj: this.__st8.id }, payload: St8.decycle(payload) }); } }; FlowSt8.prototype.detachParents = function (parentSt8) { if (this.__st8.stateIsSetup) { if (this.__st8.config.isFlowRoot) { if (parentSt8 && parentSt8 instanceof FlowSt8) { delete parentSt8.__st8.children[this.__st8.id]; } else { var _self = this; Object.keys(_self.__st8.flow.parents).forEach(function (pKey) { delete _self.__st8.flow.parents[pKey].__st8.children[_self.__st8.id]; }); } } else { if (this.__st8.parent) { delete this.__st8.parent.__st8.children[this.__st8.id]; this.__st8.parent = null; } } } }; FlowSt8.prototype.delete = function (parentSt8) { if (this.__st8.stateIsSetup) { this.detachParents(parentSt8); this.__st8.template.removeState(this); _.set(St8.st8Data.states, this.__st8.id, undefined); if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOWST8_DELETED, { structure: { st8Obj: this.__st8.id } }); } } }; FlowSt8.prototype.deleteChain = function () { var _self = this; Object.keys(_self.__st8.children).forEach(function (cKey) { if (_self.__st8.children[cKey] instanceof FlowSt8) { _self.__st8.children[cKey].deleteChain(); _self.__st8.children[cKey].delete(); } if (_self.__st8.children[cKey] instanceof St8Flo) { _self.__st8.children[cKey].delete(_self); } }); }; FlowSt8.prototype.clear = function () { if (this.__st8.clearFn) { this.__st8.clearFn.call(this); if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.FLOWST8_CLEARED, { state: { st8Obj: this.__st8.id } }); } } }; FlowSt8.prototype.clearChain = function () { var _self = this; _self.clear(); Object.keys(_self.__st8.children).forEach(function (cKey) { _self.__st8.children[cKey].clearChain(); }); }; FlowSt8.prototype.cloneChain = function (root, toClone) { function cloneChildren(r, childCol) { var child; Object.keys(childCol).forEach(function (f) { child = r.pipe(childCol[f].__st8.name); cloneChildren(child, childCol[f].__st8.children); }); return r; } toClone = toClone || this; if (toClone instanceof St8Flo) { toClone = toClone._root; } if (root instanceof St8Flo) { root = root._root; } if ((root instanceof FlowSt8) && (toClone instanceof FlowSt8)) { cloneChildren(root, toClone.__st8.children); } else { throw Error('either "Root" or "toClone", were not of type St8Obj. minusSkipped cloning.'); } return root; }; FlowSt8.prototype.exportStructure = function (options) { options = options || {}; var o = {}; var _self = this; o.__st8 = { id: _self.__st8.id, type: _self.__st8.type, }; if (!options.short) { Object.assign(o.__st8, { name: _self.__st8.name, registered: _self.__st8.registered, config: _self.__st8.config, stateIsSetup: _self.__st8.stateIsSetup, parent: _self.__st8.parent ? _self.__st8.parent.__st8.id : null, }); } if (!options.noChildren) { o.__st8.children = (function () { var o = {}; Object.keys(_self.__st8.children).forEach(function (tKey) { o[tKey] = _self.__st8.children[tKey].exportStructure(options); }); return o; })(); } return o; }; FlowSt8.prototype.exportState = function (options) { options = options || {}; var o = {}; var _self = this; o.__st8 = { id: _self.__st8.id, execState: _self.__st8.execState, type: _self.__st8.type, }; if (!options.short) { Object.assign(o.__st8, { state: _self.state, counts: _self.__st8.counts, dinnerServed: _self.__st8.dinnerServed, }); if (!options.noData) { Object.assign(o.__st8, { input: _self.__st8.input, triggerData: _self.__st8.triggerData, results: _self.__st8.results }); } } return o; }; FlowSt8.prototype.forEach = function (sourcePath, flow, terminalName, inputMap, destinationPath) { var _self = this; var newSt8 = _self.pipe(St8.autoID('forEach-')); var setIMap = false; // var iMapKeys = Object.keys(inputMap); // var arrayItemMapKey; if (inputMap && !_.isEmpty(inputMap)) { setIMap = true; var iMapKeys = Object.keys(inputMap); var arrayItemMapKey; } var params = {}, tParams = {}; var FE_input, source, i = 0; var minusSkipped = 0, res = []; var pResolve; newSt8.onClear(function () { source = res = []; i = minusSkipped = 0; params = tParams = {}; }); newSt8.do(function (input) { return new Promise(function (resolve, reject) { newSt8.clear(); // clear forEach FlowSt8 // flow.clear(); // clear iterating flow. FE_input = input; if (destinationPath && typeof FE_input != 'object') { throw new Error('forEach [' + newSt8.__st8.id + '] : output expected on a path [' + destinationPath + '], but input is not an object!'); } if (typeof input != 'undefined') { if (sourcePath && sourcePath != '*') source = _.get(input, sourcePath); else source = input; if (setIMap) { iMapKeys.forEach(function (iKey) { if (inputMap[iKey] != 'ArrayItem') { params[iKey] = _.get(input, inputMap[iKey]); } else { arrayItemMapKey = iKey; } }); if (!arrayItemMapKey) throw Error('Could not find a [key] = "ArrayItem" input map for forEach flow.'); } if (source && Array.isArray(source)) { if (source.length > 0) { pResolve = resolve; minusSkipped = source.length; if (setIMap) { tParams = Object.assign(tParams, params); tParams[arrayItemMapKey] = source[i]; flow.trigger(tParams); } else { flow.trigger(source[i]); } } else { if (destinationPath) { _.set(FE_input, destinationPath, []); setTimeout(resolve, 0, FE_input); } else setTimeout(resolve, 0, []); } } else { console.log('--> St8 ERROR forEach - received input is not an array !'); return reject(Error('received input is not an array !')); } } else { console.log('--> St8 ERROR forEach - no input recieved !'); return resolve(Error('no input recieved !')); } }); }); flow.setFeed(function () { if (i < source.length) { var tParams = {}, tbr; console.log('st8flo - FOR EACH feedCB - Processing next array item - ', source[i]); /* Below, Done this way, to avoid wierd behaviour while maintaining performance as best as possible. tParams forms a fresh object that is passed as input to the flow, that holds a fresh copy of the current array item, while still maintaining previous references to other keys without needing to rebuild those references. If not done this way, subsequent calls to feedCB will replace array item with their own indexes, before the item has even been processed by the entire chain. ie. chain : A -> B -> C -> D. Since input to the flow is the same object reference being passed from state to state, By the time the value hits D, 2 additional calls to feedCb will already be made & the value will be replaced with 3nd item of array ie array[2] instead of array[0], before being processed by D for the first time. */ if (setIMap) { tParams = Object.assign(tParams, params); tParams[arrayItemMapKey] = source[i]; tbr = tParams; } else { tbr = source[i]; } i++; console.log('st8flo - FOR EACH feedCB - Feeding item as - ', tbr); return tbr; } else { return 'st8-EOD'; } }); flow.autoFeedWhile(function () { return i < source.length; }); flow.setSkip(function () { minusSkipped--; }); flow.setDone(terminalName, function (result) { console.log('st8flo - FOR EACH - doneCB called. got data as', result); res.push(result); if (res.length == minusSkipped) { var tbr; console.log('st8flo - FOR EACH [%s] RESOLVING - source length[%s], results length [%s], skipped items [%s] = ', newSt8.__st8.id, res.length, source.length, (source.length - minusSkipped)); if (destinationPath) { _.set(FE_input, destinationPath, res); tbr = FE_input; } else tbr = res; console.log('st8flo - RESOLVED Value = ', tbr); setTimeout(pResolve, 0, tbr); } }); return newSt8; }; FlowSt8.prototype.skipIF = function (testFn, callback) { var _self = this; var newSt8 = _self.pipe(St8.autoID('skipIF-')); newSt8 .do(function (input, packet) { switch (testFn.call(_self, input, packet)) { case false: return Promise.resolve(input); case true: if (callback) callback.call(_self, input, packet); return Promise.reject(); } }); return newSt8; }; /*----------- Class St8Marker - Used to mark & record data at various points in execution ------------ Markers record data and trigger associated St8s. */ function St8Marker(sName, options) { if (typeof sName == 'string' && sName.length > 0) { var a = _.get(St8.st8Data.appMarkers, sName); if (a && a.id && a.type == St8.types.MARKER) { return a; } this.id = sName; this.type = St8.types.MARKER; this.deps = []; this.state = {}; this.triggered = false; this.counts = { triggered: 0, debouncedSet: 0 }; _.set(St8.st8Data.appMarkers, sName, this); if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.MARKER_CREATE, { structure: { marker: this.exportStructure({ noChildren: true }) } }); } return this; } else { throw Error('state name must be a string'); } } St8Marker.prototype.set = function (v, options) { if (!options || !options.silent) console.log('---------- St8Flo - marker [ %s ] set to ', this.id, v, '\n\n'); this.value = v; this.triggered = true; this.counts.triggered++; if (St8.connector && St8.connector.enableLog) { St8.connector.log(St8.connector.logCodes.MARKER_CHANGE, { state: { marker: this.exportState() } }); } for (var i = 0; i < this.deps.length; i++) { setTimeout(this.deps[i].trigger, 0, { id: this.id, value: this.value }); } return this; }; St8Marker.prototype.get = function () { return this.value; }; St8Marker.prototype.exportStructure = function (options) { options = options || {}; var o = {}; var _self = this; o = { id: _self.id, type: _self.type, deps: _self.deps.map(function (e) { return { name: e.name }; }) }; return o; }; St8Marker.prototype.exportState = function (options) { options = options || {}; var o = {}; var _self = this; o = { id: _self.id, type: _self.type, value: St8.decycle(_self.value) }; return o; }; /*----------------------------------------------------- DATA API Encapsulates raw data into a st8 data/signal combo ------------------------------------------------------*/ function St8Data(value, signals) { // If passed value is already St8Data, return as is, overwriting signals if present. if (value instanceof St8Data) { if (signals) { value.signals = Object.assign(value.signals, signals); } return value; } // else if value is a St8Data-Like object, convert into St8Data and return (overwriting signals if present) else if (typeof value == 'object') { if (value.signals || value.data) { this.data = value.data; this.signals = value.signals; if (signals) { this.signals = Object.assign(this.signals, signals); } } // // TODO - is this really required ?? // else if (value.__st8 && value.__st8.signals) { // this.data = value; // this.signals = value.__st8.signals; // delete this.data.__st8.signals; // } else { this.data = value; this.signals = {}; if (signals) { this.signals = Object.assign(t