/** * Recycle the dev server once hot rebuilds have piled up. * * Every rebuild of a `.wrn` file has to be given a new module identity, * because Bun caches modules by path and would otherwise serve the old one. * Bun has no API to unload a module, so each rebuild retains its predecessor * for the life of the process -- measured at roughly 0.66 MB per rebuild, * while edits that mint no new module (CSS) cost nothing. Over a long session * that is the difference between a fast dev server and a stuck one. * * The process therefore recycles itself: the child exits with * RESTART_EXIT_CODE and the CLI supervisor respawns it. Browsers reconnect on * their own because the HMR client already retries. * * Recycling is deferred until the server has been idle for a moment, so it * never interrupts a request in flight. The cost is that in-memory state * (realtime rooms, warmed caches) resets at that point, which is why the * threshold is high enough that an ordinary editing session never reaches it. */ export interface RecycleMonitorOptions { /** Retained rebuilds tolerated before a recycle is armed. */ threshold: number; /** Quiet period required before recycling, in milliseconds. */ idleMs: number; onRecycle: (reason: string) => void; } export interface RecycleMonitor { /** Count one rebuild that retained a module version. */ recordRebuild(): void; /** Note that a request was served, at `now`. */ recordRequest(now: number): void; /** Recycle if the threshold is passed and the server has gone quiet. */ tick(now: number): void; retained(): number; } export function createRecycleMonitor(options: RecycleMonitorOptions): RecycleMonitor { let rebuilds = 0; let lastRequestAt: number | null = null; let recycled = false; return { recordRebuild() { rebuilds++; }, recordRequest(now: number) { lastRequestAt = now; }, tick(now: number) { if (recycled) return; if (rebuilds < options.threshold) return; // A server that has served nothing is idle by definition. if (lastRequestAt !== null && now - lastRequestAt < options.idleMs) return; recycled = true; options.onRecycle( `${rebuilds} hot rebuilds retained; restarting to release the memory they hold`, ); }, retained() { return rebuilds; }, }; }