threadsx 2.0.1 → 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 +28 -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 +28 -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`.
|
|
@@ -86,6 +100,12 @@ function createTsRuntimeModule(scriptPath, runtime) {
|
|
|
86
100
|
return content;
|
|
87
101
|
}
|
|
88
102
|
function rebaseScriptPath(scriptPath, ignoreRegex) {
|
|
103
|
+
// An already-absolute path (e.g. from `new Worker(new URL("./w", import.meta.url))`,
|
|
104
|
+
// which threadsx normalizes to a filesystem path) must not be rebased onto the
|
|
105
|
+
// caller's directory — doing so would prepend it and corrupt the path.
|
|
106
|
+
if (path.isAbsolute(scriptPath)) {
|
|
107
|
+
return scriptPath;
|
|
108
|
+
}
|
|
89
109
|
const parentCallSite = (0, callsites_1.default)().find((callsite) => {
|
|
90
110
|
const filename = callsite.getFileName();
|
|
91
111
|
return Boolean(filename &&
|
|
@@ -162,8 +182,14 @@ function initWorkerThreadsWorker() {
|
|
|
162
182
|
allWorkers = [];
|
|
163
183
|
};
|
|
164
184
|
// Take care to not leave orphaned processes behind. See #147.
|
|
165
|
-
|
|
166
|
-
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
|
+
}
|
|
167
193
|
class BlobWorker extends Worker {
|
|
168
194
|
constructor(blob, options) {
|
|
169
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`.
|
|
@@ -45,6 +59,12 @@ function createTsRuntimeModule(scriptPath, runtime) {
|
|
|
45
59
|
return content;
|
|
46
60
|
}
|
|
47
61
|
function rebaseScriptPath(scriptPath, ignoreRegex) {
|
|
62
|
+
// An already-absolute path (e.g. from `new Worker(new URL("./w", import.meta.url))`,
|
|
63
|
+
// which threadsx normalizes to a filesystem path) must not be rebased onto the
|
|
64
|
+
// caller's directory — doing so would prepend it and corrupt the path.
|
|
65
|
+
if (path.isAbsolute(scriptPath)) {
|
|
66
|
+
return scriptPath;
|
|
67
|
+
}
|
|
48
68
|
const parentCallSite = getCallsites().find((callsite) => {
|
|
49
69
|
const filename = callsite.getFileName();
|
|
50
70
|
return Boolean(filename &&
|
|
@@ -121,8 +141,14 @@ function initWorkerThreadsWorker() {
|
|
|
121
141
|
allWorkers = [];
|
|
122
142
|
};
|
|
123
143
|
// Take care to not leave orphaned processes behind. See #147.
|
|
124
|
-
|
|
125
|
-
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
|
+
}
|
|
126
152
|
class BlobWorker extends Worker {
|
|
127
153
|
constructor(blob, options) {
|
|
128
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)));
|