UNPKG

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