import { STREAMING_ERROR_CODES, StreamingError } from './stream-parser.js';
/**
* Utility class for managing timeouts in async operations
* Reduces code duplication for timeout handling patterns
*/
export class TimeoutManager {
/**
* Wraps a promise with a timeout that will reject if not resolved in time
*
* @param {Promise} promise - The promise to wrap with timeout
* @param {number} timeoutMs - Timeout duration in milliseconds
* @param {string} operationName - Name of the operation for error messages
* @returns {Promise} The result of the promise or throws timeout error
*
* @example
* const result = await TimeoutManager.withTimeout(
* fetchData(),
* 5000,
* 'Data fetch operation'
* );
*/
static async withTimeout(promise, timeoutMs, operationName = 'Operation') {
let timeoutHandle;
const timeoutPromise = new Promise((_, reject) => {
timeoutHandle = setTimeout(() => {
reject(
new StreamingError(
`${operationName} timed out after ${timeoutMs / 1000} seconds`,
STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED
)
);
}, timeoutMs);
});
try {
// Race between the actual promise and the timeout
const result = await Promise.race([promise, timeoutPromise]);
// Clear timeout if promise resolved first
clearTimeout(timeoutHandle);
return result;
} catch (error) {
// Always clear timeout on error
clearTimeout(timeoutHandle);
throw error;
}
}
/**
* Wraps a promise with a timeout, but returns undefined instead of throwing on timeout
* Useful for optional operations that shouldn't fail the main flow
*
* @param {Promise} promise - The promise to wrap with timeout
* @param {number} timeoutMs - Timeout duration in milliseconds
* @param {*} defaultValue - Value to return on timeout (default: undefined)
* @returns {Promise} The result of the promise or defaultValue on timeout
*
* @example
* const usage = await TimeoutManager.withSoftTimeout(
* getUsageStats(),
* 1000,
* { tokens: 0 }
* );
*/
static async withSoftTimeout(promise, timeoutMs, defaultValue = undefined) {
let timeoutHandle;
const timeoutPromise = new Promise((resolve) => {
timeoutHandle = setTimeout(() => {
resolve(defaultValue);
}, timeoutMs);
});
try {
const result = await Promise.race([promise, timeoutPromise]);
clearTimeout(timeoutHandle);
return result;
} catch (error) {
// On error, clear timeout and return default value
clearTimeout(timeoutHandle);
return defaultValue;
}
}
/**
* Creates a reusable timeout controller for multiple operations
* Useful when you need to apply the same timeout to multiple promises
*
* @param {number} timeoutMs - Timeout duration in milliseconds
* @param {string} operationName - Base name for operations
* @returns {Object} Controller with wrap method
*
* @example
* const controller = TimeoutManager.createController(60000, 'AI Service');
* const result1 = await controller.wrap(service.call1(), 'call 1');
* const result2 = await controller.wrap(service.call2(), 'call 2');
*/
static createController(timeoutMs, operationName = 'Operation') {
return {
timeoutMs,
operationName,
async wrap(promise, specificName = null) {
const fullName = specificName
? `${operationName} - ${specificName}`
: operationName;
return TimeoutManager.withTimeout(promise, timeoutMs, fullName);
},
async wrapSoft(promise, defaultValue = undefined) {
return TimeoutManager.withSoftTimeout(promise, timeoutMs, defaultValue);
}
};
}
/**
* Checks if an error is a timeout error from this manager
*
* @param {Error} error - The error to check
* @returns {boolean} True if this is a timeout error
*/
static isTimeoutError(error) {
return (
error instanceof StreamingError &&
error.code === STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED &&
error.message.includes('timed out')
);
}
}
/**
* Duration helper class for more readable timeout specifications
*/
export class Duration {
constructor(value, unit = 'ms') {
this.milliseconds = this._toMilliseconds(value, unit);
}
static milliseconds(value) {
return new Duration(value, 'ms');
}
static seconds(value) {
return new Duration(value, 's');
}
static minutes(value) {
return new Duration(value, 'm');
}
static hours(value) {
return new Duration(value, 'h');
}
get seconds() {
return this.milliseconds / 1000;
}
get minutes() {
return this.milliseconds / 60000;
}
get hours() {
return this.milliseconds / 3600000;
}
toString() {
if (this.milliseconds < 1000) {
return `${this.milliseconds}ms`;
} else if (this.milliseconds < 60000) {
return `${this.seconds}s`;
} else if (this.milliseconds < 3600000) {
return `${Math.floor(this.minutes)}m ${Math.floor(this.seconds % 60)}s`;
} else {
return `${Math.floor(this.hours)}h ${Math.floor(this.minutes % 60)}m`;
}
}
_toMilliseconds(value, unit) {
const conversions = {
ms: 1,
s: 1000,
m: 60000,
h: 3600000
};
return value * (conversions[unit] || 1);
}
}