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 |
|
8 |
|
9 | //------------------------------------------------------------------------------
|
10 | // Requirements
|
11 | //------------------------------------------------------------------------------
|
12 |
|
13 | const streams = require("memory-streams")
|
14 | const NpmRunAllError = require("./npm-run-all-error")
|
15 | const 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 | */
|
28 | function 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 | */
|
49 | module.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 | }
|