UNPKG

kist

Version:

Lightweight Package Pipeline Processor with Plugin Architecture

310 lines (272 loc) 8.75 kB
// ============================================================================ // Import // ============================================================================ import { AbstractProcess } from "../abstract/AbstractProcess.js"; // ============================================================================ // Types // ============================================================================ /** * Options for creating a progress reporter. */ interface ProgressOptions { /** Total number of items to process */ total: number; /** Label for the operation */ label?: string; /** Whether to show percentage */ showPercentage?: boolean; /** Whether to show ETA */ showEta?: boolean; /** Update interval in milliseconds */ updateInterval?: number; /** Custom format function */ formatFn?: (progress: ProgressState) => string; } /** * Current state of progress. */ interface ProgressState { /** Number of items completed */ completed: number; /** Total number of items */ total: number; /** Percentage complete (0-100) */ percentage: number; /** Elapsed time in milliseconds */ elapsed: number; /** Estimated time remaining in milliseconds */ eta: number | null; /** Items processed per second */ rate: number; /** Operation label */ label: string; } // ============================================================================ // Class // ============================================================================ /** * ProgressReporter provides progress tracking and reporting for long-running * operations. It calculates completion percentage, elapsed time, ETA, and * processing rate. * * @example * ```typescript * const progress = new ProgressReporter({ total: 100, label: 'Processing files' }); * progress.start(); * * for (const file of files) { * await processFile(file); * progress.increment(); * } * * progress.finish(); * ``` */ export class ProgressReporter extends AbstractProcess { // Parameters // ======================================================================== private total: number; private completed: number = 0; private label: string; private showPercentage: boolean; private showEta: boolean; private updateInterval: number; private formatFn?: (progress: ProgressState) => string; private startTime: number = 0; private lastUpdateTime: number = 0; private lastReportedPercentage: number = -1; private isRunning: boolean = false; // Constructor // ======================================================================== /** * Creates a new ProgressReporter. * @param options - Configuration options for the progress reporter */ constructor(options: ProgressOptions) { super(); this.total = options.total; this.label = options.label || "Processing"; this.showPercentage = options.showPercentage ?? true; this.showEta = options.showEta ?? true; this.updateInterval = options.updateInterval ?? 100; // 100ms default this.formatFn = options.formatFn; } // Public Methods // ======================================================================== /** * Starts the progress tracking. */ public start(): void { this.startTime = performance.now(); this.lastUpdateTime = this.startTime; this.completed = 0; this.isRunning = true; this.report(); } /** * Increments the completed count by the specified amount. * @param amount - Amount to increment (default: 1) */ public increment(amount: number = 1): void { this.completed = Math.min(this.completed + amount, this.total); this.maybeReport(); } /** * Sets the completed count to a specific value. * @param value - The new completed value */ public setCompleted(value: number): void { this.completed = Math.min(Math.max(0, value), this.total); this.maybeReport(); } /** * Finishes the progress tracking and reports final status. */ public finish(): void { this.completed = this.total; this.isRunning = false; this.report(true); } /** * Cancels the progress tracking. */ public cancel(): void { this.isRunning = false; this.logWarn( `${this.label}: Cancelled at ${this.getPercentage().toFixed(1)}%`, ); } /** * Gets the current progress state. */ public getState(): ProgressState { const elapsed = performance.now() - this.startTime; const rate = this.completed / (elapsed / 1000) || 0; const remaining = this.total - this.completed; const eta = rate > 0 ? (remaining / rate) * 1000 : null; return { completed: this.completed, total: this.total, percentage: this.getPercentage(), elapsed, eta, rate, label: this.label, }; } /** * Gets the current percentage complete. */ public getPercentage(): number { return this.total > 0 ? (this.completed / this.total) * 100 : 0; } /** * Gets the elapsed time in milliseconds. */ public getElapsed(): number { return performance.now() - this.startTime; } // Private Methods // ======================================================================== /** * Reports progress if enough time has passed since last report. */ private maybeReport(): void { const now = performance.now(); if (now - this.lastUpdateTime >= this.updateInterval) { this.report(); this.lastUpdateTime = now; } } /** * Reports current progress to the log. * @param force - Force reporting even if percentage hasn't changed */ private report(force: boolean = false): void { const percentage = Math.floor(this.getPercentage()); // Only report if percentage has changed or forced if (!force && percentage === this.lastReportedPercentage) { return; } this.lastReportedPercentage = percentage; const state = this.getState(); let message: string; if (this.formatFn) { message = this.formatFn(state); } else { message = this.formatProgress(state); } this.logInfo(message); } /** * Formats the progress state into a human-readable string. */ private formatProgress(state: ProgressState): string { const parts: string[] = [state.label]; parts.push(`${state.completed}/${state.total}`); if (this.showPercentage) { parts.push(`(${state.percentage.toFixed(1)}%)`); } if ( this.showEta && state.eta !== null && state.completed < state.total ) { parts.push(`ETA: ${this.formatDuration(state.eta)}`); } if (state.completed === state.total) { parts.push(`Done in ${this.formatDuration(state.elapsed)}`); } return parts.join(" "); } /** * Formats a duration in milliseconds to a human-readable string. */ private formatDuration(ms: number): string { if (ms < 1000) { return `${Math.round(ms)}ms`; } if (ms < 60000) { return `${(ms / 1000).toFixed(1)}s`; } const minutes = Math.floor(ms / 60000); const seconds = Math.round((ms % 60000) / 1000); return `${minutes}m ${seconds}s`; } } // ============================================================================ // Factory Functions // ============================================================================ /** * Creates a simple progress reporter for file operations. * @param fileCount - Total number of files * @param label - Operation label */ export function createFileProgress( fileCount: number, label: string = "Processing files", ): ProgressReporter { return new ProgressReporter({ total: fileCount, label, showPercentage: true, showEta: true, }); } /** * Creates a progress reporter for build operations. * @param stepCount - Total number of steps * @param label - Build label */ export function createBuildProgress( stepCount: number, label: string = "Building", ): ProgressReporter { return new ProgressReporter({ total: stepCount, label, showPercentage: true, showEta: true, updateInterval: 250, }); }