@trevrpc/trevrpc-js 1.5.0 → 1.6.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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 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.
5
+ `trevrpc-js` is the **browser client**: a WebTransport runtime with a QMux over WebSocket fallback, for the messages and service descriptors that [protobuf-es](https://github.com/bufbuild/protobuf-es) generates. 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
 
@@ -24,61 +24,52 @@ message HelloRequest { string name = 1; }
24
24
  message HelloReply { string message = 1; }
25
25
  ```
26
26
 
27
- Generate a client with the protoc plugin (it needs Node.js to run):
27
+ TrevRPC has no code generator of its own for JavaScript: generate the messages and the service descriptor with protobuf-es's `protoc-gen-es`, and pass the descriptor to `createClient()`.
28
28
 
29
29
  ```sh
30
- npm install @trevrpc/trevrpc-js
30
+ npm install @trevrpc/trevrpc-js @bufbuild/protobuf
31
+ npm install --save-dev @bufbuild/protoc-gen-es
31
32
  protoc \
32
- --plugin=protoc-gen-trevrpc-js=node_modules/.bin/protoc-gen-trevrpc-js \
33
- --trevrpc-js_out=src/gen \
33
+ --plugin=protoc-gen-es=node_modules/.bin/protoc-gen-es \
34
+ --es_out=src/gen --es_opt=target=ts \
34
35
  greeter.proto
35
36
  ```
36
37
 
37
- This writes `greeter.trevrpc.js` and `greeter.trevrpc.d.ts`. Plugin parameters (`--trevrpc-js_out=key=value,key=value:dir`):
38
+ This writes `greeter_pb.ts` with `HelloRequestSchema`, `HelloReplySchema` and the `Greeter` service descriptor ([`buf generate`](https://buf.build/docs/generate/overview/) with the `buf.build/bufbuild/es` plugin works the same). `@bufbuild/protobuf` is a peer dependency, so the generated code and the client share one copy.
38
39
 
39
- | Parameter | Default | Meaning |
40
- | --------------------- | ------------------------ | ------------------------------------------------------------------------------------ |
41
- | `runtime_import` | `@trevrpc/trevrpc-js` | Module specifier the generated JavaScript imports. |
42
- | `runtime_type_import` | same as `runtime_import` | Module specifier the declarations import; required when `runtime_import` is a URL. |
43
- | `file_suffix` | `.trevrpc.js` | Suffix of the generated files. |
44
- | `int64` | `bigint` | How decoded 64-bit integers are represented: `bigint`, `number`, `string` or `long`. |
40
+ ### Messages
45
41
 
46
- ### Generated types
47
-
48
- The declarations cover the file's own messages and enums and every type they and its services use, including those of imported files. Each message has two shapes:
49
-
50
- - `Foo`, a decoded message, which calls return and streams yield. Every field reads a value: scalars and enums their default when unset, repeated fields and maps an empty array and object, and message and `optional` fields `null` when unset.
51
- - `FooInit`, a message to send, in which every field may be left out. Requests take it, and a decoded `Foo` can be sent as one.
52
-
53
- Each enum is exported as a frozen object of its values and as the type of those values. When every value starts with the enum's name in upper snake case, the prefix is dropped, as protobuf-es does: `TIME_INTERVAL_HOUR` of `TimeInterval` is `TimeInterval.HOUR`.
42
+ Messages are protobuf-es messages; see its [manual](https://github.com/bufbuild/protobuf-es/blob/main/MANUAL.md) for the details. Requests take a message or the init object `create()` takes, in which every field may be left out; calls return and streams yield messages, in which scalars, repeated fields and maps always have a value and message fields are `undefined` when unset. Oneofs are `{ case, value }`, 64-bit integers are `bigint` (or `string` with `[jstype = JS_STRING]`), `google.protobuf.Struct` is a JSON object, and `@bufbuild/protobuf/wkt` has helpers for the well-known types, such as `timestampFromDate` and `timestampDate`.
54
43
 
55
44
  ```ts
56
- import { TimeInterval, type Sample } from "./gen/metrics.trevrpc.js";
45
+ import { timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
46
+
47
+ import { Metrics, TimeInterval } from "./gen/metrics_pb.js";
57
48
 
58
- const sample: Sample = await client.get({ interval: TimeInterval.HOUR });
49
+ const metrics = createClient(Metrics, channel);
50
+ const sample = await metrics.get({
51
+ interval: TimeInterval.HOUR,
52
+ at: timestampFromDate(new Date()),
53
+ });
59
54
  sample.count; // bigint, never undefined
55
+ const at = sample.at && timestampDate(sample.at);
60
56
  ```
61
57
 
62
- 64-bit integers decode as `bigint` by default; requests take a `bigint`, a `number` or a decimal `string`. The `int64` plugin option picks `number` (exact up to 2^53), `string`, or `long` (protobuf.js `Long` objects, without the conversion) instead, and a field's `jstype` option (`[jstype = JS_STRING]` or `JS_NUMBER`) overrides it for that field. 64-bit map keys are decimal strings. `JSON.stringify` throws on a `bigint`, so a decoded message with a 64-bit field needs a replacer to serialize, such as `(key, value) => (typeof value === "bigint" ? value.toString() : value)`, or generate with `int64=string`.
63
-
64
- `google.protobuf.Timestamp`, `Duration`, `Empty`, `Struct`, `Value`, `ListValue` and `NullValue` are declared once by the runtime, which generated files import (Timestamp and Duration only with `bigint`, since the runtime declares their seconds as one). The runtime has helpers for them: `timestampFromDate`, `timestampFromMs`, `timestampDate`, `timestampMs`, `durationFromMs`, `durationMs`, `structFromJson`, `structToJson`, `valueFromJson` and `valueToJson`. The Timestamp and Duration readers take seconds in any representation.
58
+ `JSON.stringify` throws on a `bigint`; use protobuf-es's `toJson()` or `toJsonString()` to serialize a message.
65
59
 
66
- ```ts
67
- import { timestampDate, timestampFromDate } from "@trevrpc/trevrpc-js";
60
+ ### Method options
68
61
 
69
- await client.put({ at: timestampFromDate(new Date()) });
70
- const at = sample.at === null ? undefined : timestampDate(sample.at);
71
- ```
62
+ `createClient()` reads the `trevrpc.method` option of each method (from [`trevrpc/options.proto`](https://trev.zip/llc/TrevRPC/wiki/Getting-Started#schema-options)) from the descriptor, which protoc-gen-es keeps: `unreliable_requests` and `unreliable_responses` mark directions whose messages may travel as WebTransport datagrams ([Datagram Messages](https://trev.zip/llc/TrevRPC/wiki/Protocol-and-Wire-Format#datagram-messages)), and `compression` lets messages be compressed without a dictionary or a stream be compressed ([Compression](#compression)). An option the method's kind cannot use, such as `unreliable_requests` on a unary method or `COMPRESSION_STREAM` on one that streams nothing, makes `createClient()` throw `InvalidArgument`. Message types with a field marked `debug_redact` are never compressed that way.
72
63
 
73
64
  ## Client
74
65
 
75
66
  ```js
76
- import { connect } from "@trevrpc/trevrpc-js";
67
+ import { connect, createClient } from "@trevrpc/trevrpc-js";
77
68
 
78
- import { GreeterClient } from "./gen/greeter.trevrpc.js";
69
+ import { Greeter } from "./gen/greeter_pb.js";
79
70
 
80
71
  const channel = await connect("https://example.com:4433/trevrpc");
81
- const client = new GreeterClient(channel, { timeoutMs: 5_000 });
72
+ const client = createClient(Greeter, channel, { timeoutMs: 5_000 });
82
73
 
83
74
  // Unary
84
75
  const reply = await client.sayHello({ name: "TrevRPC" });
@@ -110,7 +101,7 @@ channel.close();
110
101
 
111
102
  ### Call options
112
103
 
113
- 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. The constructor also takes `interceptors` ([below](#interceptors)).
104
+ A client has one function per method, named by the method's `localName` (`SayHello` is `sayHello`). Every method takes `CallOptions` as its last argument; the same options can be given to `createClient()` as defaults, and per-call `metadata` is merged over the defaults per key. `createClient()` also takes `interceptors` ([below](#interceptors)).
114
105
 
115
106
  | Option | Meaning |
116
107
  | -------------------- | --------------------------------------------------------------------------------------------- |
@@ -188,7 +179,7 @@ try {
188
179
 
189
180
  `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.
190
181
 
191
- `createChannel(url, options)` takes the same options but returns the channel right away, in the `"connecting"` state, so generated clients can be created before it is connected. Calls started while it connects wait for that first attempt, within their own deadline and signal; if it fails they fail with its error, and the channel goes on reconnecting with backoff like after a lost session. The `signal` option does not apply to it; close the channel to give up.
182
+ `createChannel(url, options)` takes the same options but returns the channel right away, in the `"connecting"` state, so clients can be created before it is connected. Calls started while it connects wait for that first attempt, within their own deadline and signal; if it fails they fail with its error, and the channel goes on reconnecting with backoff like after a lost session. The `signal` option does not apply to it; close the channel to give up.
192
183
 
193
184
  ```js
194
185
  const channel = await connect(url, {
@@ -202,7 +193,7 @@ const channel = await connect(url, {
202
193
 
203
194
  `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.
204
195
 
205
- Any object with `createBidirectionalStream()`, including a plain `WebTransport` session, can be used as the transport of a generated client. A transport that wraps a `Channel` should forward its `createBidirectionalStreamWithSession()`, `compression`, `connectionMetadata`, `streamCompressionBudget` and `interceptors`; without them its calls carry no connection metadata, get a stream compression budget of their own, and skip the channel's interceptors. 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.
196
+ Any object with `createBidirectionalStream()`, including a plain `WebTransport` session, can be used as the transport of a client. A transport that wraps a `Channel` should forward its `createBidirectionalStreamWithSession()`, `compression`, `connectionMetadata`, `streamCompressionBudget` and `interceptors`; without them its calls carry no connection metadata, get a stream compression budget of their own, and skip the channel's interceptors. 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 clients build from the service and method names.
206
197
 
207
198
  ### Connection metadata
208
199
 
@@ -236,7 +227,7 @@ const channel = createChannel(url, {
236
227
  },
237
228
  ],
238
229
  });
239
- const client = new GreeterClient(channel, { interceptors: [logCalls] });
230
+ const client = createClient(Greeter, channel, { interceptors: [logCalls] });
240
231
  ```
241
232
 
242
233
  - An interceptor runs before the call starts, in order with the others, and may wait. It may add, replace and delete entries of `call.metadata`, which is validated after the last interceptor. The call's deadline starts after the interceptors; an interceptor that waits can watch `call.signal`.
@@ -246,7 +237,7 @@ const client = new GreeterClient(channel, { interceptors: [logCalls] });
246
237
 
247
238
  ### Effect
248
239
 
249
- The separate package [`@trevrpc/effect`](effect) wraps a generated client for [Effect](https://effect.website): `createEffectClient(GreeterService, new GreeterClient(channel))` returns a client whose unary calls are effects, whose server-streaming calls are streams, and whose client-streaming and bidirectional calls take their requests as a stream. Interrupting an effect or stream cancels its call. `TrevRpcError` has a `_tag` of `"TrevRpcError"`, so `Effect.catchTag` matches it.
240
+ The separate package [`@trevrpc/effect`](effect) makes clients for [Effect](https://effect.website): `createEffectClient(Greeter, channel)` returns a client whose unary calls are effects, whose server-streaming calls are streams, and whose client-streaming and bidirectional calls take their requests as a stream. Interrupting an effect or stream cancels its call. `TrevRpcError` has a `_tag` of `"TrevRpcError"`, so `Effect.catchTag` matches it.
250
241
 
251
242
  ### Compression
252
243
 
@@ -284,7 +275,7 @@ console.log(channel.transport); // "webtransport" or "websocket"
284
275
 
285
276
  ## Interop client
286
277
 
287
- `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.
278
+ `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 protoc-gen-es, so it also serves as a complete example of a client.
288
279
 
289
280
  ```sh
290
281
  trevrpc-interop-browser --browser chromium|firefox|safari \
@@ -301,10 +292,10 @@ It prints `ok <name>` or `fail <name>: <reason>` per scenario and exits 0 only i
301
292
  ```sh
302
293
  npm ci
303
294
  npm run check # format check, lint, typecheck, tests
304
- npm run build # generate the interop clients and bundle the interop and benchmark pages into build/interop/
295
+ npm run build # generate the interop and test descriptors and bundle the interop and benchmark pages into build/interop/
305
296
  npm run interop -- --browser chromium --url ... --cert-hash ...
306
297
  ```
307
298
 
308
- `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.
299
+ `nix build .#trevrpc-js` runs the same checks and installs the library 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.
309
300
 
310
- 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.
301
+ 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 `trevrpc.method` option, 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,7 +1,7 @@
1
1
  {
2
2
  "name": "@trevrpc/trevrpc-js",
3
- "version": "1.5.0",
4
- "description": "Browser WebTransport client and protobuf.js code generator for TrevRPC",
3
+ "version": "1.6.0",
4
+ "description": "Browser WebTransport client for TrevRPC, on protobuf-es",
5
5
  "homepage": "https://trev.zip/llc/TrevRPC",
6
6
  "bugs": {
7
7
  "url": "https://trev.zip/llc/TrevRPC/issues"
@@ -11,12 +11,8 @@
11
11
  "type": "git",
12
12
  "url": "https://trev.zip/llc/TrevRPC.git"
13
13
  },
14
- "bin": {
15
- "protoc-gen-trevrpc-js": "bin/protoc-gen-trevrpc-js.js"
16
- },
17
14
  "files": [
18
15
  "src/**",
19
- "bin/protoc-gen-trevrpc-js.js",
20
16
  "README.md",
21
17
  "LICENSE"
22
18
  ],
@@ -46,20 +42,20 @@
46
42
  "check": "npm run format:check && npm run lint && npm run typecheck && npm test",
47
43
  "interop": "node interop-browser/cli.js"
48
44
  },
49
- "dependencies": {
50
- "protobufjs": "^8.8.1"
51
- },
52
45
  "devDependencies": {
53
- "@bufbuild/protobuf": "^2.16.0",
54
- "@bufbuild/protoc-gen-es": "^2.16.0",
46
+ "@bufbuild/protobuf": "^2.17.0",
47
+ "@bufbuild/protoc-gen-es": "^2.17.0",
55
48
  "@connectrpc/connect": "^2.3.0",
56
49
  "@connectrpc/connect-web": "^2.3.0",
57
50
  "playwright": "1.64.1",
58
- "rolldown": "^1.2.16",
51
+ "rolldown": "^1.3.0",
59
52
  "typescript": "^7.0.2"
60
53
  },
54
+ "peerDependencies": {
55
+ "@bufbuild/protobuf": "^2.17.0"
56
+ },
61
57
  "engines": {
62
58
  "node": ">=24"
63
59
  },
64
- "packageManager": "npm@12.2.1"
60
+ "packageManager": "npm@12.2.2"
65
61
  }
package/src/call.js CHANGED
@@ -859,5 +859,5 @@ function timeoutField({ timeoutMs }) {
859
859
  if (nanos > MaxUint64) {
860
860
  throw invalidArgument("RPC timeout does not fit in 64-bit nanoseconds");
861
861
  }
862
- return { timeoutNanos: String(nanos === 0n ? 1n : nanos) };
862
+ return { timeoutNanos: nanos === 0n ? 1n : nanos };
863
863
  }
package/src/channel.js CHANGED
@@ -3,7 +3,14 @@ import { ConnectionMetadataSource } from "./connection-metadata.js";
3
3
  import { interceptorList } from "./interceptor.js";
4
4
  import { QMuxWebSocketSession } from "./qmux/session.js";
5
5
  import { dialRace } from "./race.js";
6
- import { cancelled, failedPrecondition, statusFromTransportError, unavailable } from "./status.js";
6
+ import {
7
+ Code,
8
+ TrevRpcError,
9
+ cancelled,
10
+ failedPrecondition,
11
+ statusFromTransportError,
12
+ unavailable,
13
+ } from "./status.js";
7
14
  import { StreamBudget } from "./stream-compression.js";
8
15
 
9
16
  /**
@@ -63,7 +70,7 @@ export class Channel {
63
70
  try {
64
71
  const failure = await channel.#connecting;
65
72
  if (signal?.aborted) {
66
- throw abortError(signal);
73
+ throw abortError(signal, failure);
67
74
  }
68
75
  if (failure !== undefined) {
69
76
  throw failure;
@@ -433,7 +440,13 @@ function dialWebSocket(url, options, signal) {
433
440
  * may reject the ready session by throwing.
434
441
  */
435
442
  async function establish(session, name, signal, verify) {
436
- const abandon = () => closeQuietly(session);
443
+ const startedAt = Date.now();
444
+ let abandoned;
445
+ const abandon = () => {
446
+ // Closing ends the session, so how far it got is recorded first.
447
+ abandoned = abandonedMessage(session, name, startedAt);
448
+ closeQuietly(session);
449
+ };
437
450
  signal?.addEventListener("abort", abandon, { once: true });
438
451
  try {
439
452
  // A session that fails to connect settles both promises; one that closes first never becomes
@@ -447,7 +460,10 @@ async function establish(session, name, signal, verify) {
447
460
  verify?.(session);
448
461
  } catch (error) {
449
462
  closeQuietly(session);
450
- throw signal?.aborted ? abortError(signal) : statusFromTransportError(error);
463
+ if (signal?.aborted) {
464
+ throw cancelled(abandoned ?? abandonedMessage(session, name, startedAt), signal.reason);
465
+ }
466
+ throw statusFromTransportError(error);
451
467
  } finally {
452
468
  signal?.removeEventListener("abort", abandon);
453
469
  }
@@ -468,8 +484,30 @@ function transportOptions(options) {
468
484
  return picked;
469
485
  }
470
486
 
471
- function abortError(signal) {
472
- return cancelled("connection cancelled", signal.reason);
487
+ /**
488
+ * The Cancelled error of a connection attempt that `signal` aborted. It keeps the message of
489
+ * `failure`, the attempt's own Cancelled error, which says how far the attempt got.
490
+ */
491
+ function abortError(signal, failure) {
492
+ const message =
493
+ failure instanceof TrevRpcError && failure.code === Code.Cancelled
494
+ ? failure.statusMessage
495
+ : "connection cancelled";
496
+ return cancelled(message, signal.reason);
497
+ }
498
+
499
+ /**
500
+ * Says how far a session abandoned `startedAt` (from Date.now()) got: a QMux over WebSocket
501
+ * session tells the WebSocket opening from the QMux handshake; a WebTransport session cannot.
502
+ */
503
+ function abandonedMessage(session, name, startedAt) {
504
+ const progress =
505
+ {
506
+ opening: "the WebSocket was still opening",
507
+ handshaking:
508
+ "the WebSocket was open, but the server had not sent its QMux transport parameters",
509
+ }[session.stage] ?? `the ${name} session was not ready`;
510
+ return `connection cancelled after ${Date.now() - startedAt} ms: ${progress}`;
473
511
  }
474
512
 
475
513
  /** Exponential backoff with jitter, capped at maxDelayMs. */
package/src/client.js CHANGED
@@ -1,16 +1,20 @@
1
1
  import { Call, EarlyOk, defaultCallOptions, mergeCallOptions } from "./call.js";
2
- import { FrameType, marshalMessage, unmarshalMessage } from "./framing.js";
3
- import { int64Converter } from "./int64.js";
2
+ import { FrameType, decodeWireMessage, marshalMessage, unmarshalMessage } from "./framing.js";
4
3
  import { interceptCall, interceptorList } from "./interceptor.js";
5
4
  import { validateMetadata } from "./metadata.js";
6
- import { Code, cancelled, internal, statusError } from "./status.js";
5
+ import { methodSettings } from "./method-options.js";
6
+ import { Code, cancelled, internal, invalidArgument, statusError } from "./status.js";
7
7
  import { RpcAnswer, RpcHeaders, RpcKind, RpcStatus } from "./wire.js";
8
8
 
9
9
  /**
10
- * Creates a service client from a generated service descriptor. Its calls run the interceptors of
11
- * the transport, then those of `options.interceptors`.
10
+ * Creates a client for `service`, a service descriptor that protoc-gen-es generates, with one
11
+ * function per method, named by the method's `localName`. Its calls run the interceptors of the
12
+ * transport, then those of `options.interceptors`.
12
13
  */
13
- export function createServiceClient(transport, service, root, options = {}) {
14
+ export function createClient(service, transport, options = {}) {
15
+ if (service?.kind !== "service" || !Array.isArray(service.methods)) {
16
+ throw invalidArgument("createClient needs a protobuf-es service descriptor");
17
+ }
14
18
  const { interceptors: clientInterceptors, ...callOptions } = options;
15
19
  const defaults = mergeCallOptions(defaultCallOptions(), callOptions);
16
20
  const interceptors = [
@@ -19,32 +23,26 @@ export function createServiceClient(transport, service, root, options = {}) {
19
23
  ];
20
24
  const client = {};
21
25
 
22
- for (const [jsName, method] of Object.entries(service.methods)) {
23
- const requestType = root.lookupType(method.inputType);
24
- const responseType = root.lookupType(method.outputType);
26
+ for (const method of service.methods) {
27
+ const settings = methodSettings(method);
25
28
  const rpc = {
26
29
  // The wire method path, computed once per method rather than per call.
27
- path: `/${service.fullName}/${method.name}`,
28
- requestType,
29
- responseType,
30
- // Converts the 64-bit integers of decoded responses, or undefined when they need nothing.
31
- convertResponse: int64Converter(responseType),
30
+ path: `/${service.typeName}/${method.name}`,
31
+ requestType: method.input,
32
+ responseType: method.output,
32
33
  // Full protobuf names, which pick the compression dictionaries.
33
- types: { request: fullName(requestType), response: fullName(responseType) },
34
- datagrams: datagramUse(method),
35
- compressStream: streamCompressionUse(method),
36
- compress: plainCompressionUse(method),
34
+ types: { request: method.input.typeName, response: method.output.typeName },
35
+ datagrams: datagramUse(settings),
36
+ compressStream: streamCompressionUse(settings),
37
+ compress: plainCompressionUse(settings),
37
38
  };
38
39
  const resolve = (override) => mergeCallOptions(defaults, override);
39
- const shape = MethodShapes[method.kind];
40
- if (shape === undefined) {
41
- throw internal(`unsupported generated RPC kind ${JSON.stringify(method.kind)}`);
42
- }
40
+ const shape = MethodShapes[settings.kind];
43
41
  const info = {
44
42
  method: rpc.path,
45
- service: service.fullName,
43
+ service: service.typeName,
46
44
  name: method.name,
47
- kind: method.kind,
45
+ kind: settings.kind,
48
46
  };
49
47
  const call =
50
48
  interceptors.length === 0
@@ -57,7 +55,7 @@ export function createServiceClient(transport, service, root, options = {}) {
57
55
  (options) => shape.start(transport, rpc, request, options),
58
56
  shape.ended,
59
57
  );
60
- client[jsName] = shape.takesRequest ? call : (override) => call(undefined, override);
58
+ client[method.localName] = shape.takesRequest ? call : (override) => call(undefined, override);
61
59
  }
62
60
 
63
61
  return client;
@@ -183,7 +181,7 @@ const ReadAheadBytes = 1 << 20;
183
181
  * How a method uses datagrams, or undefined if it does not: `send` for request messages, which
184
182
  * needs a streaming request and `unreliableRequests`, and `receive` for response messages, which
185
183
  * every call with a streaming response accepts. A method needs a flow if it marks either
186
- * direction unreliable; the generator rejects marks on a direction that does not stream.
184
+ * direction unreliable; methodSettings rejects marks on a direction that does not stream.
187
185
  */
188
186
  function datagramUse({ kind, unreliableRequests, unreliableResponses }) {
189
187
  const streamsRequest = kind === "clientStreaming" || kind === "bidirectionalStreaming";
@@ -196,7 +194,7 @@ function datagramUse({ kind, unreliableRequests, unreliableResponses }) {
196
194
 
197
195
  /**
198
196
  * The directions a method may stream-compress, `{ requests, responses }`, or undefined for none:
199
- * those its descriptor marks (the generator leaves out directions that do not stream and message
197
+ * those its settings mark (methodSettings leaves out directions that do not stream and message
200
198
  * types with redacted fields), except directions marked unreliable, whose messages mostly travel
201
199
  * as datagrams.
202
200
  */
@@ -208,7 +206,7 @@ function streamCompressionUse({ compressStreamRequests, compressStreamResponses,
208
206
 
209
207
  /**
210
208
  * The directions whose messages a method may compress without a dictionary, `{ requests,
211
- * responses }`, or undefined for none: those its descriptor marks (the generator marks both for
209
+ * responses }`, or undefined for none: those its settings mark (methodSettings marks both for
212
210
  * `COMPRESSION_MESSAGE` and `COMPRESSION_STREAM`, except message types with redacted fields).
213
211
  */
214
212
  function plainCompressionUse({ compressRequests, compressResponses }) {
@@ -418,8 +416,7 @@ export class BidirectionalStreamingCall {
418
416
  const item = await this.#take();
419
417
  if (item.payload !== undefined) {
420
418
  try {
421
- const message = unmarshalMessage(this.#rpc.responseType, item.payload);
422
- return this.#rpc.convertResponse?.(message) ?? message;
419
+ return unmarshalMessage(this.#rpc.responseType, item.payload);
423
420
  } catch (error) {
424
421
  throw this.#settleFailure(this.#call.fail(error));
425
422
  }
@@ -594,7 +591,7 @@ export class BidirectionalStreamingCall {
594
591
  );
595
592
  }
596
593
  this.#sawAnswer = true;
597
- this.#call.learnAnswers(unmarshalMessage(RpcAnswer, frame.payload));
594
+ this.#call.learnAnswers(decodeWireMessage(RpcAnswer, frame.payload));
598
595
  continue;
599
596
  }
600
597
  case FrameType.Headers: {
@@ -603,7 +600,7 @@ export class BidirectionalStreamingCall {
603
600
  this.#sawHeaders ? "duplicate HEADERS frame" : "HEADERS frame after MESSAGE",
604
601
  );
605
602
  }
606
- const headers = unmarshalMessage(RpcHeaders, frame.payload);
603
+ const headers = decodeWireMessage(RpcHeaders, frame.payload);
607
604
  validateResponseMetadata(headers.metadata, "headers");
608
605
  this.#sawHeaders = true;
609
606
  this.#settleHeaders(headers.metadata);
@@ -620,7 +617,7 @@ export class BidirectionalStreamingCall {
620
617
  return { type: FrameType.Message, payload };
621
618
  }
622
619
  case FrameType.Status: {
623
- const status = unmarshalMessage(RpcStatus, frame.payload);
620
+ const status = decodeWireMessage(RpcStatus, frame.payload);
624
621
  if (status.refusedStreamCompression && this.#call.replayable) {
625
622
  // The server refused the optimistic call before running anything for it: the call
626
623
  // continues on a replay, and its response is read from there.
@@ -791,11 +788,6 @@ function requestFields(rpc, kind) {
791
788
  return { method: rpc.path, kind };
792
789
  }
793
790
 
794
- /** The full name of a protobuf.js message type, without the leading dot. */
795
- function fullName(type) {
796
- return type.fullName.replace(/^\./u, "");
797
- }
798
-
799
791
  function validateResponseMetadata(metadata, what) {
800
792
  try {
801
793
  validateMetadata(metadata);
package/src/framing.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
2
+
1
3
  import { byteView } from "./bytes.js";
2
4
  import { internal, invalidArgument, resourceExhausted, unavailable } from "./status.js";
3
5
  import { readVarint, varintLength, writeVarint } from "./varint.js";
4
- import { RpcAnswer, RpcHeaders, RpcRequest, RpcStatus, preflightWireMessage } from "./wire.js";
6
+ import { RpcAnswer, RpcHeaders, RpcRequest, RpcStatus } from "./wire.js";
5
7
 
6
8
  export const DefaultMaxFrameSize = 4 * 1024 * 1024;
7
9
 
@@ -19,10 +21,13 @@ export const FrameType = Object.freeze({
19
21
  const KnownFrameTypes = new Set(Object.values(FrameType));
20
22
  const EmptyBytes = new Uint8Array(0);
21
23
 
22
- /** Encodes a protobuf message body. */
23
- export function marshalMessage(messageType, message) {
24
+ /**
25
+ * Encodes a protobuf message body: `message` is a message of `schema` (a protobuf-es message
26
+ * descriptor) or the init object `create()` takes.
27
+ */
28
+ export function marshalMessage(schema, message) {
24
29
  try {
25
- return messageType.encode(messageType.fromObject(message)).finish();
30
+ return toBinary(schema, create(schema, message));
26
31
  } catch (error) {
27
32
  throw invalidArgument(
28
33
  `failed to encode protobuf message: ${error?.message ?? String(error)}`,
@@ -31,11 +36,10 @@ export function marshalMessage(messageType, message) {
31
36
  }
32
37
  }
33
38
 
34
- /** Decodes a protobuf message body, rejecting malformed wire data. */
35
- export function unmarshalMessage(messageType, body) {
39
+ /** Decodes a protobuf message body of `schema`, rejecting malformed wire data. */
40
+ export function unmarshalMessage(schema, body) {
36
41
  try {
37
- preflightWireMessage(messageType, body);
38
- return messageType.decode(body);
42
+ return fromBinary(schema, byteView(body));
39
43
  } catch (error) {
40
44
  throw invalidArgument(
41
45
  `failed to decode protobuf message: ${error?.message ?? String(error)}`,
@@ -44,6 +48,30 @@ export function unmarshalMessage(messageType, body) {
44
48
  }
45
49
  }
46
50
 
51
+ /** Encodes a wire payload with its codec from wire.js, such as RpcStatus. */
52
+ export function encodeWireMessage(codec, fields) {
53
+ try {
54
+ return codec.encode(fields);
55
+ } catch (error) {
56
+ throw invalidArgument(
57
+ `failed to encode ${codec.name}: ${error?.message ?? String(error)}`,
58
+ error,
59
+ );
60
+ }
61
+ }
62
+
63
+ /** Decodes a wire payload with its codec from wire.js, rejecting malformed wire data. */
64
+ export function decodeWireMessage(codec, payload) {
65
+ try {
66
+ return codec.decode(byteView(payload));
67
+ } catch (error) {
68
+ throw invalidArgument(
69
+ `failed to decode ${codec.name}: ${error?.message ?? String(error)}`,
70
+ error,
71
+ );
72
+ }
73
+ }
74
+
47
75
  /** Encodes one frame carrying an already-encoded payload. */
48
76
  export function encodeFrame(type, payload, maxFrameSize = DefaultMaxFrameSize) {
49
77
  checkFrameLength(payload.byteLength, maxFrameSize);
@@ -71,7 +99,7 @@ export function encodeOutgoingFrame(message, maxFrameSize = DefaultMaxFrameSize)
71
99
  * encodeMessage, when it is given, in one buffer so both go out in a single write.
72
100
  */
73
101
  export function encodeRequestFrame(request, message, maxFrameSize = DefaultMaxFrameSize) {
74
- const payload = marshalMessage(RpcRequest, request);
102
+ const payload = encodeWireMessage(RpcRequest, request);
75
103
  checkFrameLength(payload.byteLength, maxFrameSize);
76
104
  if (message === undefined) {
77
105
  return encodeFrame(FrameType.Request, payload, maxFrameSize);
@@ -86,17 +114,17 @@ export function encodeRequestFrame(request, message, maxFrameSize = DefaultMaxFr
86
114
 
87
115
  /** Encodes a HEADERS frame from RpcHeaders fields. */
88
116
  export function encodeHeadersFrame(headers, maxFrameSize = DefaultMaxFrameSize) {
89
- return encodeFrame(FrameType.Headers, marshalMessage(RpcHeaders, headers), maxFrameSize);
117
+ return encodeFrame(FrameType.Headers, encodeWireMessage(RpcHeaders, headers), maxFrameSize);
90
118
  }
91
119
 
92
120
  /** Encodes an ANSWER frame from RpcAnswer fields. */
93
121
  export function encodeAnswerFrame(answer, maxFrameSize = DefaultMaxFrameSize) {
94
- return encodeFrame(FrameType.Answer, marshalMessage(RpcAnswer, answer), maxFrameSize);
122
+ return encodeFrame(FrameType.Answer, encodeWireMessage(RpcAnswer, answer), maxFrameSize);
95
123
  }
96
124
 
97
125
  /** Encodes a STATUS frame from RpcStatus fields. */
98
126
  export function encodeStatusFrame(status, maxFrameSize = DefaultMaxFrameSize) {
99
- return encodeFrame(FrameType.Status, marshalMessage(RpcStatus, status), maxFrameSize);
127
+ return encodeFrame(FrameType.Status, encodeWireMessage(RpcStatus, status), maxFrameSize);
100
128
  }
101
129
 
102
130
  /**