@xylabs/threads 3.0.4
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/common.d.ts +4 -0
- package/dist/common.js +18 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +27 -0
- package/dist/master/get-bundle-url.browser.d.ts +3 -0
- package/dist/master/get-bundle-url.browser.js +29 -0
- package/dist/master/implementation.browser.d.ts +4 -0
- package/dist/master/implementation.browser.js +69 -0
- package/dist/master/implementation.d.ts +6 -0
- package/dist/master/implementation.js +41 -0
- package/dist/master/implementation.node.d.ts +5 -0
- package/dist/master/implementation.node.js +255 -0
- package/dist/master/index.d.ts +13 -0
- package/dist/master/index.js +16 -0
- package/dist/master/invocation-proxy.d.ts +3 -0
- package/dist/master/invocation-proxy.js +130 -0
- package/dist/master/pool-types.d.ts +65 -0
- package/dist/master/pool-types.js +15 -0
- package/dist/master/pool.d.ts +90 -0
- package/dist/master/pool.js +281 -0
- package/dist/master/register.d.ts +1 -0
- package/dist/master/register.js +12 -0
- package/dist/master/spawn.d.ts +20 -0
- package/dist/master/spawn.js +130 -0
- package/dist/master/thread.d.ts +12 -0
- package/dist/master/thread.js +22 -0
- package/dist/observable-promise.d.ts +38 -0
- package/dist/observable-promise.js +156 -0
- package/dist/observable.d.ts +19 -0
- package/dist/observable.js +43 -0
- package/dist/ponyfills.d.ts +8 -0
- package/dist/ponyfills.js +22 -0
- package/dist/promise.d.ts +5 -0
- package/dist/promise.js +29 -0
- package/dist/serializers.d.ts +16 -0
- package/dist/serializers.js +41 -0
- package/dist/symbols.d.ts +5 -0
- package/dist/symbols.js +8 -0
- package/dist/transferable.d.ts +42 -0
- package/dist/transferable.js +28 -0
- package/dist/types/master.d.ts +99 -0
- package/dist/types/master.js +14 -0
- package/dist/types/messages.d.ts +62 -0
- package/dist/types/messages.js +20 -0
- package/dist/types/worker.d.ts +11 -0
- package/dist/types/worker.js +2 -0
- package/dist/worker/bundle-entry.d.ts +1 -0
- package/dist/worker/bundle-entry.js +27 -0
- package/dist/worker/implementation.browser.d.ts +7 -0
- package/dist/worker/implementation.browser.js +28 -0
- package/dist/worker/implementation.d.ts +3 -0
- package/dist/worker/implementation.js +24 -0
- package/dist/worker/implementation.tiny-worker.d.ts +7 -0
- package/dist/worker/implementation.tiny-worker.js +38 -0
- package/dist/worker/implementation.worker_threads.d.ts +8 -0
- package/dist/worker/implementation.worker_threads.js +42 -0
- package/dist/worker/index.d.ts +13 -0
- package/dist/worker/index.js +195 -0
- package/dist/worker_threads.d.ts +8 -0
- package/dist/worker_threads.js +17 -0
- package/dist-esm/common.js +12 -0
- package/dist-esm/index.js +6 -0
- package/dist-esm/master/get-bundle-url.browser.js +25 -0
- package/dist-esm/master/implementation.browser.js +64 -0
- package/dist-esm/master/implementation.js +15 -0
- package/dist-esm/master/implementation.node.js +224 -0
- package/dist-esm/master/index.js +9 -0
- package/dist-esm/master/invocation-proxy.js +122 -0
- package/dist-esm/master/pool-types.js +12 -0
- package/dist-esm/master/pool.js +273 -0
- package/dist-esm/master/register.js +10 -0
- package/dist-esm/master/spawn.js +123 -0
- package/dist-esm/master/thread.js +19 -0
- package/dist-esm/observable-promise.js +152 -0
- package/dist-esm/observable.js +38 -0
- package/dist-esm/ponyfills.js +18 -0
- package/dist-esm/promise.js +25 -0
- package/dist-esm/serializers.js +37 -0
- package/dist-esm/symbols.js +5 -0
- package/dist-esm/transferable.js +23 -0
- package/dist-esm/types/master.js +11 -0
- package/dist-esm/types/messages.js +17 -0
- package/dist-esm/types/worker.js +1 -0
- package/dist-esm/worker/bundle-entry.js +11 -0
- package/dist-esm/worker/implementation.browser.js +26 -0
- package/dist-esm/worker/implementation.js +19 -0
- package/dist-esm/worker/implementation.tiny-worker.js +36 -0
- package/dist-esm/worker/implementation.worker_threads.js +37 -0
- package/dist-esm/worker/index.js +186 -0
- package/dist-esm/worker_threads.js +14 -0
- package/index.mjs +11 -0
- package/observable.d.ts +2 -0
- package/observable.js +3 -0
- package/observable.mjs +5 -0
- package/package.json +141 -0
- package/register.d.ts +3 -0
- package/register.js +3 -0
- package/register.mjs +2 -0
- package/rollup.config.js +16 -0
- package/src/common.ts +16 -0
- package/src/index.ts +8 -0
- package/src/master/get-bundle-url.browser.ts +31 -0
- package/src/master/implementation.browser.ts +80 -0
- package/src/master/implementation.node.ts +284 -0
- package/src/master/implementation.ts +21 -0
- package/src/master/index.ts +20 -0
- package/src/master/invocation-proxy.ts +146 -0
- package/src/master/pool-types.ts +83 -0
- package/src/master/pool.ts +391 -0
- package/src/master/register.ts +10 -0
- package/src/master/spawn.ts +172 -0
- package/src/master/thread.ts +26 -0
- package/src/observable-promise.ts +181 -0
- package/src/observable.ts +43 -0
- package/src/ponyfills.ts +31 -0
- package/src/promise.ts +26 -0
- package/src/serializers.ts +67 -0
- package/src/symbols.ts +5 -0
- package/src/transferable.ts +68 -0
- package/src/types/master.ts +130 -0
- package/src/types/messages.ts +81 -0
- package/src/types/worker.ts +14 -0
- package/src/worker/bundle-entry.ts +10 -0
- package/src/worker/implementation.browser.ts +40 -0
- package/src/worker/implementation.tiny-worker.ts +52 -0
- package/src/worker/implementation.ts +23 -0
- package/src/worker/implementation.worker_threads.ts +50 -0
- package/src/worker/index.ts +228 -0
- package/src/worker_threads.ts +28 -0
- package/test/lib/serialization.ts +38 -0
- package/test/observable-promise.test.ts +189 -0
- package/test/observable.test.ts +86 -0
- package/test/pool.test.ts +173 -0
- package/test/serialization.test.ts +21 -0
- package/test/spawn.chromium.mocha.ts +49 -0
- package/test/spawn.test.ts +71 -0
- package/test/streaming.test.ts +27 -0
- package/test/transferables.test.ts +69 -0
- package/test/workers/arraybuffer-xor.ts +11 -0
- package/test/workers/count-to-five.ts +13 -0
- package/test/workers/counter.ts +20 -0
- package/test/workers/faulty-function.ts +6 -0
- package/test/workers/hello-world.ts +6 -0
- package/test/workers/increment.ts +9 -0
- package/test/workers/minmax.ts +25 -0
- package/test/workers/serialization.ts +12 -0
- package/test/workers/top-level-throw.ts +1 -0
- package/test-tooling/rollup/app.js +20 -0
- package/test-tooling/rollup/rollup.config.ts +15 -0
- package/test-tooling/rollup/rollup.test.ts +44 -0
- package/test-tooling/rollup/worker.js +7 -0
- package/test-tooling/tsconfig/minimal-tsconfig.test.ts +7 -0
- package/test-tooling/tsconfig/minimal.ts +10 -0
- package/test-tooling/webpack/addition-worker.ts +10 -0
- package/test-tooling/webpack/app-with-inlined-worker.ts +29 -0
- package/test-tooling/webpack/app.ts +58 -0
- package/test-tooling/webpack/pool-worker.ts +6 -0
- package/test-tooling/webpack/raw-loader.d.ts +4 -0
- package/test-tooling/webpack/webpack.chromium.mocha.ts +21 -0
- package/test-tooling/webpack/webpack.node.config.js +38 -0
- package/test-tooling/webpack/webpack.test.ts +90 -0
- package/test-tooling/webpack/webpack.web.config.js +35 -0
- package/types/is-observable.d.ts +7 -0
- package/types/tiny-worker.d.ts +4 -0
- package/types/webworker.d.ts +9 -0
- package/worker.d.ts +2 -0
- package/worker.js +3 -0
- package/worker.mjs +7 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/*
|
|
3
|
+
* This source file contains the code for proxying calls in the master thread to calls in the workers
|
|
4
|
+
* by `.postMessage()`-ing.
|
|
5
|
+
*
|
|
6
|
+
* Keep in mind that this code can make or break the program's performance! Need to optimize more…
|
|
7
|
+
*/
|
|
8
|
+
import DebugLogger from 'debug';
|
|
9
|
+
import { multicast, Observable } from 'observable-fns';
|
|
10
|
+
import { deserialize, serialize } from '../common';
|
|
11
|
+
import { ObservablePromise } from '../observable-promise';
|
|
12
|
+
import { isTransferDescriptor } from '../transferable';
|
|
13
|
+
import { MasterMessageType, WorkerMessageType, } from '../types/messages';
|
|
14
|
+
const debugMessages = DebugLogger('threads:master:messages');
|
|
15
|
+
let nextJobUID = 1;
|
|
16
|
+
const dedupe = (array) => [...new Set(array)];
|
|
17
|
+
const isJobErrorMessage = (data) => data && data.type === WorkerMessageType.error;
|
|
18
|
+
const isJobResultMessage = (data) => data && data.type === WorkerMessageType.result;
|
|
19
|
+
const isJobStartMessage = (data) => data && data.type === WorkerMessageType.running;
|
|
20
|
+
function createObservableForJob(worker, jobUID) {
|
|
21
|
+
return new Observable((observer) => {
|
|
22
|
+
let asyncType;
|
|
23
|
+
const messageHandler = ((event) => {
|
|
24
|
+
debugMessages('Message from worker:', event.data);
|
|
25
|
+
if (!event.data || event.data.uid !== jobUID)
|
|
26
|
+
return;
|
|
27
|
+
if (isJobStartMessage(event.data)) {
|
|
28
|
+
asyncType = event.data.resultType;
|
|
29
|
+
}
|
|
30
|
+
else if (isJobResultMessage(event.data)) {
|
|
31
|
+
if (asyncType === 'promise') {
|
|
32
|
+
if (event.data.payload !== undefined) {
|
|
33
|
+
observer.next(deserialize(event.data.payload));
|
|
34
|
+
}
|
|
35
|
+
observer.complete();
|
|
36
|
+
worker.removeEventListener('message', messageHandler);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
if (event.data.payload) {
|
|
40
|
+
observer.next(deserialize(event.data.payload));
|
|
41
|
+
}
|
|
42
|
+
if (event.data.complete) {
|
|
43
|
+
observer.complete();
|
|
44
|
+
worker.removeEventListener('message', messageHandler);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (isJobErrorMessage(event.data)) {
|
|
49
|
+
const error = deserialize(event.data.error);
|
|
50
|
+
if (asyncType === 'promise' || !asyncType) {
|
|
51
|
+
observer.error(error);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
observer.error(error);
|
|
55
|
+
}
|
|
56
|
+
worker.removeEventListener('message', messageHandler);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
worker.addEventListener('message', messageHandler);
|
|
60
|
+
return () => {
|
|
61
|
+
if (asyncType === 'observable' || !asyncType) {
|
|
62
|
+
const cancelMessage = {
|
|
63
|
+
type: MasterMessageType.cancel,
|
|
64
|
+
uid: jobUID,
|
|
65
|
+
};
|
|
66
|
+
worker.postMessage(cancelMessage);
|
|
67
|
+
}
|
|
68
|
+
worker.removeEventListener('message', messageHandler);
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function prepareArguments(rawArgs) {
|
|
73
|
+
if (rawArgs.length === 0) {
|
|
74
|
+
// Exit early if possible
|
|
75
|
+
return {
|
|
76
|
+
args: [],
|
|
77
|
+
transferables: [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const args = [];
|
|
81
|
+
const transferables = [];
|
|
82
|
+
for (const arg of rawArgs) {
|
|
83
|
+
if (isTransferDescriptor(arg)) {
|
|
84
|
+
args.push(serialize(arg.send));
|
|
85
|
+
transferables.push(...arg.transferables);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
args.push(serialize(arg));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
args,
|
|
93
|
+
transferables: transferables.length === 0 ? transferables : dedupe(transferables),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function createProxyFunction(worker, method) {
|
|
97
|
+
return ((...rawArgs) => {
|
|
98
|
+
const uid = nextJobUID++;
|
|
99
|
+
const { args, transferables } = prepareArguments(rawArgs);
|
|
100
|
+
const runMessage = {
|
|
101
|
+
args,
|
|
102
|
+
method,
|
|
103
|
+
type: MasterMessageType.run,
|
|
104
|
+
uid,
|
|
105
|
+
};
|
|
106
|
+
debugMessages('Sending command to run function to worker:', runMessage);
|
|
107
|
+
try {
|
|
108
|
+
worker.postMessage(runMessage, transferables);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return ObservablePromise.from(Promise.reject(error));
|
|
112
|
+
}
|
|
113
|
+
return ObservablePromise.from(multicast(createObservableForJob(worker, uid)));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function createProxyModule(worker, methodNames) {
|
|
117
|
+
const proxy = {};
|
|
118
|
+
for (const methodName of methodNames) {
|
|
119
|
+
proxy[methodName] = createProxyFunction(worker, methodName);
|
|
120
|
+
}
|
|
121
|
+
return proxy;
|
|
122
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Pool event type. Specifies the type of each `PoolEvent`. */
|
|
2
|
+
export var PoolEventType;
|
|
3
|
+
(function (PoolEventType) {
|
|
4
|
+
PoolEventType["initialized"] = "initialized";
|
|
5
|
+
PoolEventType["taskCanceled"] = "taskCanceled";
|
|
6
|
+
PoolEventType["taskCompleted"] = "taskCompleted";
|
|
7
|
+
PoolEventType["taskFailed"] = "taskFailed";
|
|
8
|
+
PoolEventType["taskQueued"] = "taskQueued";
|
|
9
|
+
PoolEventType["taskQueueDrained"] = "taskQueueDrained";
|
|
10
|
+
PoolEventType["taskStart"] = "taskStart";
|
|
11
|
+
PoolEventType["terminated"] = "terminated";
|
|
12
|
+
})(PoolEventType || (PoolEventType = {}));
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/* eslint-disable unicorn/no-thenable */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
3
|
+
/* eslint-disable require-await */
|
|
4
|
+
/* eslint-disable @typescript-eslint/member-ordering */
|
|
5
|
+
/* eslint-disable unicorn/no-array-reduce */
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
7
|
+
/* eslint-disable import/export */
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-namespace */
|
|
9
|
+
import DebugLogger from 'debug';
|
|
10
|
+
import { multicast, Observable, Subject } from 'observable-fns';
|
|
11
|
+
import { allSettled } from '../ponyfills';
|
|
12
|
+
import { defaultPoolSize } from './implementation';
|
|
13
|
+
import { PoolEventType } from './pool-types';
|
|
14
|
+
import { Thread } from './thread';
|
|
15
|
+
let nextPoolID = 1;
|
|
16
|
+
function createArray(size) {
|
|
17
|
+
const array = [];
|
|
18
|
+
for (let index = 0; index < size; index++) {
|
|
19
|
+
array.push(index);
|
|
20
|
+
}
|
|
21
|
+
return array;
|
|
22
|
+
}
|
|
23
|
+
function delay(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
function flatMap(array, mapper) {
|
|
27
|
+
return array.reduce((flattened, element) => [...flattened, ...mapper(element)], []);
|
|
28
|
+
}
|
|
29
|
+
function slugify(text) {
|
|
30
|
+
return text.replaceAll(/\W/g, ' ').trim().replaceAll(/\s+/g, '-');
|
|
31
|
+
}
|
|
32
|
+
function spawnWorkers(spawnWorker, count) {
|
|
33
|
+
return createArray(count).map(() => ({
|
|
34
|
+
init: spawnWorker(),
|
|
35
|
+
runningTasks: [],
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
class WorkerPool {
|
|
39
|
+
static EventType = PoolEventType;
|
|
40
|
+
debug;
|
|
41
|
+
eventObservable;
|
|
42
|
+
options;
|
|
43
|
+
workers;
|
|
44
|
+
eventSubject = new Subject();
|
|
45
|
+
initErrors = [];
|
|
46
|
+
isClosing = false;
|
|
47
|
+
nextTaskID = 1;
|
|
48
|
+
taskQueue = [];
|
|
49
|
+
constructor(spawnWorker, optionsOrSize) {
|
|
50
|
+
const options = typeof optionsOrSize === 'number' ? { size: optionsOrSize } : optionsOrSize || {};
|
|
51
|
+
const { size = defaultPoolSize } = options;
|
|
52
|
+
this.debug = DebugLogger(`threads:pool:${slugify(options.name || String(nextPoolID++))}`);
|
|
53
|
+
this.options = options;
|
|
54
|
+
this.workers = spawnWorkers(spawnWorker, size);
|
|
55
|
+
this.eventObservable = multicast(Observable.from(this.eventSubject));
|
|
56
|
+
Promise.all(this.workers.map((worker) => worker.init)).then(() => this.eventSubject.next({
|
|
57
|
+
size: this.workers.length,
|
|
58
|
+
type: PoolEventType.initialized,
|
|
59
|
+
}), (error) => {
|
|
60
|
+
this.debug('Error while initializing pool worker:', error);
|
|
61
|
+
this.eventSubject.error(error);
|
|
62
|
+
this.initErrors.push(error);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
findIdlingWorker() {
|
|
66
|
+
const { concurrency = 1 } = this.options;
|
|
67
|
+
return this.workers.find((worker) => worker.runningTasks.length < concurrency);
|
|
68
|
+
}
|
|
69
|
+
async runPoolTask(worker, task) {
|
|
70
|
+
const workerID = this.workers.indexOf(worker) + 1;
|
|
71
|
+
this.debug(`Running task #${task.id} on worker #${workerID}...`);
|
|
72
|
+
this.eventSubject.next({
|
|
73
|
+
taskID: task.id,
|
|
74
|
+
type: PoolEventType.taskStart,
|
|
75
|
+
workerID,
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const returnValue = await task.run(await worker.init);
|
|
79
|
+
this.debug(`Task #${task.id} completed successfully`);
|
|
80
|
+
this.eventSubject.next({
|
|
81
|
+
returnValue,
|
|
82
|
+
taskID: task.id,
|
|
83
|
+
type: PoolEventType.taskCompleted,
|
|
84
|
+
workerID,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.debug(`Task #${task.id} failed`);
|
|
89
|
+
this.eventSubject.next({
|
|
90
|
+
error,
|
|
91
|
+
taskID: task.id,
|
|
92
|
+
type: PoolEventType.taskFailed,
|
|
93
|
+
workerID,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async run(worker, task) {
|
|
98
|
+
const runPromise = (async () => {
|
|
99
|
+
const removeTaskFromWorkersRunningTasks = () => {
|
|
100
|
+
worker.runningTasks = worker.runningTasks.filter((someRunPromise) => someRunPromise !== runPromise);
|
|
101
|
+
};
|
|
102
|
+
// Defer task execution by one tick to give handlers time to subscribe
|
|
103
|
+
await delay(0);
|
|
104
|
+
try {
|
|
105
|
+
await this.runPoolTask(worker, task);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
removeTaskFromWorkersRunningTasks();
|
|
109
|
+
if (!this.isClosing) {
|
|
110
|
+
this.scheduleWork();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
worker.runningTasks.push(runPromise);
|
|
115
|
+
}
|
|
116
|
+
scheduleWork() {
|
|
117
|
+
this.debug('Attempt de-queueing a task in order to run it...');
|
|
118
|
+
const availableWorker = this.findIdlingWorker();
|
|
119
|
+
if (!availableWorker)
|
|
120
|
+
return;
|
|
121
|
+
const nextTask = this.taskQueue.shift();
|
|
122
|
+
if (!nextTask) {
|
|
123
|
+
this.debug('Task queue is empty');
|
|
124
|
+
this.eventSubject.next({ type: PoolEventType.taskQueueDrained });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.run(availableWorker, nextTask);
|
|
128
|
+
}
|
|
129
|
+
taskCompletion(taskID) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const eventSubscription = this.events().subscribe((event) => {
|
|
132
|
+
if (event.type === PoolEventType.taskCompleted && event.taskID === taskID) {
|
|
133
|
+
eventSubscription.unsubscribe();
|
|
134
|
+
resolve(event.returnValue);
|
|
135
|
+
}
|
|
136
|
+
else if (event.type === PoolEventType.taskFailed && event.taskID === taskID) {
|
|
137
|
+
eventSubscription.unsubscribe();
|
|
138
|
+
reject(event.error);
|
|
139
|
+
}
|
|
140
|
+
else if (event.type === PoolEventType.terminated) {
|
|
141
|
+
eventSubscription.unsubscribe();
|
|
142
|
+
reject(Error('Pool has been terminated before task was run.'));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async settled(allowResolvingImmediately = false) {
|
|
148
|
+
const getCurrentlyRunningTasks = () => flatMap(this.workers, (worker) => worker.runningTasks);
|
|
149
|
+
const taskFailures = [];
|
|
150
|
+
const failureSubscription = this.eventObservable.subscribe((event) => {
|
|
151
|
+
if (event.type === PoolEventType.taskFailed) {
|
|
152
|
+
taskFailures.push(event.error);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
if (this.initErrors.length > 0) {
|
|
156
|
+
throw this.initErrors[0];
|
|
157
|
+
}
|
|
158
|
+
if (allowResolvingImmediately && this.taskQueue.length === 0) {
|
|
159
|
+
await allSettled(getCurrentlyRunningTasks());
|
|
160
|
+
return taskFailures;
|
|
161
|
+
}
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
const subscription = this.eventObservable.subscribe({
|
|
164
|
+
error: reject,
|
|
165
|
+
next(event) {
|
|
166
|
+
if (event.type === PoolEventType.taskQueueDrained) {
|
|
167
|
+
subscription.unsubscribe();
|
|
168
|
+
resolve(void 0);
|
|
169
|
+
}
|
|
170
|
+
}, // make a pool-wide error reject the completed() result promise
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
await allSettled(getCurrentlyRunningTasks());
|
|
174
|
+
failureSubscription.unsubscribe();
|
|
175
|
+
return taskFailures;
|
|
176
|
+
}
|
|
177
|
+
async completed(allowResolvingImmediately = false) {
|
|
178
|
+
const settlementPromise = this.settled(allowResolvingImmediately);
|
|
179
|
+
const earlyExitPromise = new Promise((resolve, reject) => {
|
|
180
|
+
const subscription = this.eventObservable.subscribe({
|
|
181
|
+
error: reject,
|
|
182
|
+
next(event) {
|
|
183
|
+
if (event.type === PoolEventType.taskQueueDrained) {
|
|
184
|
+
subscription.unsubscribe();
|
|
185
|
+
resolve(settlementPromise);
|
|
186
|
+
}
|
|
187
|
+
else if (event.type === PoolEventType.taskFailed) {
|
|
188
|
+
subscription.unsubscribe();
|
|
189
|
+
reject(event.error);
|
|
190
|
+
}
|
|
191
|
+
}, // make a pool-wide error reject the completed() result promise
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
const errors = await Promise.race([settlementPromise, earlyExitPromise]);
|
|
195
|
+
if (errors.length > 0) {
|
|
196
|
+
throw errors[0];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
events() {
|
|
200
|
+
return this.eventObservable;
|
|
201
|
+
}
|
|
202
|
+
queue(taskFunction) {
|
|
203
|
+
const { maxQueuedJobs = Number.POSITIVE_INFINITY } = this.options;
|
|
204
|
+
if (this.isClosing) {
|
|
205
|
+
throw new Error('Cannot schedule pool tasks after terminate() has been called.');
|
|
206
|
+
}
|
|
207
|
+
if (this.initErrors.length > 0) {
|
|
208
|
+
throw this.initErrors[0];
|
|
209
|
+
}
|
|
210
|
+
const taskID = this.nextTaskID++;
|
|
211
|
+
const taskCompletion = this.taskCompletion(taskID);
|
|
212
|
+
taskCompletion.catch((error) => {
|
|
213
|
+
// Prevent unhandled rejections here as we assume the user will use
|
|
214
|
+
// `pool.completed()`, `pool.settled()` or `task.catch()` to handle errors
|
|
215
|
+
this.debug(`Task #${taskID} errored:`, error);
|
|
216
|
+
});
|
|
217
|
+
const task = {
|
|
218
|
+
cancel: () => {
|
|
219
|
+
if (!this.taskQueue.includes(task))
|
|
220
|
+
return;
|
|
221
|
+
this.taskQueue = this.taskQueue.filter((someTask) => someTask !== task);
|
|
222
|
+
this.eventSubject.next({
|
|
223
|
+
taskID: task.id,
|
|
224
|
+
type: PoolEventType.taskCanceled,
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
id: taskID,
|
|
228
|
+
run: taskFunction,
|
|
229
|
+
then: taskCompletion.then.bind(taskCompletion),
|
|
230
|
+
};
|
|
231
|
+
if (this.taskQueue.length >= maxQueuedJobs) {
|
|
232
|
+
throw new Error('Maximum number of pool tasks queued. Refusing to queue another one.\n' +
|
|
233
|
+
'This usually happens for one of two reasons: We are either at peak ' +
|
|
234
|
+
"workload right now or some tasks just won't finish, thus blocking the pool.");
|
|
235
|
+
}
|
|
236
|
+
this.debug(`Queueing task #${task.id}...`);
|
|
237
|
+
this.taskQueue.push(task);
|
|
238
|
+
this.eventSubject.next({
|
|
239
|
+
taskID: task.id,
|
|
240
|
+
type: PoolEventType.taskQueued,
|
|
241
|
+
});
|
|
242
|
+
this.scheduleWork();
|
|
243
|
+
return task;
|
|
244
|
+
}
|
|
245
|
+
async terminate(force) {
|
|
246
|
+
this.isClosing = true;
|
|
247
|
+
if (!force) {
|
|
248
|
+
await this.completed(true);
|
|
249
|
+
}
|
|
250
|
+
this.eventSubject.next({
|
|
251
|
+
remainingQueue: [...this.taskQueue],
|
|
252
|
+
type: PoolEventType.terminated,
|
|
253
|
+
});
|
|
254
|
+
this.eventSubject.complete();
|
|
255
|
+
await Promise.all(this.workers.map(async (worker) => Thread.terminate(await worker.init)));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
260
|
+
*/
|
|
261
|
+
function PoolConstructor(spawnWorker, optionsOrSize) {
|
|
262
|
+
// The function exists only so we don't need to use `new` to create a pool (we still can, though).
|
|
263
|
+
// If the Pool is a class or not is an implementation detail that should not concern the user.
|
|
264
|
+
return new WorkerPool(spawnWorker, optionsOrSize);
|
|
265
|
+
}
|
|
266
|
+
;
|
|
267
|
+
PoolConstructor.EventType = PoolEventType;
|
|
268
|
+
/**
|
|
269
|
+
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
270
|
+
*/
|
|
271
|
+
export const Pool = PoolConstructor;
|
|
272
|
+
export { PoolEventType } from './pool-types';
|
|
273
|
+
export { Thread } from './thread';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { Worker as WorkerImplementation } from './index';
|
|
3
|
+
if (typeof global !== 'undefined') {
|
|
4
|
+
;
|
|
5
|
+
global.Worker = WorkerImplementation;
|
|
6
|
+
}
|
|
7
|
+
else if (window !== undefined) {
|
|
8
|
+
;
|
|
9
|
+
window.Worker = WorkerImplementation;
|
|
10
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import DebugLogger from 'debug';
|
|
4
|
+
import { Observable } from 'observable-fns';
|
|
5
|
+
import { deserialize } from '../common';
|
|
6
|
+
import { createPromiseWithResolver } from '../promise';
|
|
7
|
+
import { $errors, $events, $terminate, $worker } from '../symbols';
|
|
8
|
+
import { WorkerEventType, } from '../types/master';
|
|
9
|
+
import { createProxyFunction, createProxyModule } from './invocation-proxy';
|
|
10
|
+
const debugMessages = DebugLogger('threads:master:messages');
|
|
11
|
+
const debugSpawn = DebugLogger('threads:master:spawn');
|
|
12
|
+
const debugThreadUtils = DebugLogger('threads:master:thread-utils');
|
|
13
|
+
const isInitMessage = (data) => data && data.type === 'init';
|
|
14
|
+
const isUncaughtErrorMessage = (data) => data && data.type === 'uncaughtError';
|
|
15
|
+
const initMessageTimeout = typeof process !== 'undefined' && process.env !== undefined && process.env.THREADS_WORKER_INIT_TIMEOUT ?
|
|
16
|
+
Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10)
|
|
17
|
+
: 10000;
|
|
18
|
+
async function withTimeout(promise, timeoutInMs, errorMessage) {
|
|
19
|
+
let timeoutHandle;
|
|
20
|
+
const timeout = new Promise((resolve, reject) => {
|
|
21
|
+
timeoutHandle = setTimeout(() => reject(Error(errorMessage)), timeoutInMs);
|
|
22
|
+
});
|
|
23
|
+
const result = await Promise.race([promise, timeout]);
|
|
24
|
+
clearTimeout(timeoutHandle);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function receiveInitMessage(worker) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const messageHandler = ((event) => {
|
|
30
|
+
debugMessages('Message from worker before finishing initialization:', event.data);
|
|
31
|
+
if (isInitMessage(event.data)) {
|
|
32
|
+
worker.removeEventListener('message', messageHandler);
|
|
33
|
+
resolve(event.data);
|
|
34
|
+
}
|
|
35
|
+
else if (isUncaughtErrorMessage(event.data)) {
|
|
36
|
+
worker.removeEventListener('message', messageHandler);
|
|
37
|
+
reject(deserialize(event.data.error));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
worker.addEventListener('message', messageHandler);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function createEventObservable(worker, workerTermination) {
|
|
44
|
+
return new Observable((observer) => {
|
|
45
|
+
const messageHandler = ((messageEvent) => {
|
|
46
|
+
const workerEvent = {
|
|
47
|
+
data: messageEvent.data,
|
|
48
|
+
type: WorkerEventType.message,
|
|
49
|
+
};
|
|
50
|
+
observer.next(workerEvent);
|
|
51
|
+
});
|
|
52
|
+
const rejectionHandler = ((errorEvent) => {
|
|
53
|
+
debugThreadUtils('Unhandled promise rejection event in thread:', errorEvent);
|
|
54
|
+
const workerEvent = {
|
|
55
|
+
error: Error(errorEvent.reason),
|
|
56
|
+
type: WorkerEventType.internalError,
|
|
57
|
+
};
|
|
58
|
+
observer.next(workerEvent);
|
|
59
|
+
});
|
|
60
|
+
worker.addEventListener('message', messageHandler);
|
|
61
|
+
worker.addEventListener('unhandledrejection', rejectionHandler);
|
|
62
|
+
workerTermination.then(() => {
|
|
63
|
+
const terminationEvent = {
|
|
64
|
+
type: WorkerEventType.termination,
|
|
65
|
+
};
|
|
66
|
+
worker.removeEventListener('message', messageHandler);
|
|
67
|
+
worker.removeEventListener('unhandledrejection', rejectionHandler);
|
|
68
|
+
observer.next(terminationEvent);
|
|
69
|
+
observer.complete();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function createTerminator(worker) {
|
|
74
|
+
const [termination, resolver] = createPromiseWithResolver();
|
|
75
|
+
const terminate = async () => {
|
|
76
|
+
debugThreadUtils('Terminating worker');
|
|
77
|
+
// Newer versions of worker_threads workers return a promise
|
|
78
|
+
await worker.terminate();
|
|
79
|
+
resolver();
|
|
80
|
+
};
|
|
81
|
+
return { terminate, termination };
|
|
82
|
+
}
|
|
83
|
+
function setPrivateThreadProps(raw, worker, workerEvents, terminate) {
|
|
84
|
+
const workerErrors = workerEvents
|
|
85
|
+
.filter((event) => event.type === WorkerEventType.internalError)
|
|
86
|
+
.map((errorEvent) => errorEvent.error);
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
return Object.assign(raw, {
|
|
89
|
+
[$errors]: workerErrors,
|
|
90
|
+
[$events]: workerEvents,
|
|
91
|
+
[$terminate]: terminate,
|
|
92
|
+
[$worker]: worker,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Spawn a new thread. Takes a fresh worker instance, wraps it in a thin
|
|
97
|
+
* abstraction layer to provide the transparent API and verifies that
|
|
98
|
+
* the worker has initialized successfully.
|
|
99
|
+
*
|
|
100
|
+
* @param worker Instance of `Worker`. Either a web worker, `worker_threads` worker or `tiny-worker` worker.
|
|
101
|
+
* @param [options]
|
|
102
|
+
* @param [options.timeout] Init message timeout. Default: 10000 or set by environment variable.
|
|
103
|
+
*/
|
|
104
|
+
export async function spawn(worker, options) {
|
|
105
|
+
debugSpawn('Initializing new thread');
|
|
106
|
+
const timeout = options && options.timeout ? options.timeout : initMessageTimeout;
|
|
107
|
+
const initMessage = await withTimeout(receiveInitMessage(worker), timeout, `Timeout: Did not receive an init message from worker after ${timeout}ms. Make sure the worker calls expose().`);
|
|
108
|
+
const exposed = initMessage.exposed;
|
|
109
|
+
const { termination, terminate } = createTerminator(worker);
|
|
110
|
+
const events = createEventObservable(worker, termination);
|
|
111
|
+
if (exposed.type === 'function') {
|
|
112
|
+
const proxy = createProxyFunction(worker);
|
|
113
|
+
return setPrivateThreadProps(proxy, worker, events, terminate);
|
|
114
|
+
}
|
|
115
|
+
else if (exposed.type === 'module') {
|
|
116
|
+
const proxy = createProxyModule(worker, exposed.methods);
|
|
117
|
+
return setPrivateThreadProps(proxy, worker, events, terminate);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const type = exposed.type;
|
|
121
|
+
throw new Error(`Worker init message states unexpected type of expose(): ${type}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { $errors, $events, $terminate } from '../symbols';
|
|
2
|
+
function fail(message) {
|
|
3
|
+
throw new Error(message);
|
|
4
|
+
}
|
|
5
|
+
/** Thread utility functions. Use them to manage or inspect a `spawn()`-ed thread. */
|
|
6
|
+
export const Thread = {
|
|
7
|
+
/** Return an observable that can be used to subscribe to all errors happening in the thread. */
|
|
8
|
+
errors(thread) {
|
|
9
|
+
return thread[$errors] || fail('Error observable not found. Make sure to pass a thread instance as returned by the spawn() promise.');
|
|
10
|
+
},
|
|
11
|
+
/** Return an observable that can be used to subscribe to internal events happening in the thread. Useful for debugging. */
|
|
12
|
+
events(thread) {
|
|
13
|
+
return thread[$events] || fail('Events observable not found. Make sure to pass a thread instance as returned by the spawn() promise.');
|
|
14
|
+
},
|
|
15
|
+
/** Terminate a thread. Remember to terminate every thread when you are done using it. */
|
|
16
|
+
terminate(thread) {
|
|
17
|
+
return thread[$terminate]();
|
|
18
|
+
},
|
|
19
|
+
};
|