UNPKG

@catbee/utils

Version:

A modular, production-grade utility toolkit for Node.js and TypeScript, designed for robust, scalable applications (including Express-based services). All utilities are tree-shakable and can be imported independently.

632 lines 25.6 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; /** * Delays execution for a specified number of milliseconds. * * @param {number} ms - The number of milliseconds to sleep. * @returns {Promise<void>} A Promise that resolves after the given time. */ export function sleep(ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); } /** * Creates a debounced version of a function that delays its execution. * Provides `.cancel()` and `.flush()` methods. * * @template T * @param {T} fn - The function to debounce. * @param {number} delay - Delay in milliseconds. * @returns {T & { cancel: () => void; flush: () => void }} A debounced function. */ export function debounce(fn, delay) { var timer = null; var pendingArgs = null; function debounced() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } pendingArgs = args; if (timer) clearTimeout(timer); timer = setTimeout(function () { fn.apply(void 0, __spreadArray([], __read(pendingArgs), false)); timer = null; }, delay); } debounced.cancel = function () { if (timer) clearTimeout(timer); timer = null; pendingArgs = null; }; debounced.flush = function () { if (timer) { clearTimeout(timer); fn.apply(void 0, __spreadArray([], __read(pendingArgs), false)); timer = null; pendingArgs = null; } }; return debounced; } /** * Creates a throttled version of a function that limits its execution rate. * Allows control over leading/trailing invocation. * * @template T * @param {T} fn - The function to throttle. * @param {number} limit - Minimum time between calls in milliseconds. * @param {{ leading?: boolean, trailing?: boolean }} [opts] - Options for leading/trailing edge throttling. * @returns {(...args: Parameters<T>) => void} A throttled function. */ export function throttle(fn, limit, opts) { if (opts === void 0) { opts = { leading: true, trailing: false, }; } var lastCall = 0; var timer = null; var savedArgs = null; return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var now = Date.now(); var _a = opts.leading, leading = _a === void 0 ? true : _a, _b = opts.trailing, trailing = _b === void 0 ? false : _b; if (!lastCall && !leading) lastCall = now; var remaining = limit - (now - lastCall); if (remaining <= 0) { if (timer) { clearTimeout(timer); timer = null; } lastCall = now; fn.apply(void 0, __spreadArray([], __read(args), false)); } else if (trailing) { savedArgs = args; if (!timer) { timer = setTimeout(function () { lastCall = leading ? Date.now() : 0; timer = null; if (savedArgs) fn.apply(void 0, __spreadArray([], __read(savedArgs), false)); }, remaining); } } }; } /** * Retries an asynchronous function a given number of times with optional delay/backoff. * * @template T * @param {() => Promise<T>} fn - The async function to retry. * @param {number} [retries=3] - Number of retry attempts. * @param {number} [delay=500] - Delay in milliseconds between retries. * @param {boolean} [backoff=false] - Use exponential backoff between attempts. * @param {(error: unknown, attempt: number) => void} [onRetry] - Callback for each retry attempt. * @returns {Promise<T>} The result of the async function if successful. * @throws {*} The last encountered error if all retries fail. */ export function retry(fn_1) { return __awaiter(this, arguments, void 0, function (fn, retries, delay, backoff, onRetry) { var i, e_1; if (retries === void 0) { retries = 3; } if (delay === void 0) { delay = 500; } if (backoff === void 0) { backoff = false; } return __generator(this, function (_a) { switch (_a.label) { case 0: i = 0; _a.label = 1; case 1: if (!(i < retries)) return [3 /*break*/, 7]; _a.label = 2; case 2: _a.trys.push([2, 4, , 6]); return [4 /*yield*/, fn()]; case 3: return [2 /*return*/, _a.sent()]; case 4: e_1 = _a.sent(); if (i === retries - 1) throw e_1; if (onRetry) onRetry(e_1, i + 1); return [4 /*yield*/, sleep(backoff ? delay * Math.pow(2, i) : delay)]; case 5: _a.sent(); return [3 /*break*/, 6]; case 6: i++; return [3 /*break*/, 1]; case 7: throw new Error("Retry failed"); // should never reach here } }); }); } /** * Wraps a promise and rejects it if it doesn't resolve within the specified timeout. * * @template T * @param {Promise<T>} promise - The original promise. * @param {number} ms - Timeout in milliseconds. * @param {string} [message="Operation timed out"] - Optional timeout message. * @returns {Promise<T>} A promise that resolves or rejects within the timeout. */ export function withTimeout(promise, ms, message) { if (message === void 0) { message = "Operation timed out"; } return new Promise(function (resolve, reject) { var timeoutHandle = setTimeout(function () { return reject(new Error(message)); }, ms); promise .then(function (result) { clearTimeout(timeoutHandle); resolve(result); }) .catch(function (err) { clearTimeout(timeoutHandle); reject(err); }); }); } /** * Executes async tasks in true batches. * Each batch runs in parallel, but batches run sequentially. * All tasks in a batch start at the same time, next batch waits for full completion. * NOTE: For more granular concurrency, use a "queue" or "pooled" approach. * * @template T * @param {Array<() => Promise<T>>} tasks - An array of functions that return Promises. * @param {number} limit - Number of tasks to run in parallel per batch. * @returns {Promise<T[]>} A promise that resolves to an array of resolved values. */ export function runInBatches(tasks, limit) { return __awaiter(this, void 0, void 0, function () { var results, i, batch, batchResults; return __generator(this, function (_a) { switch (_a.label) { case 0: results = []; i = 0; _a.label = 1; case 1: if (!(i < tasks.length)) return [3 /*break*/, 4]; batch = tasks.slice(i, i + limit); return [4 /*yield*/, Promise.all(batch.map(function (fn) { return fn(); }))]; case 2: batchResults = _a.sent(); results.push.apply(results, __spreadArray([], __read(batchResults), false)); _a.label = 3; case 3: i += limit; return [3 /*break*/, 1]; case 4: return [2 /*return*/, results]; } }); }); } /** * Wraps a function and ensures it is only called once at a time. * Calls made while one is in progress will wait for the same Promise. * Optionally, new calls can be dropped while in progress (drop=true). * * @template TArgs * @template TResult * @param {(...args: TArgs) => Promise<TResult>} fn - The async function to wrap. * @param {boolean} [drop=false] - If true, new calls while one is pending are rejected. * @returns {(...args: TArgs) => Promise<TResult>} A wrapped function with singleton behavior. */ export function singletonAsync(fn, drop) { var _this = this; if (drop === void 0) { drop = false; } var promise = null; return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { if (!promise) { promise = fn.apply(void 0, __spreadArray([], __read(args), false)).finally(function () { promise = null; }); } else if (drop) { return [2 /*return*/, Promise.reject(new Error("Busy: function already running"))]; } return [2 /*return*/, promise]; }); }); }; } /** * Resolves a list of async tasks in parallel, returning both resolved and rejected results. * * @template T * @param {Array<() => Promise<T>>} tasks - Array of promise-returning functions. * @returns {Promise<PromiseSettledResult<T>[]>} Results including status and value/reason. */ export function settleAll(tasks) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, Promise.allSettled(tasks.map(function (task) { return task(); }))]; }); }); } /** * A simple task queue that executes async tasks with a concurrency limit. * Exposes pause, resume, and queue length getters. * * @param {number} limit - Maximum number of concurrent tasks. * @returns {function & { pause: () => void, resume: () => void, length: number, isPaused: boolean }} * Enqueue function plus queue controls. */ export function createTaskQueue(limit) { var queue = []; var activeCount = 0; var paused = false; var state = { /** * Pause task processing. */ pause: function () { paused = true; }, /** * Resume task processing. */ resume: function () { paused = false; next(); }, /** * The current length of the queue. * @type {number} */ get length() { return queue.length; }, /** * Whether the queue is currently paused. * @type {boolean} */ get isPaused() { return paused; }, }; var next = function () { if (paused || queue.length === 0 || activeCount >= limit) return; var task = queue.shift(); activeCount++; task().finally(function () { activeCount--; next(); }); }; /** * Enqueues a new async task to the queue. * * @template T * @param {() => Promise<T>} taskFn - The async task function. * @returns {Promise<T>} Promise resolving when task completes. */ var enqueue = function (taskFn) { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { return [2 /*return*/, new Promise(function (resolve, reject) { queue.push(function () { return __awaiter(_this, void 0, void 0, function () { var result, err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, taskFn()]; case 1: result = _a.sent(); resolve(result); return [3 /*break*/, 3]; case 2: err_1 = _a.sent(); reject(err_1); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); }); next(); })]; }); }); }; enqueue.pause = state.pause; enqueue.resume = state.resume; Object.defineProperty(enqueue, "length", { get: function () { return queue.length; }, }); Object.defineProperty(enqueue, "isPaused", { get: function () { return paused; }, }); return enqueue; } /** * Executes async functions sequentially and collects results. * Useful when order matters or tasks depend on each other. * * @template T * @param {Array<() => Promise<T>>} tasks - Array of promise-returning functions. * @returns {Promise<T[]>} Array of resolved values. */ export function runInSeries(tasks) { return __awaiter(this, void 0, void 0, function () { var results, tasks_1, tasks_1_1, task, _a, _b, e_2_1; var e_2, _c; return __generator(this, function (_d) { switch (_d.label) { case 0: results = []; _d.label = 1; case 1: _d.trys.push([1, 6, 7, 8]); tasks_1 = __values(tasks), tasks_1_1 = tasks_1.next(); _d.label = 2; case 2: if (!!tasks_1_1.done) return [3 /*break*/, 5]; task = tasks_1_1.value; _b = (_a = results).push; return [4 /*yield*/, task()]; case 3: _b.apply(_a, [_d.sent()]); _d.label = 4; case 4: tasks_1_1 = tasks_1.next(); return [3 /*break*/, 2]; case 5: return [3 /*break*/, 8]; case 6: e_2_1 = _d.sent(); e_2 = { error: e_2_1 }; return [3 /*break*/, 8]; case 7: try { if (tasks_1_1 && !tasks_1_1.done && (_c = tasks_1.return)) _c.call(tasks_1); } finally { if (e_2) throw e_2.error; } return [7 /*endfinally*/]; case 8: return [2 /*return*/, results]; } }); }); } /** * Memoizes an async function, caching results for repeated calls with identical arguments. * Optional TTL (time-to-live) for cached entries. * * @template T Function return type * @template Args Function arguments types * @param {(...args: Args) => Promise<T>} fn - The async function to memoize * @param {object} [options] - Memoization options * @param {number} [options.ttl] - Cache TTL in milliseconds (optional) * @param {(args: Args) => string} [options.keyFn] - Custom key generator function * @returns {(...args: Args) => Promise<T>} Memoized function */ export function memoizeAsync(fn, options) { if (options === void 0) { options = {}; } var cache = new Map(); var ttl = options.ttl, _a = options.keyFn, keyFn = _a === void 0 ? JSON.stringify : _a; return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return __awaiter(this, void 0, void 0, function () { var key, cached, result; return __generator(this, function (_a) { switch (_a.label) { case 0: key = keyFn(args); cached = cache.get(key); if (cached && (!ttl || Date.now() < cached.expires)) { return [2 /*return*/, cached.value]; } return [4 /*yield*/, fn.apply(void 0, __spreadArray([], __read(args), false))]; case 1: result = _a.sent(); cache.set(key, { value: result, expires: ttl ? Date.now() + ttl : Infinity, }); return [2 /*return*/, result]; } }); }); }; } /** * Creates an abortable version of a promise that can be cancelled using an AbortController. * * @template T * @param {Promise<T>} promise - The promise to make abortable * @param {AbortSignal} signal - AbortSignal from AbortController * @param {any} [abortValue] - Value to use when rejecting on abort * @returns {Promise<T>} Promise that rejects if the signal is aborted */ export function abortable(promise, signal, abortValue) { if (abortValue === void 0) { abortValue = new Error("Operation aborted"); } if (signal.aborted) { return Promise.reject(abortValue); } return Promise.race([ promise, new Promise(function (_, reject) { var abort = function () { return reject(abortValue); }; signal.addEventListener("abort", abort, { once: true }); promise.finally(function () { return signal.removeEventListener("abort", abort); }); }), ]); } /** * Creates a promise with external resolve/reject functions. * Useful for creating promises that can be resolved or rejected from outside. * * @template T * @returns {[Promise<T>, (value: T | PromiseLike<T>) => void, (reason?: any) => void]} * Tuple of [promise, resolve, reject] */ export function createDeferred() { var resolve; var reject; var promise = new Promise(function (res, rej) { resolve = res; reject = rej; }); return [promise, resolve, reject]; } /** * Chains a series of async functions, passing the result of each to the next. * Similar to function composition but for async functions. * * @template T * @param {Array<(input: any) => Promise<any>>} fns - Array of async functions to compose * @returns {(input: any) => Promise<T>} Composed function */ export function waterfall(fns) { var _this = this; return function (initialValue) { return __awaiter(_this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { return [2 /*return*/, fns.reduce(function (acc, fn) { return __awaiter(_this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = fn; return [4 /*yield*/, acc]; case 1: return [2 /*return*/, _a.apply(void 0, [_b.sent()])]; } }); }); }, Promise.resolve(initialValue))]; }); }); }; } /** * Creates a rate limiter that ensures functions aren't called more than * a specified number of times per interval. * * @template T * @param {(...args: any[]) => Promise<T>} fn - Function to rate limit * @param {number} maxCalls - Maximum calls allowed per interval * @param {number} interval - Time interval in milliseconds * @returns {(...args: any[]) => Promise<T>} Rate limited function */ export function rateLimit(fn, maxCalls, interval) { var calls = []; return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return __awaiter(this, void 0, void 0, function () { var now, oldestCall, delay, currentTime_1, nextDelay; return __generator(this, function (_a) { switch (_a.label) { case 0: now = Date.now(); calls.splice.apply(calls, __spreadArray([0, calls.length], __read(calls.filter(function (time) { return now - time < interval; })), false)); if (!(calls.length >= maxCalls)) return [3 /*break*/, 3]; oldestCall = calls[0]; delay = interval - (now - oldestCall); return [4 /*yield*/, sleep(Math.max(1, delay))]; case 1: _a.sent(); currentTime_1 = Date.now(); calls.splice.apply(calls, __spreadArray([0, calls.length], __read(calls.filter(function (time) { return currentTime_1 - time < interval; })), false)); if (!(calls.length >= maxCalls)) return [3 /*break*/, 3]; nextDelay = interval - (currentTime_1 - calls[0]); return [4 /*yield*/, sleep(Math.max(1, nextDelay))]; case 2: _a.sent(); calls.splice.apply(calls, __spreadArray([0, calls.length], __read(calls.filter(function (time) { return Date.now() - time < interval; })), false)); _a.label = 3; case 3: calls.push(Date.now()); return [2 /*return*/, fn.apply(void 0, __spreadArray([], __read(args), false))]; } }); }); }; } //# sourceMappingURL=async.utils.js.map