sequence.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. const {SequenceError} = require('../errors/sequence');
  2. /**
  3. * @method sequence
  4. * @description
  5. * Resolves a dynamic sequence of [mixed values]{@tutorial mixed}.
  6. *
  7. * The method acquires [mixed values]{@tutorial mixed} from the `source` function, one at a time, and resolves them,
  8. * till either no more values left in the sequence or an error/reject occurs.
  9. *
  10. * It supports both [linked and detached sequencing]{@tutorial sequencing}.
  11. *
  12. * @param {Function|generator} source
  13. * Expected to return the next [mixed value]{@tutorial mixed} to be resolved. Returning or resolving
  14. * with `undefined` ends the sequence, and the method resolves.
  15. *
  16. * Parameters:
  17. * - `index` = current request index in the sequence
  18. * - `data` = resolved data from the previous call (`undefined` when `index=0`)
  19. * - `delay` = number of milliseconds since the last call (`undefined` when `index=0`)
  20. *
  21. * The function inherits `this` context from the calling method.
  22. *
  23. * If the function throws an error or returns a rejected promise, the sequence terminates,
  24. * and the method rejects with {@link errors.SequenceError SequenceError}, which will have property `source` set.
  25. *
  26. * Passing in anything other than a function will reject with {@link external:TypeError TypeError} = `Parameter 'source' must be a function.`
  27. *
  28. * @param {Object} [options]
  29. * Optional Parameters.
  30. *
  31. * @param {Function|generator} [options.dest=null]
  32. * Optional destination function (or generator), to receive resolved data for each index,
  33. * process it and respond as required.
  34. *
  35. * Parameters:
  36. * - `index` = index of the resolved data in the sequence
  37. * - `data` = the data resolved
  38. * - `delay` = number of milliseconds since the last call (`undefined` when `index=0`)
  39. *
  40. * The function inherits `this` context from the calling method.
  41. *
  42. * It can optionally return a promise object, if data processing is done asynchronously.
  43. * If a promise is returned, the method will not request another value from the `source` function,
  44. * until the promise has been resolved (the resolved value is ignored).
  45. *
  46. * If the function throws an error or returns a rejected promise, the sequence terminates,
  47. * and the method rejects with {@link errors.SequenceError SequenceError}, which will have property `dest` set.
  48. *
  49. * @param {Number} [options.limit=0]
  50. * Limits the maximum size of the sequence. If the value is greater than 0, the method will
  51. * successfully resolve once the specified limit has been reached.
  52. *
  53. * When `limit` isn't specified (default), the sequence is unlimited, and it will continue
  54. * till one of the following occurs:
  55. * - `source` either returns or resolves with `undefined`
  56. * - either `source` or `dest` functions throw an error or return a rejected promise
  57. *
  58. * @param {Boolean} [options.track=false]
  59. * Changes the type of data to be resolved by this method. By default, it is `false`
  60. * (see the return result). When set to be `true`, the method tracks/collects all resolved data
  61. * into an array internally, and resolves with that array once the method has finished successfully.
  62. *
  63. * It must be used with caution, as to the size of the sequence, because accumulating data for
  64. * a very large sequence can result in consuming too much memory.
  65. *
  66. * @returns {external:Promise}
  67. *
  68. * When successful, the resolved data depends on parameter `track`. When `track` is `false`
  69. * (default), the method resolves with object `{total, duration}`:
  70. * - `total` = number of values resolved by the sequence
  71. * - `duration` = number of milliseconds consumed by the method
  72. *
  73. * When `track` is `true`, the method resolves with an array of all the data that has been resolved,
  74. * the same way that the standard $[promise.all] resolves. In addition, the array comes extended with
  75. * a hidden read-only property `duration` - number of milliseconds consumed by the method.
  76. *
  77. * When the method fails, it rejects with {@link errors.SequenceError SequenceError}.
  78. */
  79. function sequence(source, options, config) {
  80. const $p = config.promise, utils = config.utils;
  81. if (typeof source !== 'function') {
  82. return $p.reject(new TypeError('Parameter \'source\' must be a function.'));
  83. }
  84. source = utils.wrap(source);
  85. options = options || {};
  86. const limit = (options.limit > 0) ? parseInt(options.limit) : 0,
  87. dest = utils.wrap(options.dest),
  88. self = this, start = Date.now();
  89. let data, srcTime, destTime, result = [];
  90. return $p((resolve, reject) => {
  91. function loop(idx) {
  92. const srcNow = Date.now(),
  93. srcDelay = idx ? (srcNow - srcTime) : undefined;
  94. srcTime = srcNow;
  95. utils.resolve.call(self, source, [idx, data, srcDelay], (value, delayed) => {
  96. data = value;
  97. if (data === undefined) {
  98. success();
  99. } else {
  100. if (options.track) {
  101. result.push(data);
  102. }
  103. if (dest) {
  104. const destNow = Date.now(),
  105. destDelay = idx ? (destNow - destTime) : undefined;
  106. let destResult;
  107. destTime = destNow;
  108. try {
  109. destResult = dest.call(self, idx, data, destDelay);
  110. } catch (e) {
  111. fail({
  112. error: e,
  113. dest: data
  114. }, 3, dest.name);
  115. return;
  116. }
  117. if (utils.isPromise(destResult)) {
  118. destResult
  119. .then(() => {
  120. next(true);
  121. return null; // this dummy return is just to prevent Bluebird warnings;
  122. })
  123. .catch(error => {
  124. fail({
  125. error: error,
  126. dest: data
  127. }, 2, dest.name);
  128. });
  129. } else {
  130. next(delayed);
  131. }
  132. } else {
  133. next(delayed);
  134. }
  135. }
  136. }, (reason, isRej) => {
  137. fail({
  138. error: reason,
  139. source: data
  140. }, isRej ? 0 : 1, source.name);
  141. });
  142. function next(delayed) {
  143. if (limit === ++idx) {
  144. success();
  145. } else {
  146. if (delayed) {
  147. loop(idx);
  148. } else {
  149. $p.resolve()
  150. .then(() => {
  151. loop(idx);
  152. return null; // this dummy return is just to prevent Bluebird warnings;
  153. });
  154. }
  155. }
  156. }
  157. function success() {
  158. const length = Date.now() - start;
  159. if (options.track) {
  160. utils.extend(result, 'duration', length);
  161. } else {
  162. result = {
  163. total: idx,
  164. duration: length
  165. };
  166. }
  167. resolve(result);
  168. }
  169. function fail(reason, code, cbName) {
  170. reason.index = idx;
  171. reject(new SequenceError(reason, code, cbName, Date.now() - start));
  172. }
  173. }
  174. loop(0);
  175. });
  176. }
  177. module.exports = function (config) {
  178. return function (source, options) {
  179. return sequence.call(this, source, options, config);
  180. };
  181. };