aiostreams / packages /core /src /utils /general.ts
f4b404's picture
Upload folder using huggingface_hub
f55c62a verified
Raw
History Blame Contribute Delete
5.38 kB
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');
}