@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 +4 -4
- package/package.json +1 -1
- package/src/generator.js +27 -4
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
|
|
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
|
|
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
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
|
-
|
|
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 };
|