12 JavaScript Guide
trev edited this page 2026-07-13 06:27:18 -04:00

JavaScript and TypeScript Guide

The JavaScript runtime lives in trevrpc-js/. It provides a browser WebTransport client runtime, Node native client/server bindings backed by the C/MsQuic runtime, shared wire/status/metadata helpers, a protobuf.js-based code generator, and TypeScript declarations for the runtime and generated clients.

Runtime Model

In browsers, the root connect() API returns a long-lived Channel over the browser WebTransport API. In Node, the same root API resolves to a native MsQuic channel; Channel is also available from trevrpc-js/node. Reconnection is built into both routine application APIs.

Generated clients accept the channel through the runtime transport interface:

import { connect } from "trevrpc-js";
import { GreeterClient } from "./hello/v1/greeter.trevrpc.js";

const channel = await connect(
  "https://127.0.0.1:50051/trevrpc",
);
const client = new GreeterClient(channel, { timeoutMs: 5000 });

try {
  const reply = await client.sayHello({ name: "TrevRPC" });
} finally {
  channel.close({ closeCode: 0, reason: "client shutdown" });
}

The browser channel uses globalThis.WebTransport by default and replaces a closed session for future calls. Every RPC snapshots one ready generation. If it dies, the call fails without retry, replay, resumption, or movement. Calls fail immediately with Unavailable while reconnecting; use channel.waitUntilReady() before issuing work when waiting is intentional.

Non-browser runtimes can pass a compatible constructor through the WebTransport option. Browser constructor options such as allowPooling, congestionControl, requireUnreliable, and serverCertificateHashes are explicit top-level options.

The browser and Node connect helpers wait for their first ready generation. Browser timeoutMs and signal, and the corresponding Node channel options, can bound initial readiness. Later reconnects continue until channel.close(). Routine options do not expose reconnect-policy tuning; channels use a 100 ms initial delay, multiplier 2, 20 percent jitter, and a 30 second cap.

Generate Clients

The package installs protoc-gen-trevrpc-js.

Buf example:

version: v2
plugins:
  - local: protoc-gen-trevrpc-js
    out: generated
    opt:
      - runtime_import=trevrpc-js

The generator emits one JavaScript file and one TypeScript declaration file for each requested proto file that contains services:

  • greeter.trevrpc.js contains the runtime client, service descriptor, and embedded protobuf.js reflection root.
  • greeter.trevrpc.d.ts contains message interfaces, typed client methods, and service declarations.

Supported generator options:

Option Default Purpose
runtime_import trevrpc-js Import path for the TrevRPC JavaScript runtime
file_suffix .trevrpc.js Suffix for generated JavaScript files

Generated Shape

For a protobuf service named Greeter, generated JavaScript has this shape:

export const root = createRoot(...);
export const GreeterService = Object.freeze(...);

export class GreeterClient {
  constructor(transport, options = {}) { ... }
  sayHello(request, options = {}) { ... }
  lotsOfReplies(request, options = {}) { ... }
  lotsOfGreetings(options = {}) { ... }
  bidiHello(options = {}) { ... }
}

export function createGreeterClient(transport, options = {}) { ... }

export function registerGreeterNodeServer(server, handlers) { ... }

The embedded protobuf.js root means generated clients accept plain JavaScript objects for protobuf messages. You do not need separately generated JavaScript message classes for basic client use.

Generated Node server helpers accept typed handler objects and register them on NodeServer from trevrpc-js/node.

TypeScript Use

The runtime package exports src/index.d.ts, and generated .trevrpc.d.ts files sit next to generated .trevrpc.js files. TypeScript users import the JavaScript module path and get the companion declarations automatically.

import { GreeterClient } from "./hello/v1/greeter.trevrpc.js";
import type { HelloRequest, HelloReply } from "./hello/v1/greeter.trevrpc.js";

const request: HelloRequest = { name: "TrevRPC" };
const reply: HelloReply = await client.sayHello(request);

Generated declaration files include message interfaces for the protobuf root embedded in the generated JavaScript file, including imported message types used by the service.

Call Options

Generated clients merge constructor-level options first, then per-call options.

const client = new GreeterClient(channel, {
  timeoutMs: 5000,
  metadata: { authorization: "Bearer trevrpc-example-token" },
});

const reply = await client.sayHello(
  { name: "TrevRPC" },
  { timeoutMs: 1000, metadata: { traceparent: "00-..." } },
);
Option Default
timeoutMs Unset
maxResponseBodySize 4 MiB
maxResponseMessages 4096
maxResponseStreamBodySize 16 MiB
streamIdleTimeoutMs 30 seconds
metadata Empty

Metadata keys are normalized to lowercase. Values may be strings, Uint8Array, ArrayBuffer, typed-array views, or byte arrays.

Streaming Calls

Server-streaming calls resolve to async iterables:

const replies = await client.lotsOfReplies({ name: "TrevRPC" });

for await (const reply of replies) {
  console.log(reply.message);
}

Client-streaming and bidirectional-streaming calls return sendable call objects:

const greetings = await client.lotsOfGreetings();
await greetings.send({ name: "first" });
await greetings.send({ name: "second" });
const summary = await greetings.closeAndRecv();

const bidi = await client.bidiHello();
await bidi.send({ name: "first" });
const reply = await bidi.recv();
await bidi.closeSend();

WebTransport Options

Pass browser WebTransport constructor options directly to connect:

const channel = await connect("https://127.0.0.1:50051/trevrpc", {
  serverCertificateHashes: [
    {
      algorithm: "sha-256",
      value: certificateHashBytes,
    },
  ],
});

Certificate hashes are mainly useful for local self-signed WebTransport examples. Production browser deployments should use normal WebPKI certificates accepted by the browser.

Node Native

Use the trevrpc-js/node subpath for native MsQuic clients and servers:

import {
  Channel,
  NodeServer,
  bearerAuthorizer,
} from "trevrpc-js/node";

const channel = await Channel.connect("https://127.0.0.1:50051", {
  caCertFile: "ca.pem",
});

const server = await NodeServer.listen("https://127.0.0.1:50051", {
  certFile: "cert.pem",
  keyFile: "key.pem",
  enableHttp3: true,
  http3Path: "/trevrpc",
  authorizer: bearerAuthorizer("expected-token"),
});

try {
  // Use the generated client and registered server handlers.
} finally {
  channel.close();
  server.close();
}

Node native TLS validates certificates by default. Use skipCertificateValidation: true only for local development and tests. caCertFile supplies an explicit trust root for private deployments. Node native servers expose setAuthorizer, setMetrics, and setLogger for request authorization and lifecycle observability. enableHttp3 accepts ordinary HTTP/3 POST RPCs through the C/MsQuic listener; an optional synchronous http3Admission wait is bounded by initialRequestTimeoutMs.

Call channel.close() during application shutdown. Node applications may instead import the same root connect API from trevrpc-js; package exports select the native implementation.

Advanced raw transports are intentionally separate: import RawWebTransport from trevrpc-js/advanced for one browser WebTransport session, or RawNodeTransport from trevrpc-js/node/advanced for one native MsQuic connection. They do not reconnect and are intended for integrations, benchmarks, diagnostics, and deterministic tests.

Where TrevRPC controls QUIC setup, 0-RTT application data remains disabled. Browser TLS resumption behavior is provider-controlled and is only a handshake optimization, never RPC recovery.

Native unary response bodies and native stream message bodies may be backed by external ArrayBuffers whose finalizer owns the corresponding C receive allocation. This receive-side owner transfer is safe because JavaScript receives ownership of the C allocation. Native addon operations are delivered through a binding-owned completion worker source.

Outbound request and response bodies are copied into native-owned storage before asynchronous submission. TrevRPC does not expose a borrowed outbound-buffer mode: Node-API references can keep JavaScript objects alive but cannot prevent mutation, detachment, transfer, or resize of their backing stores while MsQuic owns a pointer. Historical benchmark results retained in git measured meaningful CPU and RSS reductions for 1-3 MiB bodies, but the unsafe ownership contract and duplicate send-path maintenance outweighed that benefit.

Native stream and server-call outbound operations preserve JavaScript invocation order across sends, batches, and terminal operations. A failed outbound operation rejects its own promise and advances the queue, so later work runs in order and reports its own result. Receive operations are not members of this outbound FIFO and continue polling independently so full-duplex traffic can progress.

Statuses and Errors

RPC failures throw TrevRpcError when the runtime can map the failure to a TrevRPC status.

import { Code, TrevRpcError } from "trevrpc-js";

try {
  await client.sayHello({ name: "TrevRPC" });
} catch (error) {
  if (error instanceof TrevRpcError && error.code === Code.DeadlineExceeded) {
    console.error("RPC timed out");
  }
}

Transport failures generally map to Unavailable; local aborts map to Cancelled; oversized frames map to Unavailable at the JavaScript transport boundary.

Local Example

Start a Rust or Go example server with WebTransport enabled, then serve the browser client:

cd trevrpc-js
npm run example:greeter

Open http://127.0.0.1:8080/examples/greeter/. The page loads the local example certificate hash and bearer token if the matching Rust or Go example server has written its certificate to the default path.

Current Gaps

  • JavaScript native QUIC is not exposed through browser APIs.
  • Generated clients use protobuf.js reflection and plain objects rather than generated message classes.