index.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const EventEmitter = require("eventemitter3");
  4. const p_timeout_1 = require("p-timeout");
  5. const priority_queue_1 = require("./priority-queue");
  6. // eslint-disable-next-line @typescript-eslint/no-empty-function
  7. const empty = () => { };
  8. const timeoutError = new p_timeout_1.TimeoutError();
  9. /**
  10. Promise queue with concurrency control.
  11. */
  12. class PQueue extends EventEmitter {
  13. constructor(options) {
  14. var _a, _b, _c, _d;
  15. super();
  16. this._intervalCount = 0;
  17. this._intervalEnd = 0;
  18. this._pendingCount = 0;
  19. this._resolveEmpty = empty;
  20. this._resolveIdle = empty;
  21. // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  22. options = Object.assign({ carryoverConcurrencyCount: false, intervalCap: Infinity, interval: 0, concurrency: Infinity, autoStart: true, queueClass: priority_queue_1.default }, options);
  23. if (!(typeof options.intervalCap === 'number' && options.intervalCap >= 1)) {
  24. throw new TypeError(`Expected \`intervalCap\` to be a number from 1 and up, got \`${(_b = (_a = options.intervalCap) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : ''}\` (${typeof options.intervalCap})`);
  25. }
  26. if (options.interval === undefined || !(Number.isFinite(options.interval) && options.interval >= 0)) {
  27. throw new TypeError(`Expected \`interval\` to be a finite number >= 0, got \`${(_d = (_c = options.interval) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : ''}\` (${typeof options.interval})`);
  28. }
  29. this._carryoverConcurrencyCount = options.carryoverConcurrencyCount;
  30. this._isIntervalIgnored = options.intervalCap === Infinity || options.interval === 0;
  31. this._intervalCap = options.intervalCap;
  32. this._interval = options.interval;
  33. this._queue = new options.queueClass();
  34. this._queueClass = options.queueClass;
  35. this.concurrency = options.concurrency;
  36. this._timeout = options.timeout;
  37. this._throwOnTimeout = options.throwOnTimeout === true;
  38. this._isPaused = options.autoStart === false;
  39. }
  40. get _doesIntervalAllowAnother() {
  41. return this._isIntervalIgnored || this._intervalCount < this._intervalCap;
  42. }
  43. get _doesConcurrentAllowAnother() {
  44. return this._pendingCount < this._concurrency;
  45. }
  46. _next() {
  47. this._pendingCount--;
  48. this._tryToStartAnother();
  49. this.emit('next');
  50. }
  51. _resolvePromises() {
  52. this._resolveEmpty();
  53. this._resolveEmpty = empty;
  54. if (this._pendingCount === 0) {
  55. this._resolveIdle();
  56. this._resolveIdle = empty;
  57. this.emit('idle');
  58. }
  59. }
  60. _onResumeInterval() {
  61. this._onInterval();
  62. this._initializeIntervalIfNeeded();
  63. this._timeoutId = undefined;
  64. }
  65. _isIntervalPaused() {
  66. const now = Date.now();
  67. if (this._intervalId === undefined) {
  68. const delay = this._intervalEnd - now;
  69. if (delay < 0) {
  70. // Act as the interval was done
  71. // We don't need to resume it here because it will be resumed on line 160
  72. this._intervalCount = (this._carryoverConcurrencyCount) ? this._pendingCount : 0;
  73. }
  74. else {
  75. // Act as the interval is pending
  76. if (this._timeoutId === undefined) {
  77. this._timeoutId = setTimeout(() => {
  78. this._onResumeInterval();
  79. }, delay);
  80. }
  81. return true;
  82. }
  83. }
  84. return false;
  85. }
  86. _tryToStartAnother() {
  87. if (this._queue.size === 0) {
  88. // We can clear the interval ("pause")
  89. // Because we can redo it later ("resume")
  90. if (this._intervalId) {
  91. clearInterval(this._intervalId);
  92. }
  93. this._intervalId = undefined;
  94. this._resolvePromises();
  95. return false;
  96. }
  97. if (!this._isPaused) {
  98. const canInitializeInterval = !this._isIntervalPaused();
  99. if (this._doesIntervalAllowAnother && this._doesConcurrentAllowAnother) {
  100. const job = this._queue.dequeue();
  101. if (!job) {
  102. return false;
  103. }
  104. this.emit('active');
  105. job();
  106. if (canInitializeInterval) {
  107. this._initializeIntervalIfNeeded();
  108. }
  109. return true;
  110. }
  111. }
  112. return false;
  113. }
  114. _initializeIntervalIfNeeded() {
  115. if (this._isIntervalIgnored || this._intervalId !== undefined) {
  116. return;
  117. }
  118. this._intervalId = setInterval(() => {
  119. this._onInterval();
  120. }, this._interval);
  121. this._intervalEnd = Date.now() + this._interval;
  122. }
  123. _onInterval() {
  124. if (this._intervalCount === 0 && this._pendingCount === 0 && this._intervalId) {
  125. clearInterval(this._intervalId);
  126. this._intervalId = undefined;
  127. }
  128. this._intervalCount = this._carryoverConcurrencyCount ? this._pendingCount : 0;
  129. this._processQueue();
  130. }
  131. /**
  132. Executes all queued functions until it reaches the limit.
  133. */
  134. _processQueue() {
  135. // eslint-disable-next-line no-empty
  136. while (this._tryToStartAnother()) { }
  137. }
  138. get concurrency() {
  139. return this._concurrency;
  140. }
  141. set concurrency(newConcurrency) {
  142. if (!(typeof newConcurrency === 'number' && newConcurrency >= 1)) {
  143. throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${newConcurrency}\` (${typeof newConcurrency})`);
  144. }
  145. this._concurrency = newConcurrency;
  146. this._processQueue();
  147. }
  148. /**
  149. Adds a sync or async task to the queue. Always returns a promise.
  150. */
  151. async add(fn, options = {}) {
  152. return new Promise((resolve, reject) => {
  153. const run = async () => {
  154. this._pendingCount++;
  155. this._intervalCount++;
  156. try {
  157. const operation = (this._timeout === undefined && options.timeout === undefined) ? fn() : p_timeout_1.default(Promise.resolve(fn()), (options.timeout === undefined ? this._timeout : options.timeout), () => {
  158. if (options.throwOnTimeout === undefined ? this._throwOnTimeout : options.throwOnTimeout) {
  159. reject(timeoutError);
  160. }
  161. return undefined;
  162. });
  163. resolve(await operation);
  164. }
  165. catch (error) {
  166. reject(error);
  167. }
  168. this._next();
  169. };
  170. this._queue.enqueue(run, options);
  171. this._tryToStartAnother();
  172. this.emit('add');
  173. });
  174. }
  175. /**
  176. Same as `.add()`, but accepts an array of sync or async functions.
  177. @returns A promise that resolves when all functions are resolved.
  178. */
  179. async addAll(functions, options) {
  180. return Promise.all(functions.map(async (function_) => this.add(function_, options)));
  181. }
  182. /**
  183. Start (or resume) executing enqueued tasks within concurrency limit. No need to call this if queue is not paused (via `options.autoStart = false` or by `.pause()` method.)
  184. */
  185. start() {
  186. if (!this._isPaused) {
  187. return this;
  188. }
  189. this._isPaused = false;
  190. this._processQueue();
  191. return this;
  192. }
  193. /**
  194. Put queue execution on hold.
  195. */
  196. pause() {
  197. this._isPaused = true;
  198. }
  199. /**
  200. Clear the queue.
  201. */
  202. clear() {
  203. this._queue = new this._queueClass();
  204. }
  205. /**
  206. Can be called multiple times. Useful if you for example add additional items at a later time.
  207. @returns A promise that settles when the queue becomes empty.
  208. */
  209. async onEmpty() {
  210. // Instantly resolve if the queue is empty
  211. if (this._queue.size === 0) {
  212. return;
  213. }
  214. return new Promise(resolve => {
  215. const existingResolve = this._resolveEmpty;
  216. this._resolveEmpty = () => {
  217. existingResolve();
  218. resolve();
  219. };
  220. });
  221. }
  222. /**
  223. The difference with `.onEmpty` is that `.onIdle` guarantees that all work from the queue has finished. `.onEmpty` merely signals that the queue is empty, but it could mean that some promises haven't completed yet.
  224. @returns A promise that settles when the queue becomes empty, and all promises have completed; `queue.size === 0 && queue.pending === 0`.
  225. */
  226. async onIdle() {
  227. // Instantly resolve if none pending and if nothing else is queued
  228. if (this._pendingCount === 0 && this._queue.size === 0) {
  229. return;
  230. }
  231. return new Promise(resolve => {
  232. const existingResolve = this._resolveIdle;
  233. this._resolveIdle = () => {
  234. existingResolve();
  235. resolve();
  236. };
  237. });
  238. }
  239. /**
  240. Size of the queue.
  241. */
  242. get size() {
  243. return this._queue.size;
  244. }
  245. /**
  246. Size of the queue, filtered by the given options.
  247. For example, this can be used to find the number of items remaining in the queue with a specific priority level.
  248. */
  249. sizeBy(options) {
  250. // eslint-disable-next-line unicorn/no-fn-reference-in-iterator
  251. return this._queue.filter(options).length;
  252. }
  253. /**
  254. Number of pending promises.
  255. */
  256. get pending() {
  257. return this._pendingCount;
  258. }
  259. /**
  260. Whether the queue is currently paused.
  261. */
  262. get isPaused() {
  263. return this._isPaused;
  264. }
  265. get timeout() {
  266. return this._timeout;
  267. }
  268. /**
  269. Set the timeout for future operations.
  270. */
  271. set timeout(milliseconds) {
  272. this._timeout = milliseconds;
  273. }
  274. }
  275. exports.default = PQueue;