@xylabs/threads 4.8.0-rc.2 → 4.8.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/browser/index-browser.mjs +827 -0
- package/dist/browser/index-browser.mjs.map +1 -0
- package/dist/browser/master/index-browser.mjs +793 -0
- package/dist/browser/master/index-browser.mjs.map +1 -0
- package/dist/browser/master/pool-browser.mjs +303 -0
- package/dist/browser/master/pool-browser.mjs.map +1 -0
- package/dist/browser/worker/worker.browser.mjs +3 -3
- package/dist/browser/worker/worker.browser.mjs.map +1 -1
- package/dist/neutral/master/register.mjs +21 -131
- package/dist/neutral/master/register.mjs.map +1 -1
- package/dist/{neutral/master/index.mjs → node/index-node.mjs} +81 -174
- package/dist/node/index-node.mjs.map +1 -0
- package/dist/{neutral/index.mjs → node/master/index-node.mjs} +56 -217
- package/dist/node/master/index-node.mjs.map +1 -0
- package/dist/{neutral/master/pool.mjs → node/master/pool-node.mjs} +22 -152
- package/dist/node/master/pool-node.mjs.map +1 -0
- package/dist/types/index-browser.d.ts +9 -0
- package/dist/types/index-browser.d.ts.map +1 -0
- package/dist/types/{index.d.ts → index-node.d.ts} +3 -3
- package/dist/types/index-node.d.ts.map +1 -0
- package/dist/types/master/index-browser.d.ts +13 -0
- package/dist/types/master/index-browser.d.ts.map +1 -0
- package/dist/types/master/{index.d.ts → index-node.d.ts} +3 -3
- package/dist/types/master/index-node.d.ts.map +1 -0
- package/dist/types/master/pool-browser.d.ts +93 -0
- package/dist/types/master/pool-browser.d.ts.map +1 -0
- package/dist/types/master/{pool.d.ts → pool-node.d.ts} +1 -1
- package/dist/types/master/pool-node.d.ts.map +1 -0
- package/dist/types/worker/worker.browser.d.ts.map +1 -1
- package/dist/types/worker/worker.node.d.ts.map +1 -1
- package/package.json +37 -27
- package/src/common.ts +23 -0
- package/src/index-browser.ts +11 -0
- package/src/index-node.ts +11 -0
- package/src/master/get-bundle-url.browser.ts +32 -0
- package/src/master/implementation.browser.ts +82 -0
- package/src/master/implementation.node.ts +208 -0
- package/src/master/index-browser.ts +19 -0
- package/src/master/index-node.ts +19 -0
- package/src/master/invocation-proxy.ts +151 -0
- package/src/master/pool-browser.ts +399 -0
- package/src/master/pool-node.ts +399 -0
- package/src/master/pool-types.ts +83 -0
- package/src/master/register.ts +11 -0
- package/src/master/spawn.ts +172 -0
- package/src/master/thread.ts +29 -0
- package/src/observable-promise.ts +184 -0
- package/src/observable.ts +44 -0
- package/src/promise.ts +26 -0
- package/src/serializers.ts +68 -0
- package/src/symbols.ts +5 -0
- package/src/transferable.ts +69 -0
- package/src/types/master.ts +132 -0
- package/src/types/messages.ts +72 -0
- package/src/types/worker.ts +14 -0
- package/src/worker/WorkerGlobalScope.ts +5 -0
- package/src/worker/expose.ts +234 -0
- package/src/worker/is-observable.d.ts +7 -0
- package/src/worker/worker.browser.ts +56 -0
- package/src/worker/worker.node.ts +68 -0
- package/CHANGELOG.md +0 -11
- package/dist/neutral/index.mjs.map +0 -1
- package/dist/neutral/master/implementation.mjs +0 -264
- package/dist/neutral/master/implementation.mjs.map +0 -1
- package/dist/neutral/master/index.mjs.map +0 -1
- package/dist/neutral/master/pool.mjs.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/master/implementation.d.ts +0 -7
- package/dist/types/master/implementation.d.ts.map +0 -1
- package/dist/types/master/index.d.ts.map +0 -1
- package/dist/types/master/pool.d.ts.map +0 -1
- package/dist/types/ponyfills.d.ts +0 -9
- package/dist/types/ponyfills.d.ts.map +0 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/* eslint-disable import-x/export */
|
|
2
|
+
/* eslint-disable unicorn/no-thenable */
|
|
3
|
+
|
|
4
|
+
/* eslint-disable @typescript-eslint/member-ordering */
|
|
5
|
+
/* eslint-disable unicorn/no-array-reduce */
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-namespace */
|
|
8
|
+
|
|
9
|
+
/// <reference lib="esnext" />
|
|
10
|
+
|
|
11
|
+
import DebugLogger from 'debug'
|
|
12
|
+
import {
|
|
13
|
+
multicast, Observable, Subject,
|
|
14
|
+
} from 'observable-fns'
|
|
15
|
+
|
|
16
|
+
import { defaultPoolSize } from './implementation.node.ts'
|
|
17
|
+
import type {
|
|
18
|
+
PoolEvent, QueuedTask, TaskRunFunction, WorkerDescriptor,
|
|
19
|
+
} from './pool-types.ts'
|
|
20
|
+
import { PoolEventType } from './pool-types.ts'
|
|
21
|
+
import { Thread } from './thread.ts'
|
|
22
|
+
|
|
23
|
+
export declare namespace Pool {
|
|
24
|
+
type Event<ThreadType extends Thread = any> = PoolEvent<ThreadType>
|
|
25
|
+
type EventType = PoolEventType
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let nextPoolID = 1
|
|
29
|
+
|
|
30
|
+
function createArray(size: number): number[] {
|
|
31
|
+
const array: number[] = []
|
|
32
|
+
for (let index = 0; index < size; index++) {
|
|
33
|
+
array.push(index)
|
|
34
|
+
}
|
|
35
|
+
return array
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function delay(ms: number) {
|
|
39
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function flatMap<In, Out>(array: In[], mapper: (element: In) => Out[]): Out[] {
|
|
43
|
+
return array.reduce<Out[]>((flattened, element) => [...flattened, ...mapper(element)], [])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function slugify(text: string) {
|
|
47
|
+
return text.replaceAll(/\W/g, ' ').trim().replaceAll(/\s+/g, '-')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function spawnWorkers<ThreadType extends Thread>(spawnWorker: () => Promise<ThreadType>, count: number): Array<WorkerDescriptor<ThreadType>> {
|
|
51
|
+
return createArray(count).map(
|
|
52
|
+
(): WorkerDescriptor<ThreadType> => ({
|
|
53
|
+
init: spawnWorker(),
|
|
54
|
+
runningTasks: [],
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Thread pool managing a set of worker threads.
|
|
61
|
+
* Use it to queue tasks that are run on those threads with limited
|
|
62
|
+
* concurrency.
|
|
63
|
+
*/
|
|
64
|
+
export interface Pool<ThreadType extends Thread> {
|
|
65
|
+
/**
|
|
66
|
+
* Returns a promise that resolves once the task queue is emptied.
|
|
67
|
+
* Promise will be rejected if any task fails.
|
|
68
|
+
*
|
|
69
|
+
* @param allowResolvingImmediately Set to `true` to resolve immediately if task queue is currently empty.
|
|
70
|
+
*/
|
|
71
|
+
completed(allowResolvingImmediately?: boolean): Promise<any>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns a promise that resolves once the task queue is emptied.
|
|
75
|
+
* Failing tasks will not cause the promise to be rejected.
|
|
76
|
+
*
|
|
77
|
+
* @param allowResolvingImmediately Set to `true` to resolve immediately if task queue is currently empty.
|
|
78
|
+
*/
|
|
79
|
+
settled(allowResolvingImmediately?: boolean): Promise<Error[]>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns an observable that yields pool events.
|
|
83
|
+
*/
|
|
84
|
+
events(): Observable<PoolEvent<ThreadType>>
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Queue a task and return a promise that resolves once the task has been dequeued,
|
|
88
|
+
* started and finished.
|
|
89
|
+
*
|
|
90
|
+
* @param task An async function that takes a thread instance and invokes it.
|
|
91
|
+
*/
|
|
92
|
+
queue<Return>(task: TaskRunFunction<ThreadType, Return>): QueuedTask<ThreadType, Return>
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Terminate all pool threads.
|
|
96
|
+
*
|
|
97
|
+
* @param force Set to `true` to kill the thread even if it cannot be stopped gracefully.
|
|
98
|
+
*/
|
|
99
|
+
terminate(force?: boolean): Promise<void>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface PoolOptions {
|
|
103
|
+
/** Maximum no. of tasks to run on one worker thread at a time. Defaults to one. */
|
|
104
|
+
concurrency?: number
|
|
105
|
+
|
|
106
|
+
/** Maximum no. of jobs to be queued for execution before throwing an error. */
|
|
107
|
+
maxQueuedJobs?: number
|
|
108
|
+
|
|
109
|
+
/** Gives that pool a name to be used for debug logging, letting you distinguish between log output of different pools. */
|
|
110
|
+
name?: string
|
|
111
|
+
|
|
112
|
+
/** No. of worker threads to spawn and to be managed by the pool. */
|
|
113
|
+
size?: number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class WorkerPool<ThreadType extends Thread> implements Pool<ThreadType> {
|
|
117
|
+
static EventType = PoolEventType
|
|
118
|
+
|
|
119
|
+
private readonly debug: DebugLogger.Debugger
|
|
120
|
+
private readonly eventObservable: Observable<PoolEvent<ThreadType>>
|
|
121
|
+
private readonly options: PoolOptions
|
|
122
|
+
private readonly workers: Array<WorkerDescriptor<ThreadType>>
|
|
123
|
+
|
|
124
|
+
private readonly eventSubject = new Subject<PoolEvent<ThreadType>>()
|
|
125
|
+
private initErrors: Error[] = []
|
|
126
|
+
private isClosing = false
|
|
127
|
+
private nextTaskID = 1
|
|
128
|
+
private taskQueue: Array<QueuedTask<ThreadType, any>> = []
|
|
129
|
+
|
|
130
|
+
constructor(spawnWorker: () => Promise<ThreadType>, optionsOrSize?: number | PoolOptions) {
|
|
131
|
+
const options: PoolOptions = typeof optionsOrSize === 'number' ? { size: optionsOrSize } : optionsOrSize || {}
|
|
132
|
+
|
|
133
|
+
const { size = defaultPoolSize } = options
|
|
134
|
+
|
|
135
|
+
this.debug = DebugLogger(`threads:pool:${slugify(options.name || String(nextPoolID++))}`)
|
|
136
|
+
this.options = options
|
|
137
|
+
this.workers = spawnWorkers(spawnWorker, size)
|
|
138
|
+
|
|
139
|
+
this.eventObservable = multicast(Observable.from(this.eventSubject))
|
|
140
|
+
|
|
141
|
+
Promise.all(this.workers.map(worker => worker.init)).then(
|
|
142
|
+
() =>
|
|
143
|
+
this.eventSubject.next({
|
|
144
|
+
size: this.workers.length,
|
|
145
|
+
type: PoolEventType.initialized,
|
|
146
|
+
}),
|
|
147
|
+
(error) => {
|
|
148
|
+
this.debug('Error while initializing pool worker:', error)
|
|
149
|
+
this.eventSubject.error(error)
|
|
150
|
+
this.initErrors.push(error)
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private findIdlingWorker(): WorkerDescriptor<ThreadType> | undefined {
|
|
156
|
+
const { concurrency = 1 } = this.options
|
|
157
|
+
return this.workers.find(worker => worker.runningTasks.length < concurrency)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async runPoolTask(worker: WorkerDescriptor<ThreadType>, task: QueuedTask<ThreadType, any>) {
|
|
161
|
+
const workerID = this.workers.indexOf(worker) + 1
|
|
162
|
+
|
|
163
|
+
this.debug(`Running task #${task.id} on worker #${workerID}...`)
|
|
164
|
+
this.eventSubject.next({
|
|
165
|
+
taskID: task.id,
|
|
166
|
+
type: PoolEventType.taskStart,
|
|
167
|
+
workerID,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const returnValue = await task.run(await worker.init)
|
|
172
|
+
|
|
173
|
+
this.debug(`Task #${task.id} completed successfully`)
|
|
174
|
+
this.eventSubject.next({
|
|
175
|
+
returnValue,
|
|
176
|
+
taskID: task.id,
|
|
177
|
+
type: PoolEventType.taskCompleted,
|
|
178
|
+
workerID,
|
|
179
|
+
})
|
|
180
|
+
} catch (ex) {
|
|
181
|
+
const error = ex as Error
|
|
182
|
+
this.debug(`Task #${task.id} failed`)
|
|
183
|
+
this.eventSubject.next({
|
|
184
|
+
error,
|
|
185
|
+
taskID: task.id,
|
|
186
|
+
type: PoolEventType.taskFailed,
|
|
187
|
+
workerID,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private run(worker: WorkerDescriptor<ThreadType>, task: QueuedTask<ThreadType, any>) {
|
|
193
|
+
const runPromise = (async () => {
|
|
194
|
+
const removeTaskFromWorkersRunningTasks = () => {
|
|
195
|
+
worker.runningTasks = worker.runningTasks.filter(someRunPromise => someRunPromise !== runPromise)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Defer task execution by one tick to give handlers time to subscribe
|
|
199
|
+
await delay(0)
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await this.runPoolTask(worker, task)
|
|
203
|
+
} finally {
|
|
204
|
+
removeTaskFromWorkersRunningTasks()
|
|
205
|
+
|
|
206
|
+
if (!this.isClosing) {
|
|
207
|
+
this.scheduleWork()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
})()
|
|
211
|
+
|
|
212
|
+
worker.runningTasks.push(runPromise)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private scheduleWork() {
|
|
216
|
+
this.debug('Attempt de-queueing a task in order to run it...')
|
|
217
|
+
|
|
218
|
+
const availableWorker = this.findIdlingWorker()
|
|
219
|
+
if (!availableWorker) return
|
|
220
|
+
|
|
221
|
+
const nextTask = this.taskQueue.shift()
|
|
222
|
+
if (!nextTask) {
|
|
223
|
+
this.debug('Task queue is empty')
|
|
224
|
+
this.eventSubject.next({ type: PoolEventType.taskQueueDrained })
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.run(availableWorker, nextTask)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private taskCompletion(taskID: number) {
|
|
232
|
+
return new Promise<any>((resolve, reject) => {
|
|
233
|
+
const eventSubscription = this.events().subscribe((event) => {
|
|
234
|
+
if (event.type === PoolEventType.taskCompleted && event.taskID === taskID) {
|
|
235
|
+
eventSubscription.unsubscribe()
|
|
236
|
+
resolve(event.returnValue)
|
|
237
|
+
} else if (event.type === PoolEventType.taskFailed && event.taskID === taskID) {
|
|
238
|
+
eventSubscription.unsubscribe()
|
|
239
|
+
reject(event.error)
|
|
240
|
+
} else if (event.type === PoolEventType.terminated) {
|
|
241
|
+
eventSubscription.unsubscribe()
|
|
242
|
+
reject(new Error('Pool has been terminated before task was run.'))
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async settled(allowResolvingImmediately: boolean = false): Promise<Error[]> {
|
|
249
|
+
const getCurrentlyRunningTasks = () => flatMap(this.workers, worker => worker.runningTasks)
|
|
250
|
+
|
|
251
|
+
const taskFailures: Error[] = []
|
|
252
|
+
|
|
253
|
+
const failureSubscription = this.eventObservable.subscribe((event) => {
|
|
254
|
+
if (event.type === PoolEventType.taskFailed) {
|
|
255
|
+
taskFailures.push(event.error)
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (this.initErrors.length > 0) {
|
|
260
|
+
throw this.initErrors[0]
|
|
261
|
+
}
|
|
262
|
+
if (allowResolvingImmediately && this.taskQueue.length === 0) {
|
|
263
|
+
await Promise.allSettled(getCurrentlyRunningTasks())
|
|
264
|
+
return taskFailures
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await new Promise<void>((resolve, reject) => {
|
|
268
|
+
const subscription = this.eventObservable.subscribe({
|
|
269
|
+
error: reject,
|
|
270
|
+
next(event) {
|
|
271
|
+
if (event.type === PoolEventType.taskQueueDrained) {
|
|
272
|
+
subscription.unsubscribe()
|
|
273
|
+
resolve(void 0)
|
|
274
|
+
}
|
|
275
|
+
}, // make a pool-wide error reject the completed() result promise
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
await Promise.allSettled(getCurrentlyRunningTasks())
|
|
280
|
+
failureSubscription.unsubscribe()
|
|
281
|
+
|
|
282
|
+
return taskFailures
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async completed(allowResolvingImmediately: boolean = false) {
|
|
286
|
+
const settlementPromise = this.settled(allowResolvingImmediately)
|
|
287
|
+
|
|
288
|
+
const earlyExitPromise = new Promise<Error[]>((resolve, reject) => {
|
|
289
|
+
const subscription = this.eventObservable.subscribe({
|
|
290
|
+
error: reject,
|
|
291
|
+
next(event) {
|
|
292
|
+
if (event.type === PoolEventType.taskQueueDrained) {
|
|
293
|
+
subscription.unsubscribe()
|
|
294
|
+
resolve(settlementPromise)
|
|
295
|
+
} else if (event.type === PoolEventType.taskFailed) {
|
|
296
|
+
subscription.unsubscribe()
|
|
297
|
+
reject(event.error)
|
|
298
|
+
}
|
|
299
|
+
}, // make a pool-wide error reject the completed() result promise
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const errors = await Promise.race([settlementPromise, earlyExitPromise])
|
|
304
|
+
|
|
305
|
+
if (errors.length > 0) {
|
|
306
|
+
throw errors[0]
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
events() {
|
|
311
|
+
return this.eventObservable
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
queue(taskFunction: TaskRunFunction<ThreadType, any>) {
|
|
315
|
+
const { maxQueuedJobs = Number.POSITIVE_INFINITY } = this.options
|
|
316
|
+
|
|
317
|
+
if (this.isClosing) {
|
|
318
|
+
throw new Error('Cannot schedule pool tasks after terminate() has been called.')
|
|
319
|
+
}
|
|
320
|
+
if (this.initErrors.length > 0) {
|
|
321
|
+
throw this.initErrors[0]
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const taskID = this.nextTaskID++
|
|
325
|
+
const taskCompletion = this.taskCompletion(taskID)
|
|
326
|
+
|
|
327
|
+
taskCompletion.catch((error) => {
|
|
328
|
+
// Prevent unhandled rejections here as we assume the user will use
|
|
329
|
+
// `pool.completed()`, `pool.settled()` or `task.catch()` to handle errors
|
|
330
|
+
this.debug(`Task #${taskID} errored:`, error)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const task: QueuedTask<ThreadType, any> = {
|
|
334
|
+
cancel: () => {
|
|
335
|
+
if (!this.taskQueue.includes(task)) return
|
|
336
|
+
this.taskQueue = this.taskQueue.filter(someTask => someTask !== task)
|
|
337
|
+
this.eventSubject.next({
|
|
338
|
+
taskID: task.id,
|
|
339
|
+
type: PoolEventType.taskCanceled,
|
|
340
|
+
})
|
|
341
|
+
},
|
|
342
|
+
id: taskID,
|
|
343
|
+
run: taskFunction,
|
|
344
|
+
then: taskCompletion.then.bind(taskCompletion),
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (this.taskQueue.length >= maxQueuedJobs) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
'Maximum number of pool tasks queued. Refusing to queue another one.\n'
|
|
350
|
+
+ 'This usually happens for one of two reasons: We are either at peak '
|
|
351
|
+
+ "workload right now or some tasks just won't finish, thus blocking the pool.",
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.debug(`Queueing task #${task.id}...`)
|
|
356
|
+
this.taskQueue.push(task)
|
|
357
|
+
|
|
358
|
+
this.eventSubject.next({
|
|
359
|
+
taskID: task.id,
|
|
360
|
+
type: PoolEventType.taskQueued,
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
this.scheduleWork()
|
|
364
|
+
return task
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async terminate(force?: boolean) {
|
|
368
|
+
this.isClosing = true
|
|
369
|
+
if (!force) {
|
|
370
|
+
await this.completed(true)
|
|
371
|
+
}
|
|
372
|
+
this.eventSubject.next({
|
|
373
|
+
remainingQueue: [...this.taskQueue],
|
|
374
|
+
type: PoolEventType.terminated,
|
|
375
|
+
})
|
|
376
|
+
this.eventSubject.complete()
|
|
377
|
+
await Promise.all(this.workers.map(async worker => Thread.terminate(await worker.init)))
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
383
|
+
*/
|
|
384
|
+
function PoolConstructor<ThreadType extends Thread>(spawnWorker: () => Promise<ThreadType>, optionsOrSize?: number | PoolOptions) {
|
|
385
|
+
// The function exists only so we don't need to use `new` to create a pool (we still can, though).
|
|
386
|
+
// If the Pool is a class or not is an implementation detail that should not concern the user.
|
|
387
|
+
return new WorkerPool(spawnWorker, optionsOrSize)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
;(PoolConstructor as any).EventType = PoolEventType
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Thread pool constructor. Creates a new pool and spawns its worker threads.
|
|
394
|
+
*/
|
|
395
|
+
export const Pool = PoolConstructor as typeof PoolConstructor & { EventType: typeof PoolEventType }
|
|
396
|
+
|
|
397
|
+
export type { PoolEvent, QueuedTask } from './pool-types.ts'
|
|
398
|
+
export { PoolEventType } from './pool-types.ts'
|
|
399
|
+
export { Thread } from './thread.ts'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable @typescript-eslint/member-ordering */
|
|
3
|
+
import type { Thread } from './thread.ts'
|
|
4
|
+
|
|
5
|
+
/** Pool event type. Specifies the type of each `PoolEvent`. */
|
|
6
|
+
export enum PoolEventType {
|
|
7
|
+
initialized = 'initialized',
|
|
8
|
+
taskCanceled = 'taskCanceled',
|
|
9
|
+
taskCompleted = 'taskCompleted',
|
|
10
|
+
taskFailed = 'taskFailed',
|
|
11
|
+
taskQueued = 'taskQueued',
|
|
12
|
+
taskQueueDrained = 'taskQueueDrained',
|
|
13
|
+
taskStart = 'taskStart',
|
|
14
|
+
terminated = 'terminated',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type TaskRunFunction<ThreadType extends Thread, Return> = (worker: ThreadType) => Promise<Return>
|
|
18
|
+
|
|
19
|
+
/** Pool event. Subscribe to those events using `pool.events()`. Useful for debugging. */
|
|
20
|
+
export type PoolEvent<ThreadType extends Thread> =
|
|
21
|
+
| {
|
|
22
|
+
type: PoolEventType.initialized
|
|
23
|
+
size: number
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
type: PoolEventType.taskQueued
|
|
27
|
+
taskID: number
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: PoolEventType.taskQueueDrained
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
type: PoolEventType.taskStart
|
|
34
|
+
taskID: number
|
|
35
|
+
workerID: number
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
type: PoolEventType.taskCompleted
|
|
39
|
+
returnValue: any
|
|
40
|
+
taskID: number
|
|
41
|
+
workerID: number
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
type: PoolEventType.taskFailed
|
|
45
|
+
error: Error
|
|
46
|
+
taskID: number
|
|
47
|
+
workerID: number
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
type: PoolEventType.taskCanceled
|
|
51
|
+
taskID: number
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: PoolEventType.terminated
|
|
55
|
+
remainingQueue: Array<QueuedTask<ThreadType, any>>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface WorkerDescriptor<ThreadType extends Thread> {
|
|
59
|
+
init: Promise<ThreadType>
|
|
60
|
+
runningTasks: Array<Promise<any>>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Task that has been `pool.queued()`-ed.
|
|
65
|
+
*/
|
|
66
|
+
export interface QueuedTask<ThreadType extends Thread, Return> {
|
|
67
|
+
/** @private */
|
|
68
|
+
id: number
|
|
69
|
+
|
|
70
|
+
/** @private */
|
|
71
|
+
run: TaskRunFunction<ThreadType, Return>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Queued tasks can be cancelled until the pool starts running them on a worker thread.
|
|
75
|
+
*/
|
|
76
|
+
cancel(): void
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `QueuedTask` is thenable, so you can `await` it.
|
|
80
|
+
* Resolves when the task has successfully been executed. Rejects if the task fails.
|
|
81
|
+
*/
|
|
82
|
+
then: Promise<Return>['then']
|
|
83
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import { Worker as WorkerImplementation } from './index-node.ts'
|
|
4
|
+
|
|
5
|
+
declare const window: any
|
|
6
|
+
|
|
7
|
+
if (typeof globalThis !== 'undefined') {
|
|
8
|
+
;(globalThis as any).Worker = WorkerImplementation
|
|
9
|
+
} else if (window !== undefined) {
|
|
10
|
+
;(window as any).Worker = WorkerImplementation
|
|
11
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
4
|
+
import DebugLogger from 'debug'
|
|
5
|
+
import { Observable } from 'observable-fns'
|
|
6
|
+
|
|
7
|
+
import { deserialize } from '../common.ts'
|
|
8
|
+
import { createPromiseWithResolver } from '../promise.ts'
|
|
9
|
+
import {
|
|
10
|
+
$errors, $events, $terminate, $worker,
|
|
11
|
+
} from '../symbols.ts'
|
|
12
|
+
import type {
|
|
13
|
+
FunctionThread,
|
|
14
|
+
ModuleThread,
|
|
15
|
+
PrivateThreadProps,
|
|
16
|
+
StripAsync,
|
|
17
|
+
Worker as WorkerType,
|
|
18
|
+
WorkerEvent,
|
|
19
|
+
WorkerInternalErrorEvent,
|
|
20
|
+
WorkerMessageEvent,
|
|
21
|
+
WorkerTerminationEvent,
|
|
22
|
+
} from '../types/master.ts'
|
|
23
|
+
import { WorkerEventType } from '../types/master.ts'
|
|
24
|
+
import type { WorkerInitMessage, WorkerUncaughtErrorMessage } from '../types/messages.ts'
|
|
25
|
+
import type { WorkerFunction, WorkerModule } from '../types/worker.ts'
|
|
26
|
+
import { createProxyFunction, createProxyModule } from './invocation-proxy.ts'
|
|
27
|
+
|
|
28
|
+
type ArbitraryWorkerInterface = WorkerFunction & WorkerModule<string> & { somekeythatisneverusedinproductioncode123: 'magicmarker123' }
|
|
29
|
+
type ArbitraryThreadType = FunctionThread<any, any> & ModuleThread<any>
|
|
30
|
+
|
|
31
|
+
export type ExposedToThreadType<Exposed extends WorkerFunction | WorkerModule<any>> =
|
|
32
|
+
Exposed extends ArbitraryWorkerInterface ? ArbitraryThreadType
|
|
33
|
+
: Exposed extends WorkerFunction ? FunctionThread<Parameters<Exposed>, StripAsync<ReturnType<Exposed>>>
|
|
34
|
+
: Exposed extends WorkerModule<any> ? ModuleThread<Exposed>
|
|
35
|
+
: never
|
|
36
|
+
|
|
37
|
+
const debugMessages = DebugLogger('threads:master:messages')
|
|
38
|
+
const debugSpawn = DebugLogger('threads:master:spawn')
|
|
39
|
+
const debugThreadUtils = DebugLogger('threads:master:thread-utils')
|
|
40
|
+
|
|
41
|
+
const isInitMessage = (data: any): data is WorkerInitMessage => data && data.type === ('init' as const)
|
|
42
|
+
const isUncaughtErrorMessage = (data: any): data is WorkerUncaughtErrorMessage => data && data.type === ('uncaughtError' as const)
|
|
43
|
+
|
|
44
|
+
const initMessageTimeout
|
|
45
|
+
= typeof process !== 'undefined' && process.env !== undefined && process.env.THREADS_WORKER_INIT_TIMEOUT
|
|
46
|
+
? Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10)
|
|
47
|
+
: 10_000
|
|
48
|
+
|
|
49
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutInMs: number, errorMessage: string): Promise<T> {
|
|
50
|
+
let timeoutHandle: any
|
|
51
|
+
|
|
52
|
+
const timeout = new Promise<never>((resolve, reject) => {
|
|
53
|
+
timeoutHandle = setTimeout(() => reject(new Error(errorMessage)), timeoutInMs)
|
|
54
|
+
})
|
|
55
|
+
const result = await Promise.race([promise, timeout])
|
|
56
|
+
|
|
57
|
+
clearTimeout(timeoutHandle)
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function receiveInitMessage(worker: WorkerType): Promise<WorkerInitMessage> {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const messageHandler = ((event: MessageEvent) => {
|
|
64
|
+
debugMessages('Message from worker before finishing initialization:', event.data)
|
|
65
|
+
if (isInitMessage(event.data)) {
|
|
66
|
+
worker.removeEventListener('message', messageHandler)
|
|
67
|
+
resolve(event.data)
|
|
68
|
+
} else if (isUncaughtErrorMessage(event.data)) {
|
|
69
|
+
worker.removeEventListener('message', messageHandler)
|
|
70
|
+
reject(deserialize(event.data.error))
|
|
71
|
+
}
|
|
72
|
+
}) as EventListener
|
|
73
|
+
worker.addEventListener('message', messageHandler)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createEventObservable(worker: WorkerType, workerTermination: Promise<any>): Observable<WorkerEvent> {
|
|
78
|
+
return new Observable<WorkerEvent>((observer) => {
|
|
79
|
+
const messageHandler = ((messageEvent: MessageEvent) => {
|
|
80
|
+
const workerEvent: WorkerMessageEvent<any> = {
|
|
81
|
+
data: messageEvent.data,
|
|
82
|
+
type: WorkerEventType.message,
|
|
83
|
+
}
|
|
84
|
+
observer.next(workerEvent)
|
|
85
|
+
}) as EventListener
|
|
86
|
+
const rejectionHandler = ((errorEvent: PromiseRejectionEvent) => {
|
|
87
|
+
debugThreadUtils('Unhandled promise rejection event in thread:', errorEvent)
|
|
88
|
+
const workerEvent: WorkerInternalErrorEvent = {
|
|
89
|
+
error: new Error(errorEvent.reason),
|
|
90
|
+
type: WorkerEventType.internalError,
|
|
91
|
+
}
|
|
92
|
+
observer.next(workerEvent)
|
|
93
|
+
}) as EventListener
|
|
94
|
+
worker.addEventListener('message', messageHandler)
|
|
95
|
+
worker.addEventListener('unhandledrejection', rejectionHandler)
|
|
96
|
+
|
|
97
|
+
workerTermination.then(() => {
|
|
98
|
+
const terminationEvent: WorkerTerminationEvent = { type: WorkerEventType.termination }
|
|
99
|
+
worker.removeEventListener('message', messageHandler)
|
|
100
|
+
worker.removeEventListener('unhandledrejection', rejectionHandler)
|
|
101
|
+
observer.next(terminationEvent)
|
|
102
|
+
observer.complete()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createTerminator(worker: WorkerType): { terminate: () => Promise<void>; termination: Promise<void> } {
|
|
108
|
+
const [termination, resolver] = createPromiseWithResolver<void>()
|
|
109
|
+
const terminate = async () => {
|
|
110
|
+
debugThreadUtils('Terminating worker')
|
|
111
|
+
// Newer versions of worker_threads workers return a promise
|
|
112
|
+
await worker.terminate()
|
|
113
|
+
resolver()
|
|
114
|
+
}
|
|
115
|
+
return { terminate, termination }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function setPrivateThreadProps<T>(
|
|
119
|
+
raw: T,
|
|
120
|
+
worker: WorkerType,
|
|
121
|
+
workerEvents: Observable<WorkerEvent>,
|
|
122
|
+
terminate: () => Promise<void>,
|
|
123
|
+
): T & PrivateThreadProps {
|
|
124
|
+
const workerErrors = workerEvents
|
|
125
|
+
.filter(event => event.type === WorkerEventType.internalError)
|
|
126
|
+
.map(errorEvent => (errorEvent as WorkerInternalErrorEvent).error)
|
|
127
|
+
|
|
128
|
+
return Object.assign(raw as any, {
|
|
129
|
+
[$errors]: workerErrors,
|
|
130
|
+
[$events]: workerEvents,
|
|
131
|
+
[$terminate]: terminate,
|
|
132
|
+
[$worker]: worker,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Spawn a new thread. Takes a fresh worker instance, wraps it in a thin
|
|
138
|
+
* abstraction layer to provide the transparent API and verifies that
|
|
139
|
+
* the worker has initialized successfully.
|
|
140
|
+
*
|
|
141
|
+
* @param worker Instance of `Worker`. Either a web worker, `worker_threads` worker or `tiny-worker` worker.
|
|
142
|
+
* @param [options]
|
|
143
|
+
* @param [options.timeout] Init message timeout. Default: 10000 or set by environment variable.
|
|
144
|
+
*/
|
|
145
|
+
export async function spawn<Exposed extends WorkerFunction | WorkerModule<any> = ArbitraryWorkerInterface>(
|
|
146
|
+
worker: WorkerType,
|
|
147
|
+
options?: { timeout?: number },
|
|
148
|
+
): Promise<ExposedToThreadType<Exposed>> {
|
|
149
|
+
debugSpawn('Initializing new thread')
|
|
150
|
+
|
|
151
|
+
const timeout = options && options.timeout ? options.timeout : initMessageTimeout
|
|
152
|
+
const initMessage = await withTimeout(
|
|
153
|
+
receiveInitMessage(worker),
|
|
154
|
+
timeout,
|
|
155
|
+
`Timeout: Did not receive an init message from worker after ${timeout}ms. Make sure the worker calls expose().`,
|
|
156
|
+
)
|
|
157
|
+
const exposed = initMessage.exposed
|
|
158
|
+
|
|
159
|
+
const { termination, terminate } = createTerminator(worker)
|
|
160
|
+
const events = createEventObservable(worker, termination)
|
|
161
|
+
|
|
162
|
+
if (exposed.type === 'function') {
|
|
163
|
+
const proxy = createProxyFunction(worker)
|
|
164
|
+
return setPrivateThreadProps(proxy, worker, events, terminate) as ExposedToThreadType<Exposed>
|
|
165
|
+
} else if (exposed.type === 'module') {
|
|
166
|
+
const proxy = createProxyModule(worker, exposed.methods)
|
|
167
|
+
return setPrivateThreadProps(proxy, worker, events, terminate) as ExposedToThreadType<Exposed>
|
|
168
|
+
} else {
|
|
169
|
+
const type = (exposed as WorkerInitMessage['exposed']).type
|
|
170
|
+
throw new Error(`Worker init message states unexpected type of expose(): ${type}`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
2
|
+
import type { Observable } from 'observable-fns'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
$errors, $events, $terminate,
|
|
6
|
+
} from '../symbols.ts'
|
|
7
|
+
import type { Thread as ThreadType, WorkerEvent } from '../types/master.ts'
|
|
8
|
+
|
|
9
|
+
function fail(message: string): never {
|
|
10
|
+
throw new Error(message)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Thread = ThreadType
|
|
14
|
+
|
|
15
|
+
/** Thread utility functions. Use them to manage or inspect a `spawn()`-ed thread. */
|
|
16
|
+
export const Thread = {
|
|
17
|
+
/** Return an observable that can be used to subscribe to all errors happening in the thread. */
|
|
18
|
+
errors<ThreadT extends ThreadType>(thread: ThreadT): Observable<Error> {
|
|
19
|
+
return thread[$errors] || fail('Error observable not found. Make sure to pass a thread instance as returned by the spawn() promise.')
|
|
20
|
+
},
|
|
21
|
+
/** Return an observable that can be used to subscribe to internal events happening in the thread. Useful for debugging. */
|
|
22
|
+
events<ThreadT extends ThreadType>(thread: ThreadT): Observable<WorkerEvent> {
|
|
23
|
+
return thread[$events] || fail('Events observable not found. Make sure to pass a thread instance as returned by the spawn() promise.')
|
|
24
|
+
},
|
|
25
|
+
/** Terminate a thread. Remember to terminate every thread when you are done using it. */
|
|
26
|
+
terminate<ThreadT extends ThreadType>(thread: ThreadT) {
|
|
27
|
+
return thread[$terminate]()
|
|
28
|
+
},
|
|
29
|
+
}
|