npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lspeasy/client

v3.0.2

Published

Connect to LSP servers with typed client API

Readme

@lspeasy/client

Connect to Language Server Protocol servers with a simple, type-safe client API.

Overview

@lspeasy/client provides a high-level LSP client with:

  • LSPClient: Complete LSP client implementation with lifecycle management
  • High-Level API: Strongly-typed textDocument.* and workspace.* methods
  • Request/Notification: Low-level access to send any LSP request or notification
  • Cancellation: Built-in cancellation support for long-running requests
  • Event Subscriptions: Subscribe to server notifications and events
  • Server Requests: Handle requests from server to client
  • Notification Waiting: Promise-based one-shot waiting with timeout and filters
  • Connection Health: State transition and message activity monitoring
  • Type Safety: Full TypeScript types from LSP protocol definitions

Installation

npm install @lspeasy/client @lspeasy/core vscode-languageserver-protocol
# or
pnpm add @lspeasy/client @lspeasy/core vscode-languageserver-protocol
# or
yarn add @lspeasy/client @lspeasy/core vscode-languageserver-protocol

Quick Start

Basic Client

import { LSPClient } from '@lspeasy/client';
import { StdioTransport } from '@lspeasy/core';
import { spawn } from 'child_process';

// Spawn language server
const serverProcess = spawn('typescript-language-server', ['--stdio']);

// Create transport
const transport = new StdioTransport({
  input: serverProcess.stdout,
  output: serverProcess.stdin
});

// Create client
const client = new LSPClient({
  name: 'My Client',
  version: '1.0.0',
  transport
});

// Connect to server (sends initialize + initialized)
await client.connect(transport);

// Use high-level API
const hover = await client.textDocument.hover({
  textDocument: { uri: 'file:///path/to/file.ts' },
  position: { line: 10, character: 5 }
});

console.log('Hover:', hover?.contents);

// Disconnect
await client.disconnect();

With Capabilities

Declare client capabilities:

const client = new LSPClient({
  name: 'Advanced Client',
  version: '1.0.0',
  transport,
  capabilities: {
    textDocument: {
      hover: {
        contentFormat: ['markdown', 'plaintext']
      },
      completion: {
        completionItem: {
          snippetSupport: true,
          commitCharactersSupport: true
        }
      }
    },
    workspace: {
      applyEdit: true,
      workspaceEdit: {
        documentChanges: true
      }
    }
  }
});

High-Level API

Text Document Methods

// Hover
const hover = await client.textDocument.hover({
  textDocument: { uri: 'file:///test.ts' },
  position: { line: 0, character: 0 }
});

// Completion
const completion = await client.textDocument.completion({
  textDocument: { uri: 'file:///test.ts' },
  position: { line: 5, character: 10 }
});

// Go to Definition
const definition = await client.textDocument.definition({
  textDocument: { uri: 'file:///test.ts' },
  position: { line: 10, character: 15 }
});

// Find References
const references = await client.textDocument.references({
  textDocument: { uri: 'file:///test.ts' },
  position: { line: 20, character: 5 },
  context: { includeDeclaration: false }
});

// Document Symbols
const symbols = await client.textDocument.documentSymbol({
  textDocument: { uri: 'file:///test.ts' }
});

Document Synchronization

// Open document
await client.textDocument.didOpen({
  textDocument: {
    uri: 'file:///test.ts',
    languageId: 'typescript',
    version: 1,
    text: 'console.log("Hello");'
  }
});

// Change document
await client.textDocument.didChange({
  textDocument: {
    uri: 'file:///test.ts',
    version: 2
  },
  contentChanges: [
    {
      text: 'console.log("Hello, World!");'
    }
  ]
});

// Save document
await client.textDocument.didSave({
  textDocument: { uri: 'file:///test.ts' },
  text: 'console.log("Hello, World!");'
});

// Close document
await client.textDocument.didClose({
  textDocument: { uri: 'file:///test.ts' }
});

Workspace Methods

// Workspace symbols
const symbols = await client.workspace.symbol({
  query: 'MyClass'
});

// Configuration (if server requests it)
// Server will call this via client.onRequest('workspace/configuration')

// Workspace folders
await client.workspace.didChangeWorkspaceFolders({
  event: {
    added: [{ uri: 'file:///new/folder', name: 'New Folder' }],
    removed: []
  }
});

// File watching
await client.workspace.didChangeWatchedFiles({
  changes: [
    {
      uri: 'file:///test.ts',
      type: 2 // Changed
    }
  ]
});

Low-Level API

Send Requests

// Send any request
const result = await client.sendRequest<ParamsType, ResultType>(
  'custom/method',
  { /* params */ }
);

Send Notifications

// Send any notification
await client.sendNotification<ParamsType>(
  'custom/notification',
  { /* params */ }
);

Cancellable Requests

import { CancellationTokenSource } from '@lspeasy/core';

const source = new CancellationTokenSource();

// Send cancellable request
const { promise, cancel } = client.sendRequestCancellable(
  'textDocument/hover',
  params,
  source.token
);

// Cancel after 5 seconds
setTimeout(() => {
  source.cancel();
  // or use the returned cancel function
  // cancel();
}, 5000);

try {
  const result = await promise;
} catch (error) {
  if (error.message.includes('cancelled')) {
    console.log('Request was cancelled');
  }
}

Event Subscriptions

Dynamic Capability Registration

const client = new LSPClient({
  capabilities: {
    workspace: {
      didChangeWatchedFiles: { dynamicRegistration: true }
    }
  },
  dynamicRegistration: {
    allowUndeclaredDynamicRegistration: false
  }
});

const runtime = client.getRuntimeCapabilities();
console.log(runtime.dynamicRegistrations);
  • client/registerCapability and client/unregisterCapability are handled automatically.
  • Unknown unregister ids return JSON-RPC -32602.
  • Set allowUndeclaredDynamicRegistration: true for compatibility-mode acceptance.

Partial Result Streaming

const result = await client.sendRequestWithPartialResults('workspace/symbol', { query: 'My' }, {
  token: 'symbols-1',
  onPartial: (batch) => console.log('partial batch', batch)
});

if (result.cancelled) {
  console.log(result.partialResults);
} else {
  console.log(result.finalResult);
}

Notebook Namespace

await client.notebookDocument.didOpen(params);
await client.notebookDocument.didChange(params);
await client.notebookDocument.didSave(params);
await client.notebookDocument.didClose(params);

Connection Events

// Connected to server
client.onConnected(() => {
  console.log('Connected to language server');
});

// Disconnected from server
client.onDisconnected(() => {
  console.log('Disconnected from language server');
});

// Connection errors
client.onError((error) => {
  console.error('Client error:', error);
});

waitForNotification

Use waitForNotification when you need the next matching server notification as a Promise.

const diagnostics = await client.waitForNotification('textDocument/publishDiagnostics', {
  timeout: 5000,
  filter: (params) => params.uri === 'file:///example.ts'
});

console.log(diagnostics.diagnostics);

Notes:

  • timeout is required.
  • Waiters are cleaned up automatically on resolve, timeout, or disconnect.
  • Multiple concurrent waiters for the same method are supported.

Connection Health Monitoring

const client = new LSPClient({
  name: 'health-aware-client',
  version: '1.0.0',
  heartbeat: {
    enabled: true,
    interval: 30000,
    timeout: 10000
  }
});

const stateSubscription = client.onConnectionStateChange((event) => {
  console.log('state', event.previous, '->', event.current, event.reason);
});

const healthSubscription = client.onConnectionHealthChange((health) => {
  console.log('last sent', health.lastMessageSent);
  console.log('last received', health.lastMessageReceived);
});

const health = client.getConnectionHealth();
console.log(health.state);

stateSubscription.dispose();
healthSubscription.dispose();

Server Notifications

// Diagnostics from server
client.onNotification('textDocument/publishDiagnostics', (params) => {
  console.log(`Diagnostics for ${params.uri}:`, params.diagnostics);
});

// Show message from server
client.onNotification('window/showMessage', (params) => {
  console.log(`Server message (${params.type}): ${params.message}`);
});

// Log message from server
client.onNotification('window/logMessage', (params) => {
  console.log(`Server log (${params.type}): ${params.message}`);
});

Server Requests

Handle requests from server to client:

// Configuration request
client.onRequest('workspace/configuration', async (params) => {
  return [
    { enable: true },
    { maxProblems: 100 }
  ];
});

// Apply workspace edit
client.onRequest('workspace/applyEdit', async (params) => {
  // Apply the edit
  applyWorkspaceEdit(params.edit);
  return { applied: true };
});

// Show message request (with actions)
client.onRequest('window/showMessageRequest', async (params) => {
  // Show dialog to user
  const choice = await showDialog(params.message, params.actions);
  return choice;
});

When handling server-to-client requests:

  • The handler parameter and return value are inferred from the method.
  • If no handler exists, client replies with JSON-RPC -32601 (method not found).
  • If handler throws, client replies with JSON-RPC -32603 (internal error).

WebSocket Client

import { LSPClient } from '@lspeasy/client';
import { WebSocketTransport } from '@lspeasy/core';

// Connect over WebSocket with automatic reconnection
const transport = new WebSocketTransport({
  url: 'ws://localhost:3000',
  enableReconnect: true,
  maxReconnectAttempts: 5,
  reconnectDelay: 1000,
  maxReconnectDelay: 30000,
  reconnectBackoffMultiplier: 2
});

const client = new LSPClient({
  name: 'WebSocket Client',
  version: '1.0.0',
  transport
});

// Handle reconnection
transport.onClose(() => {
  console.log('Connection lost, attempting to reconnect...');
});

// Connect
await client.connect();

Document Tracking

Implement a simple document tracker:

class DocumentTracker {
  private documents = new Map<string, { version: number; content: string }>();

  async open(client: LSPClient, uri: string, languageId: string, content: string): Promise<void> {
    this.documents.set(uri, { version: 1, content });

    await client.textDocument.didOpen({
      textDocument: {
        uri,
        languageId,
        version: 1,
        text: content
      }
    });
  }

  async change(client: LSPClient, uri: string, newContent: string): Promise<void> {
    const doc = this.documents.get(uri);
    if (!doc) return;

    const newVersion = doc.version + 1;
    this.documents.set(uri, { version: newVersion, content: newContent });

    await client.textDocument.didChange({
      textDocument: { uri, version: newVersion },
      contentChanges: [{ text: newContent }]
    });
  }

  async close(client: LSPClient, uri: string): Promise<void> {
    this.documents.delete(uri);

    await client.textDocument.didClose({
      textDocument: { uri }
    });
  }

  get(uri: string): string | undefined {
    return this.documents.get(uri)?.content;
  }
}

// Usage
const tracker = new DocumentTracker();
await tracker.open(client, 'file:///test.ts', 'typescript', 'console.log();');
await tracker.change(client, 'file:///test.ts', 'console.log("Hello");');
await tracker.close(client, 'file:///test.ts');

Diagnostic Handling

const diagnostics = new Map<string, Diagnostic[]>();

client.onNotification('textDocument/publishDiagnostics', (params) => {
  diagnostics.set(params.uri, params.diagnostics);

  // Display diagnostics
  for (const diagnostic of params.diagnostics) {
    console.log(`${params.uri}:${diagnostic.range.start.line + 1}: ${diagnostic.message}`);
  }
});

// Get diagnostics for a file
function getDiagnostics(uri: string): Diagnostic[] {
  return diagnostics.get(uri) || [];
}

Testing

import { LSPClient } from '@lspeasy/client';
import { MockTransport } from '@lspeasy/core/test/utils';

describe('LSP Client', () => {
  it('should send hover request', async () => {
    const transport = new MockTransport();
    const client = new LSPClient({
      name: 'Test Client',
      version: '1.0.0',
      transport
    });

    await client.connect();

    // Send hover request
    const hoverPromise = client.textDocument.hover({
      textDocument: { uri: 'file:///test.ts' },
      position: { line: 0, character: 0 }
    });

    // Simulate server response
    const request = transport.sentMessages.find(m => m.method === 'textDocument/hover');
    transport.simulateMessage({
      jsonrpc: '2.0',
      id: request.id,
      result: {
        contents: 'Test hover'
      }
    });

    const hover = await hoverPromise;
    expect(hover?.contents).toBe('Test hover');
  });
});

Best Practices

Always Call connect()

Ensure the client is initialized before sending requests:

await client.connect();
// Now safe to send requests

Handle Disconnections

Subscribe to disconnection events and handle gracefully:

client.onDisconnected(() => {
  console.log('Server disconnected');
  // Attempt to reconnect or notify user
});

Use High-Level API

Prefer high-level methods over low-level sendRequest:

// Good
const hover = await client.textDocument.hover(params);

// Less type-safe
const hover = await client.sendRequest('textDocument/hover', params);

Clean Up Resources

Always disconnect when done:

try {
  await client.connect();
  // Use client
} finally {
  await client.disconnect();
}

Handle Cancellation

Use cancellation tokens for long-running operations:

const source = new CancellationTokenSource();
const { promise } = client.sendRequestCancellable(method, params, source.token);

// Cancel if needed
setTimeout(() => source.cancel(), 5000);

API Reference

See API.md for complete API documentation.

Architecture

See ARCHITECTURE.md for system architecture details.

License

MIT