import { config as appConfig } from '../config/index.js'; import { createLogger } from '../logging/logger.js'; import { Time } from './time.js'; import path from 'path'; import { Addon, Preset } from '../db/schemas.js'; import { URL } from 'url'; const logger = createLogger('utils-general'); export function getDataFolder(): string { const url = new URL(appConfig.bootstrap.databaseUri); if (url.protocol === 'sqlite:') { let filename = url.pathname; if (url.hostname === '.') { filename = path.join(process.cwd(), url.pathname.replace(/^\//, '')); } return path.dirname(filename); } return path.join(process.cwd(), 'data'); } export function getAddonName(addon: Addon | Preset): string { return 'type' in addon ? addon.type : `${addon.name}${addon.displayIdentifier || addon.identifier ? ` ${addon.displayIdentifier || addon.identifier}` : ''}`; } export interface RetryOptions { /** * Number of retry attempts (not including initial attempt) * @default 1 */ retries?: number; /** * Optional function to determine if an error should trigger a retry * @param error The error that was thrown * @returns true if should retry, false otherwise */ shouldRetry?: (error: any) => boolean; /** * Optional function to get context for error logging * @returns string context to include in error logs */ getContext?: () => string; } /** * Utility function to retry an async operation * @param operation The async operation to retry * @param options Retry configuration options * @returns The result of the operation * @throws The last error encountered if all retries fail */ export async function withRetry( operation: () => Promise, options: RetryOptions = {} ): Promise { const { retries = 1, shouldRetry, getContext } = options; const maxAttempts = retries + 1; // +1 for initial attempt for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await operation(); } catch (error) { const isLastAttempt = attempt === maxAttempts - 1; const shouldTryAgain = !isLastAttempt && (!shouldRetry || shouldRetry(error)); const context = getContext ? ` for ${getContext()}` : ''; if (shouldTryAgain) { logger.warn( `Operation failed${context}: ${error}. Will retry ${maxAttempts - attempt - 1} more time(s).` ); } else { if (isLastAttempt) { logger.warn( `Operation failed${context}: ${error}. All retries exhausted.` ); } else { logger.warn( `Operation failed${context}: ${error}. Not retrying due to error type.` ); } throw error; } } } // This line should never be reached due to the throw in the catch block throw new Error('Unexpected state in retry logic'); } export interface TimeoutOptions { /** * Timeout duration in milliseconds * @default Time.Second * 5 */ timeout?: number; /** * Optional function to check if the operation should be allowed to proceed * @returns true if operation should proceed, false otherwise */ shouldProceed?: () => boolean; /** * Optional function to get context for error logging * @returns string context to include in error logs */ getContext?: () => string; } /** * Utility function to execute an async operation with timeout * @param operation The async operation to execute * @param fallback Value to return if operation times out or fails * @param options Timeout configuration options * @returns The result of the operation or fallback value */ export async function withTimeout( operation: () => Promise, fallback: T, options: TimeoutOptions = {} ): Promise { const { timeout = 5 * Time.Second, shouldProceed, getContext } = options; // Check if operation should proceed if (shouldProceed && !shouldProceed()) { const context = getContext ? ` for ${getContext()}` : ''; logger.error(`Operation skipped${context}: Precondition check failed`); return fallback; } try { // Create a promise that rejects after timeout const timeoutPromise = new Promise((_, reject) => { const id = setTimeout(() => { clearTimeout(id); reject(new Error(`Operation timed out after ${timeout}ms`)); }, timeout); }); // Race the operation against the timeout return await Promise.race([operation(), timeoutPromise]); } catch (err) { const context = getContext ? ` for ${getContext()}` : ''; logger.error(`Operation failed${context}: ${err}`); return fallback; } } /** * Base64 URL safe encoding * @param data - The data to encode * @returns The base64 URL safe encoded data */ export function toUrlSafeBase64(string: string): string { return Buffer.from(string) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); } /** * Base64 URL safe decoding * @param data - The data to decode * @returns The base64 URL safe decoded data */ export function fromUrlSafeBase64(data: string): string { // Add padding if needed const padding = data.length % 4; const paddedData = padding ? data + '='.repeat(4 - padding) : data; return Buffer.from( paddedData.replace(/-/g, '+').replace(/_/g, '/'), 'base64' ).toString('utf-8'); }