@parallel-park/run-jobs 0.3.0
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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/index.js +6 -0
- package/dist/run-jobs.js +119 -0
- package/package.json +30 -0
- package/src/index.ts +1 -0
- package/src/run-jobs.ts +159 -0
- package/tsconfig.json +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License Copyright (c) 2022-2026 Lily Skye
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of
|
|
4
|
+
charge, to any person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use, copy, modify, merge,
|
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice
|
|
12
|
+
(including the next paragraph) shall be included in all copies or substantial
|
|
13
|
+
portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
16
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
18
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
19
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @parallel-park/run-jobs
|
|
2
|
+
|
|
3
|
+
Parallel async work with an optional concurrency limit, like [Bluebird's Promise.map](http://bluebirdjs.com/docs/api/promise.map.html).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
`@parallel-park/run-jobs` exports one function: `runJobs`.
|
|
8
|
+
|
|
9
|
+
### `runJobs`
|
|
10
|
+
|
|
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
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { runJobs } from "parallel-park";
|
|
19
|
+
|
|
20
|
+
const inputs = ["alice", "bob", "carl", "dorsey", "edith"];
|
|
21
|
+
const results = await runJobs(
|
|
22
|
+
inputs,
|
|
23
|
+
async (name, index, inputsCount) => {
|
|
24
|
+
// Do whatever async work you want inside this mapper function.
|
|
25
|
+
// In this case, we use a hypothetical "getUser" function to
|
|
26
|
+
// retrieve data about a user from some web API.
|
|
27
|
+
console.log(`Getting fullName for ${name}...`);
|
|
28
|
+
const user = await getUser(name);
|
|
29
|
+
return user.fullName;
|
|
30
|
+
},
|
|
31
|
+
// This options object with concurrency is an optional argument.
|
|
32
|
+
// If unspecified, it defaults to { concurrency: 8 }
|
|
33
|
+
{
|
|
34
|
+
// This number specifies how many times to call the mapper
|
|
35
|
+
// function before waiting for one of the returned Promises
|
|
36
|
+
// to resolve. Ie. "How many promises to have in-flight concurrently"
|
|
37
|
+
concurrency: 2,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
// Logs these two immediately:
|
|
41
|
+
//
|
|
42
|
+
// Getting fullName for alice...
|
|
43
|
+
// Getting fullName for bob...
|
|
44
|
+
//
|
|
45
|
+
// But, it doesn't log anything else yet, because we told it to only run two things at a time.
|
|
46
|
+
// Then, after one of those Promises has finished, it logs:
|
|
47
|
+
//
|
|
48
|
+
// Getting fullName for carl...
|
|
49
|
+
//
|
|
50
|
+
// And so forth, until all of them are done.
|
|
51
|
+
|
|
52
|
+
// `results` is an Array of the resolved value you returned from your mapper function.
|
|
53
|
+
// The indices in the array correspond to the indices of your inputs.
|
|
54
|
+
console.log(results);
|
|
55
|
+
// Logs:
|
|
56
|
+
// [
|
|
57
|
+
// "Alice Smith",
|
|
58
|
+
// "Bob Eriksson",
|
|
59
|
+
// "Carl Martinez",
|
|
60
|
+
// "Dorsey Toth",
|
|
61
|
+
// "Edith Skalla"
|
|
62
|
+
// ]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setDebug = exports.runJobs = void 0;
|
|
4
|
+
var run_jobs_1 = require("./run-jobs");
|
|
5
|
+
Object.defineProperty(exports, "runJobs", { enumerable: true, get: function () { return run_jobs_1.runJobs; } });
|
|
6
|
+
Object.defineProperty(exports, "setDebug", { enumerable: true, get: function () { return run_jobs_1.setDebug; } });
|
package/dist/run-jobs.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setDebug = setDebug;
|
|
4
|
+
exports.runJobs = runJobs;
|
|
5
|
+
let debug = null;
|
|
6
|
+
function setDebug(debugFunction) {
|
|
7
|
+
debug = debugFunction;
|
|
8
|
+
}
|
|
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;
|
|
17
|
+
async function runJobs(inputs, mapper, {
|
|
18
|
+
/**
|
|
19
|
+
* How many jobs are allowed to run at once.
|
|
20
|
+
*/
|
|
21
|
+
concurrency = 8, } = {}) {
|
|
22
|
+
const callId = runJobsCallId;
|
|
23
|
+
runJobsCallId++;
|
|
24
|
+
if (debug) {
|
|
25
|
+
debug(`runJobs called (callId: ${callId})`, {
|
|
26
|
+
inputs,
|
|
27
|
+
mapper,
|
|
28
|
+
concurrency,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (concurrency < 1) {
|
|
32
|
+
throw new Error("Concurrency can't be less than one; that doesn't make any sense.");
|
|
33
|
+
}
|
|
34
|
+
const inputsArray = [];
|
|
35
|
+
const inputIteratorFactory = inputs[Symbol.asyncIterator || NOTHING] || inputs[Symbol.iterator];
|
|
36
|
+
const inputIterator = inputIteratorFactory.call(inputs);
|
|
37
|
+
const maybeLength = Array.isArray(inputs) ? inputs.length : null;
|
|
38
|
+
let iteratorDone = false;
|
|
39
|
+
async function readInput() {
|
|
40
|
+
if (debug) {
|
|
41
|
+
debug(`reading next input (callId: ${callId})`);
|
|
42
|
+
}
|
|
43
|
+
let nextResult = inputIterator.next();
|
|
44
|
+
if (isThenable(nextResult)) {
|
|
45
|
+
nextResult = await nextResult;
|
|
46
|
+
}
|
|
47
|
+
if (nextResult.done) {
|
|
48
|
+
iteratorDone = true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
let value = nextResult.value;
|
|
53
|
+
if (isThenable(value)) {
|
|
54
|
+
value = await value;
|
|
55
|
+
}
|
|
56
|
+
inputsArray.push(value);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let unstartedIndex = 0;
|
|
61
|
+
const results = new Array(maybeLength || 0);
|
|
62
|
+
const runningPromises = new Set();
|
|
63
|
+
let error = null;
|
|
64
|
+
async function takeInput() {
|
|
65
|
+
const read = await readInput();
|
|
66
|
+
if (!read)
|
|
67
|
+
return;
|
|
68
|
+
const inputIndex = unstartedIndex;
|
|
69
|
+
unstartedIndex++;
|
|
70
|
+
const input = inputsArray[inputIndex];
|
|
71
|
+
if (debug) {
|
|
72
|
+
debug(`mapping input into Promise (callId: ${callId})`);
|
|
73
|
+
}
|
|
74
|
+
const promise = mapper(input, inputIndex, maybeLength || Infinity);
|
|
75
|
+
if (!isThenable(promise)) {
|
|
76
|
+
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.");
|
|
77
|
+
}
|
|
78
|
+
const promiseWithMore = promise.then((result) => {
|
|
79
|
+
if (debug) {
|
|
80
|
+
debug(`child Promise resolved for input (callId: ${callId}):`, input);
|
|
81
|
+
}
|
|
82
|
+
results[inputIndex] = result;
|
|
83
|
+
runningPromises.delete(promiseWithMore);
|
|
84
|
+
}, (err) => {
|
|
85
|
+
if (debug) {
|
|
86
|
+
debug(`child Promise rejected for input (callId: ${callId}):`, input, "with error:", err);
|
|
87
|
+
}
|
|
88
|
+
runningPromises.delete(promiseWithMore);
|
|
89
|
+
error = err;
|
|
90
|
+
});
|
|
91
|
+
runningPromises.add(promiseWithMore);
|
|
92
|
+
}
|
|
93
|
+
async function proceed() {
|
|
94
|
+
while (!iteratorDone && runningPromises.size < concurrency) {
|
|
95
|
+
await takeInput();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await proceed();
|
|
99
|
+
while (runningPromises.size > 0 && !error) {
|
|
100
|
+
await Promise.race(runningPromises.values());
|
|
101
|
+
if (error) {
|
|
102
|
+
if (debug) {
|
|
103
|
+
debug(`throwing error (callId: ${callId})`);
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
await proceed();
|
|
108
|
+
}
|
|
109
|
+
if (error) {
|
|
110
|
+
if (debug) {
|
|
111
|
+
debug(`throwing error (callId: ${callId})`);
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
if (debug) {
|
|
116
|
+
debug(`all done (callId: ${callId})`);
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parallel-park/run-jobs",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Parallel async work with an optional concurrency limit, like Bluebird's Promise.map",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"build": "rm -rf ./dist && tsc"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"parallel",
|
|
13
|
+
"concurrent",
|
|
14
|
+
"concurrency",
|
|
15
|
+
"co-operative",
|
|
16
|
+
"promise",
|
|
17
|
+
"map",
|
|
18
|
+
"bluebird",
|
|
19
|
+
"bluebird.map"
|
|
20
|
+
],
|
|
21
|
+
"author": "Lily Skye <me@suchipi.com>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/suchipi/parallel-park.git"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runJobs, setDebug } from "./run-jobs";
|
package/src/run-jobs.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
let debug: null | ((...args: any) => void) = null;
|
|
2
|
+
|
|
3
|
+
export function setDebug(debugFunction: typeof debug) {
|
|
4
|
+
debug = debugFunction;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isThenable<T>(value: unknown): value is Promise<T> {
|
|
8
|
+
return (
|
|
9
|
+
typeof value === "object" &&
|
|
10
|
+
value != null &&
|
|
11
|
+
// @ts-ignore accessing .then
|
|
12
|
+
typeof value.then === "function"
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const NOTHING = Symbol("NOTHING");
|
|
17
|
+
|
|
18
|
+
let runJobsCallId = 0;
|
|
19
|
+
|
|
20
|
+
export async function runJobs<T, U>(
|
|
21
|
+
inputs: Iterable<T | Promise<T>> | AsyncIterable<T | Promise<T>>,
|
|
22
|
+
mapper: (input: T, index: number, length: number) => Promise<U>,
|
|
23
|
+
{
|
|
24
|
+
/**
|
|
25
|
+
* How many jobs are allowed to run at once.
|
|
26
|
+
*/
|
|
27
|
+
concurrency = 8,
|
|
28
|
+
}: {
|
|
29
|
+
/**
|
|
30
|
+
* How many jobs are allowed to run at once.
|
|
31
|
+
*/
|
|
32
|
+
concurrency?: number;
|
|
33
|
+
} = {}
|
|
34
|
+
): Promise<Array<U>> {
|
|
35
|
+
const callId = runJobsCallId;
|
|
36
|
+
runJobsCallId++;
|
|
37
|
+
|
|
38
|
+
if (debug) {
|
|
39
|
+
debug(`runJobs called (callId: ${callId})`, {
|
|
40
|
+
inputs,
|
|
41
|
+
mapper,
|
|
42
|
+
concurrency,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (concurrency < 1) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Concurrency can't be less than one; that doesn't make any sense."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const inputsArray: Array<T> = [];
|
|
53
|
+
const inputIteratorFactory =
|
|
54
|
+
inputs[Symbol.asyncIterator || NOTHING] || inputs[Symbol.iterator];
|
|
55
|
+
const inputIterator = inputIteratorFactory.call(inputs);
|
|
56
|
+
const maybeLength = Array.isArray(inputs) ? inputs.length : null;
|
|
57
|
+
|
|
58
|
+
let iteratorDone = false;
|
|
59
|
+
|
|
60
|
+
async function readInput(): Promise<boolean> {
|
|
61
|
+
if (debug) {
|
|
62
|
+
debug(`reading next input (callId: ${callId})`);
|
|
63
|
+
}
|
|
64
|
+
let nextResult = inputIterator.next();
|
|
65
|
+
if (isThenable(nextResult)) {
|
|
66
|
+
nextResult = await nextResult;
|
|
67
|
+
}
|
|
68
|
+
if (nextResult.done) {
|
|
69
|
+
iteratorDone = true;
|
|
70
|
+
return false;
|
|
71
|
+
} else {
|
|
72
|
+
let value = nextResult.value;
|
|
73
|
+
if (isThenable<T>(value)) {
|
|
74
|
+
value = await value;
|
|
75
|
+
}
|
|
76
|
+
inputsArray.push(value);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let unstartedIndex = 0;
|
|
82
|
+
|
|
83
|
+
const results = new Array(maybeLength || 0);
|
|
84
|
+
const runningPromises = new Set();
|
|
85
|
+
let error: Error | null = null;
|
|
86
|
+
|
|
87
|
+
async function takeInput() {
|
|
88
|
+
const read = await readInput();
|
|
89
|
+
if (!read) return;
|
|
90
|
+
|
|
91
|
+
const inputIndex = unstartedIndex;
|
|
92
|
+
unstartedIndex++;
|
|
93
|
+
|
|
94
|
+
const input = inputsArray[inputIndex];
|
|
95
|
+
if (debug) {
|
|
96
|
+
debug(`mapping input into Promise (callId: ${callId})`);
|
|
97
|
+
}
|
|
98
|
+
const promise = mapper(input, inputIndex, maybeLength || Infinity);
|
|
99
|
+
|
|
100
|
+
if (!isThenable(promise)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"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."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const promiseWithMore = promise.then(
|
|
107
|
+
(result) => {
|
|
108
|
+
if (debug) {
|
|
109
|
+
debug(`child Promise resolved for input (callId: ${callId}):`, input);
|
|
110
|
+
}
|
|
111
|
+
results[inputIndex] = result;
|
|
112
|
+
runningPromises.delete(promiseWithMore);
|
|
113
|
+
},
|
|
114
|
+
(err) => {
|
|
115
|
+
if (debug) {
|
|
116
|
+
debug(
|
|
117
|
+
`child Promise rejected for input (callId: ${callId}):`,
|
|
118
|
+
input,
|
|
119
|
+
"with error:",
|
|
120
|
+
err
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
runningPromises.delete(promiseWithMore);
|
|
124
|
+
error = err;
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
runningPromises.add(promiseWithMore);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function proceed() {
|
|
131
|
+
while (!iteratorDone && runningPromises.size < concurrency) {
|
|
132
|
+
await takeInput();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await proceed();
|
|
137
|
+
while (runningPromises.size > 0 && !error) {
|
|
138
|
+
await Promise.race(runningPromises.values());
|
|
139
|
+
if (error) {
|
|
140
|
+
if (debug) {
|
|
141
|
+
debug(`throwing error (callId: ${callId})`);
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
await proceed();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (error) {
|
|
149
|
+
if (debug) {
|
|
150
|
+
debug(`throwing error (callId: ${callId})`);
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (debug) {
|
|
156
|
+
debug(`all done (callId: ${callId})`);
|
|
157
|
+
}
|
|
158
|
+
return results;
|
|
159
|
+
}
|