/** * The forge's five-step progress model, assembled from both halves of the run. * * A forge is split across two machines — steps 1-2 on api-server, arriving as SSE frames, and * steps 3-5 in this process, arriving through `ForgeDeps.onProgress` — and a creator watching it * does not care which. So both feed ONE tracker, which owns the whole picture: which step is * running, how far through it is, and what the level being built contains. * * The tracker has two outputs and they are deliberately different: * * - `log` — the terminal. Lines, appended, throttled so a thousand-chunk bake does not bury * everything else. * - `report` — the job record `bitmagic dev` reads (`project/jobs.ts`). A SNAPSHOT, rewritten in * place, because a panel showing five live rows wants the current state of all five, not the * last sentence anyone said. * * Pure: no filesystem, no browser, no network, no clock it does not accept as a parameter. That * matters here more than anywhere else in the command — this is the display logic for a * thirty-minute, expensive, hard-to-reproduce operation, so it has to be exercisable in * milliseconds. * * This replaced `bake-progress.ts`, which printed the level bake's chunk counts from a separate * subscription to the browser page. That subscription existed only because the transport dropped * progress messages after re-arming its idle timer; it now forwards them * (`ForgeTransportOptions.onProgress`), so the counts arrive as ordinary step events and the * second, parallel path is gone. */ import { type ForgeLayoutMap, type ForgeProgressEvent, type ForgeProgressSummary, type ForgeStepKey } from '@bitmagic/world-forger/pipeline/index.js'; export type ForgeStepStatus = 'pending' | 'running' | 'done'; export interface ForgeStepState { key: ForgeStepKey; label: string; status: ForgeStepStatus; /** Wall time from this step's first event to its `done`, once it has one. */ durationMs?: number; /** Determinate progress, for the steps that have a countable unit. */ completed?: number; total?: number; /** What the step is on right now — cleared when the step finishes. */ detail?: string; /** A resume found this step's artifact and skipped it. */ resumed?: boolean; } /** The whole picture, as a host renders it. Rewritten in place on every report. */ export interface ForgeProgressSnapshot { steps: ForgeStepState[]; /** What the level being built contains, accumulated across the steps that know parts of it. */ summary?: ForgeProgressSummary; } export interface ForgeProgressTracker { /** Fold one event from either lane into the model. */ apply(event: ForgeProgressEvent): void; /** Record a step the pipeline skipped because a resume found its artifact. */ skip(key: ForgeStepKey, message: string): void; snapshot(): ForgeProgressSnapshot; } export interface ForgeProgressTrackerOptions { /** Terminal printer. Receives fully-formatted lines. */ log: (message: string) => void; /** Job-record writer. Receives a fresh snapshot; may be omitted when nothing is watching. */ report?: (snapshot: ForgeProgressSnapshot) => void; /** * The level's map, handed over the once it arrives. * * A separate sink rather than a field on the snapshot, and that is the point: the snapshot is * rewritten on every tick, and ~25KB of raster in it would be rewritten with it for the rest of * a thirty-minute run. Routing it out here makes "written once" structural. */ onLayout?: (layout: ForgeLayoutMap) => void; now?: () => number; /** Overridden in tests. */ logIntervalMs?: number; reportIntervalMs?: number; /** * The run's step list. Defaults to the 3D `FORGE_STEPS`; a side-on run passes * `SIDEON_FORGE_STEPS` (4 steps, no level bake) so the `[n/4]` banners and the * row model match what actually runs — with the default, a side-on run would * print `[n/5]` and leave "Baking level" pending forever. */ steps?: ReadonlyArray<{ readonly key: ForgeStepKey; readonly label: string; }>; } /** * Minimum gap between printed `update` lines. The bake is already throttled at its source * (`voxelize-level.ts`), but the archetype step is not: a city forge can carry hundreds of * archetypes and each one emits, so without this the step list scrolls away. */ export declare const PROGRESS_LOG_MIN_INTERVAL_MS = 3000; /** * Minimum gap between job-record writes. Every write is a temp-file-and-rename that the dev * sidecar's directory watcher then picks up, so this is the one throttle that protects somebody * else's process rather than this one's output. Shorter than the log interval because the panel * re-renders at 1 Hz anyway and a stale bar looks broken in a way a missing log line does not. */ export declare const PROGRESS_REPORT_MIN_INTERVAL_MS = 1000; export declare function createForgeProgressTracker(options: ForgeProgressTrackerOptions): ForgeProgressTracker;