Skip to content

Commit 93da5af

Browse files
committed
fix(graph): auto-reconnect file watcher when daemon closes
When the daemon restarts (e.g., due to lock file or version changes), the graph command's file watcher connection is closed. Instead of exiting, the watcher now attempts to reconnect with exponential backoff and automatically re-registers itself with the new daemon. This ensures that the graph watch mode continues working seamlessly through daemon restarts without user intervention.
1 parent b94a914 commit 93da5af

File tree

1 file changed

+133
-44
lines changed
  • packages/nx/src/command-line/graph

1 file changed

+133
-44
lines changed

packages/nx/src/command-line/graph/graph.ts

Lines changed: 133 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -807,57 +807,146 @@ function debounce(fn: (...args) => void, time: number) {
807807
};
808808
}
809809

810+
const WATCHER_RECONNECT_CONFIG = {
811+
initialDelayMs: 100,
812+
maxDelayMs: 5000,
813+
backoffMultiplier: 1.5,
814+
maxAttempts: 30,
815+
};
816+
817+
async function waitForWatcherReconnectWithExponentialBackoff(
818+
config = WATCHER_RECONNECT_CONFIG
819+
): Promise<boolean> {
820+
let delay = config.initialDelayMs;
821+
let attempts = 0;
822+
823+
while (attempts < config.maxAttempts) {
824+
try {
825+
// Try to register file watcher
826+
const testUnregister = await daemonClient.registerFileWatcher(
827+
{
828+
watchProjects: 'all',
829+
includeGlobalWorkspaceFiles: true,
830+
allowPartialGraph: true,
831+
},
832+
() => {
833+
// This is just a test connection, close it immediately
834+
}
835+
);
836+
testUnregister();
837+
return true;
838+
} catch (e) {
839+
attempts++;
840+
if (attempts >= config.maxAttempts) {
841+
return false;
842+
}
843+
await new Promise((resolve) => setTimeout(resolve, delay));
844+
delay = Math.min(delay * config.backoffMultiplier, config.maxDelayMs);
845+
}
846+
}
847+
848+
return false;
849+
}
850+
810851
function createFileWatcher() {
811-
return daemonClient.registerFileWatcher(
812-
{
813-
watchProjects: 'all',
814-
includeGlobalWorkspaceFiles: true,
815-
allowPartialGraph: true,
816-
},
817-
debounce(async (error, changes) => {
818-
if (error === 'closed') {
819-
output.error({ title: `Watch error: Daemon closed the connection` });
852+
let unregisterCurrentWatcher: (() => void) | undefined;
853+
854+
const watcherCallback = debounce(async (error, changes) => {
855+
if (error === 'closed') {
856+
output.note({
857+
title: `Daemon connection closed. Attempting to reconnect...`,
858+
});
859+
860+
// Try to reconnect with exponential backoff
861+
const reconnected = await waitForWatcherReconnectWithExponentialBackoff();
862+
863+
if (reconnected) {
864+
output.note({
865+
title: 'Reconnected to daemon. Resuming file watching.',
866+
});
867+
// Re-register the watcher with the new daemon connection
868+
unregisterCurrentWatcher = await daemonClient.registerFileWatcher(
869+
{
870+
watchProjects: 'all',
871+
includeGlobalWorkspaceFiles: true,
872+
allowPartialGraph: true,
873+
},
874+
watcherCallback
875+
);
876+
} else {
877+
output.error({
878+
title: `Failed to reconnect to daemon. Stopping file watcher.`,
879+
});
820880
process.exit(1);
821-
} else if (error) {
822-
output.error({ title: `Watch error: ${error?.message ?? 'Unknown'}` });
823-
} else if (changes !== null && changes.changedFiles.length > 0) {
824-
output.note({ title: 'Recalculating project graph...' });
825-
826-
const { projectGraphClientResponse, sourceMapResponse } =
827-
await createProjectGraphAndSourceMapClientResponse();
828-
829-
if (
830-
projectGraphClientResponse.hash !==
831-
currentProjectGraphClientResponse.hash &&
832-
sourceMapResponse
833-
) {
834-
if (projectGraphClientResponse.errors?.length > 0) {
835-
projectGraphClientResponse.errors.forEach((e) => {
836-
output.error({
837-
title: e.message,
838-
bodyLines: [e.stack],
839-
});
840-
});
841-
output.warn({
842-
title: `${
843-
projectGraphClientResponse.errors.length > 1
844-
? `${projectGraphClientResponse.errors.length} errors`
845-
: `An error`
846-
} occured while processing the project graph. Showing partial graph.`,
881+
}
882+
} else if (error) {
883+
output.error({ title: `Watch error: ${error?.message ?? 'Unknown'}` });
884+
} else if (changes !== null && changes.changedFiles.length > 0) {
885+
output.note({ title: 'Recalculating project graph...' });
886+
887+
const { projectGraphClientResponse, sourceMapResponse } =
888+
await createProjectGraphAndSourceMapClientResponse();
889+
890+
if (
891+
projectGraphClientResponse.hash !==
892+
currentProjectGraphClientResponse.hash &&
893+
sourceMapResponse
894+
) {
895+
if (projectGraphClientResponse.errors?.length > 0) {
896+
projectGraphClientResponse.errors.forEach((e) => {
897+
output.error({
898+
title: e.message,
899+
bodyLines: [e.stack],
847900
});
848-
}
849-
output.note({ title: 'Graph changes updated.' });
901+
});
902+
output.warn({
903+
title: `${
904+
projectGraphClientResponse.errors.length > 1
905+
? `${projectGraphClientResponse.errors.length} errors`
906+
: `An error`
907+
} occured while processing the project graph. Showing partial graph.`,
908+
});
909+
}
910+
output.note({ title: 'Graph changes updated.' });
850911

851-
currentProjectGraphClientResponse = projectGraphClientResponse;
852-
currentSourceMapsClientResponse = sourceMapResponse;
853-
// Clear task graph cache when project graph changes
854-
clearTaskGraphCache();
912+
currentProjectGraphClientResponse = projectGraphClientResponse;
913+
currentSourceMapsClientResponse = sourceMapResponse;
914+
// Clear task graph cache when project graph changes
915+
clearTaskGraphCache();
916+
} else {
917+
output.note({ title: 'No graph changes found.' });
918+
}
919+
}
920+
}, 500);
921+
922+
return daemonClient
923+
.registerFileWatcher(
924+
{
925+
watchProjects: 'all',
926+
includeGlobalWorkspaceFiles: true,
927+
allowPartialGraph: true,
928+
},
929+
async (error, changes) => {
930+
if (error === 'closed') {
931+
// Store the unregister callback before attempting reconnection
932+
const previousUnregister = unregisterCurrentWatcher;
933+
await watcherCallback(error, changes);
934+
if (
935+
unregisterCurrentWatcher &&
936+
unregisterCurrentWatcher !== previousUnregister
937+
) {
938+
// Successfully reconnected with a new watcher
939+
return;
940+
}
855941
} else {
856-
output.note({ title: 'No graph changes found.' });
942+
await watcherCallback(error, changes);
857943
}
858944
}
859-
}, 500)
860-
);
945+
)
946+
.then((unregister) => {
947+
unregisterCurrentWatcher = unregister;
948+
return unregister;
949+
});
861950
}
862951

863952
async function createProjectGraphAndSourceMapClientResponse(

0 commit comments

Comments
 (0)