page.js 7.6 KB

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