threadsx 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +31 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/master/implementation.node.js +22 -2
- package/dist/master/invocation-proxy.js +45 -11
- package/dist/master/pool.d.ts +1 -24
- package/dist/master/pool.js +25 -3
- package/dist/transferable.d.ts +1 -0
- package/dist/types/master.d.ts +22 -1
- package/dist/types/master.js +0 -1
- package/dist/types/worker.d.ts +1 -0
- package/dist/worker/implementation.browser.d.ts +1 -1
- package/dist/worker/implementation.worker_threads.d.ts +1 -1
- package/dist-esm/errors.js +26 -0
- package/dist-esm/index.js +1 -0
- package/dist-esm/master/implementation.node.js +22 -2
- package/dist-esm/master/invocation-proxy.js +45 -11
- package/dist-esm/master/pool.js +25 -3
- package/dist-esm/types/master.js +0 -1
- package/esm/common.mjs +20 -0
- package/esm/errors.mjs +34 -0
- package/esm/index.mjs +16 -0
- package/esm/master/get-bundle-url.browser.mjs +32 -0
- package/esm/master/implementation.browser.mjs +76 -0
- package/esm/master/implementation.mjs +24 -0
- package/esm/master/implementation.node.mjs +190 -0
- package/esm/master/index.mjs +17 -0
- package/esm/master/invocation-proxy.mjs +164 -0
- package/esm/master/pool-types.mjs +20 -0
- package/esm/master/pool.mjs +313 -0
- package/esm/master/register.mjs +14 -0
- package/esm/master/spawn.mjs +162 -0
- package/esm/master/thread.mjs +28 -0
- package/esm/observable-promise.mjs +155 -0
- package/esm/observable.mjs +45 -0
- package/esm/ponyfills.mjs +26 -0
- package/esm/promise.mjs +32 -0
- package/esm/serializers.mjs +55 -0
- package/esm/symbols.mjs +13 -0
- package/esm/transferable.mjs +31 -0
- package/esm/types/master.mjs +15 -0
- package/esm/types/messages.mjs +25 -0
- package/esm/worker/implementation.browser.mjs +31 -0
- package/esm/worker/implementation.mjs +22 -0
- package/esm/worker/implementation.worker_threads.mjs +46 -0
- package/esm/worker/index.mjs +224 -0
- package/esm/worker_threads.mjs +21 -0
- package/package.json +20 -20
- package/index.mjs +0 -4
- package/observable.mjs +0 -1
- package/register.mjs +0 -2
- package/worker.mjs +0 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import { fileURLToPath as __fileURLToPath } from "url";
|
|
3
|
+
import { dirname as __pathDirname } from "path";
|
|
4
|
+
const require = __createRequire(import.meta.url);
|
|
5
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = __pathDirname(__filename);
|
|
7
|
+
import DebugLogger from 'debug';
|
|
8
|
+
import { Subject, multicast, Observable } from 'observable-fns';
|
|
9
|
+
import { allSettled } from '../ponyfills.mjs';
|
|
10
|
+
import { defaultPoolSize } from './implementation.mjs';
|
|
11
|
+
import { PoolEventType } from './pool-types.mjs';
|
|
12
|
+
import { Thread } from './thread.mjs';
|
|
13
|
+
|
|
14
|
+
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
15
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
16
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
17
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
18
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
19
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
20
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
let nextPoolID = 1;
|
|
24
|
+
function createArray(size) {
|
|
25
|
+
const array = [];
|
|
26
|
+
for (let index = 0; index < size; index++) {
|
|
27
|
+
array.push(index);
|
|
28
|
+
}
|
|
29
|
+
return array;
|
|
30
|
+
}
|
|
31
|
+
function delay(ms) {
|
|
32
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
33
|
+
}
|
|
34
|
+
function flatMap(array, mapper) {
|
|
35
|
+
return array.reduce((flattened, element) => [...flattened, ...mapper(element)], []);
|
|
36
|
+
}
|
|
37
|
+
function slugify(text) {
|
|
38
|
+
return text.replace(/\W/g, " ").trim().replace(/\s+/g, "-");
|
|
39
|
+
}
|
|
40
|
+
function spawnWorkers(spawnWorker, count) {
|
|
41
|
+
return createArray(count).map(() => ({
|
|
42
|
+
init: spawnWorker(),
|
|
43
|
+
runningTasks: []
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
class WorkerPool {
|
|
47
|
+
constructor(spawnWorker, optionsOrSize) {
|
|
48
|
+
this.eventSubject = new Subject();
|
|
49
|
+
this.initErrors = [];
|
|
50
|
+
this.isClosing = false;
|
|
51
|
+
this.nextTaskID = 1;
|
|
52
|
+
this.taskQueue = [];
|
|
53
|
+
const options = typeof optionsOrSize === "number"
|
|
54
|
+
? { size: optionsOrSize }
|
|
55
|
+
: optionsOrSize || {};
|
|
56
|
+
const { size = defaultPoolSize } = options;
|
|
57
|
+
this.debug = DebugLogger(`threads:pool:${slugify(options.name || String(nextPoolID++))}`);
|
|
58
|
+
this.options = options;
|
|
59
|
+
this.workers = spawnWorkers(spawnWorker, size);
|
|
60
|
+
this.eventObservable = multicast(Observable.from(this.eventSubject));
|
|
61
|
+
Promise.all(this.workers.map(worker => worker.init)).then(() => this.eventSubject.next({
|
|
62
|
+
type: PoolEventType.initialized,
|
|
63
|
+
size: this.workers.length
|
|
64
|
+
}), error => {
|
|
65
|
+
this.debug("Error while initializing pool worker:", error);
|
|
66
|
+
this.eventSubject.error(error);
|
|
67
|
+
this.initErrors.push(error);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
findIdlingWorker() {
|
|
71
|
+
const { concurrency = 1 } = this.options;
|
|
72
|
+
return this.workers.find(worker => worker.runningTasks.length < concurrency);
|
|
73
|
+
}
|
|
74
|
+
runPoolTask(worker, task) {
|
|
75
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
76
|
+
const workerID = this.workers.indexOf(worker) + 1;
|
|
77
|
+
this.debug(`Running task #${task.id} on worker #${workerID}...`);
|
|
78
|
+
this.eventSubject.next({
|
|
79
|
+
type: PoolEventType.taskStart,
|
|
80
|
+
taskID: task.id,
|
|
81
|
+
workerID
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const returnValue = yield task.run(yield worker.init);
|
|
85
|
+
this.debug(`Task #${task.id} completed successfully`);
|
|
86
|
+
this.eventSubject.next({
|
|
87
|
+
type: PoolEventType.taskCompleted,
|
|
88
|
+
returnValue,
|
|
89
|
+
taskID: task.id,
|
|
90
|
+
workerID
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.debug(`Task #${task.id} failed`);
|
|
95
|
+
this.eventSubject.next({
|
|
96
|
+
type: PoolEventType.taskFailed,
|
|
97
|
+
taskID: task.id,
|
|
98
|
+
error: error,
|
|
99
|
+
workerID
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
run(worker, task) {
|
|
105
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
const runPromise = (() => __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
const removeTaskFromWorkersRunningTasks = () => {
|
|
108
|
+
worker.runningTasks = worker.runningTasks.filter(someRunPromise => someRunPromise !== runPromise);
|
|
109
|
+
};
|
|
110
|
+
// Defer task execution by one tick to give handlers time to subscribe
|
|
111
|
+
yield delay(0);
|
|
112
|
+
try {
|
|
113
|
+
yield this.runPoolTask(worker, task);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
removeTaskFromWorkersRunningTasks();
|
|
117
|
+
if (!this.isClosing) {
|
|
118
|
+
this.scheduleWork();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}))();
|
|
122
|
+
worker.runningTasks.push(runPromise);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
scheduleWork() {
|
|
126
|
+
this.debug(`Attempt de-queueing a task in order to run it...`);
|
|
127
|
+
const availableWorker = this.findIdlingWorker();
|
|
128
|
+
if (!availableWorker)
|
|
129
|
+
return;
|
|
130
|
+
const nextTask = this.taskQueue.shift();
|
|
131
|
+
if (!nextTask) {
|
|
132
|
+
this.debug(`Task queue is empty`);
|
|
133
|
+
this.eventSubject.next({ type: PoolEventType.taskQueueDrained });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.run(availableWorker, nextTask);
|
|
137
|
+
}
|
|
138
|
+
taskCompletion(taskID) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const eventSubscription = this.events().subscribe(event => {
|
|
141
|
+
if (event.type === PoolEventType.taskCompleted && event.taskID === taskID) {
|
|
142
|
+
eventSubscription.unsubscribe();
|
|
143
|
+
resolve(event.returnValue);
|
|
144
|
+
}
|
|
145
|
+
else if (event.type === PoolEventType.taskFailed && event.taskID === taskID) {
|
|
146
|
+
eventSubscription.unsubscribe();
|
|
147
|
+
reject(event.error);
|
|
148
|
+
}
|
|
149
|
+
else if (event.type === PoolEventType.terminated) {
|
|
150
|
+
eventSubscription.unsubscribe();
|
|
151
|
+
reject(Error("Pool has been terminated before task was run."));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
settled() {
|
|
157
|
+
return __awaiter(this, arguments, void 0, function* (allowResolvingImmediately = false) {
|
|
158
|
+
const getCurrentlyRunningTasks = () => flatMap(this.workers, worker => worker.runningTasks);
|
|
159
|
+
const taskFailures = [];
|
|
160
|
+
const failureSubscription = this.eventObservable.subscribe(event => {
|
|
161
|
+
if (event.type === PoolEventType.taskFailed) {
|
|
162
|
+
taskFailures.push(event.error);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
if (this.initErrors.length > 0) {
|
|
166
|
+
return Promise.reject(this.initErrors[0]);
|
|
167
|
+
}
|
|
168
|
+
if (allowResolvingImmediately && this.taskQueue.length === 0) {
|
|
169
|
+
yield allSettled(getCurrentlyRunningTasks());
|
|
170
|
+
return taskFailures;
|
|
171
|
+
}
|
|
172
|
+
yield new Promise((resolve, reject) => {
|
|
173
|
+
const subscription = this.eventObservable.subscribe({
|
|
174
|
+
next(event) {
|
|
175
|
+
if (event.type === PoolEventType.taskQueueDrained || event.type === PoolEventType.terminated) {
|
|
176
|
+
subscription.unsubscribe();
|
|
177
|
+
resolve(void 0);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
complete() {
|
|
181
|
+
// The pool was terminated while we were waiting; resolve instead of hanging.
|
|
182
|
+
subscription.unsubscribe();
|
|
183
|
+
resolve(void 0);
|
|
184
|
+
},
|
|
185
|
+
error: reject // make a pool-wide error reject the completed() result promise
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
yield allSettled(getCurrentlyRunningTasks());
|
|
189
|
+
failureSubscription.unsubscribe();
|
|
190
|
+
return taskFailures;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
completed() {
|
|
194
|
+
return __awaiter(this, arguments, void 0, function* (allowResolvingImmediately = false) {
|
|
195
|
+
const settlementPromise = this.settled(allowResolvingImmediately);
|
|
196
|
+
const earlyExitPromise = new Promise((resolve, reject) => {
|
|
197
|
+
const subscription = this.eventObservable.subscribe({
|
|
198
|
+
next(event) {
|
|
199
|
+
if (event.type === PoolEventType.taskQueueDrained || event.type === PoolEventType.terminated) {
|
|
200
|
+
subscription.unsubscribe();
|
|
201
|
+
resolve(settlementPromise);
|
|
202
|
+
}
|
|
203
|
+
else if (event.type === PoolEventType.taskFailed) {
|
|
204
|
+
subscription.unsubscribe();
|
|
205
|
+
reject(event.error);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
complete() {
|
|
209
|
+
// The pool was terminated while we were waiting; resolve instead of hanging.
|
|
210
|
+
subscription.unsubscribe();
|
|
211
|
+
resolve(settlementPromise);
|
|
212
|
+
},
|
|
213
|
+
error: reject // make a pool-wide error reject the completed() result promise
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
const errors = yield Promise.race([
|
|
217
|
+
settlementPromise,
|
|
218
|
+
earlyExitPromise
|
|
219
|
+
]);
|
|
220
|
+
if (errors.length > 0) {
|
|
221
|
+
throw errors[0];
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
events() {
|
|
226
|
+
return this.eventObservable;
|
|
227
|
+
}
|
|
228
|
+
queue(taskFunction) {
|
|
229
|
+
const { maxQueuedJobs = Infinity } = this.options;
|
|
230
|
+
if (this.isClosing) {
|
|
231
|
+
throw Error(`Cannot schedule pool tasks after terminate() has been called.`);
|
|
232
|
+
}
|
|
233
|
+
if (this.initErrors.length > 0) {
|
|
234
|
+
throw this.initErrors[0];
|
|
235
|
+
}
|
|
236
|
+
const taskID = this.nextTaskID++;
|
|
237
|
+
const taskCompletion = this.taskCompletion(taskID);
|
|
238
|
+
taskCompletion.catch((error) => {
|
|
239
|
+
// Prevent unhandled rejections here as we assume the user will use
|
|
240
|
+
// `pool.completed()`, `pool.settled()` or `task.catch()` to handle errors
|
|
241
|
+
this.debug(`Task #${taskID} errored:`, error);
|
|
242
|
+
});
|
|
243
|
+
const task = {
|
|
244
|
+
id: taskID,
|
|
245
|
+
run: taskFunction,
|
|
246
|
+
cancel: () => {
|
|
247
|
+
if (this.taskQueue.indexOf(task) === -1)
|
|
248
|
+
return;
|
|
249
|
+
this.taskQueue = this.taskQueue.filter(someTask => someTask !== task);
|
|
250
|
+
this.eventSubject.next({
|
|
251
|
+
type: PoolEventType.taskCanceled,
|
|
252
|
+
taskID: task.id
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
then: taskCompletion.then.bind(taskCompletion)
|
|
256
|
+
};
|
|
257
|
+
if (this.taskQueue.length >= maxQueuedJobs) {
|
|
258
|
+
throw Error("Maximum number of pool tasks queued. Refusing to queue another one.\n" +
|
|
259
|
+
"This usually happens for one of two reasons: We are either at peak " +
|
|
260
|
+
"workload right now or some tasks just won't finish, thus blocking the pool.");
|
|
261
|
+
}
|
|
262
|
+
this.debug(`Queueing task #${task.id}...`);
|
|
263
|
+
this.taskQueue.push(task);
|
|
264
|
+
this.eventSubject.next({
|
|
265
|
+
type: PoolEventType.taskQueued,
|
|
266
|
+
taskID: task.id
|
|
267
|
+
});
|
|
268
|
+
this.scheduleWork();
|
|
269
|
+
return task;
|
|
270
|
+
}
|
|
271
|
+
terminate(force) {
|
|
272
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
273
|
+
this.isClosing = true;
|
|
274
|
+
if (!force) {
|
|
275
|
+
yield this.completed(true);
|
|
276
|
+
}
|
|
277
|
+
this.eventSubject.next({
|
|
278
|
+
type: PoolEventType.terminated,
|
|
279
|
+
remainingQueue: [...this.taskQueue]
|
|
280
|
+
});
|
|
281
|
+
this.eventSubject.complete();
|
|
282
|
+
yield Promise.all(this.workers.map((worker) => __awaiter(this, void 0, void 0, function* () {
|
|
283
|
+
try {
|
|
284
|
+
yield Thread.terminate(yield worker.init);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
// The worker never finished initializing (e.g. it hit the init
|
|
288
|
+
// timeout), so `worker.init` rejected and there is no thread to
|
|
289
|
+
// terminate here — spawn() already tore down the underlying worker.
|
|
290
|
+
this.debug("Worker did not initialize; nothing to terminate:", error);
|
|
291
|
+
}
|
|
292
|
+
})));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
WorkerPool.EventType = PoolEventType;
|
|
297
|
+
/**
|
|
298
|
+
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
299
|
+
*/
|
|
300
|
+
function PoolConstructor(spawnWorker, optionsOrSize) {
|
|
301
|
+
// The function exists only so we don't need to use `new` to create a pool (we still can, though).
|
|
302
|
+
// If the Pool is a class or not is an implementation detail that should not concern the user.
|
|
303
|
+
// The explicit `Pool<ThreadType>` return type keeps the private `WorkerPool` class out of the
|
|
304
|
+
// public type surface, so consumers can name/re-export the pool type (#417).
|
|
305
|
+
return new WorkerPool(spawnWorker, optionsOrSize);
|
|
306
|
+
}
|
|
307
|
+
PoolConstructor.EventType = PoolEventType;
|
|
308
|
+
/**
|
|
309
|
+
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
310
|
+
*/
|
|
311
|
+
const Pool = PoolConstructor;
|
|
312
|
+
|
|
313
|
+
export { Pool, PoolEventType, Thread };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import { fileURLToPath as __fileURLToPath } from "url";
|
|
3
|
+
import { dirname as __pathDirname } from "path";
|
|
4
|
+
const require = __createRequire(import.meta.url);
|
|
5
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = __pathDirname(__filename);
|
|
7
|
+
import { Worker } from './index.mjs';
|
|
8
|
+
|
|
9
|
+
if (typeof global !== "undefined") {
|
|
10
|
+
global.Worker = Worker;
|
|
11
|
+
}
|
|
12
|
+
else if (typeof window !== "undefined") {
|
|
13
|
+
window.Worker = Worker;
|
|
14
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import { fileURLToPath as __fileURLToPath } from "url";
|
|
3
|
+
import { dirname as __pathDirname } from "path";
|
|
4
|
+
const require = __createRequire(import.meta.url);
|
|
5
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = __pathDirname(__filename);
|
|
7
|
+
import DebugLogger from 'debug';
|
|
8
|
+
import { Observable } from 'observable-fns';
|
|
9
|
+
import { deserialize } from '../common.mjs';
|
|
10
|
+
import { createPromiseWithResolver } from '../promise.mjs';
|
|
11
|
+
import { $worker, $terminate, $events, $errors } from '../symbols.mjs';
|
|
12
|
+
import { WorkerEventType } from '../types/master.mjs';
|
|
13
|
+
import { createProxyModule, createProxyFunction } from './invocation-proxy.mjs';
|
|
14
|
+
|
|
15
|
+
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
16
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
17
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
18
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
19
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
20
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
21
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const debugMessages = DebugLogger("threads:master:messages");
|
|
25
|
+
const debugSpawn = DebugLogger("threads:master:spawn");
|
|
26
|
+
const debugThreadUtils = DebugLogger("threads:master:thread-utils");
|
|
27
|
+
const isInitMessage = (data) => data && data.type === "init";
|
|
28
|
+
const isUncaughtErrorMessage = (data) => data && data.type === "uncaughtError";
|
|
29
|
+
const initMessageTimeout = typeof process !== "undefined" && typeof process.env !== "undefined" && process.env.THREADS_WORKER_INIT_TIMEOUT
|
|
30
|
+
? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10)
|
|
31
|
+
: 10000;
|
|
32
|
+
function withTimeout(promise, timeoutInMs, errorMessage) {
|
|
33
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
let timeoutHandle;
|
|
35
|
+
const timeout = new Promise((resolve, reject) => {
|
|
36
|
+
timeoutHandle = setTimeout(() => reject(Error(errorMessage)), timeoutInMs);
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
return yield Promise.race([
|
|
40
|
+
promise,
|
|
41
|
+
timeout
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
// Always clear the timer, even when `promise` rejects. Otherwise a pending
|
|
46
|
+
// timeout keeps the event loop alive until it fires (which on a failed
|
|
47
|
+
// spawn showed up as ava "failed to exit" on slower runners).
|
|
48
|
+
clearTimeout(timeoutHandle);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function receiveInitMessage(worker) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const messageHandler = ((event) => {
|
|
55
|
+
debugMessages("Message from worker before finishing initialization:", event.data);
|
|
56
|
+
if (isInitMessage(event.data)) {
|
|
57
|
+
worker.removeEventListener("message", messageHandler);
|
|
58
|
+
resolve(event.data);
|
|
59
|
+
}
|
|
60
|
+
else if (isUncaughtErrorMessage(event.data)) {
|
|
61
|
+
worker.removeEventListener("message", messageHandler);
|
|
62
|
+
reject(deserialize(event.data.error));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
worker.addEventListener("message", messageHandler);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function createEventObservable(worker, workerTermination) {
|
|
69
|
+
return new Observable(observer => {
|
|
70
|
+
const messageHandler = ((messageEvent) => {
|
|
71
|
+
const workerEvent = {
|
|
72
|
+
type: WorkerEventType.message,
|
|
73
|
+
data: messageEvent.data
|
|
74
|
+
};
|
|
75
|
+
observer.next(workerEvent);
|
|
76
|
+
});
|
|
77
|
+
const rejectionHandler = ((errorEvent) => {
|
|
78
|
+
debugThreadUtils("Unhandled promise rejection event in thread:", errorEvent);
|
|
79
|
+
const workerEvent = {
|
|
80
|
+
type: WorkerEventType.internalError,
|
|
81
|
+
error: Error(errorEvent.reason)
|
|
82
|
+
};
|
|
83
|
+
observer.next(workerEvent);
|
|
84
|
+
});
|
|
85
|
+
worker.addEventListener("message", messageHandler);
|
|
86
|
+
worker.addEventListener("unhandledrejection", rejectionHandler);
|
|
87
|
+
workerTermination.then(() => {
|
|
88
|
+
const terminationEvent = {
|
|
89
|
+
type: WorkerEventType.termination
|
|
90
|
+
};
|
|
91
|
+
worker.removeEventListener("message", messageHandler);
|
|
92
|
+
worker.removeEventListener("unhandledrejection", rejectionHandler);
|
|
93
|
+
observer.next(terminationEvent);
|
|
94
|
+
observer.complete();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function createTerminator(worker) {
|
|
99
|
+
const [termination, resolver] = createPromiseWithResolver();
|
|
100
|
+
const terminate = () => __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
debugThreadUtils("Terminating worker");
|
|
102
|
+
// Newer versions of worker_threads workers return a promise
|
|
103
|
+
yield worker.terminate();
|
|
104
|
+
resolver();
|
|
105
|
+
});
|
|
106
|
+
return { terminate, termination };
|
|
107
|
+
}
|
|
108
|
+
function setPrivateThreadProps(raw, worker, workerEvents, terminate) {
|
|
109
|
+
const workerErrors = workerEvents
|
|
110
|
+
.filter(event => event.type === WorkerEventType.internalError)
|
|
111
|
+
.map(errorEvent => errorEvent.error);
|
|
112
|
+
return Object.assign(raw, {
|
|
113
|
+
[$errors]: workerErrors,
|
|
114
|
+
[$events]: workerEvents,
|
|
115
|
+
[$terminate]: terminate,
|
|
116
|
+
[$worker]: worker
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Spawn a new thread. Takes a fresh worker instance, wraps it in a thin
|
|
121
|
+
* abstraction layer to provide the transparent API and verifies that
|
|
122
|
+
* the worker has initialized successfully.
|
|
123
|
+
*
|
|
124
|
+
* @param worker Instance of `Worker`. Either a web worker or a `worker_threads` worker.
|
|
125
|
+
* @param [options]
|
|
126
|
+
* @param [options.timeout] Init message timeout. Default: 10000 or set by environment variable.
|
|
127
|
+
*/
|
|
128
|
+
function spawn(worker, options) {
|
|
129
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
var _a, _b;
|
|
131
|
+
debugSpawn("Initializing new thread");
|
|
132
|
+
const timeout = options && options.timeout ? options.timeout : initMessageTimeout;
|
|
133
|
+
let initMessage;
|
|
134
|
+
try {
|
|
135
|
+
initMessage = yield withTimeout(receiveInitMessage(worker), timeout, `Timeout: Did not receive an init message from worker after ${timeout}ms. Make sure the worker calls expose().`);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// The worker failed to initialise (e.g. it threw before calling expose(),
|
|
139
|
+
// or never sent an init message). Tear it down so it does not leak a live
|
|
140
|
+
// worker handle and keep the process from exiting.
|
|
141
|
+
yield Promise.resolve((_b = (_a = worker).terminate) === null || _b === void 0 ? void 0 : _b.call(_a)).catch(() => undefined);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
const exposed = initMessage.exposed;
|
|
145
|
+
const { termination, terminate } = createTerminator(worker);
|
|
146
|
+
const events = createEventObservable(worker, termination);
|
|
147
|
+
if (exposed.type === "function") {
|
|
148
|
+
const proxy = createProxyFunction(worker);
|
|
149
|
+
return setPrivateThreadProps(proxy, worker, events, terminate);
|
|
150
|
+
}
|
|
151
|
+
else if (exposed.type === "module") {
|
|
152
|
+
const proxy = createProxyModule(worker, exposed.methods);
|
|
153
|
+
return setPrivateThreadProps(proxy, worker, events, terminate);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const type = exposed.type;
|
|
157
|
+
throw Error(`Worker init message states unexpected type of expose(): ${type}`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { spawn };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import { fileURLToPath as __fileURLToPath } from "url";
|
|
3
|
+
import { dirname as __pathDirname } from "path";
|
|
4
|
+
const require = __createRequire(import.meta.url);
|
|
5
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = __pathDirname(__filename);
|
|
7
|
+
import { $terminate, $events, $errors } from '../symbols.mjs';
|
|
8
|
+
|
|
9
|
+
function fail(message) {
|
|
10
|
+
throw Error(message);
|
|
11
|
+
}
|
|
12
|
+
/** Thread utility functions. Use them to manage or inspect a `spawn()`-ed thread. */
|
|
13
|
+
const Thread = {
|
|
14
|
+
/** Return an observable that can be used to subscribe to all errors happening in the thread. */
|
|
15
|
+
errors(thread) {
|
|
16
|
+
return thread[$errors] || fail("Error observable not found. Make sure to pass a thread instance as returned by the spawn() promise.");
|
|
17
|
+
},
|
|
18
|
+
/** Return an observable that can be used to subscribe to internal events happening in the thread. Useful for debugging. */
|
|
19
|
+
events(thread) {
|
|
20
|
+
return thread[$events] || fail("Events observable not found. Make sure to pass a thread instance as returned by the spawn() promise.");
|
|
21
|
+
},
|
|
22
|
+
/** Terminate a thread. Remember to terminate every thread when you are done using it. */
|
|
23
|
+
terminate(thread) {
|
|
24
|
+
return thread[$terminate]();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export { Thread };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import { fileURLToPath as __fileURLToPath } from "url";
|
|
3
|
+
import { dirname as __pathDirname } from "path";
|
|
4
|
+
const require = __createRequire(import.meta.url);
|
|
5
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = __pathDirname(__filename);
|
|
7
|
+
import { Observable } from 'observable-fns';
|
|
8
|
+
|
|
9
|
+
var _a;
|
|
10
|
+
const doNothing = () => undefined;
|
|
11
|
+
const returnInput = (input) => input;
|
|
12
|
+
const runDeferred = (fn) => Promise.resolve().then(fn);
|
|
13
|
+
function fail(error) {
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
function isThenable(thing) {
|
|
17
|
+
return thing && typeof thing.then === "function";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a hybrid, combining the APIs of an Observable and a Promise.
|
|
21
|
+
*
|
|
22
|
+
* It is used to proxy async process states when we are initially not sure
|
|
23
|
+
* if that async process will yield values once (-> Promise) or multiple
|
|
24
|
+
* times (-> Observable).
|
|
25
|
+
*
|
|
26
|
+
* Note that the observable promise inherits some of the observable's characteristics:
|
|
27
|
+
* The `init` function will be called *once for every time anyone subscribes to it*.
|
|
28
|
+
*
|
|
29
|
+
* If this is undesired, derive a hot observable from it using `makeHot()` and
|
|
30
|
+
* subscribe to that.
|
|
31
|
+
*/
|
|
32
|
+
class ObservablePromise extends Observable {
|
|
33
|
+
constructor(init) {
|
|
34
|
+
super((originalObserver) => {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
36
|
+
const self = this;
|
|
37
|
+
const observer = Object.assign(Object.assign({}, originalObserver), { complete() {
|
|
38
|
+
originalObserver.complete();
|
|
39
|
+
self.onCompletion();
|
|
40
|
+
},
|
|
41
|
+
error(error) {
|
|
42
|
+
originalObserver.error(error);
|
|
43
|
+
self.onError(error);
|
|
44
|
+
},
|
|
45
|
+
next(value) {
|
|
46
|
+
originalObserver.next(value);
|
|
47
|
+
self.onNext(value);
|
|
48
|
+
} });
|
|
49
|
+
try {
|
|
50
|
+
this.initHasRun = true;
|
|
51
|
+
return init(observer);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
observer.error(error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
this.initHasRun = false;
|
|
58
|
+
this.fulfillmentCallbacks = [];
|
|
59
|
+
this.rejectionCallbacks = [];
|
|
60
|
+
this.firstValueSet = false;
|
|
61
|
+
this.state = "pending";
|
|
62
|
+
this[_a] = "[object ObservablePromise]";
|
|
63
|
+
}
|
|
64
|
+
onNext(value) {
|
|
65
|
+
if (!this.firstValueSet) {
|
|
66
|
+
this.firstValue = value;
|
|
67
|
+
this.firstValueSet = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
onError(error) {
|
|
71
|
+
this.state = "rejected";
|
|
72
|
+
this.rejection = error;
|
|
73
|
+
for (const onRejected of this.rejectionCallbacks) {
|
|
74
|
+
// Promisifying the call to turn errors into unhandled promise rejections
|
|
75
|
+
// instead of them failing sync and cancelling the iteration
|
|
76
|
+
runDeferred(() => onRejected(error));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
onCompletion() {
|
|
80
|
+
this.state = "fulfilled";
|
|
81
|
+
for (const onFulfilled of this.fulfillmentCallbacks) {
|
|
82
|
+
// Promisifying the call to turn errors into unhandled promise rejections
|
|
83
|
+
// instead of them failing sync and cancelling the iteration
|
|
84
|
+
runDeferred(() => onFulfilled(this.firstValue));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
then(onFulfilledRaw, onRejectedRaw) {
|
|
88
|
+
const onFulfilled = onFulfilledRaw || returnInput;
|
|
89
|
+
const onRejected = onRejectedRaw || fail;
|
|
90
|
+
let onRejectedCalled = false;
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const rejectionCallback = (error) => {
|
|
93
|
+
if (onRejectedCalled)
|
|
94
|
+
return;
|
|
95
|
+
onRejectedCalled = true;
|
|
96
|
+
try {
|
|
97
|
+
resolve(onRejected(error));
|
|
98
|
+
}
|
|
99
|
+
catch (anotherError) {
|
|
100
|
+
reject(anotherError);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const fulfillmentCallback = (value) => {
|
|
104
|
+
try {
|
|
105
|
+
resolve(onFulfilled(value));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
rejectionCallback(error);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
if (!this.initHasRun) {
|
|
112
|
+
this.subscribe({ error: rejectionCallback });
|
|
113
|
+
}
|
|
114
|
+
if (this.state === "fulfilled") {
|
|
115
|
+
return resolve(onFulfilled(this.firstValue));
|
|
116
|
+
}
|
|
117
|
+
if (this.state === "rejected") {
|
|
118
|
+
onRejectedCalled = true;
|
|
119
|
+
return resolve(onRejected(this.rejection));
|
|
120
|
+
}
|
|
121
|
+
this.fulfillmentCallbacks.push(fulfillmentCallback);
|
|
122
|
+
this.rejectionCallbacks.push(rejectionCallback);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch(onRejected) {
|
|
126
|
+
return this.then(undefined, onRejected);
|
|
127
|
+
}
|
|
128
|
+
finally(onCompleted) {
|
|
129
|
+
const handler = onCompleted || doNothing;
|
|
130
|
+
return this.then((value) => {
|
|
131
|
+
handler();
|
|
132
|
+
return value;
|
|
133
|
+
}, () => handler());
|
|
134
|
+
}
|
|
135
|
+
static from(thing) {
|
|
136
|
+
if (isThenable(thing)) {
|
|
137
|
+
return new ObservablePromise(observer => {
|
|
138
|
+
const onFulfilled = (value) => {
|
|
139
|
+
observer.next(value);
|
|
140
|
+
observer.complete();
|
|
141
|
+
};
|
|
142
|
+
const onRejected = (error) => {
|
|
143
|
+
observer.error(error);
|
|
144
|
+
};
|
|
145
|
+
thing.then(onFulfilled, onRejected);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
return super.from(thing);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
_a = Symbol.toStringTag;
|
|
154
|
+
|
|
155
|
+
export { ObservablePromise };
|