// 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));