threadsx 2.0.2 → 2.2.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 (115) 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 +34 -18
  6. package/dist/master/invocation-proxy.js +45 -11
  7. package/dist/master/pool.d.ts +1 -24
  8. package/dist/master/pool.js +41 -8
  9. package/dist/master/spawn.js +24 -7
  10. package/dist/node-require.d.ts +13 -0
  11. package/dist/node-require.js +87 -0
  12. package/dist/transferable.d.ts +1 -0
  13. package/dist/types/master.d.ts +22 -1
  14. package/dist/types/master.js +0 -1
  15. package/dist/types/worker.d.ts +1 -0
  16. package/dist/worker/implementation.browser.d.ts +1 -1
  17. package/dist/worker/implementation.worker_threads.d.ts +1 -1
  18. package/dist/worker_threads.d.ts +3 -7
  19. package/dist/worker_threads.js +35 -11
  20. package/dist-esm/common.js +1 -0
  21. package/dist-esm/common.js.map +1 -0
  22. package/dist-esm/errors.js +27 -0
  23. package/dist-esm/errors.js.map +1 -0
  24. package/dist-esm/index.js +2 -0
  25. package/dist-esm/index.js.map +1 -0
  26. package/dist-esm/master/get-bundle-url.browser.js +1 -0
  27. package/dist-esm/master/get-bundle-url.browser.js.map +1 -0
  28. package/dist-esm/master/implementation.browser.js +1 -0
  29. package/dist-esm/master/implementation.browser.js.map +1 -0
  30. package/dist-esm/master/implementation.js +1 -0
  31. package/dist-esm/master/implementation.js.map +1 -0
  32. package/dist-esm/master/implementation.node.js +35 -18
  33. package/dist-esm/master/implementation.node.js.map +1 -0
  34. package/dist-esm/master/index.js +1 -0
  35. package/dist-esm/master/index.js.map +1 -0
  36. package/dist-esm/master/invocation-proxy.js +46 -11
  37. package/dist-esm/master/invocation-proxy.js.map +1 -0
  38. package/dist-esm/master/pool-types.js +1 -0
  39. package/dist-esm/master/pool-types.js.map +1 -0
  40. package/dist-esm/master/pool.js +42 -8
  41. package/dist-esm/master/pool.js.map +1 -0
  42. package/dist-esm/master/register.js +1 -0
  43. package/dist-esm/master/register.js.map +1 -0
  44. package/dist-esm/master/spawn.js +25 -7
  45. package/dist-esm/master/spawn.js.map +1 -0
  46. package/dist-esm/master/thread.js +1 -0
  47. package/dist-esm/master/thread.js.map +1 -0
  48. package/dist-esm/node-require.js +50 -0
  49. package/dist-esm/node-require.js.map +1 -0
  50. package/dist-esm/observable-promise.js +1 -0
  51. package/dist-esm/observable-promise.js.map +1 -0
  52. package/dist-esm/observable.js +1 -0
  53. package/dist-esm/observable.js.map +1 -0
  54. package/dist-esm/ponyfills.js +1 -0
  55. package/dist-esm/ponyfills.js.map +1 -0
  56. package/dist-esm/promise.js +1 -0
  57. package/dist-esm/promise.js.map +1 -0
  58. package/dist-esm/serializers.js +1 -0
  59. package/dist-esm/serializers.js.map +1 -0
  60. package/dist-esm/symbols.js +1 -0
  61. package/dist-esm/symbols.js.map +1 -0
  62. package/dist-esm/transferable.js +1 -0
  63. package/dist-esm/transferable.js.map +1 -0
  64. package/dist-esm/types/master.js +1 -1
  65. package/dist-esm/types/master.js.map +1 -0
  66. package/dist-esm/types/messages.js +1 -0
  67. package/dist-esm/types/messages.js.map +1 -0
  68. package/dist-esm/types/worker.js +1 -0
  69. package/dist-esm/types/worker.js.map +1 -0
  70. package/dist-esm/worker/bundle-entry.js +1 -0
  71. package/dist-esm/worker/bundle-entry.js.map +1 -0
  72. package/dist-esm/worker/implementation.browser.js +1 -0
  73. package/dist-esm/worker/implementation.browser.js.map +1 -0
  74. package/dist-esm/worker/implementation.js +1 -0
  75. package/dist-esm/worker/implementation.js.map +1 -0
  76. package/dist-esm/worker/implementation.worker_threads.js +1 -0
  77. package/dist-esm/worker/implementation.worker_threads.js.map +1 -0
  78. package/dist-esm/worker/index.js +1 -0
  79. package/dist-esm/worker/index.js.map +1 -0
  80. package/dist-esm/worker_threads.js +3 -11
  81. package/dist-esm/worker_threads.js.map +1 -0
  82. package/esm/common.mjs +20 -0
  83. package/esm/errors.mjs +34 -0
  84. package/esm/index.mjs +16 -0
  85. package/esm/master/get-bundle-url.browser.mjs +32 -0
  86. package/esm/master/implementation.browser.mjs +76 -0
  87. package/esm/master/implementation.mjs +24 -0
  88. package/esm/master/implementation.node.mjs +186 -0
  89. package/esm/master/index.mjs +17 -0
  90. package/esm/master/invocation-proxy.mjs +164 -0
  91. package/esm/master/pool-types.mjs +20 -0
  92. package/esm/master/pool.mjs +324 -0
  93. package/esm/master/register.mjs +14 -0
  94. package/esm/master/spawn.mjs +179 -0
  95. package/esm/master/thread.mjs +28 -0
  96. package/esm/node-require.mjs +58 -0
  97. package/esm/observable-promise.mjs +155 -0
  98. package/esm/observable.mjs +45 -0
  99. package/esm/ponyfills.mjs +26 -0
  100. package/esm/promise.mjs +32 -0
  101. package/esm/serializers.mjs +55 -0
  102. package/esm/symbols.mjs +13 -0
  103. package/esm/transferable.mjs +31 -0
  104. package/esm/types/master.mjs +15 -0
  105. package/esm/types/messages.mjs +25 -0
  106. package/esm/worker/implementation.browser.mjs +31 -0
  107. package/esm/worker/implementation.mjs +21 -0
  108. package/esm/worker/implementation.worker_threads.mjs +44 -0
  109. package/esm/worker/index.mjs +224 -0
  110. package/esm/worker_threads.mjs +13 -0
  111. package/package.json +32 -24
  112. package/index.mjs +0 -4
  113. package/observable.mjs +0 -1
  114. package/register.mjs +0 -2
  115. 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,18 +40,34 @@ 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");
51
+ const node_require_1 = require("../node-require");
52
+ const worker_threads_1 = __importDefault(require("../worker_threads"));
47
53
  let detectedTsRuntime;
48
54
  exports.defaultPoolSize = (0, os_1.cpus)().length;
55
+ /**
56
+ * Whether the application opted out of threadsx installing SIGINT/SIGTERM
57
+ * handlers (which call `process.exit()`), via `THREADS_SKIP_SIGNAL_HANDLERS`.
58
+ */
59
+ function signalHandlersDisabled() {
60
+ const value = typeof process !== "undefined" && process.env
61
+ ? process.env.THREADS_SKIP_SIGNAL_HANDLERS
62
+ : undefined;
63
+ return value === "1" || value === "true";
64
+ }
49
65
  /**
50
66
  * Detects an available TypeScript runtime so `.ts`/`.tsx` worker files can be
51
67
  * spawned directly during development. Prefers `tsx`, falls back to `ts-node`.
52
68
  */
53
69
  function detectTsRuntime() {
54
- if (typeof __non_webpack_require__ === "function") {
70
+ if ((0, node_require_1.isWebpackBundle)()) {
55
71
  // Webpack build: => No TS runtime required or possible
56
72
  return null;
57
73
  }
@@ -61,7 +77,7 @@ function detectTsRuntime() {
61
77
  detectedTsRuntime = null;
62
78
  for (const candidate of ["tsx", "ts-node"]) {
63
79
  try {
64
- eval("require").resolve(candidate);
80
+ (0, node_require_1.getNodeRequire)().resolve(candidate);
65
81
  detectedTsRuntime = candidate;
66
82
  break;
67
83
  }
@@ -109,19 +125,17 @@ function rebaseScriptPath(scriptPath, ignoreRegex) {
109
125
  }
110
126
  function resolveScriptPath(scriptPath, baseURL) {
111
127
  const makeRelative = (filePath) => {
112
- // eval() hack is also webpack-related
113
- return path.isAbsolute(filePath) ? filePath : path.join(baseURL || eval("__dirname"), filePath);
128
+ return path.isAbsolute(filePath) ? filePath : path.join(baseURL || (0, node_require_1.getModuleDirname)(), filePath);
114
129
  };
115
- const workerFilePath = typeof __non_webpack_require__ === "function"
116
- ? __non_webpack_require__.resolve(makeRelative(scriptPath))
117
- : eval("require").resolve(makeRelative(rebaseScriptPath(scriptPath, /[/\\]worker_threads[/\\]/)));
130
+ // In a webpack bundle, walking the call stack is useless (every caller lives
131
+ // inside the bundle), so relative paths resolve without rebasing.
132
+ const workerFilePath = (0, node_require_1.isWebpackBundle)()
133
+ ? (0, node_require_1.getNodeRequire)().resolve(makeRelative(scriptPath))
134
+ : (0, node_require_1.getNodeRequire)().resolve(makeRelative(rebaseScriptPath(scriptPath, /[/\\]worker_threads[/\\]/)));
118
135
  return workerFilePath;
119
136
  }
120
137
  function initWorkerThreadsWorker() {
121
- // Webpack hack
122
- const NativeWorker = typeof __non_webpack_require__ === "function"
123
- ? __non_webpack_require__("worker_threads").Worker
124
- : eval("require")("worker_threads").Worker;
138
+ const NativeWorker = (0, worker_threads_1.default)().Worker;
125
139
  let allWorkers = [];
126
140
  class Worker extends NativeWorker {
127
141
  constructor(scriptPath, options) {
@@ -168,8 +182,14 @@ function initWorkerThreadsWorker() {
168
182
  allWorkers = [];
169
183
  };
170
184
  // Take care to not leave orphaned processes behind. See #147.
171
- process.on("SIGINT", () => terminateWorkersAndMaster());
172
- 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
+ }
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 }));
@@ -194,9 +214,5 @@ function getWorkerImplementation() {
194
214
  return implementation;
195
215
  }
196
216
  function isWorkerRuntime() {
197
- // Webpack hack
198
- const isMainThread = typeof __non_webpack_require__ === "function"
199
- ? __non_webpack_require__("worker_threads").isMainThread
200
- : eval("require")("worker_threads").isMainThread;
201
- return !isMainThread;
217
+ return !(0, worker_threads_1.default)().isMainThread;
202
218
  }
@@ -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
  */
@@ -147,6 +147,13 @@ class WorkerPool {
147
147
  eventSubscription.unsubscribe();
148
148
  reject(event.error);
149
149
  }
150
+ else if (event.type === pool_types_1.PoolEventType.taskCanceled && event.taskID === taskID) {
151
+ // Without this branch a canceled task would keep its event
152
+ // subscription alive for the pool's whole lifetime and its promise
153
+ // would never settle.
154
+ eventSubscription.unsubscribe();
155
+ reject(Error("Task has been canceled."));
156
+ }
150
157
  else if (event.type === pool_types_1.PoolEventType.terminated) {
151
158
  eventSubscription.unsubscribe();
152
159
  reject(Error("Pool has been terminated before task was run."));
@@ -164,20 +171,27 @@ class WorkerPool {
164
171
  }
165
172
  });
166
173
  if (this.initErrors.length > 0) {
174
+ failureSubscription.unsubscribe();
167
175
  return Promise.reject(this.initErrors[0]);
168
176
  }
169
177
  if (allowResolvingImmediately && this.taskQueue.length === 0) {
170
178
  yield (0, ponyfills_1.allSettled)(getCurrentlyRunningTasks());
179
+ failureSubscription.unsubscribe();
171
180
  return taskFailures;
172
181
  }
173
182
  yield new Promise((resolve, reject) => {
174
183
  const subscription = this.eventObservable.subscribe({
175
184
  next(event) {
176
- if (event.type === pool_types_1.PoolEventType.taskQueueDrained) {
185
+ if (event.type === pool_types_1.PoolEventType.taskQueueDrained || event.type === pool_types_1.PoolEventType.terminated) {
177
186
  subscription.unsubscribe();
178
187
  resolve(void 0);
179
188
  }
180
189
  },
190
+ complete() {
191
+ // The pool was terminated while we were waiting; resolve instead of hanging.
192
+ subscription.unsubscribe();
193
+ resolve(void 0);
194
+ },
181
195
  error: reject // make a pool-wide error reject the completed() result promise
182
196
  });
183
197
  });
@@ -192,7 +206,7 @@ class WorkerPool {
192
206
  const earlyExitPromise = new Promise((resolve, reject) => {
193
207
  const subscription = this.eventObservable.subscribe({
194
208
  next(event) {
195
- if (event.type === pool_types_1.PoolEventType.taskQueueDrained) {
209
+ if (event.type === pool_types_1.PoolEventType.taskQueueDrained || event.type === pool_types_1.PoolEventType.terminated) {
196
210
  subscription.unsubscribe();
197
211
  resolve(settlementPromise);
198
212
  }
@@ -201,6 +215,11 @@ class WorkerPool {
201
215
  reject(event.error);
202
216
  }
203
217
  },
218
+ complete() {
219
+ // The pool was terminated while we were waiting; resolve instead of hanging.
220
+ subscription.unsubscribe();
221
+ resolve(settlementPromise);
222
+ },
204
223
  error: reject // make a pool-wide error reject the completed() result promise
205
224
  });
206
225
  });
@@ -224,6 +243,13 @@ class WorkerPool {
224
243
  if (this.initErrors.length > 0) {
225
244
  throw this.initErrors[0];
226
245
  }
246
+ // Check the queue limit before taskCompletion() below subscribes to the
247
+ // pool events — throwing after would leak that subscription.
248
+ if (this.taskQueue.length >= maxQueuedJobs) {
249
+ throw Error("Maximum number of pool tasks queued. Refusing to queue another one.\n" +
250
+ "This usually happens for one of two reasons: We are either at peak " +
251
+ "workload right now or some tasks just won't finish, thus blocking the pool.");
252
+ }
227
253
  const taskID = this.nextTaskID++;
228
254
  const taskCompletion = this.taskCompletion(taskID);
229
255
  taskCompletion.catch((error) => {
@@ -245,11 +271,6 @@ class WorkerPool {
245
271
  },
246
272
  then: taskCompletion.then.bind(taskCompletion)
247
273
  };
248
- if (this.taskQueue.length >= maxQueuedJobs) {
249
- throw Error("Maximum number of pool tasks queued. Refusing to queue another one.\n" +
250
- "This usually happens for one of two reasons: We are either at peak " +
251
- "workload right now or some tasks just won't finish, thus blocking the pool.");
252
- }
253
274
  this.debug(`Queueing task #${task.id}...`);
254
275
  this.taskQueue.push(task);
255
276
  this.eventSubject.next({
@@ -270,7 +291,17 @@ class WorkerPool {
270
291
  remainingQueue: [...this.taskQueue]
271
292
  });
272
293
  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); })));
294
+ yield Promise.all(this.workers.map((worker) => __awaiter(this, void 0, void 0, function* () {
295
+ try {
296
+ yield thread_1.Thread.terminate(yield worker.init);
297
+ }
298
+ catch (error) {
299
+ // The worker never finished initializing (e.g. it hit the init
300
+ // timeout), so `worker.init` rejected and there is no thread to
301
+ // terminate here — spawn() already tore down the underlying worker.
302
+ this.debug("Worker did not initialize; nothing to terminate:", error);
303
+ }
304
+ })));
274
305
  });
275
306
  }
276
307
  }
@@ -281,6 +312,8 @@ WorkerPool.EventType = pool_types_1.PoolEventType;
281
312
  function PoolConstructor(spawnWorker, optionsOrSize) {
282
313
  // The function exists only so we don't need to use `new` to create a pool (we still can, though).
283
314
  // If the Pool is a class or not is an implementation detail that should not concern the user.
315
+ // The explicit `Pool<ThreadType>` return type keeps the private `WorkerPool` class out of the
316
+ // public type surface, so consumers can name/re-export the pool type (#417).
284
317
  return new WorkerPool(spawnWorker, optionsOrSize);
285
318
  }
286
319
  PoolConstructor.EventType = pool_types_1.PoolEventType;
@@ -81,17 +81,32 @@ function createEventObservable(worker, workerTermination) {
81
81
  };
82
82
  observer.next(workerEvent);
83
83
  });
84
- worker.addEventListener("message", messageHandler);
85
- worker.addEventListener("unhandledrejection", rejectionHandler);
86
- workerTermination.then(() => {
84
+ let finished = false;
85
+ const removeListeners = () => {
86
+ worker.removeEventListener("message", messageHandler);
87
+ worker.removeEventListener("unhandledrejection", rejectionHandler);
88
+ worker.removeEventListener("exit", exitHandler);
89
+ };
90
+ const finish = () => {
91
+ if (finished)
92
+ return;
93
+ finished = true;
87
94
  const terminationEvent = {
88
95
  type: master_1.WorkerEventType.termination
89
96
  };
90
- worker.removeEventListener("message", messageHandler);
91
- worker.removeEventListener("unhandledrejection", rejectionHandler);
97
+ removeListeners();
92
98
  observer.next(terminationEvent);
93
99
  observer.complete();
94
- });
100
+ };
101
+ // A worker can also go away without Thread.terminate() being called (crash,
102
+ // process.exit() in the worker). Complete the observable and detach the
103
+ // listeners in that case, too, instead of leaving them behind forever.
104
+ const exitHandler = (() => finish());
105
+ worker.addEventListener("message", messageHandler);
106
+ worker.addEventListener("unhandledrejection", rejectionHandler);
107
+ worker.addEventListener("exit", exitHandler);
108
+ workerTermination.then(finish);
109
+ return removeListeners;
95
110
  });
96
111
  }
97
112
  function createTerminator(worker) {
@@ -126,7 +141,7 @@ function setPrivateThreadProps(raw, worker, workerEvents, terminate) {
126
141
  */
127
142
  function spawn(worker, options) {
128
143
  return __awaiter(this, void 0, void 0, function* () {
129
- var _a, _b;
144
+ var _a, _b, _c, _d;
130
145
  debugSpawn("Initializing new thread");
131
146
  const timeout = options && options.timeout ? options.timeout : initMessageTimeout;
132
147
  let initMessage;
@@ -153,6 +168,8 @@ function spawn(worker, options) {
153
168
  }
154
169
  else {
155
170
  const type = exposed.type;
171
+ // Same teardown as a failed init: don't leak a live worker handle.
172
+ yield Promise.resolve((_d = (_c = worker).terminate) === null || _d === void 0 ? void 0 : _d.call(_c)).catch(() => undefined);
156
173
  throw Error(`Worker init message states unexpected type of expose(): ${type}`);
157
174
  }
158
175
  });
@@ -0,0 +1,13 @@
1
+ /** Whether this code is running inside a webpack bundle. */
2
+ export declare function isWebpackBundle(): boolean;
3
+ /**
4
+ * Returns a `require` that resolves against the real filesystem at runtime,
5
+ * even inside a bundle.
6
+ */
7
+ export declare function getNodeRequire(): NodeRequire;
8
+ /**
9
+ * Directory to resolve relative worker paths against when no caller path and
10
+ * no base URL are available. Matches this module's directory where the runtime
11
+ * provides one, and falls back to the current working directory.
12
+ */
13
+ export declare function getModuleDirname(): string;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isWebpackBundle = isWebpackBundle;
37
+ exports.getNodeRequire = getNodeRequire;
38
+ exports.getModuleDirname = getModuleDirname;
39
+ /*
40
+ * Runtime module/path resolution for the Node.js implementation.
41
+ *
42
+ * Spawning a worker needs real filesystem paths at runtime, but webpack
43
+ * rewrites `require`, `require.resolve` and `__dirname` at compile time.
44
+ * `__non_webpack_require__` is webpack's documented escape hatch: it compiles
45
+ * to the untouched `require` of the output bundle, so resolution happens at
46
+ * runtime against the real filesystem. Every other environment (plain Node,
47
+ * esbuild, rollup, Vite) keeps `createRequire` and `__dirname` intact, so no
48
+ * `eval()` tricks are needed there.
49
+ *
50
+ * This module is only ever loaded on Node targets: the package.json "browser"
51
+ * field maps its sole importers to `false` for browser bundles.
52
+ */
53
+ const module_1 = require("module");
54
+ const path = __importStar(require("path"));
55
+ /** Whether this code is running inside a webpack bundle. */
56
+ function isWebpackBundle() {
57
+ return typeof __non_webpack_require__ === "function";
58
+ }
59
+ let nodeRequire;
60
+ /**
61
+ * Returns a `require` that resolves against the real filesystem at runtime,
62
+ * even inside a bundle.
63
+ */
64
+ function getNodeRequire() {
65
+ if (typeof __non_webpack_require__ === "function") {
66
+ return __non_webpack_require__;
67
+ }
68
+ if (!nodeRequire) {
69
+ // CJS build: `__filename` is this module's real path. ESM build (esm/):
70
+ // the build banner derives `__filename` from `import.meta.url`. In an
71
+ // environment that defines neither (dist-esm bundled straight to ESM by a
72
+ // non-webpack bundler), resolve from the current working directory.
73
+ const base = typeof __filename !== "undefined"
74
+ ? __filename
75
+ : path.join(process.cwd(), "__threadsx_resolve__.js");
76
+ nodeRequire = (0, module_1.createRequire)(base);
77
+ }
78
+ return nodeRequire;
79
+ }
80
+ /**
81
+ * Directory to resolve relative worker paths against when no caller path and
82
+ * no base URL are available. Matches this module's directory where the runtime
83
+ * provides one, and falls back to the current working directory.
84
+ */
85
+ function getModuleDirname() {
86
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
87
+ }
@@ -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
  };