Skip to content

Commit ef64052

Browse files
committed
emit event when watcher gives up
1 parent 23868a7 commit ef64052

File tree

16 files changed

+285
-67
lines changed

16 files changed

+285
-67
lines changed

apps/nxls/src/main.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getDocumentLinks } from '@nx-console/language-server-capabilities-docum
1010
import { getHover } from '@nx-console/language-server-capabilities-hover';
1111
import {
1212
NxChangeWorkspace,
13+
NxWatcherOperationalNotification,
1314
NxWorkspacePathRequest,
1415
NxWorkspaceRefreshNotification,
1516
NxWorkspaceRefreshStartedNotification,
@@ -40,7 +41,10 @@ import {
4041
killGroup,
4142
loadRootEnvFiles,
4243
} from '@nx-console/shared-utils';
43-
import { NativeWatcher } from '@nx-console/shared-watcher';
44+
import {
45+
DaemonWatcherCallback,
46+
NativeWatcher,
47+
} from '@nx-console/shared-watcher';
4448
import type { ProjectGraph } from 'nx/src/devkit-exports';
4549
import type { ConfigurationSourceMaps } from 'nx/src/project-graph/utils/project-configuration-utils';
4650
import { ClientCapabilities, TextDocument } from 'vscode-json-languageservice';
@@ -72,6 +76,24 @@ let CLIENT_CAPABILITIES: ClientCapabilities | undefined = undefined;
7276
let unregisterFileWatcher: () => Promise<void> = async () => {
7377
//noop
7478
};
79+
const fileWatcherCallback: DaemonWatcherCallback = async (
80+
_,
81+
projectGraphAndSourceMaps,
82+
) => {
83+
if (!WORKING_PATH) {
84+
return;
85+
}
86+
await reconfigureAndSendNotificationWithBackoff(
87+
WORKING_PATH,
88+
projectGraphAndSourceMaps,
89+
);
90+
};
91+
const fileWatcherOperationalCallback = (isOperational: boolean) => {
92+
lspLogger.log(`File watcher is operational: ${isOperational}`);
93+
connection.sendNotification(NxWatcherOperationalNotification.method, {
94+
isOperational,
95+
});
96+
};
7597
let reconfigureAttempts = 0;
7698

7799
const connection = createConnection(ProposedFeatures.all);
@@ -107,19 +129,8 @@ connection.onInitialize(async (params) => {
107129

108130
unregisterFileWatcher = await languageServerWatcher(
109131
WORKING_PATH,
110-
async (error, projectGraphAndSourceMaps) => {
111-
if (!WORKING_PATH) {
112-
return;
113-
}
114-
if (error) {
115-
lspLogger.log(error.toString());
116-
} else {
117-
await reconfigureAndSendNotificationWithBackoff(
118-
WORKING_PATH,
119-
projectGraphAndSourceMaps,
120-
);
121-
}
122-
},
132+
fileWatcherCallback,
133+
fileWatcherOperationalCallback,
123134
);
124135
} catch (e) {
125136
lspLogger.log('Unable to get Nx info: ' + e.toString());
@@ -417,9 +428,11 @@ async function reconfigure(
417428

418429
unregisterFileWatcher?.();
419430

420-
unregisterFileWatcher = await languageServerWatcher(workingPath, async () => {
421-
reconfigureAndSendNotificationWithBackoff(workingPath);
422-
});
431+
unregisterFileWatcher = await languageServerWatcher(
432+
workingPath,
433+
fileWatcherCallback,
434+
fileWatcherOperationalCallback,
435+
);
423436

424437
return workspace;
425438
}

libs/language-server/types/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export const NxWorkspaceRefreshNotification: NotificationType<void> =
3434
export const NxWorkspaceRefreshStartedNotification: NotificationType<void> =
3535
new NotificationType('nx/refreshWorkspaceStarted');
3636

37+
export const NxWatcherOperationalNotification: NotificationType<{
38+
isOperational: boolean;
39+
}> = new NotificationType('nx/fileWatcherOperational');
40+
3741
export const NxStopDaemonRequest: RequestType<undefined, undefined, unknown> =
3842
new RequestType('nx/stopDaemon');
3943

libs/language-server/watcher/src/lib/watcher.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,36 @@ export async function cleanupAllWatchers(): Promise<void> {
5858
export async function languageServerWatcher(
5959
workspacePath: string,
6060
callback: DaemonWatcherCallback,
61+
watcherOperationalCallback?: (isOperational: boolean) => void,
6162
): Promise<() => Promise<void>> {
6263
const nxVersion = await getNxVersion(workspacePath);
6364
if (!nxVersion || !gte(nxVersion, '16.4.0')) {
6465
lspLogger.log(
6566
'File watching is not supported for Nx versions below 16.4.0.',
6667
);
68+
watcherOperationalCallback?.(false);
6769
return async () => {
6870
lspLogger.log('unregistering empty watcher');
6971
};
7072
}
7173

7274
if (gte(nxVersion, '22.0.0')) {
73-
return registerPassiveDaemonWatcher(workspacePath, callback);
75+
return registerPassiveDaemonWatcher(
76+
workspacePath,
77+
callback,
78+
watcherOperationalCallback,
79+
);
7480
} else {
81+
// older versions don't have this granular watcher tracking so we just assume true
82+
watcherOperationalCallback?.(true);
7583
return registerOldWatcher(workspacePath, nxVersion, callback);
7684
}
7785
}
7886

7987
async function registerPassiveDaemonWatcher(
8088
workspacePath: string,
8189
callback: DaemonWatcherCallback,
90+
watcherOperationalCallback?: (isOperational: boolean) => void,
8291
): Promise<() => Promise<void>> {
8392
const daemonClient = await getNxDaemonClient(workspacePath, lspLogger);
8493

@@ -89,7 +98,11 @@ async function registerPassiveDaemonWatcher(
8998
};
9099
}
91100
try {
92-
_passiveDaemonWatcher = new PassiveDaemonWatcher(workspacePath, lspLogger);
101+
_passiveDaemonWatcher = new PassiveDaemonWatcher(
102+
workspacePath,
103+
lspLogger,
104+
watcherOperationalCallback,
105+
);
93106
await _passiveDaemonWatcher.start();
94107
_passiveDaemonWatcher.listen((error, projectGraphAndSourceMaps) => {
95108
callback(error, projectGraphAndSourceMaps);

libs/shared/nx-workspace-info/src/lib/workspace.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,8 @@ async function _workspace(
9595

9696
const isLerna = await fileExists(join(workspacePath, 'lerna.json'));
9797

98-
const isAvailable =
99-
await daemonClientModule?.daemonClient.isServerAvailable();
100-
const isRunning = execSync(`npx nx daemon`);
101-
102-
logger.log(`isAvailable: ${isAvailable}, isRunning: ${isRunning}`);
10398
return {
10499
daemonEnabled: daemonClientModule?.isDaemonEnabled() ?? false,
105-
daemonRunning:
106-
(await daemonClientModule?.daemonClient.isServerAvailable()) ?? false,
107100
projectGraph: projectGraph ?? {
108101
nodes: {},
109102
dependencies: {},
@@ -129,7 +122,6 @@ async function _workspace(
129122
// Default to nx workspace
130123
return {
131124
daemonEnabled: false,
132-
daemonRunning: false,
133125
validWorkspaceJson: false,
134126
projectGraph: {
135127
nodes: {},

libs/shared/types/src/lib/nx-workspace.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export type NxProjectConfiguration = ProjectConfiguration & {
1313

1414
export interface NxWorkspace {
1515
daemonEnabled: boolean;
16-
daemonRunning: boolean;
1716
validWorkspaceJson: boolean;
1817
nxJson: NxJsonConfiguration;
1918
projectGraph: ProjectGraph;

libs/shared/watcher/src/lib/passive-daemon-watcher.spec.ts

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ describe('PassiveDaemonWatcher', () => {
290290

291291
describe('Exponential Backoff', () => {
292292
it(
293-
'should use exponential backoff for retries (2s, 5s, 10s, 20s, 40s)',
293+
'should use exponential backoff for retries (2s, 5s, 10s, 20s)',
294294
async () => {
295295
(getNxDaemonClient as jest.Mock).mockRejectedValue(
296296
new Error('Always fails'),
@@ -314,16 +314,13 @@ describe('PassiveDaemonWatcher', () => {
314314
await new Promise((resolve) => setTimeout(resolve, 20500));
315315
expect(getNxDaemonClient).toHaveBeenCalledTimes(5);
316316

317-
await new Promise((resolve) => setTimeout(resolve, 40500));
318-
expect(getNxDaemonClient).toHaveBeenCalledTimes(6);
319-
320317
watcher.dispose();
321318
},
322-
90000,
319+
50000,
323320
);
324321

325322
it(
326-
'should stop retrying after 5 failed attempts',
323+
'should stop retrying after 4 failed attempts',
327324
async () => {
328325
(getNxDaemonClient as jest.Mock).mockRejectedValue(
329326
new Error('Always fails'),
@@ -334,17 +331,17 @@ describe('PassiveDaemonWatcher', () => {
334331

335332
await flushPromises();
336333

337-
await new Promise((resolve) => setTimeout(resolve, 80000));
334+
await new Promise((resolve) => setTimeout(resolve, 40000));
338335

339-
expect(getNxDaemonClient).toHaveBeenCalledTimes(6);
336+
expect(getNxDaemonClient).toHaveBeenCalledTimes(5);
340337

341338
await new Promise((resolve) => setTimeout(resolve, 5000));
342339

343-
expect(getNxDaemonClient).toHaveBeenCalledTimes(6);
340+
expect(getNxDaemonClient).toHaveBeenCalledTimes(5);
344341

345342
watcher.dispose();
346343
},
347-
90000,
344+
50000,
348345
);
349346
});
350347

@@ -400,6 +397,114 @@ describe('PassiveDaemonWatcher', () => {
400397
expect(listener).not.toHaveBeenCalled();
401398
});
402399
});
400+
401+
describe('Operational State Callback', () => {
402+
it('should call callback with true when starting', async () => {
403+
const onOperationalStateChange = jest.fn();
404+
const watcher = new PassiveDaemonWatcher(
405+
'/workspace',
406+
mockLogger,
407+
onOperationalStateChange,
408+
);
409+
410+
watcher.start();
411+
await flushPromises();
412+
413+
expect(onOperationalStateChange).toHaveBeenCalledWith(true);
414+
415+
watcher.dispose();
416+
});
417+
418+
it('should call callback with true when listening', async () => {
419+
const onOperationalStateChange = jest.fn();
420+
const watcher = new PassiveDaemonWatcher(
421+
'/workspace',
422+
mockLogger,
423+
onOperationalStateChange,
424+
);
425+
426+
watcher.start();
427+
await flushPromises();
428+
429+
expect(onOperationalStateChange).toHaveBeenCalledWith(true);
430+
431+
watcher.dispose();
432+
});
433+
434+
it('should call callback with true when failed but can retry', async () => {
435+
const onOperationalStateChange = jest.fn();
436+
(getNxDaemonClient as jest.Mock).mockRejectedValueOnce(
437+
new Error('Failed once'),
438+
);
439+
440+
const watcher = new PassiveDaemonWatcher(
441+
'/workspace',
442+
mockLogger,
443+
onOperationalStateChange,
444+
);
445+
446+
watcher.start();
447+
await flushPromises();
448+
449+
const trueCalls = onOperationalStateChange.mock.calls.filter(
450+
(call) => call[0] === true,
451+
);
452+
expect(trueCalls.length).toBeGreaterThan(0);
453+
454+
watcher.dispose();
455+
});
456+
457+
it(
458+
'should call callback with false when permanently failed',
459+
async () => {
460+
const onOperationalStateChange = jest.fn();
461+
(getNxDaemonClient as jest.Mock).mockRejectedValue(
462+
new Error('Always fails'),
463+
);
464+
465+
const watcher = new PassiveDaemonWatcher(
466+
'/workspace',
467+
mockLogger,
468+
onOperationalStateChange,
469+
);
470+
471+
watcher.start();
472+
await flushPromises();
473+
474+
await new Promise((resolve) => setTimeout(resolve, 45000));
475+
await flushPromises();
476+
477+
const falseCalls = onOperationalStateChange.mock.calls.filter(
478+
(call) => call[0] === false,
479+
);
480+
expect(falseCalls.length).toBeGreaterThan(0);
481+
482+
watcher.dispose();
483+
},
484+
50000,
485+
);
486+
487+
it('should call callback with true after stop', async () => {
488+
const onOperationalStateChange = jest.fn();
489+
const watcher = new PassiveDaemonWatcher(
490+
'/workspace',
491+
mockLogger,
492+
onOperationalStateChange,
493+
);
494+
495+
watcher.start();
496+
await flushPromises();
497+
498+
onOperationalStateChange.mockClear();
499+
500+
watcher.stop();
501+
await flushPromises();
502+
503+
expect(onOperationalStateChange).toHaveBeenCalledWith(true);
504+
505+
watcher.dispose();
506+
});
507+
});
403508
});
404509

405510
async function flushPromises() {

0 commit comments

Comments
 (0)