80deb407502243b75373c7b18623318fc81445309c252722f8b60a39f5b36a25ccfcc7059f6daec61cc73c16124451f0854974786ed146fc65b937cfb8be75 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. // Conditional and repeated task invocation for node and browser.
  2. /*globals setTimeout, define, module */
  3. (function (globals) {
  4. 'use strict';
  5. if (typeof define === 'function' && define.amd) {
  6. define(function () {
  7. return tryer;
  8. });
  9. } else if (typeof module !== 'undefined' && module !== null) {
  10. module.exports = tryer;
  11. } else {
  12. globals.tryer = tryer;
  13. }
  14. // Public function `tryer`.
  15. //
  16. // Performs some action when pre-requisite conditions are met and/or until
  17. // post-requisite conditions are satisfied.
  18. //
  19. // @option action {function} The function that you want to invoke. Defaults to `() => {}`.
  20. // If `action` returns a promise, iterations will not end until
  21. // the promise is resolved or rejected. Alternatively, `action`
  22. // may take a callback argument, `done`, to signal that it is
  23. // asynchronous. In that case, you are responsible for calling
  24. // `done` when the action is finished.
  25. //
  26. // @option when {function} Predicate used to test pre-conditions. Should return `false`
  27. // to postpone `action` or `true` to perform it. Defaults to
  28. // `() => true`.
  29. //
  30. // @option until {function} Predicate used to test post-conditions. Should return `false`
  31. // to retry `action` or `true` to terminate it. Defaults to
  32. // `() => true`.
  33. //
  34. // @option fail {function} Callback to be invoked if `limit` tries are reached. Defaults
  35. // to `() => {}`.
  36. //
  37. // @option pass {function} Callback to be invoked after `until` has returned truthily.
  38. // Defaults to `() => {}`.
  39. //
  40. // @option interval {number} Retry interval in milliseconds. A negative number indicates
  41. // that subsequent retries should wait for double the interval
  42. // from the preceding iteration (exponential backoff). Defaults
  43. // to -1000.
  44. //
  45. // @option limit {number} Maximum retry count, at which point the call fails and retries
  46. // will cease. A negative number indicates that retries should
  47. // continue indefinitely. Defaults to -1.
  48. //
  49. // @example
  50. // tryer({
  51. // when: () => db.isConnected,
  52. // action: () => db.insert(user),
  53. // fail () {
  54. // log.error('No database connection, terminating.');
  55. // process.exit(1);
  56. // },
  57. // interval: 1000,
  58. // limit: 10
  59. // });
  60. //
  61. // @example
  62. // let sent = false;
  63. // tryer({
  64. // until: () => sent,
  65. // action: done => {
  66. // smtp.send(email, error => {
  67. // if (! error) {
  68. // sent = true;
  69. // }
  70. // done();
  71. // });
  72. // },
  73. // pass: next,
  74. // interval: -1000,
  75. // limit: -1
  76. // });
  77. function tryer (options) {
  78. options = normaliseOptions(options);
  79. iterateWhen();
  80. function iterateWhen () {
  81. if (preRecur()) {
  82. iterateUntil();
  83. }
  84. }
  85. function preRecur () {
  86. return conditionallyRecur('when', iterateWhen);
  87. }
  88. function conditionallyRecur (predicateKey, iterate) {
  89. if (! options[predicateKey]()) {
  90. incrementCount(options);
  91. if (shouldFail(options)) {
  92. options.fail();
  93. } else {
  94. recur(iterate, postIncrementInterval(options));
  95. }
  96. return false;
  97. }
  98. return true;
  99. }
  100. function iterateUntil () {
  101. var result;
  102. if (isActionSynchronous(options)) {
  103. result = options.action();
  104. if (result && isFunction(result.then)) {
  105. return result.then(postRecur, postRecur);
  106. }
  107. return postRecur();
  108. }
  109. options.action(postRecur);
  110. }
  111. function postRecur () {
  112. if (conditionallyRecur('until', iterateUntil)) {
  113. options.pass();
  114. }
  115. }
  116. }
  117. function normaliseOptions (options) {
  118. options = options || {};
  119. return {
  120. count: 0,
  121. when: normalisePredicate(options.when),
  122. until: normalisePredicate(options.until),
  123. action: normaliseFunction(options.action),
  124. fail: normaliseFunction(options.fail),
  125. pass: normaliseFunction(options.pass),
  126. interval: normaliseNumber(options.interval, -1000),
  127. limit: normaliseNumber(options.limit, -1)
  128. };
  129. }
  130. function normalisePredicate (fn) {
  131. return normalise(fn, isFunction, yes);
  132. }
  133. function isFunction (fn) {
  134. return typeof fn === 'function';
  135. }
  136. function yes () {
  137. return true;
  138. }
  139. function normaliseFunction (fn) {
  140. return normalise(fn, isFunction, nop);
  141. }
  142. function nop () {
  143. }
  144. function normalise (thing, predicate, defaultValue) {
  145. if (predicate(thing)) {
  146. return thing;
  147. }
  148. return defaultValue;
  149. }
  150. function normaliseNumber (number, defaultNumber) {
  151. return normalise(number, isNumber, defaultNumber);
  152. }
  153. function isNumber (number) {
  154. return typeof number === 'number' && number === number;
  155. }
  156. function isActionSynchronous (options) {
  157. return options.action.length === 0;
  158. }
  159. function incrementCount (options) {
  160. options.count += 1;
  161. }
  162. function shouldFail (options) {
  163. return options.limit >= 0 && options.count >= options.limit;
  164. }
  165. function postIncrementInterval (options) {
  166. var currentInterval = options.interval;
  167. if (options.interval < 0) {
  168. options.interval *= 2;
  169. }
  170. return currentInterval;
  171. }
  172. function recur (fn, interval) {
  173. setTimeout(fn, Math.abs(interval));
  174. }
  175. }(this));