@lspeasy/client 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +589 -0
  3. package/dist/capability-guard.d.ts +62 -0
  4. package/dist/capability-guard.d.ts.map +1 -0
  5. package/dist/capability-guard.js +230 -0
  6. package/dist/capability-guard.js.map +1 -0
  7. package/dist/capability-proxy.d.ts +16 -0
  8. package/dist/capability-proxy.d.ts.map +1 -0
  9. package/dist/capability-proxy.js +69 -0
  10. package/dist/capability-proxy.js.map +1 -0
  11. package/dist/client.d.ts +184 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +692 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/connection/health.d.ts +14 -0
  16. package/dist/connection/health.d.ts.map +1 -0
  17. package/dist/connection/health.js +77 -0
  18. package/dist/connection/health.js.map +1 -0
  19. package/dist/connection/heartbeat.d.ts +18 -0
  20. package/dist/connection/heartbeat.d.ts.map +1 -0
  21. package/dist/connection/heartbeat.js +57 -0
  22. package/dist/connection/heartbeat.js.map +1 -0
  23. package/dist/connection/index.d.ts +5 -0
  24. package/dist/connection/index.d.ts.map +1 -0
  25. package/dist/connection/index.js +4 -0
  26. package/dist/connection/index.js.map +1 -0
  27. package/dist/connection/types.d.ts +32 -0
  28. package/dist/connection/types.d.ts.map +1 -0
  29. package/dist/connection/types.js +9 -0
  30. package/dist/connection/types.js.map +1 -0
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +8 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/notifications/index.d.ts +3 -0
  36. package/dist/notifications/index.d.ts.map +1 -0
  37. package/dist/notifications/index.js +2 -0
  38. package/dist/notifications/index.js.map +1 -0
  39. package/dist/notifications/wait.d.ts +19 -0
  40. package/dist/notifications/wait.d.ts.map +1 -0
  41. package/dist/notifications/wait.js +46 -0
  42. package/dist/notifications/wait.js.map +1 -0
  43. package/dist/progress.d.ts +54 -0
  44. package/dist/progress.d.ts.map +1 -0
  45. package/dist/progress.js +52 -0
  46. package/dist/progress.js.map +1 -0
  47. package/dist/types.d.ts +82 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +5 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/validation.d.ts +43 -0
  52. package/dist/validation.d.ts.map +1 -0
  53. package/dist/validation.js +56 -0
  54. package/dist/validation.js.map +1 -0
  55. package/package.json +58 -0
package/dist/client.js ADDED
@@ -0,0 +1,692 @@
1
+ /**
2
+ * LSP Client implementation
3
+ */
4
+ import { CancellationTokenSource, ConsoleLogger, executeMiddlewarePipeline, LogLevel } from '@lspeasy/core';
5
+ import { DisposableEventEmitter, HandlerRegistry } from '@lspeasy/core/utils';
6
+ import { PendingRequestTracker, TransportAttachment } from '@lspeasy/core/utils/internal';
7
+ import { initializeCapabilityMethods, updateCapabilityMethods } from './capability-proxy.js';
8
+ import { CapabilityGuard, ClientCapabilityGuard } from './capability-guard.js';
9
+ import { ConnectionHealthTracker, ConnectionState, HeartbeatMonitor } from './connection/index.js';
10
+ /**
11
+ * Interface for dynamically added namespace methods
12
+ */
13
+ /**
14
+ * Type alias to add capability-aware methods to LSPClient
15
+ * These methods are added at runtime via capability-proxy.ts
16
+ */
17
+ /**
18
+ * LSP Client for connecting to language servers
19
+ *
20
+ * This class dynamically extends with capability-aware methods based on server capabilities.
21
+ * Use `.expect<ServerCaps>()` after initialization to get typed access to server capabilities.
22
+ *
23
+ * @template ClientCaps - Client capabilities (defaults to ClientCapabilities)
24
+ *
25
+ * @example
26
+ * // Create a client, then narrow server capabilities after connecting
27
+ * const client = new LSPClient<MyClientCaps>();
28
+ * await client.connect(transport);
29
+ * const typed = client.expect<{ hoverProvider: true; completionProvider: {} }>();
30
+ * // typed.textDocument.hover is available (typed)
31
+ * // typed.textDocument.completion is available (typed)
32
+ */
33
+ class BaseLSPClient {
34
+ transport;
35
+ connected;
36
+ initialized;
37
+ pendingRequests;
38
+ requestHandlers;
39
+ notificationHandlers;
40
+ events;
41
+ transportAttachment;
42
+ logger;
43
+ middlewareRegistrations;
44
+ options;
45
+ capabilities;
46
+ serverCapabilities;
47
+ serverInfo;
48
+ onValidationError;
49
+ capabilityGuard;
50
+ clientCapabilityGuard;
51
+ healthTracker;
52
+ heartbeatMonitor;
53
+ notificationWaiters;
54
+ constructor(options = {}) {
55
+ this.connected = false;
56
+ this.initialized = false;
57
+ // Set up options with defaults
58
+ this.options = {
59
+ name: options.name ?? 'lspeasy-client',
60
+ version: options.version ?? '1.0.0',
61
+ logger: options.logger ?? new ConsoleLogger(options.logLevel ?? LogLevel.Info),
62
+ logLevel: options.logLevel ?? LogLevel.Info,
63
+ requestTimeout: options.requestTimeout,
64
+ strictCapabilities: options.strictCapabilities ?? false,
65
+ heartbeat: options.heartbeat
66
+ };
67
+ this.logger = this.options.logger;
68
+ this.middlewareRegistrations = options.middleware ?? [];
69
+ this.pendingRequests = new PendingRequestTracker(this.options.requestTimeout);
70
+ this.requestHandlers = new HandlerRegistry();
71
+ this.notificationHandlers = new HandlerRegistry();
72
+ this.events = new DisposableEventEmitter();
73
+ this.transportAttachment = new TransportAttachment();
74
+ this.notificationWaiters = new Set();
75
+ this.healthTracker = new ConnectionHealthTracker();
76
+ if (options.capabilities) {
77
+ this.capabilities = options.capabilities;
78
+ }
79
+ if (options.onValidationError) {
80
+ this.onValidationError = options.onValidationError;
81
+ }
82
+ // Initialize capability guard for server-to-client handlers
83
+ this.clientCapabilityGuard = new ClientCapabilityGuard(this.capabilities ?? {}, this.logger, this.options.strictCapabilities);
84
+ // Initialize capability-aware methods on the client object
85
+ // These will be empty initially and populated after server capabilities are received
86
+ initializeCapabilityMethods(this);
87
+ }
88
+ /**
89
+ * Connect to server and complete initialization
90
+ */
91
+ async connect(transport) {
92
+ if (this.connected) {
93
+ throw new Error('Client is already connected');
94
+ }
95
+ this.healthTracker.setState(ConnectionState.Connecting);
96
+ this.transport = transport;
97
+ this.connected = true;
98
+ this.transportAttachment.attach(transport, {
99
+ onMessage: (message) => this.handleMessage(message),
100
+ onError: (error) => this.handleError(error),
101
+ onClose: () => this.handleClose()
102
+ });
103
+ try {
104
+ // Send initialize request
105
+ this.logger.debug('Sending initialize request');
106
+ const initializeParams = {
107
+ processId: process.pid,
108
+ clientInfo: {
109
+ name: this.options.name,
110
+ version: this.options.version
111
+ },
112
+ capabilities: this.capabilities ?? {},
113
+ rootUri: null
114
+ };
115
+ const result = await this.sendRequest('initialize', initializeParams);
116
+ this.serverCapabilities = result.capabilities;
117
+ if (result.serverInfo) {
118
+ this.serverInfo = result.serverInfo;
119
+ }
120
+ this.initialized = true;
121
+ // Create capability guard to validate outgoing requests
122
+ this.capabilityGuard = new CapabilityGuard(result.capabilities, this.logger, this.options.strictCapabilities);
123
+ // Update capability-aware methods based on server capabilities
124
+ updateCapabilityMethods(this);
125
+ // Send initialized notification
126
+ await this.sendNotification('initialized', {});
127
+ this.healthTracker.setState(ConnectionState.Connected);
128
+ this.startHeartbeatIfConfigured(this.options.heartbeat);
129
+ this.logger.info('Client initialized', {
130
+ serverName: this.serverInfo?.name,
131
+ serverVersion: this.serverInfo?.version
132
+ });
133
+ this.events.emit('connected');
134
+ return result;
135
+ }
136
+ catch (error) {
137
+ // Ensure we clean up state and transport on initialization failure
138
+ try {
139
+ if (this.transport) {
140
+ await this.transport.close();
141
+ }
142
+ }
143
+ catch (closeError) {
144
+ this.logger.error('Error closing transport after failed initialize', closeError);
145
+ }
146
+ // Reset client state and detach listeners via common close handler
147
+ this.handleClose();
148
+ throw error;
149
+ }
150
+ }
151
+ /**
152
+ * Disconnect from server
153
+ */
154
+ async disconnect() {
155
+ if (!this.connected) {
156
+ return;
157
+ }
158
+ this.healthTracker.setState(ConnectionState.Disconnecting);
159
+ try {
160
+ // Send shutdown request
161
+ if (this.initialized) {
162
+ this.logger.debug('Sending shutdown request');
163
+ await this.sendRequest('shutdown');
164
+ // Send exit notification
165
+ this.logger.debug('Sending exit notification');
166
+ await this.sendNotification('exit');
167
+ }
168
+ }
169
+ catch (error) {
170
+ this.logger.error('Error during shutdown', error);
171
+ }
172
+ finally {
173
+ // Close transport
174
+ if (this.transport) {
175
+ await this.transport.close();
176
+ }
177
+ this.handleClose();
178
+ }
179
+ }
180
+ /**
181
+ * Check if client is connected
182
+ */
183
+ isConnected() {
184
+ return this.connected && this.initialized;
185
+ }
186
+ /**
187
+ * Send a request to the server
188
+ */
189
+ async sendRequest(method, params, token) {
190
+ if (!this.connected || !this.transport) {
191
+ throw new Error('Client is not connected');
192
+ }
193
+ // Validate request against server capabilities
194
+ if (this.capabilityGuard && !this.capabilityGuard.canSendRequest(method)) {
195
+ this.logger.warn(`Server does not support request method ${method}`);
196
+ }
197
+ // Check if already cancelled before doing anything
198
+ if (token?.isCancellationRequested) {
199
+ return Promise.reject(new Error('Request was cancelled'));
200
+ }
201
+ const { id, promise } = this.pendingRequests.create(this.options.requestTimeout, method);
202
+ const request = {
203
+ jsonrpc: '2.0',
204
+ id,
205
+ method,
206
+ params
207
+ };
208
+ this.logger.debug(`Sending request ${method}`, { id, params });
209
+ let cancellationDisposable;
210
+ if (token) {
211
+ cancellationDisposable = token.onCancellationRequested(() => {
212
+ this.sendNotification('$/cancelRequest', { id }).catch((err) => {
213
+ this.logger.error('Failed to send cancellation', err);
214
+ });
215
+ // Use process.nextTick to defer rejection slightly
216
+ // This gives the caller a chance to attach rejection handlers before the rejection occurs
217
+ process.nextTick(() => {
218
+ this.pendingRequests.reject(id, new Error('Request was cancelled'));
219
+ });
220
+ });
221
+ }
222
+ void promise.then(() => {
223
+ cancellationDisposable?.dispose();
224
+ }, () => {
225
+ cancellationDisposable?.dispose();
226
+ });
227
+ this.sendWithMiddleware({
228
+ direction: 'clientToServer',
229
+ messageType: 'request',
230
+ method,
231
+ message: request,
232
+ onSend: async () => {
233
+ this.healthTracker.markMessageSent();
234
+ await this.transport.send(request);
235
+ },
236
+ onShortCircuit: (result) => {
237
+ this.resolveShortCircuitRequest(id, result);
238
+ },
239
+ onError: (error) => {
240
+ this.pendingRequests.reject(id, error);
241
+ }
242
+ });
243
+ return promise;
244
+ }
245
+ /**
246
+ * Send a notification to the server
247
+ */
248
+ async sendNotification(method, params) {
249
+ if (!this.connected || !this.transport) {
250
+ throw new Error('Client is not connected');
251
+ }
252
+ // Validate notification against server capabilities
253
+ if (this.capabilityGuard && !this.capabilityGuard.canSendNotification(method)) {
254
+ this.logger.warn(`Server does not support notification method ${method}`);
255
+ }
256
+ const notification = {
257
+ jsonrpc: '2.0',
258
+ method,
259
+ params
260
+ };
261
+ this.logger.debug(`Sending notification ${method}`, { params });
262
+ await this.sendWithMiddleware({
263
+ direction: 'clientToServer',
264
+ messageType: 'notification',
265
+ method,
266
+ message: notification,
267
+ onSend: async () => {
268
+ this.healthTracker.markMessageSent();
269
+ await this.transport.send(notification);
270
+ }
271
+ });
272
+ }
273
+ /**
274
+ * Send a cancellable request
275
+ */
276
+ sendCancellableRequest(method, params) {
277
+ const cancelSource = new CancellationTokenSource();
278
+ const promise = this.sendRequest(method, params, cancelSource.token);
279
+ return {
280
+ promise,
281
+ cancel: () => cancelSource.cancel()
282
+ };
283
+ }
284
+ /**
285
+ * Register handler for server-to-client requests
286
+ */
287
+ onRequest(method, handler) {
288
+ if (this.clientCapabilityGuard && !this.clientCapabilityGuard.canRegisterHandler(method)) {
289
+ this.logger.warn(`Client capability not declared for handler ${method}`);
290
+ }
291
+ return this.requestHandlers.register(method, handler);
292
+ }
293
+ /**
294
+ * Register handler for server notifications
295
+ */
296
+ onNotification(method, handler) {
297
+ if (this.clientCapabilityGuard && !this.clientCapabilityGuard.canRegisterHandler(method)) {
298
+ this.logger.warn(`Client capability not declared for handler ${method}`);
299
+ }
300
+ return this.notificationHandlers.register(method, handler);
301
+ }
302
+ /**
303
+ * Wait for the next matching server notification.
304
+ */
305
+ waitForNotification(method, options) {
306
+ return new Promise((resolve, reject) => {
307
+ let timeoutHandle;
308
+ const waiter = {
309
+ method,
310
+ resolve: (params) => {
311
+ resolve(params);
312
+ },
313
+ reject: (error) => {
314
+ reject(error);
315
+ },
316
+ cleanup: () => {
317
+ if (timeoutHandle) {
318
+ clearTimeout(timeoutHandle);
319
+ timeoutHandle = undefined;
320
+ }
321
+ this.notificationWaiters.delete(waiter);
322
+ }
323
+ };
324
+ if (options.filter) {
325
+ waiter.filter = options.filter;
326
+ }
327
+ this.notificationWaiters.add(waiter);
328
+ timeoutHandle = setTimeout(() => {
329
+ waiter.cleanup();
330
+ reject(new Error(`Timed out waiting for notification '${method}' after ${options.timeout}ms`));
331
+ }, options.timeout);
332
+ });
333
+ }
334
+ /**
335
+ * Subscribe to connection events
336
+ */
337
+ onConnected(handler) {
338
+ return this.events.on('connected', handler);
339
+ }
340
+ /**
341
+ * Subscribe to disconnection events
342
+ */
343
+ onDisconnected(handler) {
344
+ return this.events.on('disconnected', handler);
345
+ }
346
+ /**
347
+ * Subscribe to error events
348
+ */
349
+ onError(handler) {
350
+ return this.events.on('error', handler);
351
+ }
352
+ onConnectionStateChange(handler) {
353
+ const dispose = this.healthTracker.onStateChange(handler);
354
+ return { dispose };
355
+ }
356
+ onConnectionHealthChange(handler) {
357
+ const dispose = this.healthTracker.onHealthChange(handler);
358
+ return { dispose };
359
+ }
360
+ getConnectionHealth() {
361
+ return this.healthTracker.getHealth();
362
+ }
363
+ /**
364
+ * Get server capabilities
365
+ */
366
+ getServerCapabilities() {
367
+ return this.serverCapabilities;
368
+ }
369
+ /**
370
+ * Get server info
371
+ */
372
+ getServerInfo() {
373
+ return this.serverInfo;
374
+ }
375
+ /**
376
+ * Set client capabilities
377
+ */
378
+ setCapabilities(capabilities) {
379
+ this.capabilities = capabilities;
380
+ this.clientCapabilityGuard = new ClientCapabilityGuard(capabilities, this.logger, this.options.strictCapabilities);
381
+ // Note: Client capabilities are sent during initialize, so this only affects
382
+ // the local reference. To update server-side, would need client/registerCapability request.
383
+ }
384
+ /**
385
+ * Get client capabilities
386
+ */
387
+ getClientCapabilities() {
388
+ return this.capabilities;
389
+ }
390
+ /**
391
+ * Register a single client capability, returning a new typed reference via intersection.
392
+ * The returned reference is the same instance, with a narrowed type that includes
393
+ * the newly registered capability.
394
+ *
395
+ * Note: This updates the local capability reference. To notify the server of capability
396
+ * changes, the LSP 3.17 client/registerCapability request should be used separately.
397
+ *
398
+ * @template K - The capability key to register
399
+ * @param key - The client capability key
400
+ * @param value - The capability value
401
+ * @returns The same client instance with an expanded capability type
402
+ *
403
+ * @example
404
+ * const client = new LSPClient();
405
+ * const withWorkspace = client.registerCapability('workspace', { workspaceFolders: true });
406
+ * // withWorkspace is typed as LSPClient<ClientCaps & Pick<ClientCapabilities, 'workspace'>>
407
+ */
408
+ registerCapability(key, value) {
409
+ const current = this.capabilities ?? {};
410
+ const updated = { ...current, [key]: value };
411
+ this.setCapabilities(updated);
412
+ return this;
413
+ }
414
+ /**
415
+ * Zero-cost type narrowing for server capabilities.
416
+ * Returns `this` cast to include capability-aware methods for the given ServerCaps.
417
+ *
418
+ * @template S - The expected server capabilities shape
419
+ * @returns The same client instance, typed with capability-aware methods
420
+ *
421
+ * @example
422
+ * const client = new LSPClient<MyClientCaps>();
423
+ * await client.connect(transport);
424
+ * const typed = client.expect<{ hoverProvider: true }>();
425
+ * const result = await typed.textDocument.hover(params);
426
+ */
427
+ expect() {
428
+ return this;
429
+ }
430
+ /**
431
+ * Handle incoming message from transport
432
+ */
433
+ handleMessage(message) {
434
+ this.healthTracker.markMessageReceived();
435
+ this.heartbeatMonitor?.markPong();
436
+ if (this.heartbeatMonitor) {
437
+ this.healthTracker.setHeartbeat(this.heartbeatMonitor.getStatus());
438
+ }
439
+ // Response message
440
+ if ('id' in message && ('result' in message || 'error' in message)) {
441
+ void this.handleResponse(message).catch((error) => {
442
+ this.logger.error('Error handling response', error);
443
+ });
444
+ return;
445
+ }
446
+ // Request message
447
+ if ('id' in message && 'method' in message) {
448
+ // Fire and forget with error handling
449
+ void this.handleRequest(message).catch((error) => {
450
+ this.logger.error('Error handling server request', error);
451
+ });
452
+ return;
453
+ }
454
+ // Notification message
455
+ if ('method' in message && !('id' in message)) {
456
+ this.handleNotification(message);
457
+ return;
458
+ }
459
+ this.logger.warn('Received unknown message type', message);
460
+ }
461
+ /**
462
+ * Handle response message
463
+ */
464
+ async handleResponse(response) {
465
+ const { id } = response;
466
+ const method = this.pendingRequests.getMetadata(String(id));
467
+ if (!method) {
468
+ this.logger.warn(`Received response for unknown request ${id}`);
469
+ return;
470
+ }
471
+ await this.sendWithMiddleware({
472
+ direction: 'serverToClient',
473
+ messageType: 'error' in response ? 'error' : 'response',
474
+ method,
475
+ message: response,
476
+ onSend: async () => {
477
+ if ('error' in response && response.error) {
478
+ this.logger.error(`Request ${method} failed`, response.error);
479
+ this.pendingRequests.reject(String(id), new Error(`${response.error.message} (code: ${response.error.code})`));
480
+ return;
481
+ }
482
+ if ('result' in response) {
483
+ this.logger.debug(`Request ${method} succeeded`, { id });
484
+ this.pendingRequests.resolve(String(id), response.result);
485
+ return;
486
+ }
487
+ this.pendingRequests.reject(String(id), new Error('Invalid response message'));
488
+ },
489
+ onShortCircuit: (result) => {
490
+ this.resolveShortCircuitRequest(String(id), result);
491
+ },
492
+ onError: (error) => {
493
+ this.pendingRequests.reject(String(id), error);
494
+ }
495
+ });
496
+ }
497
+ resolveShortCircuitRequest(id, result) {
498
+ if (result.error) {
499
+ this.pendingRequests.reject(id, new Error(`${result.error.error.message} (code: ${result.error.error.code})`));
500
+ return;
501
+ }
502
+ if (result.response && 'error' in result.response && result.response.error) {
503
+ this.pendingRequests.reject(id, new Error(`${result.response.error.message} (code: ${result.response.error.code})`));
504
+ return;
505
+ }
506
+ if (result.response && 'result' in result.response) {
507
+ this.pendingRequests.resolve(id, result.response.result);
508
+ return;
509
+ }
510
+ this.pendingRequests.reject(id, new Error('Middleware short-circuit missing response payload'));
511
+ }
512
+ buildMiddlewareContext(direction, messageType, method, message) {
513
+ return {
514
+ direction,
515
+ messageType,
516
+ method,
517
+ message,
518
+ metadata: {},
519
+ transport: this.transport?.constructor.name ?? 'unknown'
520
+ };
521
+ }
522
+ async sendWithMiddleware(options) {
523
+ const context = this.buildMiddlewareContext(options.direction, options.messageType, options.method, options.message);
524
+ try {
525
+ const result = await executeMiddlewarePipeline(this.middlewareRegistrations, context, async () => {
526
+ await options.onSend();
527
+ return undefined;
528
+ });
529
+ if (result?.shortCircuit && options.onShortCircuit) {
530
+ options.onShortCircuit(result);
531
+ }
532
+ }
533
+ catch (error) {
534
+ const normalized = error instanceof Error ? error : new Error(String(error));
535
+ this.handleError(normalized);
536
+ if (options.onError) {
537
+ options.onError(normalized);
538
+ return;
539
+ }
540
+ throw normalized;
541
+ }
542
+ }
543
+ /**
544
+ * Handle request message from server
545
+ */
546
+ async handleRequest(request) {
547
+ const { id, method, params } = request;
548
+ const handler = this.requestHandlers.get(method);
549
+ if (!handler) {
550
+ this.logger.warn(`No handler for server request ${method}`);
551
+ // Send method not found error
552
+ const response = {
553
+ jsonrpc: '2.0',
554
+ id,
555
+ error: {
556
+ code: -32601,
557
+ message: `Method not found: ${method}`
558
+ }
559
+ };
560
+ try {
561
+ if (this.transport) {
562
+ await this.transport.send(response);
563
+ }
564
+ }
565
+ catch (error) {
566
+ this.logger.error('Failed to send error response', error);
567
+ }
568
+ return;
569
+ }
570
+ try {
571
+ const result = await handler(params);
572
+ const response = {
573
+ jsonrpc: '2.0',
574
+ id,
575
+ result
576
+ };
577
+ try {
578
+ if (this.transport) {
579
+ await this.transport.send(response);
580
+ }
581
+ }
582
+ catch (error) {
583
+ this.logger.error('Failed to send success response', error);
584
+ }
585
+ }
586
+ catch (error) {
587
+ this.logger.error(`Handler for ${method} threw error`, error);
588
+ const response = {
589
+ jsonrpc: '2.0',
590
+ id,
591
+ error: {
592
+ code: -32603,
593
+ message: error instanceof Error ? error.message : 'Internal error'
594
+ }
595
+ };
596
+ try {
597
+ if (this.transport) {
598
+ await this.transport.send(response);
599
+ }
600
+ }
601
+ catch (sendError) {
602
+ this.logger.error('Failed to send error response', sendError);
603
+ }
604
+ }
605
+ }
606
+ /**
607
+ * Handle notification message from server
608
+ */
609
+ handleNotification(notification) {
610
+ const { method, params } = notification;
611
+ for (const waiter of Array.from(this.notificationWaiters)) {
612
+ if (waiter.method !== method) {
613
+ continue;
614
+ }
615
+ try {
616
+ if (waiter.filter && !waiter.filter(params)) {
617
+ continue;
618
+ }
619
+ waiter.cleanup();
620
+ waiter.resolve(params);
621
+ }
622
+ catch (error) {
623
+ waiter.cleanup();
624
+ const normalized = error instanceof Error ? error : new Error(String(error));
625
+ waiter.reject(normalized);
626
+ }
627
+ }
628
+ const handler = this.notificationHandlers.get(method);
629
+ if (!handler) {
630
+ this.logger.debug(`No handler for server notification ${method}`);
631
+ return;
632
+ }
633
+ try {
634
+ handler(params);
635
+ }
636
+ catch (error) {
637
+ this.logger.error(`Handler for ${method} threw error`, error);
638
+ }
639
+ }
640
+ /**
641
+ * Handle transport error
642
+ */
643
+ handleError(error) {
644
+ this.logger.error('Transport error', error);
645
+ this.events.emit('error', error);
646
+ }
647
+ /**
648
+ * Handle connection close
649
+ */
650
+ handleClose() {
651
+ if (!this.connected) {
652
+ return;
653
+ }
654
+ this.logger.info('Connection closed');
655
+ this.connected = false;
656
+ this.initialized = false;
657
+ delete this.transport;
658
+ this.heartbeatMonitor?.stop();
659
+ this.heartbeatMonitor = undefined;
660
+ this.healthTracker.setState(ConnectionState.Disconnected);
661
+ this.transportAttachment.detach();
662
+ this.pendingRequests.clear(new Error('Connection closed'));
663
+ for (const waiter of Array.from(this.notificationWaiters)) {
664
+ waiter.cleanup();
665
+ waiter.reject(new Error('Connection closed before notification was received'));
666
+ }
667
+ this.events.emit('disconnected');
668
+ }
669
+ startHeartbeatIfConfigured(config) {
670
+ if (!config || !config.enabled) {
671
+ return;
672
+ }
673
+ this.heartbeatMonitor?.stop();
674
+ this.heartbeatMonitor = new HeartbeatMonitor({
675
+ config,
676
+ onPing: () => {
677
+ this.healthTracker.setHeartbeat(this.heartbeatMonitor.getStatus());
678
+ },
679
+ onUnresponsive: () => {
680
+ this.healthTracker.setHeartbeat(this.heartbeatMonitor.getStatus());
681
+ },
682
+ onResponsive: () => {
683
+ this.healthTracker.setHeartbeat(this.heartbeatMonitor.getStatus());
684
+ }
685
+ });
686
+ this.healthTracker.setHeartbeat(this.heartbeatMonitor.getStatus());
687
+ this.heartbeatMonitor.start();
688
+ }
689
+ }
690
+ // Generic constructor that preserves type parameters
691
+ export const LSPClient = BaseLSPClient;
692
+ //# sourceMappingURL=client.js.map