UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

261 lines (237 loc) 8.46 kB
'use strict'; /** * @typedef {Object} QueuedTask * @property {(...args: any[]) => Promise<any>|Promise<any>} task - The async task to execute. * @property {(value: any) => any} resolve - The resolve function from the Promise. * @property {(reason?: any) => any} reject - The reject function from the Promise. * @property {string|undefined} [id] - Optional identifier for the task. * @property {string|null|undefined} [marker] - Optional marker for the task. * @property {number|null|undefined} [delay] - Optional delay (in ms) before the task is executed. */ /** * A queue system for managing and executing asynchronous tasks sequentially, one at a time. * * Tasks can be delayed, reordered, canceled, and processed in strict order. The queue ensures that each task * is executed after the previous one finishes, and any task can be skipped or canceled if needed. * * @class */ class TinyPromiseQueue { /** @type {QueuedTask[]} */ #queue = []; #running = false; /** @type {Record<string, ReturnType<typeof setTimeout>>} */ #timeouts = {}; /** @type {Set<string>} */ #blacklist = new Set(); /** * Returns whether the queue is currently processing a task. * * @returns {boolean} */ isRunning() { return this.#running; } /** * Processes the a normal task. * * @param {QueuedTask} data * * @returns {Promise<void>} */ async #normalProcessQueue(data) { if ( data && typeof data.task === 'function' && typeof data.resolve === 'function' && typeof data.reject === 'function' ) { const { task, resolve, reject, delay, id } = data; try { if (id && this.#blacklist.has(id)) { reject(new Error('The function was canceled on TinyPromiseQueue.')); this.#blacklist.delete(id); this.#running = false; this.#processQueue(); return; } if (delay && id) { await new Promise((resolveDelay) => { const timeoutId = setTimeout(() => { delete this.#timeouts[id]; resolveDelay(null); }, delay); this.#timeouts[id] = timeoutId; }); } const result = await task(); resolve(result); } catch (error) { reject(error); } finally { this.#running = false; this.#processQueue(); } } } /** * Processes a group task. * * @returns {Promise<void>} */ async #groupProcessQueue() { /** @type {Array<QueuedTask>} */ const grouped = []; while (this.#queue.length && this.#queue[0]?.marker === 'POINT_MARKER') { // @ts-ignore grouped.push(this.#queue.shift()); } if (grouped.length === 0) { this.#running = false; this.#processQueue(); return; } await Promise.all( grouped.map( ({ task, resolve, reject, id }) => new Promise(async (pResolve) => { if (id && this.#blacklist.has(id)) { this.#blacklist.delete(id); reject(new Error('The function was canceled on TinyPromiseQueue.')); pResolve(true); return; } await task().then(resolve).catch(reject); pResolve(true); }), ), ); this.#running = false; this.#processQueue(); } /** * Processes the next task in the queue if not already running. * Ensures tasks are executed in order, one at a time. * * @returns {Promise<void>} */ async #processQueue() { if (this.#running || this.#queue.length === 0) return; this.#running = true; if (typeof this.#queue[0]?.marker !== 'string' || this.#queue[0]?.marker !== 'POINT_MARKER') { const data = this.#queue.shift(); // @ts-ignore this.#normalProcessQueue(data); } else this.#groupProcessQueue(); } /** * Returns the index of a task by its ID. * * @param {string} id The ID of the task to locate. * @returns {number} The index of the task in the queue, or -1 if not found. */ getIndexById(id) { return this.#queue.findIndex((item) => item.id === id); } /** * Returns a list of IDs for all tasks currently in the queue. * * @returns {{ index: number, id: string }[]} An array of task IDs currently queued. */ getQueuedIds() { // @ts-ignore return this.#queue .map((item, index) => ({ index, id: item.id })) .filter((entry) => typeof entry.id === 'string'); } /** * Reorders a task in the queue from one index to another. * * @param {number} fromIndex The current index of the task to move. * @param {number} toIndex The index where the task should be placed. */ reorderQueue(fromIndex, toIndex) { if ( typeof fromIndex !== 'number' || typeof toIndex !== 'number' || fromIndex < 0 || toIndex < 0 || fromIndex >= this.#queue.length || toIndex >= this.#queue.length ) return; const [item] = this.#queue.splice(fromIndex, 1); this.#queue.splice(toIndex, 0, item); } /** * Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all. * If the queue is currently empty, behaves like a regular promise. * * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise. * @param {string} [id] Optional ID to identify the task in the queue. * @returns {Promise<any>} A Promise that resolves or rejects with the result of the task once it's processed. * @throws {Error} Throws if param is invalid. */ async enqueuePoint(task, id) { if (typeof task !== 'function') return Promise.reject(new Error('Task must be a function returning a Promise.')); if (typeof id !== 'undefined' && typeof id !== 'string') throw new Error('The "id" parameter must be a string.'); if (!this.#running) return task(); return new Promise((resolve, reject) => { this.#queue.push({ marker: 'POINT_MARKER', task, resolve, reject, id }); this.#processQueue(); }); } /** * Adds a new async task to the queue and ensures it runs in order after previous tasks. * Optionally, a delay can be added before the task is executed. * * If the task is canceled before execution, it will be rejected with the message: * "The function was canceled on TinyPromiseQueue." * * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise to be executed sequentially. * @param {number|null} [delay] Optional delay (in ms) before the task is executed. * @param {string} [id] Optional ID to identify the task in the queue. * @returns {Promise<any>} A Promise that resolves or rejects with the result of the task once it's processed. * @throws {Error} Throws if param is invalid. */ enqueue(task, delay, id) { if (typeof task !== 'function') return Promise.reject(new Error('Task must be a function returning a Promise.')); if (typeof delay !== 'undefined' && (typeof delay !== 'number' || delay < 0)) return Promise.reject(new Error('Delay must be a positive number or undefined.')); if (typeof id !== 'undefined' && typeof id !== 'string') throw new Error('The "id" parameter must be a string.'); return new Promise((resolve, reject) => { this.#queue.push({ task, resolve, reject, id, delay }); this.#processQueue(); }); } /** * Cancels a scheduled delay and removes the task from the queue. * Adds the ID to a blacklist so the task is skipped if already being processed. * * @param {string} id The ID of the task to cancel. * @returns {boolean} True if a delay was cancelled and the task was removed. * @throws {Error} Throws if `id` is not a string. */ cancelTask(id) { if (typeof id !== 'string') throw new Error('The "id" parameter must be a string.'); let cancelled = false; if (id in this.#timeouts) { clearTimeout(this.#timeouts[id]); delete this.#timeouts[id]; cancelled = true; } const index = this.getIndexById(id); if (index !== -1) { const [removed] = this.#queue.splice(index, 1); removed?.reject?.(new Error('The function was canceled on TinyPromiseQueue.')); cancelled = true; } if (cancelled) this.#blacklist.add(id); return cancelled; } } module.exports = TinyPromiseQueue;