@trevrpc/trevrpc-js 1.2.0 → 1.3.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
@@ -128,7 +128,7 @@ const channel = await connect(url, {
128
128
 
129
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
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.
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.
132
132
 
133
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
134
 
@@ -187,17 +187,17 @@ trevrpc-interop-browser --browser chromium|firefox|safari \
187
187
  --url https://127.0.0.1:4433/trevrpc \
188
188
  --cert-hash <hex sha256 of the server certificate DER> \
189
189
  [--transport webtransport|websocket|auto] [--ws-url wss://127.0.0.1:4433/trevrpc] \
190
- [--expect-transport webtransport|websocket]
190
+ [--expect-transport webtransport|websocket] [--dictionary <file>]
191
191
  ```
192
192
 
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).
193
+ It prints `ok <name>` or `fail <name>: <reason>` per scenario and exits 0 only if all scenarios passed. `--dictionary` makes the channel compress Echo messages with that zstd dictionary, the one the interop servers hold. `trevrpc-interop-browser bench` runs the load benchmark in a browser instead (see [`bench/README.md`](../bench/README.md#browsers)). 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).
194
194
 
195
195
  ## Development
196
196
 
197
197
  ```sh
198
198
  npm ci
199
199
  npm run check # format check, lint, typecheck, tests
200
- npm run build # generate the Echo client and bundle the interop page into build/interop/
200
+ npm run build # generate the interop clients and bundle the interop and benchmark pages into build/interop/
201
201
  npm run interop -- --browser chromium --url ... --cert-hash ...
202
202
  ```
203
203
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevrpc/trevrpc-js",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": {
package/src/generator.js CHANGED
@@ -185,6 +185,9 @@ const FieldLabel = Object.freeze({
185
185
  Repeated: 3,
186
186
  });
187
187
 
188
+ /** The CodeGeneratorResponse features this generator supports. */
189
+ const FEATURE_PROTO3_OPTIONAL = 1;
190
+
188
191
  /** Generic runtime types that generated declarations import only when they use them. */
189
192
  const GeneratedTypeImports = [
190
193
  "BidirectionalStreamingCall",
@@ -262,9 +265,9 @@ export function generate(request) {
262
265
  });
263
266
  }
264
267
 
265
- return { file: files };
268
+ return { file: files, supportedFeatures: FEATURE_PROTO3_OPTIONAL };
266
269
  } catch (error) {
267
- return { error: error.message };
270
+ return { error: error.message, supportedFeatures: FEATURE_PROTO3_OPTIONAL };
268
271
  }
269
272
  }
270
273
 
@@ -605,12 +608,25 @@ function generateMessageDeclaration({ fullName, message }, typeNames) {
605
608
 
606
609
  function generateFieldDeclaration(field, mapEntries, typeNames, currentFullName) {
607
610
  const optional = field.label === FieldLabel.Required ? "" : "?";
611
+ const nullable = decodesAbsentAsNull(field) ? " | null" : "";
608
612
  return ` ${typescriptPropertyName(fieldJsonName(field))}${optional}: ${fieldTypeScriptType(
609
613
  field,
610
614
  mapEntries,
611
615
  typeNames,
612
616
  currentFullName,
613
- )};`;
617
+ )}${nullable};`;
618
+ }
619
+
620
+ /** Reports whether protobuf.js decodes the field as null when it is absent. */
621
+ function decodesAbsentAsNull(field) {
622
+ if (field.label === FieldLabel.Repeated) {
623
+ return false;
624
+ }
625
+ return (
626
+ field.proto3Optional === true ||
627
+ field.type === FieldType.Message ||
628
+ field.type === FieldType.Group
629
+ );
614
630
  }
615
631
 
616
632
  function fieldTypeScriptType(field, mapEntries, typeNames, currentFullName) {
@@ -786,6 +802,11 @@ function fieldToJson(field, mapEntries) {
786
802
  if (field.defaultValue != null && field.defaultValue !== "") {
787
803
  json.options = { default: field.defaultValue };
788
804
  }
805
+ if (field.proto3Optional) {
806
+ // protobuf.js gives the field presence, decoding it as null when absent,
807
+ // only when this option marks its synthetic oneof.
808
+ json.options = { ...json.options, proto3_optional: true };
809
+ }
789
810
 
790
811
  return json;
791
812
  }
@@ -806,7 +827,9 @@ function oneofsToJson(message) {
806
827
  const oneofs = {};
807
828
  for (const [index, oneof] of (message.oneofDecl ?? []).entries()) {
808
829
  const fields = (message.field ?? [])
809
- .filter((field) => field.oneofIndex === index)
830
+ // A decoded descriptor reads an unset oneof_index as 0 from its
831
+ // prototype, so only an own property places the field in a oneof.
832
+ .filter((field) => Object.hasOwn(field, "oneofIndex") && field.oneofIndex === index)
810
833
  .map(fieldJsonName);
811
834
  if (fields.length > 0) {
812
835
  oneofs[oneof.name] = { oneof: fields };