poolifier 2.4.3 → 2.4.5

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/README.md CHANGED
@@ -113,22 +113,22 @@ Instantiate your pool based on your needed :
113
113
 
114
114
  ```js
115
115
  'use strict'
116
- const { FixedThreadPool, DynamicThreadPool } = require('poolifier')
116
+ const { DynamicThreadPool, FixedThreadPool, PoolEvents } = require('poolifier')
117
117
 
118
118
  // a fixed worker-threads pool
119
119
  const pool = new FixedThreadPool(15,
120
120
  './yourWorker.js',
121
121
  { errorHandler: (e) => console.error(e), onlineHandler: () => console.log('worker is online') })
122
122
 
123
- pool.emitter.on('busy', () => console.log('Pool is busy'))
123
+ pool.emitter.on(PoolEvents.busy, () => console.log('Pool is busy'))
124
124
 
125
125
  // or a dynamic worker-threads pool
126
126
  const pool = new DynamicThreadPool(10, 100,
127
127
  './yourWorker.js',
128
128
  { errorHandler: (e) => console.error(e), onlineHandler: () => console.log('worker is online') })
129
129
 
130
- pool.emitter.on('full', () => console.log('Pool is full'))
131
- pool.emitter.on('busy', () => console.log('Pool is busy'))
130
+ pool.emitter.on(PoolEvents.full, () => console.log('Pool is full'))
131
+ pool.emitter.on(PoolEvents.busy, () => console.log('Pool is busy'))
132
132
 
133
133
  // the execute method signature is the same for both implementations,
134
134
  // so you can easy switch from one to another
@@ -174,7 +174,14 @@ Node versions >= 16.x are supported.
174
174
  `WorkerChoiceStrategies.WEIGHTED_ROUND_ROBIN` and `WorkerChoiceStrategies.FAIR_SHARE` strategies are targeted to heavy and long tasks
175
175
  Default: `WorkerChoiceStrategies.ROUND_ROBIN`
176
176
 
177
+ - `workerChoiceStrategyOptions` (optional) - The worker choice strategy options object to use in this pool.
178
+ Properties:
179
+
180
+ - `medRunTime` (optional) - Use the tasks median run time instead of the tasks average run time in worker choice strategies.
181
+ Default: { medRunTime: false }
182
+
177
183
  - `enableEvents` (optional) - Events emission enablement in this pool. Default: true
184
+ - `enableTasksQueue` (optional, experimental) - Tasks queue per worker enablement in this pool. Default: false
178
185
 
179
186
  ### `pool = new DynamicThreadPool/DynamicClusterPool(min, max, filePath, opts)`
180
187
 
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Array with a maximum length shifting items when full.
3
+ */
4
+ export declare class CircularArray<T> extends Array<T> {
5
+ size: number;
6
+ constructor(size?: number, ...items: T[]);
7
+ push(...items: T[]): number;
8
+ unshift(...items: T[]): number;
9
+ concat(...items: Array<T | ConcatArray<T>>): CircularArray<T>;
10
+ splice(start: number, deleteCount?: number, ...items: T[]): T[];
11
+ resize(size: number): void;
12
+ empty(): boolean;
13
+ full(): boolean;
14
+ private checkSize;
15
+ }
package/lib/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  export { DynamicClusterPool } from './pools/cluster/dynamic';
2
2
  export { FixedClusterPool } from './pools/cluster/fixed';
3
3
  export type { ClusterPoolOptions } from './pools/cluster/fixed';
4
- export type { IPool, PoolEmitter, PoolOptions } from './pools/pool';
5
- export type { ErrorHandler, ExitHandler, MessageHandler, OnlineHandler } from './pools/pool-worker';
4
+ export { PoolEvents } from './pools/pool';
5
+ export type { IPool, PoolEmitter, PoolOptions, PoolEvent } from './pools/pool';
6
+ export type { ErrorHandler, ExitHandler, MessageHandler, OnlineHandler } from './pools/worker';
6
7
  export { WorkerChoiceStrategies } from './pools/selection-strategies/selection-strategies-types';
7
- export type { WorkerChoiceStrategy } from './pools/selection-strategies/selection-strategies-types';
8
+ export type { WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './pools/selection-strategies/selection-strategies-types';
8
9
  export { DynamicThreadPool } from './pools/thread/dynamic';
9
10
  export { FixedThreadPool } from './pools/thread/fixed';
10
11
  export type { ThreadWorkerWithMessageChannel } from './pools/thread/fixed';
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e,r=require("node:cluster"),t=require("node:crypto"),s=require("node:events"),i=require("node:os"),o=require("node:worker_threads"),n=require("node:async_hooks");!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(e||(e={}));const a=Object.freeze((()=>{})),h=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class k extends s{}const u=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class c{pool;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1};constructor(r){this.pool=r,this.isDynamicPool=this.pool.type===e.DYNAMIC,this.choose.bind(this)}}class l extends c{requiredStatistics={runTime:!0,avgRunTime:!0};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workers.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(Date.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0);this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(this.pool.workers[e].tasksUsage.avgRunTime??0)})}}class p extends c{requiredStatistics={runTime:!0,avgRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workers.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class m extends c{reset(){return!0}choose(){const e=this.pool.findFreeWorkerKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workers.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class d extends c{nextWorkerId=0;reset(){return this.nextWorkerId=0,!0}choose(){const e=this.nextWorkerId;return this.nextWorkerId=this.nextWorkerId===this.pool.workers.length-1?0:this.nextWorkerId+1,e}remove(e){return this.nextWorkerId===e&&(0===this.pool.workers.length?this.nextWorkerId=0:this.nextWorkerId=this.nextWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.nextWorkerId),!0}}class w extends c{requiredStatistics={runTime:!0,avgRunTime:!0};currentWorkerId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e){super(e),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerId=this.currentWorkerId===this.pool.workers.length-1?0:this.currentWorkerId+1,this.setWorkerTaskRunTime(this.currentWorkerId,t,0)),e}remove(e){this.currentWorkerId===e&&(0===this.pool.workers.length?this.currentWorkerId=0:this.currentWorkerId=this.currentWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.currentWorkerId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workers.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.pool.workers[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of i.cpus()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/i.cpus().length)}}class g{createWorkerCallback;workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r,t=u.ROUND_ROBIN){this.createWorkerCallback=r,this.workerChoiceStrategyType=t,this.execute.bind(this),this.workerChoiceStrategies=new Map([[u.ROUND_ROBIN,new d(e)],[u.LESS_USED,new m(e)],[u.LESS_BUSY,new p(e)],[u.FAIR_SHARE,new l(e)],[u.WEIGHTED_ROUND_ROBIN,new w(e)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType===e?this.workerChoiceStrategies.get(e)?.reset():this.workerChoiceStrategyType=e}execute(){const e=this.workerChoiceStrategies.get(this.workerChoiceStrategyType);return e.isDynamicPool&&!e.pool.full&&-1===e.pool.findFreeWorkerKey()?this.createWorkerCallback():e.choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class T{numberOfWorkers;filePath;opts;workers=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorker.bind(this),this.internalExecute.bind(this),this.checkAndEmitFull.bind(this),this.checkAndEmitBusy.bind(this),this.sendToWorker.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new k),this.workerChoiceStrategyContext=new g(this,(()=>{const e=this.createAndSetupWorker();return this.registerWorkerMessageListener(e,(r=>{var t;t=h.HARD,(r.kill===t||0===this.getWorkerTasksUsage(e)?.running)&&this.destroyWorker(e)})),this.getWorkerKey(e)}),this.opts.workerChoiceStrategy)}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(r){if(null==r)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(r))throw new TypeError("Cannot instantiate a pool with a non integer number of workers");if(r<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===e.FIXED&&0===r)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){if(this.opts.workerChoiceStrategy=e.workerChoiceStrategy??u.ROUND_ROBIN,!Object.values(u).includes(this.opts.workerChoiceStrategy))throw new Error(`Invalid worker choice strategy '${this.opts.workerChoiceStrategy}'`);this.opts.enableEvents=e.enableEvents??!0}get numberOfRunningTasks(){return this.promiseResponseMap.size}getWorkerKey(e){return this.workers.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.opts.workerChoiceStrategy=e;for(const[e,r]of this.workers.entries())this.setWorker(e,r.worker,{run:0,running:0,runTime:0,avgRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return this.numberOfRunningTasks>=this.numberOfWorkers&&-1===this.findFreeWorkerKey()}findFreeWorkerKey(){return this.workers.findIndex((e=>0===e.tasksUsage.running))}async execute(e){const[r,s]=this.chooseWorker(),i=t.randomUUID(),o=this.internalExecute(r,s,i);return this.checkAndEmitFull(),this.checkAndEmitBusy(),this.sendToWorker(s,{data:e??{},id:i}),o}async destroy(){await Promise.all(this.workers.map((async e=>{await this.destroyWorker(e.worker)})))}setupHook(){}beforePromiseResponseHook(e){++this.workers[e].tasksUsage.running}afterPromiseResponseHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.taskRunTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run))}chooseWorker(){const e=this.workerChoiceStrategyContext.execute();return[e,this.workers[e].worker]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??a),e.on("error",this.opts.errorHandler??a),e.on("online",this.opts.onlineHandler??a),e.on("exit",this.opts.exitHandler??a),e.once("exit",(()=>{this.removeWorker(e)})),this.pushWorker(e,{run:0,running:0,runTime:0,avgRunTime:0,error:0}),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);null!=r&&(null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterPromiseResponseHook(r.worker,e),this.promiseResponseMap.delete(e.id))}}}async internalExecute(e,r,t){return this.beforePromiseResponseHook(e),await new Promise(((e,s)=>{this.promiseResponseMap.set(t,{resolve:e,reject:s,worker:r})}))}checkAndEmitBusy(){!0===this.opts.enableEvents&&this.busy&&this.emitter?.emit("busy")}checkAndEmitFull(){this.type===e.DYNAMIC&&!0===this.opts.enableEvents&&this.full&&this.emitter?.emit("full")}getWorkerTasksUsage(e){const r=this.getWorkerKey(e);if(-1!==r)return this.workers[r].tasksUsage;throw new Error("Worker could not be found in the pool")}pushWorker(e,r){this.workers.push({worker:e,tasksUsage:r})}setWorker(e,r,t){this.workers[e]={worker:r,tasksUsage:t}}removeWorker(e){const r=this.getWorkerKey(e);this.workers.splice(r,1),this.workerChoiceStrategyContext.remove(r)}}class W extends T{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){r.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return r.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return r.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workers.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class f extends T{constructor(e,r,t={}){super(e,r,t)}isMain(){return o.isMainThread}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o.Worker(this.filePath,{env:o.SHARE_ENV})}afterWorkerSetup(e){const{port1:r,port2:t}=new o.MessageChannel;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workers.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}const y=6e4,R=h.SOFT;class S extends n.AsyncResource{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:R,maxInactiveTime:y}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=Date.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??y)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??R,this.opts.maxInactiveTime=e.maxInactiveTime??y,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){Date.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??y)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=Date.now(),s=e(r.data),i=Date.now()-t;this.sendToMainWorker({data:s,id:r.id,taskRunTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=Date.now())}}runAsync(e,r){const t=Date.now();e(r.data).then((e=>{const s=Date.now()-t;return this.sendToMainWorker({data:e,id:r.id,taskRunTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=Date.now())})).catch(a)}}exports.ClusterWorker=class extends S{constructor(e,t={}){super("worker-cluster-pool:poolifier",r.isPrimary,e,r.worker,t)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}},exports.DynamicClusterPool=class extends W{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workers.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerKey()}},exports.DynamicThreadPool=class extends f{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workers.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerKey()}},exports.FixedClusterPool=W,exports.FixedThreadPool=f,exports.KillBehaviors=h,exports.ThreadWorker=class extends S{constructor(e,r={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=u;
1
+ "use strict";var e,r=require("node:cluster"),t=require("node:crypto"),s=require("node:events"),i=require("node:os"),o=require("node:worker_threads"),n=require("node:async_hooks");!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(e||(e={}));const a=Object.freeze((()=>{})),h=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class u extends s{}const k=Object.freeze({full:"full",busy:"busy"}),c=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class l{pool;opts;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(r,t={medRunTime:!1}){this.pool=r,this.opts=t,this.checkOptions(),this.isDynamicPool=this.pool.type===e.DYNAMIC,this.choose.bind(this)}checkOptions(){this.requiredStatistics.avgRunTime&&!0===this.opts.medRunTime&&(this.requiredStatistics.medRunTime=!0)}}class d extends l{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workerNodes.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(performance.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),t=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(t??0)})}}class p extends l{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class m extends l{reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class g extends l{nextWorkerNodeId=0;reset(){return this.nextWorkerNodeId=0,!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId=this.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class T extends l{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e,r){super(e,r),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerNodeId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerNodeId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.setWorkerTaskRunTime(this.currentWorkerNodeId,t,0)),e}remove(e){this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workerNodes.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of i.cpus()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/i.cpus().length)}}class w{workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r=c.ROUND_ROBIN,t={medRunTime:!1}){this.workerChoiceStrategyType=r,this.execute.bind(this),this.workerChoiceStrategies=new Map([[c.ROUND_ROBIN,new g(e,t)],[c.LESS_USED,new m(e,t)],[c.LESS_BUSY,new p(e,t)],[c.FAIR_SHARE,new d(e,t)],[c.WEIGHTED_ROUND_ROBIN,new T(e,t)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType!==e&&(this.workerChoiceStrategyType=e),this.workerChoiceStrategies.get(this.workerChoiceStrategyType)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class W extends Array{size;constructor(e=1024,...r){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...r)}push(...e){const r=super.push(...e);return r>this.size&&super.splice(0,r-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const r=super.concat(e);return r.size=this.size,r.length>r.size&&r.splice(0,r.length-r.size),r}splice(e,r,...t){let s;return arguments.length>=3&&void 0!==r?(s=super.splice(e,r),this.push(...t)):s=2===arguments.length?super.splice(e,r):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let r=e;r<this.size;r++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class f{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorkerNode.bind(this),this.internalExecute.bind(this),this.checkAndEmitEvents.bind(this),this.sendToWorker.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new u),this.workerChoiceStrategyContext=new w(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions)}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(r){if(null==r)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(r))throw new TypeError("Cannot instantiate a pool with a non integer number of workers");if(r<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===e.FIXED&&0===r)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??c.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??{medRunTime:!1},this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1}checkValidWorkerChoiceStrategy(e){if(!Object.values(c).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}get numberOfRunningTasks(){return this.workerNodes.reduce(((e,r)=>e+r.tasksUsage.running),0)}get numberOfQueuedTasks(){return!1===this.opts.enableTasksQueue?0:this.workerNodes.reduce(((e,r)=>e+r.tasksQueue.length),0)}getWorkerNodeKey(e){return this.workerNodes.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e;for(const[e,r]of this.workerNodes.entries())this.setWorkerNode(e,r.worker,{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,error:0},r.tasksQueue);this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return this.numberOfRunningTasks>=this.numberOfWorkers&&-1===this.findFreeWorkerNodeKey()}findFreeWorkerNodeKey(){return this.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[r,s]=this.chooseWorkerNode(),i={data:e??{},id:t.randomUUID()},o=this.internalExecute(r,s,i);let n=i;return!0===this.opts.enableTasksQueue&&(this.busy||this.tasksQueueSize(r)>0)&&(n=this.enqueueDequeueTask(r,i)),this.sendToWorker(s.worker,n),this.checkAndEmitEvents(),o}async destroy(){await Promise.all(this.workerNodes.map((async e=>{this.flushTasksQueueByWorker(e.worker),await this.destroyWorker(e.worker)})))}setupHook(){}beforePromiseResponseHook(e){++this.workerNodes[e].tasksUsage.running}afterPromiseResponseHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(t.runTimeHistory.push(r.runTime??0),t.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const r=e.slice().sort(((e,r)=>e-r)),t=Math.floor(r.length/2);return r.length%2==0?r[t/2]:(r[t-1]+r[t])/2})(t.runTimeHistory)))}chooseWorkerNode(){let r;if(this.type!==e.DYNAMIC||this.full||-1!==this.findFreeWorkerNodeKey())r=this.workerChoiceStrategyContext.execute();else{const e=this.createAndSetupWorker();this.registerWorkerMessageListener(e,(r=>{var t;t=h.HARD,(r.kill===t||null!=r.kill&&0===this.getWorkerTasksUsage(e)?.running)&&(this.flushTasksQueueByWorker(e),this.destroyWorker(e))})),r=this.getWorkerNodeKey(e)}return[r,this.workerNodes[r]]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??a),e.on("error",this.opts.errorHandler??a),e.on("online",this.opts.onlineHandler??a),e.on("exit",this.opts.exitHandler??a),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);if(null!=r){null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterPromiseResponseHook(r.worker,e),this.promiseResponseMap.delete(e.id);const t=this.getWorkerNodeKey(r.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(t)>0&&this.sendToWorker(r.worker,this.dequeueTask(t))}}}}async internalExecute(e,r,t){return this.beforePromiseResponseHook(e),await new Promise(((e,s)=>{this.promiseResponseMap.set(t.id,{resolve:e,reject:s,worker:r.worker})}))}checkAndEmitEvents(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(k.busy),this.type===e.DYNAMIC&&this.full&&this.emitter?.emit(k.full))}getWorkerTasksUsage(e){const r=this.getWorkerNodeKey(e);if(-1!==r)return this.workerNodes[r].tasksUsage;throw new Error("Worker could not be found in the pool worker nodes")}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new W,avgRunTime:0,medRunTime:0,error:0},tasksQueue:[]})}setWorkerNode(e,r,t,s){this.workerNodes[e]={worker:r,tasksUsage:t,tasksQueue:s}}removeWorkerNode(e){const r=this.getWorkerNodeKey(e);this.workerNodes.splice(r,1),this.workerChoiceStrategyContext.remove(r)}enqueueDequeueTask(e,r){return this.enqueueTask(e,r),this.dequeueTask(e)}enqueueTask(e,r){this.workerNodes[e].tasksQueue.push(r)}dequeueTask(e){return this.workerNodes[e].tasksQueue.shift()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.length}flushTasksQueue(e){if(this.tasksQueueSize(e)>0){for(const r of this.workerNodes[e].tasksQueue)this.sendToWorker(this.workerNodes[e].worker,r);this.workerNodes[e].tasksQueue=[]}}flushTasksQueueByWorker(e){const r=this.getWorkerNodeKey(e);this.flushTasksQueue(r)}}class y extends f{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){r.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return r.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return r.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class N extends f{constructor(e,r,t={}){super(e,r,t)}isMain(){return o.isMainThread}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o.Worker(this.filePath,{env:o.SHARE_ENV})}afterWorkerSetup(e){const{port1:r,port2:t}=new o.MessageChannel;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return e.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}const S=6e4,R=h.SOFT;class x extends n.AsyncResource{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:R,maxInactiveTime:S}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??S)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??R,this.opts.maxInactiveTime=e.maxInactiveTime??S,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??S)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=performance.now(),s=e(r.data),i=performance.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,r){const t=performance.now();e(r.data).then((e=>{const s=performance.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(a)}}exports.ClusterWorker=class extends x{constructor(e,t={}){super("worker-cluster-pool:poolifier",r.isPrimary,e,r.worker,t)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}},exports.DynamicClusterPool=class extends y{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerNodeKey()}},exports.DynamicThreadPool=class extends N{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return e.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerNodeKey()}},exports.FixedClusterPool=y,exports.FixedThreadPool=N,exports.KillBehaviors=h,exports.PoolEvents=k,exports.ThreadWorker=class extends x{constructor(e,r={}){super("worker-thread-pool:poolifier",o.isMainThread,e,o.parentPort,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}},exports.WorkerChoiceStrategies=c;
package/lib/index.mjs CHANGED
@@ -1 +1 @@
1
- import e from"node:cluster";import r from"node:crypto";import t from"node:events";import{cpus as s}from"node:os";import{isMainThread as i,Worker as o,SHARE_ENV as n,MessageChannel as a,parentPort as h}from"node:worker_threads";import{AsyncResource as k}from"node:async_hooks";var u;!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(u||(u={}));const c=Object.freeze((()=>{})),l=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class m extends t{}const p=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class w{pool;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1};constructor(e){this.pool=e,this.isDynamicPool=this.pool.type===u.DYNAMIC,this.choose.bind(this)}}class d extends w{requiredStatistics={runTime:!0,avgRunTime:!0};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workers.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(Date.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0);this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(this.pool.workers[e].tasksUsage.avgRunTime??0)})}}class g extends w{requiredStatistics={runTime:!0,avgRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workers.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class T extends w{reset(){return!0}choose(){const e=this.pool.findFreeWorkerKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workers.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class W extends w{nextWorkerId=0;reset(){return this.nextWorkerId=0,!0}choose(){const e=this.nextWorkerId;return this.nextWorkerId=this.nextWorkerId===this.pool.workers.length-1?0:this.nextWorkerId+1,e}remove(e){return this.nextWorkerId===e&&(0===this.pool.workers.length?this.nextWorkerId=0:this.nextWorkerId=this.nextWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.nextWorkerId),!0}}class f extends w{requiredStatistics={runTime:!0,avgRunTime:!0};currentWorkerId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e){super(e),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerId=this.currentWorkerId===this.pool.workers.length-1?0:this.currentWorkerId+1,this.setWorkerTaskRunTime(this.currentWorkerId,t,0)),e}remove(e){this.currentWorkerId===e&&(0===this.pool.workers.length?this.currentWorkerId=0:this.currentWorkerId=this.currentWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.currentWorkerId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workers.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.pool.workers[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of s()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/s().length)}}class y{createWorkerCallback;workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r,t=p.ROUND_ROBIN){this.createWorkerCallback=r,this.workerChoiceStrategyType=t,this.execute.bind(this),this.workerChoiceStrategies=new Map([[p.ROUND_ROBIN,new W(e)],[p.LESS_USED,new T(e)],[p.LESS_BUSY,new g(e)],[p.FAIR_SHARE,new d(e)],[p.WEIGHTED_ROUND_ROBIN,new f(e)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType===e?this.workerChoiceStrategies.get(e)?.reset():this.workerChoiceStrategyType=e}execute(){const e=this.workerChoiceStrategies.get(this.workerChoiceStrategyType);return e.isDynamicPool&&!e.pool.full&&-1===e.pool.findFreeWorkerKey()?this.createWorkerCallback():e.choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class R{numberOfWorkers;filePath;opts;workers=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorker.bind(this),this.internalExecute.bind(this),this.checkAndEmitFull.bind(this),this.checkAndEmitBusy.bind(this),this.sendToWorker.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new m),this.workerChoiceStrategyContext=new y(this,(()=>{const e=this.createAndSetupWorker();return this.registerWorkerMessageListener(e,(r=>{var t;t=l.HARD,(r.kill===t||0===this.getWorkerTasksUsage(e)?.running)&&this.destroyWorker(e)})),this.getWorkerKey(e)}),this.opts.workerChoiceStrategy)}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(e){if(null==e)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(e))throw new TypeError("Cannot instantiate a pool with a non integer number of workers");if(e<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===u.FIXED&&0===e)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){if(this.opts.workerChoiceStrategy=e.workerChoiceStrategy??p.ROUND_ROBIN,!Object.values(p).includes(this.opts.workerChoiceStrategy))throw new Error(`Invalid worker choice strategy '${this.opts.workerChoiceStrategy}'`);this.opts.enableEvents=e.enableEvents??!0}get numberOfRunningTasks(){return this.promiseResponseMap.size}getWorkerKey(e){return this.workers.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.opts.workerChoiceStrategy=e;for(const[e,r]of this.workers.entries())this.setWorker(e,r.worker,{run:0,running:0,runTime:0,avgRunTime:0,error:0});this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return this.numberOfRunningTasks>=this.numberOfWorkers&&-1===this.findFreeWorkerKey()}findFreeWorkerKey(){return this.workers.findIndex((e=>0===e.tasksUsage.running))}async execute(e){const[t,s]=this.chooseWorker(),i=r.randomUUID(),o=this.internalExecute(t,s,i);return this.checkAndEmitFull(),this.checkAndEmitBusy(),this.sendToWorker(s,{data:e??{},id:i}),o}async destroy(){await Promise.all(this.workers.map((async e=>{await this.destroyWorker(e.worker)})))}setupHook(){}beforePromiseResponseHook(e){++this.workers[e].tasksUsage.running}afterPromiseResponseHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.taskRunTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run))}chooseWorker(){const e=this.workerChoiceStrategyContext.execute();return[e,this.workers[e].worker]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??c),e.on("error",this.opts.errorHandler??c),e.on("online",this.opts.onlineHandler??c),e.on("exit",this.opts.exitHandler??c),e.once("exit",(()=>{this.removeWorker(e)})),this.pushWorker(e,{run:0,running:0,runTime:0,avgRunTime:0,error:0}),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);null!=r&&(null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterPromiseResponseHook(r.worker,e),this.promiseResponseMap.delete(e.id))}}}async internalExecute(e,r,t){return this.beforePromiseResponseHook(e),await new Promise(((e,s)=>{this.promiseResponseMap.set(t,{resolve:e,reject:s,worker:r})}))}checkAndEmitBusy(){!0===this.opts.enableEvents&&this.busy&&this.emitter?.emit("busy")}checkAndEmitFull(){this.type===u.DYNAMIC&&!0===this.opts.enableEvents&&this.full&&this.emitter?.emit("full")}getWorkerTasksUsage(e){const r=this.getWorkerKey(e);if(-1!==r)return this.workers[r].tasksUsage;throw new Error("Worker could not be found in the pool")}pushWorker(e,r){this.workers.push({worker:e,tasksUsage:r})}setWorker(e,r,t){this.workers[e]={worker:r,tasksUsage:t}}removeWorker(e){const r=this.getWorkerKey(e);this.workers.splice(r,1),this.workerChoiceStrategyContext.remove(r)}}class S extends R{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){e.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return e.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return e.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return u.FIXED}get full(){return this.workers.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class I extends S{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return u.DYNAMIC}get full(){return this.workers.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerKey()}}class x extends R{constructor(e,r,t={}){super(e,r,t)}isMain(){return i}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o(this.filePath,{env:n})}afterWorkerSetup(e){const{port1:r,port2:t}=new a;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return u.FIXED}get full(){return this.workers.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class v extends x{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return u.DYNAMIC}get full(){return this.workers.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerKey()}}const E=6e4,C=l.SOFT;class b extends k{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:C,maxInactiveTime:E}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=Date.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??E)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??C,this.opts.maxInactiveTime=e.maxInactiveTime??E,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){Date.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??E)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=Date.now(),s=e(r.data),i=Date.now()-t;this.sendToMainWorker({data:s,id:r.id,taskRunTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=Date.now())}}runAsync(e,r){const t=Date.now();e(r.data).then((e=>{const s=Date.now()-t;return this.sendToMainWorker({data:e,id:r.id,taskRunTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=Date.now())})).catch(c)}}class M extends b{constructor(r,t={}){super("worker-cluster-pool:poolifier",e.isPrimary,r,e.worker,t)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class D extends b{constructor(e,r={}){super("worker-thread-pool:poolifier",i,e,h,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{M as ClusterWorker,I as DynamicClusterPool,v as DynamicThreadPool,S as FixedClusterPool,x as FixedThreadPool,l as KillBehaviors,D as ThreadWorker,p as WorkerChoiceStrategies};
1
+ import e from"node:cluster";import r from"node:crypto";import t from"node:events";import{cpus as s}from"node:os";import{isMainThread as i,Worker as o,SHARE_ENV as n,MessageChannel as h,parentPort as a}from"node:worker_threads";import{AsyncResource as u}from"node:async_hooks";var k;!function(e){e.FIXED="fixed",e.DYNAMIC="dynamic"}(k||(k={}));const c=Object.freeze((()=>{})),l=Object.freeze({SOFT:"SOFT",HARD:"HARD"});class d extends t{}const m=Object.freeze({full:"full",busy:"busy"}),p=Object.freeze({ROUND_ROBIN:"ROUND_ROBIN",LESS_USED:"LESS_USED",LESS_BUSY:"LESS_BUSY",FAIR_SHARE:"FAIR_SHARE",WEIGHTED_ROUND_ROBIN:"WEIGHTED_ROUND_ROBIN"});class g{pool;opts;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1,medRunTime:!1};constructor(e,r={medRunTime:!1}){this.pool=e,this.opts=r,this.checkOptions(),this.isDynamicPool=this.pool.type===k.DYNAMIC,this.choose.bind(this)}checkOptions(){this.requiredStatistics.avgRunTime&&!0===this.opts.medRunTime&&(this.requiredStatistics.medRunTime=!0)}}class w extends g{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};workerLastVirtualTaskTimestamp=new Map;reset(){return this.workerLastVirtualTaskTimestamp.clear(),!0}choose(){let e,r=1/0;for(const[t]of this.pool.workerNodes.entries()){this.computeWorkerLastVirtualTaskTimestamp(t);const s=this.workerLastVirtualTaskTimestamp.get(t)?.end??0;s<r&&(r=s,e=t)}return e}remove(e){const r=this.workerLastVirtualTaskTimestamp.delete(e);for(const[r,t]of this.workerLastVirtualTaskTimestamp.entries())r>e&&this.workerLastVirtualTaskTimestamp.set(r-1,t);return r}computeWorkerLastVirtualTaskTimestamp(e){const r=Math.max(performance.now(),this.workerLastVirtualTaskTimestamp.get(e)?.end??-1/0),t=this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime;this.workerLastVirtualTaskTimestamp.set(e,{start:r,end:r+(t??0)})}}class T extends g{requiredStatistics={runTime:!0,avgRunTime:!1,medRunTime:!1};reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage.runTime;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class f extends g{reset(){return!0}choose(){const e=this.pool.findFreeWorkerNodeKey();if(-1!==e)return e;let r,t=1/0;for(const[e,s]of this.pool.workerNodes.entries()){const i=s.tasksUsage,o=i.run+i.running;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class W extends g{nextWorkerNodeId=0;reset(){return this.nextWorkerNodeId=0,!0}choose(){const e=this.nextWorkerNodeId;return this.nextWorkerNodeId=this.nextWorkerNodeId===this.pool.workerNodes.length-1?0:this.nextWorkerNodeId+1,e}remove(e){return this.nextWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.nextWorkerNodeId=0:this.nextWorkerNodeId=this.nextWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.nextWorkerNodeId),!0}}class y extends g{requiredStatistics={runTime:!0,avgRunTime:!0,medRunTime:!1};currentWorkerNodeId=0;defaultWorkerWeight;workersTaskRunTime=new Map;constructor(e,r){super(e,r),this.defaultWorkerWeight=this.computeWorkerWeight(),this.initWorkersTaskRunTime()}reset(){return this.currentWorkerNodeId=0,this.workersTaskRunTime.clear(),this.initWorkersTaskRunTime(),!0}choose(){const e=this.currentWorkerNodeId;this.isDynamicPool&&!this.workersTaskRunTime.has(e)&&this.initWorkerTaskRunTime(e);const r=this.workersTaskRunTime.get(e)?.runTime??0,t=this.workersTaskRunTime.get(e)?.weight??this.defaultWorkerWeight;return r<t?this.setWorkerTaskRunTime(e,t,r+(this.getWorkerVirtualTaskRunTime(e)??0)):(this.currentWorkerNodeId=this.currentWorkerNodeId===this.pool.workerNodes.length-1?0:this.currentWorkerNodeId+1,this.setWorkerTaskRunTime(this.currentWorkerNodeId,t,0)),e}remove(e){this.currentWorkerNodeId===e&&(0===this.pool.workerNodes.length?this.currentWorkerNodeId=0:this.currentWorkerNodeId=this.currentWorkerNodeId>this.pool.workerNodes.length-1?this.pool.workerNodes.length-1:this.currentWorkerNodeId);const r=this.workersTaskRunTime.delete(e);for(const[r,t]of this.workersTaskRunTime)r>e&&this.workersTaskRunTime.set(r-1,t);return r}initWorkersTaskRunTime(){for(const[e]of this.pool.workerNodes.entries())this.initWorkerTaskRunTime(e)}initWorkerTaskRunTime(e){this.setWorkerTaskRunTime(e,this.defaultWorkerWeight,0)}setWorkerTaskRunTime(e,r,t){this.workersTaskRunTime.set(e,{weight:r,runTime:t})}getWorkerVirtualTaskRunTime(e){return this.requiredStatistics.medRunTime?this.pool.workerNodes[e].tasksUsage.medRunTime:this.pool.workerNodes[e].tasksUsage.avgRunTime}computeWorkerWeight(){let e=0;for(const r of s()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/s().length)}}class N{workerChoiceStrategyType;workerChoiceStrategies;constructor(e,r=p.ROUND_ROBIN,t={medRunTime:!1}){this.workerChoiceStrategyType=r,this.execute.bind(this),this.workerChoiceStrategies=new Map([[p.ROUND_ROBIN,new W(e,t)],[p.LESS_USED,new f(e,t)],[p.LESS_BUSY,new T(e,t)],[p.FAIR_SHARE,new w(e,t)],[p.WEIGHTED_ROUND_ROBIN,new y(e,t)]])}getRequiredStatistics(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategyType!==e&&(this.workerChoiceStrategyType=e),this.workerChoiceStrategies.get(this.workerChoiceStrategyType)?.reset()}execute(){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).choose()}remove(e){return this.workerChoiceStrategies.get(this.workerChoiceStrategyType).remove(e)}}class S extends Array{size;constructor(e=1024,...r){super(),this.checkSize(e),this.size=e,arguments.length>1&&this.push(...r)}push(...e){const r=super.push(...e);return r>this.size&&super.splice(0,r-this.size),this.length}unshift(...e){return super.unshift(...e)>this.size&&super.splice(this.size,e.length),this.length}concat(...e){const r=super.concat(e);return r.size=this.size,r.length>r.size&&r.splice(0,r.length-r.size),r}splice(e,r,...t){let s;return arguments.length>=3&&void 0!==r?(s=super.splice(e,r),this.push(...t)):s=2===arguments.length?super.splice(e,r):super.splice(e),s}resize(e){if(this.checkSize(e),0===e)this.length=0;else if(e<this.size)for(let r=e;r<this.size;r++)super.pop();this.size=e}empty(){return 0===this.length}full(){return this.length===this.size}checkSize(e){if(!Number.isSafeInteger(e))throw new TypeError(`Invalid circular array size: ${e} is not a safe integer`);if(e<0)throw new RangeError(`Invalid circular array size: ${e} < 0`)}}class R{numberOfWorkers;filePath;opts;workerNodes=[];emitter;promiseResponseMap=new Map;workerChoiceStrategyContext;constructor(e,r,t){if(this.numberOfWorkers=e,this.filePath=r,this.opts=t,!this.isMain())throw new Error("Cannot start a pool from a worker!");this.checkNumberOfWorkers(this.numberOfWorkers),this.checkFilePath(this.filePath),this.checkPoolOptions(this.opts),this.chooseWorkerNode.bind(this),this.internalExecute.bind(this),this.checkAndEmitEvents.bind(this),this.sendToWorker.bind(this),this.setupHook();for(let e=1;e<=this.numberOfWorkers;e++)this.createAndSetupWorker();!0===this.opts.enableEvents&&(this.emitter=new d),this.workerChoiceStrategyContext=new N(this,this.opts.workerChoiceStrategy,this.opts.workerChoiceStrategyOptions)}checkFilePath(e){if(null==e||"string"==typeof e&&0===e.trim().length)throw new Error("Please specify a file with a worker implementation")}checkNumberOfWorkers(e){if(null==e)throw new Error("Cannot instantiate a pool without specifying the number of workers");if(!Number.isSafeInteger(e))throw new TypeError("Cannot instantiate a pool with a non integer number of workers");if(e<0)throw new RangeError("Cannot instantiate a pool with a negative number of workers");if(this.type===k.FIXED&&0===e)throw new Error("Cannot instantiate a fixed pool with no worker")}checkPoolOptions(e){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??p.ROUND_ROBIN,this.checkValidWorkerChoiceStrategy(this.opts.workerChoiceStrategy),this.opts.workerChoiceStrategyOptions=e.workerChoiceStrategyOptions??{medRunTime:!1},this.opts.enableEvents=e.enableEvents??!0,this.opts.enableTasksQueue=e.enableTasksQueue??!1}checkValidWorkerChoiceStrategy(e){if(!Object.values(p).includes(e))throw new Error(`Invalid worker choice strategy '${e}'`)}get numberOfRunningTasks(){return this.workerNodes.reduce(((e,r)=>e+r.tasksUsage.running),0)}get numberOfQueuedTasks(){return!1===this.opts.enableTasksQueue?0:this.workerNodes.reduce(((e,r)=>e+r.tasksQueue.length),0)}getWorkerNodeKey(e){return this.workerNodes.findIndex((r=>r.worker===e))}setWorkerChoiceStrategy(e){this.checkValidWorkerChoiceStrategy(e),this.opts.workerChoiceStrategy=e;for(const[e,r]of this.workerNodes.entries())this.setWorkerNode(e,r.worker,{run:0,running:0,runTime:0,runTimeHistory:new S,avgRunTime:0,medRunTime:0,error:0},r.tasksQueue);this.workerChoiceStrategyContext.setWorkerChoiceStrategy(e)}internalBusy(){return this.numberOfRunningTasks>=this.numberOfWorkers&&-1===this.findFreeWorkerNodeKey()}findFreeWorkerNodeKey(){return this.workerNodes.findIndex((e=>0===e.tasksUsage?.running))}async execute(e){const[t,s]=this.chooseWorkerNode(),i={data:e??{},id:r.randomUUID()},o=this.internalExecute(t,s,i);let n=i;return!0===this.opts.enableTasksQueue&&(this.busy||this.tasksQueueSize(t)>0)&&(n=this.enqueueDequeueTask(t,i)),this.sendToWorker(s.worker,n),this.checkAndEmitEvents(),o}async destroy(){await Promise.all(this.workerNodes.map((async e=>{this.flushTasksQueueByWorker(e.worker),await this.destroyWorker(e.worker)})))}setupHook(){}beforePromiseResponseHook(e){++this.workerNodes[e].tasksUsage.running}afterPromiseResponseHook(e,r){const t=this.getWorkerTasksUsage(e);--t.running,++t.run,null!=r.error&&++t.error,this.workerChoiceStrategyContext.getRequiredStatistics().runTime&&(t.runTime+=r.runTime??0,this.workerChoiceStrategyContext.getRequiredStatistics().avgRunTime&&0!==t.run&&(t.avgRunTime=t.runTime/t.run),this.workerChoiceStrategyContext.getRequiredStatistics().medRunTime&&(t.runTimeHistory.push(r.runTime??0),t.medRunTime=(e=>{if(Array.isArray(e)&&1===e.length)return e[0];const r=e.slice().sort(((e,r)=>e-r)),t=Math.floor(r.length/2);return r.length%2==0?r[t/2]:(r[t-1]+r[t])/2})(t.runTimeHistory)))}chooseWorkerNode(){let e;if(this.type!==k.DYNAMIC||this.full||-1!==this.findFreeWorkerNodeKey())e=this.workerChoiceStrategyContext.execute();else{const r=this.createAndSetupWorker();this.registerWorkerMessageListener(r,(e=>{var t;t=l.HARD,(e.kill===t||null!=e.kill&&0===this.getWorkerTasksUsage(r)?.running)&&(this.flushTasksQueueByWorker(r),this.destroyWorker(r))})),e=this.getWorkerNodeKey(r)}return[e,this.workerNodes[e]]}createAndSetupWorker(){const e=this.createWorker();return e.on("message",this.opts.messageHandler??c),e.on("error",this.opts.errorHandler??c),e.on("online",this.opts.onlineHandler??c),e.on("exit",this.opts.exitHandler??c),e.once("exit",(()=>{this.removeWorkerNode(e)})),this.pushWorkerNode(e),this.afterWorkerSetup(e),e}workerListener(){return e=>{if(null!=e.id){const r=this.promiseResponseMap.get(e.id);if(null!=r){null!=e.error?r.reject(e.error):r.resolve(e.data),this.afterPromiseResponseHook(r.worker,e),this.promiseResponseMap.delete(e.id);const t=this.getWorkerNodeKey(r.worker);!0===this.opts.enableTasksQueue&&this.tasksQueueSize(t)>0&&this.sendToWorker(r.worker,this.dequeueTask(t))}}}}async internalExecute(e,r,t){return this.beforePromiseResponseHook(e),await new Promise(((e,s)=>{this.promiseResponseMap.set(t.id,{resolve:e,reject:s,worker:r.worker})}))}checkAndEmitEvents(){!0===this.opts.enableEvents&&(this.busy&&this.emitter?.emit(m.busy),this.type===k.DYNAMIC&&this.full&&this.emitter?.emit(m.full))}getWorkerTasksUsage(e){const r=this.getWorkerNodeKey(e);if(-1!==r)return this.workerNodes[r].tasksUsage;throw new Error("Worker could not be found in the pool worker nodes")}pushWorkerNode(e){return this.workerNodes.push({worker:e,tasksUsage:{run:0,running:0,runTime:0,runTimeHistory:new S,avgRunTime:0,medRunTime:0,error:0},tasksQueue:[]})}setWorkerNode(e,r,t,s){this.workerNodes[e]={worker:r,tasksUsage:t,tasksQueue:s}}removeWorkerNode(e){const r=this.getWorkerNodeKey(e);this.workerNodes.splice(r,1),this.workerChoiceStrategyContext.remove(r)}enqueueDequeueTask(e,r){return this.enqueueTask(e,r),this.dequeueTask(e)}enqueueTask(e,r){this.workerNodes[e].tasksQueue.push(r)}dequeueTask(e){return this.workerNodes[e].tasksQueue.shift()}tasksQueueSize(e){return this.workerNodes[e].tasksQueue.length}flushTasksQueue(e){if(this.tasksQueueSize(e)>0){for(const r of this.workerNodes[e].tasksQueue)this.sendToWorker(this.workerNodes[e].worker,r);this.workerNodes[e].tasksQueue=[]}}flushTasksQueueByWorker(e){const r=this.getWorkerNodeKey(e);this.flushTasksQueue(r)}}class I extends R{opts;constructor(e,r,t={}){super(e,r,t),this.opts=t}setupHook(){e.setupPrimary({...this.opts.settings,exec:this.filePath})}isMain(){return e.isPrimary}destroyWorker(e){this.sendToWorker(e,{kill:1}),e.kill()}sendToWorker(e,r){e.send(r)}registerWorkerMessageListener(e,r){e.on("message",r)}createWorker(){return e.fork(this.opts.env)}afterWorkerSetup(e){this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class v extends I{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerNodeKey()}}class x extends R{constructor(e,r,t={}){super(e,r,t)}isMain(){return i}async destroyWorker(e){this.sendToWorker(e,{kill:1}),await e.terminate()}sendToWorker(e,r){e.postMessage(r)}registerWorkerMessageListener(e,r){e.port2?.on("message",r)}createWorker(){return new o(this.filePath,{env:n})}afterWorkerSetup(e){const{port1:r,port2:t}=new h;e.postMessage({parent:r},[r]),e.port1=r,e.port2=t,this.registerWorkerMessageListener(e,super.workerListener())}get type(){return k.FIXED}get full(){return this.workerNodes.length===this.numberOfWorkers}get busy(){return this.internalBusy()}}class C extends x{max;constructor(e,r,t,s={}){super(e,t,s),this.max=r}get type(){return k.DYNAMIC}get full(){return this.workerNodes.length===this.max}get busy(){return this.full&&-1===this.findFreeWorkerNodeKey()}}const b=6e4,E=l.SOFT;class M extends u{isMain;mainWorker;opts;lastTaskTimestamp;aliveInterval;constructor(e,r,t,s,i={killBehavior:E,maxInactiveTime:b}){super(e),this.isMain=r,this.mainWorker=s,this.opts=i,this.checkFunctionInput(t),this.checkWorkerOptions(this.opts),this.isMain||(this.lastTaskTimestamp=performance.now(),this.aliveInterval=setInterval(this.checkAlive.bind(this),(this.opts.maxInactiveTime??b)/2),this.checkAlive.bind(this)()),this.mainWorker?.on("message",(e=>{this.messageListener(e,t)}))}messageListener(e,r){null!=e.data&&null!=e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):null!=e.parent?this.mainWorker=e.parent:null!=e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??E,this.opts.maxInactiveTime=e.maxInactiveTime??b,this.opts.async=e.async??!1}checkFunctionInput(e){if(null==e)throw new Error("fn parameter is mandatory");if("function"!=typeof e)throw new TypeError("fn parameter is not a function")}getMainWorker(){if(null==this.mainWorker)throw new Error("Main worker was not set");return this.mainWorker}checkAlive(){performance.now()-this.lastTaskTimestamp>(this.opts.maxInactiveTime??b)&&this.sendToMainWorker({kill:this.opts.killBehavior})}handleError(e){return e}run(e,r){try{const t=performance.now(),s=e(r.data),i=performance.now()-t;this.sendToMainWorker({data:s,id:r.id,runTime:i})}catch(e){const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})}finally{!this.isMain&&(this.lastTaskTimestamp=performance.now())}}runAsync(e,r){const t=performance.now();e(r.data).then((e=>{const s=performance.now()-t;return this.sendToMainWorker({data:e,id:r.id,runTime:s}),null})).catch((e=>{const t=this.handleError(e);this.sendToMainWorker({error:t,id:r.id})})).finally((()=>{!this.isMain&&(this.lastTaskTimestamp=performance.now())})).catch(c)}}class O extends M{constructor(r,t={}){super("worker-cluster-pool:poolifier",e.isPrimary,r,e.worker,t)}sendToMainWorker(e){this.getMainWorker().send(e)}handleError(e){return e instanceof Error?e.message:e}}class D extends M{constructor(e,r={}){super("worker-thread-pool:poolifier",i,e,a,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{O as ClusterWorker,v as DynamicClusterPool,C as DynamicThreadPool,I as FixedClusterPool,x as FixedThreadPool,l as KillBehaviors,m as PoolEvents,D as ThreadWorker,p as WorkerChoiceStrategies};
@@ -1,9 +1,9 @@
1
1
  import type { MessageValue, PromiseResponseWrapper } from '../utility-types';
2
- import type { PoolOptions } from './pool';
2
+ import { type PoolOptions } from './pool';
3
3
  import { PoolEmitter } from './pool';
4
- import type { IPoolInternal, WorkerType } from './pool-internal';
4
+ import type { IPoolInternal } from './pool-internal';
5
5
  import { PoolType } from './pool-internal';
6
- import type { IPoolWorker } from './pool-worker';
6
+ import type { IWorker, WorkerNode } from './worker';
7
7
  import { type WorkerChoiceStrategy } from './selection-strategies/selection-strategies-types';
8
8
  import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choice-strategy-context';
9
9
  /**
@@ -13,21 +13,21 @@ import { WorkerChoiceStrategyContext } from './selection-strategies/worker-choic
13
13
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
14
14
  * @typeParam Response - Type of response of execution. This can only be serializable data.
15
15
  */
16
- export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = unknown, Response = unknown> implements IPoolInternal<Worker, Data, Response> {
16
+ export declare abstract class AbstractPool<Worker extends IWorker, Data = unknown, Response = unknown> implements IPoolInternal<Worker, Data, Response> {
17
17
  readonly numberOfWorkers: number;
18
18
  readonly filePath: string;
19
19
  readonly opts: PoolOptions<Worker>;
20
20
  /** @inheritDoc */
21
- readonly workers: Array<WorkerType<Worker>>;
21
+ readonly workerNodes: Array<WorkerNode<Worker, Data>>;
22
22
  /** @inheritDoc */
23
23
  readonly emitter?: PoolEmitter;
24
24
  /**
25
- * The promise response map.
25
+ * The execution response promise map.
26
26
  *
27
27
  * - `key`: The message id of each submitted task.
28
- * - `value`: An object that contains the worker, the promise resolve and reject callbacks.
28
+ * - `value`: An object that contains the worker, the execution response promise resolve and reject callbacks.
29
29
  *
30
- * When we receive a message from the worker we get a map entry with the promise resolve/reject bound to the message.
30
+ * When we receive a message from the worker, we get a map entry with the promise resolve/reject bound to the message id.
31
31
  */
32
32
  protected promiseResponseMap: Map<string, PromiseResponseWrapper<Worker, Response>>;
33
33
  /**
@@ -47,19 +47,24 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
47
47
  private checkFilePath;
48
48
  private checkNumberOfWorkers;
49
49
  private checkPoolOptions;
50
+ private checkValidWorkerChoiceStrategy;
50
51
  /** @inheritDoc */
51
52
  abstract get type(): PoolType;
52
53
  /**
53
- * Number of tasks concurrently running in the pool.
54
+ * Number of tasks running in the pool.
54
55
  */
55
56
  private get numberOfRunningTasks();
56
57
  /**
57
- * Gets the given worker key.
58
+ * Number of tasks queued in the pool.
59
+ */
60
+ private get numberOfQueuedTasks();
61
+ /**
62
+ * Gets the given worker its worker node key.
58
63
  *
59
64
  * @param worker - The worker.
60
- * @returns The worker key if the worker is found in the pool, `-1` otherwise.
65
+ * @returns The worker node key if the worker is found in the pool worker nodes, `-1` otherwise.
61
66
  */
62
- private getWorkerKey;
67
+ private getWorkerNodeKey;
63
68
  /** @inheritDoc */
64
69
  setWorkerChoiceStrategy(workerChoiceStrategy: WorkerChoiceStrategy): void;
65
70
  /** @inheritDoc */
@@ -68,20 +73,19 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
68
73
  abstract get busy(): boolean;
69
74
  protected internalBusy(): boolean;
70
75
  /** @inheritDoc */
71
- findFreeWorkerKey(): number;
76
+ findFreeWorkerNodeKey(): number;
72
77
  /** @inheritDoc */
73
78
  execute(data: Data): Promise<Response>;
74
79
  /** @inheritDoc */
75
80
  destroy(): Promise<void>;
76
81
  /**
77
- * Shutdowns given worker in the pool.
82
+ * Shutdowns the given worker.
78
83
  *
79
- * @param worker - A worker within `workers`.
84
+ * @param worker - A worker within `workerNodes`.
80
85
  */
81
86
  protected abstract destroyWorker(worker: Worker): void | Promise<void>;
82
87
  /**
83
- * Setup hook that can be overridden by a Poolifier pool implementation
84
- * to run code before workers are created in the abstract constructor.
88
+ * Setup hook to run code before worker node are created in the abstract constructor.
85
89
  * Can be overridden
86
90
  *
87
91
  * @virtual
@@ -95,9 +99,9 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
95
99
  * Hook executed before the worker task promise resolution.
96
100
  * Can be overridden.
97
101
  *
98
- * @param workerKey - The worker key.
102
+ * @param workerNodeKey - The worker node key.
99
103
  */
100
- protected beforePromiseResponseHook(workerKey: number): void;
104
+ protected beforePromiseResponseHook(workerNodeKey: number): void;
101
105
  /**
102
106
  * Hook executed after the worker task promise resolution.
103
107
  * Can be overridden.
@@ -107,13 +111,13 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
107
111
  */
108
112
  protected afterPromiseResponseHook(worker: Worker, message: MessageValue<Response>): void;
109
113
  /**
110
- * Chooses a worker for the next task.
114
+ * Chooses a worker node for the next task.
111
115
  *
112
116
  * The default uses a round robin algorithm to distribute the load.
113
117
  *
114
- * @returns [worker key, worker].
118
+ * @returns [worker node key, worker node].
115
119
  */
116
- protected chooseWorker(): [number, Worker];
120
+ protected chooseWorkerNode(): [number, WorkerNode<Worker, Data>];
117
121
  /**
118
122
  * Sends a message to the given worker.
119
123
  *
@@ -122,7 +126,7 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
122
126
  */
123
127
  protected abstract sendToWorker(worker: Worker, message: MessageValue<Data>): void;
124
128
  /**
125
- * Registers a listener callback on a given worker.
129
+ * Registers a listener callback on the given worker.
126
130
  *
127
131
  * @param worker - The worker which should register a listener.
128
132
  * @param listener - The message listener callback.
@@ -133,55 +137,60 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
133
137
  */
134
138
  protected abstract createWorker(): Worker;
135
139
  /**
136
- * Function that can be hooked up when a worker has been newly created and moved to the workers registry.
140
+ * Function that can be hooked up when a worker has been newly created and moved to the pool worker nodes.
137
141
  *
138
142
  * Can be used to update the `maxListeners` or binding the `main-worker`\<-\>`worker` connection if not bind by default.
139
143
  *
140
144
  * @param worker - The newly created worker.
141
- * @virtual
142
145
  */
143
146
  protected abstract afterWorkerSetup(worker: Worker): void;
144
147
  /**
145
- * Creates a new worker for this pool and sets it up completely.
148
+ * Creates a new worker and sets it up completely in the pool worker nodes.
146
149
  *
147
150
  * @returns New, completely set up worker.
148
151
  */
149
152
  protected createAndSetupWorker(): Worker;
150
153
  /**
151
- * This function is the listener registered for each worker.
154
+ * This function is the listener registered for each worker message.
152
155
  *
153
156
  * @returns The listener function to execute when a message is received from a worker.
154
157
  */
155
158
  protected workerListener(): (message: MessageValue<Response>) => void;
156
159
  private internalExecute;
157
- private checkAndEmitBusy;
158
- private checkAndEmitFull;
160
+ private checkAndEmitEvents;
159
161
  /**
160
- * Gets the given worker tasks usage in the pool.
162
+ * Gets the given worker its tasks usage in the pool.
161
163
  *
162
164
  * @param worker - The worker.
163
165
  * @returns The worker tasks usage.
164
166
  */
165
167
  private getWorkerTasksUsage;
166
168
  /**
167
- * Pushes the given worker in the pool.
169
+ * Pushes the given worker in the pool worker nodes.
168
170
  *
169
171
  * @param worker - The worker.
170
- * @param tasksUsage - The worker tasks usage.
172
+ * @returns The worker nodes length.
171
173
  */
172
- private pushWorker;
174
+ private pushWorkerNode;
173
175
  /**
174
- * Sets the given worker in the pool.
176
+ * Sets the given worker in the pool worker nodes.
175
177
  *
176
- * @param workerKey - The worker key.
178
+ * @param workerNodeKey - The worker node key.
177
179
  * @param worker - The worker.
178
180
  * @param tasksUsage - The worker tasks usage.
181
+ * @param tasksQueue - The worker task queue.
179
182
  */
180
- private setWorker;
183
+ private setWorkerNode;
181
184
  /**
182
- * Removes the given worker from the pool.
185
+ * Removes the given worker from the pool worker nodes.
183
186
  *
184
- * @param worker - The worker that will be removed.
187
+ * @param worker - The worker.
185
188
  */
186
- protected removeWorker(worker: Worker): void;
189
+ private removeWorkerNode;
190
+ private enqueueDequeueTask;
191
+ private enqueueTask;
192
+ private dequeueTask;
193
+ private tasksQueueSize;
194
+ private flushTasksQueue;
195
+ private flushTasksQueueByWorker;
187
196
  }
@@ -13,7 +13,7 @@ import { FixedClusterPool } from './fixed';
13
13
  * @since 2.0.0
14
14
  */
15
15
  export declare class DynamicClusterPool<Data = unknown, Response = unknown> extends FixedClusterPool<Data, Response> {
16
- private readonly max;
16
+ readonly max: number;
17
17
  /**
18
18
  * Constructs a new poolifier dynamic cluster pool.
19
19
  *
@@ -1,5 +1,5 @@
1
1
  import type { IPool } from './pool';
2
- import type { IPoolWorker } from './pool-worker';
2
+ import type { IWorker, WorkerNode } from './worker';
3
3
  /**
4
4
  * Internal pool types.
5
5
  *
@@ -9,25 +9,6 @@ export declare enum PoolType {
9
9
  FIXED = "fixed",
10
10
  DYNAMIC = "dynamic"
11
11
  }
12
- /**
13
- * Internal tasks usage statistics.
14
- */
15
- export interface TasksUsage {
16
- run: number;
17
- running: number;
18
- runTime: number;
19
- avgRunTime: number;
20
- error: number;
21
- }
22
- /**
23
- * Internal worker type.
24
- *
25
- * @typeParam Worker - Type of worker type items which manages this pool.
26
- */
27
- export interface WorkerType<Worker extends IPoolWorker> {
28
- worker: Worker;
29
- tasksUsage: TasksUsage;
30
- }
31
12
  /**
32
13
  * Internal contract definition for a poolifier pool.
33
14
  *
@@ -35,11 +16,11 @@ export interface WorkerType<Worker extends IPoolWorker> {
35
16
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
36
17
  * @typeParam Response - Type of response of execution. This can only be serializable data.
37
18
  */
38
- export interface IPoolInternal<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends IPool<Data, Response> {
19
+ export interface IPoolInternal<Worker extends IWorker, Data = unknown, Response = unknown> extends IPool<Data, Response> {
39
20
  /**
40
- * Pool worker type items array.
21
+ * Pool worker nodes.
41
22
  */
42
- readonly workers: Array<WorkerType<Worker>>;
23
+ readonly workerNodes: Array<WorkerNode<Worker, Data>>;
43
24
  /**
44
25
  * Pool type.
45
26
  *
@@ -59,13 +40,13 @@ export interface IPoolInternal<Worker extends IPoolWorker, Data = unknown, Respo
59
40
  */
60
41
  readonly busy: boolean;
61
42
  /**
62
- * Finds a free worker key based on the number of tasks the worker has applied.
43
+ * Finds a free worker node key based on the number of tasks the worker has applied.
63
44
  *
64
- * If a worker is found with `0` running tasks, it is detected as free and its key is returned.
45
+ * If a worker is found with `0` running tasks, it is detected as free and its worker node key is returned.
65
46
  *
66
- * If no free worker is found, `false` is returned.
47
+ * If no free worker is found, `-1` is returned.
67
48
  *
68
- * @returns A worker key if there is one, `-1` otherwise.
49
+ * @returns A worker node key if there is one, `-1` otherwise.
69
50
  */
70
- findFreeWorkerKey: () => number;
51
+ findFreeWorkerNodeKey: () => number;
71
52
  }
@@ -1,12 +1,23 @@
1
1
  /// <reference types="node" />
2
2
  import EventEmitter from 'node:events';
3
- import type { ErrorHandler, ExitHandler, MessageHandler, OnlineHandler } from './pool-worker';
4
- import type { WorkerChoiceStrategy } from './selection-strategies/selection-strategies-types';
3
+ import type { ErrorHandler, ExitHandler, MessageHandler, OnlineHandler } from './worker';
4
+ import type { WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './selection-strategies/selection-strategies-types';
5
5
  /**
6
6
  * Pool events emitter.
7
7
  */
8
8
  export declare class PoolEmitter extends EventEmitter {
9
9
  }
10
+ /**
11
+ * Enumeration of pool events.
12
+ */
13
+ export declare const PoolEvents: Readonly<{
14
+ readonly full: "full";
15
+ readonly busy: "busy";
16
+ }>;
17
+ /**
18
+ * Pool event.
19
+ */
20
+ export type PoolEvent = keyof typeof PoolEvents;
10
21
  /**
11
22
  * Options for a poolifier pool.
12
23
  */
@@ -31,12 +42,23 @@ export interface PoolOptions<Worker> {
31
42
  * The worker choice strategy to use in this pool.
32
43
  */
33
44
  workerChoiceStrategy?: WorkerChoiceStrategy;
45
+ /**
46
+ * The worker choice strategy options.
47
+ */
48
+ workerChoiceStrategyOptions?: WorkerChoiceStrategyOptions;
34
49
  /**
35
50
  * Pool events emission.
36
51
  *
37
52
  * @defaultValue true
38
53
  */
39
54
  enableEvents?: boolean;
55
+ /**
56
+ * Pool worker tasks queue.
57
+ *
58
+ * @experimental
59
+ * @defaultValue false
60
+ */
61
+ enableTasksQueue?: boolean;
40
62
  }
41
63
  /**
42
64
  * Contract definition for a poolifier pool.
@@ -1,6 +1,6 @@
1
1
  import type { IPoolInternal } from '../pool-internal';
2
- import type { IPoolWorker } from '../pool-worker';
3
- import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
2
+ import type { IWorker } from '../worker';
3
+ import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
4
4
  /**
5
5
  * Worker choice strategy abstract base class.
6
6
  *
@@ -8,22 +8,25 @@ import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-stra
8
8
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
9
9
  * @typeParam Response - Type of response of execution. This can only be serializable data.
10
10
  */
11
- export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> implements IWorkerChoiceStrategy<Worker, Data, Response> {
12
- readonly pool: IPoolInternal<Worker, Data, Response>;
11
+ export declare abstract class AbstractWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> implements IWorkerChoiceStrategy {
12
+ protected readonly pool: IPoolInternal<Worker, Data, Response>;
13
+ protected readonly opts: WorkerChoiceStrategyOptions;
13
14
  /** @inheritDoc */
14
- readonly isDynamicPool: boolean;
15
+ protected readonly isDynamicPool: boolean;
15
16
  /** @inheritDoc */
16
- requiredStatistics: RequiredStatistics;
17
+ readonly requiredStatistics: RequiredStatistics;
17
18
  /**
18
19
  * Constructs a worker choice strategy bound to the pool.
19
20
  *
20
21
  * @param pool - The pool instance.
22
+ * @param opts - The worker choice strategy options.
21
23
  */
22
- constructor(pool: IPoolInternal<Worker, Data, Response>);
24
+ constructor(pool: IPoolInternal<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
25
+ private checkOptions;
23
26
  /** @inheritDoc */
24
27
  abstract reset(): boolean;
25
28
  /** @inheritDoc */
26
29
  abstract choose(): number;
27
30
  /** @inheritDoc */
28
- abstract remove(workerKey: number): boolean;
31
+ abstract remove(workerNodeKey: number): boolean;
29
32
  }
@@ -1,4 +1,4 @@
1
- import type { IPoolWorker } from '../pool-worker';
1
+ import type { IWorker } from '../worker';
2
2
  import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
3
3
  import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
4
4
  /**
@@ -9,11 +9,11 @@ import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-stra
9
9
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
10
10
  * @typeParam Response - Type of response of execution. This can only be serializable data.
11
11
  */
12
- export declare class FairShareWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
12
+ export declare class FairShareWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
13
13
  /** @inheritDoc */
14
14
  readonly requiredStatistics: RequiredStatistics;
15
15
  /**
16
- * Worker last virtual task execution timestamp.
16
+ * Worker last virtual task execution timestamp.
17
17
  */
18
18
  private readonly workerLastVirtualTaskTimestamp;
19
19
  /** @inheritDoc */
@@ -21,11 +21,11 @@ export declare class FairShareWorkerChoiceStrategy<Worker extends IPoolWorker, D
21
21
  /** @inheritDoc */
22
22
  choose(): number;
23
23
  /** @inheritDoc */
24
- remove(workerKey: number): boolean;
24
+ remove(workerNodeKey: number): boolean;
25
25
  /**
26
26
  * Computes worker last virtual task timestamp.
27
27
  *
28
- * @param workerKey - The worker key.
28
+ * @param workerNodeKey - The worker node key.
29
29
  */
30
30
  private computeWorkerLastVirtualTaskTimestamp;
31
31
  }
@@ -1,4 +1,4 @@
1
- import type { IPoolWorker } from '../pool-worker';
1
+ import type { IWorker } from '../worker';
2
2
  import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
3
3
  import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
4
4
  /**
@@ -8,7 +8,7 @@ import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-stra
8
8
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
9
9
  * @typeParam Response - Type of response of execution. This can only be serializable data.
10
10
  */
11
- export declare class LessBusyWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
11
+ export declare class LessBusyWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
12
12
  /** @inheritDoc */
13
13
  readonly requiredStatistics: RequiredStatistics;
14
14
  /** @inheritDoc */
@@ -16,5 +16,5 @@ export declare class LessBusyWorkerChoiceStrategy<Worker extends IPoolWorker, Da
16
16
  /** @inheritDoc */
17
17
  choose(): number;
18
18
  /** @inheritDoc */
19
- remove(workerKey: number): boolean;
19
+ remove(workerNodeKey: number): boolean;
20
20
  }
@@ -1,4 +1,4 @@
1
- import type { IPoolWorker } from '../pool-worker';
1
+ import type { IWorker } from '../worker';
2
2
  import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
3
3
  import type { IWorkerChoiceStrategy } from './selection-strategies-types';
4
4
  /**
@@ -8,11 +8,11 @@ import type { IWorkerChoiceStrategy } from './selection-strategies-types';
8
8
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
9
9
  * @typeParam Response - Type of response of execution. This can only be serializable data.
10
10
  */
11
- export declare class LessUsedWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
11
+ export declare class LessUsedWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
12
12
  /** @inheritDoc */
13
13
  reset(): boolean;
14
14
  /** @inheritDoc */
15
15
  choose(): number;
16
16
  /** @inheritDoc */
17
- remove(workerKey: number): boolean;
17
+ remove(workerNodeKey: number): boolean;
18
18
  }
@@ -1,4 +1,4 @@
1
- import type { IPoolWorker } from '../pool-worker';
1
+ import type { IWorker } from '../worker';
2
2
  import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
3
3
  import type { IWorkerChoiceStrategy } from './selection-strategies-types';
4
4
  /**
@@ -8,15 +8,15 @@ import type { IWorkerChoiceStrategy } from './selection-strategies-types';
8
8
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
9
9
  * @typeParam Response - Type of response of execution. This can only be serializable data.
10
10
  */
11
- export declare class RoundRobinWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
11
+ export declare class RoundRobinWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
12
12
  /**
13
- * Id of the next worker.
13
+ * Id of the next worker node.
14
14
  */
15
- private nextWorkerId;
15
+ private nextWorkerNodeId;
16
16
  /** @inheritDoc */
17
17
  reset(): boolean;
18
18
  /** @inheritDoc */
19
19
  choose(): number;
20
20
  /** @inheritDoc */
21
- remove(workerKey: number): boolean;
21
+ remove(workerNodeKey: number): boolean;
22
22
  }
@@ -1,5 +1,3 @@
1
- import type { IPoolInternal } from '../pool-internal';
2
- import type { IPoolWorker } from '../pool-worker';
3
1
  /**
4
2
  * Enumeration of worker choice strategies.
5
3
  */
@@ -29,27 +27,29 @@ export declare const WorkerChoiceStrategies: Readonly<{
29
27
  * Worker choice strategy.
30
28
  */
31
29
  export type WorkerChoiceStrategy = keyof typeof WorkerChoiceStrategies;
30
+ /**
31
+ * Worker choice strategy options.
32
+ */
33
+ export interface WorkerChoiceStrategyOptions {
34
+ /**
35
+ * Use tasks median run time instead of average run time.
36
+ */
37
+ medRunTime?: boolean;
38
+ }
32
39
  /**
33
40
  * Pool worker tasks usage statistics requirements.
34
41
  */
35
42
  export interface RequiredStatistics {
36
43
  runTime: boolean;
37
44
  avgRunTime: boolean;
45
+ medRunTime: boolean;
38
46
  }
39
47
  /**
40
48
  * Worker choice strategy interface.
41
49
  */
42
- export interface IWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> {
43
- /**
44
- * The pool instance.
45
- */
46
- readonly pool: IPoolInternal<Worker, Data, Response>;
47
- /**
48
- * Is the pool bound to the strategy dynamic?.
49
- */
50
- readonly isDynamicPool: boolean;
50
+ export interface IWorkerChoiceStrategy {
51
51
  /**
52
- * Required pool tasks usage statistics.
52
+ * Required tasks usage statistics.
53
53
  */
54
54
  readonly requiredStatistics: RequiredStatistics;
55
55
  /**
@@ -57,13 +57,13 @@ export interface IWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknow
57
57
  */
58
58
  reset: () => boolean;
59
59
  /**
60
- * Chooses a worker in the pool and returns its key.
60
+ * Chooses a worker node in the pool and returns its key.
61
61
  */
62
62
  choose: () => number;
63
63
  /**
64
- * Removes a worker reference from strategy internals.
64
+ * Removes a worker node key from strategy internals.
65
65
  *
66
- * @param workerKey - The worker key.
66
+ * @param workerNodeKey - The worker node key.
67
67
  */
68
- remove: (workerKey: number) => boolean;
68
+ remove: (workerNodeKey: number) => boolean;
69
69
  }
@@ -1,7 +1,7 @@
1
1
  import type { IPoolInternal } from '../pool-internal';
2
- import type { IPoolWorker } from '../pool-worker';
2
+ import type { IWorker } from '../worker';
3
3
  import { AbstractWorkerChoiceStrategy } from './abstract-worker-choice-strategy';
4
- import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-strategies-types';
4
+ import type { IWorkerChoiceStrategy, RequiredStatistics, WorkerChoiceStrategyOptions } from './selection-strategies-types';
5
5
  /**
6
6
  * Selects the next worker with a weighted round robin scheduling algorithm.
7
7
  * Loosely modeled after the weighted round robin queueing algorithm: https://en.wikipedia.org/wiki/Weighted_round_robin.
@@ -10,33 +10,34 @@ import type { IWorkerChoiceStrategy, RequiredStatistics } from './selection-stra
10
10
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
11
11
  * @typeParam Response - Type of response of execution. This can only be serializable data.
12
12
  */
13
- export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
13
+ export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends IWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
14
14
  /** @inheritDoc */
15
15
  readonly requiredStatistics: RequiredStatistics;
16
16
  /**
17
- * Worker id where the current task will be submitted.
17
+ * Worker node id where the current task will be submitted.
18
18
  */
19
- private currentWorkerId;
19
+ private currentWorkerNodeId;
20
20
  /**
21
21
  * Default worker weight.
22
22
  */
23
23
  private readonly defaultWorkerWeight;
24
24
  /**
25
- * Per worker virtual task runtime map.
25
+ * Workers' virtual task runtime.
26
26
  */
27
27
  private readonly workersTaskRunTime;
28
28
  /**
29
29
  * Constructs a worker choice strategy that selects with a weighted round robin scheduling algorithm.
30
30
  *
31
31
  * @param pool - The pool instance.
32
+ * @param opts - The worker choice strategy options.
32
33
  */
33
- constructor(pool: IPoolInternal<Worker, Data, Response>);
34
+ constructor(pool: IPoolInternal<Worker, Data, Response>, opts?: WorkerChoiceStrategyOptions);
34
35
  /** @inheritDoc */
35
36
  reset(): boolean;
36
37
  /** @inheritDoc */
37
38
  choose(): number;
38
39
  /** @inheritDoc */
39
- remove(workerKey: number): boolean;
40
+ remove(workerNodeKey: number): boolean;
40
41
  private initWorkersTaskRunTime;
41
42
  private initWorkerTaskRunTime;
42
43
  private setWorkerTaskRunTime;
@@ -1,6 +1,6 @@
1
1
  import type { IPoolInternal } from '../pool-internal';
2
- import type { IPoolWorker } from '../pool-worker';
3
- import type { RequiredStatistics, WorkerChoiceStrategy } from './selection-strategies-types';
2
+ import type { IWorker } from '../worker';
3
+ import type { RequiredStatistics, WorkerChoiceStrategy, WorkerChoiceStrategyOptions } from './selection-strategies-types';
4
4
  /**
5
5
  * The worker choice strategy context.
6
6
  *
@@ -8,18 +8,17 @@ import type { RequiredStatistics, WorkerChoiceStrategy } from './selection-strat
8
8
  * @typeParam Data - Type of data sent to the worker. This can only be serializable data.
9
9
  * @typeParam Response - Type of response of execution. This can only be serializable data.
10
10
  */
11
- export declare class WorkerChoiceStrategyContext<Worker extends IPoolWorker, Data = unknown, Response = unknown> {
12
- private readonly createWorkerCallback;
11
+ export declare class WorkerChoiceStrategyContext<Worker extends IWorker, Data = unknown, Response = unknown> {
13
12
  private workerChoiceStrategyType;
14
13
  private readonly workerChoiceStrategies;
15
14
  /**
16
15
  * Worker choice strategy context constructor.
17
16
  *
18
17
  * @param pool - The pool instance.
19
- * @param createWorkerCallback - The worker creation callback for dynamic pool.
20
- * @param workerChoiceStrategy - The worker choice strategy.
18
+ * @param workerChoiceStrategyType - The worker choice strategy.
19
+ * @param opts - The worker choice strategy options.
21
20
  */
22
- constructor(pool: IPoolInternal<Worker, Data, Response>, createWorkerCallback: () => number, workerChoiceStrategyType?: WorkerChoiceStrategy);
21
+ constructor(pool: IPoolInternal<Worker, Data, Response>, workerChoiceStrategyType?: WorkerChoiceStrategy, opts?: WorkerChoiceStrategyOptions);
23
22
  /**
24
23
  * Gets the worker choice strategy in the context required statistics.
25
24
  *
@@ -35,14 +34,14 @@ export declare class WorkerChoiceStrategyContext<Worker extends IPoolWorker, Dat
35
34
  /**
36
35
  * Executes the worker choice strategy algorithm in the context.
37
36
  *
38
- * @returns The key of the chosen one.
37
+ * @returns The key of the worker node.
39
38
  */
40
39
  execute(): number;
41
40
  /**
42
- * Removes a worker from the worker choice strategy in the context.
41
+ * Removes a worker node key from the worker choice strategy in the context.
43
42
  *
44
- * @param workerKey - The key of the worker to remove.
43
+ * @param workerNodeKey - The key of the worker node.
45
44
  * @returns `true` if the removal is successful, `false` otherwise.
46
45
  */
47
- remove(workerKey: number): boolean;
46
+ remove(workerNodeKey: number): boolean;
48
47
  }
@@ -14,7 +14,7 @@ import { FixedThreadPool } from './fixed';
14
14
  * @since 0.0.1
15
15
  */
16
16
  export declare class DynamicThreadPool<Data = unknown, Response = unknown> extends FixedThreadPool<Data, Response> {
17
- private readonly max;
17
+ readonly max: number;
18
18
  /**
19
19
  * Constructs a new poolifier dynamic thread pool.
20
20
  *
@@ -1,3 +1,4 @@
1
+ import type { CircularArray } from '../circular-array';
1
2
  /**
2
3
  * Callback invoked if the worker has received a message.
3
4
  */
@@ -15,9 +16,28 @@ export type OnlineHandler<Worker> = (this: Worker) => void;
15
16
  */
16
17
  export type ExitHandler<Worker> = (this: Worker, code: number) => void;
17
18
  /**
18
- * Interface that describes the minimum required implementation of listener events for a pool worker.
19
+ * Worker task interface.
19
20
  */
20
- export interface IPoolWorker {
21
+ export interface Task<Data = unknown> {
22
+ data: Data;
23
+ id: string;
24
+ }
25
+ /**
26
+ * Worker tasks usage statistics.
27
+ */
28
+ export interface TasksUsage {
29
+ run: number;
30
+ running: number;
31
+ runTime: number;
32
+ runTimeHistory: CircularArray<number>;
33
+ avgRunTime: number;
34
+ medRunTime: number;
35
+ error: number;
36
+ }
37
+ /**
38
+ * Worker interface.
39
+ */
40
+ export interface IWorker {
21
41
  /**
22
42
  * Register an event listener.
23
43
  *
@@ -33,3 +53,11 @@ export interface IPoolWorker {
33
53
  */
34
54
  once: (event: 'exit', handler: ExitHandler<this>) => void;
35
55
  }
56
+ /**
57
+ * Worker node interface.
58
+ */
59
+ export interface WorkerNode<Worker extends IWorker, Data = unknown> {
60
+ worker: Worker;
61
+ tasksUsage: TasksUsage;
62
+ tasksQueue: Array<Task<Data>>;
63
+ }
@@ -3,7 +3,7 @@
3
3
  import type { Worker as ClusterWorker } from 'node:cluster';
4
4
  import type { MessagePort } from 'node:worker_threads';
5
5
  import type { KillBehavior } from './worker/worker-options';
6
- import type { IPoolWorker } from './pools/pool-worker';
6
+ import type { IWorker } from './pools/worker';
7
7
  /**
8
8
  * Make all properties in T non-readonly.
9
9
  */
@@ -31,9 +31,9 @@ export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker |
31
31
  */
32
32
  readonly error?: string;
33
33
  /**
34
- * Task runtime.
34
+ * Runtime.
35
35
  */
36
- readonly taskRunTime?: number;
36
+ readonly runTime?: number;
37
37
  /**
38
38
  * Reference to main worker.
39
39
  *
@@ -47,7 +47,7 @@ export interface MessageValue<Data = unknown, MainWorker extends ClusterWorker |
47
47
  * @typeParam Worker - Type of worker.
48
48
  * @typeParam Response - Type of execution response. This can only be serializable data.
49
49
  */
50
- export interface PromiseResponseWrapper<Worker extends IPoolWorker, Response = unknown> {
50
+ export interface PromiseResponseWrapper<Worker extends IWorker, Response = unknown> {
51
51
  /**
52
52
  * Resolve callback to fulfill the promise.
53
53
  */
@@ -57,7 +57,7 @@ export interface PromiseResponseWrapper<Worker extends IPoolWorker, Response = u
57
57
  */
58
58
  readonly reject: (reason?: string) => void;
59
59
  /**
60
- * The worker handling the promise.
60
+ * The worker handling the execution.
61
61
  */
62
62
  readonly worker: Worker;
63
63
  }
package/lib/utils.d.ts CHANGED
@@ -2,3 +2,10 @@
2
2
  * An intentional empty function.
3
3
  */
4
4
  export declare const EMPTY_FUNCTION: () => void;
5
+ /**
6
+ * Returns the median of the given data set.
7
+ *
8
+ * @param dataSet - Data set.
9
+ * @returns The median of the given data set.
10
+ */
11
+ export declare const median: (dataSet: number[]) => number;
@@ -23,7 +23,7 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
23
23
  */
24
24
  protected lastTaskTimestamp: number;
25
25
  /**
26
- * Handler Id of the `aliveInterval` worker alive check.
26
+ * Handler id of the `aliveInterval` worker alive check.
27
27
  */
28
28
  protected readonly aliveInterval?: NodeJS.Timeout;
29
29
  /**
@@ -36,7 +36,13 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
36
36
  * @param opts - Options for the worker.
37
37
  */
38
38
  constructor(type: string, isMain: boolean, fn: (data: Data) => Response, mainWorker: MainWorker | undefined | null, opts?: WorkerOptions);
39
- protected messageListener(value: MessageValue<Data, MainWorker>, fn: (data: Data) => Response): void;
39
+ /**
40
+ * Worker message listener.
41
+ *
42
+ * @param message - Message received.
43
+ * @param fn - Function processed by the worker when the pool's `execution` function is invoked.
44
+ */
45
+ protected messageListener(message: MessageValue<Data, MainWorker>, fn: (data: Data) => Response): void;
40
46
  private checkWorkerOptions;
41
47
  /**
42
48
  * Checks if the `fn` parameter is passed to the constructor.
@@ -71,14 +77,14 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
71
77
  * Runs the given function synchronously.
72
78
  *
73
79
  * @param fn - Function that will be executed.
74
- * @param value - Input data for the given function.
80
+ * @param message - Input data for the given function.
75
81
  */
76
- protected run(fn: (data?: Data) => Response, value: MessageValue<Data>): void;
82
+ protected run(fn: (data?: Data) => Response, message: MessageValue<Data>): void;
77
83
  /**
78
84
  * Runs the given function asynchronously.
79
85
  *
80
86
  * @param fn - Function that will be executed.
81
- * @param value - Input data for the given function.
87
+ * @param message - Input data for the given function.
82
88
  */
83
- protected runAsync(fn: (data?: Data) => Promise<Response>, value: MessageValue<Data>): void;
89
+ protected runAsync(fn: (data?: Data) => Promise<Response>, message: MessageValue<Data>): void;
84
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poolifier",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
4
4
  "description": "A fast, easy to use Node.js Worker Thread Pool and Cluster Pool implementation",
5
5
  "license": "MIT",
6
6
  "main": "./lib/index.js",
@@ -84,20 +84,20 @@
84
84
  "@typescript-eslint/parser": "^5.57.1",
85
85
  "benny": "^3.7.1",
86
86
  "c8": "^7.13.0",
87
- "eslint": "^8.37.0",
87
+ "eslint": "^8.38.0",
88
88
  "eslint-config-standard": "^17.0.0",
89
89
  "eslint-config-standard-with-typescript": "^34.0.1",
90
90
  "eslint-define-config": "^1.17.0",
91
91
  "eslint-import-resolver-typescript": "^3.5.5",
92
92
  "eslint-plugin-import": "^2.27.5",
93
- "eslint-plugin-jsdoc": "^40.1.1",
93
+ "eslint-plugin-jsdoc": "^40.1.2",
94
94
  "eslint-plugin-n": "^15.7.0",
95
95
  "eslint-plugin-promise": "^6.1.1",
96
96
  "eslint-plugin-spellcheck": "^0.0.20",
97
97
  "eslint-plugin-tsdoc": "^0.2.17",
98
98
  "expect": "^29.5.0",
99
99
  "husky": "^8.0.3",
100
- "lint-staged": "^13.2.0",
100
+ "lint-staged": "^13.2.1",
101
101
  "microtime": "^3.1.1",
102
102
  "mocha": "^10.2.0",
103
103
  "mochawesome": "^7.1.3",
@@ -112,7 +112,7 @@
112
112
  "source-map-support": "^0.5.21",
113
113
  "ts-standard": "^12.0.2",
114
114
  "typedoc": "^0.23.28",
115
- "typescript": "^5.0.3"
115
+ "typescript": "^5.0.4"
116
116
  },
117
117
  "scripts": {
118
118
  "preinstall": "npx only-allow pnpm",