first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
# `progress/tracker` — real-time import progress
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.