promise-tools
Version:
Tools for working with Promises
332 lines (283 loc) • 8.65 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TimeoutError = TimeoutError;
exports.delay = delay;
exports.defer = defer;
exports.series = series;
exports.parallelLimit = exports.parallel = parallel;
exports.map = map;
exports.timeout = timeout;
exports.whilst = whilst;
exports.doWhilst = doWhilst;
exports.retry = retry;
exports.default = void 0;
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
var hasProp = {}.hasOwnProperty;
var extend = function extend(child, parent) {
for (var key in parent) {
if (hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;
return child;
};
function TimeoutError(message) {
if (!(this instanceof TimeoutError)) {
return new TimeoutError(message);
}
if (Error.captureStackTrace) {
// This is better, because it makes the resulting stack trace have the correct error name. But, it
// only works in V8/Chrome.
TimeoutError.__super__.constructor.apply(this, arguments);
Error.captureStackTrace(this, this.constructor);
} else {
// Hackiness for other browsers.
this.stack = new Error(message).stack;
}
this.message = message;
this.name = "TimeoutError";
}
extend(TimeoutError, Error);
/*
* Returns a Promise which resolves after `ms` milliseconds have elapsed. The returned Promise will never reject.
*/
function delay(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
/*
* Returns a `{promise, resolve, reject}` object. The returned `promise` will resolve or reject when `resolve` or
* `reject` are called.
*/
function defer() {
var answer = {};
answer.promise = new Promise(function (resolve, reject) {
answer.resolve = resolve;
answer.reject = reject;
});
return answer;
}
/*
* Given an array, `tasks`, of functions which return Promises, executes each function in `tasks` in series, only
* calling the next function once the previous function has completed.
*/
function series(tasks) {
var results = [];
return tasks.reduce(function (series, task) {
return series.then(task).then(function (result) {
results.push(result);
});
}, Promise.resolve()).then(function () {
return results;
});
}
;
/*
* Given an array, `tasks`, of functions which return Promises, executes each function in `tasks` in parallel.
* If `limit` is supplied, then at most `limit` tasks will be executed concurrently.
*/
function parallel(tasks, limit) {
if (!limit || limit < 1 || limit >= tasks.length) {
return Promise.all(tasks.map(function (task) {
return Promise.resolve().then(task);
}));
}
return new Promise(function (resolve, reject) {
var results = [];
var currentTask = 0;
var running = 0;
var errored = false;
var startTask = function startTask() {
if (errored) {
return;
}
if (currentTask >= tasks.length) {
return;
}
var taskNumber = currentTask++;
var task = tasks[taskNumber];
running++;
Promise.resolve().then(task).then(function (result) {
results[taskNumber] = result;
running--;
if (currentTask < tasks.length && running < limit) {
startTask();
} else if (running === 0) {
resolve(results);
}
}, function (err) {
if (errored) {
return;
}
errored = true;
reject(err);
});
}; // Start up `limit` tasks.
for (var i = 0; i < limit; i++) {
startTask();
}
});
}
/*
* Given an array `arr` of items, calls `iter(item, index)` for every item in `arr`. `iter()` should return a
* Promise. Up to `limit` items will be called in parallel (defaults to 1.)
*/
function map(arr, iter, limit) {
var taskLimit = limit;
if (!limit || limit < 1) {
taskLimit = 1;
}
if (limit >= arr.length) {
taskLimit = arr.length;
}
var tasks = arr.map(function (item, index) {
return function () {
return iter(item, index);
};
});
return parallel(tasks, taskLimit);
}
/*
* Add a timeout to an existing Promise.
*
* Resolves to the same value as `p` if `p` resolves within `ms` milliseconds, otherwise the returned Promise will
* reject with the error "Timeout: Promise did not resolve within ${ms} milliseconds"
*/
function timeout(p, ms) {
return new Promise(function (resolve, reject) {
var timer = setTimeout(function () {
timer = null;
reject(new TimeoutError("Timeout: Promise did not resolve within ".concat(ms, " milliseconds")));
}, ms);
p.then(function (result) {
if (timer !== null) {
clearTimeout(timer);
resolve(result);
}
}, function (err) {
if (timer !== null) {
clearTimeout(timer);
reject(err);
}
});
});
}
/*
* Continually call `fn()` while `test()` returns true.
*
* `fn()` should return a Promise. `test()` is a synchronous function which returns true of false.
*
* `whilst` will resolve to the last value that `fn()` resolved to, or will reject immediately with an error if
* `fn()` rejects or if `fn()` or `test()` throw.
*/
function whilst(test, fn) {
return new Promise(function (resolve, reject) {
var lastResult = null;
var doIt = function doIt() {
try {
if (test()) {
Promise.resolve().then(fn).then(function (result) {
lastResult = result;
setTimeout(doIt, 0);
}, reject);
} else {
resolve(lastResult);
}
} catch (err) {
reject(err);
}
};
doIt();
});
}
function doWhilst(fn, test) {
var first = true;
var doTest = function doTest() {
var answer = first || test();
first = false;
return answer;
};
return whilst(doTest, fn);
}
;
/*
* keep calling `fn` until it returns a non-error value, doesn't throw, or returns a Promise that resolves. `fn` will be
* attempted `times` many times before rejecting. If `times` is given as `Infinity`, then `retry` will attempt to
* resolve forever (useful if you are just waiting for something to finish).
* @param {Object|Number} options hash to provide `times` and `interval`. Defaults (times=5, interval=0). If this value
* is a number, only `times` will be set.
* @param {Function} fn the task/check to be performed. Can either return a synchronous value, throw an error, or
* return a promise
* @returns {Promise}
*/
function retry(options, fn) {
var times = 5;
var interval = 0;
var attempts = 0;
var lastAttempt = null;
function makeTimeOptionError(value) {
return new Error("Unsupported argument type for 'times': ".concat(_typeof(value)));
}
if ('function' === typeof options) {
fn = options;
options = {};
} else if ('number' === typeof options) {
times = +options;
} else if ('object' === _typeof(options)) {
if ('number' === typeof options.times) {
times = +options.times;
} else if (options.times) {
return Promise.reject(makeTimeOptionError(options.times));
}
if (options.interval) {
if (options.interval === Infinity) {
return Promise.reject(new Error("'interval' may not be Infinity"));
}
interval = +options.interval;
}
} else if (options) {
return Promise.reject(makeTimeOptionError(options));
} else {
return Promise.reject(new Error('No parameters given'));
}
return new Promise(function (resolve, reject) {
var doIt = function doIt() {
Promise.resolve().then(function () {
return fn(lastAttempt);
}).then(resolve).catch(function (err) {
attempts++;
lastAttempt = err;
if (times !== Infinity && attempts === times) {
reject(lastAttempt);
} else {
setTimeout(doIt, interval);
}
});
};
doIt();
});
}
;
var _default = {
TimeoutError: TimeoutError,
delay: delay,
defer: defer,
series: series,
parallel: parallel,
parallelLimit: parallel,
map: map,
timeout: timeout,
whilst: whilst,
doWhilst: doWhilst,
retry: retry
};
exports.default = _default;