// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
+import { randomUUID } from 'node:crypto'
import { EventEmitterAsyncResource } from 'node:events'
import { SHARE_ENV, Worker } from 'node:worker_threads'
} from './WorkerTypes.js'
import { randomizeDelay, sleep } from './WorkerUtils.js'
-export class WorkerSet extends WorkerAbstract<WorkerData> {
+interface ResponseWrapper<R extends WorkerData> {
+ resolve: (value: R | PromiseLike<R>) => void
+ reject: (reason?: unknown) => void
+ workerSetElement: WorkerSetElement
+}
+
+export class WorkerSet<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<D, R> {
public readonly emitter: EventEmitterAsyncResource | undefined
private readonly workerSet: Set<WorkerSetElement>
+ private readonly promiseResponseMap: Map<
+ `${string}-${string}-${string}-${string}`,
+ ResponseWrapper<R>
+ >
+
private started: boolean
private workerStartup: boolean
throw new RangeError('Elements per worker must be greater than zero')
}
this.workerSet = new Set<WorkerSetElement>()
+ this.promiseResponseMap = new Map<
+ `${string}-${string}-${string}-${string}`,
+ ResponseWrapper<R>
+ >()
if (this.workerOptions.poolOptions?.enableEvents === true) {
this.emitter = new EventEmitterAsyncResource({ name: 'workerset' })
}
version: workerSetVersion,
type: 'set',
worker: 'thread',
+ started: this.started,
size: this.size,
elementsExecuting: [...this.workerSet].reduce(
(accumulator, workerSetElement) => accumulator + workerSetElement.numberOfWorkerElements,
resolve()
})
})
+ worker.unref()
await worker.terminate()
await waitWorkerExit
- this.emitter?.emit(WorkerSetEvents.stopped, this.info)
- this.emitter?.emitDestroy()
- this.emitter?.removeAllListeners()
- this.started = false
}
+ this.emitter?.emit(WorkerSetEvents.stopped, this.info)
+ this.started = false
+ this.emitter?.emitDestroy()
}
/** @inheritDoc */
- public async addElement (elementData: WorkerData): Promise<void> {
+ public async addElement (elementData: D): Promise<R> {
if (!this.started) {
throw new Error('Cannot add a WorkerSet element: not started')
}
const workerSetElement = await this.getWorkerSetElement()
- workerSetElement.worker.postMessage({
- event: WorkerMessageEvents.startWorkerElement,
- data: elementData
+ const sendMessageToWorker = new Promise<R>((resolve, reject) => {
+ const message = {
+ uuid: randomUUID(),
+ event: WorkerMessageEvents.addWorkerElement,
+ data: elementData
+ } satisfies WorkerMessage<D>
+ workerSetElement.worker.postMessage(message)
+ this.promiseResponseMap.set(message.uuid, {
+ resolve,
+ reject,
+ workerSetElement
+ })
})
- ++workerSetElement.numberOfWorkerElements
+ const response = await sendMessageToWorker
// Add element sequentially to optimize memory at startup
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (this.workerOptions.elementStartDelay! > 0) {
+ if (this.workerOptions.elementAddDelay! > 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- await sleep(randomizeDelay(this.workerOptions.elementStartDelay!))
+ await sleep(randomizeDelay(this.workerOptions.elementAddDelay!))
}
+ return response
}
/**
...this.workerOptions.poolOptions?.workerOptions
})
worker.on('message', this.workerOptions.poolOptions?.messageHandler ?? EMPTY_FUNCTION)
- worker.on('message', (message: WorkerMessage<WorkerData>) => {
- if (message.event === WorkerMessageEvents.startedWorkerElement) {
- this.emitter?.emit(WorkerSetEvents.elementStarted, this.info)
- } else if (message.event === WorkerMessageEvents.startWorkerElementError) {
- this.emitter?.emit(WorkerSetEvents.elementError, message.data)
+ worker.on('message', (message: WorkerMessage<R>) => {
+ const { uuid, event, data } = message
+ if (this.promiseResponseMap.has(uuid)) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const { resolve, reject, workerSetElement } = this.promiseResponseMap.get(uuid)!
+ switch (event) {
+ case WorkerMessageEvents.addedWorkerElement:
+ this.emitter?.emit(WorkerSetEvents.elementAdded, this.info)
+ ++workerSetElement.numberOfWorkerElements
+ resolve(data)
+ break
+ case WorkerMessageEvents.workerElementError:
+ this.emitter?.emit(WorkerSetEvents.elementError, data)
+ reject(data)
+ break
+ default:
+ reject(
+ new Error(
+ `Unknown worker message event: '${event}' received with data: '${JSON.stringify(
+ data,
+ undefined,
+ 2
+ )}'`
+ )
+ )
+ }
+ this.promiseResponseMap.delete(uuid)
}
})
worker.on('error', this.workerOptions.poolOptions?.errorHandler ?? EMPTY_FUNCTION)
- worker.on('error', error => {
+ worker.once('error', error => {
this.emitter?.emit(WorkerSetEvents.error, error)
if (
this.workerOptions.poolOptions?.restartWorkerOnError === true &&
) {
this.addWorkerSetElement()
}
+ worker.unref()
+ worker.terminate().catch((error: unknown) => this.emitter?.emit(WorkerSetEvents.error, error))
})
worker.on('online', this.workerOptions.poolOptions?.onlineHandler ?? EMPTY_FUNCTION)
worker.on('exit', this.workerOptions.poolOptions?.exitHandler ?? EMPTY_FUNCTION)
worker.once('exit', () => {
this.removeWorkerSetElement(this.getWorkerSetElementByWorker(worker))
})
- const workerSetElement: WorkerSetElement = { worker, numberOfWorkerElements: 0 }
+ const workerSetElement: WorkerSetElement = {
+ worker,
+ numberOfWorkerElements: 0
+ }
this.workerSet.add(workerSetElement)
this.workerStartup = false
return workerSetElement