kist
Version:
Lightweight Package Pipeline Processor with Plugin Architecture
310 lines (272 loc) • 8.75 kB
text/typescript
// ============================================================================
// 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,
});
}