fix(watcher): cap startup wait

This commit is contained in:
2026-05-20 00:09:07 -04:00
parent b7e421483d
commit 62fc80c70f
6 changed files with 34 additions and 158 deletions

View File

@@ -18,8 +18,8 @@ import {
type LaunchContext,
} from "./daemonProtocol.ts";
// Default Idle TTL - 5 minutes. Per-server overrides via ServerConfig.idleTtlMs.
const DEFAULT_IDLE_TTL_MS = 5 * 60 * 1000;
const WATCHER_READY_TIMEOUT_MS = 5000;
// Client Entry - One LspClient per (server.id, rootDir), plus the bookkeeping
// needed to keep files in sync and evict on idleness.
@@ -38,9 +38,6 @@ interface ClientEntry {
idleTimer: NodeJS.Timeout | null;
ttlMs: number;
lastUsed: number;
// watcher: Lazy - created on first registerCapability for watched files,
// disposed on eviction. Null if the server never registered any watchers
// (or if PI_LSP_DISABLE_WATCHERS is set).
watcher: WorkspaceWatcher | null;
unsubscribeWatchers: (() => void) | null;
}
@@ -109,10 +106,7 @@ async function getOrCreateEntry(
return entry;
}
// Attach Watcher - Subscribes to the client's watcher-registration events.
// The first non-empty registration lazily creates the WorkspaceWatcher;
// subsequent register/unregister calls update its pattern set in place.
// Honors PI_LSP_DISABLE_WATCHERS for emergency rollback.
// Attach Watcher - Registration can happen during initialize, before the daemon subscribes.
async function attachWatcher(entry: ClientEntry): Promise<void> {
if (process.env.PI_LSP_DISABLE_WATCHERS) return;
const sync = async () => {
@@ -128,17 +122,34 @@ async function attachWatcher(entry: ClientEntry): Promise<void> {
log(`watcher patterns`, entry.server.id, JSON.stringify(patterns));
}
entry.watcher.setPatterns(patterns);
if (patterns.length > 0) await entry.watcher.ready();
if (patterns.length > 0) await waitForWatcherReady(entry);
};
entry.unsubscribeWatchers = entry.client.onWatchersChanged(() => void sync());
// Initial Sync - Server may have already sent registerCapability during
// initialize before we subscribed. Wait for chokidar's initial scan so
// externally-created files are not swallowed as ignoreInitial events.
await sync();
}
// Forward Events - Sends a batched workspace/didChangeWatchedFiles to the
// server. We catch errors so a downed transport doesn't crash the daemon.
async function waitForWatcherReady(entry: ClientEntry): Promise<void> {
if (!entry.watcher) return;
let timeout: NodeJS.Timeout | null = null;
let timedOut = false;
try {
await Promise.race([
entry.watcher.ready(),
new Promise<void>((resolve) => {
timeout = setTimeout(() => {
timedOut = true;
resolve();
}, WATCHER_READY_TIMEOUT_MS);
}),
]);
} finally {
if (timeout) clearTimeout(timeout);
}
if (timedOut) {
log(`watcher ready timeout`, entry.server.id, entry.rootDir);
}
}
function forwardEvents(entry: ClientEntry, events: FileEvent[]): void {
try {
if (process.env.LSP_DEBUG) {