perf: lighten gateway watch startup

This commit is contained in:
Peter Steinberger
2026-05-04 08:36:45 +01:00
parent e11a8a84ac
commit 7129db1960
4 changed files with 180 additions and 74 deletions

View File

@@ -0,0 +1,63 @@
import path from "node:path";
import {
BUNDLED_PLUGIN_PATH_PREFIX,
BUNDLED_PLUGIN_ROOT_DIR,
} from "./lib/bundled-plugin-paths.mjs";
export const runNodeSourceRoots = ["src", BUNDLED_PLUGIN_ROOT_DIR];
export const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"];
export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles];
export const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]);
const ignoredRunNodeRepoPaths = new Set([
"src/canvas-host/a2ui/.bundle.hash",
"src/canvas-host/a2ui/a2ui.bundle.js",
]);
const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/;
export const normalizeRunNodePath = (filePath) => String(filePath ?? "").replaceAll("\\", "/");
const isIgnoredSourcePath = (relativePath) => {
const normalizedPath = normalizeRunNodePath(relativePath);
return (
normalizedPath.endsWith(".test.ts") ||
normalizedPath.endsWith(".test.tsx") ||
normalizedPath.endsWith("test-helpers.ts")
);
};
const isBuildRelevantSourcePath = (relativePath) => {
const normalizedPath = normalizeRunNodePath(relativePath);
return extensionSourceFilePattern.test(normalizedPath) && !isIgnoredSourcePath(normalizedPath);
};
const isRestartRelevantExtensionPath = (relativePath) => {
const normalizedPath = normalizeRunNodePath(relativePath);
if (extensionRestartMetadataFiles.has(path.posix.basename(normalizedPath))) {
return true;
}
return isBuildRelevantSourcePath(normalizedPath);
};
const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => {
const normalizedPath = normalizeRunNodePath(repoPath).replace(/^\.\/+/, "");
if (ignoredRunNodeRepoPaths.has(normalizedPath)) {
return false;
}
if (runNodeConfigFiles.includes(normalizedPath)) {
return true;
}
if (normalizedPath.startsWith("src/")) {
return !isIgnoredSourcePath(normalizedPath.slice("src/".length));
}
if (normalizedPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
return isRelevantBundledPluginPath(normalizedPath.slice(BUNDLED_PLUGIN_PATH_PREFIX.length));
}
return false;
};
export const isBuildRelevantRunNodePath = (repoPath) =>
isRelevantRunNodePath(repoPath, isBuildRelevantSourcePath);
export const isRestartRelevantRunNodePath = (repoPath) =>
isRelevantRunNodePath(repoPath, isRestartRelevantExtensionPath);

View File

@@ -16,14 +16,22 @@ import {
writeRuntimePostBuildStamp as writeDistRuntimePostBuildStamp,
} from "./lib/local-build-metadata.mjs";
import { listStaticExtensionAssetSources } from "./lib/static-extension-assets.mjs";
import {
extensionRestartMetadataFiles,
isBuildRelevantRunNodePath,
isRestartRelevantRunNodePath,
normalizeRunNodePath as normalizePath,
runNodeConfigFiles,
runNodeSourceRoots,
runNodeWatchedPaths,
} from "./run-node-watch-paths.mjs";
import { runRuntimePostBuild } from "./runtime-postbuild.mjs";
export { isBuildRelevantRunNodePath, isRestartRelevantRunNodePath, runNodeWatchedPaths };
const buildScript = "scripts/tsdown-build.mjs";
const compilerArgs = [buildScript, "--no-clean"];
const runNodeSourceRoots = ["src", BUNDLED_PLUGIN_ROOT_DIR];
const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"];
export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles];
const runtimePostBuildWatchedPaths = [
"scripts/copy-bundled-plugin-metadata.mjs",
"scripts/copy-plugin-sdk-root-alias.mjs",
@@ -40,63 +48,10 @@ const runtimePostBuildWatchedPaths = [
"src/plugin-sdk/root-alias.cjs",
BUNDLED_PLUGIN_ROOT_DIR,
];
const ignoredRunNodeRepoPaths = new Set([
"src/canvas-host/a2ui/.bundle.hash",
"src/canvas-host/a2ui/a2ui.bundle.js",
]);
const runtimePostBuildScriptPaths = new Set(
runtimePostBuildWatchedPaths.filter((entry) => entry.startsWith("scripts/")),
);
const runtimePostBuildStaticAssetPaths = new Set(listStaticExtensionAssetSources());
const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/;
const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]);
const normalizePath = (filePath) => String(filePath ?? "").replaceAll("\\", "/");
const isIgnoredSourcePath = (relativePath) => {
const normalizedPath = normalizePath(relativePath);
return (
normalizedPath.endsWith(".test.ts") ||
normalizedPath.endsWith(".test.tsx") ||
normalizedPath.endsWith("test-helpers.ts")
);
};
const isBuildRelevantSourcePath = (relativePath) => {
const normalizedPath = normalizePath(relativePath);
return extensionSourceFilePattern.test(normalizedPath) && !isIgnoredSourcePath(normalizedPath);
};
const isRestartRelevantExtensionPath = (relativePath) => {
const normalizedPath = normalizePath(relativePath);
if (extensionRestartMetadataFiles.has(path.posix.basename(normalizedPath))) {
return true;
}
return isBuildRelevantSourcePath(normalizedPath);
};
const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => {
const normalizedPath = normalizePath(repoPath).replace(/^\.\/+/, "");
if (ignoredRunNodeRepoPaths.has(normalizedPath)) {
return false;
}
if (runNodeConfigFiles.includes(normalizedPath)) {
return true;
}
if (normalizedPath.startsWith("src/")) {
return !isIgnoredSourcePath(normalizedPath.slice("src/".length));
}
if (normalizedPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
return isRelevantBundledPluginPath(normalizedPath.slice(BUNDLED_PLUGIN_PATH_PREFIX.length));
}
return false;
};
export const isBuildRelevantRunNodePath = (repoPath) =>
isRelevantRunNodePath(repoPath, isBuildRelevantSourcePath);
export const isRestartRelevantRunNodePath = (repoPath) =>
isRelevantRunNodePath(repoPath, isRestartRelevantExtensionPath);
const statMtime = (filePath, fsImpl = fs) => {
try {

View File

@@ -5,7 +5,7 @@ import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import { isRestartRelevantRunNodePath, runNodeWatchedPaths } from "./run-node.mjs";
import { isRestartRelevantRunNodePath, runNodeWatchedPaths } from "./run-node-watch-paths.mjs";
const WATCH_NODE_RUNNER = "scripts/run-node.mjs";
const WATCH_RESTART_SIGNAL = "SIGTERM";
@@ -255,19 +255,6 @@ const releaseWatchLock = (lockHandle) => {
* }} [params]
*/
export async function runWatchMain(params = {}) {
let createWatcher = params.createWatcher;
if (!createWatcher) {
try {
const chokidarModule = await (params.loadChokidar ?? loadChokidar)();
createWatcher = (watchPaths, options) => chokidarModule.watch(watchPaths, options);
} catch (err) {
if (isInvalidPackageConfigError(err)) {
printFriendlyWatchStartupError(err);
}
throw err;
}
}
const deps = {
spawn: params.spawn ?? spawn,
process: params.process ?? process,
@@ -278,7 +265,8 @@ export async function runWatchMain(params = {}) {
sleep: params.sleep ?? sleep,
signalProcess: params.signalProcess ?? ((pid, signal) => process.kill(pid, signal)),
lockDisabled: params.lockDisabled === true,
createWatcher,
createWatcher: params.createWatcher,
loadChokidar: params.loadChokidar ?? loadChokidar,
watchPaths: params.watchPaths ?? runNodeWatchedPaths,
};
@@ -293,7 +281,7 @@ export async function runWatchMain(params = {}) {
childEnv.OPENCLAW_WATCH_COMMAND = deps.args.join(" ");
}
return await new Promise((resolve) => {
return await new Promise((resolve, reject) => {
let settled = false;
let shuttingDown = false;
let restartRequested = false;
@@ -357,6 +345,38 @@ export async function runWatchMain(params = {}) {
settle(1);
};
const rejectWatcherStartupError = (err) => {
if (settled) {
return;
}
settled = true;
shuttingDown = true;
if (watchProcess && typeof watchProcess.kill === "function") {
watchProcess.kill(WATCH_RESTART_SIGNAL);
}
releaseWatchLock(lockHandle);
watcher?.close?.().catch?.(() => {});
if (onSigInt) {
deps.process.off("SIGINT", onSigInt);
}
if (onSigTerm) {
deps.process.off("SIGTERM", onSigTerm);
}
reject(err);
};
const resolveCreateWatcher = async () => {
try {
const chokidarModule = await deps.loadChokidar();
return (watchPaths, options) => chokidarModule.watch(watchPaths, options);
} catch (err) {
if (isInvalidPackageConfigError(err)) {
printFriendlyWatchStartupError(err);
}
throw err;
}
};
const runAutoDoctorAndRestart = () => {
autoDoctorAttempted = true;
logWatcher(
@@ -405,8 +425,11 @@ export async function runWatchMain(params = {}) {
}
};
const startWatcher = () => {
watcher = deps.createWatcher(deps.watchPaths, {
const attachWatcher = (createWatcher) => {
if (settled) {
return;
}
watcher = createWatcher(deps.watchPaths, {
ignoreInitial: true,
ignored: (watchPath, stats) =>
isIgnoredWatchPath(watchPath, deps.cwd, deps.watchPaths, stats),
@@ -417,6 +440,14 @@ export async function runWatchMain(params = {}) {
watcher.on("error", handleWatcherError);
};
const startWatcher = () => {
if (deps.createWatcher) {
attachWatcher(deps.createWatcher);
return;
}
void resolveCreateWatcher().then(attachWatcher).catch(rejectWatcherStartupError);
};
onSigInt = () => {
shuttingDown = true;
if (watchProcess && typeof watchProcess.kill === "function") {