@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
JavaScript
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