Source: ResourceSelector.js

const { Collection } = require('./collections/Collection')
, { EqualityComparer } = require('./collections/EqualityComparer')
, { getRandomNumber } = require('./tools/Random');


/**
 * This enumeration holds supported selection strategies for the class
 * @see {ResourceSelector}.
 * 
 * @readonly
 * @enum {Number}
 */
const ResourceSelectionStrategy = {
	/**
	 * The @see {ResourceSelector} will throw without a strategy.
	 */
	None: 0,
	/**
	 * Randomly select
	 */
	Random: 1,
	/**
	 * Least-recently used
	 */
	LRU: 2,
	/**
	 * Least-used
	 */
	LU: 3,
	/**
	 * Most-recently used
	 */
	MRU: 4,
	/**
	 * Most-used
	 */
	MU: 5,
	/**
	 * Least-recently added
	 */
	LRA: 6,
	/**
	 * Most-recently added
	 */
	MRA: 7,
	/**
	 * Round-Robin
	 */
	RoundRobin: 8
};


/**
 * Encapsulate any item that is a resource.
 * 
 * @template T
 * @author Sebastian Hönel <development@hoenel.net>
 */
class Resource {
	/**
	 * @param {T} resource The actual resource
	 */
	constructor(resource) {
		/** @protected */
		this._resource = resource;
		/** @protected */
		this._numUsed = 0;
		/** @protected */
		this._created = Date.now();
		/**
		 * @protected
		 * @type {Number}
		 */
		this._lastUsed = null;
	};

	get numUsed() {
		return this._numUsed;
	};

	get created() {
		return this._created;
	};

	get lastUsed() {
		return this._lastUsed;
	};

	/**
	 * Obtain the resource and increase the internal use-counter
	 * and last-used date.
	 * 
	 * @returns {T} The underlying resource.
	 */
	use() {
		this._numUsed++;
		this._lastUsed = Date.now();
		return this._resource;
	};
};


/**
 * Given any number of resources, this class helps to select an appropriate
 * resource by a pre-defined strategy, such as least-recently used.
 * 
 * @template T
 * @extends {Collection<Resource<T>>}
 * @author Sebastian Hönel <development@hoenel.net>
 */
class ResourceSelector extends Collection {
	/**
	 * @param {ResourceSelectionStrategy|Number} strategy
	 * @param {EqualityComparer<T>} eqComparer
	 */
	constructor(strategy = ResourceSelectionStrategy.RoundRobin, eqComparer = EqualityComparer.default) {
		super(eqComparer);

		/**
		 * @protected
		 * @type {ResourceSelectionStrategy|Number}
		 */
		this._strategy = null;
		this.strategy = strategy;
	};

	/**
	 * @protected
	 * @param {ResourceSelectionStrategy|Number} strategy
	 * @throws {Error} If the given strategy is not valid.
	 * @returns {ResourceSelectionStrategy}
	 */
	_validateStrategy(strategy) {
		switch (strategy) {
			case ResourceSelectionStrategy.None:
			case ResourceSelectionStrategy.Random:
			case ResourceSelectionStrategy.LRU:
			case ResourceSelectionStrategy.LU:
			case ResourceSelectionStrategy.MRU:
			case ResourceSelectionStrategy.MU:
			case ResourceSelectionStrategy.LRA:
			case ResourceSelectionStrategy.MRA:
			case ResourceSelectionStrategy.RoundRobin:
				return strategy;
			default:
				throw new Error(`The strategy '${strategy}' is not known.`);
		}
	};

	/**
	 * @type {ResourceSelectionStrategy|Number}
	 */
	get strategy() {
		return this._strategy;
	};

	/**
	 * @param {ResourceSelectionStrategy|Number} strategy
	 */
	set strategy(strategy) {
		this._strategy = this._validateStrategy(strategy);
	};

	/**
	 * @type {Boolean}
	 */
	get hasResources() {
		return this._items.length > 0;
	};

	/**
	 * @param {T} resource A resource
	 * @throws {Error} If the resource was added previously.
	 * @returns {this}
	 */
	addResource(resource) {
		if (this.hasResource(resource)) {
			throw new Error('This resource has already been added.');
		}
		this._items.push(new Resource(resource));
		return this;
	};

	/**
	 * Note that this method compares the actual resource (@see {T}),
	 * not its wrapper @see {Resource<T>}.
	 * 
	 * @param {T} resource
	 * @returns {Boolean}
	 */
	hasResource(resource) {
		const idx = this._items.findIndex(res =>
			this.equalityComparer.equals(res._resource, resource));
		return idx >= 0;
	};

	/**
	 * Remove a previously added resource from this selector.
	 * 
	 * @param {T} resource The resource to remove.
	 * @returns {this}
	 */
	removeResource(resource) {
		const idx = this._items.findIndex(res =>
			this.equalityComparer.equals(res._resource, resource));

		if (idx < 0) {
			throw new Error('Unknown resource.');
		}
		this._items.splice(idx, 1);

		return this;
	};

	/**
	 * The main method to select the next resource. You must not rely on the
	 * expected order if all resources are yet unused
	 * 
	 * @returns {Resource<T>} The next resource, according to the currently
	 * selected strategy. It can be used by calling @see {Resource.use()}.
	 */
	select() {
		if (!this.hasResources) {
			throw new Error('No resources available.');
		}

		/** @type {Array<Resource<T>>} */
		let sort = null;

		switch (this.strategy) {
			case ResourceSelectionStrategy.RoundRobin:
				const res = this._items.shift();
				this._items.push(res);
				return res;
			case ResourceSelectionStrategy.Random:
				return this._items.sort(() => getRandomNumber())[1];
			case ResourceSelectionStrategy.LU:
			case ResourceSelectionStrategy.MU:
				sort = this.asArray.sort((a, b) => a.numUsed - b.numUsed);
				return this.strategy === ResourceSelectionStrategy.LU ? sort.shift() : sort.pop();
			case ResourceSelectionStrategy.LRU:
			case ResourceSelectionStrategy.MRU:
				sort = this.asArray.sort((a, b) => {
					if (a.lastUsed === null) {
						return -1;
					} else if (b.lastUsed === null) {
						return 1;
					}
					return a.lastUsed - b.lastUsed;
				});
				return this.strategy === ResourceSelectionStrategy.LRU ? sort.shift() : sort.pop();
			case ResourceSelectionStrategy.LRA:
			case ResourceSelectionStrategy.MRA:
				sort = this.asArray.sort((a, b) => a.created - b.created);
				return this.strategy === ResourceSelectionStrategy.LRA ? sort.shift() : sort.pop();
			case ResourceSelectionStrategy.None:
			default:
				throw new Error('No selection strategy selected.');
		}
	};
};


module.exports = Object.freeze({
	Resource,
	ResourceSelectionStrategy,
	ResourceSelector
});