type WorkerOptions
} from './worker-options'
import type {
+ TaskAsyncFunction,
+ TaskFunction,
TaskFunctions,
- WorkerAsyncFunction,
- WorkerFunction,
- WorkerSyncFunction
-} from './worker-functions'
+ TaskSyncFunction
+} from './task-functions'
const DEFAULT_MAX_INACTIVE_TIME = 60000
const DEFAULT_KILL_BEHAVIOR: KillBehavior = KillBehaviors.SOFT
/**
* Task function(s) processed by the worker when the pool's `execution` function is invoked.
*/
- protected taskFunctions!: Map<string, WorkerFunction<Data, Response>>
+ protected taskFunctions!: Map<string, TaskFunction<Data, Response>>
/**
* Timestamp of the last task processed by this worker.
*/
public constructor (
type: string,
protected readonly isMain: boolean,
- protected readonly mainWorker: MainWorker,
- taskFunctions:
- | WorkerFunction<Data, Response>
- | TaskFunctions<Data, Response>,
+ private readonly mainWorker: MainWorker,
+ taskFunctions: TaskFunction<Data, Response> | TaskFunctions<Data, Response>,
protected readonly opts: WorkerOptions = {
/**
* The kill behavior option on this worker or its default value.
* The maximum time to keep this worker active while idle.
* The pool automatically checks and terminates this worker when the time expires.
*/
- maxInactiveTime: DEFAULT_MAX_INACTIVE_TIME
+ maxInactiveTime: DEFAULT_MAX_INACTIVE_TIME,
+ /**
+ * The function to call when the worker is killed.
+ */
+ killHandler: EMPTY_FUNCTION
}
) {
super(type)
this.opts.killBehavior = opts.killBehavior ?? DEFAULT_KILL_BEHAVIOR
this.opts.maxInactiveTime =
opts.maxInactiveTime ?? DEFAULT_MAX_INACTIVE_TIME
+ this.opts.killHandler = opts.killHandler ?? EMPTY_FUNCTION
delete this.opts.async
}
* @param taskFunctions - The task function(s) parameter that should be checked.
*/
private checkTaskFunctions (
- taskFunctions:
- | WorkerFunction<Data, Response>
- | TaskFunctions<Data, Response>
+ taskFunctions: TaskFunction<Data, Response> | TaskFunctions<Data, Response>
): void {
if (taskFunctions == null) {
throw new Error('taskFunctions parameter is mandatory')
}
- this.taskFunctions = new Map<string, WorkerFunction<Data, Response>>()
+ this.taskFunctions = new Map<string, TaskFunction<Data, Response>>()
if (typeof taskFunctions === 'function') {
const boundFn = taskFunctions.bind(this)
this.taskFunctions.set(DEFAULT_TASK_NAME, boundFn)
this.taskFunctions.set(
typeof taskFunctions.name === 'string' &&
- taskFunctions.name.trim().length > 0
+ taskFunctions.name.trim().length > 0
? taskFunctions.name
: 'fn1',
boundFn
'A taskFunctions parameter object key is not a string'
)
}
+ if (typeof name === 'string' && name.trim().length === 0) {
+ throw new TypeError(
+ 'A taskFunctions parameter object key an empty string'
+ )
+ }
if (typeof fn !== 'function') {
throw new TypeError(
'A taskFunctions parameter object value is not a function'
if (typeof name !== 'string') {
throw new TypeError('name parameter is not a string')
}
+ if (typeof name === 'string' && name.trim().length === 0) {
+ throw new TypeError('name parameter is an empty string')
+ }
return this.taskFunctions.has(name)
}
*/
public addTaskFunction (
name: string,
- fn: WorkerFunction<Data, Response>
+ fn: TaskFunction<Data, Response>
): boolean {
if (typeof name !== 'string') {
throw new TypeError('name parameter is not a string')
}
+ if (typeof name === 'string' && name.trim().length === 0) {
+ throw new TypeError('name parameter is an empty string')
+ }
if (name === DEFAULT_TASK_NAME) {
throw new Error(
'Cannot add a task function with the default reserved name'
this.taskFunctions.set(DEFAULT_TASK_NAME, boundFn)
}
this.taskFunctions.set(name, boundFn)
+ this.sendTaskFunctionsListToMainWorker()
return true
} catch {
return false
if (typeof name !== 'string') {
throw new TypeError('name parameter is not a string')
}
+ if (typeof name === 'string' && name.trim().length === 0) {
+ throw new TypeError('name parameter is an empty string')
+ }
if (name === DEFAULT_TASK_NAME) {
throw new Error(
'Cannot remove the task function with the default reserved name'
'Cannot remove the task function used as the default task function'
)
}
- return this.taskFunctions.delete(name)
+ const deleteStatus = this.taskFunctions.delete(name)
+ this.sendTaskFunctionsListToMainWorker()
+ return deleteStatus
}
/**
* @returns The names of the worker's task functions.
*/
public listTaskFunctions (): string[] {
- return Array.from(this.taskFunctions.keys())
+ return [...this.taskFunctions.keys()]
}
/**
if (typeof name !== 'string') {
throw new TypeError('name parameter is not a string')
}
+ if (typeof name === 'string' && name.trim().length === 0) {
+ throw new TypeError('name parameter is an empty string')
+ }
if (name === DEFAULT_TASK_NAME) {
throw new Error(
'Cannot set the default task function reserved name as the default task function'
try {
this.taskFunctions.set(
DEFAULT_TASK_NAME,
- this.taskFunctions.get(name) as WorkerFunction<Data, Response>
+ this.taskFunctions.get(name) as TaskFunction<Data, Response>
)
return true
} catch {
}
}
+ /**
+ * Handles the ready message sent by the main worker.
+ *
+ * @param message - The ready message.
+ */
+ protected abstract handleReadyMessage (message: MessageValue<Data>): void
+
/**
* Worker message listener.
*
* @param message - The received message.
*/
protected messageListener (message: MessageValue<Data>): void {
- if (message.workerId === this.id) {
- if (message.statistics != null) {
- // Statistics message received
- this.statistics = message.statistics
- } else if (message.checkActive != null) {
- // Check active message received
- !this.isMain && message.checkActive
- ? this.startCheckActive()
- : this.stopCheckActive()
- } else if (message.id != null && message.data != null) {
- // Task message received
- this.run(message)
- } else if (message.kill === true) {
- // Kill message received
- !this.isMain && this.stopCheckActive()
- this.emitDestroy()
- }
+ if (this.isMain) {
+ throw new Error('Cannot handle message to worker in main worker')
+ } else if (message.workerId == null) {
+ throw new Error('Message worker id is not set')
+ } else if (message.workerId != null && message.workerId !== this.id) {
+ throw new Error(
+ `Message worker id ${message.workerId} does not match the worker id ${this.id}`
+ )
+ }
+ if (message.statistics != null) {
+ // Statistics message received
+ this.statistics = message.statistics
+ } else if (message.checkActive != null) {
+ // Check active message received
+ message.checkActive ? this.startCheckActive() : this.stopCheckActive()
+ } else if (message.taskId != null && message.data != null) {
+ // Task message received
+ this.run(message)
+ } else if (message.kill === true) {
+ // Kill message received
+ this.handleKillMessage(message)
}
}
/**
- * Handles the ready message sent by the main worker.
+ * Handles a kill message sent by the main worker.
*
- * @param message - The ready message.
+ * @param message - The kill message.
*/
- protected abstract handleReadyMessage (message: MessageValue<Data>): void
+ protected handleKillMessage (message: MessageValue<Data>): void {
+ this.stopCheckActive()
+ if (isAsyncFunction(this.opts.killHandler)) {
+ (this.opts.killHandler?.() as Promise<void>)
+ .then(() => {
+ this.sendToMainWorker({ kill: 'success', workerId: this.id })
+ return null
+ })
+ .catch(() => {
+ this.sendToMainWorker({ kill: 'failure', workerId: this.id })
+ })
+ .finally(() => {
+ this.emitDestroy()
+ })
+ .catch(EMPTY_FUNCTION)
+ } else {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
+ this.opts.killHandler?.() as void
+ this.sendToMainWorker({ kill: 'success', workerId: this.id })
+ } catch {
+ this.sendToMainWorker({ kill: 'failure', workerId: this.id })
+ } finally {
+ this.emitDestroy()
+ }
+ }
+ }
/**
* Starts the worker check active interval.
this.activeInterval = setInterval(
this.checkActive.bind(this),
(this.opts.maxInactiveTime ?? DEFAULT_MAX_INACTIVE_TIME) / 2
- ).unref()
+ )
}
/**
}
/**
- * Sends a message to the main worker.
+ * Sends a message to main worker.
*
* @param message - The response message.
*/
message: MessageValue<Response, Data>
): void
+ /**
+ * Sends the list of task function names to the main worker.
+ */
+ protected sendTaskFunctionsListToMainWorker (): void {
+ this.sendToMainWorker({
+ taskFunctions: this.listTaskFunctions(),
+ workerId: this.id
+ })
+ }
+
/**
* Handles an error and convert it to a string so it can be sent back to the main worker.
*
* @throws {@link https://nodejs.org/api/errors.html#class-error} If the task function is not found.
*/
protected run (task: Task<Data>): void {
- if (this.isMain) {
- throw new Error('Cannot run a task in the main worker')
- }
const fn = this.getTaskFunction(task.name)
if (isAsyncFunction(fn)) {
this.runInAsyncScope(this.runAsync.bind(this), this, fn, task)
* @param task - Input data for the task function.
*/
protected runSync (
- fn: WorkerSyncFunction<Data, Response>,
+ fn: TaskSyncFunction<Data, Response>,
task: Task<Data>
): void {
+ const { name, taskId, data } = task
try {
- let taskPerformance = this.beginTaskPerformance(task.name)
- const res = fn(task.data)
+ let taskPerformance = this.beginTaskPerformance(name)
+ const res = fn(data)
taskPerformance = this.endTaskPerformance(taskPerformance)
this.sendToMainWorker({
data: res,
taskPerformance,
workerId: this.id,
- id: task.id
+ taskId
})
} catch (e) {
const errorMessage = this.handleError(e as Error | string)
this.sendToMainWorker({
taskError: {
- name: task.name ?? DEFAULT_TASK_NAME,
+ name: name ?? DEFAULT_TASK_NAME,
message: errorMessage,
- data: task.data
+ data
},
workerId: this.id,
- id: task.id
+ taskId
})
} finally {
this.updateLastTaskTimestamp()
* @param task - Input data for the task function.
*/
protected runAsync (
- fn: WorkerAsyncFunction<Data, Response>,
+ fn: TaskAsyncFunction<Data, Response>,
task: Task<Data>
): void {
- let taskPerformance = this.beginTaskPerformance(task.name)
- fn(task.data)
- .then(res => {
+ const { name, taskId, data } = task
+ let taskPerformance = this.beginTaskPerformance(name)
+ fn(data)
+ .then((res) => {
taskPerformance = this.endTaskPerformance(taskPerformance)
this.sendToMainWorker({
data: res,
taskPerformance,
workerId: this.id,
- id: task.id
+ taskId
})
return null
})
- .catch(e => {
+ .catch((e) => {
const errorMessage = this.handleError(e as Error | string)
this.sendToMainWorker({
taskError: {
- name: task.name ?? DEFAULT_TASK_NAME,
+ name: name ?? DEFAULT_TASK_NAME,
message: errorMessage,
- data: task.data
+ data
},
workerId: this.id,
- id: task.id
+ taskId
})
})
.finally(() => {
* @returns The task function.
* @throws {@link https://nodejs.org/api/errors.html#class-error} If the task function is not found.
*/
- private getTaskFunction (name?: string): WorkerFunction<Data, Response> {
+ private getTaskFunction (name?: string): TaskFunction<Data, Response> {
name = name ?? DEFAULT_TASK_NAME
const fn = this.taskFunctions.get(name)
if (fn == null) {
}
private updateLastTaskTimestamp (): void {
- if (!this.isMain && this.activeInterval != null) {
+ if (this.activeInterval != null) {
this.lastTaskTimestamp = performance.now()
}
}