run-tasks.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. /**
  2. * @module run-tasks-in-parallel
  3. * @author Toru Nagashima
  4. * @copyright 2015 Toru Nagashima. All rights reserved.
  5. * See LICENSE file in root directory for full license.
  6. */
  7. "use strict"
  8. //------------------------------------------------------------------------------
  9. // Requirements
  10. //------------------------------------------------------------------------------
  11. const MemoryStream = require("memorystream")
  12. const NpmRunAllError = require("./npm-run-all-error")
  13. const runTask = require("./run-task")
  14. //------------------------------------------------------------------------------
  15. // Helpers
  16. //------------------------------------------------------------------------------
  17. /**
  18. * Remove the given value from the array.
  19. * @template T
  20. * @param {T[]} array - The array to remove.
  21. * @param {T} x - The item to be removed.
  22. * @returns {void}
  23. */
  24. function remove(array, x) {
  25. const index = array.indexOf(x)
  26. if (index !== -1) {
  27. array.splice(index, 1)
  28. }
  29. }
  30. //------------------------------------------------------------------------------
  31. // Public Interface
  32. //------------------------------------------------------------------------------
  33. /**
  34. * Run npm-scripts of given names in parallel.
  35. *
  36. * If a npm-script exited with a non-zero code, this aborts other all npm-scripts.
  37. *
  38. * @param {string} tasks - A list of npm-script name to run in parallel.
  39. * @param {object} options - An option object.
  40. * @returns {Promise} A promise object which becomes fullfilled when all npm-scripts are completed.
  41. * @private
  42. */
  43. module.exports = function runTasks(tasks, options) {
  44. return new Promise((resolve, reject) => {
  45. if (tasks.length === 0) {
  46. resolve([])
  47. return
  48. }
  49. const results = tasks.map(task => ({ name: task, code: undefined }))
  50. const queue = tasks.map((task, index) => ({ name: task, index }))
  51. const promises = []
  52. let error = null
  53. let aborted = false
  54. /**
  55. * Done.
  56. * @returns {void}
  57. */
  58. function done() {
  59. if (error == null) {
  60. resolve(results)
  61. }
  62. else {
  63. reject(error)
  64. }
  65. }
  66. /**
  67. * Aborts all tasks.
  68. * @returns {void}
  69. */
  70. function abort() {
  71. if (aborted) {
  72. return
  73. }
  74. aborted = true
  75. if (promises.length === 0) {
  76. done()
  77. }
  78. else {
  79. for (const p of promises) {
  80. p.abort()
  81. }
  82. Promise.all(promises).then(done, reject)
  83. }
  84. }
  85. /**
  86. * Runs a next task.
  87. * @returns {void}
  88. */
  89. function next() {
  90. if (aborted) {
  91. return
  92. }
  93. if (queue.length === 0) {
  94. if (promises.length === 0) {
  95. done()
  96. }
  97. return
  98. }
  99. const originalOutputStream = options.stdout
  100. const optionsClone = Object.assign({}, options)
  101. const writer = new MemoryStream(null, {
  102. readable: false,
  103. })
  104. if (options.aggregateOutput) {
  105. optionsClone.stdout = writer
  106. }
  107. const task = queue.shift()
  108. const promise = runTask(task.name, optionsClone)
  109. promises.push(promise)
  110. promise.then(
  111. (result) => {
  112. remove(promises, promise)
  113. if (aborted) {
  114. return
  115. }
  116. if (options.aggregateOutput) {
  117. originalOutputStream.write(writer.toString())
  118. }
  119. // Save the result.
  120. results[task.index].code = result.code
  121. // Aborts all tasks if it's an error.
  122. if (result.code) {
  123. error = new NpmRunAllError(result, results)
  124. if (!options.continueOnError) {
  125. abort()
  126. return
  127. }
  128. }
  129. // Aborts all tasks if options.race is true.
  130. if (options.race && !result.code) {
  131. abort()
  132. return
  133. }
  134. // Call the next task.
  135. next()
  136. },
  137. (thisError) => {
  138. remove(promises, promise)
  139. if (!options.continueOnError || options.race) {
  140. error = thisError
  141. abort()
  142. return
  143. }
  144. next()
  145. }
  146. )
  147. }
  148. const max = options.maxParallel
  149. const end = (typeof max === "number" && max > 0)
  150. ? Math.min(tasks.length, max)
  151. : tasks.length
  152. for (let i = 0; i < end; ++i) {
  153. next()
  154. }
  155. })
  156. }