UNPKG

koroutine

Version:

Small, lightweight coroutine scheduler for node based on ES6 generators

252 lines (221 loc) 7.64 kB
'use strict'; /* Koroutine library configuration */ let _enableBreadcrumbs = false; let _errorHandler = console.log; /** * Enable/disable full stack traces for all coroutines * @param flag {Boolean} */ exports.enableBreadcrumbs = function (flag) { _enableBreadcrumbs = flag; }; /* Current executing Koroutine */ let _resume = false; /** * Set error handle function. Koroutine library passes uncaught exceptions thrown from Koroutine into this function * @param {Function} error_handler_function(errorObject) */ exports.setErrorHandler = function (errHandler) { _errorHandler = errHandler; }; function stitchBreadcrumbs (error, breadcrumbs) { if (breadcrumbs) { breadcrumbs.push(error); const filteredLines = []; for (let i = breadcrumbs.length - 1; i >= 0; i--) { const stack = breadcrumbs[i].stack; if (stack) { const lines = stack.split('\n'); for (let line of lines) { if (!line.includes('koroutine.js') && !line.includes('next (native)')) { filteredLines.push(line); } } } error.stack = filteredLines.join('\n'); } } } /** * Utility function that resumes the coroutine by throwing TimedOut exception when coroutine/callback/future does not finish before set timeout * @param {Function} either resume or future function * @param {String} 'coroutine' or 'callback' or 'future' * @param {String} Name of the callback or future involved. optional. * @param {Number} timeout in milliseconds */ function timedOut (resume, type, name, timeout) { const e = new Error((type || '') + ' ' + (name || '') + ' did not finish within ' + timeout + ' ms.'); e.cause = 'TimedOut'; e.name = name; resume(e); } /** * Used to run coroutine for the first time after it is created * @param iter {Iterator} Iterator received by calling generator function * @param options {Object} Optional options object. It can include following properties * - name: Name of the Koroutine * - timeout: Maximum time in milliseconds up to which this Koroutine is allowed to run * - enableStackTrace: Enable clean stack trace across yields * - stackDepth: Max number of lines to print in case of clean stack traces enabled exceptions */ exports.run = function (iter, options) { if (_resume) { throw new Error('Cannot spawn new koroutine from within another koroutine.'); } if (!iter || typeof iter[Symbol.iterator] !== 'function') { throw new Error('First parameter to koroutine.create() must be iterator returned by a generator function.'); } // const o = options || null; const name = (options && options.name) || ''; const errorHandlerFn = (options && options.errorHandler) || _errorHandler; let breadcrumbs = ((options && options.enableBreadcrumbs) || _enableBreadcrumbs) ? [] : null; let state = new Map(); const resume = function (error, ...rest) { if (!iter) { return; // koroutine already finished } // Callback was invoked so cancel callback timer, if any const cbTimer = resume.callbackTimer; if (cbTimer) { clearTimeout(cbTimer); } try { if (error) { error.cause = error.cause || 'Exception'; stitchBreadcrumbs(error, breadcrumbs); error.koroutine = name; } // Resume suspended koroutine resume.cbInProgress = false; resume.timer = null; exports.state = state; _resume = resume; const result = error ? iter.throw(error) : iter.next(rest); if (result.done) { iter = breadcrumbs = state = null; return result.value; } } catch (e) { e.message = 'Unhandled exception in koroutine ' + (name || '') + ' : ' + e.message; e.koroutine = name; stitchBreadcrumbs(e, breadcrumbs); iter = breadcrumbs = state = null; errorHandlerFn(e); } finally { // we are outside running coroutine, clear "current coroutine" variables exports.state = null; _resume = null; } }; // This is the global timeout that limits duration of the entire Koroutine execution const timeout = (options && options.timeout) || null; if (timeout && timeout > 0) { setTimeout(timedOut, timeout, resume, 'coroutine', name, timeout); } resume.krName = name; resume.breadcrumbs = breadcrumbs; // Start coroutine execution resume(); }; function prepareKoroutineCB (resume) { if (!resume) { throw new Error('koroutine.callback() must be invoked from within an active koroutine'); } if (resume.cbInProgress) { throw new Error('koroutine.callback() called when there is already another callback in progress'); } const breadcrumbs = resume.breadcrumbs; if (breadcrumbs) { const name = resume.krName; const errMessage = name ? name + ' suspended at' : 'suspended at'; breadcrumbs.push(new Error(errMessage)); } } /** * Returns NodeJs style callback function - callback(err, data) - which resumes suspended coroutine when called * @param timeout {Number} in milliseconds. optional. set to null or 0 for infinite time out. * @param name callback name. optional * @return {Function} callback function */ exports.callback = function (timeout, name) { const resume = _resume; prepareKoroutineCB(resume); resume.cbInProgress = true; if (timeout && timeout > 0) { resume.callbackTimer = setTimeout(timedOut, timeout, resume, 'callback', name, timeout); } return resume; }; /** * Returns a Future object that can be passed in place of normal node callback. Future * objects work with koroutine.join() to facilitate firing multiple async operations * from a single coroutine without blocking or yielding and then waiting for all of them * to finish at a single 'join' point in the code * @param timeout {Number} Number of milliseconds after which this future will timeout wih error.cause = 'timedout' * @param name {String} future name. optional. * @return {Function} future callback */ exports.future = function (timeout, name) { const resume = _resume; prepareKoroutineCB(resume); const future = function (error, ...rest) { if (future.done === true) { return; } future.done = true; if (error) { error.cause = error.cause || 'Exception'; future.error = error; } else { future.data = rest; } if (future.isJoined) { resume(null, future); } }; if ((timeout) && (timeout > 0)) { setTimeout(timedOut, timeout, future, 'future', name, timeout); } return future; }; /** * Wait till all the passed in futures are complete - i.e. done executing * @param {Array of futur objects} Futures to wait on * @return {Number} number of futures who returned error */ exports.join = function* (...futures) { let errorCount = 0; for (const future of futures) { while (future.done !== true) { future.isJoined = true; yield; } // future.isJoined = null; if (future.error) { errorCount += 1; } } return errorCount; }; function assertCalledFromKoroutine (name) { const resume = _resume; if (!resume) { throw new Error(name + ' must be called from within an active koroutine'); } return resume; } /** * Sleep for given number of milliseconds. Doesn't block the node's event loop * @param ms {Number} Number of milliseconds to sleep */ exports.sleep = function (timeout) { const resume = assertCalledFromKoroutine('sleep'); setTimeout(resume, timeout); }; /** * Akin to thread.yield() */ exports.defer = function () { const resume = assertCalledFromKoroutine('defer'); setImmediate(resume); };