/** * Deciding *when* the browser should reload. * * The hard part is not noticing a change — `fs.watch` does that — it is not reloading at the wrong * moment. Three wrong moments, each of which this file exists to avoid: * * 1. **Mid-burst.** An agent editing five files produces five reloads unless the writes are let * go quiet first. Hence the debounce. * 2. **Mid-compile.** A `.ts` edit changes `src/` long before `tsc --watch` finishes emitting * `dist/`, and every module the project loads resolves into `dist/`. Reloading on the source * write shows the creator the *old* build and reads as "my change did nothing". Hence the * compile gate: reloads are held while a compile is open and released when it lands clean. * 3. **After a failed compile.** `dist/` still holds the last good emit, so a reload would show * working code for broken source — the most confusing outcome of the three. The pending reload * is dropped and the shell is told the build failed instead. * * `ReloadCoordinator` holds all of that and touches no I/O, so it is testable with fake timers and * no filesystem. `watchProject` and `pipeTscWatch` are the thin I/O shells that feed it. */ import type { ChildProcess } from 'child_process'; import type { BuildStatus } from './reload-bus.js'; /** * How long the writes must stay quiet before a reload fires. * * Long enough that `tsc --watch` (which has its own ~250 ms debounce before it reports * `File change detected`) has almost always opened its compile by the time this elapses, so the * gate below actually gets to hold the reload. Too short and a `.ts` edit produces two reloads — * one stale, one correct — instead of one. */ export declare const RELOAD_QUIET_MS = 700; /** * How long after one of our own writes to ignore file events. * * `/api/scene/save` and the HQ generation both write `world.json`, and a reload fired at the * creator's own gizmo drag would throw away the drag that caused it. `fs.watch` delivers * asynchronously but promptly, so this only has to outlast the delivery, not the write. */ export declare const OWN_WRITE_GRACE_MS = 400; export interface ReloadCoordinatorOptions { onReload: (reason: string) => void; onBuild: (status: BuildStatus, message?: string) => void; quietMs?: number; } export declare class ReloadCoordinator { private readonly options; private timer; private pendingReason; private compiling; private suppressUntil; private stopped; constructor(options: ReloadCoordinatorOptions); /** A file under a watched directory changed. Arms — or re-arms — the quiet window. */ noteFileChange(reason: string): void; /** * Reload as soon as it is safe to — now, or the moment the compile in flight lands clean. * * The wait is the whole point for `bitmagic reload`: an agent's stop hook fires immediately after * its last edit, which is precisely when `tsc` is still compiling it. Returns whether the reload * was queued behind a compile rather than sent, so the command can say which happened. */ requestNow(reason: string): { queued: boolean; }; /** The sidecar wrote `world.json` itself; the resulting file events are not news. */ noteOwnWrite(): void; noteCompileStart(): void; noteCompileEnd(errorCount: number): void; stop(): void; /** Send the pending reload, unless a compile is still open — then it waits for `noteCompileEnd`. */ private release; } export interface ProjectWatcher { close(): void; } /** * Watch the two directories that decide what the browser shows: `src/` (what the agent and the * creator edit — including `work/world.json`) and `dist/` (what the page actually loads). * * `dist/` is watched as well as `src/` even though the compile gate already covers the normal * `tsc --watch` path, because it is what a build run outside this process — a manual `tsc`, an * editor's own compiler — lands in, and that should reach the browser too. * * A directory that cannot be watched is skipped with a note rather than failing the command: a * missing `dist/` means "not built yet", and losing auto-reload is a smaller loss than losing * `bitmagic dev`. */ export declare function watchProject(root: string, coordinator: ReloadCoordinator, log: (message: string) => void): ProjectWatcher; /** * Feed one line of `tsc --watch` output to the coordinator. Exported for its test — the markers are * tsc's own wording, and a version that changes them would otherwise break auto-reload silently. */ export declare function noteTscLine(line: string, coordinator: ReloadCoordinator): void; /** * Spawn `tsc --watch` with its output piped through us instead of inherited. * * Piping is what makes the compile gate possible — the only signal that a build finished, and * whether it finished clean, is tsc's own `Found N errors` line. Every line is re-echoed verbatim, * so the creator sees exactly what an inherited stdio would have shown, and `--pretty` is forced * back on when our own stdout is a terminal (tsc drops colour the moment it is piped). */ export declare function pipeTscWatch(tsc: string, cwd: string, coordinator: ReloadCoordinator): ChildProcess;