@trevrpc/trevrpc-js 1.0.0 → 1.2.0
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 +86 -18
- package/bin/protoc-gen-trevrpc-js.js +1 -1
- package/package.json +17 -12
- package/src/call.js +616 -40
- package/src/channel.js +190 -18
- package/src/client.js +555 -106
- package/src/compression.js +330 -0
- package/src/connection-metadata.js +114 -0
- package/src/datagram.js +357 -0
- package/src/framing.js +124 -110
- package/src/generator.js +147 -16
- package/src/index.d.ts +223 -17
- package/src/qmux/frames.js +262 -0
- package/src/qmux/params.js +104 -0
- package/src/qmux/session.js +740 -0
- package/src/race.js +90 -0
- package/src/status.js +1 -1
- package/src/stream-compression.js +298 -0
- package/src/varint.js +73 -0
- package/src/wire.js +47 -28
- package/src/zstd.d.ts +35 -0
- package/src/zstd.js +407 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# trevrpc-js
|
|
2
2
|
|
|
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 call Go or Rust servers over WebTransport.
|
|
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 call Go or Rust servers over WebTransport, falling back to QMux over WebSocket where WebTransport is missing or UDP is blocked.
|
|
4
4
|
|
|
5
|
-
`trevrpc-js` is the **browser client**: a WebTransport runtime plus `protoc-gen-trevrpc-js`, which generates typed clients (no server code). It has no Node.js runtime; servers are written in Go or Rust, native QUIC clients in Go or Rust, and HTTP/3 clients in Kotlin.
|
|
5
|
+
`trevrpc-js` is the **browser client**: a WebTransport runtime with a QMux over WebSocket fallback, plus `protoc-gen-trevrpc-js`, which generates typed clients (no server code). It has no Node.js runtime; servers are written in Go or Rust, native QUIC clients in Go or Rust, and HTTP/3 clients in Kotlin.
|
|
6
6
|
|
|
7
7
|
Full documentation: https://trev.zip/llc/TrevRPC/wiki
|
|
8
8
|
|
|
@@ -46,6 +46,7 @@ This writes `greeter.trevrpc.js` and `greeter.trevrpc.d.ts`. Plugin parameters (
|
|
|
46
46
|
|
|
47
47
|
```js
|
|
48
48
|
import { connect } from "@trevrpc/trevrpc-js";
|
|
49
|
+
|
|
49
50
|
import { GreeterClient } from "./gen/greeter.trevrpc.js";
|
|
50
51
|
|
|
51
52
|
const channel = await connect("https://example.com:4433/trevrpc");
|
|
@@ -64,6 +65,11 @@ await upload.send({ name: "Alice" });
|
|
|
64
65
|
await upload.send({ name: "Bob" });
|
|
65
66
|
const summary = await upload.closeAndRecv();
|
|
66
67
|
|
|
68
|
+
// Client streaming from an iterable or async iterable
|
|
69
|
+
const batch = await client.lotsOfGreetings();
|
|
70
|
+
await batch.sendAll([{ name: "Alice" }, { name: "Bob" }]);
|
|
71
|
+
const batchSummary = await batch.closeAndRecv();
|
|
72
|
+
|
|
67
73
|
// Bidirectional streaming
|
|
68
74
|
const chat = await client.bidiHello();
|
|
69
75
|
await chat.send({ name: "Alice" });
|
|
@@ -78,12 +84,14 @@ channel.close();
|
|
|
78
84
|
|
|
79
85
|
Every method takes `CallOptions` as its last argument; the same options can be given to the client constructor as defaults, and per-call `metadata` is merged over the defaults per key.
|
|
80
86
|
|
|
81
|
-
| Option
|
|
82
|
-
|
|
|
83
|
-
| `timeoutMs`
|
|
84
|
-
| `signal`
|
|
85
|
-
| `metadata`
|
|
86
|
-
| `maxFrameSize`
|
|
87
|
+
| Option | Meaning |
|
|
88
|
+
| -------------------- | --------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `timeoutMs` | Deadline for the whole call, from the request to the final status. Also sent to the server. |
|
|
90
|
+
| `signal` | `AbortSignal` that cancels the call. |
|
|
91
|
+
| `metadata` | Request metadata (`{ key: "value" }` or a `Map`); keys are lowercase, `trevrpc-` is reserved. |
|
|
92
|
+
| `maxFrameSize` | Largest frame accepted or sent in bytes (default 4 MiB). |
|
|
93
|
+
| `onResponseHeaders` | Called with the response headers. |
|
|
94
|
+
| `onResponseMetadata` | Called with the trailing metadata of a successful call. |
|
|
87
95
|
|
|
88
96
|
The client enforces no other response limits: there is no cap on the number of streamed
|
|
89
97
|
messages, their total size, or idle time between them. Use `timeoutMs` or `signal` to bound a
|
|
@@ -93,12 +101,22 @@ call.
|
|
|
93
101
|
|
|
94
102
|
- Failed calls throw `TrevRpcError` with a `code` (`Code.NotFound`, ...), `statusMessage` and `metadata`. A deadline surfaces as `Code.DeadlineExceeded`, a cancelled call as `Code.Cancelled` (with the abort reason as `cause`), and a lost connection as `Code.Unavailable`.
|
|
95
103
|
- Streaming responses end with the server's final status: iteration finishes after `OK` and throws the `TrevRpcError` otherwise. `stream.status` settles with the same outcome and carries trailing metadata. Leaving a `for await` loop early, or calling `close()`, cancels the call.
|
|
96
|
-
-
|
|
97
|
-
-
|
|
104
|
+
- Response headers and trailing metadata are separate. Every call reports them to the `onResponseHeaders(headers)` and `onResponseMetadata(metadata)` callbacks of its options: the headers (empty when the server sent none) as soon as they, the first message or the final status arrive, and the trailing metadata of the final status when the call succeeds, before its result is returned or its stream ends. A failed call carries its metadata on the `TrevRpcError`, and a callback that throws fails the call. Server-streaming and bidirectional calls also have a `headers` promise that resolves as soon as the headers, the first message or the final status arrive, so headers are available before the first message, and rejects with the call's error if the call fails first.
|
|
105
|
+
- Frames follow the [wire format](https://trev.zip/llc/TrevRPC/wiki/Protocol-and-Wire-Format): a type, a length and a payload, with messages sent as raw protobuf. The client skips frame types it does not know, and `maxFrameSize` limits every frame, unknown ones included.
|
|
106
|
+
- The server's response decides the outcome of a call, and a call reports it as soon as the final status arrives. If the server ends a call before the upload is done (client-streaming and bidirectional calls), the upload is cancelled and `send()` rejects at once, without waiting for the server to stop reading and whether or not the caller receives:
|
|
107
|
+
- with the server's `TrevRpcError` if its status is not OK, and the same error is the call's outcome (`closeAndRecv()`, `recv()`, `status`);
|
|
108
|
+
- with `Code.Cancelled` ("the server completed the call before the request stream ended") if its status is `OK`, for instance a server that answers after the first messages. The call itself succeeded: `closeAndRecv()` returns the response, and `recv()` and `status` end with `OK`;
|
|
109
|
+
- with the failure of the response stream, for instance `Code.Unavailable` for a lost connection, if the server stopped reading the request and the response then failed. A bare "stream closed" error from the transport is used only when the response gives no other explanation.
|
|
110
|
+
|
|
111
|
+
`send()` calls that are blocked on backpressure fail the same way, and so do all sends of a call that is cancelled or whose deadline expires. A deadline or abort that fires after the final status arrived does not change the outcome, even if the server has not finished the response stream yet.
|
|
112
|
+
|
|
113
|
+
`sendAll(requests)` sends every message of an iterable or async iterable and then closes the request stream. When the server ends the call first it stops and resolves, so `closeAndRecv()`, `recv()`, or `status` report the server's outcome instead of an upload error. Requests that throw cancel the call.
|
|
114
|
+
|
|
115
|
+
- To find the final status while the caller only sends, a client-streaming or bidirectional call reads the response ahead of `recv()` while its request side is open: at most 32 frames or 1 MiB (plus one frame of at most `maxFrameSize`) are held; beyond that reading pauses and flow control applies until `recv()` takes messages. Responses read ahead are delivered in order, including before an error that ended the stream.
|
|
98
116
|
|
|
99
117
|
### Connections
|
|
100
118
|
|
|
101
|
-
`connect()` opens a
|
|
119
|
+
`connect()` opens a session and resolves when it is ready; it rejects if no transport can establish one. After that the `Channel` re-establishes the session in the background with exponential backoff whenever it is lost. Calls fail fast with `Unavailable` while `channel.state` is `"reconnecting"` and are never replayed; use `await channel.waitUntilReady()` or the `onStateChange` option to follow the state. `channel.close()` stops reconnecting.
|
|
102
120
|
|
|
103
121
|
```js
|
|
104
122
|
const channel = await connect(url, {
|
|
@@ -108,21 +126,71 @@ const channel = await connect(url, {
|
|
|
108
126
|
});
|
|
109
127
|
```
|
|
110
128
|
|
|
111
|
-
`serverCertificateHashes` trusts a self-signed ECDSA certificate that is valid for at most 14 days by its SHA-256 hash, which is how development servers and the interop tests are reached. `allowPooling`, `congestionControl` and `requireUnreliable` are passed to the `WebTransport` constructor.
|
|
129
|
+
`serverCertificateHashes` trusts a self-signed ECDSA certificate that is valid for at most 14 days by its SHA-256 hash, which is how development servers and the interop tests are reached. It applies to WebTransport only: the WebSocket fallback needs a certificate the browser trusts. `allowPooling`, `congestionControl` and `requireUnreliable` are passed to the `WebTransport` constructor.
|
|
130
|
+
|
|
131
|
+
`connect()` always offers the WebTransport protocol `trevrpc-1` (wire version 1) through the `protocols` constructor option; it cannot be changed. If the server selects any other protocol, that connection attempt fails with `Code.FailedPrecondition` and the channel carries on as after any failed dial: the WebSocket fallback starts, or a reconnect is retried with backoff. An empty or missing selection means version 1, because older browsers cannot negotiate protocols and the Rust server cannot echo one.
|
|
132
|
+
|
|
133
|
+
Any object with `createBidirectionalStream()`, including a plain `WebTransport` session, can be used as the transport of a generated client. Create such a session with `new WebTransport(url, { protocols: ["trevrpc-1"] })` so the server knows the wire version. Requests name the method by its full path, `/<package.Service>/<Method>` (for example `/example.greeter.Greeter/SayHello`), which the generated clients build from the service and method names.
|
|
134
|
+
|
|
135
|
+
### Connection metadata
|
|
136
|
+
|
|
137
|
+
Metadata every call of a channel carries, such as a bearer token, can be set once on the channel instead of on each call. The channel sends it in full on the first calls of each session, until the server confirms that it stored it for the session, and refers to it by ID after that, so it crosses the connection once. The server merges it into each call's metadata, and a call's own `metadata` wins over an entry with the same key.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const channel = await connect(url, { connectionMetadata: { authorization: `Bearer ${token}` } });
|
|
141
|
+
channel.setConnectionMetadata({ authorization: `Bearer ${refreshed}` }); // calls started from now on
|
|
142
|
+
channel.setConnectionMetadata(); // removes it
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A changed set is sent in full again on every session, and so is the set on a session after a reconnect. Keys follow the rules of call metadata. The `Channel` is the only transport that supports it.
|
|
146
|
+
|
|
147
|
+
### Compression
|
|
148
|
+
|
|
149
|
+
Messages can travel compressed with zstd and a dictionary trained on them (see [Compression](https://trev.zip/llc/TrevRPC/wiki/Compression)). The codec lives in a separate entry point, `@trevrpc/trevrpc-js/zstd`, which loads libzstd as WebAssembly (about 85 KB gzipped), so applications without compression never load it:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import { createCompression } from "@trevrpc/trevrpc-js/zstd";
|
|
153
|
+
|
|
154
|
+
const compression = await createCompression({
|
|
155
|
+
dictionaries: [{ data: dictionaryBytes, messages: ["example.greeter.HelloRequest"] }],
|
|
156
|
+
});
|
|
157
|
+
const channel = await connect(url, { compression });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Generated clients pick each message's dictionary by its protobuf type. The channel asks each new session's server which dictionaries it holds, so a message whose dictionary the server lacks travels uncompressed, and request messages are compressed once the answer arrives. A transport other than a `Channel` compresses when it has a `compression` property with these settings.
|
|
161
|
+
|
|
162
|
+
Compression settings also enable stream compression, even without dictionaries (`createCompression()`): streaming methods marked `option (trevrpc.method) = {compression: COMPRESSION_STREAM}` compress each streamed direction with one zstd context per call, from its third message, so later messages compress against earlier ones. The contexts of a channel's calls may hold up to `streamCompressionMemory` bytes (64 MiB by default; 0 turns stream compression off).
|
|
163
|
+
|
|
164
|
+
### WebSocket fallback
|
|
165
|
+
|
|
166
|
+
`connect()` races two transports. WebTransport starts first; QMux over WebSocket (subprotocol `qmux-02.trevrpc-1`, served by Go and Rust on the server's TCP port) starts after `fallbackDelayMs` (default 300) or immediately if WebTransport fails or the browser has no `WebTransport` API. The first session to be ready wins and the other is closed. After the WebSocket wins, reconnects use it directly until `reprobeIntervalMs` (default 5 minutes) has passed since WebTransport was lost, then race again.
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
const channel = await connect("https://example.com:4433/trevrpc", {
|
|
170
|
+
transports: ["webtransport", "websocket"], // default; the first is preferred, a single entry disables the race
|
|
171
|
+
webSocketUrl: "wss://example.com:4433/trevrpc", // default: `url` with the wss: scheme
|
|
172
|
+
fallbackDelayMs: 300,
|
|
173
|
+
reprobeIntervalMs: 5 * 60_000,
|
|
174
|
+
onStateChange: ({ state, transport }) => console.log(state, transport),
|
|
175
|
+
});
|
|
176
|
+
console.log(channel.transport); // "webtransport" or "websocket"
|
|
177
|
+
```
|
|
112
178
|
|
|
113
|
-
|
|
179
|
+
`channel.transport` and `ChannelStateChange.transport` (set when the channel becomes ready) report which one is in use. `WebSocket` replaces the global WebSocket constructor, like `WebTransport` does for WebTransport. The QMux client is in-tree (`src/qmux/`); WebSocket backpressure polls `bufferedAmount` against a 1 MiB limit.
|
|
114
180
|
|
|
115
181
|
## Interop client
|
|
116
182
|
|
|
117
|
-
`trevrpc-interop-browser` runs the cross-runtime interop scenarios from [`interop/README.md`](../interop/README.md) in a real browser over WebTransport. It is built from `interop/proto/interop.proto` with the generator in this package, so it also serves as a complete example of generated client code.
|
|
183
|
+
`trevrpc-interop-browser` runs the cross-runtime interop scenarios from [`interop/README.md`](../interop/README.md) in a real browser over WebTransport, QMux over WebSocket, or the fallback between them. It is built from `interop/proto/interop.proto` with the generator in this package, so it also serves as a complete example of generated client code.
|
|
118
184
|
|
|
119
185
|
```sh
|
|
120
|
-
trevrpc-interop-browser --browser chromium|firefox|
|
|
186
|
+
trevrpc-interop-browser --browser chromium|firefox|safari \
|
|
121
187
|
--url https://127.0.0.1:4433/trevrpc \
|
|
122
|
-
--cert-hash <hex sha256 of the server certificate DER>
|
|
188
|
+
--cert-hash <hex sha256 of the server certificate DER> \
|
|
189
|
+
[--transport webtransport|websocket|auto] [--ws-url wss://127.0.0.1:4433/trevrpc] \
|
|
190
|
+
[--expect-transport webtransport|websocket]
|
|
123
191
|
```
|
|
124
192
|
|
|
125
|
-
It prints `ok <name>` or `fail <name>: <reason>` per scenario and exits 0 only if all scenarios passed.
|
|
193
|
+
It prints `ok <name>` or `fail <name>: <reason>` per scenario and exits 0 only if all scenarios passed. Chromium and Firefox are launched through Playwright from `PLAYWRIGHT_BROWSERS_PATH`, or from `TREVRPC_BROWSER_CHROMIUM` and `TREVRPC_BROWSER_FIREFOX` when those name an executable, and ignore certificate errors because WSS cannot pin a certificate hash. Safari is driven through `safaridriver` (`TREVRPC_BROWSER_SAFARI` overrides its path), which needs `sudo safaridriver --enable` once; Safari cannot ignore certificate errors, so the server certificate must be trusted by the system, as `interop/run.py` does. The CLI serves the page from a loopback origin; the page fetches its input from there and posts each result back, so where the browser has no WebTransport at all every scenario fails with a clear reason. `--write-page <dir>` writes that page to a directory instead, and `--page-url <url> --cert <pem> --key <pem>` opens it from a TrevRPC server that serves the directory through its HTTP fallback; the page then reports to the CLI over HTTPS, at the origin in its `report` query parameter. `--page-protocol h2|h3` has the browser load that page over HTTP/2 or HTTP/3 and fails every scenario unless it did (`h3` in Chromium only).
|
|
126
194
|
|
|
127
195
|
## Development
|
|
128
196
|
|
|
@@ -135,4 +203,4 @@ npm run interop -- --browser chromium --url ... --cert-hash ...
|
|
|
135
203
|
|
|
136
204
|
`nix build .#trevrpc-js` runs the same checks and installs the library, the plugin and the wrapped `trevrpc-interop-browser` with matching Playwright browsers. The pinned `playwright` version must equal nixpkgs' `playwright-driver`; `nix run .#sync-playwright` updates it.
|
|
137
205
|
|
|
138
|
-
Tests (`node --test`) cover the wire format against `testdata/wire-golden-vectors.txt`, framing, the generator, and the client — deadlines, cancellation, stream termination and reconnection — against in-memory fake servers, including a fake Echo server that runs all interop scenarios.
|
|
206
|
+
Tests (`node --test`) cover the wire format against `testdata/wire-golden-vectors.txt` (frame bytes, and the frame reader against the shared byte streams), the QMux frames against `testdata/qmux-golden-vectors.txt`, framing, the generator, and the client — deadlines, cancellation, stream termination and reconnection — against in-memory fake servers, including a fake Echo server that runs all interop scenarios.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trevrpc/trevrpc-js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Browser WebTransport client and protobuf.js code generator for TrevRPC",
|
|
5
5
|
"homepage": "https://trev.zip/llc/TrevRPC",
|
|
6
6
|
"bugs": {
|
|
@@ -26,34 +26,39 @@
|
|
|
26
26
|
".": {
|
|
27
27
|
"types": "./src/index.d.ts",
|
|
28
28
|
"import": "./src/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./zstd": {
|
|
31
|
+
"types": "./src/zstd.d.ts",
|
|
32
|
+
"import": "./src/zstd.js"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"publishConfig": {
|
|
32
36
|
"access": "public"
|
|
33
37
|
},
|
|
34
38
|
"scripts": {
|
|
35
|
-
"build": "node interop-browser/build.js",
|
|
39
|
+
"build": "node zstd-wasm/embed.js && node interop-browser/build.js",
|
|
36
40
|
"format": "oxfmt --write",
|
|
37
41
|
"format:check": "oxfmt --check",
|
|
38
42
|
"lint": "oxlint --deny-warnings",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"pretest": "npm run build",
|
|
42
|
-
"test": "node --test --test-concurrency=1 test/*.test.js",
|
|
43
|
+
"typecheck": "npm run build && tsc -p tsconfig.json",
|
|
44
|
+
"test": "npm run build && node --test --test-concurrency=1 test/*.test.js",
|
|
43
45
|
"check": "npm run format:check && npm run lint && npm run typecheck && npm test",
|
|
44
|
-
"interop": "node interop-browser/cli.js"
|
|
45
|
-
"prepack": "node -e 'if (process.env.TREVRPC_NIX_RELEASE !== \"1\") throw new Error(\"Build release tarballs with nix build .#trevrpc-js^npm\")'"
|
|
46
|
+
"interop": "node interop-browser/cli.js"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"protobufjs": "^8.8.1"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"
|
|
52
|
-
"
|
|
52
|
+
"@bufbuild/protobuf": "^2.16.0",
|
|
53
|
+
"@bufbuild/protoc-gen-es": "^2.16.0",
|
|
54
|
+
"@connectrpc/connect": "^2.3.0",
|
|
55
|
+
"@connectrpc/connect-web": "^2.3.0",
|
|
56
|
+
"playwright": "1.64.1",
|
|
57
|
+
"rolldown": "^1.2.16",
|
|
53
58
|
"typescript": "^7.0.2"
|
|
54
59
|
},
|
|
55
60
|
"engines": {
|
|
56
|
-
"node": ">=24
|
|
61
|
+
"node": ">=24"
|
|
57
62
|
},
|
|
58
|
-
"packageManager": "npm@12.2.
|
|
63
|
+
"packageManager": "npm@12.2.1"
|
|
59
64
|
}
|