UNPKG

futoin-asyncsteps

Version:

Mimic traditional threads in single threaded event loop

634 lines (604 loc) 21.1 kB
"use strict"; /** * @file Module's entry point and AsyncSteps class itself * @author Andrey Galkin <andrey@futoin.org> * * * Copyright 2014-2017 FutoIn Project (https://futoin.org) * Copyright 2014-2017 Andrey Galkin <andrey@futoin.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @module futoin-asyncsteps */ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var AsyncTool = require('./lib/AsyncTool'); var _require = require('./Errors'), InternalError = _require.InternalError; var AsyncStepProtector = require('./lib/AsyncStepProtector'); var ParallelStep = require('./lib/ParallelStep'); var _require2 = require('./lib/common'), isProduction = _require2.isProduction, checkFunc = _require2.checkFunc, checkOnError = _require2.checkOnError, noop = _require2.noop, _loop = _require2.loop, _repeat = _require2.repeat, _forEach = _require2.forEach, as_await = _require2.as_await, EMPTY_ARRAY = _require2.EMPTY_ARRAY, newExecStack = _require2.newExecStack; var sanityCheck = isProduction ? noop : function (as) { if (as._stack.length > 0) { as.error(InternalError, "Top level add in execution"); } }; var sanityCheckAdd = isProduction ? noop : function (as, func, onerror) { sanityCheck(as); checkFunc(as, func); checkOnError(as, onerror); }; // This small trick has a huge speedup result (75-90%) var EXEC_BURST = 100; var g_curr_burst = EXEC_BURST; // avoid another AsyncSteps instance continuation var g_burst_owner = null; var post_execute_cb = function post_execute_cb(asi) { asi._post_exec = noop; asi._execute(); }; /** * Root AsyncStep implementation */ var AsyncSteps = /*#__PURE__*/function () { function AsyncSteps() { var _this = this; var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; var async_tool = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : AsyncTool; _classCallCheck(this, AsyncSteps); if (state === null) { state = function state() { return this.state; }; } this.state = state; this._queue = []; this._stack = []; this._exec_stack = newExecStack(); this._in_exec = false; this._post_exec = noop; this._exec_event = null; this._next_args = EMPTY_ARRAY; this._async_tool = async_tool; // --- var callImmediate = async_tool.callImmediate; var event_execute_cb = function event_execute_cb() { g_curr_burst = EXEC_BURST; g_burst_owner = _this; _this._exec_event = null; _this._execute(); }; this._scheduleExecute = function () { if (--g_curr_burst <= 0 || !_this._in_exec || g_burst_owner !== _this) { _this._exec_event = callImmediate(event_execute_cb); } else if (_this._in_exec) { _this._post_exec = post_execute_cb; } }; } /** * Add sub-step. Can be called multiple times. * @param {ExecFunc} func - function defining non-blocking step execution * @param {ErrorFunc=} onerror - Optional, provide error handler * @returns {AsyncSteps} self * @alias AsyncSteps#add */ return _createClass(AsyncSteps, [{ key: "add", value: function add(func, onerror) { sanityCheckAdd(this, func, onerror); this._queue.push([func, onerror]); return this; } /** * Creates a step internally and returns specialized AsyncSteps interfaces all steps * of which are executed in quasi-parallel. * @param {ErrorFunc=} onerror - Optional, provide error handler * @returns {AsyncSteps} interface for parallel step adding * @alias AsyncSteps#parallel */ }, { key: "parallel", value: function parallel(onerror) { sanityCheck(this); checkOnError(this, onerror); var p = new ParallelStep(this, this); this._queue.push([function (as) { p.executeParallel(as); }, onerror]); return p; } /* globals ISync */ /** * Add sub-step with synchronization against supplied object. * @param {ISync} object - Mutex, Throttle or other type of synchronization implementation. * @param {ExecFunc} func - function defining non-blocking step execution * @param {ErrorFunc=} onerror - Optional, provide error handler * @returns {AsyncSteps} self * @alias AsyncSteps#sync */ }, { key: "sync", value: function sync(object, func, onerror) { sanityCheckAdd(this, func, onerror); object.sync(this, func, onerror); return this; } /** * Set error and throw to abort execution. * * **NOTE: If called outside of AsyncSteps stack (e.g. by external event), make sure you catch the exception** * @param {string} name - error message, expected to be identifier "InternalError" * @param {string=} error_info - optional descriptive message assigned to as.state.error_info * @throws {Error} * @alias AsyncSteps#error */ }, { key: "error", value: function error(name, error_info) { this.state.error_info = error_info; var e = new Error(name); if (!this._in_exec) { this.state.last_exception = e; this._handle_error(name); } throw e; } /** * Copy steps and not yet defined state variables from "model" AsyncSteps instance * @param {AsyncSteps} other - model instance, which must get be executed * @returns {AsyncSteps} self * @alias AsyncSteps#copyFrom */ }, { key: "copyFrom", value: function copyFrom(other) { this._queue.push.apply(this._queue, other._queue); var os = other.state; var s = this.state; for (var k in os) { if (!(k in s)) { s[k] = os[k]; } } return this; } /** * @private * @param {Array} [args] List of success() args */ }, { key: "_handle_success", value: function _handle_success() { var args = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : EMPTY_ARRAY; var stack = this._stack; if (!stack.length) { this.error(InternalError, 'Invalid success completion'); } this._next_args = args; for (var asp = stack[stack.length - 1];;) { var limit_event = asp._limit_event; if (limit_event) { this._async_tool.cancelCall(limit_event); asp._limit_event = null; } asp._cleanup(); // aid GC stack.pop(); // --- if (!stack.length) { break; } asp = stack[stack.length - 1]; if (asp._queue.length) { break; } } if (stack.length || this._queue.length) { this._scheduleExecute(); } } /** * @private * @param {string} [name] Error to handle */ }, { key: "_handle_error", value: function _handle_error(name) { if (this._exec_event) { this.cancel(); return; } this._next_args = EMPTY_ARRAY; var stack = this._stack; var exec_stack = this._exec_stack; this.state.async_stack = exec_stack; var orig_in_exec = this._in_exec; var cleanup = true; while (stack.length) { var asp = stack[stack.length - 1]; var limit_event = asp._limit_event; var on_cancel = asp._on_cancel; var on_error = asp._on_error; if (limit_event) { this._async_tool.cancelCall(limit_event); asp._limit_event = null; } if (on_cancel) { on_cancel.call(null, asp); asp._on_cancel = null; } if (on_error) { var slen = stack.length; asp._queue = null; // suppress non-empty queue for success() in onerror asp._on_error = null; // do no repeat exec_stack.push(on_error); try { this._in_exec = true; on_error.call(null, asp, name); if (slen !== stack.length) { cleanup = false; break; // override with success() } if (asp._queue !== null) { cleanup = false; this._scheduleExecute(); break; } } catch (e) { this.state.last_exception = e; name = e.message; } finally { this._in_exec = orig_in_exec; } } asp._cleanup(); // aid GC stack.pop(); } if (cleanup) { // Clear queue on finish this._queue = []; } else if (!orig_in_exec) { this._post_exec(this); } } /** * Use only on root AsyncSteps instance. Abort execution of AsyncSteps instance in progress. * @returns {AsyncSteps} self * @alias AsyncSteps#cancel */ }, { key: "cancel", value: function cancel() { this._next_args = EMPTY_ARRAY; var exec_event = this._exec_event; if (exec_event) { this._async_tool.cancelImmediate(exec_event); this._exec_event = null; } var stack = this._stack; var async_tool = this._async_tool; while (stack.length) { var asp = stack.pop(); var limit_event = asp._limit_event; var on_cancel = asp._on_cancel; if (limit_event) { async_tool.cancelCall(limit_event); asp._limit_event = null; } if (on_cancel) { on_cancel.call(null, asp); asp._on_cancel = null; } asp._cleanup(); // aid GC } // Clear queue on finish this._queue = []; return this; } /** * Start execution of AsyncSteps using AsyncTool * * It must not be called more than once until cancel/complete (instance can be re-used) * @returns {AsyncSteps} self * @alias AsyncSteps#execute */ }, { key: "execute", value: function execute() { var prev_owner = g_burst_owner; g_burst_owner = this; this._execute(); g_burst_owner = prev_owner; return this; } }, { key: "_execute", value: function _execute() { var stack = this._stack; var q; if (stack.length) { q = stack[stack.length - 1]._queue; } else { q = this._queue; } if (!q.length) { return; } var curr = q.shift(); var func = curr[0]; this._exec_stack.push(func); var next_args = this._next_args; var na_len = next_args.length; var asp = new AsyncStepProtector(this, curr[1], next_args); stack.push(asp); try { var oc = stack.length; this._in_exec = true; if (!na_len) { func(asp); } else { this._next_args = EMPTY_ARRAY; func.apply(void 0, [asp].concat(_toConsumableArray(next_args))); } if (oc === stack.length) { if (asp._queue !== null) { this._scheduleExecute(); } else if (!asp._on_cancel && !asp._limit_event) { // Implicit success this._handle_success(this._next_args); } } } catch (e) { this.state.last_exception = e; this._handle_error(e.message); } finally { this._in_exec = false; } this._post_exec(this); } /** * Optimized success() which performs burst execution * @param {Array} [args] List of success() args * @private */ }, { key: "_burst_success", value: function _burst_success() { var args = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : EMPTY_ARRAY; try { this._in_exec = true; g_burst_owner = this; this._handle_success(args); } catch (e) { this.state.last_exception = e; this._handle_error(e.message); } finally { this._in_exec = false; } this._post_exec(this); } /** * It is just a subset of *ExecFunc* * @callback LoopFunc * @param {AsyncSteps} as - the only valid reference to AsyncSteps with required level of protection * @alias loop_callback * @see ExecFunc */ /** * Execute loop until *as.break()* or *as.error()* is called * @param {LoopFunc} func - loop body * @param {string=} label - optional label to use for *as.break()* and *as.continue()* in inner loops * @returns {AsyncSteps} self * @alias AsyncSteps#loop */ }, { key: "loop", value: function loop(func, label) { sanityCheckAdd(this, func); _loop(this, this, func, label); return this; } /** * It is just a subset of *ExecFunc* * @callback RepeatFunc * @param {AsyncSteps} as - the only valid reference to AsyncSteps with required level of protection * @param {number} i - current iteration starting from 0 * @alias repeat_callback * @see ExecFunc */ /** * Call *func(as, i)* for *count* times * @param {number} count - how many times to call the *func* * @param {RepeatFunc} func - loop body * @param {string=} label - optional label to use for *as.break()* and *as.continue()* in inner loops * @returns {AsyncSteps} self * @alias AsyncSteps#repeat */ }, { key: "repeat", value: function repeat(count, func, label) { sanityCheckAdd(this, func); _repeat(this, this, count, func, label); return this; } /** * It is just a subset of *ExecFunc* * @callback ForEachFunc * @param {AsyncSteps} as - the only valid reference to AsyncSteps with required level of protection * @param {number|string} key - key ID or name * @param {*} value - value associated with key * @alias foreach_callback * @see ExecFunc */ /** * For each *map* or *list* element call *func( as, key, value )* * @param {number} map_or_list - map or list to iterate over * @param {ForEachFunc} func - loop body * @param {string=} label - optional label to use for *as.break()* and *as.continue()* in inner loops * @returns {AsyncSteps} self * @alias AsyncSteps#forEach */ }, { key: "forEach", value: function forEach(map_or_list, func, label) { sanityCheckAdd(this, func); _forEach(this, this, map_or_list, func, label); return this; } /** * Shortcut for `this.add( ( as ) => as.success( ...args ) )` * @param {...any} [args] - argument to pass, if any * @alias AsyncSteps#successStep * @returns {AsyncSteps} self */ }, { key: "successStep", value: function successStep() { var _this2 = this; for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } sanityCheck(this); var queue = this._queue; if (queue.length > 0) { queue.push([function () { _this2._handle_success(args); }, undefined]); } else { this._next_args = args; } return this; } /** * Integrate a promise as a step. * @param {Promise} promise - promise to add as a step * @param {Function} [onerror] error handler to check * @alias AsyncSteps#await * @returns {AsyncSteps} self */ }, { key: "await", value: function _await(promise, onerror) { sanityCheck(this); as_await(this, this, promise, onerror); return this; } /** * Execute AsyncSteps with Promise interface * @alias AsyncSteps#promise * @returns {Promise} - promise wrapper for AsyncSteps */ }, { key: "promise", value: function promise() { var _this3 = this; sanityCheck(this); return new Promise(function (resolve, reject) { var q = _this3._queue; _this3._queue = [[function (as) { as._queue = q; }, function (as, err) { reject(new Error(err)); }], [function (as, res) { resolve(res); }, undefined]]; g_burst_owner = _this3; _this3._execute(); }); } /** * Create a new instance of AsyncSteps for independent execution * @alias AsyncSteps#newInstance * @returns {AsyncSteps} new instance */ }, { key: "newInstance", value: function newInstance() { return new AsyncSteps(null, this._async_tool); } /** * Not standard API for assertion with multiple instances of the module. * @private * @returns {boolean} true */ }, { key: "isAsyncSteps", value: function isAsyncSteps() { return true; } }]); }(); /** * **execute_callback** as defined in **FTN12: FutoIn AsyncSteps** specification. Function must have * non-blocking body calling: *as.success()* or *as.error()* or *as.add()/as.parallel()*. * @callback ExecFunc * @param {AsyncSteps} as - the only valid reference to AsyncSteps with required level of protection * @param {...*} [val] - any result values passed to the previous as.success() call * @alias execute_callback */ /** * **error_callback** as defined in **FTN12: FutoIn AsyncSteps** specification. * Function can: * * - do nothing, * - override error message with `as.error( new_error )`, * - continue execution with `as.success()`. * @callback ErrorFunc * @param {AsyncSteps} as - the only valid reference to AsyncSteps with required level of protection * @param {string} err - error message * @alias error_callback */ /** * **cancel_callback** as defined in **FTN12: FutoIn AsyncSteps** specification. * @callback CancelFunc * It must be used to cancel any external processing to avoid invalidated AsyncSteps object use. * @param {AsyncSteps} as - the only valid reference to AsyncSteps with required level of protection * @alias cancel_callback */ /** * Get AsyncSteps state object. * * **Note: There is a JS-specific improvement: as.state === as.state()** * * The are the following pre-defined state variables: * * - **error_info** - error description, if provided to *as.error()* * - **last_exception** - the last exception caught * - **async_stack** - array of references to executed step handlers in current stack * @returns {object} * @alias AsyncSteps#state */ module.exports = AsyncSteps; //# sourceMappingURL=AsyncSteps.js.map