poolifier 2.4.1 → 2.4.3

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
@@ -120,11 +120,14 @@ 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'))
124
+
123
125
  // or a dynamic worker-threads pool
124
126
  const pool = new DynamicThreadPool(10, 100,
125
127
  './yourWorker.js',
126
128
  { errorHandler: (e) => console.error(e), onlineHandler: () => console.log('worker is online') })
127
129
 
130
+ pool.emitter.on('full', () => console.log('Pool is full'))
128
131
  pool.emitter.on('busy', () => console.log('Pool is busy'))
129
132
 
130
133
  // the execute method signature is the same for both implementations,
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e,r=require("node:cluster"),t=require("node:crypto"),s=require("node:events"),o=require("node:os"),i=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 m 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 o=s.tasksUsage.runTime;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class p 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 o=s.tasksUsage,i=o?.run+o?.running;if(0===i)return e;i<t&&(t=i,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&&(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&&(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 o.cpus()){const t=r.speed.toString().length-1;e+=1/(r.speed/Math.pow(10,t))*Math.pow(10,t)}return Math.round(e/o.cpus().length)}}class g{pool;createWorkerCallback;workerChoiceStrategy;constructor(e,r,t=u.ROUND_ROBIN){this.pool=e,this.createWorkerCallback=r,this.execute.bind(this),this.setWorkerChoiceStrategy(t)}getRequiredStatistics(){return this.workerChoiceStrategy.requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategy?.reset(),this.workerChoiceStrategy=function(e,r=u.ROUND_ROBIN){switch(r){case u.ROUND_ROBIN:return new d(e);case u.LESS_USED:return new p(e);case u.LESS_BUSY:return new m(e);case u.FAIR_SHARE:return new l(e);case u.WEIGHTED_ROUND_ROBIN:return new w(e);default:throw new Error(`Worker choice strategy '${r}' not found`)}}(this.pool,e)}execute(){return this.workerChoiceStrategy.isDynamicPool&&!this.pool.full&&-1===this.pool.findFreeWorkerKey()?this.createWorkerCallback():this.workerChoiceStrategy.choose()}remove(e){return this.workerChoiceStrategy.remove(e)}}class W{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.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){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??u.ROUND_ROBIN,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(),o=t.randomUUID(),i=this.internalExecute(r,s,o);return this.checkAndEmitBusy(),this.sendToWorker(s,{data:e??{},id:o}),i}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))}removeWorker(e){const r=this.getWorkerKey(e);this.workers.splice(r,1),this.workerChoiceStrategyContext.remove(r)}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(void 0!==e.id){const r=this.promiseResponseMap.get(e.id);void 0!==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")}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}}}class T extends W{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 W{constructor(e,r,t={}){super(e,r,t)}isMain(){return i.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 i.Worker(this.filePath,{env:i.SHARE_ENV})}afterWorkerSetup(e){const{port1:r,port2:t}=new i.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 x extends n.AsyncResource{isMain;mainWorker;lastTaskTimestamp;aliveInterval;opts;constructor(e,r,t,s,o={killBehavior:R,maxInactiveTime:y}){super(e),this.isMain=r,this.mainWorker=s,this.opts=o,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){void 0!==e.data&&void 0!==e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):void 0!==e.parent?this.mainWorker=e.parent:void 0!==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),o=Date.now()-t;this.sendToMainWorker({data:s,id:r.id,taskRunTime:o})}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 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 T{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=T,exports.FixedThreadPool=f,exports.KillBehaviors=h,exports.ThreadWorker=class extends x{constructor(e,r={}){super("worker-thread-pool:poolifier",i.isMainThread,e,i.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 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;
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 o,Worker as i,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 d{pool;isDynamicPool;requiredStatistics={runTime:!1,avgRunTime:!1};constructor(e){this.pool=e,this.isDynamicPool=this.pool.type===u.DYNAMIC,this.choose.bind(this)}}class w extends d{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 d{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 o=s.tasksUsage.runTime;if(0===o)return e;o<t&&(t=o,r=e)}return r}remove(e){return!0}}class W extends d{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 o=s.tasksUsage,i=o?.run+o?.running;if(0===i)return e;i<t&&(t=i,r=e)}return r}remove(e){return!0}}class T extends d{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&&(this.nextWorkerId=this.nextWorkerId>this.pool.workers.length-1?this.pool.workers.length-1:this.nextWorkerId),!0}}class f extends d{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&&(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{pool;createWorkerCallback;workerChoiceStrategy;constructor(e,r,t=p.ROUND_ROBIN){this.pool=e,this.createWorkerCallback=r,this.execute.bind(this),this.setWorkerChoiceStrategy(t)}getRequiredStatistics(){return this.workerChoiceStrategy.requiredStatistics}setWorkerChoiceStrategy(e){this.workerChoiceStrategy?.reset(),this.workerChoiceStrategy=function(e,r=p.ROUND_ROBIN){switch(r){case p.ROUND_ROBIN:return new T(e);case p.LESS_USED:return new W(e);case p.LESS_BUSY:return new g(e);case p.FAIR_SHARE:return new w(e);case p.WEIGHTED_ROUND_ROBIN:return new f(e);default:throw new Error(`Worker choice strategy '${r}' not found`)}}(this.pool,e)}execute(){return this.workerChoiceStrategy.isDynamicPool&&!this.pool.full&&-1===this.pool.findFreeWorkerKey()?this.createWorkerCallback():this.workerChoiceStrategy.choose()}remove(e){return this.workerChoiceStrategy.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.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){this.opts.workerChoiceStrategy=e.workerChoiceStrategy??p.ROUND_ROBIN,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(),o=r.randomUUID(),i=this.internalExecute(t,s,o);return this.checkAndEmitBusy(),this.sendToWorker(s,{data:e??{},id:o}),i}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))}removeWorker(e){const r=this.getWorkerKey(e);this.workers.splice(r,1),this.workerChoiceStrategyContext.remove(r)}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(void 0!==e.id){const r=this.promiseResponseMap.get(e.id);void 0!==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")}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}}}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 v extends R{constructor(e,r,t={}){super(e,r,t)}isMain(){return o}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 i(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 x extends v{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,b=l.SOFT;class M extends k{isMain;mainWorker;lastTaskTimestamp;aliveInterval;opts;constructor(e,r,t,s,o={killBehavior:b,maxInactiveTime:E}){super(e),this.isMain=r,this.mainWorker=s,this.opts=o,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){void 0!==e.data&&void 0!==e.id?!0===this.opts.async?this.runInAsyncScope(this.runAsync.bind(this),this,r,e):this.runInAsyncScope(this.run.bind(this),this,r,e):void 0!==e.parent?this.mainWorker=e.parent:void 0!==e.kill&&(null!=this.aliveInterval&&clearInterval(this.aliveInterval),this.emitDestroy())}checkWorkerOptions(e){this.opts.killBehavior=e.killBehavior??b,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),o=Date.now()-t;this.sendToMainWorker({data:s,id:r.id,taskRunTime:o})}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 C 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",o,e,h,r)}sendToMainWorker(e){this.getMainWorker().postMessage(e)}}export{C as ClusterWorker,I as DynamicClusterPool,x as DynamicThreadPool,S as FixedClusterPool,v 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 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};
@@ -17,9 +17,9 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
17
17
  readonly numberOfWorkers: number;
18
18
  readonly filePath: string;
19
19
  readonly opts: PoolOptions<Worker>;
20
- /** {@inheritDoc} */
20
+ /** @inheritDoc */
21
21
  readonly workers: Array<WorkerType<Worker>>;
22
- /** {@inheritDoc} */
22
+ /** @inheritDoc */
23
23
  readonly emitter?: PoolEmitter;
24
24
  /**
25
25
  * The promise response map.
@@ -31,9 +31,9 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
31
31
  */
32
32
  protected promiseResponseMap: Map<string, PromiseResponseWrapper<Worker, Response>>;
33
33
  /**
34
- * Worker choice strategy instance implementing the worker choice algorithm.
34
+ * Worker choice strategy context referencing a worker choice algorithm implementation.
35
35
  *
36
- * Default to a strategy implementing a round robin algorithm.
36
+ * Default to a round robin algorithm.
37
37
  */
38
38
  protected workerChoiceStrategyContext: WorkerChoiceStrategyContext<Worker, Data, Response>;
39
39
  /**
@@ -47,10 +47,10 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
47
47
  private checkFilePath;
48
48
  private checkNumberOfWorkers;
49
49
  private checkPoolOptions;
50
- /** {@inheritDoc} */
50
+ /** @inheritDoc */
51
51
  abstract get type(): PoolType;
52
52
  /**
53
- * Number of tasks concurrently running.
53
+ * Number of tasks concurrently running in the pool.
54
54
  */
55
55
  private get numberOfRunningTasks();
56
56
  /**
@@ -60,21 +60,21 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
60
60
  * @returns The worker key if the worker is found in the pool, `-1` otherwise.
61
61
  */
62
62
  private getWorkerKey;
63
- /** {@inheritDoc} */
63
+ /** @inheritDoc */
64
64
  setWorkerChoiceStrategy(workerChoiceStrategy: WorkerChoiceStrategy): void;
65
- /** {@inheritDoc} */
65
+ /** @inheritDoc */
66
66
  abstract get full(): boolean;
67
- /** {@inheritDoc} */
67
+ /** @inheritDoc */
68
68
  abstract get busy(): boolean;
69
69
  protected internalBusy(): boolean;
70
- /** {@inheritDoc} */
70
+ /** @inheritDoc */
71
71
  findFreeWorkerKey(): number;
72
- /** {@inheritDoc} */
72
+ /** @inheritDoc */
73
73
  execute(data: Data): Promise<Response>;
74
- /** {@inheritDoc} */
74
+ /** @inheritDoc */
75
75
  destroy(): Promise<void>;
76
76
  /**
77
- * Shutdowns given worker.
77
+ * Shutdowns given worker in the pool.
78
78
  *
79
79
  * @param worker - A worker within `workers`.
80
80
  */
@@ -82,6 +82,9 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
82
82
  /**
83
83
  * Setup hook that can be overridden by a Poolifier pool implementation
84
84
  * to run code before workers are created in the abstract constructor.
85
+ * Can be overridden
86
+ *
87
+ * @virtual
85
88
  */
86
89
  protected setupHook(): void;
87
90
  /**
@@ -103,16 +106,10 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
103
106
  * @param message - The received message.
104
107
  */
105
108
  protected afterPromiseResponseHook(worker: Worker, message: MessageValue<Response>): void;
106
- /**
107
- * Removes the given worker from the pool.
108
- *
109
- * @param worker - The worker that will be removed.
110
- */
111
- protected removeWorker(worker: Worker): void;
112
109
  /**
113
110
  * Chooses a worker for the next task.
114
111
  *
115
- * The default implementation uses a round robin algorithm to distribute the load.
112
+ * The default uses a round robin algorithm to distribute the load.
116
113
  *
117
114
  * @returns [worker key, worker].
118
115
  */
@@ -141,6 +138,7 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
141
138
  * Can be used to update the `maxListeners` or binding the `main-worker`\<-\>`worker` connection if not bind by default.
142
139
  *
143
140
  * @param worker - The newly created worker.
141
+ * @virtual
144
142
  */
145
143
  protected abstract afterWorkerSetup(worker: Worker): void;
146
144
  /**
@@ -157,26 +155,33 @@ export declare abstract class AbstractPool<Worker extends IPoolWorker, Data = un
157
155
  protected workerListener(): (message: MessageValue<Response>) => void;
158
156
  private internalExecute;
159
157
  private checkAndEmitBusy;
158
+ private checkAndEmitFull;
160
159
  /**
161
- * Gets worker tasks usage.
160
+ * Gets the given worker tasks usage in the pool.
162
161
  *
163
162
  * @param worker - The worker.
164
163
  * @returns The worker tasks usage.
165
164
  */
166
165
  private getWorkerTasksUsage;
167
166
  /**
168
- * Pushes the given worker.
167
+ * Pushes the given worker in the pool.
169
168
  *
170
169
  * @param worker - The worker.
171
170
  * @param tasksUsage - The worker tasks usage.
172
171
  */
173
172
  private pushWorker;
174
173
  /**
175
- * Sets the given worker.
174
+ * Sets the given worker in the pool.
176
175
  *
177
176
  * @param workerKey - The worker key.
178
177
  * @param worker - The worker.
179
178
  * @param tasksUsage - The worker tasks usage.
180
179
  */
181
180
  private setWorker;
181
+ /**
182
+ * Removes the given worker from the pool.
183
+ *
184
+ * @param worker - The worker that will be removed.
185
+ */
186
+ protected removeWorker(worker: Worker): void;
182
187
  }
@@ -23,10 +23,10 @@ export declare class DynamicClusterPool<Data = unknown, Response = unknown> exte
23
23
  * @param opts - Options for this dynamic cluster pool.
24
24
  */
25
25
  constructor(min: number, max: number, filePath: string, opts?: ClusterPoolOptions);
26
- /** {@inheritDoc} */
26
+ /** @inheritDoc */
27
27
  get type(): PoolType;
28
- /** {@inheritDoc} */
28
+ /** @inheritDoc */
29
29
  get full(): boolean;
30
- /** {@inheritDoc} */
30
+ /** @inheritDoc */
31
31
  get busy(): boolean;
32
32
  }
@@ -43,24 +43,24 @@ export declare class FixedClusterPool<Data = unknown, Response = unknown> extend
43
43
  * @param opts - Options for this fixed cluster pool.
44
44
  */
45
45
  constructor(numberOfWorkers: number, filePath: string, opts?: ClusterPoolOptions);
46
- /** {@inheritDoc} */
46
+ /** @inheritDoc */
47
47
  protected setupHook(): void;
48
- /** {@inheritDoc} */
48
+ /** @inheritDoc */
49
49
  protected isMain(): boolean;
50
- /** {@inheritDoc} */
50
+ /** @inheritDoc */
51
51
  destroyWorker(worker: Worker): void;
52
- /** {@inheritDoc} */
52
+ /** @inheritDoc */
53
53
  protected sendToWorker(worker: Worker, message: MessageValue<Data>): void;
54
- /** {@inheritDoc} */
54
+ /** @inheritDoc */
55
55
  registerWorkerMessageListener<Message extends Data | Response>(worker: Worker, listener: (message: MessageValue<Message>) => void): void;
56
- /** {@inheritDoc} */
56
+ /** @inheritDoc */
57
57
  protected createWorker(): Worker;
58
- /** {@inheritDoc} */
58
+ /** @inheritDoc */
59
59
  protected afterWorkerSetup(worker: Worker): void;
60
- /** {@inheritDoc} */
60
+ /** @inheritDoc */
61
61
  get type(): PoolType;
62
- /** {@inheritDoc} */
62
+ /** @inheritDoc */
63
63
  get full(): boolean;
64
- /** {@inheritDoc} */
64
+ /** @inheritDoc */
65
65
  get busy(): boolean;
66
66
  }
@@ -2,6 +2,8 @@ import type { IPool } from './pool';
2
2
  import type { IPoolWorker } from './pool-worker';
3
3
  /**
4
4
  * Internal pool types.
5
+ *
6
+ * @enum
5
7
  */
6
8
  export declare enum PoolType {
7
9
  FIXED = "fixed",
@@ -50,7 +50,8 @@ export interface IPool<Data = unknown, Response = unknown> {
50
50
  *
51
51
  * Events that can currently be listened to:
52
52
  *
53
- * - `'busy'`
53
+ * - `'full'`: Emitted when the pool is dynamic and full.
54
+ * - `'busy'`: Emitted when the pool is busy.
54
55
  */
55
56
  readonly emitter?: PoolEmitter;
56
57
  /**
@@ -8,22 +8,22 @@ 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, Response> implements IWorkerChoiceStrategy {
12
- protected readonly pool: IPoolInternal<Worker, Data, Response>;
13
- /** {@inheritDoc} */
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>;
13
+ /** @inheritDoc */
14
14
  readonly isDynamicPool: boolean;
15
- /** {@inheritDoc} */
15
+ /** @inheritDoc */
16
16
  requiredStatistics: RequiredStatistics;
17
17
  /**
18
- * Constructs a worker choice strategy attached to the pool.
18
+ * Constructs a worker choice strategy bound to the pool.
19
19
  *
20
20
  * @param pool - The pool instance.
21
21
  */
22
22
  constructor(pool: IPoolInternal<Worker, Data, Response>);
23
- /** {@inheritDoc} */
23
+ /** @inheritDoc */
24
24
  abstract reset(): boolean;
25
- /** {@inheritDoc} */
25
+ /** @inheritDoc */
26
26
  abstract choose(): number;
27
- /** {@inheritDoc} */
27
+ /** @inheritDoc */
28
28
  abstract remove(workerKey: number): boolean;
29
29
  }
@@ -9,18 +9,18 @@ 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, Response> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
13
- /** {@inheritDoc} */
12
+ export declare class FairShareWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
13
+ /** @inheritDoc */
14
14
  readonly requiredStatistics: RequiredStatistics;
15
15
  /**
16
16
  * Worker last virtual task execution timestamp.
17
17
  */
18
18
  private readonly workerLastVirtualTaskTimestamp;
19
- /** {@inheritDoc} */
19
+ /** @inheritDoc */
20
20
  reset(): boolean;
21
- /** {@inheritDoc} */
21
+ /** @inheritDoc */
22
22
  choose(): number;
23
- /** {@inheritDoc} */
23
+ /** @inheritDoc */
24
24
  remove(workerKey: number): boolean;
25
25
  /**
26
26
  * Computes worker last virtual task timestamp.
@@ -8,13 +8,13 @@ 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, Response> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
12
- /** {@inheritDoc} */
11
+ export declare class LessBusyWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
12
+ /** @inheritDoc */
13
13
  readonly requiredStatistics: RequiredStatistics;
14
- /** {@inheritDoc} */
14
+ /** @inheritDoc */
15
15
  reset(): boolean;
16
- /** {@inheritDoc} */
16
+ /** @inheritDoc */
17
17
  choose(): number;
18
- /** {@inheritDoc} */
18
+ /** @inheritDoc */
19
19
  remove(workerKey: number): boolean;
20
20
  }
@@ -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, Response> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
12
- /** {@inheritDoc} */
11
+ export declare class LessUsedWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
12
+ /** @inheritDoc */
13
13
  reset(): boolean;
14
- /** {@inheritDoc} */
14
+ /** @inheritDoc */
15
15
  choose(): number;
16
- /** {@inheritDoc} */
16
+ /** @inheritDoc */
17
17
  remove(workerKey: number): boolean;
18
18
  }
@@ -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, Response> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
11
+ export declare class RoundRobinWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
12
12
  /**
13
13
  * Id of the next worker.
14
14
  */
15
15
  private nextWorkerId;
16
- /** {@inheritDoc} */
16
+ /** @inheritDoc */
17
17
  reset(): boolean;
18
- /** {@inheritDoc} */
18
+ /** @inheritDoc */
19
19
  choose(): number;
20
- /** {@inheritDoc} */
20
+ /** @inheritDoc */
21
21
  remove(workerKey: number): boolean;
22
22
  }
@@ -1,3 +1,5 @@
1
+ import type { IPoolInternal } from '../pool-internal';
2
+ import type { IPoolWorker } from '../pool-worker';
1
3
  /**
2
4
  * Enumeration of worker choice strategies.
3
5
  */
@@ -37,9 +39,13 @@ export interface RequiredStatistics {
37
39
  /**
38
40
  * Worker choice strategy interface.
39
41
  */
40
- export interface IWorkerChoiceStrategy {
42
+ export interface IWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> {
41
43
  /**
42
- * Is the pool attached to the strategy dynamic?.
44
+ * The pool instance.
45
+ */
46
+ readonly pool: IPoolInternal<Worker, Data, Response>;
47
+ /**
48
+ * Is the pool bound to the strategy dynamic?.
43
49
  */
44
50
  readonly isDynamicPool: boolean;
45
51
  /**
@@ -10,8 +10,8 @@ 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, Response> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy {
14
- /** {@inheritDoc} */
13
+ export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends IPoolWorker, Data = unknown, Response = unknown> extends AbstractWorkerChoiceStrategy<Worker, Data, Response> implements IWorkerChoiceStrategy<Worker, Data, Response> {
14
+ /** @inheritDoc */
15
15
  readonly requiredStatistics: RequiredStatistics;
16
16
  /**
17
17
  * Worker id where the current task will be submitted.
@@ -31,11 +31,11 @@ export declare class WeightedRoundRobinWorkerChoiceStrategy<Worker extends IPool
31
31
  * @param pool - The pool instance.
32
32
  */
33
33
  constructor(pool: IPoolInternal<Worker, Data, Response>);
34
- /** {@inheritDoc} */
34
+ /** @inheritDoc */
35
35
  reset(): boolean;
36
- /** {@inheritDoc} */
36
+ /** @inheritDoc */
37
37
  choose(): number;
38
- /** {@inheritDoc} */
38
+ /** @inheritDoc */
39
39
  remove(workerKey: number): boolean;
40
40
  private initWorkersTaskRunTime;
41
41
  private initWorkerTaskRunTime;
@@ -8,10 +8,10 @@ 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, Response> {
12
- private readonly pool;
11
+ export declare class WorkerChoiceStrategyContext<Worker extends IPoolWorker, Data = unknown, Response = unknown> {
13
12
  private readonly createWorkerCallback;
14
- private workerChoiceStrategy;
13
+ private workerChoiceStrategyType;
14
+ private readonly workerChoiceStrategies;
15
15
  /**
16
16
  * Worker choice strategy context constructor.
17
17
  *
@@ -19,9 +19,9 @@ export declare class WorkerChoiceStrategyContext<Worker extends IPoolWorker, Dat
19
19
  * @param createWorkerCallback - The worker creation callback for dynamic pool.
20
20
  * @param workerChoiceStrategy - The worker choice strategy.
21
21
  */
22
- constructor(pool: IPoolInternal<Worker, Data, Response>, createWorkerCallback: () => number, workerChoiceStrategy?: WorkerChoiceStrategy);
22
+ constructor(pool: IPoolInternal<Worker, Data, Response>, createWorkerCallback: () => number, workerChoiceStrategyType?: WorkerChoiceStrategy);
23
23
  /**
24
- * Gets the worker choice strategy required statistics.
24
+ * Gets the worker choice strategy in the context required statistics.
25
25
  *
26
26
  * @returns The required statistics.
27
27
  */
@@ -33,13 +33,13 @@ export declare class WorkerChoiceStrategyContext<Worker extends IPoolWorker, Dat
33
33
  */
34
34
  setWorkerChoiceStrategy(workerChoiceStrategy: WorkerChoiceStrategy): void;
35
35
  /**
36
- * Chooses a worker with the worker choice strategy.
36
+ * Executes the worker choice strategy algorithm in the context.
37
37
  *
38
38
  * @returns The key of the chosen one.
39
39
  */
40
40
  execute(): number;
41
41
  /**
42
- * Removes a worker in the worker choice strategy internals.
42
+ * Removes a worker from the worker choice strategy in the context.
43
43
  *
44
44
  * @param workerKey - The key of the worker to remove.
45
45
  * @returns `true` if the removal is successful, `false` otherwise.
@@ -24,10 +24,10 @@ export declare class DynamicThreadPool<Data = unknown, Response = unknown> exten
24
24
  * @param opts - Options for this dynamic thread pool.
25
25
  */
26
26
  constructor(min: number, max: number, filePath: string, opts?: PoolOptions<ThreadWorkerWithMessageChannel>);
27
- /** {@inheritDoc} */
27
+ /** @inheritDoc */
28
28
  get type(): PoolType;
29
- /** {@inheritDoc} */
29
+ /** @inheritDoc */
30
30
  get full(): boolean;
31
- /** {@inheritDoc} */
31
+ /** @inheritDoc */
32
32
  get busy(): boolean;
33
33
  }
@@ -29,22 +29,22 @@ export declare class FixedThreadPool<Data = unknown, Response = unknown> extends
29
29
  * @param opts - Options for this fixed thread pool.
30
30
  */
31
31
  constructor(numberOfThreads: number, filePath: string, opts?: PoolOptions<ThreadWorkerWithMessageChannel>);
32
- /** {@inheritDoc} */
32
+ /** @inheritDoc */
33
33
  protected isMain(): boolean;
34
- /** {@inheritDoc} */
34
+ /** @inheritDoc */
35
35
  destroyWorker(worker: ThreadWorkerWithMessageChannel): Promise<void>;
36
- /** {@inheritDoc} */
36
+ /** @inheritDoc */
37
37
  protected sendToWorker(worker: ThreadWorkerWithMessageChannel, message: MessageValue<Data>): void;
38
- /** {@inheritDoc} */
38
+ /** @inheritDoc */
39
39
  registerWorkerMessageListener<Message extends Data | Response>(messageChannel: ThreadWorkerWithMessageChannel, listener: (message: MessageValue<Message>) => void): void;
40
- /** {@inheritDoc} */
40
+ /** @inheritDoc */
41
41
  protected createWorker(): ThreadWorkerWithMessageChannel;
42
- /** {@inheritDoc} */
42
+ /** @inheritDoc */
43
43
  protected afterWorkerSetup(worker: ThreadWorkerWithMessageChannel): void;
44
- /** {@inheritDoc} */
44
+ /** @inheritDoc */
45
45
  get type(): PoolType;
46
- /** {@inheritDoc} */
46
+ /** @inheritDoc */
47
47
  get full(): boolean;
48
- /** {@inheritDoc} */
48
+ /** @inheritDoc */
49
49
  get busy(): boolean;
50
50
  }
@@ -6,7 +6,7 @@ import { AsyncResource } from 'node:async_hooks';
6
6
  import type { Worker } from 'node:cluster';
7
7
  import type { MessagePort } from 'node:worker_threads';
8
8
  import type { MessageValue } from '../utility-types';
9
- import { type WorkerOptions } from './worker-options';
9
+ import type { WorkerOptions } from './worker-options';
10
10
  /**
11
11
  * Base class that implements some shared logic for all poolifier workers.
12
12
  *
@@ -17,6 +17,7 @@ import { type WorkerOptions } from './worker-options';
17
17
  export declare abstract class AbstractWorker<MainWorker extends Worker | MessagePort, Data = unknown, Response = unknown> extends AsyncResource {
18
18
  protected readonly isMain: boolean;
19
19
  protected mainWorker: MainWorker | undefined | null;
20
+ protected readonly opts: WorkerOptions;
20
21
  /**
21
22
  * Timestamp of the last task processed by this worker.
22
23
  */
@@ -25,10 +26,6 @@ export declare abstract class AbstractWorker<MainWorker extends Worker | Message
25
26
  * Handler Id of the `aliveInterval` worker alive check.
26
27
  */
27
28
  protected readonly aliveInterval?: NodeJS.Timeout;
28
- /**
29
- * Options for the worker.
30
- */
31
- readonly opts: WorkerOptions;
32
29
  /**
33
30
  * Constructs a new poolifier worker.
34
31
  *
@@ -25,8 +25,8 @@ export declare class ClusterWorker<Data = unknown, Response = unknown> extends A
25
25
  * @param opts - Options for the worker.
26
26
  */
27
27
  constructor(fn: (data: Data) => Response, opts?: WorkerOptions);
28
- /** {@inheritDoc} */
28
+ /** @inheritDoc */
29
29
  protected sendToMainWorker(message: MessageValue<Response>): void;
30
- /** {@inheritDoc} */
30
+ /** @inheritDoc */
31
31
  protected handleError(e: Error | string): string;
32
32
  }
@@ -25,6 +25,6 @@ export declare class ThreadWorker<Data = unknown, Response = unknown> extends Ab
25
25
  * @param opts - Options for the worker.
26
26
  */
27
27
  constructor(fn: (data: Data) => Response, opts?: WorkerOptions);
28
- /** {@inheritDoc} */
28
+ /** @inheritDoc */
29
29
  protected sendToMainWorker(message: MessageValue<Response>): void;
30
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poolifier",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
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",
@@ -88,7 +88,7 @@
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
- "eslint-import-resolver-typescript": "^3.5.4",
91
+ "eslint-import-resolver-typescript": "^3.5.5",
92
92
  "eslint-plugin-import": "^2.27.5",
93
93
  "eslint-plugin-jsdoc": "^40.1.1",
94
94
  "eslint-plugin-n": "^15.7.0",
@@ -1,11 +0,0 @@
1
- import type { IPoolInternal } from '../pool-internal';
2
- import type { IPoolWorker } from '../pool-worker';
3
- import type { IWorkerChoiceStrategy, WorkerChoiceStrategy } from './selection-strategies-types';
4
- /**
5
- * Gets the worker choice strategy instance.
6
- *
7
- * @param pool - The pool instance.
8
- * @param workerChoiceStrategy - The worker choice strategy.
9
- * @returns The worker choice strategy instance.
10
- */
11
- export declare function getWorkerChoiceStrategy<Worker extends IPoolWorker, Data, Response>(pool: IPoolInternal<Worker, Data, Response>, workerChoiceStrategy?: WorkerChoiceStrategy): IWorkerChoiceStrategy;