Source: env/grammar/asyncGenerator.js



define(['mmirf/resources', 'mmirf/logger', 'mmirf/asyncUtils', 'require', 'module'],
/**
 * Module that provides a generator for parser-compiler WebWorker instances:
 *
 * initializes the WebWorker instance and provides functions for communicating with
 * the WebWorker.
 *
 * @class
 * @constant
 * @public
 * @name AsyncGenerator
 * @memberOf mmir.env.grammar
 * @hideconstructor
 *
 * @requires WebWorker
 *
 * @example
 *
 * //initializes a WebWorker at MMIR's worker path, i.e. at resources.getWorkerPath() + "file.name":
 * var asyncCompiler = asyncGen.createWorker("jisonCompiler.js");
 *
 * //create job ID (must be unique for each job):
 * var taskId = '1';
 *
 * //register callback for the job ID:
 * asyncCompiler.addCallback(taskId, function(evtData){
 *
 * 		if(evtData.error){
 * 			//... do something
 * 		} else {
 * 			var hasError = evtData.hasError;
 * 			var grammarParser = evtData.def;
 *
 * 			//... do something
 * 		}
 *
 * });
 *
 * //start a compile job on the worker:
 * asyncCompiler.postMessage({
 * 		cmd: 'parse',
 *  	id: taskId,
 *  	config: options,		//compile options (specific to compiler engine)
 *  	text: grammarDefinition	//parser definition (using the syntax of the specific compiler engine)
 * });
 *
 */
function(resources, Logger, asyncUtils, require, module){

var COMPILER_WORKER_POSTFIX = 'Compiler.js';

var MODULE_ID_PREFIX = 'mmirf/';

/**
 * Factory for creating WebWorkers that compile grammar-parser
 *
 * @class AsyncCompiler
 * @memberOf mmir.env.grammar
 * @hideconstructor
 */
return {

	/**
	 * Create a WebWorker for compiling grammars.
	 *
	 * The returned WebWorker provides 2 additional convenience functions
	 * for communicating with the WebWorker thread:
	 *
	 * <code>addCallback(taskId : String, callback : Function)</code>
	 * where <em>taskId</em> is a unique String identifying a single compile-job
	 * and <em>callback</em> handles messages that come back from the WebWorker thread:
	 *
	 * The <code>callback(eventData)</code> will invoke the callback with the message's data
	 * The property <code>eventData.done</code> has a special function:
	 * 		 if this property is present with a TRUTHY value, then callback will be removed, i.e.
	 *       not listening anymore for the specified taskId.
	 *
	 * After adding a <em>callback</em>, messages to the WebWorker thread can be send with
	 * the <code>postMessage({id: taskId, cmd: 'parse', ...})</code>
	 *
	 * @param {String} parserEngineId
	 * 			the name of the file that holds the WebWorker implementation.
	 * 			The file must be located in MMIR's {@link Resources#getWorkerPath}
	 * 			directory.
	 *
	 * @returns {mmir.env.grammar.CompileWebWorker}
	 * 			a WebWorker that provides some convenience functions for
	 * 			communicating with the WebWorker/thread.
	 *
	 * @memberOf mmir.env.grammar.AsyncCompiler#
	 */
	createWorker: function(parserEngineId){

		/**  @memberOf mmir.env.grammar.CompileWebWorker# */
		var workerPath = resources.getWorkerPath() + parserEngineId + COMPILER_WORKER_POSTFIX;

		/**  @memberOf mmir.env.grammar.CompileWebWorker# */
		var mmirLibPath = resources.getMmirBasePath();

		//webpack web worker wrapper:
		var WpWorker = typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD? require('../../workers/' + parserEngineId + COMPILER_WORKER_POSTFIX) : null;

		/**
		 * web-worker for compile jobs
		 *
		 * @class CompileWebWorker
		 * @memberOf mmir.env.grammar
		 */
		var asyncCompiler = typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD?
					new WpWorker() :
					new Worker(workerPath);


		var _modConf = module.config(module);
		/**  @memberOf mmir.env.grammar.CompileWebWorker# */
		asyncCompiler._logger = Logger.create(parserEngineId+'AsyncWorker', _modConf? _modConf.logLevel : void(0));

		/**
		 * dictionary for currently active compilations
		 *
		 * <pre>
		 * [id] -> callback-function()
		 * </pre>
		 *
		 * @memberOf mmir.env.grammar.CompileWebWorker#
		 * @member _activeTaskIds
		 */
		asyncCompiler._activeTaskIds = {};

		/**
		 * listener for init-message
		 *
		 * DEFAULT: prints a message to the console
		 *
		 * Init Message:
		 *  * success:
		 *    <code>{init:true}</code>
		 *  * failed:
		 *    <code>{init:false,error:"message"}</code>
		 *
		 * @memberOf mmir.env.grammar.CompileWebWorker#
		 * @function _oninit
		 */
		asyncCompiler._oninit = function(evtData){
			if(evtData.init){
				if(this._logger.isDebug()) this._logger.debug('initialized: '+JSON.stringify(evtData));
			} else {
				this._logger.error('failed to initialize: '+JSON.stringify(evtData));
			}
		};

		/**
		 * HELPER generate & setup oninit signal for sync + async modules.
		 *
		 * Side Effects:
		 * generates and sets #_oninit
		 *
		 * @param {ParserGen} syncGen
		 * 				the sync parser generator
		 * @param {Deferred} asyncDef
		 * 				the deferred that should be resolved when async generator is initialized
		 *
		 * @returns {AsyncInitMesssage}
		 * 				the message object that should be sent to the async generator's WebWorker
		 *
		 * @memberOf mmir.env.grammar.CompileWebWorker#
		 * @function prepareOnInit
		 */
		asyncCompiler.prepareOnInit = function(syncGen, asyncDef){

			//setup async init-signaling:
			var isSyncGenInit = false;
			var isAsyncGenInit = false;

			//HELPER signal as "completely initialized" when sync & async modules are initialized:
			var checkInit = function(){
				if(isSyncGenInit && isAsyncGenInit){
					asyncDef.resolve();
				}
			};

			var initSyncGenDef = syncGen.init();//<- get init-signal for sync-generator
			var onComplete = function(){
				isSyncGenInit = true;
				checkInit();
			};
			initSyncGenDef.then(onComplete, onComplete);

			this._oninit = function(initEvt){
				if(initEvt.init === false){
					this._logger.error('Failed to initialize '+JSON.stringify(initEvt));
				} else {
					isAsyncGenInit = true;
				}
				checkInit();
			};
			var engineUrl = typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD? null : require.toUrl(MODULE_ID_PREFIX + syncGen.engineId)
			return {cmd:'init', engineUrl: engineUrl, libUrl: mmirLibPath};

		};

		/**
		 * Error handler for errors signaled by the webworker
		 *
		 * @memberOf mmir.env.grammar.CompileWebWorker#
		 * @function _onerror
		 */
		asyncCompiler._onerror = function(errorMsg, error){
			this._logger.error(errorMsg, error);
		};

		/**
		 * HELPER: register a callback (usage: see e.g. jisonGen.compileGrammar())
		 *
		 * @memberOf mmir.env.grammar.CompileWebWorker#
		 * @function addCallback
		 */
		asyncCompiler.addCallback = function(id, cb){
			this._activeTaskIds[id] = cb;
		};

		/**
		 * handler for messages/event from web-worker:
		 *
		 * @memberOf mmir.env.grammar.CompileWebWorker#
		 * @function onmessage
		 */
		var onmessage = function(evt){

			var id = evt.data.id;
			if(id){

				var handler = this._activeTaskIds[id];

				//this was the final message for this ID -> remove callback-function from "active tasks" dictionary:
				if(evt.data.done){
					delete this._activeTaskIds[id];
				}

				if(handler){

					handler.call(this, evt.data);

				} else {
					this._logger.warn('no callback registered for ID "'+id+'" -> ' + JSON.stringify(evt.data));
				}

			}
			else if(evt.data.error){

				this._onerror('Error in '+workerPath+': '+evt.data.error, evt.data);
			}
			else if(typeof evt.data.init === 'boolean'){

				asyncCompiler._oninit(evt.data);
			}
			else {

				this._onerror('ERROR unknown message -> ' + JSON.stringify(evt.data), evt.data);
			}

			if(this._onidle && !this.hasPendingCallback()){
				this._onidle();
			}

		};
		asyncUtils.onMessage(asyncCompiler, onmessage);

		/**
		 * check if the worker has "pending callbacks"
		 *
		 * @memberOf mmir.env.grammar.CompileWebWorker#
		 * @function hasPendingCallback
		 */
		asyncCompiler.hasPendingCallback = function(){
			for(var n in asyncCompiler._activeTaskIds){
				if(asyncCompiler._activeTaskIds[n]){
					return true;
				}
			}
		};

		return asyncCompiler;
	}
};

});