Spaces:
Paused
Paused
| 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<T>( | |
| operation: () => Promise<T>, | |
| options: RetryOptions = {} | |
| ): Promise<T> { | |
| 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<T>( | |
| operation: () => Promise<T>, | |
| fallback: T, | |
| options: TimeoutOptions = {} | |
| ): Promise<T> { | |
| 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<never>((_, 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'); | |
| } | |