Fix memory leak in dev server

This commit is contained in:
2026-10-16 14:25:18 +01:00
parent 3d0771f2d0
commit 5dc783fe97
36 changed files with 642 additions and 406 deletions
+58 -19
View File
@@ -1,6 +1,6 @@
import chokidar from "chokidar";
import path from "path";
import { existsSync } from "fs";
import { existsSync, mkdirSync, writeFileSync } from "fs";
import grabDirNames from "../../utils/grab-dir-names";
import fullRebuild from "./full-rebuild";
import { AppData } from "../../data/app-data";
@@ -10,10 +10,18 @@ import grabAllPages from "../../utils/grab-all-pages";
import serverPostBuildFn from "./server-post-build-fn";
import { log } from "../../utils/log";
import { logBuildErrorsOnce } from "../../utils/build-error-log";
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
// Tracks the last watcher re-creation so the 5s watchdog in start-server
// doesn't fight fullRebuild (which also restarts the watcher after a rebuild).
const { ROOT_DIR, BUNX_CWD_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
// The watchdog writes this file and expects the watcher to report it. It
// sits under `.bunext` (otherwise ignored), so only this path is watched.
const WATCH_PROBE_DIR = path.join(BUNX_CWD_DIR, "watch-probe");
const WATCH_PROBE_FILE = path.join(WATCH_PROBE_DIR, "probe");
// Dedupes near-simultaneous restarts (fullRebuild, watchdog).
let last_watcher_restart = 0;
// The current watcher finished its initial scan; probes written before
// that may not produce events.
let watcher_ready = false;
// A probe was written and the watcher hasn't reported it yet.
let probe_pending = false;
/**
* Closes and re-creates the FS watcher. Deduped: callers that restart the
* watcher around the same time (fullRebuild, reloadWatcher, watchdog) only
@@ -48,11 +56,27 @@ function grabPagesSnapshot() {
}
}
/**
* True when the watcher was last (re)created long enough ago that the
* watchdog should consider re-creating it again.
* Called periodically by the dev server. Re-creates the watcher only when it
* failed to report the probe written on the previous tick, i.e. when it went
* stale. A healthy watcher is left alone: re-creating it rescans the whole
* project tree.
*/
export function watcherRestartAvailable() {
return Date.now() - last_watcher_restart > 6000;
export function watcherWatchdogTick() {
if (!global.BUNEXT_PAGES_SRC_WATCHER || !watcher_ready)
return;
if (probe_pending) {
restartWatcher();
return;
}
try {
mkdirSync(WATCH_PROBE_DIR, { recursive: true });
writeFileSync(WATCH_PROBE_FILE, String(Date.now()));
probe_pending = true;
}
catch (error) { }
}
function isWatchProbePath(p) {
return (p === WATCH_PROBE_FILE || p === WATCH_PROBE_DIR || p === BUNX_CWD_DIR);
}
const TARGET_FILES_MATCH = /\.(tsx?|jsx?|css)$/;
// Rebuild coalescing: editors emit bursts of events per save (change +
@@ -170,10 +194,10 @@ function isPathExcluded(filename, full_file_path) {
// install watches (and deliver events) for trees the bundler never reads.
function buildIgnoredPatterns() {
const ignored = [
/(^|[\/\\])\../,
(p) => !isWatchProbePath(p) && /(^|[\/\\])\../.test(p),
/(^|[\/\\])node_modules([\/\\]|$)/,
/(^|[\/\\])public([\/\\]|$)/,
/(^|[\/\\])\.bunext([\/\\]|$)/,
(p) => !isWatchProbePath(p) && /(^|[\/\\])\.bunext([\/\\]|$)/.test(p),
/(^|[\/\\])\.git([\/\\]|$)/,
/(^|[\/\\])dist([\/\\]|$)/,
/bun\.lockb$/,
@@ -195,6 +219,12 @@ function buildIgnoredPatterns() {
return ignored;
}
export default async function watcherEsbuildCTX(params) {
try {
mkdirSync(WATCH_PROBE_DIR, { recursive: true });
}
catch (error) { }
watcher_ready = false;
probe_pending = false;
const pages_src_watcher = chokidar.watch(ROOT_DIR, {
ignored: buildIgnoredPatterns(),
persistent: true,
@@ -202,18 +232,27 @@ export default async function watcherEsbuildCTX(params) {
});
watcherHandlers(pages_src_watcher, (event, filePath) => handleEvent(event, filePath));
const pages_snapshot = params?.pages_snapshot;
if (pages_snapshot !== undefined) {
pages_src_watcher.on("ready", () => {
const current = grabPagesSnapshot();
if (current !== undefined && current !== pages_snapshot) {
pending_structural = true;
scheduleRebuild();
}
});
}
pages_src_watcher.on("ready", () => {
if (global.BUNEXT_PAGES_SRC_WATCHER === pages_src_watcher) {
watcher_ready = true;
}
if (pages_snapshot === undefined)
return;
const current = grabPagesSnapshot();
if (current !== undefined && current !== pages_snapshot) {
pending_structural = true;
scheduleRebuild();
}
});
global.BUNEXT_PAGES_SRC_WATCHER = pages_src_watcher;
}
function handleEvent(event, filePath) {
if (isWatchProbePath(filePath)) {
if (filePath === WATCH_PROBE_FILE) {
probe_pending = false;
}
return;
}
try {
const filename = path.relative(ROOT_DIR, filePath);
const full_file_path = filePath;