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 CHANGED
@@ -1 +1 @@
1
- v16.14.0
1
+ v20.11.1
package/.npm-version CHANGED
@@ -1 +1 @@
1
- 8.3.1
1
+ 10.2.4
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- MIT License Copyright (c) 2022 Lily Scott
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 comms = fs_1.default.createWriteStream(
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 [inputsString, fnString, callingFile] = process.argv.slice(2);
13
- function onSuccess(data) {
14
- comms.write(JSON.stringify({ type: "success", data }));
15
- }
16
- function onError(error) {
17
- comms.write(JSON.stringify({
18
- type: "error",
19
- error: {
20
- name: error.name,
21
- message: error.message,
22
- stack: error.stack,
23
- },
24
- }));
25
- }
26
- try {
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
- catch (err) {
43
- onError(err);
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
  }
@@ -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("in inChildProcess");
19
- const callingFileLine = here.stack.split("\n").slice(2)[0];
20
- const matches = callingFileLine.match(/\(([^\)]+):\d+:\d+\)/);
21
- const [_, callingFile] = matches;
22
- const child = child_process_1.default.spawn(process.argv[0], [runnerPath, JSON.stringify(inputs), functionToRun.toString(), callingFile], {
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
- const comms = child.stdio && child.stdio[3];
29
- if (!comms) {
30
- reject(new Error("Failed to establish communication pipe with child process"));
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(\.<anonymous>)?/, "<function passed into inChildProcess>");
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
- if (inputs.length === 0) {
13
- return Promise.resolve([]);
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(inputs.length);
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 = inputs[inputIndex];
24
- const promise = mapper(input, inputIndex, inputs.length);
25
- if (typeof promise !== "object" ||
26
- promise == null ||
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
- if (unstartedIndex < inputs.length) {
41
- while (runningPromises.size < concurrency) {
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.0",
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 Scott <me@suchipi.com>",
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.17.5",
32
- "@babel/preset-env": "^7.16.11",
33
- "@babel/preset-typescript": "^7.16.7",
34
- "@types/jest": "^27.4.0",
35
- "@types/node": "^17.0.18",
36
- "babel-jest": "^27.5.1",
37
- "jest": "^27.5.1",
38
- "typescript": "^4.5.5"
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
- "make-module-env": "^1.0.1"
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 comms = fs.createWriteStream(
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
- const [inputsString, fnString, callingFile] = process.argv.slice(2);
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 onSuccess(data: any) {
14
- comms.write(JSON.stringify({ type: "success", data }));
15
- }
34
+ function onReady(inputs: any, fnString: string, callingFile: string) {
35
+ debug("in onReady", { inputs, fnString, callingFile });
16
36
 
17
- function onError(error: Error) {
18
- comms.write(
19
- JSON.stringify({
20
- type: "error",
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
- } catch (err) {
57
- onError(err as Error);
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
  }
@@ -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("in inChildProcess");
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
- const callingFileLine = here.stack!.split("\n").slice(2)[0];
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
- process.argv[0],
38
- [runnerPath, JSON.stringify(inputs), functionToRun.toString(), callingFile],
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
- const comms = child.stdio && child.stdio[3];
49
- if (!comms) {
50
- reject(
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(\.<anonymous>)?/,
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: Array<T>,
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
- if (inputs.length === 0) {
23
- return Promise.resolve([]);
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
- concurrency = Math.min(concurrency, inputs.length);
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(inputs.length);
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 = inputs[inputIndex];
39
- const promise = mapper(input, inputIndex, inputs.length);
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
- if (unstartedIndex < inputs.length) {
66
- while (runningPromises.size < concurrency) {
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
  }