Source: JobQueueCapabilities.js

const { Job, JobQueue, JobQueueCapacityPolicy } = require('./JobQueue');


/**
 * @extends {JobQueue}
 * @author Sebastian Hönel <development@hoenel.net>
 */
class JobQueueCapabilities extends JobQueue {
	/**
	 * @param {Number} capabilities 
	 * @param {Boolean} [allowExclusiveJobs] Optional. Defaults to false. A
	 * boolean that indicates whether jobs that require the whole queue's
	 * capabilities are allowed on this queue. Such jobs have a cost equal
	 * to or higher than the queue's capabilities.
	 * @param {Number} [capacity] Optional. The maximum capacity of this
	 * JobQueue. Only has an effect if the chosen policy is not set to
	 * ignore excess items. The maximum capacity is the total amount of
	 * Jobs either currently running or in the backlog this JobQueue can
	 * accomodate.
	 * @param {JobQueueCapacityPolicy|Number} Optional. The policy to
	 * use when the maximum capacity is reached, and new items are being
	 * enqueued.
	 */
	constructor(capabilities, allowExclusiveJobs = false, capacity = Number.MAX_SAFE_INTEGER, capacityPolicy = JobQueueCapacityPolicy.Ignore) {
		super(Number.MAX_SAFE_INTEGER, capacity, capacityPolicy);
		if (isNaN(capabilities) || capabilities <= 0) {
			throw new Error('The capabilities must be a positive number.');
		}
		this.capabilities = capabilities;
		this.allowExclusiveJobs = !!allowExclusiveJobs;
	};

	/**
	 * Returns the total cost of all jobs that need to be run.
	 * 
	 * @type {Number}
	 */
	get backlogCost() {
		return this.queue.map(job => job.cost).reduce((a, b) => a + b, 0);
	};

	/**
	 * Returns a number that indicates the ratio between currently
	 * running and enqueued jobs and this queue's capabilities. 0 means that
	 * the queue is idle; a value close or equal to one means that the queue
	 * runs at or near its full capacity and a value greater than one means
	 * that there are exclusive jobs or a non-empty backlog of waiting jobs.
	 * Albeit overwritten, this property can be perfectly used to compare the
	 * load of parallel job queues.
	 * 
	 * @type {Number}
	 */
	get load() {
		const totalCost = this.backlogCost + this.capabilitiesUsed;
		return totalCost / this.capabilities;
	};

	/**
	 * Returns number indicating the utilization of this queue in terms of a
	 * ratio between used capabilities and available capabilities. Note that,
	 * if this queue allows exclusive jobs, the utilization may be greater
	 * than 100%. The range therefore is [0, infinity].
	 * 
	 * @type {Number}
	 */
	get utilization() {
		return this.capabilitiesUsed / this.capabilities;
	};

	/**
	 * Only returns true, iff the remaining capabilities are exactly 0 (zero).
	 * 
	 * @type {Boolean}
	 */
	get isBusy() {
		return this.capabilitiesFree === 0;
	};

	/**
	 * Return the accumulated cost of all currently running jobs.
	 * 
	 * @type {Number}
	 */
	get capabilitiesUsed() {
		return this.currentJobs.map(j => j.cost).reduce((a, b) => a + b, 0);
	};

	/**
	 * Returns the remaining capabilities. Exclusive jobs may use more capabilities than
	 * this queue provides. However, this property can at minimum only return 0 (zero)
	 * (i.e. not negative values).
	 * 
	 * @type {Number}
	 */
	get capabilitiesFree() {
		return Math.max(0, this.capabilities - this.capabilitiesUsed);
	};

	/**
	 * @template T
	 * @override
	 * @inheritdoc
	 * @param {Job|producerHandler<Promise<T>>} job 
	 * @param {Number} [cost]
	 */
	addJob(job, cost = void 0) {
		if (!(job instanceof Job)) {
			if (job instanceof Function) {
				job = new Job(job);
			} else {
				throw new Error(`The given Job is not an instance of Job nor is it an instance of Function.`);
			}
		}

		if (!job.hasCost && (Object.prototype.toString.call(cost) !== '[object Number]' || cost <= 0)) {
			throw new Error(`You must provide a valid value for parameter cost. Given: '${cost}'`);
		}
		job.cost = cost || job.cost;

		if (job.cost >= this.capabilities && !this.allowExclusiveJobs) {
			throw new Error(`The job's cost of ${job.cost} exceeds the queue's capabilities of ${this.capabilities} and this queue does not allow such (exclusive) jobs.`);
		}

		return super.addJob(job);
	};

	/**
	 * @protected
	 * @override
	 * @inheritdoc
	 */
	_runNext() {
		// Check next job's cost
		if (this.backlog > 0 && this.queue.peekIndex(0).cost > this.capabilitiesFree && this.numJobsRunning > 0) {
			return; // Wait for more jobs to finish
		}

		super._runNext();
	};
};


module.exports = Object.freeze({
	JobQueueCapabilities
});