Files
suaveplan/packages/api-stack/api/api-plugin-import/src/progress/tracker.md
2026-05-27 23:29:53 +01:00

3.2 KiB

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

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

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.