parallel-park 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.node-version +1 -1
- package/.npm-version +1 -1
- package/LICENSE +2 -2
- package/dist/child-process-worker.js +42 -20
- package/dist/in-child-process.js +39 -14
- package/dist/read-until-end.js +26 -0
- package/dist/run-jobs.js +16 -0
- package/package.json +14 -12
- package/src/child-process-worker.ts +51 -23
- package/src/in-child-process.ts +55 -23
- package/src/read-until-end.ts +25 -0
- package/src/run-jobs.ts +22 -0
package/.node-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
v20.11.1
|
package/.npm-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
10.2.4
|
package/LICENSE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
MIT License Copyright (c) 2022 Lily
|
|
1
|
+
MIT License Copyright (c) 2022-2024 Lily Skye
|
|
2
2
|
|
|
3
3
|
Permission is hereby granted, free of
|
|
4
4
|
charge, to any person obtaining a copy of this software and associated
|
|
@@ -18,4 +18,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
|
18
18
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
19
19
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
20
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -6,27 +6,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const fs_1 = __importDefault(require("fs"));
|
|
7
7
|
const vm_1 = __importDefault(require("vm"));
|
|
8
8
|
const make_module_env_1 = __importDefault(require("make-module-env"));
|
|
9
|
-
const
|
|
9
|
+
const debug_1 = __importDefault(require("debug"));
|
|
10
|
+
const read_until_end_1 = require("./read-until-end");
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const debug = (0, debug_1.default)("parallel-park:child-process-worker");
|
|
13
|
+
const commsIn = fs_1.default.createReadStream(
|
|
10
14
|
// @ts-ignore
|
|
11
15
|
null, { fd: 3 });
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
16
|
+
const commsOut = fs_1.default.createWriteStream(
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
null, { fd: 4 });
|
|
19
|
+
debug("reading input data...");
|
|
20
|
+
(0, read_until_end_1.readUntilEnd)(commsIn)
|
|
21
|
+
.then((data) => {
|
|
22
|
+
debug("parsing input data...");
|
|
23
|
+
try {
|
|
24
|
+
const [inputs, fnString, callingFile] = JSON.parse(data);
|
|
25
|
+
onReady(inputs, fnString, callingFile);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
onError(err);
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
.catch(onError);
|
|
32
|
+
function onReady(inputs, fnString, callingFile) {
|
|
33
|
+
debug("in onReady %o", { inputs, fnString, callingFile });
|
|
34
|
+
// Relevant when callingFile is eg. "REPL2" (from Node.js repl)
|
|
35
|
+
if (!path_1.default.isAbsolute(callingFile)) {
|
|
36
|
+
callingFile = path_1.default.join(process.cwd(), "fake-path.js");
|
|
37
|
+
}
|
|
27
38
|
const wrapperFn = vm_1.default.runInThisContext(`(function moduleWrapper(exports, require, module, __filename, __dirname) {
|
|
28
|
-
return ${fnString};})`);
|
|
29
|
-
const inputs = JSON.parse(inputsString);
|
|
39
|
+
return ${fnString};})`);
|
|
30
40
|
const env = (0, make_module_env_1.default)(callingFile);
|
|
31
41
|
const fn = wrapperFn(env.exports, env.require, env.module, env.__filename, env.__dirname);
|
|
32
42
|
const result = fn(inputs);
|
|
@@ -39,6 +49,18 @@ return ${fnString};})`);
|
|
|
39
49
|
onSuccess(result);
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
function onSuccess(data) {
|
|
53
|
+
debug("in onSuccess %o", { data });
|
|
54
|
+
commsOut.end(JSON.stringify({ type: "success", data }));
|
|
55
|
+
}
|
|
56
|
+
function onError(error) {
|
|
57
|
+
debug("in onError %o", { error });
|
|
58
|
+
commsOut.end(JSON.stringify({
|
|
59
|
+
type: "error",
|
|
60
|
+
error: {
|
|
61
|
+
name: error.name,
|
|
62
|
+
message: error.message,
|
|
63
|
+
stack: error.stack,
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
44
66
|
}
|
package/dist/in-child-process.js
CHANGED
|
@@ -5,8 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.inChildProcess = void 0;
|
|
7
7
|
const child_process_1 = __importDefault(require("child_process"));
|
|
8
|
+
const error_utils_1 = require("@suchipi/error-utils");
|
|
9
|
+
const debug_1 = __importDefault(require("debug"));
|
|
10
|
+
const util_1 = __importDefault(require("util"));
|
|
11
|
+
const read_until_end_1 = require("./read-until-end");
|
|
12
|
+
const debug = (0, debug_1.default)("parallel-park:in-child-process");
|
|
8
13
|
const runnerPath = require.resolve("../dist/child-process-worker");
|
|
9
14
|
const inChildProcess = (...args) => {
|
|
15
|
+
var _a;
|
|
10
16
|
const inputs = typeof args[0] === "function" ? {} : args[0];
|
|
11
17
|
const functionToRun = typeof args[0] === "function" ? args[0] : args[1];
|
|
12
18
|
if (typeof inputs !== "object") {
|
|
@@ -15,24 +21,33 @@ const inChildProcess = (...args) => {
|
|
|
15
21
|
if (typeof functionToRun !== "function") {
|
|
16
22
|
throw new Error("The second argument to inChildProcess should be a function to run in the child process.");
|
|
17
23
|
}
|
|
18
|
-
const here = new Error("
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const child = child_process_1.default.spawn(process.argv[0], [runnerPath
|
|
23
|
-
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
24
|
+
const here = new error_utils_1.ParsedError(new Error("here"));
|
|
25
|
+
const callingFrame = here.stackFrames[1];
|
|
26
|
+
const callingFile = (_a = callingFrame === null || callingFrame === void 0 ? void 0 : callingFrame.fileName) !== null && _a !== void 0 ? _a : "unknown file";
|
|
27
|
+
debug("spawning child process: %o", [process.argv[0], runnerPath]);
|
|
28
|
+
const child = child_process_1.default.spawn(process.argv[0], [runnerPath], {
|
|
29
|
+
stdio: ["inherit", "inherit", "inherit", "pipe", "pipe"],
|
|
24
30
|
});
|
|
25
31
|
return new Promise((resolve, reject) => {
|
|
26
32
|
child.on("error", reject);
|
|
33
|
+
const commsOut = child.stdio[3];
|
|
34
|
+
const commsIn = child.stdio[4];
|
|
35
|
+
child.on("spawn", () => {
|
|
36
|
+
const dataToSend = JSON.stringify([
|
|
37
|
+
inputs,
|
|
38
|
+
functionToRun.toString(),
|
|
39
|
+
callingFile,
|
|
40
|
+
]);
|
|
41
|
+
debug("sending inputs to child process: %o", dataToSend);
|
|
42
|
+
commsOut.end(dataToSend, "utf-8");
|
|
43
|
+
});
|
|
27
44
|
let receivedData = "";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
comms.on("data", (chunk) => {
|
|
33
|
-
receivedData += chunk.toString();
|
|
45
|
+
(0, read_until_end_1.readUntilEnd)(commsIn).then((data) => {
|
|
46
|
+
debug("received data from child process: %o", data);
|
|
47
|
+
receivedData = data;
|
|
34
48
|
});
|
|
35
49
|
child.on("close", (code, signal) => {
|
|
50
|
+
debug("child process closed: %o", { code, signal });
|
|
36
51
|
if (code !== 0) {
|
|
37
52
|
reject(new Error(`Child process exited with nonzero status code: ${JSON.stringify({
|
|
38
53
|
code,
|
|
@@ -40,13 +55,23 @@ const inChildProcess = (...args) => {
|
|
|
40
55
|
})}`));
|
|
41
56
|
}
|
|
42
57
|
else {
|
|
43
|
-
|
|
58
|
+
debug("parsing received data from child process...");
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = JSON.parse(receivedData);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
reject(new Error(`parallel-park error: failed to parse received data as JSON. data was: ${util_1.default.inspect(receivedData, { colors: true, depth: Infinity })}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
44
67
|
switch (result.type) {
|
|
45
68
|
case "success": {
|
|
69
|
+
debug("child process finished successfully with result: %o", result.data);
|
|
46
70
|
resolve(result.data);
|
|
47
71
|
break;
|
|
48
72
|
}
|
|
49
73
|
case "error": {
|
|
74
|
+
debug("child process errored: %o", result.error);
|
|
50
75
|
const error = new Error(result.error.message);
|
|
51
76
|
Object.defineProperty(error, "name", { value: result.error.name });
|
|
52
77
|
Object.defineProperty(error, "stack", {
|
|
@@ -60,7 +85,7 @@ const inChildProcess = (...args) => {
|
|
|
60
85
|
.filter((line) => !/node:internal|node:events/.test(line))
|
|
61
86
|
.map((line) => {
|
|
62
87
|
if (/evalmachine/.test(line)) {
|
|
63
|
-
const lineWithoutEvalMachine = line.replace(/evalmachine(
|
|
88
|
+
const lineWithoutEvalMachine = line.replace(/evalmachine(?:\.<anonymous>)?/, "<function passed into inChildProcess>");
|
|
64
89
|
const matches = line.match(/:(\d+):(\d+)\)?$/);
|
|
65
90
|
if (!matches) {
|
|
66
91
|
return lineWithoutEvalMachine;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readUntilEnd = void 0;
|
|
7
|
+
const debug_1 = __importDefault(require("debug"));
|
|
8
|
+
const debug = (0, debug_1.default)("parallel-park:read-until-end");
|
|
9
|
+
let streamId = 0;
|
|
10
|
+
function readUntilEnd(stream) {
|
|
11
|
+
const id = `${process.pid}-${streamId}`;
|
|
12
|
+
streamId++;
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
let data = "";
|
|
15
|
+
stream.on("data", (chunk) => {
|
|
16
|
+
const chunkStr = chunk.toString("utf-8");
|
|
17
|
+
debug("received data chunk from stream %s: %o", id, chunkStr);
|
|
18
|
+
data += chunkStr;
|
|
19
|
+
});
|
|
20
|
+
stream.on("close", () => {
|
|
21
|
+
debug("stream %s closed; resolving with: %o", id, data);
|
|
22
|
+
resolve(data);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
exports.readUntilEnd = readUntilEnd;
|
package/dist/run-jobs.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.runJobs = void 0;
|
|
7
|
+
const debug_1 = __importDefault(require("debug"));
|
|
8
|
+
const debug = (0, debug_1.default)("parallel-park:run-jobs");
|
|
4
9
|
function isThenable(value) {
|
|
5
10
|
return (typeof value === "object" &&
|
|
6
11
|
value != null &&
|
|
@@ -8,11 +13,15 @@ function isThenable(value) {
|
|
|
8
13
|
typeof value.then === "function");
|
|
9
14
|
}
|
|
10
15
|
const NOTHING = Symbol("NOTHING");
|
|
16
|
+
let runJobsCallId = 0;
|
|
11
17
|
async function runJobs(inputs, mapper, {
|
|
12
18
|
/**
|
|
13
19
|
* How many jobs are allowed to run at once.
|
|
14
20
|
*/
|
|
15
21
|
concurrency = 8, } = {}) {
|
|
22
|
+
const callId = runJobsCallId;
|
|
23
|
+
runJobsCallId++;
|
|
24
|
+
debug(`runJobs called (callId: ${callId})`, { inputs, mapper, concurrency });
|
|
16
25
|
if (concurrency < 1) {
|
|
17
26
|
throw new Error("Concurrency can't be less than one; that doesn't make any sense.");
|
|
18
27
|
}
|
|
@@ -22,6 +31,7 @@ concurrency = 8, } = {}) {
|
|
|
22
31
|
const maybeLength = Array.isArray(inputs) ? inputs.length : null;
|
|
23
32
|
let iteratorDone = false;
|
|
24
33
|
async function readInput() {
|
|
34
|
+
debug(`reading next input (callId: ${callId})`);
|
|
25
35
|
let nextResult = inputIterator.next();
|
|
26
36
|
if (isThenable(nextResult)) {
|
|
27
37
|
nextResult = await nextResult;
|
|
@@ -50,14 +60,17 @@ concurrency = 8, } = {}) {
|
|
|
50
60
|
const inputIndex = unstartedIndex;
|
|
51
61
|
unstartedIndex++;
|
|
52
62
|
const input = inputsArray[inputIndex];
|
|
63
|
+
debug(`mapping input into Promise (callId: ${callId})`);
|
|
53
64
|
const promise = mapper(input, inputIndex, maybeLength || Infinity);
|
|
54
65
|
if (!isThenable(promise)) {
|
|
55
66
|
throw new Error("Mapper function passed into runJobs didn't return a Promise. The mapper function should always return a Promise. The easiest way to ensure this is the case is to make your mapper function an async function.");
|
|
56
67
|
}
|
|
57
68
|
const promiseWithMore = promise.then((result) => {
|
|
69
|
+
debug(`child Promise resolved for input (callId: ${callId}):`, input);
|
|
58
70
|
results[inputIndex] = result;
|
|
59
71
|
runningPromises.delete(promiseWithMore);
|
|
60
72
|
}, (err) => {
|
|
73
|
+
debug(`child Promise rejected for input (callId: ${callId}):`, input, "with error:", err);
|
|
61
74
|
runningPromises.delete(promiseWithMore);
|
|
62
75
|
error = err;
|
|
63
76
|
});
|
|
@@ -72,13 +85,16 @@ concurrency = 8, } = {}) {
|
|
|
72
85
|
while (runningPromises.size > 0 && !error) {
|
|
73
86
|
await Promise.race(runningPromises.values());
|
|
74
87
|
if (error) {
|
|
88
|
+
debug(`throwing error (callId: ${callId})`);
|
|
75
89
|
throw error;
|
|
76
90
|
}
|
|
77
91
|
await proceed();
|
|
78
92
|
}
|
|
79
93
|
if (error) {
|
|
94
|
+
debug(`throwing error (callId: ${callId})`);
|
|
80
95
|
throw error;
|
|
81
96
|
}
|
|
97
|
+
debug(`all done (callId: ${callId})`);
|
|
82
98
|
return results;
|
|
83
99
|
}
|
|
84
100
|
exports.runJobs = runJobs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parallel-park",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Parallel/concurrent async work, optionally using multiple processes",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -21,23 +21,25 @@
|
|
|
21
21
|
"promise",
|
|
22
22
|
"map"
|
|
23
23
|
],
|
|
24
|
-
"author": "Lily
|
|
24
|
+
"author": "Lily Skye <me@suchipi.com>",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/suchipi/parallel-park.git"
|
|
28
|
+
"url": "git+https://github.com/suchipi/parallel-park.git"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@babel/core": "^7.
|
|
32
|
-
"@babel/preset-env": "^7.
|
|
33
|
-
"@babel/preset-typescript": "^7.
|
|
34
|
-
"@types/
|
|
35
|
-
"@types/
|
|
36
|
-
"
|
|
37
|
-
"jest": "^
|
|
38
|
-
"
|
|
31
|
+
"@babel/core": "^7.24.5",
|
|
32
|
+
"@babel/preset-env": "^7.24.5",
|
|
33
|
+
"@babel/preset-typescript": "^7.24.1",
|
|
34
|
+
"@types/debug": "^4.1.12",
|
|
35
|
+
"@types/jest": "^29.5.12",
|
|
36
|
+
"@types/node": "^20.12.10",
|
|
37
|
+
"babel-jest": "^29.7.0",
|
|
38
|
+
"jest": "^29.7.0",
|
|
39
|
+
"typescript": "^5.4.5"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"
|
|
42
|
+
"@suchipi/error-utils": "^0.2.0",
|
|
43
|
+
"make-module-env": "^1.1.3"
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -1,39 +1,48 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import vm from "vm";
|
|
3
3
|
import makeModuleEnv from "make-module-env";
|
|
4
|
+
import makeDebug from "debug";
|
|
5
|
+
import { readUntilEnd } from "./read-until-end";
|
|
6
|
+
import path from "path";
|
|
4
7
|
|
|
5
|
-
const
|
|
8
|
+
const debug = makeDebug("parallel-park:child-process-worker");
|
|
9
|
+
|
|
10
|
+
const commsIn = fs.createReadStream(
|
|
6
11
|
// @ts-ignore
|
|
7
12
|
null,
|
|
8
13
|
{ fd: 3 }
|
|
9
14
|
);
|
|
15
|
+
const commsOut = fs.createWriteStream(
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
null,
|
|
18
|
+
{ fd: 4 }
|
|
19
|
+
);
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
debug("reading input data...");
|
|
22
|
+
readUntilEnd(commsIn)
|
|
23
|
+
.then((data) => {
|
|
24
|
+
debug("parsing input data...");
|
|
25
|
+
try {
|
|
26
|
+
const [inputs, fnString, callingFile] = JSON.parse(data);
|
|
27
|
+
onReady(inputs, fnString, callingFile);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
onError(err as Error);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.catch(onError);
|
|
12
33
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
}
|
|
34
|
+
function onReady(inputs: any, fnString: string, callingFile: string) {
|
|
35
|
+
debug("in onReady %o", { inputs, fnString, callingFile });
|
|
16
36
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
error: {
|
|
22
|
-
name: error.name,
|
|
23
|
-
message: error.message,
|
|
24
|
-
stack: error.stack,
|
|
25
|
-
},
|
|
26
|
-
})
|
|
27
|
-
);
|
|
28
|
-
}
|
|
37
|
+
// Relevant when callingFile is eg. "REPL2" (from Node.js repl)
|
|
38
|
+
if (!path.isAbsolute(callingFile)) {
|
|
39
|
+
callingFile = path.join(process.cwd(), "fake-path.js");
|
|
40
|
+
}
|
|
29
41
|
|
|
30
|
-
try {
|
|
31
42
|
const wrapperFn = vm.runInThisContext(
|
|
32
43
|
`(function moduleWrapper(exports, require, module, __filename, __dirname) {
|
|
33
|
-
return ${fnString};})`
|
|
44
|
+
return ${fnString};})`
|
|
34
45
|
);
|
|
35
|
-
const inputs = JSON.parse(inputsString);
|
|
36
|
-
|
|
37
46
|
const env = makeModuleEnv(callingFile);
|
|
38
47
|
const fn = wrapperFn(
|
|
39
48
|
env.exports,
|
|
@@ -53,6 +62,25 @@ return ${fnString};})`
|
|
|
53
62
|
} else {
|
|
54
63
|
onSuccess(result);
|
|
55
64
|
}
|
|
56
|
-
}
|
|
57
|
-
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onSuccess(data: any) {
|
|
68
|
+
debug("in onSuccess %o", { data });
|
|
69
|
+
|
|
70
|
+
commsOut.end(JSON.stringify({ type: "success", data }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onError(error: Error) {
|
|
74
|
+
debug("in onError %o", { error });
|
|
75
|
+
|
|
76
|
+
commsOut.end(
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
type: "error",
|
|
79
|
+
error: {
|
|
80
|
+
name: error.name,
|
|
81
|
+
message: error.message,
|
|
82
|
+
stack: error.stack,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
);
|
|
58
86
|
}
|
package/src/in-child-process.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import child_process from "child_process";
|
|
2
|
+
import type * as stream from "stream";
|
|
3
|
+
import { ParsedError } from "@suchipi/error-utils";
|
|
4
|
+
import makeDebug from "debug";
|
|
5
|
+
import util from "util";
|
|
6
|
+
import { readUntilEnd } from "./read-until-end";
|
|
7
|
+
|
|
8
|
+
const debug = makeDebug("parallel-park:in-child-process");
|
|
2
9
|
|
|
3
10
|
const runnerPath = require.resolve("../dist/child-process-worker");
|
|
4
11
|
|
|
@@ -11,7 +18,7 @@ type InChildProcess = {
|
|
|
11
18
|
<Result>(functionToRun: () => Result | Promise<Result>): Promise<Result>;
|
|
12
19
|
};
|
|
13
20
|
|
|
14
|
-
export const inChildProcess: InChildProcess = (...args) => {
|
|
21
|
+
export const inChildProcess: InChildProcess = (...args: Array<any>) => {
|
|
15
22
|
const inputs = typeof args[0] === "function" ? {} : args[0];
|
|
16
23
|
const functionToRun = typeof args[0] === "function" ? args[0] : args[1];
|
|
17
24
|
|
|
@@ -27,36 +34,42 @@ export const inChildProcess: InChildProcess = (...args) => {
|
|
|
27
34
|
);
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
const here = new Error("
|
|
37
|
+
const here = new ParsedError(new Error("here"));
|
|
38
|
+
|
|
39
|
+
const callingFrame = here.stackFrames[1];
|
|
40
|
+
const callingFile = callingFrame?.fileName ?? "unknown file";
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
const matches = callingFileLine.match(/\(([^\)]+):\d+:\d+\)/);
|
|
34
|
-
const [_, callingFile] = matches!;
|
|
42
|
+
debug("spawning child process: %o", [process.argv[0], runnerPath]);
|
|
35
43
|
|
|
36
|
-
const child = child_process.spawn(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
41
|
-
}
|
|
42
|
-
);
|
|
44
|
+
const child = child_process.spawn(process.argv[0], [runnerPath], {
|
|
45
|
+
stdio: ["inherit", "inherit", "inherit", "pipe", "pipe"],
|
|
46
|
+
});
|
|
43
47
|
|
|
44
48
|
return new Promise((resolve, reject) => {
|
|
45
49
|
child.on("error", reject);
|
|
46
50
|
|
|
51
|
+
const commsOut: stream.Writable = child.stdio![3] as any;
|
|
52
|
+
const commsIn: stream.Readable = child.stdio![4] as any;
|
|
53
|
+
|
|
54
|
+
child.on("spawn", () => {
|
|
55
|
+
const dataToSend = JSON.stringify([
|
|
56
|
+
inputs,
|
|
57
|
+
functionToRun.toString(),
|
|
58
|
+
callingFile,
|
|
59
|
+
]);
|
|
60
|
+
debug("sending inputs to child process: %o", dataToSend);
|
|
61
|
+
commsOut.end(dataToSend, "utf-8");
|
|
62
|
+
});
|
|
63
|
+
|
|
47
64
|
let receivedData = "";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
new Error("Failed to establish communication pipe with child process")
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
comms!.on("data", (chunk) => {
|
|
56
|
-
receivedData += chunk.toString();
|
|
65
|
+
readUntilEnd(commsIn).then((data) => {
|
|
66
|
+
debug("received data from child process: %o", data);
|
|
67
|
+
receivedData = data;
|
|
57
68
|
});
|
|
58
69
|
|
|
59
70
|
child.on("close", (code, signal) => {
|
|
71
|
+
debug("child process closed: %o", { code, signal });
|
|
72
|
+
|
|
60
73
|
if (code !== 0) {
|
|
61
74
|
reject(
|
|
62
75
|
new Error(
|
|
@@ -67,13 +80,32 @@ export const inChildProcess: InChildProcess = (...args) => {
|
|
|
67
80
|
)
|
|
68
81
|
);
|
|
69
82
|
} else {
|
|
70
|
-
|
|
83
|
+
debug("parsing received data from child process...");
|
|
84
|
+
let result: any;
|
|
85
|
+
try {
|
|
86
|
+
result = JSON.parse(receivedData);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
reject(
|
|
89
|
+
new Error(
|
|
90
|
+
`parallel-park error: failed to parse received data as JSON. data was: ${util.inspect(
|
|
91
|
+
receivedData,
|
|
92
|
+
{ colors: true, depth: Infinity }
|
|
93
|
+
)}`
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
71
98
|
switch (result.type) {
|
|
72
99
|
case "success": {
|
|
100
|
+
debug(
|
|
101
|
+
"child process finished successfully with result: %o",
|
|
102
|
+
result.data
|
|
103
|
+
);
|
|
73
104
|
resolve(result.data);
|
|
74
105
|
break;
|
|
75
106
|
}
|
|
76
107
|
case "error": {
|
|
108
|
+
debug("child process errored: %o", result.error);
|
|
77
109
|
const error = new Error(result.error.message);
|
|
78
110
|
Object.defineProperty(error, "name", { value: result.error.name });
|
|
79
111
|
Object.defineProperty(error, "stack", {
|
|
@@ -89,7 +121,7 @@ export const inChildProcess: InChildProcess = (...args) => {
|
|
|
89
121
|
.map((line) => {
|
|
90
122
|
if (/evalmachine/.test(line)) {
|
|
91
123
|
const lineWithoutEvalMachine = line.replace(
|
|
92
|
-
/evalmachine(
|
|
124
|
+
/evalmachine(?:\.<anonymous>)?/,
|
|
93
125
|
"<function passed into inChildProcess>"
|
|
94
126
|
);
|
|
95
127
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type stream from "stream";
|
|
2
|
+
import makeDebug from "debug";
|
|
3
|
+
const debug = makeDebug("parallel-park:read-until-end");
|
|
4
|
+
|
|
5
|
+
let streamId = 0;
|
|
6
|
+
|
|
7
|
+
export function readUntilEnd(stream: stream.Readable): Promise<string> {
|
|
8
|
+
const id = `${process.pid}-${streamId}`;
|
|
9
|
+
streamId++;
|
|
10
|
+
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
let data = "";
|
|
13
|
+
|
|
14
|
+
stream.on("data", (chunk) => {
|
|
15
|
+
const chunkStr = chunk.toString("utf-8");
|
|
16
|
+
debug("received data chunk from stream %s: %o", id, chunkStr);
|
|
17
|
+
data += chunkStr;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
stream.on("close", () => {
|
|
21
|
+
debug("stream %s closed; resolving with: %o", id, data);
|
|
22
|
+
resolve(data);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
package/src/run-jobs.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import makeDebug from "debug";
|
|
2
|
+
const debug = makeDebug("parallel-park:run-jobs");
|
|
3
|
+
|
|
1
4
|
function isThenable<T>(value: unknown): value is Promise<T> {
|
|
2
5
|
return (
|
|
3
6
|
typeof value === "object" &&
|
|
@@ -9,6 +12,8 @@ function isThenable<T>(value: unknown): value is Promise<T> {
|
|
|
9
12
|
|
|
10
13
|
const NOTHING = Symbol("NOTHING");
|
|
11
14
|
|
|
15
|
+
let runJobsCallId = 0;
|
|
16
|
+
|
|
12
17
|
export async function runJobs<T, U>(
|
|
13
18
|
inputs: Iterable<T | Promise<T>> | AsyncIterable<T | Promise<T>>,
|
|
14
19
|
mapper: (input: T, index: number, length: number) => Promise<U>,
|
|
@@ -24,6 +29,11 @@ export async function runJobs<T, U>(
|
|
|
24
29
|
concurrency?: number;
|
|
25
30
|
} = {}
|
|
26
31
|
): Promise<Array<U>> {
|
|
32
|
+
const callId = runJobsCallId;
|
|
33
|
+
runJobsCallId++;
|
|
34
|
+
|
|
35
|
+
debug(`runJobs called (callId: ${callId})`, { inputs, mapper, concurrency });
|
|
36
|
+
|
|
27
37
|
if (concurrency < 1) {
|
|
28
38
|
throw new Error(
|
|
29
39
|
"Concurrency can't be less than one; that doesn't make any sense."
|
|
@@ -39,6 +49,7 @@ export async function runJobs<T, U>(
|
|
|
39
49
|
let iteratorDone = false;
|
|
40
50
|
|
|
41
51
|
async function readInput(): Promise<boolean> {
|
|
52
|
+
debug(`reading next input (callId: ${callId})`);
|
|
42
53
|
let nextResult = inputIterator.next();
|
|
43
54
|
if (isThenable(nextResult)) {
|
|
44
55
|
nextResult = await nextResult;
|
|
@@ -70,6 +81,7 @@ export async function runJobs<T, U>(
|
|
|
70
81
|
unstartedIndex++;
|
|
71
82
|
|
|
72
83
|
const input = inputsArray[inputIndex];
|
|
84
|
+
debug(`mapping input into Promise (callId: ${callId})`);
|
|
73
85
|
const promise = mapper(input, inputIndex, maybeLength || Infinity);
|
|
74
86
|
|
|
75
87
|
if (!isThenable(promise)) {
|
|
@@ -80,10 +92,17 @@ export async function runJobs<T, U>(
|
|
|
80
92
|
|
|
81
93
|
const promiseWithMore = promise.then(
|
|
82
94
|
(result) => {
|
|
95
|
+
debug(`child Promise resolved for input (callId: ${callId}):`, input);
|
|
83
96
|
results[inputIndex] = result;
|
|
84
97
|
runningPromises.delete(promiseWithMore);
|
|
85
98
|
},
|
|
86
99
|
(err) => {
|
|
100
|
+
debug(
|
|
101
|
+
`child Promise rejected for input (callId: ${callId}):`,
|
|
102
|
+
input,
|
|
103
|
+
"with error:",
|
|
104
|
+
err
|
|
105
|
+
);
|
|
87
106
|
runningPromises.delete(promiseWithMore);
|
|
88
107
|
error = err;
|
|
89
108
|
}
|
|
@@ -101,14 +120,17 @@ export async function runJobs<T, U>(
|
|
|
101
120
|
while (runningPromises.size > 0 && !error) {
|
|
102
121
|
await Promise.race(runningPromises.values());
|
|
103
122
|
if (error) {
|
|
123
|
+
debug(`throwing error (callId: ${callId})`);
|
|
104
124
|
throw error;
|
|
105
125
|
}
|
|
106
126
|
await proceed();
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
if (error) {
|
|
130
|
+
debug(`throwing error (callId: ${callId})`);
|
|
110
131
|
throw error;
|
|
111
132
|
}
|
|
112
133
|
|
|
134
|
+
debug(`all done (callId: ${callId})`);
|
|
113
135
|
return results;
|
|
114
136
|
}
|