211 lines
5.6 KiB
JavaScript
211 lines
5.6 KiB
JavaScript
// Conditional and repeated task invocation for node and browser.
|
|
|
|
/*globals setTimeout, define, module */
|
|
|
|
(function (globals) {
|
|
'use strict';
|
|
|
|
if (typeof define === 'function' && define.amd) {
|
|
define(function () {
|
|
return tryer;
|
|
});
|
|
} else if (typeof module !== 'undefined' && module !== null) {
|
|
module.exports = tryer;
|
|
} else {
|
|
globals.tryer = tryer;
|
|
}
|
|
|
|
// Public function `tryer`.
|
|
//
|
|
// Performs some action when pre-requisite conditions are met and/or until
|
|
// post-requisite conditions are satisfied.
|
|
//
|
|
// @option action {function} The function that you want to invoke. Defaults to `() => {}`.
|
|
// If `action` returns a promise, iterations will not end until
|
|
// the promise is resolved or rejected. Alternatively, `action`
|
|
// may take a callback argument, `done`, to signal that it is
|
|
// asynchronous. In that case, you are responsible for calling
|
|
// `done` when the action is finished.
|
|
//
|
|
// @option when {function} Predicate used to test pre-conditions. Should return `false`
|
|
// to postpone `action` or `true` to perform it. Defaults to
|
|
// `() => true`.
|
|
//
|
|
// @option until {function} Predicate used to test post-conditions. Should return `false`
|
|
// to retry `action` or `true` to terminate it. Defaults to
|
|
// `() => true`.
|
|
//
|
|
// @option fail {function} Callback to be invoked if `limit` tries are reached. Defaults
|
|
// to `() => {}`.
|
|
//
|
|
// @option pass {function} Callback to be invoked after `until` has returned truthily.
|
|
// Defaults to `() => {}`.
|
|
//
|
|
// @option interval {number} Retry interval in milliseconds. A negative number indicates
|
|
// that subsequent retries should wait for double the interval
|
|
// from the preceding iteration (exponential backoff). Defaults
|
|
// to -1000.
|
|
//
|
|
// @option limit {number} Maximum retry count, at which point the call fails and retries
|
|
// will cease. A negative number indicates that retries should
|
|
// continue indefinitely. Defaults to -1.
|
|
//
|
|
// @example
|
|
// tryer({
|
|
// when: () => db.isConnected,
|
|
// action: () => db.insert(user),
|
|
// fail () {
|
|
// log.error('No database connection, terminating.');
|
|
// process.exit(1);
|
|
// },
|
|
// interval: 1000,
|
|
// limit: 10
|
|
// });
|
|
//
|
|
// @example
|
|
// let sent = false;
|
|
// tryer({
|
|
// until: () => sent,
|
|
// action: done => {
|
|
// smtp.send(email, error => {
|
|
// if (! error) {
|
|
// sent = true;
|
|
// }
|
|
// done();
|
|
// });
|
|
// },
|
|
// pass: next,
|
|
// interval: -1000,
|
|
// limit: -1
|
|
// });
|
|
function tryer (options) {
|
|
options = normaliseOptions(options);
|
|
|
|
iterateWhen();
|
|
|
|
function iterateWhen () {
|
|
if (preRecur()) {
|
|
iterateUntil();
|
|
}
|
|
}
|
|
|
|
function preRecur () {
|
|
return conditionallyRecur('when', iterateWhen);
|
|
}
|
|
|
|
function conditionallyRecur (predicateKey, iterate) {
|
|
if (! options[predicateKey]()) {
|
|
incrementCount(options);
|
|
|
|
if (shouldFail(options)) {
|
|
options.fail();
|
|
} else {
|
|
recur(iterate, postIncrementInterval(options));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function iterateUntil () {
|
|
var result;
|
|
|
|
if (isActionSynchronous(options)) {
|
|
result = options.action();
|
|
|
|
if (result && isFunction(result.then)) {
|
|
return result.then(postRecur, postRecur);
|
|
}
|
|
|
|
return postRecur();
|
|
}
|
|
|
|
options.action(postRecur);
|
|
}
|
|
|
|
function postRecur () {
|
|
if (conditionallyRecur('until', iterateUntil)) {
|
|
options.pass();
|
|
}
|
|
}
|
|
}
|
|
|
|
function normaliseOptions (options) {
|
|
options = options || {};
|
|
return {
|
|
count: 0,
|
|
when: normalisePredicate(options.when),
|
|
until: normalisePredicate(options.until),
|
|
action: normaliseFunction(options.action),
|
|
fail: normaliseFunction(options.fail),
|
|
pass: normaliseFunction(options.pass),
|
|
interval: normaliseNumber(options.interval, -1000),
|
|
limit: normaliseNumber(options.limit, -1)
|
|
};
|
|
}
|
|
|
|
function normalisePredicate (fn) {
|
|
return normalise(fn, isFunction, yes);
|
|
}
|
|
|
|
function isFunction (fn) {
|
|
return typeof fn === 'function';
|
|
}
|
|
|
|
function yes () {
|
|
return true;
|
|
}
|
|
|
|
function normaliseFunction (fn) {
|
|
return normalise(fn, isFunction, nop);
|
|
}
|
|
|
|
function nop () {
|
|
}
|
|
|
|
function normalise (thing, predicate, defaultValue) {
|
|
if (predicate(thing)) {
|
|
return thing;
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
function normaliseNumber (number, defaultNumber) {
|
|
return normalise(number, isNumber, defaultNumber);
|
|
}
|
|
|
|
function isNumber (number) {
|
|
return typeof number === 'number' && number === number;
|
|
}
|
|
|
|
function isActionSynchronous (options) {
|
|
return options.action.length === 0;
|
|
}
|
|
|
|
function incrementCount (options) {
|
|
options.count += 1;
|
|
}
|
|
|
|
function shouldFail (options) {
|
|
return options.limit >= 0 && options.count >= options.limit;
|
|
}
|
|
|
|
function postIncrementInterval (options) {
|
|
var currentInterval = options.interval;
|
|
|
|
if (options.interval < 0) {
|
|
options.interval *= 2;
|
|
}
|
|
|
|
return currentInterval;
|
|
}
|
|
|
|
function recur (fn, interval) {
|
|
setTimeout(fn, Math.abs(interval));
|
|
}
|
|
}(this));
|
|
|