Project Files
src / progressIndicator.ts
import { PROGRESS_THRESHOLD_MS, PROGRESS_UPDATE_INTERVAL_MS } from "./constants";
export interface ProgressOptions {
onProgress?: (percentage: number) => void;
onStatus?: (message: string) => void;
}
export class ProgressIndicator {
startTime: number = 0;
lastUpdate: number = 0;
totalDuration: number = 0;
active: boolean = false;
constructor(private options: ProgressOptions = {}) {}
start(totalDurationMs?: number): void {
this.startTime = Date.now();
this.lastUpdate = this.startTime;
this.totalDuration = totalDurationMs ?? PROGRESS_THRESHOLD_MS * 2; // Default to twice the threshold
this.active = true;
if (this.options.onStatus) {
this.options.onStatus("Starting operation...");
}
}
update(progress: number, message?: string): void {
const now = Date.now();
// Only update at regular intervals
if (now - this.lastUpdate < PROGRESS_UPDATE_INTERVAL_MS && progress < 100) return;
this.lastUpdate = now;
// Clamp percentage between 0 and 100
const clampedProgress = Math.max(0, Math.min(100, Math.floor(progress)));
if (this.options.onProgress) {
this.options.onProgress(clampedProgress);
}
if (message && this.options.onStatus) {
this.options.onStatus(message);
}
}
complete(message?: string): void {
this.active = false;
const now = Date.now();
const duration = now - this.startTime;
if (this.options.onProgress) {
this.options.onProgress(100);
}
if (message && this.options.onStatus) {
this.options.onStatus(message);
}
return; // Duration for debugging
}
fail(errorMessage: string): void {
this.active = false;
if (this.options.onProgress) {
this.options.onProgress(0);
}
if (this.options.onStatus) {
this.options.onStatus(`Failed: ${errorMessage}`);
}
}
static createWithTimeout(totalDurationMs: number, options?: ProgressOptions): ProgressIndicator {
return new ProgressIndicator(options);
}
}
// Simple progress tracker for long-running commands
export function trackProgress(
command: string,
estimatedDurationMs: number = PROGRESS_THRESHOLD_MS * 2,
callback: (progress: ProgressIndicator) => Promise<any>
): Promise<any> {
const indicator = new ProgressIndicator();
return new Promise((resolve, reject) => {
let timeoutId: NodeJS.Timeout | null = null;
if (estimatedDurationMs > PROGRESS_THRESHOLD_MS) {
// Start progress tracking
indicator.start(estimatedDurationMs);
// Periodic updates
const intervalId = setInterval(() => {
if (!indicator.active) return;
const elapsed = Date.now() - indicator.startTime;
const percentage = Math.min(100, (elapsed / estimatedDurationMs) * 100);
indicator.update(percentage, `${command} (${Math.floor(percentage)}%)`);
}, PROGRESS_UPDATE_INTERVAL_MS);
timeoutId = setInterval(() => {
if (!indicator.active) return;
const elapsed = Date.now() - indicator.startTime;
if (elapsed > estimatedDurationMs * 0.8) {
indicator.update(95, `${command} nearly complete...`);
}
}, PROGRESS_UPDATE_INTERVAL_MS);
}
callback(indicator)
.then(result => {
if (timeoutId) clearInterval(timeoutId as unknown as number);
if (indicator.active) {
indicator.complete(command === "exec" ? `Command completed successfully` : 'Operation completed');
}
resolve(result);
})
.catch(error => {
if (timeoutId) clearInterval(timeoutId as unknown as number);
indicator.fail(error instanceof Error ? error.message : String(error));
reject(error);
});
});
}