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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@valtown/ls-ws-server

v0.0.26

Published

Language server WebSocket server and proxy

Downloads

142

Readme

LS WebSocket Server

This is a WebSocket server for language servers that allows clients (typically code editors) to communicate to a running language server process.

It is meant to be used with your framework of choice, and provides a simple handleNewWebsocket handler for when a new connection has been upgraded and should be wired to an LSP.

Here's a simple example set up for the Deno language server:

// ls-proxy.ts

import { LSWSServer } from "vtls-server";
import { Hono } from "hono";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";

const lsWsServer = new LSWSServer({
  lsCommand: "deno", // proxy LS
  lsArgs: ["run", "-A", "./ls-proxy.ts"],
  lsLogPath: Deno.makeTempDirSync({ prefix: "vtlsp-procs" }),
});

const app = new Hono()
  .get("/", zValidator("query", z.object({ session: z.string() })), (c) => {
    const { socket, response } =  Deno.upgradeWebSocket(c.req.raw);
    return lsWsServer.handleNewWebsocket(
      socket,
      c.req.valid("query").session,
    );
  })

Deno.serve({ port: 5002, hostname: "0.0.0.0" }, app.fetch);

Including a small language server "proxy" server:

// main.ts

import { LSroxy, utils } from "vtls-server";

const TEMP_DIR = await Deno.makeTempDir({ prefix: "vtlsp-proxy" });

const onExit = async () => await Deno.remove(TEMP_DIR, { recursive: true });
Deno.addSignalListener("SIGINT", onExit);
Deno.addSignalListener("SIGTERM", onExit);

const proxy = new LSProxy({
  name: "lsp-server",
  cwd: TEMP_DIR, // Where the LS gets spawned from
  exec: {
    command: "deno", // real LS (we're using deno to run, and proxy, the deno language server)
    args: ["lsp", "-q"],
  },
  // Also, you can use procToClientMiddlewares, procToClientHandlers, and clientToProcHandlers
  clientToProcMiddlewares: {
    initialize: async (params) => {
      await Deno.writeTextFile(`${TEMP_DIR}/deno.json`, JSON.stringify({})); // Create a deno.json in the temp dir
      return params;
    },
    "textDocument/publishDiagnostics": async (params) => { // Params are automatically typed! All "official" LSP methods have strong types
      if (params.uri.endsWith(".test.ts")) {
        return {
          ls_proxy_code: "cancel_response" // A "magic" code that makes it so that the message is NOT passed on to the LS
        }
      }
  }
  },
  uriConverters: {
    fromProcUri: (uriString: string) => {
      // Takes URIs like /tmp/fije3189rj/buzz/foo.ts and makes it /buzz/foo.ts
      return utils.tempDirUriToVirtualUri(uriString, TEMP_DIR);
    },
    toProcUri: (uriString: string) => {
      // Takes URIs like /bar/foo.ts and makes it /tmp/fije3189rj/foo.ts
      return utils.virtualUriToTempDirUri(uriString, TEMP_DIR)!;
    },
  },
});

proxy.listen(); // Listens by default on standard in / out, and acts as a real LS

We're using Deno, but you could just as well write this in Node. To run it, you'd use a command like:

deno run -A main.ts

Or if you want the server to crash if a language server process has a "bad exit" (crash),

EXIT_ON_LS_BAD_EXIT=1 deno run -A main.ts

Routing to LS processes

Every connection to our WebSocket language server requires a ?session={}. The session IDs are unique identifiers for a language server process; if you connect to the same session in multiple places you will be "speaking" to the same language server process. As a result of this design, the WebSocket server allows multicasting language server connections. Many clients (for example, tabs) can connect to the same language server process, and when they make requests to the language server (like go to definition), only the requesting connection receives a response for their requests.

There are some additional subtileies here that you may need to think about if you're designing a language server with multiple clients. Some language servers, like the Deno language server, may crash or exhibit weird behavior if clients try to initialize and they are already initialized. Additionally, during the LSP handshake, clients learn information about supported capabilities of the language server. One easy solution to this is to use an LS proxy to "cache" the initialize handshake, so that clients that come in the future will not send additional initialize requests to the language server.

LS Proxying Server

This library exposes a language server proxy builder, which makes it really easy to automatically transform requests going to or coming out from the language server. With the language server proxy, you can:

Language server processes communicate via JSON-RPC messages - either "requests" or "notifications". Usually they communicate via inbound messages on standard in and outbound messages on standard out.

Notifications are send and forget. An example of a notification we send to the language server may look like

{ "jsonrpc": "2.0",
  "method": "textDocument/didChange",
  "params": { "textDocument": { "uri": "file:///document.txt", "version": 2 }, "contentChanges": [ { "text": "Hello" } ] }
}

Requests get exactly one reply, and look like

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "textDocument/hover",
  "params": { "textDocument": { "uri": "file:///document.txt" }, "position": { "line": 0, "character": 2 } }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": { "contents": { "kind": "plaintext", "value": "Hover information" }
  }
}

With our language server proxy builder, you can

  • Intercept notifications that leave the language server, and maybe modify or cancel them, or vice versa.
  • Intercept requests that come to the language server, and maybe modify the request parameters, or the response.
  • Define custom handlers that override existing ones or implement entirely new language server methods.

And, the result is a new language server that also reads from standard in and writes to standard out, but may transform messages before they get to the process, or the client.