parallel-park 0.1.0 → 0.2.1
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/README.md +3 -1
- package/dist/child-process-worker.js +42 -20
- package/dist/in-child-process.js +28 -13
- package/dist/read-until-end.js +25 -0
- package/dist/run-jobs.js +58 -17
- package/package.json +14 -12
- package/src/child-process-worker.ts +51 -23
- package/src/in-child-process.ts +35 -22
- package/src/read-until-end.ts +24 -0
- package/src/run-jobs.ts +71 -21
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.
|
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@ Parallel/concurrent async work, optionally using multiple processes
|
|
|
10
10
|
|
|
11
11
|
`runJobs` is kinda like `Promise.all`, but instead of running everything at once, it'll only run a few Promises at a time (you can choose how many to run at once). It's inspired by [Bluebird's Promise.map function](http://bluebirdjs.com/docs/api/promise.map.html).
|
|
12
12
|
|
|
13
|
-
To use it, you pass in an array of inputs and a mapper function that transforms each input into a Promise. You can also optionally specify the maximum number of Promises to wait on at a time by passing an object with a `concurrency` property, which is a number. The concurrency defaults to 8.
|
|
13
|
+
To use it, you pass in an iterable (array, set, generator function, etc) of inputs and a mapper function that transforms each input into a Promise. You can also optionally specify the maximum number of Promises to wait on at a time by passing an object with a `concurrency` property, which is a number. The concurrency defaults to 8.
|
|
14
|
+
|
|
15
|
+
When using an iterable, if the iterable yields a Promise (ie. `iterable.next() returns { done: false, value: Promise }`), then the yielded Promise will be awaited before being passed into your mapper function. Additionally, async iterables are supported; if `iterable.next()` returns a Promise, it will be awaited.
|
|
14
16
|
|
|
15
17
|
```ts
|
|
16
18
|
import { runJobs } from "parallel-park";
|
|
@@ -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", { 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", { data });
|
|
54
|
+
commsOut.end(JSON.stringify({ type: "success", data }));
|
|
55
|
+
}
|
|
56
|
+
function onError(error) {
|
|
57
|
+
debug("in onError", { 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,13 @@ 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 read_until_end_1 = require("./read-until-end");
|
|
11
|
+
const debug = (0, debug_1.default)("parallel-park:in-child-process");
|
|
8
12
|
const runnerPath = require.resolve("../dist/child-process-worker");
|
|
9
13
|
const inChildProcess = (...args) => {
|
|
14
|
+
var _a;
|
|
10
15
|
const inputs = typeof args[0] === "function" ? {} : args[0];
|
|
11
16
|
const functionToRun = typeof args[0] === "function" ? args[0] : args[1];
|
|
12
17
|
if (typeof inputs !== "object") {
|
|
@@ -15,24 +20,33 @@ const inChildProcess = (...args) => {
|
|
|
15
20
|
if (typeof functionToRun !== "function") {
|
|
16
21
|
throw new Error("The second argument to inChildProcess should be a function to run in the child process.");
|
|
17
22
|
}
|
|
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"],
|
|
23
|
+
const here = new error_utils_1.ParsedError(new Error("here"));
|
|
24
|
+
const callingFrame = here.stackFrames[1];
|
|
25
|
+
const callingFile = (_a = callingFrame === null || callingFrame === void 0 ? void 0 : callingFrame.fileName) !== null && _a !== void 0 ? _a : "unknown file";
|
|
26
|
+
debug("spawning child process:", [process.argv[0], runnerPath]);
|
|
27
|
+
const child = child_process_1.default.spawn(process.argv[0], [runnerPath], {
|
|
28
|
+
stdio: ["inherit", "inherit", "inherit", "pipe", "pipe"],
|
|
24
29
|
});
|
|
25
30
|
return new Promise((resolve, reject) => {
|
|
26
31
|
child.on("error", reject);
|
|
32
|
+
const commsOut = child.stdio[3];
|
|
33
|
+
const commsIn = child.stdio[4];
|
|
34
|
+
child.on("spawn", () => {
|
|
35
|
+
const dataToSend = JSON.stringify([
|
|
36
|
+
inputs,
|
|
37
|
+
functionToRun.toString(),
|
|
38
|
+
callingFile,
|
|
39
|
+
]);
|
|
40
|
+
debug("sending inputs to child process:", dataToSend);
|
|
41
|
+
commsOut.end(dataToSend, "utf-8");
|
|
42
|
+
});
|
|
27
43
|
let receivedData = "";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
comms.on("data", (chunk) => {
|
|
33
|
-
receivedData += chunk.toString();
|
|
44
|
+
(0, read_until_end_1.readUntilEnd)(commsIn).then((data) => {
|
|
45
|
+
debug("received data from child process");
|
|
46
|
+
receivedData = data;
|
|
34
47
|
});
|
|
35
48
|
child.on("close", (code, signal) => {
|
|
49
|
+
debug("child process closed:", { code, signal });
|
|
36
50
|
if (code !== 0) {
|
|
37
51
|
reject(new Error(`Child process exited with nonzero status code: ${JSON.stringify({
|
|
38
52
|
code,
|
|
@@ -40,6 +54,7 @@ const inChildProcess = (...args) => {
|
|
|
40
54
|
})}`));
|
|
41
55
|
}
|
|
42
56
|
else {
|
|
57
|
+
debug("parsing received data from child process...");
|
|
43
58
|
const result = JSON.parse(receivedData);
|
|
44
59
|
switch (result.type) {
|
|
45
60
|
case "success": {
|
|
@@ -60,7 +75,7 @@ const inChildProcess = (...args) => {
|
|
|
60
75
|
.filter((line) => !/node:internal|node:events/.test(line))
|
|
61
76
|
.map((line) => {
|
|
62
77
|
if (/evalmachine/.test(line)) {
|
|
63
|
-
const lineWithoutEvalMachine = line.replace(/evalmachine(
|
|
78
|
+
const lineWithoutEvalMachine = line.replace(/evalmachine(?:\.<anonymous>)?/, "<function passed into inChildProcess>");
|
|
64
79
|
const matches = line.match(/:(\d+):(\d+)\)?$/);
|
|
65
80
|
if (!matches) {
|
|
66
81
|
return lineWithoutEvalMachine;
|
|
@@ -0,0 +1,25 @@
|
|
|
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 = streamId;
|
|
12
|
+
streamId++;
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
let data = "";
|
|
15
|
+
stream.on("data", (chunk) => {
|
|
16
|
+
debug("received data chunk from stream", id);
|
|
17
|
+
data += chunk.toString("utf-8");
|
|
18
|
+
});
|
|
19
|
+
stream.on("close", () => {
|
|
20
|
+
debug(`stream ${id} closed; resolving`);
|
|
21
|
+
resolve(data);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
exports.readUntilEnd = readUntilEnd;
|
package/dist/run-jobs.js
CHANGED
|
@@ -1,59 +1,100 @@
|
|
|
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");
|
|
9
|
+
function isThenable(value) {
|
|
10
|
+
return (typeof value === "object" &&
|
|
11
|
+
value != null &&
|
|
12
|
+
// @ts-ignore accessing .then
|
|
13
|
+
typeof value.then === "function");
|
|
14
|
+
}
|
|
15
|
+
const NOTHING = Symbol("NOTHING");
|
|
16
|
+
let runJobsCallId = 0;
|
|
4
17
|
async function runJobs(inputs, mapper, {
|
|
5
18
|
/**
|
|
6
19
|
* How many jobs are allowed to run at once.
|
|
7
20
|
*/
|
|
8
21
|
concurrency = 8, } = {}) {
|
|
22
|
+
const callId = runJobsCallId;
|
|
23
|
+
runJobsCallId++;
|
|
24
|
+
debug(`runJobs called (callId: ${callId})`, { inputs, mapper, concurrency });
|
|
9
25
|
if (concurrency < 1) {
|
|
10
26
|
throw new Error("Concurrency can't be less than one; that doesn't make any sense.");
|
|
11
27
|
}
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
const inputsArray = [];
|
|
29
|
+
const inputIteratorFactory = inputs[Symbol.asyncIterator || NOTHING] || inputs[Symbol.iterator];
|
|
30
|
+
const inputIterator = inputIteratorFactory.call(inputs);
|
|
31
|
+
const maybeLength = Array.isArray(inputs) ? inputs.length : null;
|
|
32
|
+
let iteratorDone = false;
|
|
33
|
+
async function readInput() {
|
|
34
|
+
debug(`reading next input (callId: ${callId})`);
|
|
35
|
+
let nextResult = inputIterator.next();
|
|
36
|
+
if (isThenable(nextResult)) {
|
|
37
|
+
nextResult = await nextResult;
|
|
38
|
+
}
|
|
39
|
+
if (nextResult.done) {
|
|
40
|
+
iteratorDone = true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
let value = nextResult.value;
|
|
45
|
+
if (isThenable(value)) {
|
|
46
|
+
value = await value;
|
|
47
|
+
}
|
|
48
|
+
inputsArray.push(value);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
14
51
|
}
|
|
15
|
-
concurrency = Math.min(concurrency, inputs.length);
|
|
16
52
|
let unstartedIndex = 0;
|
|
17
|
-
const results = new Array(
|
|
53
|
+
const results = new Array(maybeLength || 0);
|
|
18
54
|
const runningPromises = new Set();
|
|
19
55
|
let error = null;
|
|
20
|
-
function takeInput() {
|
|
56
|
+
async function takeInput() {
|
|
57
|
+
const read = await readInput();
|
|
58
|
+
if (!read)
|
|
59
|
+
return;
|
|
21
60
|
const inputIndex = unstartedIndex;
|
|
22
61
|
unstartedIndex++;
|
|
23
|
-
const input =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
typeof promise.then !== "function") {
|
|
62
|
+
const input = inputsArray[inputIndex];
|
|
63
|
+
debug(`mapping input into Promise (callId: ${callId})`);
|
|
64
|
+
const promise = mapper(input, inputIndex, maybeLength || Infinity);
|
|
65
|
+
if (!isThenable(promise)) {
|
|
28
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.");
|
|
29
67
|
}
|
|
30
68
|
const promiseWithMore = promise.then((result) => {
|
|
69
|
+
debug(`child Promise resolved for input (callId: ${callId}):`, input);
|
|
31
70
|
results[inputIndex] = result;
|
|
32
71
|
runningPromises.delete(promiseWithMore);
|
|
33
72
|
}, (err) => {
|
|
73
|
+
debug(`child Promise rejected for input (callId: ${callId}):`, input, "with error:", err);
|
|
34
74
|
runningPromises.delete(promiseWithMore);
|
|
35
75
|
error = err;
|
|
36
76
|
});
|
|
37
77
|
runningPromises.add(promiseWithMore);
|
|
38
78
|
}
|
|
39
|
-
function proceed() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
takeInput();
|
|
43
|
-
}
|
|
79
|
+
async function proceed() {
|
|
80
|
+
while (!iteratorDone && runningPromises.size < concurrency) {
|
|
81
|
+
await takeInput();
|
|
44
82
|
}
|
|
45
83
|
}
|
|
46
|
-
proceed();
|
|
84
|
+
await proceed();
|
|
47
85
|
while (runningPromises.size > 0 && !error) {
|
|
48
86
|
await Promise.race(runningPromises.values());
|
|
49
87
|
if (error) {
|
|
88
|
+
debug(`throwing error (callId: ${callId})`);
|
|
50
89
|
throw error;
|
|
51
90
|
}
|
|
52
|
-
proceed();
|
|
91
|
+
await proceed();
|
|
53
92
|
}
|
|
54
93
|
if (error) {
|
|
94
|
+
debug(`throwing error (callId: ${callId})`);
|
|
55
95
|
throw error;
|
|
56
96
|
}
|
|
97
|
+
debug(`all done (callId: ${callId})`);
|
|
57
98
|
return results;
|
|
58
99
|
}
|
|
59
100
|
exports.runJobs = runJobs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parallel-park",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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", { 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", { data });
|
|
69
|
+
|
|
70
|
+
commsOut.end(JSON.stringify({ type: "success", data }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onError(error: Error) {
|
|
74
|
+
debug("in onError", { 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,10 @@
|
|
|
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 { readUntilEnd } from "./read-until-end";
|
|
6
|
+
|
|
7
|
+
const debug = makeDebug("parallel-park:in-child-process");
|
|
2
8
|
|
|
3
9
|
const runnerPath = require.resolve("../dist/child-process-worker");
|
|
4
10
|
|
|
@@ -11,7 +17,7 @@ type InChildProcess = {
|
|
|
11
17
|
<Result>(functionToRun: () => Result | Promise<Result>): Promise<Result>;
|
|
12
18
|
};
|
|
13
19
|
|
|
14
|
-
export const inChildProcess: InChildProcess = (...args) => {
|
|
20
|
+
export const inChildProcess: InChildProcess = (...args: Array<any>) => {
|
|
15
21
|
const inputs = typeof args[0] === "function" ? {} : args[0];
|
|
16
22
|
const functionToRun = typeof args[0] === "function" ? args[0] : args[1];
|
|
17
23
|
|
|
@@ -27,36 +33,42 @@ export const inChildProcess: InChildProcess = (...args) => {
|
|
|
27
33
|
);
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
const here = new Error("
|
|
36
|
+
const here = new ParsedError(new Error("here"));
|
|
37
|
+
|
|
38
|
+
const callingFrame = here.stackFrames[1];
|
|
39
|
+
const callingFile = callingFrame?.fileName ?? "unknown file";
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
const matches = callingFileLine.match(/\(([^\)]+):\d+:\d+\)/);
|
|
34
|
-
const [_, callingFile] = matches!;
|
|
41
|
+
debug("spawning child process:", [process.argv[0], runnerPath]);
|
|
35
42
|
|
|
36
|
-
const child = child_process.spawn(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
41
|
-
}
|
|
42
|
-
);
|
|
43
|
+
const child = child_process.spawn(process.argv[0], [runnerPath], {
|
|
44
|
+
stdio: ["inherit", "inherit", "inherit", "pipe", "pipe"],
|
|
45
|
+
});
|
|
43
46
|
|
|
44
47
|
return new Promise((resolve, reject) => {
|
|
45
48
|
child.on("error", reject);
|
|
46
49
|
|
|
50
|
+
const commsOut: stream.Writable = child.stdio![3] as any;
|
|
51
|
+
const commsIn: stream.Readable = child.stdio![4] as any;
|
|
52
|
+
|
|
53
|
+
child.on("spawn", () => {
|
|
54
|
+
const dataToSend = JSON.stringify([
|
|
55
|
+
inputs,
|
|
56
|
+
functionToRun.toString(),
|
|
57
|
+
callingFile,
|
|
58
|
+
]);
|
|
59
|
+
debug("sending inputs to child process:", dataToSend);
|
|
60
|
+
commsOut.end(dataToSend, "utf-8");
|
|
61
|
+
});
|
|
62
|
+
|
|
47
63
|
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();
|
|
64
|
+
readUntilEnd(commsIn).then((data) => {
|
|
65
|
+
debug("received data from child process");
|
|
66
|
+
receivedData = data;
|
|
57
67
|
});
|
|
58
68
|
|
|
59
69
|
child.on("close", (code, signal) => {
|
|
70
|
+
debug("child process closed:", { code, signal });
|
|
71
|
+
|
|
60
72
|
if (code !== 0) {
|
|
61
73
|
reject(
|
|
62
74
|
new Error(
|
|
@@ -67,6 +79,7 @@ export const inChildProcess: InChildProcess = (...args) => {
|
|
|
67
79
|
)
|
|
68
80
|
);
|
|
69
81
|
} else {
|
|
82
|
+
debug("parsing received data from child process...");
|
|
70
83
|
const result = JSON.parse(receivedData);
|
|
71
84
|
switch (result.type) {
|
|
72
85
|
case "success": {
|
|
@@ -89,7 +102,7 @@ export const inChildProcess: InChildProcess = (...args) => {
|
|
|
89
102
|
.map((line) => {
|
|
90
103
|
if (/evalmachine/.test(line)) {
|
|
91
104
|
const lineWithoutEvalMachine = line.replace(
|
|
92
|
-
/evalmachine(
|
|
105
|
+
/evalmachine(?:\.<anonymous>)?/,
|
|
93
106
|
"<function passed into inChildProcess>"
|
|
94
107
|
);
|
|
95
108
|
|
|
@@ -0,0 +1,24 @@
|
|
|
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 = streamId;
|
|
9
|
+
streamId++;
|
|
10
|
+
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
let data = "";
|
|
13
|
+
|
|
14
|
+
stream.on("data", (chunk) => {
|
|
15
|
+
debug("received data chunk from stream", id);
|
|
16
|
+
data += chunk.toString("utf-8");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
stream.on("close", () => {
|
|
20
|
+
debug(`stream ${id} closed; resolving`);
|
|
21
|
+
resolve(data);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
package/src/run-jobs.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
import makeDebug from "debug";
|
|
2
|
+
const debug = makeDebug("parallel-park:run-jobs");
|
|
3
|
+
|
|
4
|
+
function isThenable<T>(value: unknown): value is Promise<T> {
|
|
5
|
+
return (
|
|
6
|
+
typeof value === "object" &&
|
|
7
|
+
value != null &&
|
|
8
|
+
// @ts-ignore accessing .then
|
|
9
|
+
typeof value.then === "function"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const NOTHING = Symbol("NOTHING");
|
|
14
|
+
|
|
15
|
+
let runJobsCallId = 0;
|
|
16
|
+
|
|
1
17
|
export async function runJobs<T, U>(
|
|
2
|
-
inputs:
|
|
18
|
+
inputs: Iterable<T | Promise<T>> | AsyncIterable<T | Promise<T>>,
|
|
3
19
|
mapper: (input: T, index: number, length: number) => Promise<U>,
|
|
4
20
|
{
|
|
5
21
|
/**
|
|
@@ -13,36 +29,62 @@ export async function runJobs<T, U>(
|
|
|
13
29
|
concurrency?: number;
|
|
14
30
|
} = {}
|
|
15
31
|
): Promise<Array<U>> {
|
|
32
|
+
const callId = runJobsCallId;
|
|
33
|
+
runJobsCallId++;
|
|
34
|
+
|
|
35
|
+
debug(`runJobs called (callId: ${callId})`, { inputs, mapper, concurrency });
|
|
36
|
+
|
|
16
37
|
if (concurrency < 1) {
|
|
17
38
|
throw new Error(
|
|
18
39
|
"Concurrency can't be less than one; that doesn't make any sense."
|
|
19
40
|
);
|
|
20
41
|
}
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
const inputsArray: Array<T> = [];
|
|
44
|
+
const inputIteratorFactory =
|
|
45
|
+
inputs[Symbol.asyncIterator || NOTHING] || inputs[Symbol.iterator];
|
|
46
|
+
const inputIterator = inputIteratorFactory.call(inputs);
|
|
47
|
+
const maybeLength = Array.isArray(inputs) ? inputs.length : null;
|
|
25
48
|
|
|
26
|
-
|
|
49
|
+
let iteratorDone = false;
|
|
50
|
+
|
|
51
|
+
async function readInput(): Promise<boolean> {
|
|
52
|
+
debug(`reading next input (callId: ${callId})`);
|
|
53
|
+
let nextResult = inputIterator.next();
|
|
54
|
+
if (isThenable(nextResult)) {
|
|
55
|
+
nextResult = await nextResult;
|
|
56
|
+
}
|
|
57
|
+
if (nextResult.done) {
|
|
58
|
+
iteratorDone = true;
|
|
59
|
+
return false;
|
|
60
|
+
} else {
|
|
61
|
+
let value = nextResult.value;
|
|
62
|
+
if (isThenable<T>(value)) {
|
|
63
|
+
value = await value;
|
|
64
|
+
}
|
|
65
|
+
inputsArray.push(value);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
27
69
|
|
|
28
70
|
let unstartedIndex = 0;
|
|
29
71
|
|
|
30
|
-
const results = new Array(
|
|
72
|
+
const results = new Array(maybeLength || 0);
|
|
31
73
|
const runningPromises = new Set();
|
|
32
74
|
let error: Error | null = null;
|
|
33
75
|
|
|
34
|
-
function takeInput() {
|
|
76
|
+
async function takeInput() {
|
|
77
|
+
const read = await readInput();
|
|
78
|
+
if (!read) return;
|
|
79
|
+
|
|
35
80
|
const inputIndex = unstartedIndex;
|
|
36
81
|
unstartedIndex++;
|
|
37
82
|
|
|
38
|
-
const input =
|
|
39
|
-
|
|
83
|
+
const input = inputsArray[inputIndex];
|
|
84
|
+
debug(`mapping input into Promise (callId: ${callId})`);
|
|
85
|
+
const promise = mapper(input, inputIndex, maybeLength || Infinity);
|
|
40
86
|
|
|
41
|
-
if (
|
|
42
|
-
typeof promise !== "object" ||
|
|
43
|
-
promise == null ||
|
|
44
|
-
typeof promise.then !== "function"
|
|
45
|
-
) {
|
|
87
|
+
if (!isThenable(promise)) {
|
|
46
88
|
throw new Error(
|
|
47
89
|
"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."
|
|
48
90
|
);
|
|
@@ -50,10 +92,17 @@ export async function runJobs<T, U>(
|
|
|
50
92
|
|
|
51
93
|
const promiseWithMore = promise.then(
|
|
52
94
|
(result) => {
|
|
95
|
+
debug(`child Promise resolved for input (callId: ${callId}):`, input);
|
|
53
96
|
results[inputIndex] = result;
|
|
54
97
|
runningPromises.delete(promiseWithMore);
|
|
55
98
|
},
|
|
56
99
|
(err) => {
|
|
100
|
+
debug(
|
|
101
|
+
`child Promise rejected for input (callId: ${callId}):`,
|
|
102
|
+
input,
|
|
103
|
+
"with error:",
|
|
104
|
+
err
|
|
105
|
+
);
|
|
57
106
|
runningPromises.delete(promiseWithMore);
|
|
58
107
|
error = err;
|
|
59
108
|
}
|
|
@@ -61,26 +110,27 @@ export async function runJobs<T, U>(
|
|
|
61
110
|
runningPromises.add(promiseWithMore);
|
|
62
111
|
}
|
|
63
112
|
|
|
64
|
-
function proceed() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
takeInput();
|
|
68
|
-
}
|
|
113
|
+
async function proceed() {
|
|
114
|
+
while (!iteratorDone && runningPromises.size < concurrency) {
|
|
115
|
+
await takeInput();
|
|
69
116
|
}
|
|
70
117
|
}
|
|
71
118
|
|
|
72
|
-
proceed();
|
|
119
|
+
await proceed();
|
|
73
120
|
while (runningPromises.size > 0 && !error) {
|
|
74
121
|
await Promise.race(runningPromises.values());
|
|
75
122
|
if (error) {
|
|
123
|
+
debug(`throwing error (callId: ${callId})`);
|
|
76
124
|
throw error;
|
|
77
125
|
}
|
|
78
|
-
proceed();
|
|
126
|
+
await proceed();
|
|
79
127
|
}
|
|
80
128
|
|
|
81
129
|
if (error) {
|
|
130
|
+
debug(`throwing error (callId: ${callId})`);
|
|
82
131
|
throw error;
|
|
83
132
|
}
|
|
84
133
|
|
|
134
|
+
debug(`all done (callId: ${callId})`);
|
|
85
135
|
return results;
|
|
86
136
|
}
|