@brashkie/ws
v0.3.0
Published
Cliente WebSocket (RFC 6455) en TypeScript puro, de propósito general. Cero dependencias. Node 18+.
Maintainers
Readme
@brashkie/ws
A general-purpose WebSocket client (RFC 6455) in pure TypeScript. No dependencies. No native addons. No surprises.
English | Español
Table of contents
- Why @brashkie/ws?
- Package family
- Features
- Installation
- Quick start
- Examples
- API reference
- Comparison
- Performance
- Compatibility
- Roadmap
- Security
- Contributing
- License
Why @brashkie/ws?
Most Node WebSocket clients drag in native C++ addons (bufferutil, utf-8-validate) or dependency chains that end up in npm audit warnings. @brashkie/ws takes the opposite path: a complete RFC 6455 implementation built on Node's standard library (node:net, node:tls, node:crypto), with zero runtime dependencies.
- Zero dependencies. Your
node_modulestree doesn't grow and your security surface doesn't expand. - No native build step. No
node-gyp, no binaries that break in CI or on Alpine. - Portable. The same code runs anywhere Node 18+ runs.
- A clean primitive. It does one thing —speak WebSocket— and does it well. Reconnection, session, and application-level heartbeat logic is left up to you.
Need maximum throughput, or the browser?
@brashkie/wsis part of a planned family of WebSocket transports that share one contract. See Package family and the Roadmap.
Package family
@brashkie/ws is the foundation of a family of WebSocket transports that share one isomorphic contract (WebSocketLike / WebSocketConstructor). They are interchangeable: you program against the contract and pick the implementation that fits your environment and performance needs.
| Package | Environment | Implementation | Status | Best for |
| --- | --- | --- | --- | --- |
| @brashkie/ws-core | isomorphic | Contract + types only (0 runtime) | 🚧 Planned | Shared contract for the whole family |
| @brashkie/ws | Node.js | Pure TypeScript (RFC 6455 from scratch) | ✅ Available | Portability, simplicity, zero deps |
| @brashkie/ws-native | Node.js | Rust + TypeScript (napi-rs) | 🚧 Planned | Maximum throughput, SIMD masking |
| @brashkie/ws-web | Browser | Thin wrapper over the native WebSocket | 🚧 Planned | Isomorphic apps that also run in the browser |
Because they honor the same contract, switching implementation is a single import change:
// Node, pure TS (today)
import { WebSocket } from '@brashkie/ws';
// Node, native Rust core (planned) — same API
import { WebSocket } from '@brashkie/ws-native';
// Browser (planned) — same API, over the platform WebSocket
import { WebSocket } from '@brashkie/ws-web';Why a family? This package speaks the protocol over raw TCP/TLS, so it only runs on Node.js, not in the browser (the browser already implements WebSocket itself).
@brashkie/ws-webwill adapt the platformWebSocketto the same contract, and@brashkie/ws-nativewill offer a Rust-powered core for heavy workloads. The shared contract will live in@brashkie/ws-core, isomorphic and runtime-free.
Features
- ✅ Full RFC 6455 client (
ws://andwss://over TLS). - ✅ Handshake with
Sec-WebSocket-Key/Accept(SHA-1), validated against the official RFC vector. - ✅ Complete framing: 7 / 16 / 64-bit payload lengths, mandatory client masking.
- ✅ Automatic ping/pong and reassembly of fragmented messages.
- ✅ Close with application code (3000–4999) and reason.
- ✅ ESM + CommonJS + types in a single package.
- ✅ Incremental parser (correctly handles partial TCP chunks).
- ✅ Zero runtime dependencies. Node 18+.
Installation
npm install @brashkie/ws
# or
pnpm add @brashkie/ws
# or
yarn add @brashkie/wsQuick start
import { WebSocket } from '@brashkie/ws';
const ws = new WebSocket('wss://example.com/socket');
ws.on('open', () => {
console.log('connected');
ws.send('hello');
});
ws.on('message', (data, isBinary) => {
console.log('received:', isBinary ? data : data.toString());
});
ws.on('close', (code, reason) => console.log('closed:', code, reason));
ws.on('error', (err) => console.error('error:', err));Examples
Every example is copy-paste ready and uses only the package's real API.
Send and receive text
import { WebSocket } from '@brashkie/ws';
const ws = new WebSocket('wss://example.com');
ws.on('open', () => ws.send(JSON.stringify({ type: 'greet', data: 'hi' })));
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
console.log(msg);
});Send binary data
import { WebSocket } from '@brashkie/ws';
const ws = new WebSocket('wss://example.com');
ws.on('open', () => {
const buffer = Buffer.from([0x01, 0x02, 0x03, 0x04]);
ws.send(buffer);
});
ws.on('message', (data, isBinary) => {
if (isBinary) console.log('bytes:', data);
});Heartbeat (periodic ping with timeout)
import { WebSocket } from '@brashkie/ws';
function withHeartbeat(url: string, intervalMs = 30_000) {
const ws = new WebSocket(url);
let alive = true;
let timer: NodeJS.Timeout;
ws.on('open', () => {
timer = setInterval(() => {
if (!alive) return ws.close(4000, 'no response');
alive = false;
ws.ping();
}, intervalMs);
});
ws.on('pong', () => { alive = true; });
ws.on('close', () => clearInterval(timer));
return ws;
}
withHeartbeat('wss://example.com');Reconnection with exponential backoff
import { ReconnectingWebSocket } from '@brashkie/ws';
const ws = new ReconnectingWebSocket('wss://example.com', {
minDelay: 1000, // first retry after 1s
maxDelay: 30_000, // cap at 30s
factor: 2, // exponential
});
ws.on('open', () => ws.send('hi'));
ws.on('message', (data) => console.log(data.toString()));
ws.on('reconnect', (attempt, delay) => console.log(`retry #${attempt} in ${delay}ms`));
ws.close(); // stops reconnectionSubprotocols, headers and compression
import { WebSocket } from '@brashkie/ws';
const ws = new WebSocket('wss://example.com', {
protocols: ['chat', 'superchat'], // Sec-WebSocket-Protocol
headers: { Authorization: 'Bearer <token>', Cookie: 'sid=abc' },
perMessageDeflate: true, // negotiate RFC 7692 compression
});
ws.on('open', () => console.log('negotiated subprotocol:', ws.protocol));CommonJS
const { WebSocket } = require('@brashkie/ws');
const ws = new WebSocket('wss://example.com');
ws.on('open', () => ws.send('hello from CJS'));
ws.on('message', (data) => console.log(data.toString()));API reference
new WebSocket(url: string, options?: WebSocketOptions)
Creates the connection. Accepts ws:// (TCP) and wss:// (TLS). Starts the handshake immediately.
WebSocketOptions:
| Option | Type | Description |
| --- | --- | --- |
| protocols | string \| string[] | Subprotocol(s) offered in Sec-WebSocket-Protocol. |
| headers | Record<string, string> | Extra handshake headers (auth, cookies, …). |
| perMessageDeflate | boolean | Negotiate permessage-deflate compression (RFC 7692). |
| maxPayload | number | Max message size in bytes (default 100 MiB). Exceeding it closes with 1009. |
For auto-reconnect, see ReconnectingWebSocket(url, options?), which accepts the same options plus maxRetries, minDelay, maxDelay and factor.
Methods
| Method | Description |
| --- | --- |
| send(data: string \| Buffer): void | Sends a text frame (string) or binary frame (Buffer). |
| ping(data?: Buffer): void | Sends a ping frame. |
| close(code = 1000, reason = ''): void | Initiates the closing handshake. Supports application codes 3000–4999. |
| terminate(): void | Closes immediately by destroying the socket (no closing handshake). |
Properties
| Property | Type | Description |
| --- | --- | --- |
| readyState | number | Current connection state. |
| url | string | The URL the connection was created with. |
| protocol | string | Negotiated subprotocol (empty if none). |
| bufferedAmount | number | Bytes queued in the socket but not yet sent (write backpressure). |
Statics
WebSocket.CONNECTING (0) · WebSocket.OPEN (1) · WebSocket.CLOSING (2) · WebSocket.CLOSED (3)
Events
| Event | Payload | When |
| --- | --- | --- |
| open | — | Handshake complete; ready to send. |
| message | (data: Buffer, isBinary: boolean) | A full message arrived (fragments already reassembled). |
| ping | (data: Buffer) | The server sent a ping (a pong is sent automatically). |
| pong | (data: Buffer) | The server sent a pong. |
| close | (code: number, reason: string) | The connection closed. 1006 means abnormal close. |
| error | (err: Error) | Network, handshake, or protocol error. |
Exported types
import type { WebSocketLike, WebSocketConstructor } from '@brashkie/ws';WebSocketLike describes an instance's shape; WebSocketConstructor describes the class (including statics). Use them to accept any implementation in the family via injection:
import type { WebSocketConstructor } from '@brashkie/ws';
function createClient(WS: WebSocketConstructor, url: string) {
return new WS(url); // works with @brashkie/ws, @brashkie/ws-native or @brashkie/ws-web
}Comparison
| | @brashkie/ws | @brashkie/ws-native (planned) | @brashkie/ws-web (planned) | ws (npm) |
| --- | --- | --- | --- | --- |
| Environment | Node.js | Node.js | Browser | Node.js |
| Language | Pure TypeScript | Rust + TypeScript | TypeScript (wrapper) | JS (+ optional C++ addons) |
| Runtime dependencies | 0 | 0 (own binary) | 0 (platform WebSocket) | 0 (optional addons) |
| Masking / unmasking | JavaScript | Rust (SIMD, goal) | handled by the browser | JS, or bufferutil (C++) |
| Installation | no build | prebuilt binaries | no build | no build |
| Client | ✅ | ✅ | ✅ | ✅ |
| Server | — (future) | — | — | ✅ |
| permessage-deflate | planned | planned | handled by the browser | ✅ |
| Minimum runtime | Node 18+ | Node 18+ | modern browsers | Node 10+ |
| Ideal for | portability & simplicity | high throughput | browser / isomorphic apps | de-facto standard, client+server |
ws is excellent and the community standard; if you need a WebSocket server today, use it. The @brashkie family aims at something else: a minimal, first-party, dependency-free client with one contract across Node (pure TS or native Rust) and the browser.
Performance
@brashkie/ws prioritizes portability and simplicity. The parser is incremental and avoids unnecessary copies, but masking/unmasking runs in JavaScript, which is plenty for the vast majority of applications (chats, bots, dashboards, telemetry).
For very high-volume workloads (hundreds of thousands of messages/second, large payloads), the family will offer @brashkie/ws-native, with Rust-accelerated masking and native parsing. The design goal is to expose the same API, so the switch is a single import.
Comparative benchmarks (
@brashkie/wsvs@brashkie/ws-nativevsws) will ship alongside@brashkie/ws-native. We don't publish numbers we can't reproduce.
Compatibility
- Node.js 18+ (uses
node:net,node:tls,node:crypto, andBuffer). - ESM (
import) and CommonJS (require) from the same package. - TypeScript with bundled types (
.d.tsand.d.cts). - Browser: not this package — see
@brashkie/ws-web(planned).
Roadmap
Development proceeds in phases: first @brashkie/ws (the pure-TS Node client) is completed and hardened; then the shared contract is extracted into @brashkie/ws-core, enabling the browser (@brashkie/ws-web) and native (@brashkie/ws-native) implementations.
Phase 1 — @brashkie/ws (Node, pure TypeScript) · in progress
A complete, portable WebSocket client.
- [x] RFC 6455 client: handshake, 7/16/64-bit framing, masking
- [x] Automatic ping/pong, fragment reassembly, close with code
- [x] RSV-bit validation (rejects frames using non-negotiated extensions)
- [x]
ws://andwss:// - [x]
url,bufferedAmount,terminate() - [x] ESM + CJS + types, zero dependencies, vitest tests (87 tests)
- [x] Subprotocols (
Sec-WebSocket-Protocol) - [x] Custom handshake headers (auth, cookies)
- [x]
permessage-deflate(compression) vianode:zlib - [x] Optional reconnection helper with backoff (
ReconnectingWebSocket) - [x] Strict incremental UTF-8 validation on text frames (closes
1007) - [x] Close-code validation, control-frame & fragmentation rules,
maxPayload(1002/1007/1009) - [x] Memory-leak & backpressure verification (
npm run test:leak/test:backpressure) - [ ] Autobahn test suite conformance (harness in
autobahn/; all required validations implemented — runwstestwith Docker to confirm the score)
Phase 2 — @brashkie/ws-core (isomorphic contract)
The shared, runtime-free contract that the whole family implements.
- [ ] Extract
WebSocketLike/WebSocketConstructorinto a standalone package - [ ] Isomorphic types (data as
Uint8Array;ping()optional, as the browser doesn't expose it) - [ ] Shared close codes and constants
- [ ] No runtime code — types and tiny helpers only
Phase 3 — @brashkie/ws-web (Browser)
A thin wrapper over the platform WebSocket, exposing the ws-core contract.
- [ ] Adapt
addEventListener/onmessageto the.on(...)event style - [ ] Normalize incoming data (
ArrayBuffer/Blob→Uint8Array) - [ ] Graceful handling of browser limitations (no app-level
ping, no custom headers) - [ ] Bundle for ESM + types
Phase 4 — @brashkie/ws-native (Node, Rust + TypeScript)
A Rust-powered core for heavy workloads, same ws-core contract.
- [ ] Native core with napi-rs implementing the contract
- [ ] SIMD-accelerated masking/unmasking and native parsing
- [ ] Prebuilt binaries per platform (linux/macos/windows · x64/arm64)
- [ ] Automatic fallback to
@brashkie/wswhen no native binary is available - [ ] Reproducible benchmarks vs
@brashkie/wsand vsws
The roadmap is indicative and may change. Checked items reflect what's already available in the current version.
Security
- Zero runtime dependencies. The published package ships only
dist/; it adds no attack surface and nonpm auditalerts to your project. - Any alerts you may see when cloning this repo come exclusively from devDependencies (the build and test chain) and are not published nor delivered to anyone installing the package.
- Found a security issue? Open an issue in the repository.
Contributing
git clone https://github.com/Brashkie/ws.git
cd ws
npm install
npm run build # ESM + CJS + types
npm test # vitest
npm run typecheck
npm run lintPRs are welcome. For large changes, open an issue first to discuss the approach.
License
Apache-2.0 © Brashkie (Hepein Oficial)
