import { Logger } from '@nestjs/common';
const logger = new Logger('Utils');
/**
* Sleep for a specified number of milliseconds
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Retry a function with exponential backoff
*/
export async function retry<T>(
fn: () => Promise<T>,
maxAttempts: number = 3,
initialDelay: number = 1000,
maxDelay: number = 10000
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === maxAttempts) {
throw lastError;
}
const delay = Math.min(initialDelay * Math.pow(2, attempt - 1), maxDelay);
logger.warn(`Attempt ${attempt} failed, retrying in ${delay}ms: ${lastError.message}`);
await sleep(delay);
}
}
throw lastError!;
}
/**
* Create a timeout promise that rejects after specified milliseconds
*/
export function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms);
})
]);
}
/**
* Validate that a string is a valid URL
*/
export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Sanitize a string to be used as a valid identifier
*/
export function sanitizeIdentifier(str: string): string {
return str
.replace(/[^a-zA-Z0-9_]/g, '_')
.replace(/^[0-9]/, '_$&')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');
}
/**
* Generate a unique ID
*/
export function generateId(prefix: string = 'id'): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `${prefix}_${timestamp}_${random}`;
}
/**
* Deep clone an object
*/
export function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime()) as unknown as T;
}
if (obj instanceof Array) {
return obj.map(item => deepClone(item)) as unknown as T;
}
if (typeof obj === 'object') {
const cloned = {} as T;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
return obj;
}
/**
* Merge objects deeply
*/
export function deepMerge<T extends Record<string, any>>(target: T, ...sources: Partial<T>[]): T {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
deepMerge(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return deepMerge(target, ...sources);
}
/**
* Check if value is a plain object
*/
export function isObject(item: any): item is Record<string, any> {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* Convert bytes to human readable format
*/
export function formatBytes(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* Format duration in milliseconds to human readable format
*/
export function formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
}
const seconds = Math.floor(ms / 1000);
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes}m ${seconds % 60}s`;
}
const hours = Math.floor(minutes / 60);
return `${hours}h ${minutes % 60}m`;
}
/**
* Capitalize first letter of a string
*/
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Convert camelCase to kebab-case
*/
export function camelToKebab(str: string): string {
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
}
/**
* Convert kebab-case to camelCase
*/
export function kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
}
/**
* Truncate string to specified length
*/
export function truncate(str: string, length: number, suffix: string = '...'): string {
if (str.length <= length) {
return str;
}
return str.substring(0, length - suffix.length) + suffix;
}
/**
* Remove undefined values from object
*/
export function removeUndefined<T extends Record<string, any>>(obj: T): Partial<T> {
const result: Partial<T> = {};
for (const key in obj) {
if (obj[key] !== undefined) {
result[key] = obj[key];
}
}
return result;
}
/**
* Parse JSON safely with error handling
*/
export function parseJson<T = any>(jsonString: string): { success: true; data: T } | { success: false; error: string } {
try {
const data = JSON.parse(jsonString);
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Invalid JSON'
};
}
}
/**
* Create a debounced function
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
/**
* Create a throttled function
*/
export function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let lastFunc: NodeJS.Timeout;
let lastRan: number;
return (...args: Parameters<T>) => {
if (!lastRan) {
func(...args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func(...args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}