@typescript-package/queue
Version:
A lightweight TypeScript library for managing various queue and stack structures.
854 lines (843 loc) • 26.1 kB
JavaScript
import { ArrayState, State, Boolean, Ability } from '@typescript-package/state';
// Abstract.
/**
* @description Array state elements in data structures such as Stack and Queue.
* @export
* @class Elements
* @template Type
* @extends {ArrayState<Type>}
*/
class Elements extends ArrayState {
/**
* @description The maximum size of the `Elements`.
* @public
* @readonly
* @type {Size}
*/
get size() {
return this.#size;
}
/**
* @description Privately stored maximum elements size.
* @type {Size}
*/
#size;
/**
* Creates an instance of `Elements`.
* @constructor
* @param {Type[]} elements
* @param {Size} [size=Infinity as Size]
*/
constructor(elements, size = Infinity) {
super(elements.length <= size ? elements : []);
// Sets the size.
this.#size = size;
// Throws an error if the elements exceeds the maximum size.
if (elements.length > size) {
throw new Error(`The \`elements\` size exceeds the maximum size ${size} by ${elements.length - size}.`);
}
}
/**
* @inheritdoc
* @public
* @param {Type} element The element of `Type` to append.
* @returns {this}
*/
append(element) {
this.#checkFull();
super.append(element);
return this;
}
/**
* @inheritdoc
* @public
* @param {number} index The index under which the specified `element` is inserted.
* @param {Type} element The element of `Type` to insert at specified `index`.
* @returns {this}
*/
insert(index, element) {
this.#checkFull();
super.insert(index, element);
return this;
}
/**
* @description Checks whether the `Elements` state is full, equal to size.
* @public
* @returns {boolean}
*/
isFull() {
return this.#size === this.length;
}
/**
* @description Add the element at the beginning of `array` state.
* @public
* @param {Type} element The element of `Type` to prepend.
* @returns {this}
*/
prepend(element) {
this.#checkFull();
super.prepend(element);
return this;
}
/**
* @inheritdoc
* @public
* @param {number} index The index to update update element.
* @param {Type} element The element of `Type` to update under the specified `index`.
* @returns {this}
*/
update(index, element) {
super.update(index, element);
return this;
}
/**
* @description Checks whether length of the array is equal to maximum size.
* @returns {this}
*/
#checkFull() {
if (this.isFull()) {
throw new Error(`Elements array state is full of size ${super.length}.`);
}
return this;
}
}
;
// Class.
/**
* @description Class designed for asynchronous processing the promises of `void`.
* @export
* @class Processing
* @extends {State<Set<Promise<void>>>} The state for active processing promises, tracking the status of asynchronous operations.
*/
class Processing extends State {
/**
* @description Tracks whether there are actively processed promises.
* @public
* @readonly
* @type {boolean}
*/
get active() {
return super.state.size > 0;
}
/**
* @description A current number of promises being processed.
* @public
* @readonly
* @type {number}
*/
get activeCount() {
return super.state.size;
}
/**
* @description Returns the first promise from processing.
* @public
* @readonly
* @type {Promise<void>}
*/
get first() {
return Array.from(super.state)[0];
}
/**
* @description Returns the last promise from processing.
* @public
* @readonly
* @type {Promise<void>}
*/
get last() {
return Array.from(super.state)[super.state.size - 1];
}
/**
* @description
* @type {*}
*/
#debug = new Boolean(false);
/**
* Creates a `Processing` object.
* @constructor
*/
constructor() {
super(new Set());
}
/**
* @description Adds the promise to the processing state.
* @public
* @param {Promise<void>} promise The promise of `void` to add.
* @returns {this}
*/
add(promise, remove = true) {
super.state.add(promise);
this.#consoleDebug("`Promise` added to processing state", { active: this.active, activeCount: this.activeCount });
remove === true && promise.finally(() => this.delete(promise));
return this;
}
/**
* @description Returns `Promise` that waits for the processing completion.
* @public
* @async
* @returns {Promise<void>}
*/
async complete() {
this.#consoleDebug("Invoked `Processing.complete()` to wait for all processes from the state ", { activeCount: this.activeCount });
const promise = Promise.all(super.state);
await promise;
if (this.#debug.isTrue()) {
promise.finally(() => this.#consoleDebug("`Processing.complete()` finally method invoked.", { activeCount: this.activeCount }));
}
}
/**
* @description Sets the `Processing` to debug state.
* @public
*/
debug() {
this.#debug.true();
return this;
}
/**
* @description Removes the specified promise from the processing state.
* @public
* @param {Promise<void>} promise
* @returns {this}
*/
delete(promise = this.first) {
this.#consoleDebug("`activeCount` state before removing the `Promise`", { activeCount: this.activeCount });
super.state.delete(promise);
this.#consoleDebug("`Promise` removed from processing state", { activeCount: this.activeCount });
return this;
}
/**
* @description Checks whether the `Processing` is active.
* @public
* @param {?boolean} [expected] An optional `boolean` type value to check the active state.
* @returns {boolean}
*/
isActive(expected) {
return typeof expected === 'boolean' ? this.active === expected : this.active;
}
/**
* @description Unset the `Processing` from the debug state.
* @public
*/
unDebug() {
this.#debug.false();
return this;
}
/**
* @description Display the console debug on debug state `true`.
* @param {string} message
* @param {?*} [data]
* @returns {this}
*/
#consoleDebug(message, data) {
this.#debug.isTrue() && console.debug(message, data || '');
return this;
}
}
// Class.
/**
* @description A standard FIFO (First In, First Out) queue.
* @export
* @abstract
* @class Queue
* @template Type
*/
class Queue {
/**
* @description The `Elements` state holder.
* @public
* @readonly
* @type {Elements<Type>}
*/
get elements() {
return this.#elements;
}
/**
* @description The actual queue length.
* @public
* @readonly
* @type {number}
*/
get length() {
return this.#elements.length;
}
/**
* @description The maximum queue size.
* @public
* @readonly
* @type {Size}
*/
get size() {
return this.#size;
}
/**
* @description The actual queue `Elements` state - raw `array` state of the queue.
* @public
* @readonly
* @type {readonly Type[]}
*/
get state() {
return this.#elements.state;
}
/**
* @description Privately stored maximum queue size of generic type variable `Size`.
* @type {Size}
*/
#size;
/**
* @description Privately stored `Array` queue elements state of `Elements`.
* @type {Elements<Type>}
*/
#elements;
/**
* Creates an instance of child class.
* @constructor
* @param {Size} [size=Infinity as Size] The maximum size of the `Queue`.
* @param {...Type[]} elements The arbitrary parameters of elements of `Type` to add.
*/
constructor(size = Infinity, ...elements) {
this.#size = size;
this.#elements = new Elements(elements, size);
}
/**
* @description Clears the queue.
* @public
* @returns {this}
*/
clear() {
this.#elements.clear();
return this;
}
/**
* @description Removes and returns the first (front) element from the queue.
* @public
* @returns {(Type | undefined)}
*/
dequeue() {
const first = this.#elements.first();
this.#elements.remove(0);
return first;
}
/**
* @description Adds a new element to the queue.
* @public
* @param {Type} element The element of `Type` to add.
* @returns {this}
*/
enqueue(element) {
if (this.isFull()) {
throw new Error(`Queue is full.`);
}
this.#elements.append(element);
return this;
}
/**
* @description Checks if the queue is empty.
* @public
* @returns {boolean}
*/
isEmpty() {
return this.length === 0;
}
/**
* @description Checks if the queue is full.
* @public
* @returns {boolean}
*/
isFull() {
return this.#elements.isFull();
}
/**
* @description Returns the first element in the queue.
* @public
* @returns {Type}
*/
peek() {
return this.#elements.first();
}
}
// Class.
/**
* @description A class designed to manage and execute a collection of asynchronous tasks with concurrently control or synchronous tasks.
* @export
* @class Tasks
* @template Type
* @extends {Processable}
*/
class Tasks extends Ability {
/**
* @description The maximum number of elements that can be processed concurrently.
* @public
* @readonly
* @type {Concurrency}
*/
get concurrency() {
return this.#concurrency;
}
/**
* @description Returns the processed elements.
* @public
* @readonly
* @type {Set<Type>}
*/
get processed() {
return this.#processed;
}
/**
* @description Returns the `Processing` object that contains active tasks.
* @public
* @readonly
* @type {Processing<Type, Concurrency>}
*/
get processing() {
return this.#processing;
}
/**
* @description Active state for synchronous processing.
* @type {Active}
*/
#active = new Boolean(false);
/**
* @description Privately stored maximum number of elements that can be processed concurrently.
* @type {Concurrency}
*/
#concurrency;
/**
* @description Privately stored debug state.
* @type {Debug}
*/
#debug = new Boolean(false);
/**
* @description A set of processed elements.
* @type {Set<Type>}
*/
#processed = new Set();
/**
* @description Privately stored `Processing` object that contains active tasks.
* @type {Processing}
*/
#processing;
/**
* Creates an instance of `Tasks`.
* @constructor
* @param {Concurrency} concurrency
*/
/**
* Creates an instance of `Tasks`.
* @constructor
* @param {boolean} enabled Enable initially `Tasks` functionality.
* @param {Concurrency} concurrency The maximum number of elements that can be processed concurrently.
*/
constructor(enabled, concurrency) {
super(enabled);
this.#processing = new Processing();
this.#concurrency = concurrency;
}
/**
* @description Set the `Tasks` to debug state.
* @public
*/
debug() {
this.#debug.true();
this.#processing.debug();
return this;
}
/**
* @description Runs asynchronous single processing on the `element`.
* @public
* @async
* @param {Type} element The element to process.
* @param {ProcessCallback<Type>} callbackFn The callback function to process the element.
* @param {ErrorCallback<Type>} [onError] An optional error handler.
* @returns {Promise<void>}
*/
async asyncProcess(element, callbackFn, onError, onProcessed) {
if (this.isDisabled()) {
throw new Error(`Enable the functionality to use the \`asyncProcess()\` method.`);
}
this.#consoleDebug("asyncProcess started", { element });
// Create a promise.
const task = (async () => {
try {
this.#consoleDebug("Processing element:", element);
await callbackFn(element);
}
catch (error) {
this.#consoleDebug("Error occurred during processing:", { element, error });
onError?.(element, error);
}
finally {
onProcessed?.(element); // What to do with the processed
this.#processed.add(element);
this.#consoleDebug("Element processed:", { element, processed: this.#processed.size });
}
})();
// Add the task to the processing state.
await this.#processing.add(task);
}
/**
* @description Starts asynchronous processing elements with concurrency control.
* @public
* @async
* @param {Iterable<Type>} elements The elements to process.
* @param {ProcessCallback<Type>} callbackFn The function to process each element.
* @param {?ErrorCallback<Type>} [onError] An optional error handler.
* @param {('default' | 'race')} [method='default']
* @returns {Promise<void>}
*/
async asyncRun(elements, callbackFn, onError, onProcessed, method = 'default') {
if (this.isDisabled()) {
throw new Error(`Enable the functionality to use the \`asyncRun()\` method.`);
}
this.#consoleDebug("asyncRun started", { method, concurrency: this.#concurrency });
switch (method) {
case 'race':
this.#consoleDebug("Using 'race' method");
for (const element of elements) {
this.#consoleDebug("Processing element with 'race'", { element, activeCount: this.#processing.activeCount });
this.#processing.activeCount >= this.#concurrency && await Promise.race(this.#processing.state);
this.asyncProcess(element, callbackFn, onError, onProcessed);
}
break;
case 'all':
default:
this.#consoleDebug("Using the 'default' / 'all' method");
const iterator = elements[Symbol.iterator]();
// Create the async process for the task.
const process = async () => {
while (this.#processing.activeCount < this.#concurrency) {
const { value: element, done } = iterator.next();
if (done)
break;
this.#consoleDebug("Processing element with default", { element, concurrency: this.#concurrency, activeCount: this.#processing.activeCount });
const task = this
.asyncProcess(element, callbackFn, onError, onProcessed)
.finally(() => (this.#processing.delete(task), process()));
this.#consoleDebug("Add the processed task to the processing.", { element, task });
this.#processing.add(task, false);
}
// Wait for the tasks to finish.
await Promise.all(this.#processing.state);
};
await process();
break;
}
this.#consoleDebug("asyncRun completed");
await this.#processing.complete();
return this.#processed;
}
/**
* @description Runs a synchronous processing on the provided `element` using the `callbackFn`.
* If an `onError` callback is provided, it will handle any errors encountered during processing.
* @param {(Type | undefined)} element The element to be processed.
* @param {ProcessCallback<Type>} callbackFn A function that processes the element synchronously.
* @param {?ErrorCallback<Type>} [onError] An optional callback function to handle errors during processing.
* @returns {this} The current instance for method chaining.
*/
process(element, callbackFn, onError, onProcessed) {
if (this.isDisabled()) {
throw new Error(`Enable the functionality to use the \`process()\` method.`);
}
this.#consoleDebug("process started", { element });
this.#active.isFalse() && this.#active.true();
this.#consoleDebug("Processing state activated", { active: this.#active.state });
if (element) {
try {
this.#consoleDebug("Processing element", { element });
callbackFn(element);
}
catch (error) {
this.#consoleDebug("Error during processing", { error, element });
onError?.(element, error);
}
finally {
onProcessed?.(element);
// Add to the processed.
this.#processed.add(element);
this.#consoleDebug("Element processed", { element, processedCount: this.#processed.size });
this.#active.false();
this.#consoleDebug("Processing state deactivated", { active: this.#active.state });
}
}
return this;
}
/**
* @description Runs the provided `callbackFn` synchronously on each element in the `elements` iterable.
* If an `onError` callback is provided, it will handle errors encountered during processing.
* @public
* @param {Iterable<Type>} elements An iterable collection of elements to be processed.
* @param {ProcessCallback<Type>} callbackFn A function that will process each element synchronously.
* @param {?ErrorCallback<Type>} [onError] Optional callback for handling errors that occur during processing.
*/
run(elements, callbackFn, onError, onProcessed) {
if (this.isDisabled()) {
throw new Error(`Enable the functionality to use the \`run()\` method.`);
}
this.#consoleDebug("run started", { elements });
for (const element of elements) {
this.#consoleDebug("Processing element synchronously", { element });
this.process(element, callbackFn, onError, onProcessed);
}
this.#consoleDebug("run completed");
}
/**
* @description Unset the `Tasks` from debug state.
* @public
*/
unDebug() {
this.#debug.false();
this.#processing.unDebug();
return this;
}
/**
* @description Console debug the important steps of the `Tasks` functionality on debug state `true`.
* @param {string} message
* @param {?*} [data]
* @returns {this}
*/
#consoleDebug(message, data) {
this.#debug.isTrue() && console.debug(message, data || '');
return this;
}
}
// Abstract.
/**
* @description A task queue that processes elements concurrently with a specified concurrency limit.
* @export
* @class TaskQueue
* @template Type
* @template {number} [Concurrency=number]
* @template {number} [Size=number]
* @extends {Queue<Type, Size>}
*/
class TaskQueue extends Queue {
/**
* @description The maximum number of elements that can be processed concurrently.
* @public
* @readonly
* @type {Concurrency}
*/
get concurrency() {
return this.#tasks.concurrency;
}
/**
* @description Returns the processed elements.
* @public
* @readonly
* @type {Set<Type>}
*/
get processed() {
return this.#tasks.processed;
}
/**
* @description Returns the `Processing` object that contains active tasks.
* @public
* @readonly
* @type {Processing<Type, Concurrency>}
*/
get processing() {
return this.#tasks.processing;
}
/**
* @description The `Tasks` object to handle the processing.
* @type {Tasks<Type, Concurrency>}
*/
#tasks;
/**
* Creates an instance of child class.
* @constructor
* @param {Concurrency} [concurrency=1 as Concurrency]
* @param {Size} [size=Infinity as Size]
* @param {...Type[]} elements
*/
constructor(concurrency = 1, size = Infinity, ...elements) {
super(size, ...elements);
this.#tasks = new Tasks(true, concurrency);
}
/**
* @description Checks whether the queue processing is completed.
* @public
* @returns {boolean}
*/
isCompleted() {
return super.length === 0 && this.#tasks.processing.activeCount === 0 && this.#tasks.processing.isActive(false);
}
//#region Public async
/**
* @description Waits for all elements in the queue to be processed and returns the set of processed elements.
* @public
* @async
* @returns {Promise<Set<Type>>}
*/
async onCompleted() {
return new Promise((resolve, reject) => {
const interval = setInterval(() => this.#tasks.processing.isActive()
? super.length === 0 && resolve([]) // TODO: this.#tasks.processed
: clearInterval(interval), 50);
});
}
/**
* @description Starts asynchronous processing queue elements with concurrency control.
* @public
* @async
* @param {ProcessCallback<Type>} callbackFn The function to process each element.
* @param {?ErrorCallback<Type>} [onError] An optional error handler.
* @returns {Promise<Set<Type>>}
*/
async asyncRun(callbackFn, onError) {
const process = async () => {
while (this.#tasks.processing.activeCount < this.#tasks.concurrency && super.length > 0) {
const element = this.dequeue();
if (element) {
const task = this.#tasks
.asyncProcess(element, callbackFn, onError)
.finally(() => (this.#tasks.processing.delete(task), process()));
this.#tasks.processing.add(task, false);
}
}
this.#tasks.processing.activeCount > 0 && await Promise.all(this.#tasks.processing.state);
};
await process();
await this.#tasks.processing.complete();
return this.#tasks.processed;
}
// #endregion Public async
/**
* @description Starts processing elements in the queue using the provided callback function.
* @public
* @param {(element: Type) => void} callbackFn A function to process each element in the queue.
* @param {?(element: Type, error: unknown) => void} [onError] An optional function to handle the error.
* @returns {void) => void}
*/
run(callbackFn, onError) {
while (super.length > 0) {
this.#tasks.process(this.dequeue(), callbackFn, onError);
}
}
}
/**
* @description A standard LIFO (Last In, First Out) queue.
* @export
* @abstract
* @class Stack
* @template Type
*/
class Stack {
/**
* @description The `Elements` of array state type.
* @public
* @readonly
* @type {Elements<Type>}
*/
get elements() {
return this.#stack.elements;
}
/**
* @description The actual stack length.
* @public
* @readonly
* @type {number}
*/
get length() {
return this.#stack.length;
}
/**
* @description The maximum stack size.
* @public
* @readonly
* @type {number}
*/
get size() {
return this.#size;
}
/**
* @description The actual stack `Elements` state.
* @public
* @readonly
* @type {readonly Type[]}
*/
get state() {
return this.#stack.elements.state;
}
/**
* @description Privately stored maximum stack size.
* @type {number}
*/
#size;
/**
* @description Privately stored `Array` stack state.
* @type {ArrayState<Type>}
*/
#stack;
/**
* Creates an instance of `Stack`.
* @constructor
* @param {number} [size=Infinity]
* @param {...Type[]} elements
*/
constructor(size = Infinity, ...elements) {
this.#size = size;
this.#stack = new (class Stack extends Queue {
})(size, ...elements);
}
/**
* @description Clears the queue.
* @public
* @returns {this}
*/
clear() {
this.#stack.clear();
return this;
}
/**
* @description Checks whether the stack is empty.
* @public
* @returns {boolean}
*/
isEmpty() {
return this.#stack.isEmpty();
}
/**
* @description Checks if the stack is full.
* @public
* @returns {boolean}
*/
isFull() {
return this.#stack.isFull();
}
/**
* @description Returns the top element on the stack.
* @public
* @returns {(Type | undefined)}
*/
peek() {
return this.#stack.elements.last();
}
/**
* @description Removes and returns the top element from the stack.
* @public
* @returns {(Type | undefined)}
*/
pop() {
const last = this.peek();
this.#stack.length > 0 && this.#stack.elements.remove(this.#stack.length - 1);
return last;
}
/**
* @description Adds a new element on the stack.
* @public
* @param {Type} element
* @returns {this}
*/
push(element) {
this.#stack.elements.append(element);
return this;
}
}
// Class.
/*
* Public API Surface of queue
*/
/**
* Generated bundle index. Do not edit.
*/
export { Elements, Processing, Queue, Stack, TaskQueue, Tasks };
//# sourceMappingURL=typescript-package-queue.mjs.map