AsyncQueue.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { AsyncSeriesHook, SyncHook } = require("tapable");
  7. const { makeWebpackError } = require("../errors/HookWebpackError");
  8. const WebpackError = require("../errors/WebpackError");
  9. const ArrayQueue = require("./ArrayQueue");
  10. const QUEUED_STATE = 0;
  11. const PROCESSING_STATE = 1;
  12. const DONE_STATE = 2;
  13. let inHandleResult = 0;
  14. /**
  15. * Defines the callback callback.
  16. * @template T
  17. * @callback Callback
  18. * @param {(WebpackError | null)=} err
  19. * @param {(T | null)=} result
  20. * @returns {void}
  21. */
  22. /**
  23. * Represents AsyncQueueEntry.
  24. * @template T
  25. * @template K
  26. * @template R
  27. */
  28. class AsyncQueueEntry {
  29. /**
  30. * Creates an instance of AsyncQueueEntry.
  31. * @param {T} item the item
  32. * @param {Callback<R>} callback the callback
  33. */
  34. constructor(item, callback) {
  35. /** @type {T} */
  36. this.item = item;
  37. /** @type {typeof QUEUED_STATE | typeof PROCESSING_STATE | typeof DONE_STATE} */
  38. this.state = QUEUED_STATE;
  39. /** @type {Callback<R> | undefined} */
  40. this.callback = callback;
  41. /** @type {Callback<R>[] | undefined} */
  42. this.callbacks = undefined;
  43. /** @type {R | null | undefined} */
  44. this.result = undefined;
  45. /** @type {WebpackError | null | undefined} */
  46. this.error = undefined;
  47. }
  48. }
  49. /**
  50. * Defines the get key type used by this module.
  51. * @template T, K
  52. * @typedef {(item: T) => K} getKey
  53. */
  54. /**
  55. * Defines the processor type used by this module.
  56. * @template T, R
  57. * @typedef {(item: T, callback: Callback<R>) => void} Processor
  58. */
  59. /**
  60. * Represents AsyncQueue.
  61. * @template T
  62. * @template K
  63. * @template R
  64. */
  65. class AsyncQueue {
  66. /**
  67. * Creates an instance of AsyncQueue.
  68. * @param {object} options options object
  69. * @param {string=} options.name name of the queue
  70. * @param {number=} options.parallelism how many items should be processed at once
  71. * @param {string=} options.context context of execution
  72. * @param {AsyncQueue<EXPECTED_ANY, EXPECTED_ANY, EXPECTED_ANY>=} options.parent parent queue, which will have priority over this queue and with shared parallelism
  73. * @param {getKey<T, K>=} options.getKey extract key from item
  74. * @param {Processor<T, R>} options.processor async function to process items
  75. */
  76. constructor({ name, context, parallelism, parent, processor, getKey }) {
  77. /** @type {string | undefined} */
  78. this._name = name;
  79. /** @type {string} */
  80. this._context = context || "normal";
  81. /** @type {number} */
  82. this._parallelism = parallelism || 1;
  83. /** @type {Processor<T, R>} */
  84. this._processor = processor;
  85. /** @type {getKey<T, K>} */
  86. this._getKey =
  87. getKey ||
  88. /** @type {getKey<T, K>} */ ((item) => /** @type {T & K} */ (item));
  89. /** @type {Map<K, AsyncQueueEntry<T, K, R>>} */
  90. this._entries = new Map();
  91. /** @type {ArrayQueue<AsyncQueueEntry<T, K, R>>} */
  92. this._queued = new ArrayQueue();
  93. /** @type {AsyncQueue<T, K, R>[] | undefined} */
  94. this._children = undefined;
  95. /** @type {number} */
  96. this._activeTasks = 0;
  97. /** @type {boolean} */
  98. this._willEnsureProcessing = false;
  99. /** @type {boolean} */
  100. this._needProcessing = false;
  101. /** @type {boolean} */
  102. this._stopped = false;
  103. /** @type {AsyncQueue<T, K, R>} */
  104. this._root = parent ? parent._root : this;
  105. if (parent) {
  106. if (this._root._children === undefined) {
  107. this._root._children = [this];
  108. } else {
  109. this._root._children.push(this);
  110. }
  111. }
  112. this.hooks = {
  113. /** @type {AsyncSeriesHook<[T]>} */
  114. beforeAdd: new AsyncSeriesHook(["item"]),
  115. /** @type {SyncHook<[T]>} */
  116. added: new SyncHook(["item"]),
  117. /** @type {AsyncSeriesHook<[T]>} */
  118. beforeStart: new AsyncSeriesHook(["item"]),
  119. /** @type {SyncHook<[T]>} */
  120. started: new SyncHook(["item"]),
  121. /** @type {SyncHook<[T, WebpackError | null | undefined, R | null | undefined]>} */
  122. result: new SyncHook(["item", "error", "result"])
  123. };
  124. this._ensureProcessing = this._ensureProcessing.bind(this);
  125. }
  126. /**
  127. * Returns context of execution.
  128. * @returns {string} context of execution
  129. */
  130. getContext() {
  131. return this._context;
  132. }
  133. /**
  134. * Updates context using the provided value.
  135. * @param {string} value context of execution
  136. */
  137. setContext(value) {
  138. this._context = value;
  139. }
  140. /**
  141. * Processes the provided item.
  142. * @param {T} item an item
  143. * @param {Callback<R>} callback callback function
  144. * @returns {void}
  145. */
  146. add(item, callback) {
  147. if (this._stopped) return callback(new WebpackError("Queue was stopped"));
  148. // skip async hook dispatch when unused — this runs per module × queue
  149. if (!this.hooks.beforeAdd.isUsed()) {
  150. this._add(item, callback);
  151. return;
  152. }
  153. this.hooks.beforeAdd.callAsync(item, (err) => {
  154. if (err) {
  155. callback(
  156. makeWebpackError(err, `AsyncQueue(${this._name}).hooks.beforeAdd`)
  157. );
  158. return;
  159. }
  160. this._add(item, callback);
  161. });
  162. }
  163. /**
  164. * Processes the provided item, after the beforeAdd hook.
  165. * @param {T} item an item
  166. * @param {Callback<R>} callback callback function
  167. * @returns {void}
  168. */
  169. _add(item, callback) {
  170. const key = this._getKey(item);
  171. const entry = this._entries.get(key);
  172. if (entry !== undefined) {
  173. if (entry.state === DONE_STATE) {
  174. if (inHandleResult++ > 3) {
  175. process.nextTick(() => callback(entry.error, entry.result));
  176. } else {
  177. callback(entry.error, entry.result);
  178. }
  179. inHandleResult--;
  180. } else if (entry.callbacks === undefined) {
  181. entry.callbacks = [callback];
  182. } else {
  183. entry.callbacks.push(callback);
  184. }
  185. return;
  186. }
  187. const newEntry = new AsyncQueueEntry(item, callback);
  188. if (this._stopped) {
  189. this.hooks.added.call(item);
  190. this._root._activeTasks++;
  191. process.nextTick(() =>
  192. this._handleResult(newEntry, new WebpackError("Queue was stopped"))
  193. );
  194. } else {
  195. this._entries.set(key, newEntry);
  196. this._queued.enqueue(newEntry);
  197. const root = this._root;
  198. root._needProcessing = true;
  199. if (root._willEnsureProcessing === false) {
  200. root._willEnsureProcessing = true;
  201. setImmediate(root._ensureProcessing);
  202. }
  203. this.hooks.added.call(item);
  204. }
  205. }
  206. /**
  207. * Processes the provided item.
  208. * @param {T} item an item
  209. * @returns {void}
  210. */
  211. invalidate(item) {
  212. const key = this._getKey(item);
  213. const entry = this._entries.get(key);
  214. // nothing queued for this item in the current run (e.g. incremental rebuild
  215. // against a persisted graph) — nothing to invalidate
  216. if (entry === undefined) return;
  217. this._entries.delete(key);
  218. if (entry.state === QUEUED_STATE) {
  219. this._queued.delete(entry);
  220. }
  221. }
  222. /**
  223. * Waits for an already started item
  224. * @param {T} item an item
  225. * @param {Callback<R>} callback callback function
  226. * @returns {void}
  227. */
  228. waitFor(item, callback) {
  229. const key = this._getKey(item);
  230. const entry = this._entries.get(key);
  231. if (entry === undefined) {
  232. return callback(
  233. new WebpackError(
  234. "waitFor can only be called for an already started item"
  235. )
  236. );
  237. }
  238. if (entry.state === DONE_STATE) {
  239. process.nextTick(() => callback(entry.error, entry.result));
  240. } else if (entry.callbacks === undefined) {
  241. entry.callbacks = [callback];
  242. } else {
  243. entry.callbacks.push(callback);
  244. }
  245. }
  246. /**
  247. * Describes how this stop operation behaves.
  248. * @returns {void}
  249. */
  250. stop() {
  251. this._stopped = true;
  252. const queue = this._queued;
  253. this._queued = new ArrayQueue();
  254. const root = this._root;
  255. for (const entry of queue) {
  256. this._entries.delete(
  257. this._getKey(/** @type {AsyncQueueEntry<T, K, R>} */ (entry).item)
  258. );
  259. root._activeTasks++;
  260. this._handleResult(
  261. /** @type {AsyncQueueEntry<T, K, R>} */ (entry),
  262. new WebpackError("Queue was stopped")
  263. );
  264. }
  265. }
  266. /**
  267. * Increase parallelism.
  268. * @returns {void}
  269. */
  270. increaseParallelism() {
  271. const root = this._root;
  272. root._parallelism++;
  273. /* istanbul ignore next */
  274. if (root._willEnsureProcessing === false && root._needProcessing) {
  275. root._willEnsureProcessing = true;
  276. setImmediate(root._ensureProcessing);
  277. }
  278. }
  279. /**
  280. * Decrease parallelism.
  281. * @returns {void}
  282. */
  283. decreaseParallelism() {
  284. const root = this._root;
  285. root._parallelism--;
  286. }
  287. /**
  288. * Checks whether this async queue is processing.
  289. * @param {T} item an item
  290. * @returns {boolean} true, if the item is currently being processed
  291. */
  292. isProcessing(item) {
  293. const key = this._getKey(item);
  294. const entry = this._entries.get(key);
  295. return entry !== undefined && entry.state === PROCESSING_STATE;
  296. }
  297. /**
  298. * Checks whether this async queue is queued.
  299. * @param {T} item an item
  300. * @returns {boolean} true, if the item is currently queued
  301. */
  302. isQueued(item) {
  303. const key = this._getKey(item);
  304. const entry = this._entries.get(key);
  305. return entry !== undefined && entry.state === QUEUED_STATE;
  306. }
  307. /**
  308. * Checks whether this async queue is done.
  309. * @param {T} item an item
  310. * @returns {boolean} true, if the item is currently queued
  311. */
  312. isDone(item) {
  313. const key = this._getKey(item);
  314. const entry = this._entries.get(key);
  315. return entry !== undefined && entry.state === DONE_STATE;
  316. }
  317. /**
  318. * Describes how this ensure processing operation behaves.
  319. * @returns {void}
  320. */
  321. _ensureProcessing() {
  322. while (this._activeTasks < this._parallelism) {
  323. const entry = this._queued.dequeue();
  324. if (entry === undefined) break;
  325. this._activeTasks++;
  326. entry.state = PROCESSING_STATE;
  327. this._startProcessing(entry);
  328. }
  329. this._willEnsureProcessing = false;
  330. if (this._queued.length > 0) return;
  331. if (this._children !== undefined) {
  332. for (const child of this._children) {
  333. while (this._activeTasks < this._parallelism) {
  334. const entry = child._queued.dequeue();
  335. if (entry === undefined) break;
  336. this._activeTasks++;
  337. entry.state = PROCESSING_STATE;
  338. child._startProcessing(entry);
  339. }
  340. if (child._queued.length > 0) return;
  341. }
  342. }
  343. if (!this._willEnsureProcessing) this._needProcessing = false;
  344. }
  345. /**
  346. * Processes the provided entry.
  347. * @param {AsyncQueueEntry<T, K, R>} entry the entry
  348. * @returns {void}
  349. */
  350. _startProcessing(entry) {
  351. // skip async hook dispatch when unused — this runs per module × queue
  352. if (!this.hooks.beforeStart.isUsed()) {
  353. this._startProcessing2(entry);
  354. return;
  355. }
  356. this.hooks.beforeStart.callAsync(entry.item, (err) => {
  357. if (err) {
  358. this._handleResult(
  359. entry,
  360. makeWebpackError(err, `AsyncQueue(${this._name}).hooks.beforeStart`)
  361. );
  362. return;
  363. }
  364. this._startProcessing2(entry);
  365. });
  366. }
  367. /**
  368. * Processes the provided entry, after the beforeStart hook.
  369. * @param {AsyncQueueEntry<T, K, R>} entry the entry
  370. * @returns {void}
  371. */
  372. _startProcessing2(entry) {
  373. let inCallback = false;
  374. try {
  375. this._processor(entry.item, (e, r) => {
  376. inCallback = true;
  377. this._handleResult(entry, e, r);
  378. });
  379. } catch (err) {
  380. if (inCallback) throw err;
  381. this._handleResult(entry, /** @type {WebpackError} */ (err), null);
  382. }
  383. this.hooks.started.call(entry.item);
  384. }
  385. /**
  386. * Processes the provided entry.
  387. * @param {AsyncQueueEntry<T, K, R>} entry the entry
  388. * @param {(WebpackError | null)=} err error, if any
  389. * @param {(R | null)=} result result, if any
  390. * @returns {void}
  391. */
  392. _handleResult(entry, err, result) {
  393. // skip async hook dispatch when unused — this runs per module × queue
  394. if (!this.hooks.result.isUsed()) {
  395. this._handleResult2(entry, err, result);
  396. return;
  397. }
  398. this.hooks.result.callAsync(entry.item, err, result, (hookError) => {
  399. this._handleResult2(
  400. entry,
  401. hookError
  402. ? makeWebpackError(
  403. hookError,
  404. `AsyncQueue(${this._name}).hooks.result`
  405. )
  406. : err,
  407. result
  408. );
  409. });
  410. }
  411. /**
  412. * Finalizes the provided entry, after the result hook.
  413. * @param {AsyncQueueEntry<T, K, R>} entry the entry
  414. * @param {(WebpackError | null)=} error error, if any
  415. * @param {(R | null)=} result result, if any
  416. * @returns {void}
  417. */
  418. _handleResult2(entry, error, result) {
  419. const callback = /** @type {Callback<R>} */ (entry.callback);
  420. const callbacks = entry.callbacks;
  421. entry.state = DONE_STATE;
  422. entry.callback = undefined;
  423. entry.callbacks = undefined;
  424. entry.result = result;
  425. entry.error = error;
  426. const root = this._root;
  427. root._activeTasks--;
  428. if (root._willEnsureProcessing === false && root._needProcessing) {
  429. root._willEnsureProcessing = true;
  430. setImmediate(root._ensureProcessing);
  431. }
  432. if (inHandleResult++ > 3) {
  433. process.nextTick(() => {
  434. callback(error, result);
  435. if (callbacks !== undefined) {
  436. for (const callback of callbacks) {
  437. callback(error, result);
  438. }
  439. }
  440. });
  441. } else {
  442. callback(error, result);
  443. if (callbacks !== undefined) {
  444. for (const callback of callbacks) {
  445. callback(error, result);
  446. }
  447. }
  448. }
  449. inHandleResult--;
  450. }
  451. clear() {
  452. this._entries.clear();
  453. this._queued.clear();
  454. this._activeTasks = 0;
  455. this._willEnsureProcessing = false;
  456. this._needProcessing = false;
  457. this._stopped = false;
  458. }
  459. }
  460. module.exports = AsyncQueue;