threadsx 2.0.2 → 2.1.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/dist/errors.d.ts +16 -0
- package/dist/errors.js +31 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/master/implementation.node.js +22 -2
- package/dist/master/invocation-proxy.js +45 -11
- package/dist/master/pool.d.ts +1 -24
- package/dist/master/pool.js +25 -3
- package/dist/transferable.d.ts +1 -0
- package/dist/types/master.d.ts +22 -1
- package/dist/types/master.js +0 -1
- package/dist/types/worker.d.ts +1 -0
- package/dist/worker/implementation.browser.d.ts +1 -1
- package/dist/worker/implementation.worker_threads.d.ts +1 -1
- package/dist-esm/errors.js +26 -0
- package/dist-esm/index.js +1 -0
- package/dist-esm/master/implementation.node.js +22 -2
- package/dist-esm/master/invocation-proxy.js +45 -11
- package/dist-esm/master/pool.js +25 -3
- package/dist-esm/types/master.js +0 -1
- package/esm/common.mjs +20 -0
- package/esm/errors.mjs +34 -0
- package/esm/index.mjs +16 -0
- package/esm/master/get-bundle-url.browser.mjs +32 -0
- package/esm/master/implementation.browser.mjs +76 -0
- package/esm/master/implementation.mjs +24 -0
- package/esm/master/implementation.node.mjs +190 -0
- package/esm/master/index.mjs +17 -0
- package/esm/master/invocation-proxy.mjs +164 -0
- package/esm/master/pool-types.mjs +20 -0
- package/esm/master/pool.mjs +313 -0
- package/esm/master/register.mjs +14 -0
- package/esm/master/spawn.mjs +162 -0
- package/esm/master/thread.mjs +28 -0
- package/esm/observable-promise.mjs +155 -0
- package/esm/observable.mjs +45 -0
- package/esm/ponyfills.mjs +26 -0
- package/esm/promise.mjs +32 -0
- package/esm/serializers.mjs +55 -0
- package/esm/symbols.mjs +13 -0
- package/esm/transferable.mjs +31 -0
- package/esm/types/master.mjs +15 -0
- package/esm/types/messages.mjs +25 -0
- package/esm/worker/implementation.browser.mjs +31 -0
- package/esm/worker/implementation.mjs +22 -0
- package/esm/worker/implementation.worker_threads.mjs +46 -0
- package/esm/worker/index.mjs +224 -0
- package/esm/worker_threads.mjs +21 -0
- package/package.json +20 -20
- package/index.mjs +0 -4
- package/observable.mjs +0 -1
- package/register.mjs +0 -2
- package/worker.mjs +0 -1
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a value passed to a thread cannot be cloned by the structured
|
|
3
|
+
* clone algorithm (e.g. functions, class instances with methods, or other
|
|
4
|
+
* non-serializable values). Wraps the underlying `DataCloneError` / `DOMException`
|
|
5
|
+
* with an actionable message. Access the original error via `.cause`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ThreadCloneError extends Error {
|
|
8
|
+
readonly cause?: unknown;
|
|
9
|
+
constructor(message: string, cause?: unknown);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether an error is a structured-clone failure. Both browsers and Node's
|
|
13
|
+
* `worker_threads` throw a `DOMException`/error named `"DataCloneError"` when a
|
|
14
|
+
* value cannot be cloned across the thread boundary.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isDataCloneError(error: unknown): boolean;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ThreadCloneError = void 0;
|
|
4
|
+
exports.isDataCloneError = isDataCloneError;
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a value passed to a thread cannot be cloned by the structured
|
|
7
|
+
* clone algorithm (e.g. functions, class instances with methods, or other
|
|
8
|
+
* non-serializable values). Wraps the underlying `DataCloneError` / `DOMException`
|
|
9
|
+
* with an actionable message. Access the original error via `.cause`.
|
|
10
|
+
*/
|
|
11
|
+
class ThreadCloneError extends Error {
|
|
12
|
+
constructor(message, cause) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ThreadCloneError";
|
|
15
|
+
this.cause = cause;
|
|
16
|
+
// Restore the prototype chain — extending built-ins breaks `instanceof`
|
|
17
|
+
// when compiled down to ES2015 without this.
|
|
18
|
+
Object.setPrototypeOf(this, ThreadCloneError.prototype);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.ThreadCloneError = ThreadCloneError;
|
|
22
|
+
/**
|
|
23
|
+
* Whether an error is a structured-clone failure. Both browsers and Node's
|
|
24
|
+
* `worker_threads` throw a `DOMException`/error named `"DataCloneError"` when a
|
|
25
|
+
* value cannot be cloned across the thread boundary.
|
|
26
|
+
*/
|
|
27
|
+
function isDataCloneError(error) {
|
|
28
|
+
return Boolean(error &&
|
|
29
|
+
typeof error === "object" &&
|
|
30
|
+
error.name === "DataCloneError");
|
|
31
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { registerSerializer } from "./common";
|
|
2
|
+
export { ThreadCloneError } from "./errors";
|
|
2
3
|
export * from "./master/index";
|
|
3
4
|
export { expose } from "./worker/index";
|
|
4
5
|
export { DefaultSerializer, JsonSerializable, Serializer, SerializerImplementation } from "./serializers";
|
package/dist/index.js
CHANGED
|
@@ -14,9 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.Transfer = exports.DefaultSerializer = exports.expose = exports.registerSerializer = void 0;
|
|
17
|
+
exports.Transfer = exports.DefaultSerializer = exports.expose = exports.ThreadCloneError = exports.registerSerializer = void 0;
|
|
18
18
|
var common_1 = require("./common");
|
|
19
19
|
Object.defineProperty(exports, "registerSerializer", { enumerable: true, get: function () { return common_1.registerSerializer; } });
|
|
20
|
+
var errors_1 = require("./errors");
|
|
21
|
+
Object.defineProperty(exports, "ThreadCloneError", { enumerable: true, get: function () { return errors_1.ThreadCloneError; } });
|
|
20
22
|
__exportStar(require("./master/index"), exports);
|
|
21
23
|
var index_1 = require("./worker/index");
|
|
22
24
|
Object.defineProperty(exports, "expose", { enumerable: true, get: function () { return index_1.expose; } });
|
|
@@ -40,12 +40,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
40
40
|
exports.defaultPoolSize = void 0;
|
|
41
41
|
exports.getWorkerImplementation = getWorkerImplementation;
|
|
42
42
|
exports.isWorkerRuntime = isWorkerRuntime;
|
|
43
|
+
// NOTE: `callsites` is intentionally pinned to ^3. v4+ is ESM-only and would
|
|
44
|
+
// break the CommonJS (dist/) build. Do not bump it to v4 without moving the
|
|
45
|
+
// package to ESM-first output. (See .github/dependabot.yml, which also ignores
|
|
46
|
+
// its major updates.)
|
|
43
47
|
const callsites_1 = __importDefault(require("callsites"));
|
|
44
48
|
const os_1 = require("os");
|
|
45
49
|
const path = __importStar(require("path"));
|
|
46
50
|
const url_1 = require("url");
|
|
47
51
|
let detectedTsRuntime;
|
|
48
52
|
exports.defaultPoolSize = (0, os_1.cpus)().length;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the application opted out of threadsx installing SIGINT/SIGTERM
|
|
55
|
+
* handlers (which call `process.exit()`), via `THREADS_SKIP_SIGNAL_HANDLERS`.
|
|
56
|
+
*/
|
|
57
|
+
function signalHandlersDisabled() {
|
|
58
|
+
const value = typeof process !== "undefined" && process.env
|
|
59
|
+
? process.env.THREADS_SKIP_SIGNAL_HANDLERS
|
|
60
|
+
: undefined;
|
|
61
|
+
return value === "1" || value === "true";
|
|
62
|
+
}
|
|
49
63
|
/**
|
|
50
64
|
* Detects an available TypeScript runtime so `.ts`/`.tsx` worker files can be
|
|
51
65
|
* spawned directly during development. Prefers `tsx`, falls back to `ts-node`.
|
|
@@ -168,8 +182,14 @@ function initWorkerThreadsWorker() {
|
|
|
168
182
|
allWorkers = [];
|
|
169
183
|
};
|
|
170
184
|
// Take care to not leave orphaned processes behind. See #147.
|
|
171
|
-
|
|
172
|
-
process.
|
|
185
|
+
//
|
|
186
|
+
// These handlers call process.exit(), which hijacks the host application's
|
|
187
|
+
// own shutdown. Applications that manage their own graceful shutdown can opt
|
|
188
|
+
// out by setting THREADS_SKIP_SIGNAL_HANDLERS. See upstream #388 / #484.
|
|
189
|
+
if (!signalHandlersDisabled()) {
|
|
190
|
+
process.on("SIGINT", () => terminateWorkersAndMaster());
|
|
191
|
+
process.on("SIGTERM", () => terminateWorkersAndMaster());
|
|
192
|
+
}
|
|
173
193
|
class BlobWorker extends Worker {
|
|
174
194
|
constructor(blob, options) {
|
|
175
195
|
super(Buffer.from(blob).toString("utf-8"), Object.assign(Object.assign({}, options), { fromSource: true }));
|
|
@@ -14,6 +14,7 @@ exports.createProxyModule = createProxyModule;
|
|
|
14
14
|
const debug_1 = __importDefault(require("debug"));
|
|
15
15
|
const observable_fns_1 = require("observable-fns");
|
|
16
16
|
const common_1 = require("../common");
|
|
17
|
+
const errors_1 = require("../errors");
|
|
17
18
|
const observable_promise_1 = require("../observable-promise");
|
|
18
19
|
const transferable_1 = require("../transferable");
|
|
19
20
|
const messages_1 = require("../types/messages");
|
|
@@ -26,6 +27,12 @@ const isJobStartMessage = (data) => data && data.type === messages_1.WorkerMessa
|
|
|
26
27
|
function createObservableForJob(worker, jobUID) {
|
|
27
28
|
return new observable_fns_1.Observable(observer => {
|
|
28
29
|
let asyncType;
|
|
30
|
+
let settled = false;
|
|
31
|
+
const cleanup = () => {
|
|
32
|
+
worker.removeEventListener("message", messageHandler);
|
|
33
|
+
worker.removeEventListener("error", errorHandler);
|
|
34
|
+
worker.removeEventListener("exit", exitHandler);
|
|
35
|
+
};
|
|
29
36
|
const messageHandler = ((event) => {
|
|
30
37
|
debugMessages("Message from worker:", event.data);
|
|
31
38
|
if (!event.data || event.data.uid !== jobUID)
|
|
@@ -38,31 +45,52 @@ function createObservableForJob(worker, jobUID) {
|
|
|
38
45
|
if (typeof event.data.payload !== "undefined") {
|
|
39
46
|
observer.next((0, common_1.deserialize)(event.data.payload));
|
|
40
47
|
}
|
|
48
|
+
settled = true;
|
|
41
49
|
observer.complete();
|
|
42
|
-
|
|
50
|
+
cleanup();
|
|
43
51
|
}
|
|
44
52
|
else {
|
|
45
|
-
if (event.data.payload) {
|
|
53
|
+
if (typeof event.data.payload !== "undefined") {
|
|
46
54
|
observer.next((0, common_1.deserialize)(event.data.payload));
|
|
47
55
|
}
|
|
48
56
|
if (event.data.complete) {
|
|
57
|
+
settled = true;
|
|
49
58
|
observer.complete();
|
|
50
|
-
|
|
59
|
+
cleanup();
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
else if (isJobErrorMessage(event.data)) {
|
|
55
64
|
const error = (0, common_1.deserialize)(event.data.error);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
else {
|
|
60
|
-
observer.error(error);
|
|
61
|
-
}
|
|
62
|
-
worker.removeEventListener("message", messageHandler);
|
|
65
|
+
settled = true;
|
|
66
|
+
observer.error(error);
|
|
67
|
+
cleanup();
|
|
63
68
|
}
|
|
64
69
|
});
|
|
70
|
+
// If the worker crashes or is terminated before the job produces a result,
|
|
71
|
+
// reject the pending job instead of leaving the promise hanging forever.
|
|
72
|
+
// See #386.
|
|
73
|
+
const errorHandler = ((event) => {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
settled = true;
|
|
77
|
+
const error = event && event.data instanceof Error
|
|
78
|
+
? event.data
|
|
79
|
+
: Error(String((event && event.data) || "Worker errored before the job completed."));
|
|
80
|
+
observer.error(error);
|
|
81
|
+
cleanup();
|
|
82
|
+
});
|
|
83
|
+
const exitHandler = ((event) => {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
settled = true;
|
|
87
|
+
const exitCode = event ? event.data : undefined;
|
|
88
|
+
observer.error(Error(`Worker terminated before the job completed (exit code: ${exitCode}).`));
|
|
89
|
+
cleanup();
|
|
90
|
+
});
|
|
65
91
|
worker.addEventListener("message", messageHandler);
|
|
92
|
+
worker.addEventListener("error", errorHandler);
|
|
93
|
+
worker.addEventListener("exit", exitHandler);
|
|
66
94
|
return () => {
|
|
67
95
|
if (asyncType === "observable" || !asyncType) {
|
|
68
96
|
const cancelMessage = {
|
|
@@ -71,7 +99,7 @@ function createObservableForJob(worker, jobUID) {
|
|
|
71
99
|
};
|
|
72
100
|
worker.postMessage(cancelMessage);
|
|
73
101
|
}
|
|
74
|
-
|
|
102
|
+
cleanup();
|
|
75
103
|
};
|
|
76
104
|
});
|
|
77
105
|
}
|
|
@@ -114,6 +142,12 @@ function createProxyFunction(worker, method) {
|
|
|
114
142
|
worker.postMessage(runMessage, transferables);
|
|
115
143
|
}
|
|
116
144
|
catch (error) {
|
|
145
|
+
if ((0, errors_1.isDataCloneError)(error)) {
|
|
146
|
+
const cloneError = new errors_1.ThreadCloneError(`Cannot send arguments to the worker thread: a value is not structured-cloneable. ` +
|
|
147
|
+
`Functions, class instances and other non-serializable values cannot be passed to a thread. ` +
|
|
148
|
+
`Original error: ${error.message}`, error);
|
|
149
|
+
return observable_promise_1.ObservablePromise.from(Promise.reject(cloneError));
|
|
150
|
+
}
|
|
117
151
|
return observable_promise_1.ObservablePromise.from(Promise.reject(error));
|
|
118
152
|
}
|
|
119
153
|
return observable_promise_1.ObservablePromise.from((0, observable_fns_1.multicast)(createObservableForJob(worker, uid)));
|
package/dist/master/pool.d.ts
CHANGED
|
@@ -54,33 +54,10 @@ export interface PoolOptions {
|
|
|
54
54
|
/** No. of worker threads to spawn and to be managed by the pool. */
|
|
55
55
|
size?: number;
|
|
56
56
|
}
|
|
57
|
-
declare class WorkerPool<ThreadType extends Thread> implements Pool<ThreadType> {
|
|
58
|
-
static EventType: typeof PoolEventType;
|
|
59
|
-
private readonly debug;
|
|
60
|
-
private readonly eventObservable;
|
|
61
|
-
private readonly options;
|
|
62
|
-
private readonly workers;
|
|
63
|
-
private readonly eventSubject;
|
|
64
|
-
private initErrors;
|
|
65
|
-
private isClosing;
|
|
66
|
-
private nextTaskID;
|
|
67
|
-
private taskQueue;
|
|
68
|
-
constructor(spawnWorker: () => Promise<ThreadType>, optionsOrSize?: number | PoolOptions);
|
|
69
|
-
private findIdlingWorker;
|
|
70
|
-
private runPoolTask;
|
|
71
|
-
private run;
|
|
72
|
-
private scheduleWork;
|
|
73
|
-
private taskCompletion;
|
|
74
|
-
settled(allowResolvingImmediately?: boolean): Promise<Error[]>;
|
|
75
|
-
completed(allowResolvingImmediately?: boolean): Promise<void>;
|
|
76
|
-
events(): Observable<PoolEvent<ThreadType>>;
|
|
77
|
-
queue(taskFunction: TaskRunFunction<ThreadType, any>): QueuedTask<ThreadType, any>;
|
|
78
|
-
terminate(force?: boolean): Promise<void>;
|
|
79
|
-
}
|
|
80
57
|
/**
|
|
81
58
|
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
82
59
|
*/
|
|
83
|
-
declare function PoolConstructor<ThreadType extends Thread>(spawnWorker: () => Promise<ThreadType>, optionsOrSize?: number | PoolOptions):
|
|
60
|
+
declare function PoolConstructor<ThreadType extends Thread>(spawnWorker: () => Promise<ThreadType>, optionsOrSize?: number | PoolOptions): Pool<ThreadType>;
|
|
84
61
|
/**
|
|
85
62
|
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
86
63
|
*/
|
package/dist/master/pool.js
CHANGED
|
@@ -173,11 +173,16 @@ class WorkerPool {
|
|
|
173
173
|
yield new Promise((resolve, reject) => {
|
|
174
174
|
const subscription = this.eventObservable.subscribe({
|
|
175
175
|
next(event) {
|
|
176
|
-
if (event.type === pool_types_1.PoolEventType.taskQueueDrained) {
|
|
176
|
+
if (event.type === pool_types_1.PoolEventType.taskQueueDrained || event.type === pool_types_1.PoolEventType.terminated) {
|
|
177
177
|
subscription.unsubscribe();
|
|
178
178
|
resolve(void 0);
|
|
179
179
|
}
|
|
180
180
|
},
|
|
181
|
+
complete() {
|
|
182
|
+
// The pool was terminated while we were waiting; resolve instead of hanging.
|
|
183
|
+
subscription.unsubscribe();
|
|
184
|
+
resolve(void 0);
|
|
185
|
+
},
|
|
181
186
|
error: reject // make a pool-wide error reject the completed() result promise
|
|
182
187
|
});
|
|
183
188
|
});
|
|
@@ -192,7 +197,7 @@ class WorkerPool {
|
|
|
192
197
|
const earlyExitPromise = new Promise((resolve, reject) => {
|
|
193
198
|
const subscription = this.eventObservable.subscribe({
|
|
194
199
|
next(event) {
|
|
195
|
-
if (event.type === pool_types_1.PoolEventType.taskQueueDrained) {
|
|
200
|
+
if (event.type === pool_types_1.PoolEventType.taskQueueDrained || event.type === pool_types_1.PoolEventType.terminated) {
|
|
196
201
|
subscription.unsubscribe();
|
|
197
202
|
resolve(settlementPromise);
|
|
198
203
|
}
|
|
@@ -201,6 +206,11 @@ class WorkerPool {
|
|
|
201
206
|
reject(event.error);
|
|
202
207
|
}
|
|
203
208
|
},
|
|
209
|
+
complete() {
|
|
210
|
+
// The pool was terminated while we were waiting; resolve instead of hanging.
|
|
211
|
+
subscription.unsubscribe();
|
|
212
|
+
resolve(settlementPromise);
|
|
213
|
+
},
|
|
204
214
|
error: reject // make a pool-wide error reject the completed() result promise
|
|
205
215
|
});
|
|
206
216
|
});
|
|
@@ -270,7 +280,17 @@ class WorkerPool {
|
|
|
270
280
|
remainingQueue: [...this.taskQueue]
|
|
271
281
|
});
|
|
272
282
|
this.eventSubject.complete();
|
|
273
|
-
yield Promise.all(this.workers.map((worker) => __awaiter(this, void 0, void 0, function* () {
|
|
283
|
+
yield Promise.all(this.workers.map((worker) => __awaiter(this, void 0, void 0, function* () {
|
|
284
|
+
try {
|
|
285
|
+
yield thread_1.Thread.terminate(yield worker.init);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
// The worker never finished initializing (e.g. it hit the init
|
|
289
|
+
// timeout), so `worker.init` rejected and there is no thread to
|
|
290
|
+
// terminate here — spawn() already tore down the underlying worker.
|
|
291
|
+
this.debug("Worker did not initialize; nothing to terminate:", error);
|
|
292
|
+
}
|
|
293
|
+
})));
|
|
274
294
|
});
|
|
275
295
|
}
|
|
276
296
|
}
|
|
@@ -281,6 +301,8 @@ WorkerPool.EventType = pool_types_1.PoolEventType;
|
|
|
281
301
|
function PoolConstructor(spawnWorker, optionsOrSize) {
|
|
282
302
|
// The function exists only so we don't need to use `new` to create a pool (we still can, though).
|
|
283
303
|
// If the Pool is a class or not is an implementation detail that should not concern the user.
|
|
304
|
+
// The explicit `Pool<ThreadType>` return type keeps the private `WorkerPool` class out of the
|
|
305
|
+
// public type surface, so consumers can name/re-export the pool type (#417).
|
|
284
306
|
return new WorkerPool(spawnWorker, optionsOrSize);
|
|
285
307
|
}
|
|
286
308
|
PoolConstructor.EventType = pool_types_1.PoolEventType;
|
package/dist/transferable.d.ts
CHANGED
package/dist/types/master.d.ts
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { Observable } from "observable-fns";
|
|
2
2
|
import { ObservablePromise } from "../observable-promise";
|
|
3
3
|
import { $errors, $events, $terminate, $worker } from "../symbols";
|
|
4
|
-
import { TransferDescriptor } from "../transferable";
|
|
4
|
+
import { Transferable, TransferDescriptor } from "../transferable";
|
|
5
|
+
declare class EventTarget {
|
|
6
|
+
addEventListener(type: string, listener: any, options?: any): void;
|
|
7
|
+
removeEventListener(type: string, listener: any, options?: any): void;
|
|
8
|
+
dispatchEvent(event: any): boolean;
|
|
9
|
+
}
|
|
10
|
+
interface WorkerOptions {
|
|
11
|
+
type?: "classic" | "module";
|
|
12
|
+
credentials?: "omit" | "same-origin" | "include";
|
|
13
|
+
name?: string;
|
|
14
|
+
}
|
|
15
|
+
interface URL {
|
|
16
|
+
readonly href: string;
|
|
17
|
+
toString(): string;
|
|
18
|
+
}
|
|
19
|
+
interface Blob {
|
|
20
|
+
readonly size: number;
|
|
21
|
+
readonly type: string;
|
|
22
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
23
|
+
slice(start?: number, end?: number, contentType?: string): Blob;
|
|
24
|
+
text(): Promise<string>;
|
|
25
|
+
}
|
|
5
26
|
interface ObservableLikeSubscription {
|
|
6
27
|
unsubscribe(): any;
|
|
7
28
|
}
|
package/dist/types/master.js
CHANGED
package/dist/types/worker.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
2
|
isWorkerRuntime: () => boolean;
|
|
3
|
-
postMessageToMaster: (message: any, transferList?: Transferable[]) => void;
|
|
3
|
+
postMessageToMaster: (message: any, transferList?: import("../transferable").Transferable[]) => void;
|
|
4
4
|
subscribeToMasterMessages: (onMessage: (data: any) => void) => () => void;
|
|
5
5
|
};
|
|
6
6
|
export default _default;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare function testImplementation(): void;
|
|
2
2
|
declare const _default: {
|
|
3
3
|
isWorkerRuntime: () => boolean;
|
|
4
|
-
postMessageToMaster: (message: any, transferList?: Transferable[]) => void;
|
|
4
|
+
postMessageToMaster: (message: any, transferList?: import("../transferable").Transferable[]) => void;
|
|
5
5
|
subscribeToMasterMessages: (onMessage: (data: any) => void) => () => void;
|
|
6
6
|
testImplementation: typeof testImplementation;
|
|
7
7
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a value passed to a thread cannot be cloned by the structured
|
|
3
|
+
* clone algorithm (e.g. functions, class instances with methods, or other
|
|
4
|
+
* non-serializable values). Wraps the underlying `DataCloneError` / `DOMException`
|
|
5
|
+
* with an actionable message. Access the original error via `.cause`.
|
|
6
|
+
*/
|
|
7
|
+
export class ThreadCloneError extends Error {
|
|
8
|
+
constructor(message, cause) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ThreadCloneError";
|
|
11
|
+
this.cause = cause;
|
|
12
|
+
// Restore the prototype chain — extending built-ins breaks `instanceof`
|
|
13
|
+
// when compiled down to ES2015 without this.
|
|
14
|
+
Object.setPrototypeOf(this, ThreadCloneError.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether an error is a structured-clone failure. Both browsers and Node's
|
|
19
|
+
* `worker_threads` throw a `DOMException`/error named `"DataCloneError"` when a
|
|
20
|
+
* value cannot be cloned across the thread boundary.
|
|
21
|
+
*/
|
|
22
|
+
export function isDataCloneError(error) {
|
|
23
|
+
return Boolean(error &&
|
|
24
|
+
typeof error === "object" &&
|
|
25
|
+
error.name === "DataCloneError");
|
|
26
|
+
}
|
package/dist-esm/index.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
+
// NOTE: `callsites` is intentionally pinned to ^3. v4+ is ESM-only and would
|
|
3
|
+
// break the CommonJS (dist/) build. Do not bump it to v4 without moving the
|
|
4
|
+
// package to ESM-first output. (See .github/dependabot.yml, which also ignores
|
|
5
|
+
// its major updates.)
|
|
2
6
|
import getCallsites from "callsites";
|
|
3
7
|
import { cpus } from 'os';
|
|
4
8
|
import * as path from "path";
|
|
5
9
|
import { fileURLToPath } from "url";
|
|
6
10
|
let detectedTsRuntime;
|
|
7
11
|
export const defaultPoolSize = cpus().length;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the application opted out of threadsx installing SIGINT/SIGTERM
|
|
14
|
+
* handlers (which call `process.exit()`), via `THREADS_SKIP_SIGNAL_HANDLERS`.
|
|
15
|
+
*/
|
|
16
|
+
function signalHandlersDisabled() {
|
|
17
|
+
const value = typeof process !== "undefined" && process.env
|
|
18
|
+
? process.env.THREADS_SKIP_SIGNAL_HANDLERS
|
|
19
|
+
: undefined;
|
|
20
|
+
return value === "1" || value === "true";
|
|
21
|
+
}
|
|
8
22
|
/**
|
|
9
23
|
* Detects an available TypeScript runtime so `.ts`/`.tsx` worker files can be
|
|
10
24
|
* spawned directly during development. Prefers `tsx`, falls back to `ts-node`.
|
|
@@ -127,8 +141,14 @@ function initWorkerThreadsWorker() {
|
|
|
127
141
|
allWorkers = [];
|
|
128
142
|
};
|
|
129
143
|
// Take care to not leave orphaned processes behind. See #147.
|
|
130
|
-
|
|
131
|
-
process.
|
|
144
|
+
//
|
|
145
|
+
// These handlers call process.exit(), which hijacks the host application's
|
|
146
|
+
// own shutdown. Applications that manage their own graceful shutdown can opt
|
|
147
|
+
// out by setting THREADS_SKIP_SIGNAL_HANDLERS. See upstream #388 / #484.
|
|
148
|
+
if (!signalHandlersDisabled()) {
|
|
149
|
+
process.on("SIGINT", () => terminateWorkersAndMaster());
|
|
150
|
+
process.on("SIGTERM", () => terminateWorkersAndMaster());
|
|
151
|
+
}
|
|
132
152
|
class BlobWorker extends Worker {
|
|
133
153
|
constructor(blob, options) {
|
|
134
154
|
super(Buffer.from(blob).toString("utf-8"), Object.assign(Object.assign({}, options), { fromSource: true }));
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import DebugLogger from "debug";
|
|
8
8
|
import { multicast, Observable } from "observable-fns";
|
|
9
9
|
import { deserialize, serialize } from "../common";
|
|
10
|
+
import { isDataCloneError, ThreadCloneError } from "../errors";
|
|
10
11
|
import { ObservablePromise } from "../observable-promise";
|
|
11
12
|
import { isTransferDescriptor } from "../transferable";
|
|
12
13
|
import { MasterMessageType, WorkerMessageType } from "../types/messages";
|
|
@@ -19,6 +20,12 @@ const isJobStartMessage = (data) => data && data.type === WorkerMessageType.runn
|
|
|
19
20
|
function createObservableForJob(worker, jobUID) {
|
|
20
21
|
return new Observable(observer => {
|
|
21
22
|
let asyncType;
|
|
23
|
+
let settled = false;
|
|
24
|
+
const cleanup = () => {
|
|
25
|
+
worker.removeEventListener("message", messageHandler);
|
|
26
|
+
worker.removeEventListener("error", errorHandler);
|
|
27
|
+
worker.removeEventListener("exit", exitHandler);
|
|
28
|
+
};
|
|
22
29
|
const messageHandler = ((event) => {
|
|
23
30
|
debugMessages("Message from worker:", event.data);
|
|
24
31
|
if (!event.data || event.data.uid !== jobUID)
|
|
@@ -31,31 +38,52 @@ function createObservableForJob(worker, jobUID) {
|
|
|
31
38
|
if (typeof event.data.payload !== "undefined") {
|
|
32
39
|
observer.next(deserialize(event.data.payload));
|
|
33
40
|
}
|
|
41
|
+
settled = true;
|
|
34
42
|
observer.complete();
|
|
35
|
-
|
|
43
|
+
cleanup();
|
|
36
44
|
}
|
|
37
45
|
else {
|
|
38
|
-
if (event.data.payload) {
|
|
46
|
+
if (typeof event.data.payload !== "undefined") {
|
|
39
47
|
observer.next(deserialize(event.data.payload));
|
|
40
48
|
}
|
|
41
49
|
if (event.data.complete) {
|
|
50
|
+
settled = true;
|
|
42
51
|
observer.complete();
|
|
43
|
-
|
|
52
|
+
cleanup();
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
else if (isJobErrorMessage(event.data)) {
|
|
48
57
|
const error = deserialize(event.data.error);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
else {
|
|
53
|
-
observer.error(error);
|
|
54
|
-
}
|
|
55
|
-
worker.removeEventListener("message", messageHandler);
|
|
58
|
+
settled = true;
|
|
59
|
+
observer.error(error);
|
|
60
|
+
cleanup();
|
|
56
61
|
}
|
|
57
62
|
});
|
|
63
|
+
// If the worker crashes or is terminated before the job produces a result,
|
|
64
|
+
// reject the pending job instead of leaving the promise hanging forever.
|
|
65
|
+
// See #386.
|
|
66
|
+
const errorHandler = ((event) => {
|
|
67
|
+
if (settled)
|
|
68
|
+
return;
|
|
69
|
+
settled = true;
|
|
70
|
+
const error = event && event.data instanceof Error
|
|
71
|
+
? event.data
|
|
72
|
+
: Error(String((event && event.data) || "Worker errored before the job completed."));
|
|
73
|
+
observer.error(error);
|
|
74
|
+
cleanup();
|
|
75
|
+
});
|
|
76
|
+
const exitHandler = ((event) => {
|
|
77
|
+
if (settled)
|
|
78
|
+
return;
|
|
79
|
+
settled = true;
|
|
80
|
+
const exitCode = event ? event.data : undefined;
|
|
81
|
+
observer.error(Error(`Worker terminated before the job completed (exit code: ${exitCode}).`));
|
|
82
|
+
cleanup();
|
|
83
|
+
});
|
|
58
84
|
worker.addEventListener("message", messageHandler);
|
|
85
|
+
worker.addEventListener("error", errorHandler);
|
|
86
|
+
worker.addEventListener("exit", exitHandler);
|
|
59
87
|
return () => {
|
|
60
88
|
if (asyncType === "observable" || !asyncType) {
|
|
61
89
|
const cancelMessage = {
|
|
@@ -64,7 +92,7 @@ function createObservableForJob(worker, jobUID) {
|
|
|
64
92
|
};
|
|
65
93
|
worker.postMessage(cancelMessage);
|
|
66
94
|
}
|
|
67
|
-
|
|
95
|
+
cleanup();
|
|
68
96
|
};
|
|
69
97
|
});
|
|
70
98
|
}
|
|
@@ -107,6 +135,12 @@ export function createProxyFunction(worker, method) {
|
|
|
107
135
|
worker.postMessage(runMessage, transferables);
|
|
108
136
|
}
|
|
109
137
|
catch (error) {
|
|
138
|
+
if (isDataCloneError(error)) {
|
|
139
|
+
const cloneError = new ThreadCloneError(`Cannot send arguments to the worker thread: a value is not structured-cloneable. ` +
|
|
140
|
+
`Functions, class instances and other non-serializable values cannot be passed to a thread. ` +
|
|
141
|
+
`Original error: ${error.message}`, error);
|
|
142
|
+
return ObservablePromise.from(Promise.reject(cloneError));
|
|
143
|
+
}
|
|
110
144
|
return ObservablePromise.from(Promise.reject(error));
|
|
111
145
|
}
|
|
112
146
|
return ObservablePromise.from(multicast(createObservableForJob(worker, uid)));
|
package/dist-esm/master/pool.js
CHANGED
|
@@ -166,11 +166,16 @@ class WorkerPool {
|
|
|
166
166
|
yield new Promise((resolve, reject) => {
|
|
167
167
|
const subscription = this.eventObservable.subscribe({
|
|
168
168
|
next(event) {
|
|
169
|
-
if (event.type === PoolEventType.taskQueueDrained) {
|
|
169
|
+
if (event.type === PoolEventType.taskQueueDrained || event.type === PoolEventType.terminated) {
|
|
170
170
|
subscription.unsubscribe();
|
|
171
171
|
resolve(void 0);
|
|
172
172
|
}
|
|
173
173
|
},
|
|
174
|
+
complete() {
|
|
175
|
+
// The pool was terminated while we were waiting; resolve instead of hanging.
|
|
176
|
+
subscription.unsubscribe();
|
|
177
|
+
resolve(void 0);
|
|
178
|
+
},
|
|
174
179
|
error: reject // make a pool-wide error reject the completed() result promise
|
|
175
180
|
});
|
|
176
181
|
});
|
|
@@ -185,7 +190,7 @@ class WorkerPool {
|
|
|
185
190
|
const earlyExitPromise = new Promise((resolve, reject) => {
|
|
186
191
|
const subscription = this.eventObservable.subscribe({
|
|
187
192
|
next(event) {
|
|
188
|
-
if (event.type === PoolEventType.taskQueueDrained) {
|
|
193
|
+
if (event.type === PoolEventType.taskQueueDrained || event.type === PoolEventType.terminated) {
|
|
189
194
|
subscription.unsubscribe();
|
|
190
195
|
resolve(settlementPromise);
|
|
191
196
|
}
|
|
@@ -194,6 +199,11 @@ class WorkerPool {
|
|
|
194
199
|
reject(event.error);
|
|
195
200
|
}
|
|
196
201
|
},
|
|
202
|
+
complete() {
|
|
203
|
+
// The pool was terminated while we were waiting; resolve instead of hanging.
|
|
204
|
+
subscription.unsubscribe();
|
|
205
|
+
resolve(settlementPromise);
|
|
206
|
+
},
|
|
197
207
|
error: reject // make a pool-wide error reject the completed() result promise
|
|
198
208
|
});
|
|
199
209
|
});
|
|
@@ -263,7 +273,17 @@ class WorkerPool {
|
|
|
263
273
|
remainingQueue: [...this.taskQueue]
|
|
264
274
|
});
|
|
265
275
|
this.eventSubject.complete();
|
|
266
|
-
yield Promise.all(this.workers.map((worker) => __awaiter(this, void 0, void 0, function* () {
|
|
276
|
+
yield Promise.all(this.workers.map((worker) => __awaiter(this, void 0, void 0, function* () {
|
|
277
|
+
try {
|
|
278
|
+
yield Thread.terminate(yield worker.init);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
// The worker never finished initializing (e.g. it hit the init
|
|
282
|
+
// timeout), so `worker.init` rejected and there is no thread to
|
|
283
|
+
// terminate here — spawn() already tore down the underlying worker.
|
|
284
|
+
this.debug("Worker did not initialize; nothing to terminate:", error);
|
|
285
|
+
}
|
|
286
|
+
})));
|
|
267
287
|
});
|
|
268
288
|
}
|
|
269
289
|
}
|
|
@@ -274,6 +294,8 @@ WorkerPool.EventType = PoolEventType;
|
|
|
274
294
|
function PoolConstructor(spawnWorker, optionsOrSize) {
|
|
275
295
|
// The function exists only so we don't need to use `new` to create a pool (we still can, though).
|
|
276
296
|
// If the Pool is a class or not is an implementation detail that should not concern the user.
|
|
297
|
+
// The explicit `Pool<ThreadType>` return type keeps the private `WorkerPool` class out of the
|
|
298
|
+
// public type surface, so consumers can name/re-export the pool type (#417).
|
|
277
299
|
return new WorkerPool(spawnWorker, optionsOrSize);
|
|
278
300
|
}
|
|
279
301
|
PoolConstructor.EventType = PoolEventType;
|
package/dist-esm/types/master.js
CHANGED