@trevrpc/trevrpc-js 0.2.3 → 1.1.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.
Files changed (47) hide show
  1. package/README.md +127 -46
  2. package/package.json +12 -45
  3. package/src/bytes.js +10 -0
  4. package/src/call.js +520 -0
  5. package/src/channel.js +258 -288
  6. package/src/client.js +543 -1254
  7. package/src/datagram.js +336 -0
  8. package/src/framing.js +151 -548
  9. package/src/generator.js +66 -109
  10. package/src/index.d.ts +235 -463
  11. package/src/index.js +4 -3
  12. package/src/qmux/frames.js +262 -0
  13. package/src/qmux/params.js +104 -0
  14. package/src/qmux/session.js +739 -0
  15. package/src/race.js +90 -0
  16. package/src/status.js +24 -70
  17. package/src/varint.js +73 -0
  18. package/src/wire.js +21 -89
  19. package/examples/greeter/README.md +0 -35
  20. package/examples/greeter/client.js +0 -172
  21. package/examples/greeter/greeter.node.trevrpc.d.ts +0 -53
  22. package/examples/greeter/greeter.node.trevrpc.js +0 -9
  23. package/examples/greeter/greeter.proto +0 -18
  24. package/examples/greeter/greeter.trevrpc.d.ts +0 -55
  25. package/examples/greeter/greeter.trevrpc.js +0 -103
  26. package/examples/greeter/index.html +0 -140
  27. package/examples/greeter/static-server.js +0 -168
  28. package/src/advanced.d.ts +0 -29
  29. package/src/advanced.js +0 -1
  30. package/src/browser-channel.js +0 -49
  31. package/src/connect.js +0 -6
  32. package/src/connect.node.js +0 -6
  33. package/src/index.node.d.ts +0 -10
  34. package/src/index.node.js +0 -3
  35. package/src/native-loader.js +0 -118
  36. package/src/node-advanced.d.ts +0 -27
  37. package/src/node-advanced.js +0 -1
  38. package/src/node-generated.d.ts +0 -24
  39. package/src/node-generated.js +0 -126
  40. package/src/node-index.d.ts +0 -1
  41. package/src/node-index.js +0 -7
  42. package/src/node.d.ts +0 -298
  43. package/src/node.js +0 -1456
  44. package/src/runtime.js +0 -66
  45. package/src/server-response.js +0 -49
  46. package/src/timer.js +0 -29
  47. package/src/webtransport.js +0 -639
package/src/race.js ADDED
@@ -0,0 +1,90 @@
1
+ import { cancelled, statusFromTransportError, unavailable } from "./status.js";
2
+
3
+ /**
4
+ * Dials `primary`, adds `fallback` once the primary fails or `delayMs` passes, and resolves with
5
+ * `{ session, transport }` from the first to connect. The loser is abandoned and closed if it
6
+ * connects anyway. Each candidate is `{ transport, dial(signal) }`.
7
+ */
8
+ export function dialRace(primary, fallback, { delayMs, signal } = {}) {
9
+ return new Promise((resolve, reject) => {
10
+ const attempts = new Map();
11
+ const errors = [];
12
+ let settled = false;
13
+ let fallbackStarted = false;
14
+ let timer;
15
+
16
+ const settle = (winner) => {
17
+ settled = true;
18
+ clearTimeout(timer);
19
+ signal?.removeEventListener("abort", onAbort);
20
+ for (const [candidate, controller] of attempts) {
21
+ if (candidate !== winner) {
22
+ controller.abort();
23
+ }
24
+ }
25
+ };
26
+ const start = (candidate) => {
27
+ const controller = new AbortController();
28
+ attempts.set(candidate, controller);
29
+ let dialing;
30
+ try {
31
+ // Dialing starts synchronously, so a session exists as soon as the race begins.
32
+ dialing = Promise.resolve(candidate.dial(controller.signal));
33
+ } catch (error) {
34
+ dialing = Promise.reject(error);
35
+ }
36
+ dialing.then(
37
+ (session) => {
38
+ if (settled) {
39
+ closeQuietly(session);
40
+ return;
41
+ }
42
+ settle(candidate);
43
+ resolve({ session, transport: candidate.transport });
44
+ },
45
+ (error) => {
46
+ attempts.delete(candidate);
47
+ if (settled) {
48
+ return;
49
+ }
50
+ errors.push(`${candidate.transport}: ${statusFromTransportError(error).message}`);
51
+ if (candidate === primary) {
52
+ startFallback();
53
+ } else if (attempts.size === 0) {
54
+ settle(null);
55
+ reject(unavailable(`transport unavailable: ${errors.join("; ")}`));
56
+ }
57
+ },
58
+ );
59
+ };
60
+ const startFallback = () => {
61
+ if (!fallbackStarted && !settled) {
62
+ fallbackStarted = true;
63
+ clearTimeout(timer);
64
+ start(fallback);
65
+ }
66
+ };
67
+ const onAbort = () => {
68
+ if (!settled) {
69
+ settle(null);
70
+ reject(cancelled("connection cancelled", signal.reason));
71
+ }
72
+ };
73
+
74
+ if (signal?.aborted) {
75
+ onAbort();
76
+ return;
77
+ }
78
+ signal?.addEventListener("abort", onAbort, { once: true });
79
+ start(primary);
80
+ timer = setTimeout(startFallback, delayMs);
81
+ });
82
+ }
83
+
84
+ function closeQuietly(session) {
85
+ try {
86
+ session.close();
87
+ } catch {
88
+ // The session may already be gone.
89
+ }
90
+ }
package/src/status.js CHANGED
@@ -18,82 +18,38 @@ export const Code = Object.freeze({
18
18
  Unauthenticated: 16,
19
19
  });
20
20
 
21
- const CODE_NAMES = Object.freeze([
22
- "Ok",
23
- "Cancelled",
24
- "Unknown",
25
- "InvalidArgument",
26
- "DeadlineExceeded",
27
- "NotFound",
28
- "AlreadyExists",
29
- "PermissionDenied",
30
- "ResourceExhausted",
31
- "FailedPrecondition",
32
- "Aborted",
33
- "OutOfRange",
34
- "Unimplemented",
35
- "Internal",
36
- "Unavailable",
37
- "DataLoss",
38
- "Unauthenticated",
39
- ]);
21
+ const CodeNames = Object.freeze(Object.keys(Code));
40
22
 
41
23
  /** Error carrying a TrevRPC status code, message, and metadata. */
42
24
  export class TrevRpcError extends Error {
43
25
  /** Creates a TrevRPC status error. */
44
26
  constructor(code, message = "", metadata = {}, options = {}) {
45
27
  const normalizedCode = codeFromNumber(code);
46
- super(message === "" ? codeName(normalizedCode) : `${codeName(normalizedCode)}: ${message}`, {
47
- cause: options.cause,
48
- });
28
+ const name = codeName(normalizedCode);
29
+ super(
30
+ message === "" ? name : `${name}: ${message}`,
31
+ options.cause === undefined ? undefined : { cause: options.cause },
32
+ );
49
33
  this.name = "TrevRpcError";
50
34
  this.code = normalizedCode;
51
35
  this.statusMessage = message;
52
36
  this.metadata = metadata;
53
- if (Number.isInteger(options.nativeCode)) {
54
- this.nativeCode = options.nativeCode;
55
- }
56
- }
57
- }
58
-
59
- /** Resource-exhausted status reported when a frame exceeds the configured size limit. */
60
- export class FrameTooLargeError extends TrevRpcError {
61
- /** Creates a frame size error. */
62
- constructor(length, max) {
63
- super(Code.ResourceExhausted, `frame length ${length} exceeds maximum ${max}`);
64
- this.name = "FrameTooLargeError";
65
- this.length = length;
66
- this.max = max;
67
37
  }
68
38
  }
69
39
 
70
40
  /** Converts a number into a known status code, defaulting unknown values to Unknown. */
71
41
  export function codeFromNumber(code) {
72
- return Number.isInteger(code) && code >= 0 && code < CODE_NAMES.length ? code : Code.Unknown;
42
+ return Number.isInteger(code) && code >= 0 && code < CodeNames.length ? code : Code.Unknown;
73
43
  }
74
44
 
75
45
  /** Returns the canonical status code name. */
76
46
  export function codeName(code) {
77
- return CODE_NAMES[codeFromNumber(code)];
47
+ return CodeNames[codeFromNumber(code)];
78
48
  }
79
49
 
80
50
  /** Creates a TrevRPC status error. */
81
- export function statusError(code, message = "", metadata = {}) {
82
- return new TrevRpcError(code, message, metadata);
83
- }
84
-
85
- /** Builds a status error from an RPC response. */
86
- export function statusFromResponse(response) {
87
- if (response == null) {
88
- return statusError(Code.Internal, "missing RPC response");
89
- }
90
-
91
- return statusError(response.status ?? Code.Ok, response.message ?? "", response.metadata ?? {});
92
- }
93
-
94
- /** Reports whether a status-like value is OK. */
95
- export function isOkStatus(status) {
96
- return status != null && codeFromNumber(status.code) === Code.Ok;
51
+ export function statusError(code, message = "", metadata = {}, cause) {
52
+ return new TrevRpcError(code, message, metadata, { cause });
97
53
  }
98
54
 
99
55
  /** Creates an internal error status. */
@@ -102,8 +58,13 @@ export function internal(message) {
102
58
  }
103
59
 
104
60
  /** Creates an invalid-argument status. */
105
- export function invalidArgument(message) {
106
- return statusError(Code.InvalidArgument, message);
61
+ export function invalidArgument(message, cause) {
62
+ return statusError(Code.InvalidArgument, message, {}, cause);
63
+ }
64
+
65
+ /** Creates a failed-precondition status. */
66
+ export function failedPrecondition(message) {
67
+ return statusError(Code.FailedPrecondition, message);
107
68
  }
108
69
 
109
70
  /** Creates a deadline-exceeded status. */
@@ -112,8 +73,8 @@ export function deadlineExceeded(message) {
112
73
  }
113
74
 
114
75
  /** Creates an unavailable status. */
115
- export function unavailable(message) {
116
- return statusError(Code.Unavailable, message);
76
+ export function unavailable(message, cause) {
77
+ return statusError(Code.Unavailable, message, {}, cause);
117
78
  }
118
79
 
119
80
  /** Creates a resource-exhausted status. */
@@ -122,24 +83,17 @@ export function resourceExhausted(message) {
122
83
  }
123
84
 
124
85
  /** Creates a cancelled status. */
125
- export function cancelled(message) {
126
- return statusError(Code.Cancelled, message);
86
+ export function cancelled(message, cause) {
87
+ return statusError(Code.Cancelled, message, {}, cause);
127
88
  }
128
89
 
129
- /** Converts a transport error into a TrevRPC status error. */
90
+ /** Converts a transport failure into a status error; status errors pass through. */
130
91
  export function statusFromTransportError(error) {
131
92
  if (error instanceof TrevRpcError) {
132
93
  return error;
133
94
  }
134
-
135
95
  if (error?.name === "AbortError") {
136
- return new TrevRpcError(Code.Cancelled, "transport closed locally", {}, { cause: error });
96
+ return cancelled("transport closed locally", error);
137
97
  }
138
-
139
- return new TrevRpcError(
140
- Code.Unavailable,
141
- `transport unavailable: ${error?.message ?? String(error)}`,
142
- {},
143
- { cause: error },
144
- );
98
+ return unavailable(`transport unavailable: ${error?.message ?? String(error)}`, error);
145
99
  }
package/src/varint.js ADDED
@@ -0,0 +1,73 @@
1
+ // QUIC variable-length integers (RFC 9000 §16), shared by the frame layer and QMux. Values below
2
+ // 2^53 are numbers; larger codepoints are bigints.
3
+
4
+ const MaxSafe = BigInt(Number.MAX_SAFE_INTEGER);
5
+
6
+ /** Encoded length of a varint. */
7
+ export function varintLength(value) {
8
+ if (typeof value === "number" && value >= 0 && value < 0x4000_0000) {
9
+ return value < 0x40 ? 1 : value < 0x4000 ? 2 : 4;
10
+ }
11
+ const v = typeof value === "bigint" ? value : BigInt(value);
12
+ if (v < 0x40n) {
13
+ return 1;
14
+ }
15
+ if (v < 0x4000n) {
16
+ return 2;
17
+ }
18
+ if (v < 0x4000_0000n) {
19
+ return 4;
20
+ }
21
+ if (v < 0x4000_0000_0000_0000n) {
22
+ return 8;
23
+ }
24
+ throw new RangeError(`varint ${v} exceeds 2^62 - 1`);
25
+ }
26
+
27
+ /** Writes a varint at offset and returns the offset after it. */
28
+ export function writeVarint(bytes, offset, value) {
29
+ const length = varintLength(value);
30
+ if (typeof value === "number" && length < 8 && Number.isInteger(value)) {
31
+ // Fast path without BigInt for the common small values.
32
+ let v = value;
33
+ for (let i = length - 1; i >= 0; i -= 1) {
34
+ bytes[offset + i] = v % 256;
35
+ v = Math.floor(v / 256);
36
+ }
37
+ bytes[offset] |= [0, 0x40, 0, 0x80][length - 1];
38
+ return offset + length;
39
+ }
40
+ let v = typeof value === "bigint" ? value : BigInt(value);
41
+ for (let i = length - 1; i >= 0; i -= 1) {
42
+ bytes[offset + i] = Number(v & 0xffn);
43
+ v >>= 8n;
44
+ }
45
+ bytes[offset] |= [0, 0x40, 0, 0x80, 0, 0, 0, 0xc0][length - 1];
46
+ return offset + length;
47
+ }
48
+
49
+ /**
50
+ * Reads a varint at offset. Returns `{ value, next }`, or null when the bytes end first. Values
51
+ * above 2^53 - 1 are bigints.
52
+ */
53
+ export function readVarint(bytes, offset) {
54
+ if (offset >= bytes.length) {
55
+ return null;
56
+ }
57
+ const length = 1 << (bytes[offset] >> 6);
58
+ if (offset + length > bytes.length) {
59
+ return null;
60
+ }
61
+ if (length < 8) {
62
+ let value = bytes[offset] & 0x3f;
63
+ for (let i = 1; i < length; i += 1) {
64
+ value = value * 256 + bytes[offset + i];
65
+ }
66
+ return { value, next: offset + length };
67
+ }
68
+ let value = BigInt(bytes[offset] & 0x3f);
69
+ for (let i = 1; i < 8; i += 1) {
70
+ value = (value << 8n) | BigInt(bytes[offset + i]);
71
+ }
72
+ return { value: value <= MaxSafe ? Number(value) : value, next: offset + 8 };
73
+ }
package/src/wire.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import protobuf from "protobufjs";
2
2
 
3
- export { protobuf };
3
+ import { byteView } from "./bytes.js";
4
4
 
5
- export const WireVersion = 1;
5
+ export { protobuf };
6
6
 
7
7
  export const RpcKind = Object.freeze({
8
8
  Unary: 0,
@@ -11,12 +11,7 @@ export const RpcKind = Object.freeze({
11
11
  BidirectionalStreaming: 3,
12
12
  });
13
13
 
14
- export const RpcStreamFrameKind = Object.freeze({
15
- Message: 0,
16
- Status: 1,
17
- });
18
-
19
- export const wireRoot = protobuf.Root.fromJSON({
14
+ const wireRoot = protobuf.Root.fromJSON({
20
15
  nested: {
21
16
  trevrpc: {
22
17
  nested: {
@@ -28,38 +23,32 @@ export const wireRoot = protobuf.Root.fromJSON({
28
23
  BidirectionalStreaming: 3,
29
24
  },
30
25
  },
31
- RpcStreamFrameKind: {
32
- values: {
33
- Message: 0,
34
- Status: 1,
35
- },
36
- },
37
26
  RpcRequest: {
38
27
  fields: {
39
- service: { type: "string", id: 1 },
40
- method: { type: "string", id: 2 },
41
- body: { type: "bytes", id: 3 },
28
+ method: { type: "string", id: 1 },
42
29
  metadata: { keyType: "string", type: "bytes", id: 4 },
43
30
  kind: { type: "RpcKind", id: 5 },
44
- version: { type: "uint32", id: 6 },
45
31
  timeoutNanos: { type: "uint64", id: 7 },
32
+ extensions: { rule: "repeated", type: "uint64", id: 8 },
33
+ datagramFlow: { type: "uint64", id: 9 },
46
34
  },
35
+ // Fields 2 (method name), 3 (request body) and 6 (wire version) must never be reused.
36
+ reserved: [
37
+ [2, 3],
38
+ [6, 6],
39
+ ],
47
40
  },
48
- RpcResponse: {
41
+ RpcHeaders: {
49
42
  fields: {
50
- status: { type: "uint32", id: 1 },
51
- message: { type: "string", id: 2 },
52
- body: { type: "bytes", id: 3 },
53
- metadata: { keyType: "string", type: "bytes", id: 4 },
43
+ metadata: { keyType: "string", type: "bytes", id: 1 },
44
+ extensions: { rule: "repeated", type: "uint64", id: 2 },
54
45
  },
55
46
  },
56
- RpcStreamFrame: {
47
+ RpcStatus: {
57
48
  fields: {
58
- kind: { type: "RpcStreamFrameKind", id: 1 },
59
- status: { type: "uint32", id: 2 },
60
- message: { type: "string", id: 3 },
61
- body: { type: "bytes", id: 4 },
62
- metadata: { keyType: "string", type: "bytes", id: 5 },
49
+ code: { type: "uint32", id: 1 },
50
+ message: { type: "string", id: 2 },
51
+ metadata: { keyType: "string", type: "bytes", id: 3 },
63
52
  },
64
53
  },
65
54
  },
@@ -68,71 +57,24 @@ export const wireRoot = protobuf.Root.fromJSON({
68
57
  });
69
58
 
70
59
  export const RpcRequest = wireRoot.lookupType("trevrpc.RpcRequest");
71
- export const RpcResponse = wireRoot.lookupType("trevrpc.RpcResponse");
72
- export const RpcStreamFrame = wireRoot.lookupType("trevrpc.RpcStreamFrame");
60
+ export const RpcHeaders = wireRoot.lookupType("trevrpc.RpcHeaders");
61
+ export const RpcStatus = wireRoot.lookupType("trevrpc.RpcStatus");
73
62
 
74
63
  const MaxFieldNumber = 0x1fffffff;
75
64
  const MaxUint32 = 0xffffffffn;
76
65
  const MaxUint64 = 0xffffffffffffffffn;
77
66
  const fatalUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
78
67
 
79
- /** Internal wire-validation failure used by production codec paths. */
80
- export class WireValidationError extends Error {
81
- constructor(reason, message) {
82
- super(message);
83
- this.name = "WireValidationError";
84
- this.reason = reason;
85
- }
86
- }
87
-
88
68
  /** Checks protobuf tags and known-field wire types before protobuf.js decoding. */
89
69
  export function preflightWireMessage(messageType, body) {
90
70
  scanMessage(messageType, byteView(body), 0);
91
71
  }
92
72
 
93
- /** Validates TrevRPC protocol enums and versions after protobuf decoding. */
94
- export function validateWireMessage(messageType, message) {
95
- if (messageType === RpcRequest) {
96
- if (message.version !== WireVersion) {
97
- throw new WireValidationError(
98
- "unsupported_wire_version",
99
- `unsupported wire version ${message.version}`,
100
- );
101
- }
102
- if (!Object.values(RpcKind).includes(message.kind)) {
103
- throw new WireValidationError("unsupported_rpc_kind", `unsupported RPC kind ${message.kind}`);
104
- }
105
- return;
106
- }
107
-
108
- if (
109
- messageType === RpcStreamFrame &&
110
- message.kind !== RpcStreamFrameKind.Message &&
111
- message.kind !== RpcStreamFrameKind.Status
112
- ) {
113
- throw new WireValidationError(
114
- "unsupported_frame_kind",
115
- `unsupported stream frame kind ${message.kind}`,
116
- );
117
- }
118
- }
119
-
120
73
  /** Creates a protobuf root from a JSON namespace definition. */
121
74
  export function createRoot(json) {
122
75
  return protobuf.Root.fromJSON(json);
123
76
  }
124
77
 
125
- /** Creates a stream message frame carrying a protobuf body. */
126
- export function messageFrame(body) {
127
- return {
128
- kind: RpcStreamFrameKind.Message,
129
- status: 0,
130
- message: "",
131
- body,
132
- metadata: {},
133
- };
134
- }
135
-
136
78
  function scanMessage(messageType, bytes, depth) {
137
79
  if (depth > 64) {
138
80
  throw malformedWire("protobuf message nesting exceeded limit");
@@ -389,15 +331,5 @@ function validateUtf8(bytes, name) {
389
331
  }
390
332
 
391
333
  function malformedWire(message) {
392
- return new WireValidationError("malformed_protobuf", message);
393
- }
394
-
395
- function byteView(value) {
396
- if (value instanceof Uint8Array) {
397
- return value;
398
- }
399
- if (ArrayBuffer.isView(value)) {
400
- return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
401
- }
402
- return new Uint8Array(value ?? 0);
334
+ return new Error(message);
403
335
  }
@@ -1,35 +0,0 @@
1
- # Greeter example
2
-
3
- This directory contains the generated Milestone 6 split:
4
-
5
- - `greeter.trevrpc.js` and `.d.ts`: protobuf root, descriptors, typed clients, and factories; browser-safe.
6
- - `greeter.node.trevrpc.js` and `.d.ts`: typed Node handler interfaces and `registerGreeterServer`.
7
-
8
- Browser code must import only `greeter.trevrpc.js`. Node servers import registration from `greeter.node.trevrpc.js` and use `createUnaryResponse` or `createStreamingResponse` when returning metadata or a custom terminal status. A direct protobuf object such as `{ message: "hello" }` remains a protobuf response.
9
-
10
- ## Run the browser page
11
-
12
- From `trevrpc-js`:
13
-
14
- ```sh
15
- npm run example:greeter
16
- ```
17
-
18
- Open `http://127.0.0.1:8080/examples/greeter/` and use a WebTransport greeter endpoint:
19
-
20
- ```text
21
- https://127.0.0.1:50051/trevrpc # Go default
22
- https://127.0.0.1:5000/trevrpc # Rust default
23
- ```
24
-
25
- The path selects the WebTransport session; TrevRPC routing remains inside the request frame. The example policies allow origin `http://127.0.0.1:8080` and use bearer token `trevrpc-example-token`.
26
-
27
- Both example servers write their development certificate to `~/.config/trevrpc/trevrpc-example-cert.pem`. Override it with `TREVRPC_EXAMPLE_CERT`. The static server computes the SHA-256 DER hash and the browser client passes it through `serverCertificateHashes`.
28
-
29
- For a temporary self-signed certificate, calculate the hash with:
30
-
31
- ```sh
32
- openssl x509 -in server.pem -outform der | openssl dgst -sha256 -binary | openssl base64
33
- ```
34
-
35
- The client fully consumes server streams, then reads terminal metadata from `stream.status`. It calls `close()` when abandoning streams. Client `timeoutMs` is absolute across setup, upload, and receive, while `streamIdleTimeoutMs` is a separate inactivity bound.
@@ -1,172 +0,0 @@
1
- import { TrevRpcError, connect } from "../../src/index.js";
2
- import { GreeterClient } from "./greeter.trevrpc.js";
3
-
4
- const form = document.querySelector("#client-form");
5
- const output = document.querySelector("#output");
6
- const submit = document.querySelector("#submit");
7
-
8
- void loadExampleDefaults();
9
-
10
- form.addEventListener("submit", (event) => {
11
- event.preventDefault();
12
- void runFromForm(new FormData(form));
13
- });
14
-
15
- for (const preset of document.querySelectorAll("[data-url]")) {
16
- preset.addEventListener("click", () => {
17
- form.elements.url.value = preset.dataset.url;
18
- });
19
- }
20
-
21
- async function runFromForm(data) {
22
- output.textContent = "";
23
- submit.disabled = true;
24
-
25
- const url = String(data.get("url") ?? "").trim();
26
- const name = String(data.get("name") ?? "TrevRPC").trim() || "TrevRPC";
27
- const token = String(data.get("token") ?? "").trim();
28
- const certificateHash = String(data.get("certificate-hash") ?? "").trim();
29
-
30
- let channel;
31
- try {
32
- log(`connecting to ${url}`);
33
- channel = await connect(url, {
34
- ...browserConnectOptions(certificateHash),
35
- timeoutMs: 10_000,
36
- });
37
- const client = new GreeterClient(channel, {
38
- timeoutMs: 5_000,
39
- metadata: token === "" ? {} : { authorization: `Bearer ${token}` },
40
- });
41
-
42
- const unary = await client.sayHello({ name });
43
- log(`SayHello: ${unary.message}`);
44
-
45
- const replies = await client.lotsOfReplies({ name });
46
- for await (const reply of replies) {
47
- log(`LotsOfReplies: ${reply.message}`);
48
- }
49
- const replyStatus = await replies.status;
50
- log(
51
- `LotsOfReplies status: ${replyStatus.code} ${replyStatus.message}; trailers=${Object.keys(replyStatus.metadata).join(",")}`,
52
- );
53
-
54
- const greetings = await client.lotsOfGreetings();
55
- await greetings.send({ name: `${name} client stream 1` });
56
- await greetings.send({ name: `${name} client stream 2` });
57
- const summary = await greetings.closeAndRecvWithResponse();
58
- log(
59
- `LotsOfGreetings: ${summary.message.message}; trailers=${Object.keys(summary.metadata).join(",")}`,
60
- );
61
-
62
- const bidi = await client.bidiHello();
63
- try {
64
- for (const suffix of ["bidi 1", "bidi 2"]) {
65
- await bidi.send({ name: `${name} ${suffix}` });
66
-
67
- const reply = await bidi.recv();
68
- if (reply == null) {
69
- throw new Error("BidiHello response stream ended early");
70
- }
71
- log(`BidiHello: ${reply.message}`);
72
- }
73
- await bidi.closeSend();
74
-
75
- for (;;) {
76
- const reply = await bidi.recv();
77
- if (reply == null) {
78
- break;
79
- }
80
- log(`BidiHello: ${reply.message}`);
81
- }
82
- } finally {
83
- await bidi.close();
84
- }
85
-
86
- log("complete");
87
- } catch (error) {
88
- logError(error);
89
- } finally {
90
- submit.disabled = false;
91
- channel?.close({ closeCode: 0, reason: "example complete" });
92
- }
93
- }
94
-
95
- function browserConnectOptions(certificateHash) {
96
- if (certificateHash === "") {
97
- return {};
98
- }
99
-
100
- return {
101
- serverCertificateHashes: [
102
- {
103
- algorithm: "sha-256",
104
- value: parseSha256Hash(certificateHash),
105
- },
106
- ],
107
- };
108
- }
109
-
110
- async function loadExampleDefaults() {
111
- try {
112
- const response = await fetch("./certificate-hash.json", { cache: "no-store" });
113
- if (!response.ok) {
114
- return;
115
- }
116
-
117
- const config = await response.json();
118
- if (typeof config.bearerToken === "string" && form.elements.token.value === "") {
119
- form.elements.token.value = config.bearerToken;
120
- }
121
- if (config.exists && typeof config.sha256Base64 === "string") {
122
- form.elements["certificate-hash"].value = config.sha256Base64;
123
- log(`loaded certificate hash from ${config.path}`);
124
- } else if (typeof config.path === "string") {
125
- log(`certificate not found at ${config.path}; start a Go or Rust example server first`);
126
- }
127
- } catch (error) {
128
- log(`could not load local certificate hash: ${error?.message ?? String(error)}`);
129
- }
130
- }
131
-
132
- function parseSha256Hash(value) {
133
- const normalized = value
134
- .trim()
135
- .replace(/^sha-?256[=:]/i, "")
136
- .trim();
137
- const hex = normalized.replace(/[:\s]/g, "");
138
-
139
- if (/^[0-9a-fA-F]{64}$/.test(hex)) {
140
- const bytes = new Uint8Array(32);
141
- for (let index = 0; index < bytes.length; index += 1) {
142
- bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
143
- }
144
- return bytes;
145
- }
146
-
147
- const base64 = normalized.replace(/-/g, "+").replace(/_/g, "/");
148
- const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
149
- const binary = atob(padded);
150
- if (binary.length !== 32) {
151
- throw new Error("certificate hash must decode to 32 bytes of SHA-256 data");
152
- }
153
-
154
- const bytes = new Uint8Array(binary.length);
155
- for (let index = 0; index < binary.length; index += 1) {
156
- bytes[index] = binary.charCodeAt(index);
157
- }
158
- return bytes;
159
- }
160
-
161
- function log(message) {
162
- output.textContent += `${message}\n`;
163
- }
164
-
165
- function logError(error) {
166
- if (error instanceof TrevRpcError) {
167
- log(`RPC error: ${error.code}: ${error.statusMessage || error.message}`);
168
- return;
169
- }
170
-
171
- log(`error: ${error?.message ?? String(error)}`);
172
- }