UNPKG

@softvisio/core

Version:
284 lines (213 loc) • 6.9 kB
import "#lib/result"; import Deque from "#lib/data-structures/deque"; import Events from "#lib/events"; import ProxyFinalizationRegistry from "#lib/proxy-finalization-registry"; import Signal from "#lib/threads/signal"; const DEFAULT_MAX_RUNNING_THREADS = 1, DEFAULT_MAX_WAITING_THREADS = Infinity; class ThreadsPoolsSet extends ProxyFinalizationRegistry { // protected _createTarget ( id, destroy, options = {} ) { return new ThreadsPool( { ...options, id, destroy } ); } _isTargetDestroyable ( target ) { return target.isDestroyable; } } export default class ThreadsPool { #id; #destroy; #maxRunningThreads; #maxWaitingThreads; #runningThreads = 0; #waitingThreads = new Deque(); #isPaused = false; #isDestroying = false; #destroySignal; #_events; constructor ( { id, destroy, maxRunningThreads, maxWaitingThreads } = {} ) { this.#id = id; this.#destroy = destroy; this.#setMaxRunningThreads( maxRunningThreads ?? DEFAULT_MAX_RUNNING_THREADS ); this.maxWaitingThreads = maxWaitingThreads ?? DEFAULT_MAX_WAITING_THREADS; } // static static get Set () { return ThreadsPoolsSet; } // properties get id () { return this.#id; } get isDestroyable () { return !this.#_events?.hasListeners() && this.isEmpty && !this.isPaused; } get maxRunningThreads () { return this.#maxRunningThreads; } set maxRunningThreads ( value ) { const oldValue = this.#maxRunningThreads; this.#setMaxRunningThreads( value ); if ( this.#maxRunningThreads > oldValue ) { this.#runWaitingThreads(); } } get maxWaitingThreads () { return this.#maxWaitingThreads; } set maxWaitingThreads ( value ) { if ( value !== Infinity ) { if ( value !== Infinity && ( !Number.isInteger( value ) || value < 0 ) ) throw TypeError( "Semaphore maxWaitingThreads value must be integer >= 0 or Infinity" ); } this.#maxWaitingThreads = value; } get runningThreads () { return this.#runningThreads; } get freeThreads () { if ( this.#isDestroying ) return 0; return this.#realFreeThreads; } get waitingThreads () { return this.#waitingThreads.length; } get freeWaitingThreads () { if ( this.#isDestroying ) return 0; const freeWaitingThreads = this.#maxWaitingThreads - this.waitingThreads; return freeWaitingThreads < 0 ? 0 : freeWaitingThreads; } get totalThreads () { return this.#maxRunningThreads + this.#maxWaitingThreads; } get totalFreeThreads () { return this.freeThreads + this.freeWaitingThreads; } get isEmpty () { return !this.#runningThreads && !this.waitingThreads; } get isPaused () { return this.#isPaused; } get isDestroying () { return this.#isDestroying; } get stats () { return { "maxRunningThreads": this.maxRunningThreads, "maxWaitingThreads": this.maxWaitingThreads, "totalThreads": this.totalThreads, "runningThreads": this.runningThreads, "freeThreads": this.freeThreads, "waitingThreads": this.waitingThreads, "freeWaitingThreads": this.freeWaitingThreads, "totalFreeThreads": this.totalFreeThreads, }; } // public async runThread ( method, { args, highPriority } = {} ) { return this.#runThread( method, args, highPriority ); } pause () { this.#isPaused = true; return this; } resume () { if ( this.#isPaused ) { this.#isPaused = false; this.#runWaitingThreads(); } return this; } // XXX whet to do, if is paused async destroy () { if ( !this.#isDestroying ) { this.#isDestroying = true; this.#destroySignal = new Signal(); } if ( this.isEmpty ) return; return this.#destroySignal.wait(); } on ( name, listener ) { this.#events.on( name, listener ); return this; } once ( name, listener ) { this.#events.once( name, listener ); return this; } off ( name, listener ) { this.#events.off( name, listener ); return this; } // private get #events () { if ( !this.#_events ) { this.#_events = new Events().watch( ( name, subscribe ) => { if ( !this.#_events.hasListeners() ) { this.#destroy?.(); } } ); } return this.#_events; } get #realFreeThreads () { if ( this.#isPaused ) return 0; const freeThreads = this.#maxRunningThreads - this.#runningThreads; return freeThreads < 0 ? 0 : freeThreads; } #setMaxRunningThreads ( value ) { if ( !Number.isInteger( value ) || value <= 0 ) throw TypeError( "Semaphore maxRunningThreads value must be integer > 0" ); this.#maxRunningThreads = value; } async #runThread ( method, args, highPriority ) { // service is destroying if ( this.#isDestroying ) return result( -32_816 ); // start thread immediately if ( this.#realFreeThreads ) { this.#runningThreads++; } // add thread to the queue else { // queue is full, too many requests" if ( !this.freeWaitingThreads ) return result( -32_802 ); if ( highPriority ) { await new Promise( resolve => this.#waitingThreads.unshift( resolve ) ); } else { await new Promise( resolve => this.#waitingThreads.push( resolve ) ); } } var res; // run thread try { res = result.try( await method( ...( args || [] ) ), { "allowUndefined": true } ); } catch ( e ) { res = result.catch( e ); } // finish thread this.#runningThreads--; this.#runWaitingThreads(); return res; } #runWaitingThreads () { // run waiting threads while ( this.waitingThreads && this.#realFreeThreads ) { this.#runningThreads++; this.#waitingThreads.shift()(); } // has free threads if ( this.freeThreads ) this.#_events?.emit( "freeThreads", this ); // pool is empty if ( this.isEmpty ) { // destroy complete if ( this.#isDestroying ) this.#destroySignal.broadcast(); this.#_events?.emit( "empty", this ); this.#destroy?.(); } } }