futoin-asyncsteps
Version:
Mimic traditional threads in single threaded event loop
634 lines (604 loc) • 21.1 kB
JavaScript
"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