# `progress/tracker` — real-time import progress _(no submodules)_ ## Purpose Provides the `ImportProgressTracker` class consumed by the canonical `ImportService` (and exported for direct use by CLI tools / batch workers) to track import jobs in real time. The tracker counts successes, failures, and overall progress, and exposes derived metrics (rows per second, percent complete, estimated remaining time) without requiring the caller to keep track of the wall clock themselves. The clock is injectable so test suites can pin deterministic ETA / rows-per-sec assertions without sleeping. Without a clock override the tracker uses `Date.now`, so the production hot path is allocation-free after construction. ## Features - Single-row (`recordSuccess`, `recordFailure`) and batched (`recordBatchSuccess(count)`, `recordBatchFailure(count)`) updates. - Listener fan-out via `onProgress(callback)` — returns an unsubscribe function so consumers can detach cleanly. - Snapshot shape (`ProgressSnapshot`) carries `rowsPerSecond` rounded to 2 decimal places, `percentComplete` rounded to an integer, and `estimatedRemainingMs` rounded to an integer. - Injectable `Clock` from `@suaveplan/types` for deterministic tests. - `isComplete()` predicate flips once `processedRows >= totalRows` — handles `totalRows === 0` as immediately complete. ## Usage: Basic Example ```ts import { ImportProgressTracker } from "@suaveplan/api-plugin-import"; const tracker = new ImportProgressTracker("job-1", 100); tracker.onProgress((snap) => console.log(snap.percentComplete)); tracker.recordBatchSuccess(50); // logs 50 tracker.recordBatchFailure(10); // logs 60 ``` ## Usage: Advanced Example ```ts import { ImportProgressTracker } from "@suaveplan/api-plugin-import"; // Deterministic test with an injected clock — advance time by a known delta let now = 1_000; const tracker = new ImportProgressTracker("j", 100, { clock: () => now }); tracker.recordBatchSuccess(50); now += 1_000; const snap = tracker.snapshot(); // snap.rowsPerSecond === 50, snap.estimatedRemainingMs === 1_000 ``` ## API Reference - `class ImportProgressTracker` - `constructor(jobId: string, totalRows: number, options?: ImportProgressTrackerOptions)` - `onProgress(callback: ProgressCallback): () => void` - `recordSuccess(): void` - `recordFailure(): void` - `recordBatchSuccess(count: number): void` - `recordBatchFailure(count: number): void` - `snapshot(): ProgressSnapshot` - `isComplete(): boolean` - `interface ImportProgressTrackerOptions { clock?: Clock }` - `interface ProgressSnapshot { jobId; totalRows; processedRows; successCount; failedCount; percentComplete; rowsPerSecond; estimatedRemainingMs; elapsedMs; startedAt }` - `type ProgressCallback = (snap: ProgressSnapshot) => void` ## Implementation Notes - `totalRows === 0` short-circuits `percentComplete` to `0` and `estimatedRemainingMs` to `0` to avoid `NaN` from a zero divisor. - The listener array is replaced (not spliced) on `off()` so a listener removing itself from inside its own callback does not invalidate the in-flight iteration. - `startedAtIso` is captured once in the constructor and re-used on every snapshot to avoid `new Date(...)` allocations on the hot path.