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.
Files changed (53) hide show
  1. package/dist/errors.d.ts +16 -0
  2. package/dist/errors.js +31 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +3 -1
  5. package/dist/master/implementation.node.js +28 -2
  6. package/dist/master/invocation-proxy.js +45 -11
  7. package/dist/master/pool.d.ts +1 -24
  8. package/dist/master/pool.js +25 -3
  9. package/dist/transferable.d.ts +1 -0
  10. package/dist/types/master.d.ts +22 -1
  11. package/dist/types/master.js +0 -1
  12. package/dist/types/worker.d.ts +1 -0
  13. package/dist/worker/implementation.browser.d.ts +1 -1
  14. package/dist/worker/implementation.worker_threads.d.ts +1 -1
  15. package/dist-esm/errors.js +26 -0
  16. package/dist-esm/index.js +1 -0
  17. package/dist-esm/master/implementation.node.js +28 -2
  18. package/dist-esm/master/invocation-proxy.js +45 -11
  19. package/dist-esm/master/pool.js +25 -3
  20. package/dist-esm/types/master.js +0 -1
  21. package/esm/common.mjs +20 -0
  22. package/esm/errors.mjs +34 -0
  23. package/esm/index.mjs +16 -0
  24. package/esm/master/get-bundle-url.browser.mjs +32 -0
  25. package/esm/master/implementation.browser.mjs +76 -0
  26. package/esm/master/implementation.mjs +24 -0
  27. package/esm/master/implementation.node.mjs +190 -0
  28. package/esm/master/index.mjs +17 -0
  29. package/esm/master/invocation-proxy.mjs +164 -0
  30. package/esm/master/pool-types.mjs +20 -0
  31. package/esm/master/pool.mjs +313 -0
  32. package/esm/master/register.mjs +14 -0
  33. package/esm/master/spawn.mjs +162 -0
  34. package/esm/master/thread.mjs +28 -0
  35. package/esm/observable-promise.mjs +155 -0
  36. package/esm/observable.mjs +45 -0
  37. package/esm/ponyfills.mjs +26 -0
  38. package/esm/promise.mjs +32 -0
  39. package/esm/serializers.mjs +55 -0
  40. package/esm/symbols.mjs +13 -0
  41. package/esm/transferable.mjs +31 -0
  42. package/esm/types/master.mjs +15 -0
  43. package/esm/types/messages.mjs +25 -0
  44. package/esm/worker/implementation.browser.mjs +31 -0
  45. package/esm/worker/implementation.mjs +22 -0
  46. package/esm/worker/implementation.worker_threads.mjs +46 -0
  47. package/esm/worker/index.mjs +224 -0
  48. package/esm/worker_threads.mjs +21 -0
  49. package/package.json +20 -20
  50. package/index.mjs +0 -4
  51. package/observable.mjs +0 -1
  52. package/register.mjs +0 -2
  53. package/worker.mjs +0 -1
@@ -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
- process.on("SIGINT", () => terminateWorkersAndMaster());
166
- process.on("SIGTERM", () => terminateWorkersAndMaster());
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
- worker.removeEventListener("message", messageHandler);
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
- worker.removeEventListener("message", messageHandler);
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
- if (asyncType === "promise" || !asyncType) {
57
- observer.error(error);
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
- worker.removeEventListener("message", messageHandler);
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)));
@@ -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): WorkerPool<ThreadType>;
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
  */
@@ -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* () { return thread_1.Thread.terminate(yield worker.init); })));
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;
@@ -1,4 +1,5 @@
1
1
  import { $transferable } from "./symbols";
2
+ export type Transferable = object;
2
3
  export interface TransferDescriptor<T = any> {
3
4
  [$transferable]: true;
4
5
  send: T;
@@ -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
  }
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- /// <reference lib="dom" />
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
3
  exports.WorkerEventType = void 0;
5
4
  const symbols_1 = require("../symbols");
@@ -1,3 +1,4 @@
1
+ import { Transferable } from "../transferable";
1
2
  type UnsubscribeFn = () => void;
2
3
  export interface AbstractedWorkerAPI {
3
4
  isWorkerRuntime(): boolean;
@@ -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,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 } from "./serializers";
@@ -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
- process.on("SIGINT", () => terminateWorkersAndMaster());
125
- process.on("SIGTERM", () => terminateWorkersAndMaster());
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
- worker.removeEventListener("message", messageHandler);
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
- worker.removeEventListener("message", messageHandler);
52
+ cleanup();
44
53
  }
45
54
  }
46
55
  }
47
56
  else if (isJobErrorMessage(event.data)) {
48
57
  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);
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
- worker.removeEventListener("message", messageHandler);
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)));