batch.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. const {BatchError} = require('../errors/batch');
  2. /**
  3. * @method batch
  4. * @description
  5. * Settles (resolves or rejects) every [mixed value]{@tutorial mixed} in the input array.
  6. *
  7. * The method resolves with an array of results, the same as the standard $[promise.all],
  8. * while providing comprehensive error details in case of a reject, in the form of
  9. * type {@link errors.BatchError BatchError}.
  10. *
  11. * @param {Array} values
  12. * Array of [mixed values]{@tutorial mixed} (it can be empty), to be resolved asynchronously, in no particular order.
  13. *
  14. * Passing in anything other than an array will reject with {@link external:TypeError TypeError} =
  15. * `Method 'batch' requires an array of values.`
  16. *
  17. * @param {Object} [options]
  18. * Optional Parameters.
  19. *
  20. * @param {Function|generator} [options.cb]
  21. * Optional callback (or generator) to receive the result for each settled value.
  22. *
  23. * Callback Parameters:
  24. * - `index` = index of the value in the source array
  25. * - `success` - indicates whether the value was resolved (`true`), or rejected (`false`)
  26. * - `result` = resolved data, if `success`=`true`, or else the rejection reason
  27. * - `delay` = number of milliseconds since the last call (`undefined` when `index=0`)
  28. *
  29. * The function inherits `this` context from the calling method.
  30. *
  31. * It can optionally return a promise to indicate that notifications are handled asynchronously.
  32. * And if the returned promise resolves, it signals a successful handling, while any resolved
  33. * data is ignored.
  34. *
  35. * If the function returns a rejected promise or throws an error, the entire method rejects
  36. * with {@link errors.BatchError BatchError} where the corresponding value in property `data`
  37. * is set to `{success, result, origin}`:
  38. * - `success` = `false`
  39. * - `result` = the rejection reason or the error thrown by the notification callback
  40. * - `origin` = the original data passed into the callback as object `{success, result}`
  41. *
  42. * @returns {external:Promise}
  43. *
  44. * The method resolves with an array of individual resolved results, the same as the standard $[promise.all].
  45. * In addition, the array is extended with a hidden read-only property `duration` - number of milliseconds
  46. * spent resolving all the data.
  47. *
  48. * The method rejects with {@link errors.BatchError BatchError} when any of the following occurs:
  49. * - one or more values rejected or threw an error while being resolved as a [mixed value]{@tutorial mixed}
  50. * - notification callback `cb` returned a rejected promise or threw an error
  51. *
  52. */
  53. function batch(values, options, config) {
  54. const $p = config.promise, utils = config.utils;
  55. if (!Array.isArray(values)) {
  56. return $p.reject(new TypeError('Method \'batch\' requires an array of values.'));
  57. }
  58. if (!values.length) {
  59. const empty = [];
  60. utils.extend(empty, 'duration', 0);
  61. return $p.resolve(empty);
  62. }
  63. options = options || {};
  64. const cb = utils.wrap(options.cb),
  65. self = this, start = Date.now();
  66. return $p((resolve, reject) => {
  67. let cbTime, remaining = values.length;
  68. const errors = [], result = new Array(remaining);
  69. values.forEach((item, i) => {
  70. utils.resolve.call(self, item, null, data => {
  71. result[i] = data;
  72. step(i, true, data);
  73. }, reason => {
  74. result[i] = {success: false, result: reason};
  75. errors.push(i);
  76. step(i, false, reason);
  77. });
  78. });
  79. function step(idx, pass, data) {
  80. if (cb) {
  81. const cbNow = Date.now(),
  82. cbDelay = idx ? (cbNow - cbTime) : undefined;
  83. let cbResult;
  84. cbTime = cbNow;
  85. try {
  86. cbResult = cb.call(self, idx, pass, data, cbDelay);
  87. } catch (e) {
  88. setError(e);
  89. }
  90. if (utils.isPromise(cbResult)) {
  91. cbResult
  92. .then(check)
  93. .catch(error => {
  94. setError(error);
  95. check();
  96. });
  97. } else {
  98. check();
  99. }
  100. } else {
  101. check();
  102. }
  103. function setError(e) {
  104. const r = pass ? {success: false} : result[idx];
  105. if (pass) {
  106. result[idx] = r;
  107. errors.push(idx);
  108. }
  109. r.result = e;
  110. r.origin = {success: pass, result: data};
  111. }
  112. function check() {
  113. if (!--remaining) {
  114. if (errors.length) {
  115. errors.sort();
  116. if (errors.length < result.length) {
  117. for (let i = 0, k = 0; i < result.length; i++) {
  118. if (i === errors[k]) {
  119. k++;
  120. } else {
  121. result[i] = {success: true, result: result[i]};
  122. }
  123. }
  124. }
  125. reject(new BatchError(result, errors, Date.now() - start));
  126. } else {
  127. utils.extend(result, 'duration', Date.now() - start);
  128. resolve(result);
  129. }
  130. }
  131. return null; // this dummy return is just to prevent Bluebird warnings;
  132. }
  133. }
  134. });
  135. }
  136. module.exports = function (config) {
  137. return function (values, options) {
  138. return batch.call(this, values, options, config);
  139. };
  140. };