/**
* Factory for creating the state-machine's <em>raise()</em> function.
*
* Creates implementation for the state-engine's
* <pre>
* {
* name: STRING // engine name / description
* doc: STRING // the URL to the SCXML document that holds the engine's definition
* evalScript: BOOLEAN // true
* onraise: FUNCTION // default impl. for onraise: print debug output (depending on logging-level for DialogEngine / InputEngine)
* onload: FUNCTION // default impl. handling initialization of state-engine after SCXML definition was loaded
* raise: FUNCTION // default impl. for raise-function: use execution-queue for allowing raise-invocations from within SCXML scripts
* }
* </pre>
*
* @class StateEngineFactory
* @memberOf mmir.state
* @hideconstructor
*
* @requires cordova.plugins.queuePlugin [Cordova/android]: needed for Android before 4.4 -> Cordova plugin for non-WebWorker-based execution-queue
* or
* @requires Worker (WebWorker) on window or global object
* or
* @requires setTimeout (fallback/stub implementation)
*/
define(['mmirf/resources', 'mmirf/scionEngine', 'mmirf/asyncUtils', 'mmirf/util/extend', 'require'], function(resources, createScionEngine, asyncUtils, extend, require) {
/**
* HELPER logging for state-changes
*
* @param {Engine} ctx
* the context for the state-machine, i.e. the DialogEngine or InputEngine instance
*
* @memberOf mmir.state.StateEngineFactory#
*/
var printDebugStates = function(ctx){
if(!ctx._logger.isDebug()){
return;
}
ctx._logger.debug(ctx.name, 'current state: ' + JSON.stringify(ctx.getStates()));
ctx._logger.debug(ctx.name, 'active states: ' + JSON.stringify(ctx.getActiveStates()));
ctx._logger.debug(ctx.name, 'active events: ',+ JSON.stringify(ctx.getActiveEvents()));
ctx._logger.debug(ctx.name, 'active transitions: '+ JSON.stringify(ctx.getStates()) + ":"+ JSON.stringify(ctx.getActiveTransitions()));
};
/**
* Factory for base / default implementation for state-engine, returns
* <pre>
* {
* name: STRING // engine name / description
* doc: STRING // the URL to the SCXML document that holds the engine's definition
* evalScript: BOOLEAN // true
* onraise: FUNCTION // default impl. for onraise: print debug output (depending on logging-level for DialogEngine / InputEngine)
* onload: FUNCTION // default impl. handling initialization of state-engine after SCXML definition was loaded
* raise: FUNCTION // raise-function created by envFactory(_instance)
* }
* </pre>
*
* @param {Engine} _engine
* the instance of the SCION state-machine
*
* @param {ExecFactory} envFactory
* the factory for creating the Worker/Thread and the raise() function
*
* @returns the implementation with raise-function
*
* @memberOf mmir.state.StateEngineFactory#
*/
var _baseFactory = function(_instance, envFactory){
/** @class StateEngineDefaultImpl
* @memberOf mmir.state
*/
return /** @lends mmir.state.StateEngineDefaultImpl# */ {
/** @type String
* @memberOf mmir.state.StateEngineDefaultImpl#
*/
name : envFactory.name? envFactory.name : 'base_engine',
/** @type String */
doc : null,
/** @function */
onraise : function() {
if (this._logger.isd()) {
printDebugStates(_instance);
};
},
/** @type Boolean */
evalScript : true,
/** @function */
onload : function(scion, deferred) {
//FIX (russa) for jQuery > 2.x: extend() uses isPlainObject(), which's impl. changed in 2.x
// -> isPlainObject() now requires a constructor that is different from the native Object.constructor
// in order to be able to detect, that an object is NOT a plain object
// ... but scion does not provide such a non-native constructor for its _scion property
// (which causes deep-copy in extend() to run into an infinite loop)
// QUICK-FIX: just attach a dummy constructor function, so that isPlainObject will correctly detect property
// _scion as not-a-plain-object (this should really be FIXed in the scion library itself...)
//
//TODO russa: check if we really need a deep copy here (maybe we should make a copy TO scion and replace _instance with the ext. scion obj. ...?)
if(scion['_scion']) scion['_scion'].constructor = function dummy(){};//NOTE _scion only does exist on old SCION lib version!
extend(_instance, scion);
_instance.__proto__ = scion.constructor.prototype;//FIX: in newer SCION implementations we need the prototype too
_instance.worker = envFactory.createWorker(_instance, _instance.gen);//_instance._genFuncFactory(_instance, _instance.gen);
//FIXME @russa: is this a general functionality? should this be removed?
if (!_instance.evalScript){
_instance.ignoreScript();
}
// delete _instance.gen;
delete _instance.evalScript;
_instance.start();
deferred.resolve(_instance);
},//END: onload
/** @function */
raise: envFactory.createRaise(_instance),
/** @function */
destroy: function(){
if(_instance.worker.destroy){
_instance.worker.destroy();
}
}
};
};
/**
* Factory for WebWorker-based implementation of raise function.
*
* Provide creator-functions:
* <code>createWorker(_engineInstance, genFunc) : WebWorker</code>
* <code>createRaise(_engineInstance) : Function</code>
*
* @class StateEngineWebWorkerImpl
* @memberOf mmir.state.StateEngineFactory
*
* @requires workers/scion-queue
*/
var _browserFactory = /** @lends mmir.stateStateEngineFactory.StateEngineWebWorkerImpl# */ {
/**
* @type {String}
* @memberOf mmir.stateStateEngineFactory.StateEngineWebWorkerImpl
*/
name: 'webworkerGen',
/**
* @type {Array<{_engineId: number, _engineInstance: ScionEngine, _engineGen: Function}>}
* @memberOf mmir.stateStateEngineFactory.StateEngineWebWorkerImpl
*/
engines: [],
/** @function */
workerConstructor: void(0),
/** @type Worker */
workerInstance: void(0),
/** @function */
createWorkerInstance: function() {
var scionQueueWorker = new this.workerConstructor(
typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD?
void(0) :
resources.getWorkerPath()+ 'scionQueueWorker.js'
);
scionQueueWorker._engines = this.engines;
asyncUtils.onMessage(scionQueueWorker, function(evt) {
if (evt.data.command == 'processNextJob') {
var jobData = evt.data.job;
var engine = this._engines[jobData.engineId];
var inst = engine._engineInstance;
if(inst._logger.isd()){
inst._logger.debug('raising: ' + jobData.event);
}
engine._engineGen.call(inst, jobData.event, jobData.eventData);
// after gen-call has finished, notify worker queue to
// process next job (if there is any):
this.postMessage({
command: 'finishedJob',
engineId: this._engineId
});
inst.onraise();
}
});
// if worker implementation supports unref(), do "declare" that program
// should exit even if the worker is still running:
if(typeof scionQueueWorker.unref === 'function'){
scionQueueWorker.unref();
}
if(typeof scionQueueWorker.terminate === 'function'){
scionQueueWorker.destroy = function(){
this.terminate();
};
} else {
scionQueueWorker.destroy = function(){};
}
scionQueueWorker.addEngine = function(engineEntry){
var id = this._engines.length;
engineEntry._engineId = id;
this._engines.push(engineEntry);
};
scionQueueWorker.removeEngine = function(engineEntry){
this._engines.splice(engineEntry._engineId, 1);
};
this.workerInstance = scionQueueWorker;
return scionQueueWorker;
},
/** @function */
createWorker: function(instance, gen) {
var workerInstance = this.workerInstance || this.createWorkerInstance();
var scionQueueWorker = {
_engineId: -1,
_engineInstance: instance,
_engineGen: gen,
_workerInstance: workerInstance,
destroy: function(){
this._workerInstance.removeEngine(this._engineId);
if(this._workerInstance._engines.length === 0){
this._workerInstance.destroy();
} else {
this._workerInstance.postMessage({
command: 'destroyQueue',
engineId: this._engineId
});
}
}
};
workerInstance.addEngine(scionQueueWorker);
return scionQueueWorker;
},
/** @function */
createRaise: function(instance){
return function(event, eventData) {
if (this._logger.isd()){
this._logger.debug('new Job: ' + event);
}
instance.worker._workerInstance.postMessage({
command: 'queueJob',
engineId: this._engineId,
job: {
engineId: instance.worker._engineId,
event: event,
eventData: eventData
}
});
};
}
};
/**
* Factory for CordovaPlugin-based implementation of raise function.
*
* Provide creator-functions:
* <code>createWorker(_engineInstance, genFunc) : WebWorker</code>
* <code>createRaise(_engineInstance) : Function</code>
*
*
* @requires cordova.plugins.queuePlugin (Cordova plugin for execution-queue)
*
* @class StateEngineQueuePluginImpl
* @memberOf mmir.state.StateEngineFactory
*/
var _queuePluginFactory = /** @lends mmir.state.StateEngineFactory.StateEngineQueuePluginImpl# */ {
/** @memberOf mmir.state.StateEngineFactory.StateEngineQueuePluginImpl# */
name: 'queuepluginGen',
/** @function */
createWorker: (function initWorkerFactory() {
//"global" ID-list for all queues (i.e. ID-list for all engines)
var callBackList = [];
return function workerFactory(_instance, gen){
var id = callBackList.length;
var execQueue = window.cordova.plugins.queuePlugin;
function successCallBackHandler(args){
if (args.length===2){
// console.debug('QueuePlugin: success '+ JSON.stringify(args[0]) + ', '+JSON.stringify(args[1]));//DEBUG
callBackList[args[0]](args[1]);
}
}
function failureFallbackHandler(_err){
_instance._logger.error('failed to initialize SCION extension for ANDROID env');
_instance.worker = (function(gen){
return {
raiseCordova: function fallback(event, eventData){
setTimeout(function(){
gen.call(_instance, event, eventData);
}, 10);
}
};
})();//END: fallback
}
callBackList.push(function(data){
var inst = _instance;
if(inst._logger.isv()) inst._logger.debug('raising:'+ data.event);
var generatedState = gen.call(inst, data.event, data.eventData);
if(inst._logger.isv()) inst._logger.debug('QueuePlugin: processed event '+id+' for '+ data.event+' -> new state: '+JSON.stringify(generatedState)+ ' at ' + inst.url);
execQueue.readyForJob(id, successCallBackHandler, failureFallbackHandler);
inst.onraise();
});
execQueue.newQueue(id, function(_args){
if(_instance._logger.isv()) _instance._logger.debug('QueuePlugin: entry '+id+' created.' + ' at ' + _instance.url);
}, failureFallbackHandler
);
return {
_engineInstance: _instance,
raiseCordova: function (event, eventData){
if(this._engineInstance._logger.isv()) this._engineInstance._logger.debug('QueuePlugin: new Job: '+ id + ' at ' + this._engineInstance.url);
execQueue.newJob(id, {event: event, eventData: eventData}, successCallBackHandler,failureFallbackHandler);
}
};
};//END: workerFactory(_instance, gen)
})(),//END: initWorkerFactory()
/** @function */
createRaise: function(_instance){
return function(event, eventData) {
if (eventData) _instance._logger.log('new Job:' + event);
_instance.worker.raiseCordova(event, eventData);
};
}
};
/**
* Factory for stub/dummy implementation of raise function.
*
* Provide creator-functions:
* <code>createWorker(_engineInstance, genFunc) : WebWorker</code>
* <code>createRaise(_engineInstance) : Function</code>
*
*
* @requires cordova.plugins.queuePlugin (Cordova plugin for execution-queue)
*
* @class StateEngineStubImpl
* @memberOf mmir.state.StateEngineFactory
*/
var _stubFactory = /** @lends mmir.state.StateEngineFactory.StateEngineStubImpl# */ {
/** @memberOf mmir.state.StateEngineFactory.StateEngineStubImpl# */
name: 'stubGen',
/** @function */
createWorker: function(_instance, gen) {
return {
_engineGen: gen,
_engineInstance: _instance,
raiseStubImpl: function fallback(event, eventData){
_instance._engineRaiseTimeout = setTimeout(function(){
gen.call(_instance, event, eventData);
}, 50);
},
destroy: function(){
clearTimeout(_instance._engineRaiseTimeout);
}
};
},
/** @function */
createRaise: function(_instance){
return function(event, eventData) {
if (eventData) _instance._logger.log('new Job:' + event);
_instance.worker.raiseStubImpl(event, eventData);
};
}
};
/**
* Get factory for raise-impl. depending on the runtime environment.
*
* Currently there are 3 impl. available
* * WebWorker
* * Android QueuePlugin
* * STUB IMPLEMENTATION
*
* @memberOf mmir.state.StateEngineFactory#
*/
function getScionEnvFactory(){
var ctx = typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this;
var hasWebWorkers = ctx && typeof ctx.Worker !== 'undefined';
//TODO make this configurable? through ConfigurationManager?
if(hasWebWorkers){
if(!_browserFactory.workerConstructor){
_browserFactory.workerConstructor = typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD? require('../../workers/scionQueueWorker.js') : ctx.Worker;
}
return _browserFactory; //_browser;
}
else {
var isCordovaEnv = resources.isCordovaEnv();
//if queue-plugin is available:
if(isCordovaEnv && cordova.plugins && cordova.plugins.queuePlugin){
return _queuePluginFactory;//_cordova;
}
else {
//otherwise use fallback:
return _stubFactory;//_stub;
}
}
}
/**
* no-op function for dummy logger
* @memberOf mmir.state.StateEngineFactory#
*/
function noop(){};
/**
* always-false function for dummy logger
* @memberOf mmir.state.StateEngineFactory#
*/
function deny(){return false;};
// /**
// * print-warning function for dummy logger
// * @memberOf mmir.state.StateEngineFactory#
// */
// function pw(){console.warn.apply(console,arguments);};
/**
* print-error function for dummy logger
* @memberOf mmir.state.StateEngineFactory#
*/
function pe(){console.error.apply(console,arguments);};
/**
* dummy logger that does nothing:
* the engine-creator should replace this with a "real" implementation
* e.g. something like this (see also init() in dialogManager):
* @example
* engine = require('mmirf/engineConfig')('some-url', 'some-mode');
* engine._logger = require('mmirf/logger').create('my-module-id');
*
* @class StubLogger
* @memberOf mmir.state.StateEngineFactory
*/
var nolog = /** @lends mmir.state.StateEngineFactory.StubLogger# */ {
/** @memberOf mmir.state.StateEngineFactory.StubLogger# */
d: noop,
/** @function */
debug: noop,
/** @function */
w: noop,//pw,
/** @function */
warn: noop,//pw,
/** @function */
e: pe,
/** @function */
error: pe,
/** @function */
log: noop,
/** @function */
isVerbose: deny,
/** @function */
isv: deny,
/** @function */
isDebug: deny,
/** @function */
isd: deny
};
/**
* Exported factory function for creating / adding impl. to SCION engine instance
*
* @memberOf mmir.state.StateEngineFactory#
*/
return function create(url, _mode) {
var baseFactory = _baseFactory;
var scionEnvConfig = getScionEnvFactory();
var _instance = {url: url,_logger: nolog};
var scionConfig = baseFactory(_instance, scionEnvConfig);
scionConfig.doc = url;
_instance = createScionEngine(scionConfig, _instance);
return _instance;
};
});