@barchart/common-js
Version:
Library of common JavaScript utilities
224 lines (194 loc) • 6.05 kB
JavaScript
const assert = require('./assert');
module.exports = (() => {
'use strict';
/**
* Utilities for working with promises.
*
* @public
* @module lang/promise
*/
return {
/**
* Creates a composite promise which resolves normally or rejects is a specified
* amount of time elapses.
*
* @public
* @static
* @async
* @param {Promise} promise
* @param {Number} milliseconds
* @param {String=} description
* @returns {Promise<*>}
*/
async timeout(promise, milliseconds, description) {
return Promise.resolve()
.then(() => {
assert.argumentIsRequired(promise, 'promise', Promise, 'Promise');
assert.argumentIsRequired(milliseconds, 'milliseconds', Number);
assert.argumentIsOptional(description, 'description', String);
if (!(milliseconds > 0)) {
return Promise.reject('Unable to configure promise timeout, the "milliseconds" argument must be positive');
}
let timeoutToken = null;
const timeoutPromise = this.build((resolveCallback, rejectCallback) => {
timeoutToken = setTimeout(() => {
rejectCallback(description || `Promise timed out after ${milliseconds} milliseconds`);
}, milliseconds);
});
const userPromise = Promise.resolve()
.then(() => {
return promise;
}).then((result) => {
if (timeoutToken !== null) {
clearTimeout(timeoutToken);
}
return result;
}).catch((e) => {
if (timeoutToken !== null) {
clearTimeout(timeoutToken);
}
return Promise.reject(e);
});
return Promise.race([ userPromise, timeoutPromise ]);
});
},
/**
* A mapping function that works asynchronously. Given an array of items, each item through
* a mapping function, which can return a promise. Then, this function returns a single promise
* which is the result of each mapped promise.
*
* @public
* @static
* @async
* @param {Array} items - The items to map
* @param {Function} mapper - The mapping function (e.g. given an item, return a promise).
* @param {Number=} concurrency - The maximum number of promises that are allowed to run at once.
* @returns {Promise<Array>}
*/
async map(items, mapper, concurrency) {
return Promise.resolve()
.then(() => {
assert.argumentIsArray(items, 'items');
assert.argumentIsRequired(mapper, 'mapper', Function);
assert.argumentIsOptional(concurrency, 'concurrency', Number);
const c = Math.max(0, concurrency || 0);
let mapPromise;
if (c === 0 || items.length === 0) {
mapPromise = Promise.all(items.map((item) => Promise.resolve(mapper(item))));
} else {
let total = items.length;
let active = 0;
let complete = 0;
let failure = false;
const results = Array.of(total);
const executors = items.map((item, index) => {
return () => {
return Promise.resolve()
.then(() => {
return mapper(item);
}).then((result) => {
results[index] = result;
});
};
});
mapPromise = this.build((resolveCallback, rejectCallback) => {
const execute = () => {
if (!(executors.length > 0 && c > active && !failure)) {
return;
}
active = active + 1;
const executor = executors.shift();
executor()
.then(() => {
if (failure) {
return;
}
active = active - 1;
complete = complete + 1;
if (complete < total) {
execute();
} else {
resolveCallback(results);
}
}).catch((error) => {
failure = false;
rejectCallback(error);
});
execute();
};
execute();
});
}
return mapPromise;
});
},
/**
* Runs a series of functions sequentially (where each function can be
* synchronous or asynchronous). The result of each function is passed
* to the successive function and the result of the final function is
* returned to the consumer.
*
* @static
* @public
* @async
* @param {Function[]} functions - An array of functions, each expecting a single argument.
* @param {*=} input - The argument to pass the first function.
* @returns {Promise<*>}
*/
async pipeline(functions, input) {
return Promise.resolve()
.then(() => {
assert.argumentIsArray(functions, 'functions', Function);
return functions.reduce((previous, fn) => previous.then((result) => fn(result)), Promise.resolve(input));
});
},
/**
* Given an array of functions, where each returns a promise, runs
* the functions in sequential order, until one of the function
* returns a successful promise with a non-null result. Any
* rejected promise is ignored.
*
* @public
* @async
* @param {Function[]} executors
* @returns {Promise}
*/
async first(executors) {
return Promise.resolve()
.then(() => {
assert.argumentIsArray(executors, 'executors', Function);
return executors.reduce((previous, executor) => {
return previous.then((result) => {
if (result === null) {
return executor().catch(() => Promise.resolve(null));
} else {
return previous;
}
});
}, Promise.resolve(null));
});
},
/**
* Creates a new promise, given an executor.
*
* This is a wrapper for the {@link Promise} constructor; however, any error
* is caught and the resulting promise is rejected (instead of letting the
* error bubble up to the top-level handler).
*
* @public
* @static
* @async
* @param {Function} executor - A function which has two callback parameters. The first is used to resolve the promise, the second rejects it.
* @returns {Promise}
*/
async build(executor) {
return new Promise((resolve, reject) => {
try {
executor(resolve, reject);
} catch(e) {
reject(e);
}
});
}
};
})();