mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 08:52:52 +08:00
fix(watch): a watch callback that throws no longer ends the host's process (#3092)
A program that uses CodeGraph as a library passes callbacks to watch(). FileWatcher.flush() runs from a timer with no caller to catch anything, and it called onSyncError and onDegraded (through degrade()) in its catch block. A throw from either left flush() as an unhandled rejection, which ends the host process with exit 1. A throw from onDegraded also skipped the stop() that degrade() owes, so a watcher reporting itself degraded kept retrying. A throw from onSyncComplete sat inside the try, so it counted a sync that worked as a failed one and called onSyncError with an error that was no sync failure. All three now go through one guard, callHost(), which logs the throw as a warning and changes nothing else. The CLI and MCP server are not affected: their callbacks only write to stderr. Only a host's own callback can reach this. Validation: - Three new tests in watcher.test.ts fail on main and pass here (64/64 in the file). Putting each guard back one at a time fails exactly its own test. - A host script on the public API (watch() with a throwing onSyncComplete and onSyncError, then a real file edit): main exits 1, this branch logs the throw and keeps running, exit 0. PR #3074 listed ten lint findings; this keeps the two watcher call sites' purpose and leaves out the rest, which change no behaviour: - flushOrdered(): flushChain's body catches everything, so it cannot reject. - stop() from timers and signals: nothing stop() awaits can reject today, and a forced rejection already exits (exit 1, one fatal line). - await on getCodeGraph and the upgrade functions: plain values, nothing else runs in that microtask. - { cause } on the rebuild and daemon-lock errors: the rebuild message already carries the reason, and nothing reads or prints cause. Reimplements PR #3074 by @johnhldiver-beep. Refs issue 2643. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
17d5426d63
commit
22cf7d3aa1
+18
-3
@@ -853,6 +853,21 @@ export class FileWatcher {
|
||||
return this.ignoreMatcher.ignores(rel + '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Call one of the host's {@link WatchOptions} callbacks. They are diagnostics:
|
||||
* one that throws is logged and changes nothing here. A sync runs from a timer,
|
||||
* so a throw would have no caller and end the host's process as an unhandled
|
||||
* rejection. It would also read as a failed sync, or skip the stop that a
|
||||
* degrade owes.
|
||||
*/
|
||||
private callHost(name: string, call: () => void): void {
|
||||
try {
|
||||
call();
|
||||
} catch (err) {
|
||||
logWarn(`File watcher ${name} callback threw`, { error: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable live watching after a terminal runtime failure
|
||||
* (watch-resource exhaustion, lock contention past the retry budget, or a
|
||||
@@ -866,7 +881,7 @@ export class FileWatcher {
|
||||
this.degradedByLock = byLock;
|
||||
this.recoveringFromLock = false;
|
||||
logWarn('File watcher disabled', { projectRoot: this.projectRoot, reason, ...context });
|
||||
this.onDegraded?.(reason);
|
||||
this.callHost('onDegraded', () => this.onDegraded?.(reason));
|
||||
this.stop();
|
||||
}
|
||||
|
||||
@@ -1114,7 +1129,7 @@ export class FileWatcher {
|
||||
this.pendingFiles.delete(filePath);
|
||||
}
|
||||
}
|
||||
this.onSyncComplete?.(result);
|
||||
this.callHost('onSyncComplete', () => this.onSyncComplete?.(result));
|
||||
} catch (err) {
|
||||
if (!scoped) this.needsFullScan = true;
|
||||
if (err instanceof LockUnavailableError) {
|
||||
@@ -1145,7 +1160,7 @@ export class FileWatcher {
|
||||
error: error.message,
|
||||
retryCount: this.syncFailureRetryCount,
|
||||
});
|
||||
this.onSyncError?.(error);
|
||||
this.callHost('onSyncError', () => this.onSyncError?.(error));
|
||||
// A persistent (deterministic) sync failure — a broken extractor on a
|
||||
// specific file, DB corruption, SQLITE_FULL, an OOM in resolution —
|
||||
// would otherwise retry forever at the debounce cadence, spamming logs
|
||||
|
||||
Reference in New Issue
Block a user