Skip to content

Engine port ​

The engine is the replaceable server-side WebSocket implementation. Everything above it — the router, clients, rooms, subscriptions, the wire protocol — is written against this contract and nothing else, which is what makes uWebSockets.js a drop-in swap rather than a fork.

The engine is the WebSocket's way in, not the only one: a WebTransport session (attachSession), a WebRTC data channel (attachChannel) and the broker binding (attachBrokerRpc) reach the same RpcServer through attach(transport) and never pass through an engine.

js
const { createNodeEngine, isEngine } = require('@alexify/wrpc/engine');

This port covers the server side only. Client-side extensibility is the WrpcClient.transport registry — see Client.

Engine ​

ts
interface Engine {
  name: string;
  standalone?: boolean;
  capabilities: EngineCapabilities;
  attach(options: EngineAttachOptions): EngineConnectionSource;
  listen?(options: { host?: string; port?: number }): Promise<unknown>;
  close(options?: { code?: number; reason?: string }): void;
}

isEngine(value) is the structural check the Server shell and the adapters run — duck typing, never instanceof, so an engine from a different copy of the package still works.

Hosted vs standalone ​

Two shapes satisfy the port, and the difference is who owns the network stack.

Hosted (standalone falsy) attaches to a listener someone else owns:

js
const source = engine.attach({ server: httpServer, path: '/api', verifyClient });

It may also expose handleUpgrade(req, socket, head) on its source, for driving handshakes by hand from an app's own 'upgrade' listener — that is exactly what the express adapter does, attaching with no server at all.

Standalone (standalone: true) owns everything, node:http included. It is attached without a server, receives the core's HTTP entry point, and must implement listen():

js
const source = engine.attach({ path: '/*', verifyClient, onHttpCall: (call) => rpc.handleHttpCall(call) });
const address = await engine.listen({ host, port });

With a standalone engine, Server creates no node:http server: httpServer is null and server.address() is the only way to read the bound address.

attach(options) ​

OptionMeaning
serverThe node http(s) server. Required for hosted engines, absent otherwise.
pathRestrict upgrades to this pathname.
verifyClient({ req, socket, head })Gate the handshake. socket/head are null for standalone engines.
protocols / handleProtocols(offered, req)Subprotocol negotiation; false rejects the handshake. offered never holds wRPC's carrier tokens (wrpc.h., wrpc.m., wrpc.bearer.), and an engine must not echo one.
perMessageDeflatetrue, or { threshold, filter, contextTakeover, async, … }. See wire format.
coalesceBuilt-in engine: cork the writes of one event-loop turn into one flush. Default true.
pingIntervalProtocol-ping interval for engines that own liveness.
maxBuffer / maxBackpressure / fragmentThreshold / closeTimeoutEngine limits.
onHttpCall(call)Standalone engines only: the core's HTTP entry point.
loggerThe structured logger the engine and its connections report through — the Server shell and the adapters pass their own. An engine built with its own logger keeps it.

It returns an EngineConnectionSource — an EventEmitter that emits 'connection'(socket, req).

WrpcSocket ​

One per peer. The built-in engine's Connection satisfies it natively; an adapter normalizes its own socket to this shape.

ts
interface WrpcSocket extends EventEmitter {
  send(data: string | Buffer, options?: { compress?: boolean }): boolean;   // false = above the high-water mark
  sendPrepared?(message: SharedMessage): boolean;   // optional: capability `prepared`
  readonly bufferedAmount: number;
  readonly remoteAddress?: string;
  protocol?: string;
  close(code?: number, reason?: string): void;
  terminate(): void;
  pause?(): void;                          // optional receive-side flow control
  resume?(): void;
}

Events: 'message'(data, isBinary), 'drain', 'ping'(payload), 'pong'(payload), 'close'(code, reason), 'error'(error).

Three rules the whole stack depends on:

  • send() returns an honest boolean. false means the buffer is above its high-water mark and a 'drain' will follow. The built-in engine's asynchronous deflate paths (context takeover, async) count their queued bytes into that buffer, so the boolean covers them too. That is what makes stream and subscription backpressure real rather than aspirational — an engine that always returned true would turn a slow consumer into unbounded server memory.
  • A received payload may share memory with the receive buffer. Copy it if you retain it past the listener call.
  • protocol is the selected subprotocol, and it decides the revision. The core reads socket.protocol once, when the socket is attached: wrpc.v2 is revision 2 and the connection carries framed messages, anything else — wrpc.v1, '', a property the engine never set — is revision 1, bytes as 1.0's JSON. An adapter that forgets it is not refused; every client is served at revision 1, which only the wrpc.revision attribute of wrpc.server.connections and the revision.peer debug line show.

Two optional extensions, both feature-detected by the core:

  • send(data, { compress: false }) asks for this one message to go uncompressed on a deflate-negotiated connection. A socket that ignores the second argument simply compresses as usual.
  • sendPrepared(message) is the fan-out path (capability prepared). A room broadcast hands every recipient one { text, frames, compress } (text is the JSON, or the bytes of an attachments frame — a BINARY frame then) object; the socket claims frames with its own cache — the encoded frame, plus a deflated one per negotiated window — when it is null, reuses it when it already holds that engine's cache, and falls back to send(text) when another engine claimed it (a mixed room). The built-in engine implements it; a socket without it receives send(text).

EngineRequest ​

The upgrade request handed to verifyClient, handleProtocols and the 'connection' listener. The built-in engine passes a real IncomingMessage; a standalone engine never sees one and synthesizes a look-alike, so the port promises only what both provide:

ts
interface EngineRequest {
  method?: string;
  url?: string;
  headers: Record<string, string | Array<string> | undefined>;
  socket: { remoteAddress?: string };
}

Narrow it with a cast when you know which engine you have.

Capabilities ​

ts
interface EngineCapabilities {
  backpressure: boolean;
  ping: boolean;
  deflate: boolean;
  cork: boolean;
  pause: boolean;
  prepared: boolean;
}

Advertised, not enforced: nothing in the core branches on these today. They exist so a host, a test or a future optimization can ask what it is running on rather than guess — and so a difference between engines is stated rather than discovered.

node engineuws engine
backpressure✅✅
ping✅❌ — uws owns liveness through idleTimeout
deflate✅ (off by default)follows compression
cork✅✅
pause✅❌ — no socket-level pause
prepared✅ — sendPrepared, one frame per fan-out❌ — uws frames inside send()

The built-in engine ​

js
const { createNodeEngine } = require('@alexify/wrpc/engine');

// Both are opt-in; permessage-deflate in particular is off until asked for.
const engine = createNodeEngine({ perMessageDeflate: true, maxBackpressure: 1024 * 1024 });
new Server({ router, engine });

A thin wrapper over the from-scratch RFC 6455 implementation in src/websocket/ — handshake, framing, backpressure accounting, permessage-deflate. It is the default, so you only construct one to pass options. The implementation itself is published as @alexify/wrpc/ws.

Writing an engine ​

Implement Engine, yield sockets that satisfy WrpcSocket, and run the contract suite against it:

js
const { runEngineContract } = require('./tests/engine/engineContract.js');

test('my engine', (t) => runEngineContract(() => createMyEngine(), t));

tests/engine/engineContract.js is a shared helper (not a *.test.js) that checks the whole port: engine shape and capability types, 'connection' delivering a socket and an upgrade request, text and binary echo, send() returning a boolean, drain ordering, close codes, ping/pong. Hosted engines pass a bare factory (the suite owns an http server); standalone ones pass standaloneHarness(createEngine), which asks the engine to listen instead.

The same suite runs against the built-in engine and the uws adapter, so a new engine is held to exactly the behaviour the rest of the stack assumes.

Released under the MIT License.