UNPKG

@merchise/action-queue

Version:

A coordinated queue of actions

383 lines (382 loc) 11.1 kB
class u { constructor(e) { typeof e < "u" ? this._options = { createPromises: typeof e.createPromises == "boolean" ? e.createPromises : !0, rejectCanceled: typeof e.rejectCanceled == "boolean" ? e.rejectCanceled : !0, workers: typeof e.workers == "number" && e.workers > 1 ? e.workers : 1 } : this._options = { createPromises: !0, workers: 1, rejectCanceled: !0 }, this._thens = [], this._catchs = [], this._finallys = [], this._cancels = [], this._paused = !1, this._queue = [], this._rolling = null, this._workers = {}, this._idle = /* @__PURE__ */ new Set(), [...Array(this._options.workers).keys()].map((t) => this._idle.add(t)); } /** * Register an new callback to call when any of the actions in the queue * is completed and not canceled. * * @param {function} callback */ then(e) { this._thens.push(e); } /** * Register a new callback to call when any of the actions in the queue * is rejected without being cancelled. * * @param {function} callback */ catch(e) { this._catchs.push(e); } /** * Register a new callback which is going to be called when any of the * actions in the queue is either completed, rejected or cancelled. * * These callbacks are always called after the other (more specific) * callbacks. * * @param {function} callback */ finally(e) { this._finallys.push(e); } /** * Register a new callback which is going to be called when any of the * actions in the queue is cancelled. * * @param {function} callback */ oncancel(e) { this._cancels.push(e); } /** * Insert the action at the beginning of the queue. If a current action * is already running don't cancel it, instead queue the given action to * be run later but before any other action in the queue. * * @param {function} fn The action to perform * @param {...any} extra Extra arguments to pass to callbacks * * If the option `createPromises` is true, return a promise that is * equivalent to the one that would be returned by the action after it * starts running. This promise will resolve only if/when the action * runs and resolves, and it will reject when the action rejects or is * cancelled. * * If `createPromises` is false, return undefined. */ prepend(e, ...t) { let n = this._build_action(e, t); return this._queue.splice(0, 0, n), this._run(), n.external_promise; } /** * Insert the action at the end of the queue. * * @param {function} fn The action to perform * @param {...any} extra Extra arguments to pass to callbacks * * If the option `createPromises` is true, return a promise that is * equivalent to the one that would be returned by the action after it * starts running. This promise will resolve only if/when the action * runs and resolves, and it will reject when the action rejects or is * cancelled. * * If `createPromises` is false, return undefined. */ append(e, ...t) { let n = this._build_action(e, t); return this._queue.push(n), this._run(), n.external_promise; } _build_action(e, t) { let n = { resolve: () => { }, reject: () => { } }, l; this._options.createPromises ? l = new Promise(function(h, r) { n.resolve = h, n.reject = r; }) : l = void 0; let c = { fn: e, connectors: n, extra: t, external_promise: l, inner_promise: void 0, cancelled: !1, cancel: () => { this._cancel_action(c); } }; return c; } /** * Replaces the entire queue with the given action. Cancel pending and * running actions. * * @param {function} fn The action to perform * @param {...any} extra Extra arguments to pass to callbacks * * Returns a promise that is equivalent to the one that would be * returned by the action after it starts running. This promise will * resolve only if/when the action runs and resolves, and it will * reject when the action rejects or is cancelled. */ replace(e, ...t) { return this.clear(), this.append(e, ...t); } /** * Replaces the entire queue with the given action. Without canceling * running actions. * * @param {function} fn The action to perform * @param {...any} extra Extra arguments to pass to callbacks */ replace_pending(e, ...t) { let n = this._queue.concat(); for (this._queue.splice(0, this._queue.length); n.length > 0; ) { let l = n.shift(); this._cancel_action(l); } this.append(e, ...t); } /** * Clear the entire queue. Cancel pending and running actions. */ clear() { let e = this._queue.concat(); for (this._queue.splice(0, this._queue.length), this._cancel_running(); e.length > 0; ) { let t = e.shift(); this._cancel_action(t); } } /** * Return the length of the queue */ length() { return this._queue.length + this.running(); } /** * Return the amount of tasks currently running. */ running() { return this._options.workers - this._idle.size; } /** * Return True if the queue is busy, either running or with waiting * actions. */ busy() { return this.length() > 0; } /** * Return a promise that resolves/rejects just as soon as the first * pending action resolves/rejects. * * Cancelations don't affect the promise. If the running action gets * cancelled midway, this promise will take over on the next action. * If no action is scheduled to be next, we wait. * * The only way this promise is rejected, is if the running action is * rejected. The only way this promise is resolved, is when the * running action is resolved. * * When the same queue is used several times, calls to promise may * return different promises. */ promise() { return this._rolling === null && this._setup_rolling_promise(), this._rolling.promise; } /** * Return true if the queue is paused. */ paused() { return this._paused; } /** * Pause the queue. No tasks are going to be run. */ pause() { this._paused = !0; } /** * Resume the queue. */ resume() { this._paused = !1, this._run(); } /** * Return an object with the running and pending jobs in the queue. * * The result is an object with two properties: 'running' and 'pending'. * Each is an array of objects the properties: * * - `args`; which is an array (possibly empty) with the extra arguments * passed to `append`, `prepend` or `replace`. * * - `cancel`; a function that allows to cancel this particular action * * - `promise`; the promise attached to this action (undefined if * `createPromises` is false). * */ info() { let e = function(l) { let { extra: c, cancel: h, external_promise: r } = l; return { args: c, cancel: h, promise: r }; }; const t = Object.values(this._workers), n = [].concat(this._queue); return { running: t.map(e), pending: n.map(e) }; } _setup_rolling_promise() { let e = this; e._rolling = { promise: null, resolve: null, reject: null }, e._rolling.promise = new Promise(function(t, n) { e._rolling.resolve = t, e._rolling.reject = n; }); } /** * Run the next action in the queue. * * If there's an action running already or if the queue is empty, ignore * the request. * * After the action is finished, request to run the next one. */ _run() { if (!this.paused() && this._idle.size > 0 && this._queue.length > 0) { let e = this._queue.shift(), { fn: t, connectors: n, cancelled: l } = e; if (l) { this._run(); return; } let c = t(); e.inner_promise = c; let h = this._acquire(e), r = this; c.then(function(...s) { r._release(h); try { n.resolve(...s); } catch (i) { console.error(i); } s.length == 1 && typeof s[0] > "u" && (s = []); let a = e.extra || []; if (r._rolling !== null) { let i = r._rolling.resolve; if (typeof i < "u" && i !== null) try { i.apply(r, s.concat(a)); } catch (o) { console.error(o); } r._rolling = null; } r._thens.forEach(function(i) { try { i.apply(r, s.concat(a)); } catch (o) { console.error(o); } }), r._finallys.forEach(function(i) { try { i.apply(r, s.concat(a)); } catch (o) { console.error(o); } }), r._run(); }).catch(function(...s) { r._release(h); try { n.reject(...s); } catch (i) { console.error(i); } s.length == 1 && typeof s[0] > "u" && (s = []); let a = e.extra || []; if (r._rolling !== null) { let i = r._rolling.reject; if (typeof i < "u" && i !== null) try { i.apply(r, s.concat(a)); } catch (o) { console.error(o); } r._rolling = null; } r._catchs.forEach(function(i) { try { i.apply(r, s.concat(a)); } catch (o) { console.error(o); } }), r._finallys.forEach(function(i) { try { i.apply(r, s.concat(a)); } catch (o) { console.error(o); } }), r._run(); }), r._run(); } } /** * Cancel all running actions if any. * * If the underlying promise has a `cancel` function, call it. Fallback * to `abort`. * * Call registered callbacks (oncancel and finally). */ _cancel_running() { for (const e of Object.values(this._workers)) { let t = e.inner_promise; try { typeof t.cancel == "function" ? t.cancel() : typeof t.abort == "function" && t.abort(); } catch (n) { console.error(n); } this._cancel_action(e); } this._workers = {}, this._idle = /* @__PURE__ */ new Set(), [...Array(this._options.workers).keys()].map((e) => this._idle.add(e)); } /** * Call the cancelled and finally callbacks for the action. */ _cancel_action(e) { e.cancelled = !0; let t = e.extra, n = this; if (this._cancels.forEach(function(l) { try { l.apply(n, t); } catch (c) { console.error(c); } }), this._options.rejectCanceled) try { e.connectors.reject(new Error("Action was cancelled")); } catch (l) { console.error(l); } this._finallys.forEach(function(l) { try { l.apply(n, t); } catch (c) { console.error(c); } }); } _acquire(e) { let n = this._idle.values().next().value; return this._idle.delete(n), this._workers[n] = e, n; } _release(e) { delete this._workers[e], this._idle.add(e); } } export { u as default };