threadsx 2.0.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 +164 -0
- package/dist/common.d.ts +4 -0
- package/dist/common.js +16 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +26 -0
- package/dist/master/get-bundle-url.browser.d.ts +3 -0
- package/dist/master/get-bundle-url.browser.js +28 -0
- package/dist/master/implementation.browser.d.ts +4 -0
- package/dist/master/implementation.browser.js +72 -0
- package/dist/master/implementation.d.ts +6 -0
- package/dist/master/implementation.js +51 -0
- package/dist/master/implementation.node.d.ts +4 -0
- package/dist/master/implementation.node.js +196 -0
- package/dist/master/index.d.ts +14 -0
- package/dist/master/index.js +15 -0
- package/dist/master/invocation-proxy.d.ts +3 -0
- package/dist/master/invocation-proxy.js +128 -0
- package/dist/master/pool-types.d.ts +65 -0
- package/dist/master/pool-types.js +15 -0
- package/dist/master/pool.d.ts +89 -0
- package/dist/master/pool.js +290 -0
- package/dist/master/register.d.ts +1 -0
- package/dist/master/register.js +9 -0
- package/dist/master/spawn.d.ts +20 -0
- package/dist/master/spawn.js +159 -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 +150 -0
- package/dist/observable.d.ts +19 -0
- package/dist/observable.js +40 -0
- package/dist/ponyfills.d.ts +8 -0
- package/dist/ponyfills.js +21 -0
- package/dist/promise.d.ts +5 -0
- package/dist/promise.js +27 -0
- package/dist/serializers.d.ts +16 -0
- package/dist/serializers.js +51 -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 +26 -0
- package/dist/types/master.d.ts +98 -0
- package/dist/types/master.js +12 -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 +24 -0
- package/dist/worker/implementation.browser.d.ts +6 -0
- package/dist/worker/implementation.browser.js +25 -0
- package/dist/worker/implementation.d.ts +3 -0
- package/dist/worker/implementation.js +18 -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 +224 -0
- package/dist/worker_threads.d.ts +8 -0
- package/dist/worker_threads.js +16 -0
- package/dist-esm/common.js +11 -0
- package/dist-esm/index.js +5 -0
- package/dist-esm/master/get-bundle-url.browser.js +25 -0
- package/dist-esm/master/implementation.browser.js +67 -0
- package/dist-esm/master/implementation.js +15 -0
- package/dist-esm/master/implementation.node.js +155 -0
- package/dist-esm/master/index.js +9 -0
- package/dist-esm/master/invocation-proxy.js +121 -0
- package/dist-esm/master/pool-types.js +12 -0
- package/dist-esm/master/pool.js +283 -0
- package/dist-esm/master/register.js +7 -0
- package/dist-esm/master/spawn.js +153 -0
- package/dist-esm/master/thread.js +19 -0
- package/dist-esm/observable-promise.js +146 -0
- package/dist-esm/observable.js +36 -0
- package/dist-esm/ponyfills.js +18 -0
- package/dist-esm/promise.js +24 -0
- package/dist-esm/serializers.js +47 -0
- package/dist-esm/symbols.js +5 -0
- package/dist-esm/transferable.js +22 -0
- package/dist-esm/types/master.js +9 -0
- package/dist-esm/types/messages.js +17 -0
- package/dist-esm/types/worker.js +1 -0
- package/dist-esm/worker/bundle-entry.js +8 -0
- package/dist-esm/worker/implementation.browser.js +23 -0
- package/dist-esm/worker/implementation.js +13 -0
- package/dist-esm/worker/implementation.worker_threads.js +37 -0
- package/dist-esm/worker/index.js +215 -0
- package/dist-esm/worker_threads.js +13 -0
- package/index.mjs +4 -0
- package/observable.mjs +1 -0
- package/package.json +168 -0
- package/register.mjs +2 -0
- package/worker.mjs +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getBundleURL } from "./get-bundle-url.browser";
|
|
2
|
+
export const defaultPoolSize = typeof navigator !== "undefined" && navigator.hardwareConcurrency
|
|
3
|
+
? navigator.hardwareConcurrency
|
|
4
|
+
: 4;
|
|
5
|
+
const isAbsoluteURL = (value) => /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value);
|
|
6
|
+
function createSourceBlobURL(code) {
|
|
7
|
+
const blob = new Blob([code], { type: "application/javascript" });
|
|
8
|
+
return URL.createObjectURL(blob);
|
|
9
|
+
}
|
|
10
|
+
function selectWorkerImplementation() {
|
|
11
|
+
if (typeof Worker === "undefined") {
|
|
12
|
+
// Might happen on Safari, for instance
|
|
13
|
+
// The idea is to only fail if the constructor is actually used
|
|
14
|
+
return class NoWebWorker {
|
|
15
|
+
constructor() {
|
|
16
|
+
throw Error("No web worker implementation available. You might have tried to spawn a worker within a worker in a browser that doesn't support workers in workers.");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
class WebWorker extends Worker {
|
|
21
|
+
constructor(url, options) {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
if (typeof url === "string" && options && options._baseURL) {
|
|
24
|
+
url = new URL(url, options._baseURL);
|
|
25
|
+
}
|
|
26
|
+
else if (typeof url === "string" && !isAbsoluteURL(url) && getBundleURL().match(/^file:\/\//i)) {
|
|
27
|
+
url = new URL(url, getBundleURL().replace(/\/[^/]+$/, "/"));
|
|
28
|
+
if ((_a = options === null || options === void 0 ? void 0 : options.CORSWorkaround) !== null && _a !== void 0 ? _a : true) {
|
|
29
|
+
url = createSourceBlobURL(`importScripts(${JSON.stringify(url)});`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (typeof url === "string" && isAbsoluteURL(url)) {
|
|
33
|
+
// Create source code blob loading JS file via `importScripts()`
|
|
34
|
+
// to circumvent worker CORS restrictions
|
|
35
|
+
if ((_b = options === null || options === void 0 ? void 0 : options.CORSWorkaround) !== null && _b !== void 0 ? _b : true) {
|
|
36
|
+
url = createSourceBlobURL(`importScripts(${JSON.stringify(url)});`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
super(url, options);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
class BlobWorker extends WebWorker {
|
|
43
|
+
constructor(blob, options) {
|
|
44
|
+
const url = window.URL.createObjectURL(blob);
|
|
45
|
+
super(url, options);
|
|
46
|
+
}
|
|
47
|
+
static fromText(source, options) {
|
|
48
|
+
const blob = new window.Blob([source], { type: "text/javascript" });
|
|
49
|
+
return new BlobWorker(blob, options);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
blob: BlobWorker,
|
|
54
|
+
default: WebWorker
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
let implementation;
|
|
58
|
+
export function getWorkerImplementation() {
|
|
59
|
+
if (!implementation) {
|
|
60
|
+
implementation = selectWorkerImplementation();
|
|
61
|
+
}
|
|
62
|
+
return implementation;
|
|
63
|
+
}
|
|
64
|
+
export function isWorkerRuntime() {
|
|
65
|
+
const isWindowContext = typeof self !== "undefined" && typeof Window !== "undefined" && self instanceof Window;
|
|
66
|
+
return typeof self !== "undefined" && typeof self.postMessage === "function" && !isWindowContext;
|
|
67
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is only a stub to make './implementation' resolve to the right module.
|
|
3
|
+
*/
|
|
4
|
+
// We alias `src/master/implementation` to `src/master/implementation.browser` for web
|
|
5
|
+
// browsers already in the package.json, so if get here, it's safe to pass-through the
|
|
6
|
+
// node implementation
|
|
7
|
+
import * as BrowserImplementation from "./implementation.browser";
|
|
8
|
+
import * as NodeImplementation from "./implementation.node";
|
|
9
|
+
const runningInNode = typeof process !== 'undefined' && process.arch !== 'browser' && 'pid' in process;
|
|
10
|
+
const implementation = runningInNode ? NodeImplementation : BrowserImplementation;
|
|
11
|
+
/** Default size of pools. Depending on the platform the value might vary from device to device. */
|
|
12
|
+
export const defaultPoolSize = implementation.defaultPoolSize;
|
|
13
|
+
export const getWorkerImplementation = implementation.getWorkerImplementation;
|
|
14
|
+
/** Returns `true` if this code is currently running in a worker. */
|
|
15
|
+
export const isWorkerRuntime = implementation.isWorkerRuntime;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import getCallsites from "callsites";
|
|
3
|
+
import { cpus } from 'os';
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
let detectedTsRuntime;
|
|
7
|
+
export const defaultPoolSize = cpus().length;
|
|
8
|
+
/**
|
|
9
|
+
* Detects an available TypeScript runtime so `.ts`/`.tsx` worker files can be
|
|
10
|
+
* spawned directly during development. Prefers `tsx`, falls back to `ts-node`.
|
|
11
|
+
*/
|
|
12
|
+
function detectTsRuntime() {
|
|
13
|
+
if (typeof __non_webpack_require__ === "function") {
|
|
14
|
+
// Webpack build: => No TS runtime required or possible
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (detectedTsRuntime !== undefined) {
|
|
18
|
+
return detectedTsRuntime;
|
|
19
|
+
}
|
|
20
|
+
detectedTsRuntime = null;
|
|
21
|
+
for (const candidate of ["tsx", "ts-node"]) {
|
|
22
|
+
try {
|
|
23
|
+
eval("require").resolve(candidate);
|
|
24
|
+
detectedTsRuntime = candidate;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error && error.code === "MODULE_NOT_FOUND") {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// Re-throw
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return detectedTsRuntime;
|
|
36
|
+
}
|
|
37
|
+
function createTsRuntimeModule(scriptPath, runtime) {
|
|
38
|
+
const register = runtime === "tsx"
|
|
39
|
+
? `require("tsx/cjs");`
|
|
40
|
+
: `require("ts-node/register/transpile-only");`;
|
|
41
|
+
const content = `
|
|
42
|
+
${register}
|
|
43
|
+
require(${JSON.stringify(scriptPath)});
|
|
44
|
+
`;
|
|
45
|
+
return content;
|
|
46
|
+
}
|
|
47
|
+
function rebaseScriptPath(scriptPath, ignoreRegex) {
|
|
48
|
+
const parentCallSite = getCallsites().find((callsite) => {
|
|
49
|
+
const filename = callsite.getFileName();
|
|
50
|
+
return Boolean(filename &&
|
|
51
|
+
!filename.match(ignoreRegex) &&
|
|
52
|
+
!filename.match(/[/\\]master[/\\]implementation/) &&
|
|
53
|
+
!filename.match(/^internal\/process/));
|
|
54
|
+
});
|
|
55
|
+
const rawCallerPath = parentCallSite ? parentCallSite.getFileName() : null;
|
|
56
|
+
let callerPath = rawCallerPath ? rawCallerPath : null;
|
|
57
|
+
if (callerPath && callerPath.startsWith('file:')) {
|
|
58
|
+
callerPath = fileURLToPath(callerPath);
|
|
59
|
+
}
|
|
60
|
+
const rebasedScriptPath = callerPath ? path.join(path.dirname(callerPath), scriptPath) : scriptPath;
|
|
61
|
+
return rebasedScriptPath;
|
|
62
|
+
}
|
|
63
|
+
function resolveScriptPath(scriptPath, baseURL) {
|
|
64
|
+
const makeRelative = (filePath) => {
|
|
65
|
+
// eval() hack is also webpack-related
|
|
66
|
+
return path.isAbsolute(filePath) ? filePath : path.join(baseURL || eval("__dirname"), filePath);
|
|
67
|
+
};
|
|
68
|
+
const workerFilePath = typeof __non_webpack_require__ === "function"
|
|
69
|
+
? __non_webpack_require__.resolve(makeRelative(scriptPath))
|
|
70
|
+
: eval("require").resolve(makeRelative(rebaseScriptPath(scriptPath, /[/\\]worker_threads[/\\]/)));
|
|
71
|
+
return workerFilePath;
|
|
72
|
+
}
|
|
73
|
+
function initWorkerThreadsWorker() {
|
|
74
|
+
// Webpack hack
|
|
75
|
+
const NativeWorker = typeof __non_webpack_require__ === "function"
|
|
76
|
+
? __non_webpack_require__("worker_threads").Worker
|
|
77
|
+
: eval("require")("worker_threads").Worker;
|
|
78
|
+
let allWorkers = [];
|
|
79
|
+
class Worker extends NativeWorker {
|
|
80
|
+
constructor(scriptPath, options) {
|
|
81
|
+
// Bundlers like webpack 5 pass a `URL` (from `new Worker(new URL(...))`)
|
|
82
|
+
// pointing at the emitted worker chunk.
|
|
83
|
+
const normalizedScriptPath = scriptPath instanceof URL ? fileURLToPath(scriptPath) : scriptPath;
|
|
84
|
+
const resolvedScriptPath = options && options.fromSource
|
|
85
|
+
? null
|
|
86
|
+
: resolveScriptPath(normalizedScriptPath, (options || {})._baseURL);
|
|
87
|
+
const tsRuntime = resolvedScriptPath && /\.tsx?$/i.test(resolvedScriptPath) ? detectTsRuntime() : null;
|
|
88
|
+
if (!resolvedScriptPath) {
|
|
89
|
+
// `options.fromSource` is true
|
|
90
|
+
const sourceCode = scriptPath;
|
|
91
|
+
super(sourceCode, Object.assign(Object.assign({}, options), { eval: true }));
|
|
92
|
+
}
|
|
93
|
+
else if (tsRuntime) {
|
|
94
|
+
super(createTsRuntimeModule(resolvedScriptPath, tsRuntime), Object.assign(Object.assign({}, options), { eval: true }));
|
|
95
|
+
}
|
|
96
|
+
else if (resolvedScriptPath.match(/\.asar[/\\]/)) {
|
|
97
|
+
// See <https://github.com/andywer/threads-plugin/issues/17>
|
|
98
|
+
super(resolvedScriptPath.replace(/\.asar([/\\])/, ".asar.unpacked$1"), options);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
super(resolvedScriptPath, options);
|
|
102
|
+
}
|
|
103
|
+
this.mappedEventListeners = new WeakMap();
|
|
104
|
+
allWorkers.push(this);
|
|
105
|
+
}
|
|
106
|
+
addEventListener(eventName, rawListener) {
|
|
107
|
+
const listener = (message) => {
|
|
108
|
+
rawListener({ data: message });
|
|
109
|
+
};
|
|
110
|
+
this.mappedEventListeners.set(rawListener, listener);
|
|
111
|
+
this.on(eventName, listener);
|
|
112
|
+
}
|
|
113
|
+
removeEventListener(eventName, rawListener) {
|
|
114
|
+
const listener = this.mappedEventListeners.get(rawListener) || rawListener;
|
|
115
|
+
this.off(eventName, listener);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const terminateWorkersAndMaster = () => {
|
|
119
|
+
// we should terminate all workers and then gracefully shutdown self process
|
|
120
|
+
Promise.all(allWorkers.map(worker => worker.terminate())).then(() => process.exit(0), () => process.exit(1));
|
|
121
|
+
allWorkers = [];
|
|
122
|
+
};
|
|
123
|
+
// Take care to not leave orphaned processes behind. See #147.
|
|
124
|
+
process.on("SIGINT", () => terminateWorkersAndMaster());
|
|
125
|
+
process.on("SIGTERM", () => terminateWorkersAndMaster());
|
|
126
|
+
class BlobWorker extends Worker {
|
|
127
|
+
constructor(blob, options) {
|
|
128
|
+
super(Buffer.from(blob).toString("utf-8"), Object.assign(Object.assign({}, options), { fromSource: true }));
|
|
129
|
+
}
|
|
130
|
+
static fromText(source, options) {
|
|
131
|
+
return new Worker(source, Object.assign(Object.assign({}, options), { fromSource: true }));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
blob: BlobWorker,
|
|
136
|
+
default: Worker
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
let implementation;
|
|
140
|
+
function selectWorkerImplementation() {
|
|
141
|
+
return initWorkerThreadsWorker();
|
|
142
|
+
}
|
|
143
|
+
export function getWorkerImplementation() {
|
|
144
|
+
if (!implementation) {
|
|
145
|
+
implementation = selectWorkerImplementation();
|
|
146
|
+
}
|
|
147
|
+
return implementation;
|
|
148
|
+
}
|
|
149
|
+
export function isWorkerRuntime() {
|
|
150
|
+
// Webpack hack
|
|
151
|
+
const isMainThread = typeof __non_webpack_require__ === "function"
|
|
152
|
+
? __non_webpack_require__("worker_threads").isMainThread
|
|
153
|
+
: eval("require")("worker_threads").isMainThread;
|
|
154
|
+
return !isMainThread;
|
|
155
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getWorkerImplementation, isWorkerRuntime } from "./implementation";
|
|
2
|
+
export { Pool } from "./pool";
|
|
3
|
+
export { spawn } from "./spawn";
|
|
4
|
+
export { Thread } from "./thread";
|
|
5
|
+
export { isWorkerRuntime };
|
|
6
|
+
/** Separate class to spawn workers from source code blobs or strings. */
|
|
7
|
+
export const BlobWorker = getWorkerImplementation().blob;
|
|
8
|
+
/** Worker implementation. Either web worker or a node.js Worker class. */
|
|
9
|
+
export const Worker = getWorkerImplementation().default;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This source file contains the code for proxying calls in the master thread to calls in the workers
|
|
3
|
+
* by `.postMessage()`-ing.
|
|
4
|
+
*
|
|
5
|
+
* Keep in mind that this code can make or break the program's performance! Need to optimize more…
|
|
6
|
+
*/
|
|
7
|
+
import DebugLogger from "debug";
|
|
8
|
+
import { multicast, Observable } from "observable-fns";
|
|
9
|
+
import { deserialize, serialize } from "../common";
|
|
10
|
+
import { ObservablePromise } from "../observable-promise";
|
|
11
|
+
import { isTransferDescriptor } from "../transferable";
|
|
12
|
+
import { MasterMessageType, WorkerMessageType } from "../types/messages";
|
|
13
|
+
const debugMessages = DebugLogger("threads:master:messages");
|
|
14
|
+
let nextJobUID = 1;
|
|
15
|
+
const dedupe = (array) => Array.from(new Set(array));
|
|
16
|
+
const isJobErrorMessage = (data) => data && data.type === WorkerMessageType.error;
|
|
17
|
+
const isJobResultMessage = (data) => data && data.type === WorkerMessageType.result;
|
|
18
|
+
const isJobStartMessage = (data) => data && data.type === WorkerMessageType.running;
|
|
19
|
+
function createObservableForJob(worker, jobUID) {
|
|
20
|
+
return new Observable(observer => {
|
|
21
|
+
let asyncType;
|
|
22
|
+
const messageHandler = ((event) => {
|
|
23
|
+
debugMessages("Message from worker:", event.data);
|
|
24
|
+
if (!event.data || event.data.uid !== jobUID)
|
|
25
|
+
return;
|
|
26
|
+
if (isJobStartMessage(event.data)) {
|
|
27
|
+
asyncType = event.data.resultType;
|
|
28
|
+
}
|
|
29
|
+
else if (isJobResultMessage(event.data)) {
|
|
30
|
+
if (asyncType === "promise") {
|
|
31
|
+
if (typeof event.data.payload !== "undefined") {
|
|
32
|
+
observer.next(deserialize(event.data.payload));
|
|
33
|
+
}
|
|
34
|
+
observer.complete();
|
|
35
|
+
worker.removeEventListener("message", messageHandler);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (event.data.payload) {
|
|
39
|
+
observer.next(deserialize(event.data.payload));
|
|
40
|
+
}
|
|
41
|
+
if (event.data.complete) {
|
|
42
|
+
observer.complete();
|
|
43
|
+
worker.removeEventListener("message", messageHandler);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (isJobErrorMessage(event.data)) {
|
|
48
|
+
const error = deserialize(event.data.error);
|
|
49
|
+
if (asyncType === "promise" || !asyncType) {
|
|
50
|
+
observer.error(error);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
observer.error(error);
|
|
54
|
+
}
|
|
55
|
+
worker.removeEventListener("message", messageHandler);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
worker.addEventListener("message", messageHandler);
|
|
59
|
+
return () => {
|
|
60
|
+
if (asyncType === "observable" || !asyncType) {
|
|
61
|
+
const cancelMessage = {
|
|
62
|
+
type: MasterMessageType.cancel,
|
|
63
|
+
uid: jobUID
|
|
64
|
+
};
|
|
65
|
+
worker.postMessage(cancelMessage);
|
|
66
|
+
}
|
|
67
|
+
worker.removeEventListener("message", messageHandler);
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function prepareArguments(rawArgs) {
|
|
72
|
+
if (rawArgs.length === 0) {
|
|
73
|
+
// Exit early if possible
|
|
74
|
+
return {
|
|
75
|
+
args: [],
|
|
76
|
+
transferables: []
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const args = [];
|
|
80
|
+
const transferables = [];
|
|
81
|
+
for (const arg of rawArgs) {
|
|
82
|
+
if (isTransferDescriptor(arg)) {
|
|
83
|
+
args.push(serialize(arg.send));
|
|
84
|
+
transferables.push(...arg.transferables);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
args.push(serialize(arg));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
args,
|
|
92
|
+
transferables: transferables.length === 0 ? transferables : dedupe(transferables)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function createProxyFunction(worker, method) {
|
|
96
|
+
return ((...rawArgs) => {
|
|
97
|
+
const uid = nextJobUID++;
|
|
98
|
+
const { args, transferables } = prepareArguments(rawArgs);
|
|
99
|
+
const runMessage = {
|
|
100
|
+
type: MasterMessageType.run,
|
|
101
|
+
uid,
|
|
102
|
+
method,
|
|
103
|
+
args
|
|
104
|
+
};
|
|
105
|
+
debugMessages("Sending command to run function to worker:", runMessage);
|
|
106
|
+
try {
|
|
107
|
+
worker.postMessage(runMessage, transferables);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return ObservablePromise.from(Promise.reject(error));
|
|
111
|
+
}
|
|
112
|
+
return ObservablePromise.from(multicast(createObservableForJob(worker, uid)));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function createProxyModule(worker, methodNames) {
|
|
116
|
+
const proxy = {};
|
|
117
|
+
for (const methodName of methodNames) {
|
|
118
|
+
proxy[methodName] = createProxyFunction(worker, methodName);
|
|
119
|
+
}
|
|
120
|
+
return proxy;
|
|
121
|
+
}
|
|
@@ -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,283 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import DebugLogger from "debug";
|
|
11
|
+
import { multicast, Observable, Subject } from "observable-fns";
|
|
12
|
+
import { allSettled } from "../ponyfills";
|
|
13
|
+
import { defaultPoolSize } from "./implementation";
|
|
14
|
+
import { PoolEventType } from "./pool-types";
|
|
15
|
+
import { Thread } from "./thread";
|
|
16
|
+
export { PoolEventType, Thread };
|
|
17
|
+
let nextPoolID = 1;
|
|
18
|
+
function createArray(size) {
|
|
19
|
+
const array = [];
|
|
20
|
+
for (let index = 0; index < size; index++) {
|
|
21
|
+
array.push(index);
|
|
22
|
+
}
|
|
23
|
+
return array;
|
|
24
|
+
}
|
|
25
|
+
function delay(ms) {
|
|
26
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
function flatMap(array, mapper) {
|
|
29
|
+
return array.reduce((flattened, element) => [...flattened, ...mapper(element)], []);
|
|
30
|
+
}
|
|
31
|
+
function slugify(text) {
|
|
32
|
+
return text.replace(/\W/g, " ").trim().replace(/\s+/g, "-");
|
|
33
|
+
}
|
|
34
|
+
function spawnWorkers(spawnWorker, count) {
|
|
35
|
+
return createArray(count).map(() => ({
|
|
36
|
+
init: spawnWorker(),
|
|
37
|
+
runningTasks: []
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
class WorkerPool {
|
|
41
|
+
constructor(spawnWorker, optionsOrSize) {
|
|
42
|
+
this.eventSubject = new Subject();
|
|
43
|
+
this.initErrors = [];
|
|
44
|
+
this.isClosing = false;
|
|
45
|
+
this.nextTaskID = 1;
|
|
46
|
+
this.taskQueue = [];
|
|
47
|
+
const options = typeof optionsOrSize === "number"
|
|
48
|
+
? { size: optionsOrSize }
|
|
49
|
+
: optionsOrSize || {};
|
|
50
|
+
const { size = defaultPoolSize } = options;
|
|
51
|
+
this.debug = DebugLogger(`threads:pool:${slugify(options.name || String(nextPoolID++))}`);
|
|
52
|
+
this.options = options;
|
|
53
|
+
this.workers = spawnWorkers(spawnWorker, size);
|
|
54
|
+
this.eventObservable = multicast(Observable.from(this.eventSubject));
|
|
55
|
+
Promise.all(this.workers.map(worker => worker.init)).then(() => this.eventSubject.next({
|
|
56
|
+
type: PoolEventType.initialized,
|
|
57
|
+
size: this.workers.length
|
|
58
|
+
}), error => {
|
|
59
|
+
this.debug("Error while initializing pool worker:", error);
|
|
60
|
+
this.eventSubject.error(error);
|
|
61
|
+
this.initErrors.push(error);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
findIdlingWorker() {
|
|
65
|
+
const { concurrency = 1 } = this.options;
|
|
66
|
+
return this.workers.find(worker => worker.runningTasks.length < concurrency);
|
|
67
|
+
}
|
|
68
|
+
runPoolTask(worker, task) {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
const workerID = this.workers.indexOf(worker) + 1;
|
|
71
|
+
this.debug(`Running task #${task.id} on worker #${workerID}...`);
|
|
72
|
+
this.eventSubject.next({
|
|
73
|
+
type: PoolEventType.taskStart,
|
|
74
|
+
taskID: task.id,
|
|
75
|
+
workerID
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const returnValue = yield task.run(yield worker.init);
|
|
79
|
+
this.debug(`Task #${task.id} completed successfully`);
|
|
80
|
+
this.eventSubject.next({
|
|
81
|
+
type: PoolEventType.taskCompleted,
|
|
82
|
+
returnValue,
|
|
83
|
+
taskID: task.id,
|
|
84
|
+
workerID
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.debug(`Task #${task.id} failed`);
|
|
89
|
+
this.eventSubject.next({
|
|
90
|
+
type: PoolEventType.taskFailed,
|
|
91
|
+
taskID: task.id,
|
|
92
|
+
error: error,
|
|
93
|
+
workerID
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
run(worker, task) {
|
|
99
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
100
|
+
const runPromise = (() => __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
const removeTaskFromWorkersRunningTasks = () => {
|
|
102
|
+
worker.runningTasks = worker.runningTasks.filter(someRunPromise => someRunPromise !== runPromise);
|
|
103
|
+
};
|
|
104
|
+
// Defer task execution by one tick to give handlers time to subscribe
|
|
105
|
+
yield delay(0);
|
|
106
|
+
try {
|
|
107
|
+
yield this.runPoolTask(worker, task);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
removeTaskFromWorkersRunningTasks();
|
|
111
|
+
if (!this.isClosing) {
|
|
112
|
+
this.scheduleWork();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}))();
|
|
116
|
+
worker.runningTasks.push(runPromise);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
scheduleWork() {
|
|
120
|
+
this.debug(`Attempt de-queueing a task in order to run it...`);
|
|
121
|
+
const availableWorker = this.findIdlingWorker();
|
|
122
|
+
if (!availableWorker)
|
|
123
|
+
return;
|
|
124
|
+
const nextTask = this.taskQueue.shift();
|
|
125
|
+
if (!nextTask) {
|
|
126
|
+
this.debug(`Task queue is empty`);
|
|
127
|
+
this.eventSubject.next({ type: PoolEventType.taskQueueDrained });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.run(availableWorker, nextTask);
|
|
131
|
+
}
|
|
132
|
+
taskCompletion(taskID) {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const eventSubscription = this.events().subscribe(event => {
|
|
135
|
+
if (event.type === PoolEventType.taskCompleted && event.taskID === taskID) {
|
|
136
|
+
eventSubscription.unsubscribe();
|
|
137
|
+
resolve(event.returnValue);
|
|
138
|
+
}
|
|
139
|
+
else if (event.type === PoolEventType.taskFailed && event.taskID === taskID) {
|
|
140
|
+
eventSubscription.unsubscribe();
|
|
141
|
+
reject(event.error);
|
|
142
|
+
}
|
|
143
|
+
else if (event.type === PoolEventType.terminated) {
|
|
144
|
+
eventSubscription.unsubscribe();
|
|
145
|
+
reject(Error("Pool has been terminated before task was run."));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
settled() {
|
|
151
|
+
return __awaiter(this, arguments, void 0, function* (allowResolvingImmediately = false) {
|
|
152
|
+
const getCurrentlyRunningTasks = () => flatMap(this.workers, worker => worker.runningTasks);
|
|
153
|
+
const taskFailures = [];
|
|
154
|
+
const failureSubscription = this.eventObservable.subscribe(event => {
|
|
155
|
+
if (event.type === PoolEventType.taskFailed) {
|
|
156
|
+
taskFailures.push(event.error);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
if (this.initErrors.length > 0) {
|
|
160
|
+
return Promise.reject(this.initErrors[0]);
|
|
161
|
+
}
|
|
162
|
+
if (allowResolvingImmediately && this.taskQueue.length === 0) {
|
|
163
|
+
yield allSettled(getCurrentlyRunningTasks());
|
|
164
|
+
return taskFailures;
|
|
165
|
+
}
|
|
166
|
+
yield new Promise((resolve, reject) => {
|
|
167
|
+
const subscription = this.eventObservable.subscribe({
|
|
168
|
+
next(event) {
|
|
169
|
+
if (event.type === PoolEventType.taskQueueDrained) {
|
|
170
|
+
subscription.unsubscribe();
|
|
171
|
+
resolve(void 0);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
error: reject // make a pool-wide error reject the completed() result promise
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
yield allSettled(getCurrentlyRunningTasks());
|
|
178
|
+
failureSubscription.unsubscribe();
|
|
179
|
+
return taskFailures;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
completed() {
|
|
183
|
+
return __awaiter(this, arguments, void 0, function* (allowResolvingImmediately = false) {
|
|
184
|
+
const settlementPromise = this.settled(allowResolvingImmediately);
|
|
185
|
+
const earlyExitPromise = new Promise((resolve, reject) => {
|
|
186
|
+
const subscription = this.eventObservable.subscribe({
|
|
187
|
+
next(event) {
|
|
188
|
+
if (event.type === PoolEventType.taskQueueDrained) {
|
|
189
|
+
subscription.unsubscribe();
|
|
190
|
+
resolve(settlementPromise);
|
|
191
|
+
}
|
|
192
|
+
else if (event.type === PoolEventType.taskFailed) {
|
|
193
|
+
subscription.unsubscribe();
|
|
194
|
+
reject(event.error);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
error: reject // make a pool-wide error reject the completed() result promise
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
const errors = yield Promise.race([
|
|
201
|
+
settlementPromise,
|
|
202
|
+
earlyExitPromise
|
|
203
|
+
]);
|
|
204
|
+
if (errors.length > 0) {
|
|
205
|
+
throw errors[0];
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
events() {
|
|
210
|
+
return this.eventObservable;
|
|
211
|
+
}
|
|
212
|
+
queue(taskFunction) {
|
|
213
|
+
const { maxQueuedJobs = Infinity } = this.options;
|
|
214
|
+
if (this.isClosing) {
|
|
215
|
+
throw Error(`Cannot schedule pool tasks after terminate() has been called.`);
|
|
216
|
+
}
|
|
217
|
+
if (this.initErrors.length > 0) {
|
|
218
|
+
throw this.initErrors[0];
|
|
219
|
+
}
|
|
220
|
+
const taskID = this.nextTaskID++;
|
|
221
|
+
const taskCompletion = this.taskCompletion(taskID);
|
|
222
|
+
taskCompletion.catch((error) => {
|
|
223
|
+
// Prevent unhandled rejections here as we assume the user will use
|
|
224
|
+
// `pool.completed()`, `pool.settled()` or `task.catch()` to handle errors
|
|
225
|
+
this.debug(`Task #${taskID} errored:`, error);
|
|
226
|
+
});
|
|
227
|
+
const task = {
|
|
228
|
+
id: taskID,
|
|
229
|
+
run: taskFunction,
|
|
230
|
+
cancel: () => {
|
|
231
|
+
if (this.taskQueue.indexOf(task) === -1)
|
|
232
|
+
return;
|
|
233
|
+
this.taskQueue = this.taskQueue.filter(someTask => someTask !== task);
|
|
234
|
+
this.eventSubject.next({
|
|
235
|
+
type: PoolEventType.taskCanceled,
|
|
236
|
+
taskID: task.id
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
then: taskCompletion.then.bind(taskCompletion)
|
|
240
|
+
};
|
|
241
|
+
if (this.taskQueue.length >= maxQueuedJobs) {
|
|
242
|
+
throw Error("Maximum number of pool tasks queued. Refusing to queue another one.\n" +
|
|
243
|
+
"This usually happens for one of two reasons: We are either at peak " +
|
|
244
|
+
"workload right now or some tasks just won't finish, thus blocking the pool.");
|
|
245
|
+
}
|
|
246
|
+
this.debug(`Queueing task #${task.id}...`);
|
|
247
|
+
this.taskQueue.push(task);
|
|
248
|
+
this.eventSubject.next({
|
|
249
|
+
type: PoolEventType.taskQueued,
|
|
250
|
+
taskID: task.id
|
|
251
|
+
});
|
|
252
|
+
this.scheduleWork();
|
|
253
|
+
return task;
|
|
254
|
+
}
|
|
255
|
+
terminate(force) {
|
|
256
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
257
|
+
this.isClosing = true;
|
|
258
|
+
if (!force) {
|
|
259
|
+
yield this.completed(true);
|
|
260
|
+
}
|
|
261
|
+
this.eventSubject.next({
|
|
262
|
+
type: PoolEventType.terminated,
|
|
263
|
+
remainingQueue: [...this.taskQueue]
|
|
264
|
+
});
|
|
265
|
+
this.eventSubject.complete();
|
|
266
|
+
yield Promise.all(this.workers.map((worker) => __awaiter(this, void 0, void 0, function* () { return Thread.terminate(yield worker.init); })));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
WorkerPool.EventType = PoolEventType;
|
|
271
|
+
/**
|
|
272
|
+
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
273
|
+
*/
|
|
274
|
+
function PoolConstructor(spawnWorker, optionsOrSize) {
|
|
275
|
+
// The function exists only so we don't need to use `new` to create a pool (we still can, though).
|
|
276
|
+
// If the Pool is a class or not is an implementation detail that should not concern the user.
|
|
277
|
+
return new WorkerPool(spawnWorker, optionsOrSize);
|
|
278
|
+
}
|
|
279
|
+
PoolConstructor.EventType = PoolEventType;
|
|
280
|
+
/**
|
|
281
|
+
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
282
|
+
*/
|
|
283
|
+
export const Pool = PoolConstructor;
|