'use strict';
const ERROR_TYPES_SYSTEM = Object.freeze([ EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError ]);
const FN_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
const FN_ARROWS = /\(?([^\)]*?)\)?=>.*$/mg;
const FN_DEFAULT_PARAMS = /=[^>][^,]+/mg;
const FN_PARAMS_SEP = /([^\s,]+)/g;
/**
* ⇝ Manages multiple `async function`/`await` tasks that can be ran in **series/sequential** and/or **parallel/concurrent**
* order in relation to one another. See {@tutorial started} for more details.
*/
class Asynchro {
// TODO : ESM use... export class Asynchro {
/**
* Constructor
* @param {Object} [result] the object used for storing results (omit to prevent capturing of results)
* @param {(Boolean|Object|String)} [throws] one of the following values (unless superseded, will be applied to all queued tasks):
* - `true` to throw any errors and instantly stop any further execution
* - `false` to catch any errors that are thrown
* - `Object` an object containing the following properties:
* - - `invert` true to catch errors when matches are made, false/omit to throw errors when matches are made
* - - `matches` An array that contains any of the following:
* - - - `system` A string value equal to "system" that will match when an error is a type within {@link Asynchro.systemErrorTypes}
* - - - `Object` An object that contains property names/values that will be matched against property name/values in the caught error.
* When invert is true, all names/values on the caught error must match the names/values on the object in order to be
* **caught/captured** in {@link Asynchro.errors}. When invert is false/omitted, all names/values on the caught error must match
* all of the names/values on the object in order to be **rethrown**.
* - `system` a single `matches` string (invert defaults to false)
*
* Re-thrown errors will set `Error.cause` to the originally thrown error.
* @param {Function} [log] `function(tagArray, object)` that will log process output (omit to prevent logging)
* @param {Function} [includeErrorMsgCheck] A `function(name, operation, error)` that will return true when the error message should be
* included in the final {@link Asynchro.messages} output (defaults to false)
*/
constructor(result, throws, log, includeErrorMsgCheck) {
const asy = internal(this);
asy.at.status = Asynchro.QUEUEING;
asy.at.throws = throws;
asy.at.errorMetaName = Asynchro.name;
asy.at.includeErrorMsgCheck = includeErrorMsgCheck;
asy.at.trk = { que: [], waiting: 0, errors: [], rslt: result, log, messages: [], verify: {}, backgrounds: [], waitingBackground: 0 };
}
/**
* Queues an `async function` to run in **series** relative to other functions in the queue
* @param {String} [name] The name given for the task where the result will be stored as a property of the {@link Asynchro.result} object
* (omit to prevent results from being set from function return value - a name/ID will be generated and returned)
* @param {Function} fn The function to queue for asynchronicity (can also be a synchronous function)
* @param {...*} args Aguments that will be passed into the queued function
* @returns {String} The queued name/ID (either passed or generated)
*/
series(name, fn, ...args) {
const asy = internal(this);
return asynchroQueue(asy.this, true, asy.at.throws, name, fn, args);
}
/**
* Queues an `async function` to run in **parallel** relative to other functions in the queue
* @param {String} [name] The name given for the task where the result will be stored as a property of the {@link Asynchro.result} object
* (omit to prevent results from being set from function return value - a name/ID will be generated and returned)
* @param {Function} fn The function to queue for asynchronicity
* @param {...*} args Aguments that will be passed into the queued function
* @returns {String} The queued name/ID (either passed or generated)
*/
parallel(name, fn, ...args) {
const asy = internal(this);
return asynchroQueue(asy.this, false, asy.at.throws, name, fn, args);
}
/**
* Queues an `async function` to run in the **background** (i.e. the queue wont wait for the results and will not be captured).
* Thrown errors within the scope of specified `throws` flag(s) will be thrown and will stop further execution of the queue.
* @param {String} [name] The name given for the task that can be used in conjunction with {@link Asynchro.verify}
* (no results will be set from the function's return value - omit will cause the name/ID will be generated and returned)
* @param {Function} fn The function to queue for asynchronicity
* @param {...*} args Aguments that will be passed into the queued function
* @returns {String} The queued name/ID (either passed or generated)
*/
background(name, fn, ...args) {
const asy = internal(this);
return asynchroQueue(asy.this, false, asy.at.throws, name, fn, args, asy.at.trk.errors);
}
/**
* Queues an `async function` to run in **series** relative to other functions in the queue while overriding the `throws` option set during
* construction.
* @param {String} [name] The name given for the task where the result will be stored as a property of the {@link Asynchro.result} object
* (omit to prevent results from being set from function return value - a name/ID will be generated and returned)
* @param {(Boolean|Object|String)} [throws] One of the following values (supersedes any `throws` parameters passed during construction):
* - `true` to throw any errors and instantly stop any further execution
* - `false` to catch any errors that are thrown
* - `Object` an object containing the following properties:
* - - `invert` true to catch errors when matches are made, false/omit to throw errors when matches are made
* - - `matches` An array that contains any of the following:
* - - - `system` A string value equal to "system" that will match when an error is a type within {@link Asynchro.systemErrorTypes}
* - - - `Object` An object that contains property names/values that will be matched against property name/values in the caught error.
* When invert is true, all names/values on the caught error must match the names/values on the object in order to be
* **caught/captured** in {@link Asynchro.errors}. When invert is false/omitted, all names/values on the caught error must match
* all of the names/values on the object in order to be **rethrown**.
* - `system` a single `matches` string (invert defaults to false)
*
* Re-thrown errors will set `Error.cause` to the originally thrown error.
* @param {Function} fn The function to queue for asynchronicity
* @param {...*} args Aguments that will be passed into the queued function
* @returns {String} The queued name/ID (either passed or generated)
*/
seriesThrowOverride(name, throws, fn, ...args) {
return asynchroQueue(this, true, throws, name, fn, args);
}
/**
* Queues an `async function` to run in **parallel** relative to other functions in the queue while overriding the `throws` option set during
* construction.
* @param {String} [name] The name given for the task where the result will be stored as a property of the {@link Asynchro.result} object
* (omit to prevent results from being set from function return value - a name/ID will be generated and returned)
* @param {(Boolean|Object|String)} [throws] One of the following values (supersedes any `throws` parameters passed during construction):
* - `true` to throw any errors and instantly stop any further execution
* - `false` to catch any errors that are thrown
* - `Object` an object containing the following properties:
* - - `invert` true to catch errors when matches are made, false/omit to throw errors when matches are made
* - - `matches` An array that contains any of the following:
* - - - `system` A string value equal to "system" that will match when an error is a type within {@link Asynchro.systemErrorTypes}
* - - - `Object` An object that contains property names/values that will be matched against property name/values in the caught error.
* When invert is true, all names/values on the caught error must match the names/values on the object in order to be
* **caught/captured** in {@link Asynchro.errors}. When invert is false/omitted, all names/values on the caught error must match
* all of the names/values on the object in order to be **rethrown**.
* - `system` a single `matches` string (invert defaults to false)
*
* Re-thrown errors will set `Error.cause` to the originally thrown error.
* @param {Function} fn The function to queue for asynchronicity
* @param {...*} args Aguments that will be passed into the queued function
* @returns {String} The queued name/ID (either passed or generated)
*/
parallelThrowOverride(name, throws, fn, ...args) {
return asynchroQueue(this, false, throws, name, fn, args);
}
/**
* Queues an `async function` to run in the **background** (i.e. the queue wont wait for the results and will not be captured).
* Thrown errors within the scope of specified `throws` flag(s) will be thrown and will stop further execution of the queue.
* @param {String} [name] The name given for the task that can be used in conjunction with {@link Asynchro.verify}
* (no results will be set from the function's return value - omit will cause the name/ID will be generated and returned)
* @param {(Boolean|Object|String)} [throws] One of the following values (supersedes any `throws` parameters passed during construction):
* - `true` to throw any errors and instantly stop any further execution
* - `false` to catch any errors that are thrown
* - `Object` an object containing the following properties:
* - - `invert` true to catch errors when matches are made, false/omit to throw errors when matches are made
* - - `matches` An array that contains any of the following:
* - - - `system` A string value equal to "system" that will match when an error is a type within {@link Asynchro.systemErrorTypes}
* - - - `Object` An object that contains property names/values that will be matched against property name/values in the caught error.
* When invert is true, all names/values on the caught error must match the names/values on the object in order to be
* **caught/captured** in {@link Asynchro.errors}. When invert is false/omitted, all names/values on the caught error must match
* all of the names/values on the object in order to be **rethrown**.
* - `system` a single `matches` string (invert defaults to false)
*
* Re-thrown errors will set `Error.cause` to the originally thrown error.
* @param {Function} fn The function to queue for asynchronicity
* @param {...*} args Aguments that will be passed into the queued function
* @returns {String} The queued name/ID (either passed or generated)
*/
backgroundThrowsOverride(name, throws, fn, ...args) {
const asy = internal(this);
return asynchroQueue(asy.this, false, throws, name, fn, args, asy.at.trk.errors);
}
/**
* Each `verify` is an `async` function that will be called after the queued task that matches the registered _name_ has ran. **It's important to note
* that _parallel_ tasks will call the registered _verify_ function TWICE. Once when the `async` function is invoked (`isPending === true`) and
* antoher time when `await` completes (`isPending !== true`).** There is only one _verify_ per registered name. So, registering _verify_ multiple
* times for the same name will overwrite any _verify_ functions that were set by previous calls.
* @param {String} name Either the name designated as the property name or the `Function.name` from the function used when calling `parallel` or `series`
* @param {Function} fn An `async function` that will accept a single object argument that contains the follwing properties:
* 1. `error` A _mutable_ error object that occurred during execution of the queued task (changes to this value will be reflected in the final
* {@link Asynchro.result} or re-thrown depending on the rules/`throws` set for re-throwing/catching defined on the corresponding queued task).
* Setting the _error_ will have the same effect as throwing an Error from within the _verify_ function.
* 2. `result` A _mutable_ result value returned from the queued task execution (changes to this value will be reflected in the final
* {@link Asynchro.result}).
* 3. `isPending` An _immutable_ boolean value indicating the task has not yet returned from `await`. The value will always be `false` for **series**
* tasks since they `await` before calling the _verify_. When a **parallel** task is called, but not yet returned from `await` the value will be `true`.
* In the subsequent **parallel** call to the _verify_ function the value will be `false` since `await` has completed. When **background** tasks are
* called the value will always be `true` since they will never `await` on the task to complete.
* 4. `isParallel` An _immutable_ boolean value indicating if the task was ran in **parallel** or **series**
* 5. `isBackground` An _immutable_ boolean value indicating if the task was ran in the **background** (i.e. calls the task without `await`)
* 6. `event` An _immutable_ event name defined when the task originated from the function returned by {@link Asynchro.promisifyEventTarget}
* 7. `name` An _immutable_ string value reflecting the original name passed into {@link Asynchro.verify}
* 8. `operation` An _immutable_ string value reflecting the original function name of the function passed into {@link Asynchro.verify}
* 9. `message` A write-only _mutable_ string value that will override the default message that will be added to {@link Asynchro.messages}
*
* The return value should be one of the following:
* 1. `false` will stop execution of pending tasks that have been queued and {@link Asynchro.status} will be set to {@link Asynchro.STOPPED}.
* 2. Another `Asynchro` instance that will cause the current queue to stop while the new queue will take over via `await` {@link Asynchro.run}.
* {@link Asynchro.status} will be set to {@link Asynchro.TRANSFERRED} with any {@link Asynchro.messages} and/or {@link Asynchro.errors}
* being appended to the new queue instance. Also, {@link Asynchro.result} will be merged into the new queue instance.
* 3. Any other value will have no impact.
*
* The _verify_ `this` reference will point to the {@link Asynchro} instance
*
* **NOTE:** There may be some residuale parallel/concurrent/background functions that were already running prior to the queue being stopped that
* may still be running after a queue has been stopped/transferred by _verify_.
*/
verify(name, fn) {
if (!name || !name.trim()) throw new Error(`Task verify must designate a name that matches a call to parallel/series, not "${name}"`);
if (!fn || typeof fn !== 'function') throw new Error(`Verify must be a Function, not ${fn}`);
const asy = internal(this);
asy.at.trk.verify[name] = fn;
}
/**
* Registers a handler function that will be executed once {@link Asynchro.run} has completed. Only one handler is allowed per instance
* and will overwrite any end handler that has been previously been set.
* @param {Function} fn A _synchronous_ `function` that will accept a single argument that will be either set to a new {@link Asynchro} instance when
* execution is being transferred to a new queue (i.e. {@link Asynchro.status} is set to {@link Asynchro.TRANSFERRED}) or omitted when it's not.
* `this` will reference the current {@link Asynchro} instance. Any errors that occur within the function will be thrown.
*/
set endHandler(fn) {
if (!fn || typeof fn !== 'function') throw new Error(`End handler must be a Function, not ${fn}`);
const asy = internal(this);
asy.at.endHandler = fn;
}
/**
* A one-time execution run of all queued asynchronous functions in insertion order with parallel/concurrent running simultaneously and series
* tasks running in succession. Any queued {@link Asynchro.background} tasks will continue to run after {@link Asynchro.run} completes,
* possibly accumulating additional {@link Asynchro.errors} as those tasks complete (see {@link Asynchro.backgroundWaiter} to wait for
* any background tasks to finish completing).
* @async
* @returns {Object} The result from {@link Asynchro.result}
*/
async run() {
const asy = internal(this);
if (asy.at.status !== Asynchro.QUEUEING) throw new Error(`To respond status must be ${Asynchro.QUEUEING}, not ${asy.at.status}`);
if (!asy.at.trk.que.length) throw new Error(`Nothing to run/execute`);
asy.at.status = Asynchro.RUNNING;
const rtn = await asynchro(asy.at.trk, asy.this);
if (asy.at.trk.errors.length) asy.at.status = Asynchro.FAILED;
else if (rtn.done === false || rtn.tx) asy.at.status = Asynchro.STOPPED;
else asy.at.status = Asynchro.SUCCEEDED;
asy.at.trk.que.length = 0; // clear queue since the queue is stopped before completing
asy.at.trk.verify = null; // reset verify functions
var rslt;
if (rtn.tx) { // migrate queue state and run returned queue that execution will be transferred to
asy.at.trk.waiting = 0; // clear waiting since there may be some tasks waiting before transfer
const asyn = internal(rtn.tx);
const errs = asy.at.trk.errors, nerrs = asyn.at.trk.errors, msgs = asy.at.trk.messages, nmsgs = asyn.at.trk.messages;
asyn.at.trk.errors = errs.length ? (nerrs.length && errs.concat(nerrs)) || errs : nerrs;
asyn.at.trk.messages = msgs.length ? (nmsgs.length && msgs.concat(nmsgs)) || msgs : nmsgs;
//const bgs = asy.at.trk.backgrounds, nbgs = asyn.at.trk.backgrounds;
//asyn.at.trk.backgrounds = bgs.length ? (nbgs.length && [...new Set(bgs.concat(nbgs))]) || bgs.slice() : nbgs;
if (asyn.at.trk.rslt !== asy.at.trk.rslt) merge(asyn.at.trk.rslt, asy.at.trk.rslt, { deep: true });
asy.at.status = Asynchro.TRANSFERRED;
if (asy.at.endHandler) asy.at.endHandler.call(asy.this, asyn.this);
// need to accumulate the transfer instances before the the next run
if (asy.at.trk.brchs) asy.at.trk.brchs.push(asyn);
else asy.at.trk.brchs = [asy, asyn];
asyn.at.trk.brchs = asy.at.trk.brchs;
if (arguments.length) await asyn.this.run.apply(asyn.this, arguments) /*<- for extending class args*/
else await asyn.this.run();
rslt = asyn.at.trk.rslt;
} else {
if (asy.at.endHandler) asy.at.endHandler.call(asy.this);
rslt = asy.at.trk.rslt;
}
return rslt;
}
/**
* Waits for any pending {@link Asynchro.background} functions to complete and captures the results/caught errors.
* @example
* const ax = new Asynchro({});
* ax.background('myBgTask', myAsyncFunc, myAsyncFuncArg1, myAsyncFunc2);
* // ...other queued tasks?
* await ax.run();
* // now that Asynchro.run has completed we can optionally wait for the background tasks to complete
* // NOTE: awlays use return Asynchro instance in case branching took place
* const abx = await ax.backgroundWaiter();
* // if errors are caught, should print out errors thrown from the background async function
* for (let error of abx.errors) console.error(error);
* // if no error for myBgTask, should print out the return value from the background async function
* console.log(abx.result.myBgTask);
* @async
* @param {(Object|Boolean)} [resultObj=true] Either the object where the background results will be set or `true` to use the
* {@link Asynchro.result} (may be from a different {@link Asynchro} instance when branching). Each property name that matches the
* _name_ passed into the original call to {@link Asynchro.background} that queued the _background_ function will be set on the
* _result_ object.
* @returns {Asynchro} Either the {@link Asynchro} instance that the `backgroundWaiter` was called from, or the __last__
* {@link Asynchro} instance returned in the chain of branching/transfer operations using {@link Asynchro.verify}
*/
async backgroundWaiter(resultObj = true) {
const asy = internal(this), brchs = asy.at.trk.brchs || [asy];
if (!brchs || !brchs.length) return;
var rslt, result, thiz;
for (let tx of brchs) {
thiz = tx.this;
result = resultObj === true ? tx.this.result : resultObj;
for (let itm of tx.at.trk.backgrounds) {
if (!itm.backgroundPromise) throw new Error(`Missing "backgroundPromise" on item ${itm.name || itm.fn.name || itm.fn.toString()}`);
rslt = await itm.backgroundPromise;
if (result && itm.name) result[itm.name] = rslt;
tx.at.trk.waitingBackground--;
}
tx.at.trk.backgrounds.length = 0; // wipe all background queues
}
brchs.length = 0; // wipe all transfer instances
return thiz;
}
/**
* Resolves to a previously completed result value from a queued task function so the results from one task can be passed into subsequent
* tasks during execution via {@link Asynchro.run}
* @example
* const ax = new Asynchro({});
* ax.series('one', async () => {
* // other async operations here
* return { array: [1] };
* });
* ax.series('two', async (a) => {
* // other async operations here
* console.log(a); // prints out 1
* }, ax.arg('one.array[0]'));
* await ax.run();
* @param {String} name The name given for the task where the result will be stored as a property of the {@link Asynchro.result} object.
* Can use dot notation to express a path to other objects (e.g. `someObject.someOtherObject.someValue` would equate to
* `asynchro.result.someObject.someOtherObject.someValue` once the queued task function is executed)
* @returns {ResultArg} The {@link ResultArg}
*/
arg(name) {
if (!name || typeof name !== 'string') throw new Error(`Invlaid name: ${name}`);
return new ResultArg(name);
}
/**
* The accumulated message(s) gathered while running queued tasks during a {@link Asynchro.run}
* @param {String} [delimiter] The delimter to use between messages
* @returns {String} The cumulative messages
*/
messages(delimiter) {
return internal(this).at.trk.messages.join(delimiter);
}
/**
* Determines if an `Error` or a `Function` construct to an `Error` will be thrown when encountered during an execution run
* @param {(Error|Function)} errorOrType Either an `Error` or a `Function` construct to an Error
* @param {Boolean} [throwWhenTrue] `true` to actually throw the error when `errorOrType` is an actual `Error` and it is determined that the error should throw
* @returns {Boolean} Returns true if the `Error` or `Function` construct to an `Error` will be thrown when encountered during an execution run
*/
throwsError(errorOrType, throwWhenTrue) {
const asy = internal(this);
return throwsError(asy.at.at.throws, errorOrType, throwWhenTrue, asy.this.systemErrorTypes);
}
/**
* An immutable array of error type constructs that will be thrown when they are encountered during queue execution
* using `instanceof` on the thrown error (used by `system` values for throwing in {@link Asynchro.throwsError})
* @type {Function[]}
*/
get systemErrorTypes() {
return ERROR_TYPES_SYSTEM;
}
/**
* The result of all of the cumulative execution results that had designated names assigned
* @type {Object}
*/
get result() {
return internal(this).at.trk.rslt;
}
/**
* An immutable array of all the caught errors encountered during execution
* @type {Error[]}
*/
get errors() {
return internal(this).at.trk.errors;
}
/**
* The total number of tasks queued for execution
* @type {Integer}
*/
get count() {
return internal(this).at.trk.que.length;
}
/**
* The number of tasks queued that are _waiting_ for execution excluding {@link Asynchro.background} tasks
* (see {@link Asynchro.waitingBackground}).
* @type {Integer}
*/
get waiting() {
return internal(this).at.trk.waiting;
}
/**
* The number of {@link Asynchro.background} tasks queued that are _waiting_ for execution. __NOTE:__ If
* {@link Asynchro.backgroundWaiter} is never called the count will remain indefinitely.
* @type {Integer}
*/
get waitingBackground() {
return internal(this).at.trk.waitingBackground;
}
/**
* The current execution status
* @type {String}
*/
get status() {
return internal(this).at.status;
}
/**
* The status indicating that the queue is available for tasks to be added and is waiting to be ran
* @type {String}
*/
static get QUEUEING() {return 'QUEUEING'; }
/**
* The status indicating that the queue is sealed from adding new tasks and is waiting to complete
* @type {String}
*/
static get RUNNING() {return 'RUNNING'; }
/**
* The status indicating that the queue was ran, but failed to complete
* @type {String}
*/
static get FAILED() { return 'FAILED'; }
/**
* The status indicating that the queue has successfully ran to completion
* @type {String}
*/
static get SUCCEEDED() { return 'SUCCEEDED'; }
/**
* The status indicating that the queue has been stopped before completing
* @type {String}
*/
static get STOPPED() { return 'STOPPED'; }
/**
* The status indicating that the queue has been transferred to another queue before completing
* @type {String}
*/
static get TRANSFERRED() { return 'TRANSFERRED'; }
/**
* The default system error types that will be used for the `system` error type
* @type {Function[]}
*/
static get DEFAULT_SYSTEM_ERROR_TYPES() { return ERROR_TYPES_SYSTEM; }
/**
* Promisifies event(s) fired from a event target
* @param {Object} target The object that will fire event(s)
* @param {Integer} [tko=60000] The timeout delay in milliseconds to wait for the event before _rejecting_ or
* _resolving_ the promise (set to zero for _unlimited_ timeout or override using `events[].tko`)
* @param {Integer} [eventMax=1] The number of times any one of the events must fire before the promise is resolved
* @param {Integer} [eventErrorMax=1] Rhe number of times any one of the events must fire that contain an `Error` as
* the first argument before the promise is rejected with that `Error` (or `Error[]` if there are more than one)
* @param {Boolean} [implyError=true] `true` will automatically listen for an `error` events that will reject or resolve
* @param {Boolean} [resolveOnTimeout=false] `true` to _resolve_ when the `tko` timeout is reached, false to _reject_
* and uses the passed `tko` for the timeout delay
* @returns {Function} The function that will listen for events to fire before resolving/rejecting with either an array
* of arguments passed into the listener, a single value when the listener was passed one argument, `undefined` when
* the listener was not passed any arguments or an `Object` generated using `listenerParams` to map the argument values
* passed into the listener in the order they are received. The function accepts the following arguments:
* - `event` either the event name that will be listened to or an object that will override
* - `listenerParams` an optional `String[]` of parameter names for the given function that will be used as the
* property names on the resolved promise object (or `Object[]` when `eventMax > 1`), a `Function` to extract the
* property names from, or omit/`false` to simply use an array of argument values when resolving the promise
* @example
* // 30 sec timeout, 1 event max (default), 1 error event max (default)
* const listenAsync = Asynchro.promisifyEventTarget(myEventTarget, 30000);
* setTimeout(() => {
* myEventTarget.dispatchEvent('my-event-1', 'done');
* myEventTarget.dispatchEvent('my-event-2', 1, 2, 3);
* myEventTarget.dispatchEvent('my-event-2', 4, 5, 6);
* // my-event-2 will never set the following since it exceeds event max of 1
* myEventTarget.dispatchEvent('my-event-2', 'not set');
* myEventTarget.dispatchEvent('my-event-3', 1, 2, 3);
* myEventTarget.dispatchEvent('my-event-4', 'a', 'b', 'c');
* myEventTarget.dispatchEvent('my-event-4', 'd', 'e', 'f');
* }, 10);
* // run in parallel
* const p1 = listenAsync('my-event-1');
* const p2 = listenAsync({ name: 'my-event-2', eventMax: 2 });
* const p3 = listenAsync('my-event-3', ['one', 'two', 'three']);
* const p4 = listenAsync({ name: 'my-event-4', eventMax: 2 }, ['name1', 'name2', 'name3']);
* const p4x = listenAsync({ name: 'my-event-4', eventMax: 2 }, function demoNames(name1, name2, name3){});
* console.log(await p1); // done
* console.log(await p2); // [[1, 2, 3], [4, 5, 6]]
* console.log(await p3); // { one: 1, two: 2, three: 3 }
* console.log(await p4); // [{ name1: 'a', name2: 'b', name3: 'c' }, { name1: 'd', name2: 'e', name3: 'f' }]
* console.log(await p4x); // [{ name1: 'a', name2: 'b', name3: 'c' }, { name1: 'd', name2: 'e', name3: 'f' }]
* @example
* // example setup
* const tko = 30000, delay = 10, a = 1, b = 2, c = 3, d = 4, e = 5, l1 = 200, l2 = 300;
* function multiply(a, b, c) {
* return new Promise((resolve, reject) => {
* setTimeout(() => {
* resolve({ m1: 10 * (a || 0), m2: 20 * (b || 0), m3: 30 * (c || 0) });
* }, delay);
* });
* }
* // class just for example purposes
* class MyEventTarg {
* constructor() {
* this.listeners = {};
* }
* addListener(event, listener) {
* this.listeners[event] = this.listeners[event] || [];
* this.listeners[event].push(listener);
* }
* removeListener(event, listener) {
* var idx = -1;
* if (!(event in this.listeners)) idx;
* for (let lsn of this.listeners[event]) if (++idx && lsn === listener) {
* this.listeners[event].splice(idx, 1);
* return idx;
* }
* }
* dispatchEvent(type) {
* if (this.listeners[type]) {
* // pass all the args to the listener except the 1st arg that is the event name
* const args = Array.prototype.slice.call(arguments, 1);
* for (let lsn of this.listeners[type]) lsn.apply(this, args);
* }
* }
* }
* const trg = new MyEventTarg();
*
* // 30 sec timeout, 1 event max (default), 1 error event max (default)
* const listenAsync = Asynchro.promisifyEventTarget(trg, tko);
* const ax = new Asynchro({});
* ax.parallel('one', multiply, a, b, c);
* ax.series('two', listenAsync, 'event-1');
* ax.series('three', multiply, d, e);
* ax.parallel('four', listenAsync, 'event-2');
*
* setTimeout(() => {
* trg.dispatchEvent('event-1', l1);
* setTimeout(() => {
* trg.dispatchEvent('event-2', l2);
* }, delay * 2); // delay must be between delay on "two"/"three" and "tko"
* }, delay); // delay must be between delay on "two" and "tko" ("one" is in parallel)
*
* const rslt = await ax.run();
* // { one: { m1: 10, m2: 40, m3: 90 }, two: 200, three: { m1: 40, m2: 100, m3: 0 }, four: 300 }
* console.log(rslt);
*/
static promisifyEventTarget(target, tko = 60000, eventMax = 1, eventErrorMax = 1, implyError = true, resolveOnTimeout = false) {
const on = target['once'] && eventMax === 1 ? 'once' : (target['on'] && 'on') || (target['addListener'] && 'addListener')
|| (target['addEventListener'] && 'addEventListener');
const off = (target['off'] && 'off') || (target['removeListener'] ? 'removeListener' : target['removeEventListener'] && 'removeEventListener');
const promisifierEvent = function promisifierEvent(event, listenerParams) {
const isEvtStr = event && typeof event === 'string';
if (!event) throw new Error(`Invlaid event: ${event}`);
if (!isEvtStr && (!event.name || typeof event.name !== 'string')) throw new Error(`Event name must be a non-empty string, not: ${event.name}`);
if (!on || !off) throw new Error(`Invalid event target ${target}`);
if (listenerParams && typeof listenerParams === 'function') listenerParams = Asynchro.extractFuncArgs(listenerParams);
var timers = {}, listeners = {}, errors, results, counts = { events: 0, errors: 0 }, it = {};
const clearAll = (done) => {
for (let handle in timers) clearTimeout(timers[handle]);
timers = null;
for (let event in listeners) target[off](event, listeners[event]);
listeners = null;
if (done) it.done = true;
};
const addlistener = (event) => {
listeners[event.name] = function listener(err) {
const isError = err && err instanceof Error;
if ((isError && ++counts.errors >= event.errorMax) || (!isError && ++counts.events >= event.max)) {
clearAll(true);
if (counts.errors > event.errorMax || counts.events > event.max) return;
}
if (isError && event.errorMax !== 1) {
errors = errors || [];
if (arguments.length > 1) err[event.name] = Array.prototype.slice.call(arguments, 1);
errors.push(err);
} else if (!isError) {
var args;
if (listenerParams) {
args = {};
var fni = -1;
for (let name of listenerParams) {
args[name] = arguments[++fni];
}
} else args = event.max === 1 && arguments.length === 1 ? arguments[0] : arguments.length > 0 ? Array.prototype.slice.call(arguments) : undefined;
if (results) results.push(args);
else if (event.max === 1) results = args;
else results = [args];
}
if ((isError && counts.errors !== event.errorMax) || (!isError && counts.events !== event.max)) return;
if (isError) it.reject(errors || err);
else it.resolve(results);
};
target[on](event.name, listeners[event.name]);
if (!event.tko) return;
timers[event.name] = setTimeout(() => {
clearAll();
if (it.done) return;
it.done = true;
const err = new Error(`Promisify events for event "${event.name}" timeout at ${event.tko}ms`);
if (event.resolveOnTimeout) it.resolve(err);
else it.reject(err);
}, event.tko);
};
const name = isEvtStr ? event : event.name, etko = !isEvtStr && event.hasOwnProperty('tko') ? event.tko : tko;
const max = !isEvtStr && event.hasOwnProperty('eventMax') ? event.eventMax : eventMax;
const errorMax = !isEvtStr && event.hasOwnProperty('eventErrorMax') ? event.eventErrorMax : eventErrorMax;
const rlvOnTko = !isEvtStr && event.hasOwnProperty('resolveOnTimeout') ? event.resolveOnTimeout : resolveOnTimeout;
addlistener({ name, tko: etko, max, errorMax, resolveOnTimeout: rlvOnTko });
if (implyError && name !== 'error') addlistener({ name: 'error', max, errorMax });
return new Promise((resolve, reject) => {
it.done = false;
it.resolve = resolve;
it.reject = reject;
});
}
Object.defineProperty(promisifierEvent, 'isPromisifiyEvent', { value: true });
return promisifierEvent;
}
/**
* Takes an object's function with the last argument being a callback function which accepts __multiple parameter arguments__ (1st argument being
* an `Error`) and converts it to a promise. Rejects when an `Error` is passed in as the first argument or resolves using the arguments passed
* into the callback (excluding the 1st error parameter). Also supports `this` reference within the passed function (set to the passed `obj`)
* @param {Object} obj The object that contains the function that will be promisfied
* @param {String} funcName The name of the function property in the `obj`
* @param {(String[]|Function)} [funcParams] A `String[]` of parameter names for the given function that will be used as the property names on the
* resolved promise object (should not include the error or callback parameter names), a `Function` to extract the property names from, or
* omit/`false` to simply use an array of argument values when resolving the promise
* @returns {Function} A `function(..args)` that returns a promise
*/
static promisifyCallback(obj, funcName, funcParams) {
return function promisifierCallback() {
const args = Array.prototype.slice.call(arguments);
// callback needs to be in the correct position regardless of what was passed
for (let ai = arguments.length, ln = obj[funcName].length - 1; ai < ln; ++ai) args.push(undefined);
if (funcParams && typeof funcParams === 'function') {
funcParams = Asynchro.extractFuncArgs(funcParams);
funcParams.shift(); // first argument should be the error
funcParams.pop(); // last argument should be the callback
}
return new Promise((resolve, reject) => {
args.push(function promisifierCb(err) {
if (err) reject(err);
else if (funcParams) {
const rtn = {};
var fni = 0; // skip error argument
for (let name of funcParams) {
rtn[name] = arguments[++fni];
}
resolve(rtn);
} else resolve(Array.prototype.slice.call(arguments, 1)); // remove error argument and return array
});
obj[funcName].apply(obj, args);
});
};
}
/**
* Extracts the argument names that a function accepts
* @param {Function} fn The function to extract paramter names from
* @returns {String[]} The array of function parameter names in the order that they are defined
*/
static extractFuncArgs(fn) {
if (!fn || typeof fn !== 'function') return [];
var ftxt = fn.toString().replace(FN_COMMENTS, '').replace(FN_DEFAULT_PARAMS, '').replace(FN_ARROWS, '($1)');
const rtn = ftxt.slice(ftxt.indexOf('(') + 1, ftxt.indexOf(')')).match(FN_PARAMS_SEP);
return rtn || [];
}
}
// TODO : ESM remove the following line...
module.exports = Asynchro;
/**
* Queues a promise for **series/paralel** relative to other promises in the queue
* @private
* @ignore
* @param {Asynchro} asyi The `async` processor
* @param {Boolean} series `true` to run in **series** relative to other tasks, false to run in **parallel**
* @param {(Boolean|Object|String)} [throws] One of the following values (supersedes any `throws` parameters passed during construction):
* - `true` to throw any errors and instantly stop any further execution
* - `false` to catch any errors that are thrown
* - `Object` an object containing the following properties:
* - - `invert` true to catch errors when matches are made, false/omit to throw errors when matches are made
* - - `matches` An array that contains any of the following:
* - - - `system` A string value equal to "system" that will match when an error is a type within {@link Asynchro.systemErrorTypes}
* - - - `Object` An object that contains property names/values that will be matched against property name/values in the caught error.
* When invert is true, all names/values on the caught error must match the names/values on the object in order to be
* **caught/captured** in {@link Asynchro.errors}. When invert is false/omitted, all names/values on the caught error must match
* all of the names/values on the object in order to be **rethrown**.
* - `system` a single `matches` string (invert defaults to false)
*
* Re-thrown errors will set `Error.cause` to the originally thrown error.
* @param {String} [name] The name given for the task where the result will be stored as a property of the {@link Asynchro.result} object
* (omit to prevent results from being set from function return value - a name/ID will be generated and returned)
* @param {Function} fn The function to queue for asynchronicity
* @param {*} args Arguments that will be passed into the queued function
* @param {Error[]} [bgErrors] An array that will store caught errors from queued background functions. The queued task function will __not__
* wait before contining to the next task in the queue (omit when the function is __not__ a background task).
* @returns {String} The queued name/ID (either passed or generated)
*/
function asynchroQueue(asyi, series, throws, name, fn, args, bgErrors) {
const asy = internal(asyi), isBg = !!bgErrors;
if (!fn || typeof fn !== 'function') {
throw new Error(`A ${series ? 'series' : isBg ? 'background' : 'parallel'} task must be a Function, but found ${typeof fn} (${fn})`);
}
if (asy.at.status !== Asynchro.QUEUEING) {
throw new Error(`A ${series ? 'series' : isBg ? 'background' : 'parallel'} must be in status ${Asynchro.QUEUEING}, not ${asy.at.status}`);
}
throws = throws === true || throws === false ? throws : (throws && typeof throws === 'object' && throws) || (throws && { matches: throws });
const noResult = (!name || !name.trim()) && (name = guid()) || isBg ? true : false;
// TODO : should async allow "this" to be passed?
const itm = { series, index: asy.at.trk.que.length, isBackground: isBg, throws, name, fn, args, noResult, errorMetaName: asy.at.errorMetaName };
itm.noAwait = itm.isBackground;
itm.event = fn.isPromisifiyEvent && args[0];
if (isBg && itm.throws !== true) setBackgroundFunction(itm, asyi.systemErrorTypes, bgErrors);
asy.at.trk.que.push(itm);
if (itm.isBackground) asy.at.trk.waitingBackground++;
else asy.at.trk.waiting++;
return name;
}
/**
* Runs/Executes all queued asynchronous functions in insertion order with parallel/concurrent running simultaneously
* @private
* @ignore
* @param {Object} trk The tracking object from {@link Asynchro}
* @param {Asynchro} [asyi] The {@link Asynchro} instance
* @returns {Object} An object that contains the following properties:
* - `done`: _true_ when ran to completion, _false_ when the process has been stopped before finishing
* - `tx`: the first {@link Asynchro} instance that was returned from {@link asyncHandler} that queue execution should
* be transferred to
* - `item`: the queued _item_ where the stop/transfer occurred
*/
async function asynchro(trk, asyi) {
var pends = [], rtn = { done: true }, hdl;
for (let itm of trk.que) {
hdl = await asyncHandler(trk, asyi, itm, pends); // execute the queued tasks
if (!itm.promise && !itm.backgroundPromise) trk.waiting--;
if (hdl === false) { // stop
rtn.done = hdl;
rtn.item = itm;
break;
} else if (asyi !== hdl && hdl instanceof Asynchro) { // transfer
rtn.done = false;
rtn.item = itm;
rtn.tx = hdl;
break;
}
}
for (let itm of pends) {
hdl = await asyncHandler(trk, asyi, itm, pends); // wait for pending parallel/concurrent executions to complete
trk.waiting--;
if (rtn.done === false || rtn.tx instanceof Asynchro) continue; // do not override first stop
if (hdl === false) {
rtn.done = hdl;
rtn.item = itm;
} else if (asyi !== hdl && hdl instanceof Asynchro) {
rtn.done = false;
rtn.item = itm;
rtn.tx = hdl;
}
}
return rtn;
}
/**
* Processes a queued `async function` from {@link Asynchro}
* @private
* @ignore
* @param {Object} trk The _private_ tracking object from {@link Asynchro}
* @param {Asynchro} asyi The `Asynchro` instance
* @param {Object} itm The queued `async` item from {@link asynchroQueue}
* @param {Object[]} pends The pending parallel/concurrent items- each originating from {@link asynchroQueue}
* @returns {(Boolean|Asynchro)} `false` or another `Asynchro` instance should **stop** iteration
*/
async function asyncHandler(trk, asyi, itm, pends) { // return false or another Asynchro instance should stop iteration
var rtn = true, msg;
const it = {}, type = itm.series ? 'series' : itm.isBackground ? 'background' : 'parallel';
if (itm.throws === true && itm.noAwait) handleAsync(asyi, itm, pends, trk.backgrounds);
else if (itm.throws === true) it.result = await handleAsync(asyi, itm, pends, trk.backgrounds);
else try {
it.result = await handleAsync(asyi, itm, pends, trk.backgrounds);
} catch (err) {
defineItemMeta(err, itm, itm.promise instanceof Promise, itm.errorMetaName);
if (itm.throws) throwsError(itm.throws, err, true, asyi.systemErrorTypes);
it.error = err;
}
const pendPromise = itm.promise instanceof Promise;
if (itm.name && trk.verify[itm.name]) try { // verify functions should only be called once the promise is await is performed
defineItemMeta(it, itm, pendPromise);
Object.defineProperty(it, 'message', { set: msgOrErr => msg = msgOrErr, enumerable: true });
rtn = await trk.verify[itm.name].call(asyi, it);
rtn = asyi !== rtn && rtn instanceof Asynchro ? rtn : rtn !== false;
} catch (verifyError) {
defineItemMeta(verifyError, itm, pendPromise, itm.errorMetaName);
if (itm.throws) throwsError(itm.throws, verifyError, true, asyi.systemErrorTypes);
if (verifyError !== it.error) {
Object.defineProperty(verifyError[itm.errorMetaName], 'cause', { value: it.error, enumerable: true });
if (trk.log) trk.log(['asynchro', 'warn', 'verify'], { stack: verifyError.stack, error: verifyError });
it.error = verifyError;
}
}
if (it.error) {
if (trk.log && (!it.error[itm.errorMetaName] || !it.error[itm.errorMetaName].cause)) {
trk.log(['asynchro', 'warn'], { stack: it.error.stack, error: it.error });
}
trk.errors.push(it.error);
}
if (pendPromise) return rtn; // need to wait for the promise to complete
if (!itm.noResult && itm.name && trk.rslt && typeof it.result !== 'undefined') trk.rslt[itm.name] = it.result;
if (!it.error && trk.log) trk.log(['asynchro', 'result', 'debug'], { type, name: itm.name, operation: itm.fn.name, result: it.result });
appendMessage(asyi, itm.name, itm.fn.name, msg || it.error || it.result);
return rtn;
}
/**
* Executes an `async function` queued from {@link asyncHandler}
* @private
* @ignore
* @param {Asynchro} asyi The `Asynchro` instance
* @param {Object} itm The queued `async` item from {@link asynchroQueue}
* @param {Object[]} pends The pending parallel/concurrent items- each originating from {@link asynchroQueue}
* @param {Object[]} [backgrounds] where background task items will be stored for future promise resolution
* @returns {*} The result from the function execution or `undefined` when the `item.promise` has been set/generated
*/
async function handleAsync(asyi, itm, pends, backgrounds) {
var rslt;
if (!itm.noResult && itm.args && asyi && asyi.result) resolveArgs(asyi, itm);
if (itm.series) {
rslt = itm.fn.apply(itm.thiz, itm.args);
if (rslt instanceof Promise) rslt = await rslt; // performance gain when not async function
} else if (itm.promise) {
rslt = await itm.promise;
itm.promise = true;
} else {
itm.promise = false; // in case the function fails
itm.promise = itm.fn.apply(itm.thiz, itm.args);
if (!(itm.promise instanceof Promise)) {
const msg = `Call to ${itm.name || ''}/${itm.fn.name} must return a Promise, not ${itm.promise}`;
itm.promise = false;
throw new Error(msg);
}
if (itm.isBackground) { // run in background, don't wait for promise
itm.backgroundPromise = itm.promise;
itm.promise = false;
// wait until background promise is pending in case the queue is stopped/transferred
if (backgrounds) backgrounds.push(itm);
} else pends.push(itm);
}
return rslt;
}
/**
* Proxies an `itm.fn` in order to handle re-throwing/catching the desired errors
* @private
* @ignore
* @param {Object} itm The queued `async` item from {@link asynchroQueue}
* @param {*} systemErrorTypes The {@link Asynchro.systemErrorTypes} that will be applied to the background function check
* @param {Error[]} errors The {@link Asynchro.errors} where caught errors will be added
*/
function setBackgroundFunction(itm, systemErrorTypes, errors) {
const func = itm.fn;
itm.fn = new Proxy(func, {
async apply(func, thiz, args) {
try {
return await (args.length ? func.apply(thiz, args) : thiz ? func.call(thiz) : func());
} catch (err) {
defineItemMeta(err, itm, false, itm.errorMetaName);
if (itm.throws) throwsError(itm.throws, err, true, systemErrorTypes);
errors.push(err);
}
},
get(trg, prop) {
return prop === 'name' ? func.name : Reflect.get(...arguments);
}
});
}
/**
* Defines read-only item metadata on a task object
* @private
* @ignore
* @param {Object} it The object to define the item metadata on
* @param {Object} itm The item where metadata will be extracted from
* @param {Boolean} pendPromise `true` when a promise is pending
* @param {String} [name] Instead of defining properties/values directly on `it` they will be set on a newly defined `it[name] = {}`
*/
function defineItemMeta(it, itm, pendPromise, name) {
var obj = it;
if (name) {
if (!obj[name]) Object.defineProperty(obj, name, { value: {}, enumerable: true });
obj = obj[name];
}
Object.defineProperty(obj, 'isPending', { value: !!pendPromise, enumerable: true });
Object.defineProperty(obj, 'isParallel', { value: !itm.series && !itm.isBackground, enumerable: true });
Object.defineProperty(obj, 'isBackground', { value: itm.isBackground, enumerable: true });
Object.defineProperty(obj, 'event', { value: itm.event, enumerable: true });
Object.defineProperty(obj, 'name', { value: itm.name, enumerable: true });
Object.defineProperty(obj, 'operation', { value: itm.fn.name, enumerable: true });
}
/**
* Resolves any arguments that are set in an item that are a {@link ResultArg} to the corresponding {@link Asynchro.result}
* path resolved value
* @private
* @ignore
* @param {Asynchro} asyi The {@link Asynchro} instance
* @param {Object} itm The queued `async` item from {@link asynchroQueue}
*/
function resolveArgs(asyi, itm) {
var argi = 0, names, mtch, val;
for (let arg of itm.args) {
if (arg instanceof ResultArg) {
// convert any obj['path']["to"][`value`][0] -> obj.path.to.value -> [ obj, path, to, value[0] ]
names = arg.name.replace(/\[['"`](\w+)['"`]\]/g, '.$1').split('.');
val = asyi.result;
for (let nm of names) {
if (typeof val !== 'object') break;
mtch = nm.match(/^([^\[]+)\[(\d+)\]$/);
nm = (mtch && mtch[1]) || nm;
if (mtch && mtch[2]) val = val[nm][parseInt(mtch[2])]; // should be an array with index
else val = val[nm];
}
itm.args[argi] = val; // argument should be resolved to the Asynchro result property value
}
argi++;
}
}
/**
* Adds a specified message to the commulative `message` on the {@link Asynchro.result} object
* @private
* @ignore
* @param {Asynchro} asyi The {@link Asynchro} instance
* @param {String} [name] The name of the task that was ran
* @param {(Error|Object|String)} [errorOrMessage] Either an `Error`, `{ message }` or message string that wil be appended to the overall messages
* @param {String} [operation] An operation name that ran the task, typically a function name
* @returns {String} The message added to the commulative messages
*/
function appendMessage(asyi, name, operation, errorOrMessage) {
const asy = internal(asyi);
if (asy.at.status !== Asynchro.RUNNING) throw new Error(`Message can only be added when status is ${Asynchro.RUNNING}, not ${asy.at.status}`);
const isError = errorOrMessage && errorOrMessage instanceof Error;
const includeErrorMsg = isError && asy.at.includeErrorMsgCheck ? asy.at.includeErrorMsgCheck(name, operation, errorOrMessage) : false;
const msg = (errorOrMessage && (!isError || includeErrorMsg) && (errorOrMessage.message || typeof errorOrMessage === 'string' ? errorOrMessage : '').replace(/"/g, "'"))
|| (isError && `Internal ERROR${name ? ` for ${name}` : ''}${operation && operation !== name ? `on operation: ${operation}`: ''}`) || '';
if (msg) asy.at.trk.messages.push(msg);
return msg;
}
/**
* Determines if an `Error` or a `Function` construct to an `Error` will be thrown when encountered during an execution run
* @private
* @ignore
* @param {(Boolean|Object|String)} [throws] One of the following values (supersedes any `throws` parameters passed during construction):
* - `true` to throw any errors and instantly stop any further execution
* - `false` to catch any errors that are thrown
* - `Object` an object containing the following properties:
* - - `invert` true to catch errors when matches are made, false/omit to throw errors when matches are made
* - - `matches` An array that contains any of the following:
* - - - `system` A string value equal to "system" that will match when an error is a type within {@link Asynchro.systemErrorTypes}
* - - - `Object` An object that contains property names/values that will be matched against property name/values in the caught error.
* When invert is true, all names/values on the caught error must match the names/values on the object in order to be
* **caught/captured** in {@link Asynchro.errors}. When invert is false/omitted, all names/values on the caught error must match
* all of the names/values on the object in order to be **rethrown**.
* - `system` a single `matches` string (invert defaults to false)
* @param {(Error|Function)} errorOrType Either an `Error` or a `Function` construct to an Error
* @param {Boolean} [throwWhenTrue] `true` to actually throw the error when `errorOrType` is an actual `Error` and it is determined that the error should throw
* @param {Function[]} systemErrorTypes Value from {@link Asynchro.systemErrorTypes}
* @returns {Boolean} Returns true if the `Error` or `Function` construct to an `Error` will be thrown when encountered during an execution run
*/
function throwsError(throws, errorOrType, throwWhenTrue, systemErrorTypes) {
const isError = errorOrType instanceof Error, isErrorType = !isError && typeof errorOrType === 'function', throType = typeof throws;
if (!isError && !isErrorType) return false;
if (throType === 'boolean') {
if (throws && throwWhenTrue && isError) throw errorOrType;
return throws;
}
if (throws.matches === 'system') {
if (Array.isArray(systemErrorTypes)) {
for (let stype of systemErrorTypes) {
if (errorOrType instanceof stype) {
if (throws.invert) return false; // any system error will capture
if (throwWhenTrue && isError) throw errorOrType; // any system error will throw
return true;
}
}
}
if (throws.invert) { // if not captured by prior checks, must be throw
if (throwWhenTrue && isError) throw errorOrType;
return true;
}
} else if (typeof throws.matches === 'object') {
for (let prop in throws.matches) {
if (errorOrType[prop] !== throws.matches[prop]) {
//console.log(`Property "${prop}" (invert = ${!!throws.invert}): "${errorOrType[prop]}" !== "${throws.matches[prop]}"`)
if (throws.invert) { // if not captured by prior checks, must be throw
if (throwWhenTrue && isError) throw errorOrType;
return true;
}
return false; // does not meet the throw criteria
}
}
if (!throws.invert) { // if not captured by prior checks, must be throw
if (throwWhenTrue && isError) throw errorOrType;
return true;
}
}
return false;
}
/**
* Result argument wrapper
* @private
* @ignore
*/
class ResultArg {
/**
* Constructor
* @param {String} name The name of the property in the {@link Asynchro} `result` object
*/
constructor(name) {
const rsa = internal(this);
rsa.at.name = name;
}
/**
* @returns {String} The name of the result argument
*/
get name() {
return internal(this).at.name;
}
}
/**
* Merges an object with the properties of another object *USE WITH CAUTION - merging can be an expensive operation depending on the
* source/destination objects
* @private
* @ignore
* @param {Object} dest The destination object where the properties will be added
* @param {Object} src The source object that will be used for adding new properties to the destination
* @param {Object} [opts] Merge options
* @param {Boolean} [opts.ctyp] Flag that ensures that source values are constrained to the same type as the destination values when present
* @param {Boolean} [opts.nou] Flag that prevents merge of undefined values
* @param {Boolean} [opts.non] Flag that prevents merge of null values
* @param {Boolean} [opts.deep] Flag that indicates that any objects/arrays found will be cloned instead of referenced
* @param {(Object[] | String[])} [opts.exc] Properties to exclude from the merge
* @param {(Object | String)} [opts.exc[]] Either object describing a property or a property name
* @param {String} [opts.exc[].prop] The property name to exclude
* @param {Integer} [opts.exc[].depth=Infinity] The depth level to exclude
* @param {Integer} [depth=1] The initial object depth used for exclusions
* @returns {Object} The destination object
*/
function merge(dest, src, opts, depth) {
if (!src || typeof src !== 'object') return dest;
opts = opts || {};
if (isNaN(depth)) depth = 1;
var keys = Object.keys(src);
var i = keys.length, dt, st;
while (i--) {
st = typeof src[keys[i]];
if (isNaN(keys[i]) && src.hasOwnProperty(keys[i]) &&
(!opts.nou || st !== 'undefined') &&
(!opts.non || src[keys[i]] !== null) &&
!(opts.ctyp && dest[keys[i]] != null && (dt = typeof dest[keys[i]]) !== 'undefined' && dt !== typeof src[keys[i]]) &&
(!opts.exc || !exclude(opts.exc, keys[i], depth))) {
if (opts.deep && src[keys[i]] !== null && st === 'object') {
if (Array.isArray(src[keys[i]])) dest[keys[i]] = src[keys[i]].slice();
else merge(dest[keys[i]] = {}, src[keys[i]], opts, depth + 1);
} else dest[keys[i]] = src[keys[i]];
}
}
return dest;
}
/**
* Generates formats a GUID
* @private
* @ignore
* @param {String} [value] When present, will add any missing hyphens (if `hyphenate=true`) instead of generating a new value
* @param {Boolean} [hyphenate=true] `true` to include hyphens in generated result
* @returns {String} The generated GUID
*/
function guid(value, hyphenate = true) {
const hyp = hyphenate ? '-' : '';
if (value) return hyphenate ? value.replace(/(.{8})-?(.{4})-?(.{4})-?(.{4})-?(.{12})/gi, `$1${hyp}$2${hyp}$3${hyp}$4${hyp}$5`) : value;
return `xxxxxxxx${hyp}xxxx${hyp}4xxx${hyp}yxxx${hyp}xxxxxxxxxxxx`.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// private mapping
let map = new WeakMap();
let internal = function(object) {
if (!map.has(object)) {
map.set(object, {});
}
return {
at: map.get(object),
this: object
};
};