UNPKG

@typescript-package/queue

Version:

A lightweight TypeScript library for managing various queue and stack structures.

854 lines (843 loc) 26.1 kB
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