@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.
506 lines (501 loc) • 14.5 kB
JavaScript
/*
* The MIT License
*
* Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
;
var logger = require('@catbee/utils/logger');
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
__name(sleep, "sleep");
function debounce(fn, delay) {
let timer = null;
let pendingArgs = null;
function debounced(...args) {
pendingArgs = args;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
try {
fn(...pendingArgs);
} catch (err) {
logger.getLogger().error({
err
}, "Error in debounced function");
}
timer = null;
}, delay);
}
__name(debounced, "debounced");
debounced.cancel = () => {
if (timer) clearTimeout(timer);
timer = null;
pendingArgs = null;
};
debounced.flush = () => {
if (timer) {
clearTimeout(timer);
try {
fn(...pendingArgs);
} catch (err) {
logger.getLogger().error({
err
}, "Error in debounced.flush");
}
timer = null;
pendingArgs = null;
}
};
return debounced;
}
__name(debounce, "debounce");
var DEFAULT_THROTTLE_OPTS = {
leading: true,
trailing: false
};
function throttle(fn, limit, opts = DEFAULT_THROTTLE_OPTS) {
let lastCall = 0;
let timer = null;
let savedArgs = null;
return function(...args) {
const now = Date.now();
const { leading = true, trailing = false } = opts;
if (!lastCall && !leading) lastCall = now;
const remaining = limit - (now - lastCall);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastCall = now;
try {
fn(...args);
} catch (err) {
logger.getLogger().error({
err
}, "Error in throttled function");
}
} else if (trailing) {
savedArgs = args;
if (!timer) {
timer = setTimeout(() => {
lastCall = leading ? Date.now() : 0;
timer = null;
if (savedArgs) {
try {
fn(...savedArgs);
} catch (err) {
logger.getLogger().error({
err
}, "Error in throttled trailing call");
}
}
}, remaining);
}
}
};
}
__name(throttle, "throttle");
async function retry(fn, retries = 3, delay = 500, backoff = false, onRetry) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (e) {
if (i === retries - 1) throw e;
if (onRetry) onRetry(e, i + 1);
await sleep(backoff ? delay * Math.pow(2, i) : delay);
}
}
throw new Error("Retry failed");
}
__name(retry, "retry");
function withTimeout(promise, ms, message = "Operation timed out") {
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => reject(new Error(message)), ms);
promise.then((result) => {
clearTimeout(timeoutHandle);
resolve(result);
}).catch((err) => {
clearTimeout(timeoutHandle);
reject(err);
});
});
}
__name(withTimeout, "withTimeout");
async function runInBatches(tasks, limit) {
const results = [];
for (let i = 0; i < tasks.length; i += limit) {
const batch = tasks.slice(i, i + limit);
const batchResults = await Promise.all(batch.map((fn) => fn()));
results.push(...batchResults);
}
return results;
}
__name(runInBatches, "runInBatches");
function singletonAsync(fn, drop = false) {
let promise = null;
return async (...args) => {
if (!promise) {
promise = fn(...args).finally(() => {
promise = null;
});
} else if (drop) {
throw new Error("Function is already in progress");
}
return promise;
};
}
__name(singletonAsync, "singletonAsync");
async function settleAll(tasks) {
return Promise.allSettled(tasks.map((task) => task()));
}
__name(settleAll, "settleAll");
function createTaskQueue(limit) {
const queue = [];
let activeCount = 0;
let paused = false;
const state = {
/**
* Pause task processing.
*/
pause() {
paused = true;
},
/**
* Resume task processing.
*/
resume() {
paused = false;
next();
}};
const next = /* @__PURE__ */ __name(() => {
if (paused || queue.length === 0 || activeCount >= limit) return;
const task = queue.shift();
activeCount++;
task().finally(() => {
activeCount--;
next();
});
}, "next");
const enqueue = /* @__PURE__ */ __name(async function(taskFn) {
return new Promise((resolve, reject) => {
queue.push(async () => {
try {
const result = await taskFn();
resolve(result);
} catch (err) {
reject(err);
}
});
next();
});
}, "enqueue");
enqueue.pause = state.pause;
enqueue.resume = state.resume;
Object.defineProperty(enqueue, "length", {
get: /* @__PURE__ */ __name(() => queue.length, "get")
});
Object.defineProperty(enqueue, "isPaused", {
get: /* @__PURE__ */ __name(() => paused, "get")
});
return enqueue;
}
__name(createTaskQueue, "createTaskQueue");
async function runInSeries(tasks) {
const results = [];
for (const task of tasks) {
results.push(await task());
}
return results;
}
__name(runInSeries, "runInSeries");
function memoizeAsync(fn, options = {}) {
const cache = /* @__PURE__ */ new Map();
const { ttl } = options;
const generateKey = /* @__PURE__ */ __name((args) => {
try {
return options.keyFn?.(args) ?? JSON.stringify(args);
} catch {
logger.getLogger().warn("memoizeAsync: key generation failed, falling back to JSON.stringify");
return JSON.stringify(args);
}
}, "generateKey");
return async (...args) => {
const key = generateKey(args);
const cached = cache.get(key);
if (cached && Date.now() < cached.expires) {
return cached.value;
}
const result = await fn(...args);
cache.set(key, {
value: result,
expires: ttl ? Date.now() + ttl : Infinity
});
return result;
};
}
__name(memoizeAsync, "memoizeAsync");
function abortable(promise, signal, abortValue = new Error("Operation aborted")) {
if (signal.aborted) {
return Promise.reject(abortValue);
}
return Promise.race([
promise,
new Promise((_, reject) => {
const abort = /* @__PURE__ */ __name(() => {
reject(abortValue);
signal.removeEventListener("abort", abort);
}, "abort");
signal.addEventListener("abort", abort, {
once: true
});
promise.finally(() => signal.removeEventListener("abort", abort));
})
]);
}
__name(abortable, "abortable");
function createDeferred() {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return [
promise,
resolve,
reject
];
}
__name(createDeferred, "createDeferred");
function waterfall(fns) {
return async (initialValue) => {
return fns.reduce(async (acc, fn) => fn(await acc), Promise.resolve(initialValue));
};
}
__name(waterfall, "waterfall");
function rateLimit(fn, maxCalls, interval) {
const calls = [];
return async function(...args) {
const now = Date.now();
calls.splice(0, calls.length, ...calls.filter((time) => now - time < interval));
if (calls.length >= maxCalls) {
const oldestCall = calls[0];
const delay = interval - (now - oldestCall);
await sleep(Math.max(1, delay));
const currentTime = Date.now();
calls.splice(0, calls.length, ...calls.filter((time) => currentTime - time < interval));
if (calls.length >= maxCalls) {
const nextDelay = interval - (currentTime - calls[0]);
await sleep(Math.max(1, nextDelay));
calls.splice(0, calls.length, ...calls.filter((time) => Date.now() - time < interval));
}
}
calls.push(Date.now());
return fn(...args);
};
}
__name(rateLimit, "rateLimit");
var CircuitBreakerOpenError = class extends Error {
static {
__name(this, "CircuitBreakerOpenError");
}
constructor(message = "Circuit breaker is open") {
super(message);
this.name = "CircuitBreakerOpenError";
}
};
var CircuitBreakerState = /* @__PURE__ */ (function(CircuitBreakerState2) {
CircuitBreakerState2["CLOSED"] = "CLOSED";
CircuitBreakerState2["OPEN"] = "OPEN";
CircuitBreakerState2["HALF_OPEN"] = "HALF_OPEN";
return CircuitBreakerState2;
})({});
function circuitBreaker(fn, options = {}) {
const { failureThreshold = 5, resetTimeout = 1e4, successThreshold = 1, onOpen, onClose, onHalfOpen } = options;
let state = "CLOSED";
let fails = 0;
let halfSuccess = 0;
let unlockAt = 0;
const reset = /* @__PURE__ */ __name(() => {
fails = 0;
halfSuccess = 0;
state = "CLOSED";
onClose?.();
}, "reset");
const open = /* @__PURE__ */ __name(() => {
state = "OPEN";
unlockAt = Date.now() + resetTimeout;
onOpen?.();
}, "open");
const halfOpen = /* @__PURE__ */ __name(() => {
state = "HALF_OPEN";
onHalfOpen?.();
}, "halfOpen");
return async (...args) => {
const now = Date.now();
if (state === "OPEN" && now < unlockAt) {
throw new CircuitBreakerOpenError();
}
if (state === "OPEN" && now >= unlockAt) {
halfOpen();
}
try {
const res = await fn(...args);
if (state === "HALF_OPEN") {
halfSuccess++;
if (halfSuccess >= successThreshold) {
reset();
} else {
state = "HALF_OPEN";
}
} else {
fails = 0;
}
return res;
} catch (err) {
fails++;
if (state === "HALF_OPEN" || state === "CLOSED") {
if (fails >= failureThreshold) {
open();
}
}
throw err;
}
};
}
__name(circuitBreaker, "circuitBreaker");
async function runWithConcurrency(tasks, options = {}) {
const { concurrency = 3, onProgress, signal } = options;
const results = [];
const totalTasks = tasks.length;
let completed = 0;
if (totalTasks === 0) return results;
if (signal?.aborted) {
throw new Error("Aborted");
}
return new Promise((resolve, reject) => {
let taskIndex = 0;
let rejected = false;
const processNext = /* @__PURE__ */ __name(async () => {
if (rejected) return;
const currentTaskIndex = taskIndex++;
if (currentTaskIndex >= totalTasks) return;
try {
const result = await tasks[currentTaskIndex]();
if (signal?.aborted) {
rejected = true;
reject(new Error("Aborted"));
return;
}
results[currentTaskIndex] = result;
completed++;
if (onProgress) {
try {
onProgress(completed, totalTasks);
} catch (err) {
logger.getLogger().error({
err
}, "Error in onProgress callback");
}
}
} catch (error) {
rejected = true;
reject(error);
return;
}
if (completed === totalTasks) {
resolve(results);
return;
}
processNext();
}, "processNext");
const initialBatch = Math.min(concurrency, totalTasks);
for (let i = 0; i < initialBatch; i++) {
processNext();
}
if (signal) {
signal.addEventListener("abort", () => {
rejected = true;
reject(new Error("Aborted"));
}, {
once: true
});
}
});
}
__name(runWithConcurrency, "runWithConcurrency");
function optionalRequire(name) {
try {
return __require(name);
} catch {
return null;
}
}
__name(optionalRequire, "optionalRequire");
async function raceWithValue(promises) {
return new Promise((resolve, reject) => {
let rejectedCount = 0;
let lastError;
promises.forEach((p, index) => {
p.then((value) => resolve({
value,
index
})).catch((err) => {
rejectedCount++;
lastError = err;
if (rejectedCount === promises.length) {
reject(lastError);
}
});
});
});
}
__name(raceWithValue, "raceWithValue");
exports.CircuitBreakerOpenError = CircuitBreakerOpenError;
exports.CircuitBreakerState = CircuitBreakerState;
exports.abortable = abortable;
exports.circuitBreaker = circuitBreaker;
exports.createDeferred = createDeferred;
exports.createTaskQueue = createTaskQueue;
exports.debounce = debounce;
exports.memoizeAsync = memoizeAsync;
exports.optionalRequire = optionalRequire;
exports.raceWithValue = raceWithValue;
exports.rateLimit = rateLimit;
exports.retry = retry;
exports.runInBatches = runInBatches;
exports.runInSeries = runInSeries;
exports.runWithConcurrency = runWithConcurrency;
exports.settleAll = settleAll;
exports.singletonAsync = singletonAsync;
exports.sleep = sleep;
exports.throttle = throttle;
exports.waterfall = waterfall;
exports.withTimeout = withTimeout;