@trevrpc/trevrpc-js 0.1.8 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,202 +1,96 @@
1
1
  # trevrpc-js
2
2
 
3
- `trevrpc-js` is the ESM-only JavaScript runtime and protobuf.js generator for TrevRPC.
3
+ TrevRPC is an RPC framework like gRPC, but uses QUIC (and HTTP/3 / WebTransport) instead of HTTP/2. Define services in protobuf, generate typed clients and servers, and run them over QUIC.
4
4
 
5
- ## Generated files
5
+ Full documentation: https://trev.zip/llc/TrevRPC/wiki
6
6
 
7
- For every protobuf file containing services, `protoc-gen-trevrpc-js` emits an ordinary browser-safe client module and a separate Node server module:
7
+ ## Protobuf
8
8
 
9
- ```text
10
- greeter.trevrpc.js
11
- greeter.trevrpc.d.ts
12
- greeter.node.trevrpc.js
13
- greeter.node.trevrpc.d.ts
14
- ```
15
-
16
- The ordinary module contains the protobuf root, service descriptors, typed clients, and client factories. The Node companion contains typed handler interfaces and registration helpers backed by `@trevrpc/trevrpc-js/node/generated`.
17
-
18
- ```sh
19
- protoc \
20
- --plugin=protoc-gen-trevrpc-js=./node_modules/.bin/protoc-gen-trevrpc-js \
21
- --trevrpc-js_out=. \
22
- greeter.proto
23
- ```
9
+ ```proto
10
+ syntax = "proto3";
24
11
 
25
- Generator options are `runtime_import`, `runtime_type_import`, `node_runtime_import`, `node_runtime_type_import`, and `file_suffix`. Runtime type imports default to their matching runtime import for bare and relative specifiers. URL specifiers such as `file:` require an explicit type import because TypeScript NodeNext does not resolve them in declarations. A Node import is never inferred by appending `/node` to another specifier.
12
+ package example.greeter;
26
13
 
27
- ## Browser clients
28
-
29
- Browser declarations accept only `BrowserChannelOptions`.
30
-
31
- ```js
32
- import { connect } from "@trevrpc/trevrpc-js";
33
- import { GreeterClient } from "./greeter.trevrpc.js";
34
-
35
- const channel = await connect("https://localhost:50051/trevrpc", {
36
- timeoutMs: 10_000,
37
- congestionControl: "low-latency",
38
- serverCertificateHashes: [{ algorithm: "sha-256", value: certificateHash }],
39
- });
40
- try {
41
- const client = new GreeterClient(channel, { timeoutMs: 5_000 });
42
- const reply = await client.sayHello({ name: "TrevRPC" });
43
- console.log(reply.message);
44
- } finally {
45
- channel.close({ closeCode: 0, reason: "client shutdown" });
14
+ service Greeter {
15
+ rpc SayHello(HelloRequest) returns (HelloReply);
16
+ rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
17
+ rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
18
+ rpc BidiHello(stream HelloRequest) returns (stream HelloReply);
46
19
  }
47
- ```
48
20
 
49
- Native TLS bypass, certificate validation, MsQuic buffering, and native stream options are intentionally unavailable under browser or Bundler resolution.
21
+ message HelloRequest { string name = 1; }
22
+ message HelloReply { string message = 1; }
23
+ ```
50
24
 
51
- ## Node clients
25
+ Generate with `protoc-gen-trevrpc-js` (`--trevrpc-js_out`).
52
26
 
53
- Under Node conditions, the root package uses the native channel. The explicit `@trevrpc/trevrpc-js/node` subpath also exports `Channel`, `NodeServer`, and `NodeServerCall`.
27
+ ## Client
54
28
 
55
29
  ```js
56
30
  import { connect } from "@trevrpc/trevrpc-js";
57
31
  import { GreeterClient } from "./greeter.trevrpc.js";
58
32
 
59
- const channel = await connect("https://localhost:50051", {
60
- timeoutMs: 10_000,
61
- caCertFile: "localhost-cert.pem",
62
- });
63
- try {
64
- console.log(channel.endpoint); // frozen { host, port }
65
- console.log(channel.options); // frozen effective NodeChannelOptions
66
- const client = new GreeterClient(channel, { timeoutMs: 5_000 });
67
- console.log((await client.sayHello({ name: "TrevRPC" })).message);
68
- } finally {
69
- channel.close();
70
- }
71
- ```
72
-
73
- URL and `{ host, port, ...options }` forms normalize to the same public state. Channels reconnect for future calls after native transport loss, but never replay or resume an operation that already failed.
74
-
75
- ## Streaming clients
76
-
77
- Server-streaming methods resolve to `ResponseAsyncIterable<T>`:
33
+ const channel = await connect("https://127.0.0.1:50051/trevrpc");
34
+ const client = new GreeterClient(channel);
78
35
 
79
- ```js
80
- const replies = await client.lotsOfReplies({ name: "TrevRPC" });
81
- try {
82
- for await (const reply of replies) {
83
- console.log(reply.message);
84
- }
85
- const terminal = await replies.status;
86
- console.log(terminal.code, terminal.message, terminal.metadata);
87
- } finally {
88
- await replies.close();
89
- }
90
- ```
36
+ // Unary
37
+ const reply = await client.sayHello({ name: "TrevRPC" });
91
38
 
92
- Full clean consumption resolves `status`. A non-OK terminal status rejects iteration and `status` with the same `TrevRpcError`. `close()`, iterator `return()`, and breaking a `for await` loop cancel the stream and reject `status` with `Code.Cancelled`. Consuming a valid terminal frame claims terminal precedence and cancels the absolute deadline, but `status` settles only after trailing EOF is validated; merely buffering a status frame is not completion.
39
+ // Server streaming
40
+ const stream = await client.lotsOfReplies({ name: "TrevRPC" });
41
+ for await (const r of stream) console.log(r.message);
93
42
 
94
- The status/error text distinction is deliberate:
43
+ // Client streaming
44
+ const cs = await client.lotsOfGreetings();
45
+ await cs.send({ name: "Alice" });
46
+ await cs.send({ name: "Bob" });
47
+ const summary = await cs.closeAndRecv();
95
48
 
96
- - `StreamStatus.message` is the unmodified peer terminal message.
97
- - `TrevRpcError.statusMessage` is the unmodified peer error message.
98
- - `TrevRpcError.message` is formatted JavaScript text such as `"Unavailable: down"`.
49
+ // Bidirectional streaming
50
+ const bidi = await client.bidiHello();
51
+ await bidi.send({ name: "Alice" });
52
+ console.log(await bidi.recv());
53
+ await bidi.closeSend();
54
+ for await (const r of bidi) console.log(r.message);
99
55
 
100
- Unary and client-streaming terminal metadata is available through `sayHelloWithResponse()` and `closeAndRecvWithResponse()`.
101
-
102
- Client streaming:
103
-
104
- ```js
105
- const call = await client.lotsOfGreetings();
106
- await call.send({ name: "Alice" });
107
- await call.send({ name: "Bob" });
108
- const { message, metadata } = await call.closeAndRecvWithResponse();
56
+ channel.close();
109
57
  ```
110
58
 
111
- Bidirectional streaming:
59
+ Node uses the same API via `@trevrpc/trevrpc-js/node` (native MsQuic).
112
60
 
113
- ```js
114
- const call = await client.bidiHello();
115
- try {
116
- await call.send({ name: "Alice" });
117
- console.log(await call.recv());
118
- await call.closeSend();
119
- while ((await call.recv()) != null) {}
120
- console.log(await call.status);
121
- } finally {
122
- await call.close();
123
- }
124
- ```
125
-
126
- `timeoutMs` is one absolute RPC deadline that begins before transport setup and covers upload, send, receive, and cleanup. The same `DeadlineExceeded` object wins signal and rejection settlement. `streamIdleTimeoutMs` remains an independent per-read inactivity bound.
127
-
128
- ## Typed Node servers
129
-
130
- Import the generated registration helper from the Node companion. Direct protobuf responses are never confused with metadata envelopes, even when they have `message` or `messages` properties.
61
+ ## Server (Node)
131
62
 
132
63
  ```js
133
- import { Code, createStreamingResponse, createUnaryResponse } from "@trevrpc/trevrpc-js";
134
64
  import { NodeServer } from "@trevrpc/trevrpc-js/node";
135
65
  import { registerGreeterServer } from "./greeter.node.trevrpc.js";
66
+ import { createUnaryResponse, createStreamingResponse } from "@trevrpc/trevrpc-js";
136
67
 
137
68
  const server = await NodeServer.listen({
138
69
  host: "127.0.0.1",
139
70
  port: 50051,
140
- certFile: "localhost-cert.pem",
141
- keyFile: "localhost-key.pem",
71
+ certFile: "cert.pem",
72
+ keyFile: "key.pem",
142
73
  });
143
74
 
144
75
  registerGreeterServer(server, {
145
- sayHello(request, call) {
146
- console.log(call.deadline, call.signal.aborted);
147
- return createUnaryResponse(
148
- { message: `Hello, ${request.name || "world"}` },
149
- { trailer: "unary" },
150
- );
76
+ sayHello(req) {
77
+ return createUnaryResponse({ message: `hello, ${req.name}` });
151
78
  },
152
-
153
- async lotsOfGreetings(requests) {
154
- const names = [];
155
- for await (const request of requests) names.push(request.name);
156
- return createUnaryResponse({ message: names.join(", ") }, { trailer: "client-stream" });
79
+ lotsOfReplies(req) {
80
+ return createStreamingResponse([
81
+ { message: `hello, ${req.name}` },
82
+ { message: `goodbye, ${req.name}` },
83
+ ]);
157
84
  },
158
-
159
- lotsOfReplies(request) {
160
- return createStreamingResponse([{ message: request.name }], {
161
- code: Code.Ok,
162
- message: "complete",
163
- metadata: { trailer: "server-stream" },
164
- });
85
+ async lotsOfGreetings(reqs) {
86
+ const names = [];
87
+ for await (const r of reqs) names.push(r.name);
88
+ return createUnaryResponse({ message: names.join(", ") });
165
89
  },
166
-
167
- async *bidiHello(requests) {
168
- for await (const request of requests) yield { message: request.name };
90
+ async *bidiHello(reqs) {
91
+ for await (const r of reqs) yield { message: `hello, ${r.name}` };
169
92
  },
170
93
  });
171
94
 
172
95
  await server.serve();
173
96
  ```
174
-
175
- Client-streaming completion uses streaming wire semantics: exactly one response message followed by exactly one terminal status; it never uses unary `respond()`.
176
-
177
- `NodeServerCall.signal` covers effective deadlines, server shutdown, local close, and cancellation observed by native I/O. The current native bridge cannot promptly report remote peer cancellation while a handler is idle and performs no call I/O; cancellation becomes observable when the handler next enters native call I/O.
178
-
179
- ## Native package support
180
-
181
- The core tarball is platform-neutral. Native code is supplied by the exact optional package `@trevrpc/trevrpc-js-native-linux-x64-gnu@0.1.8`.
182
-
183
- Initial support is Linux x86-64, glibc 2.42 or newer, and Node 24. Browser usage still installs and runs on Darwin, ARM, and musl. Requesting a native Node channel on an unsupported or incomplete installation throws `TrevRpcError(Code.Unavailable)` with the detected target, supported targets, expected optional package, whether loading was missing or failed, and the original loader error as `cause`.
184
-
185
- All addon errors crossing public Node transport and server boundaries become `TrevRpcError` unless already normalized. `nativeCode` and `cause` preserve native provenance.
186
-
187
- ## Package resolution
188
-
189
- Conditional ESM exports are authoritative; the package has no legacy top-level `main` or `types`. Use TypeScript NodeNext resolution for Node consumers and Bundler resolution with browser conditions for browser consumers.
190
-
191
- ## Native development
192
-
193
- Installation never builds native code. In a source checkout:
194
-
195
- ```sh
196
- npm run build:native:test
197
- npm test
198
- npm run build:native
199
- npm run verify:native:production
200
- ```
201
-
202
- The development fallback is only `build/native/trevrpc_native.node` in a source checkout.
@@ -1,4 +1,4 @@
1
- #!/nix/store/hwjfj8m2kcsl7kz2xa5yf84jbfh9jssf-nodejs-24.18.1/bin/node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { runGenerator } from "../src/generator.js";
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevrpc/trevrpc-js",
3
- "version": "0.1.8",
3
+ "version": "0.2.2",
4
4
  "description": "JavaScript WebTransport runtime and protobuf.js code generator for TrevRPC",
5
5
  "homepage": "https://trev.zip/llc/TrevRPC",
6
6
  "bugs": {
@@ -65,24 +65,25 @@
65
65
  "format": "oxfmt --write",
66
66
  "format:check": "oxfmt --check",
67
67
  "lint": "oxlint --deny-warnings",
68
+ "prepack": "node -e 'if (process.env.TREVRPC_NIX_PUBLICATION !== \"1\") throw new Error(\"Build release tarballs with nix build .#trevrpc-js^npm\")'",
68
69
  "test": "node --test --test-concurrency=1 test/*.test.js",
69
70
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.browser.json && tsc -p tsconfig.node.json",
70
71
  "verify:native:production": "node -e 'const { readFileSync } = require(\"node:fs\"); const path = \"./build/native/trevrpc_native.node\"; const expectedExports = [\"connectMsQuic\", \"createCancellation\", \"listenMsQuic\"]; const actualExports = Object.getOwnPropertyNames(require(path)).sort(); const exportsMatch = JSON.stringify(actualExports) === JSON.stringify(expectedExports); const embedsDebugNames = readFileSync(path).includes(Buffer.from(\"_debug\")); if (!exportsMatch || embedsDebugNames) throw new Error(`production addon exports ${JSON.stringify(actualExports)}; expected exactly ${JSON.stringify(expectedExports)}${embedsDebugNames ? \"; binary contains _debug\" : \"\"}`);'"
71
72
  },
72
73
  "dependencies": {
73
- "protobufjs": "^8.7.1"
74
+ "protobufjs": "^8.8.0"
74
75
  },
75
76
  "devDependencies": {
76
77
  "@types/node": "^24.13.3",
77
- "esbuild": "^0.28.1",
78
- "playwright": "^1.62.1",
78
+ "esbuild": "^0.28.2",
79
+ "playwright": "1.61.1",
79
80
  "typescript": "^7.0.2"
80
81
  },
81
- "optionalDependencies": {
82
- "@trevrpc/trevrpc-js-native-linux-x64-gnu": "0.1.8"
83
- },
84
82
  "engines": {
85
83
  "node": ">=24 <25"
86
84
  },
87
- "packageManager": "npm@11.19.0"
85
+ "packageManager": "npm@12.0.2",
86
+ "optionalDependencies": {
87
+ "@trevrpc/trevrpc-js-native-linux-x64-gnu": "0.2.2"
88
+ }
88
89
  }
@@ -1,6 +1,10 @@
1
1
  import { ChannelStateMachine, stripChannelOptions, waitForInitialReady } from "./channel.js";
2
2
  import { unavailable } from "./status.js";
3
- import { RawWebTransport, createWebTransportSession } from "./webtransport.js";
3
+ import {
4
+ RawWebTransport,
5
+ createWebTransportSession,
6
+ waitForWebTransportReady,
7
+ } from "./webtransport.js";
4
8
 
5
9
  /** Browser channel with background session reconnection. */
6
10
  export class Channel extends ChannelStateMachine {
@@ -31,7 +35,7 @@ async function connectWebTransport(url, options, signal) {
31
35
  signal.addEventListener("abort", onAbort, { once: true });
32
36
 
33
37
  try {
34
- await session.ready;
38
+ await waitForWebTransportReady(session);
35
39
  if (signal.aborted) {
36
40
  throw unavailable("channel is closed");
37
41
  }
@@ -13,17 +13,17 @@ const packageVersion = (() => {
13
13
  return "unknown";
14
14
  }
15
15
  })();
16
- const SupportedTarget = "linux/x64/glibc";
16
+ const SupportedOptionalTarget = "linux/x64/glibc";
17
17
  const LinuxX64GnuPackage = "@trevrpc/trevrpc-js-native-linux-x64-gnu";
18
18
 
19
19
  /** Loads the target-specific native addon without building during installation. */
20
- export function loadNativeAddon() {
20
+ export function loadNativeAddon({ loadOptionalPackage = true, loadBundledAddon = true } = {}) {
21
21
  const target = nativeTarget();
22
22
  const packageName = nativePackageName(target);
23
23
  let packageError;
24
24
  let packageState = "unsupported";
25
25
 
26
- if (packageName != null) {
26
+ if (loadOptionalPackage && packageName != null) {
27
27
  try {
28
28
  return require(packageName);
29
29
  } catch (error) {
@@ -32,6 +32,16 @@ export function loadNativeAddon() {
32
32
  }
33
33
  }
34
34
 
35
+ const bundled = loadBundledAddon ? bundledNativePath() : null;
36
+ let bundledError;
37
+ if (bundled != null) {
38
+ try {
39
+ return require(bundled);
40
+ } catch (error) {
41
+ bundledError = error;
42
+ }
43
+ }
44
+
35
45
  const fallback = developmentFallbackPath();
36
46
  let fallbackError;
37
47
  if (fallback != null) {
@@ -44,6 +54,8 @@ export function loadNativeAddon() {
44
54
 
45
55
  const detected = targetName(target);
46
56
  const expected = packageName ?? "no optional package (unsupported target)";
57
+ const bundledState =
58
+ bundled == null ? "no Nix-bundled addon was present" : "the Nix-bundled addon failed to load";
47
59
  const fallbackState =
48
60
  fallback == null
49
61
  ? "no source-checkout fallback was present"
@@ -52,10 +64,10 @@ export function loadNativeAddon() {
52
64
  : "the source-checkout fallback failed to load";
53
65
  throw new Error(
54
66
  `TrevRPC native addon is unavailable for detected target ${detected}. ` +
55
- `Supported targets: ${SupportedTarget}. Expected optional package: ${expected}@${packageVersion}; ` +
56
- `package state: ${packageState}; ${fallbackState}. ` +
67
+ `Supported optional-package targets: ${SupportedOptionalTarget}. Expected optional package: ${expected}@${packageVersion}; ` +
68
+ `package state: ${packageState}; ${bundledState}; ${fallbackState}. ` +
57
69
  "Install the matching optional package or build build/native/trevrpc_native.node explicitly in a source checkout; npm installation never starts a source build.",
58
- { cause: fallbackError ?? packageError },
70
+ { cause: fallbackError ?? bundledError ?? packageError },
59
71
  );
60
72
  }
61
73
 
@@ -75,6 +87,11 @@ function nativePackageName(target) {
75
87
  return null;
76
88
  }
77
89
 
90
+ function bundledNativePath() {
91
+ const path = join(packageRoot, "native", "trevrpc_native.node");
92
+ return existsSync(path) ? path : null;
93
+ }
94
+
78
95
  function developmentFallbackPath() {
79
96
  if (!existsSync(join(packageRoot, "native", "CMakeLists.txt"))) {
80
97
  return null;
@@ -30,13 +30,13 @@ export class RawWebTransport {
30
30
  /** Opens a WebTransport session and wraps it in a TrevRPC client. */
31
31
  static async connect(url, options = {}) {
32
32
  const session = createWebTransportSession(url, options);
33
- await session.ready;
33
+ await waitForWebTransportReady(session);
34
34
  return new RawWebTransport(session, options);
35
35
  }
36
36
 
37
37
  /** Waits for the underlying WebTransport session to become ready. */
38
38
  async ready() {
39
- await this.session.ready;
39
+ await waitForWebTransportReady(this.session);
40
40
  }
41
41
 
42
42
  /** Closes the underlying WebTransport session. */
@@ -146,6 +146,21 @@ export class RawWebTransport {
146
146
  }
147
147
  }
148
148
 
149
+ /** Waits for readiness without hanging when a session closes during setup. */
150
+ export async function waitForWebTransportReady(session) {
151
+ if (session.closed == null) {
152
+ await session.ready;
153
+ return;
154
+ }
155
+
156
+ await Promise.race([
157
+ session.ready,
158
+ Promise.resolve(session.closed).then(() => {
159
+ throw unavailable("WebTransport session closed before becoming ready");
160
+ }),
161
+ ]);
162
+ }
163
+
149
164
  /** Creates a WebTransport session using only browser constructor options. */
150
165
  export function createWebTransportSession(url, options = {}) {
151
166
  const WebTransportCtor = options.WebTransport ?? globalThis.WebTransport;