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

@pilimo/velo-client

v1.0.0

Published

Browser / Node WebSocket client for the Velo binary protocol

Downloads

133

Readme

@pilimo/velo-client

A browser / Node.js WebSocket client for the Velo binary protocol.

| Feature | API | |---|---| | Handshake + session criteria | VeloClient.connect() | | Request / response | client.request() | | Fire-and-forget messages | client.sendMessage() | | Server-push messages | client.onMessage() | | Streaming (STREAM_REQUEST → STREAM_ITEM* → STREAM_FINISH) | client.stream() | | Concurrency control (peer's concurrentRequests, FIFO queuing) | built in | | Keepalive (auto PONG, idle PING / half-open detection, on by default) | options.keepalive | | Optional request timeout | options.requestTimeout, per call { timeout } | | Graceful close | client.close() |

Installation

npm install @pilimo/velo-client

| Entry | Use | |---|---| | import { VeloClient } from '@pilimo/velo-client' | bundlers, Node (ESM or CommonJS) | | import { VeloClient } from '@pilimo/velo-client/browser' | a single minified ESM file for <script type="module"> and import maps | | dist/velo-client.js | classic <script> tag; exposes the Velo global |

Quick start

import { VeloClient, ResponseCode, VeloHandshakeError } from '@pilimo/velo-client';

// The second argument is the handshake payload (auth token, user id, …):
// Uint8Array | string | any JSON-serialisable object.
const { client, handshakePayload } = await VeloClient.connect('wss://host:6672', 'my-session-token');
const profile = JSON.parse(new TextDecoder().decode(handshakePayload));

// Request / response
const res = await client.request(1_001_007);
if (res.code === ResponseCode.OK) {
  const chats = JSON.parse(new TextDecoder().decode(res.payload));
}

// Objects are JSON-serialised for you
await client.request(1_001_001, { chat_id: '…', message: 'hello!' });

// Server-push messages
const stop = client.onMessage((msg) => console.log(msg.code, msg.payload));
stop();

// Fire-and-forget
client.sendMessage(2_001_001, { event: 'typing' });

// Streaming
for await (const event of client.stream(1_002_001, { page: 0 })) {
  if (event.type === 'item')   console.log('chunk', event.payload);
  if (event.type === 'finish') console.log('done, code', event.code);
}

client.onClose((ev) => console.log('closed', ev.code, ev.reason));
client.close();

API

VeloClient.connect(url, payload?, options?) → Promise<{ client, handshakePayload }>

| Parameter | Type | Default | Description | |---|---|---|---| | url | string | — | ws:// or wss:// URL | | payload | Uint8Array \| string \| object | empty | Handshake payload | | options.criteria | Partial<SessionCriteria> | DEFAULT_CRITERIA | Criteria advertised to the server | | options.keepalive | { idleTimeout, pingGrace? } \| false | DEFAULT_KEEPALIVE (30 s / 5 s) | Idle keepalive (see below); false disables | | options.requestTimeout | number (ms) | off | Fail a request() with no RESPONSE after this long | | options.handshakeTimeout | number (ms) | unlimited | Fail connect() if the handshake takes longer | | options.socketFactory | (url) => VeloSocket | new WebSocket(url) | Supply the socket (Node ws, tests, proxies) |

Rejects with VeloHandshakeError (server said no), VeloConnectionError (socket failed, closed or timed out first) or VeloProtocolError (the first frame was not a handshake response).

client.request(code, payload?, { timeout? }?) → Promise<VeloResponse>

Sends a REQUEST and resolves with { code, payload }. Requests beyond the server's concurrentRequests wait locally, in call order. Rejects with VeloCriteriaViolation before sending when the payload exceeds the server's maxRequestPayload, and with VeloConnectionError if the socket closes first.

There is no timeout by default: keepalive already detects a dead connection and rejects everything pending, so a request only waits while the server is provably alive and still working. To bound a slow server anyway, set requestTimeout on the client or timeout on one call (false lifts a client-level limit for that call). A timed-out request rejects with VeloTimeoutError, but it still counts toward the server's concurrentRequests until the server answers, because the server counts it too; the late response frees the slot and is otherwise ignored.

client.sendMessage(code, payload?)

Sends a one-way MESSAGE. Throws VeloCriteriaViolation when the server refuses messages or the payload is too large.

client.stream(code, payload?) → AsyncGenerator<StreamEvent>

type StreamEvent =
  | { type: 'item';                 payload: Uint8Array }
  | { type: 'finish'; code: number; payload: Uint8Array };

A stream counts as one in-flight request until the server finishes it, even if you leave the loop early. Streams have no timeout; a dead connection is caught by keepalive.

client.onMessage(listener) → unsubscribe and client.onClose(listener) → unsubscribe

A listener that throws does not stop the others from being called; the error is rethrown afterwards.

client.close(code?, reason?), client.isConnected, client.localCriteria, client.remoteCriteria

Keepalive

Inbound PINGs are always answered with a PONG. The client also detects half-open connections, on by default: after idleTimeout ms (30 s) without any inbound frame it sends a PING and, if nothing arrives within pingGrace ms (5 s), closes the socket, so every pending request rejects and your onClose listener fires. Without this, a connection lost to a network switch or a sleep looks open forever and requests never come back.

const { client } = await VeloClient.connect(url, token, {
  keepalive: { idleTimeout: 60_000, pingGrace: 10_000 },   // or `false` to disable
});

Criteria enforcement

The client enforces the server's criteria before sending, and its own on what arrives. A server MESSAGE that violates the local criteria closes the socket with code 4008; a malformed or out-of-place frame closes it with 4002 (exported as CLOSE_CRITERIA_VIOLATION and CLOSE_PROTOCOL_ERROR). Text WebSocket messages and server-initiated requests are ignored.

export const DEFAULT_CRITERIA: SessionCriteria = {
  concurrentRequests: 32,
  messagePossible:    true,
  maxRequestPayload:  4 * 1024 * 1024,
  maxMessagePayload:  4 * 1024 * 1024,
};

Errors

| Class | When | |---|---| | VeloError | Base class | | VeloHandshakeError | Server returned a non-OK handshake code (responseCode, responsePayload) | | VeloConnectionError | Socket failed to open, timed out, or closed (wsCode, wsReason) | | VeloTimeoutError | No RESPONSE within a configured request timeout (code, requestId, timeout) | | VeloCriteriaViolation | A frame would break the negotiated criteria | | VeloProtocolError | Malformed or unexpected frame from the server |

Development

npm install
npm test          # vitest, no network: the socket is faked
npm run typecheck
npm run build     # dist/esm, dist/cjs, dist/types, IIFE and ESM bundles

The source is split by responsibility: frame.ts (codec), handshake.ts, frame-router.ts, semaphore.ts (concurrency permits), pending-requests.ts, stream-registry.ts, keepalive.ts, listener-set.ts, and client.ts, which only composes them. Each unit has its own test file under test/; the client itself is tested end to end against test/fake-socket.ts with hand-built wire bytes from test/wire.ts.

Transport note

WebSocket only. The WebSocket framing omits the 4-byte TCP length prefix: each binary message is exactly one Velo frame. For raw TCP from Node.js, supply a socketFactory whose socket adds the length prefix from the protocol spec.