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.

506 lines (501 loc) 14.5 kB
/* * 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. */ 'use strict'; 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;