@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/src/index.d.ts CHANGED
@@ -1,139 +1,4 @@
1
- import type { INamespace, Root } from "protobufjs";
2
-
3
- export const protobuf: typeof import("protobufjs");
4
-
5
- /**
6
- * How decoded messages represent 64-bit integer fields: `bigint`, `number` (exact up to 2^53),
7
- * decimal `string`, or protobuf.js's `long` (a Long object). 64-bit map keys are decimal strings
8
- * in every representation.
9
- */
10
- export type Int64Representation = "bigint" | "number" | "string" | "long";
11
-
12
- export interface CreateRootOptions {
13
- /**
14
- * How calls decode the 64-bit integers of the root's messages; a field's `jstype` option
15
- * (`JS_STRING` or `JS_NUMBER`) overrides it. Without it they stay Long objects. Generated code
16
- * passes the generator's `int64` option.
17
- */
18
- int64?: Int64Representation;
19
- }
20
-
21
- /** Creates a protobuf root from a JSON namespace definition. */
22
- export function createRoot(json: INamespace, options?: CreateRootOptions): Root;
23
-
24
- /** A 64-bit integer in any representation, including a Long. */
25
- type Int64Value = bigint | number | string | { low: number; high: number; unsigned?: boolean };
26
-
27
- /** A decoded google.protobuf.Timestamp message. */
28
- export interface Timestamp {
29
- seconds: bigint;
30
- nanos: number;
31
- }
32
-
33
- /** A google.protobuf.Timestamp message to send. */
34
- export interface TimestampInit {
35
- seconds?: bigint | number | string;
36
- nanos?: number;
37
- }
38
-
39
- /** A decoded google.protobuf.Duration message. */
40
- export interface Duration {
41
- seconds: bigint;
42
- nanos: number;
43
- }
44
-
45
- /** A google.protobuf.Duration message to send. */
46
- export interface DurationInit {
47
- seconds?: bigint | number | string;
48
- nanos?: number;
49
- }
50
-
51
- /** A decoded google.protobuf.Empty message. */
52
- export interface Empty {}
53
-
54
- /** A google.protobuf.Empty message to send. */
55
- export interface EmptyInit {}
56
-
57
- /** Values of the google.protobuf.NullValue enum. */
58
- export const NullValue: Readonly<{ NULL_VALUE: 0 }>;
59
- /** A value of the google.protobuf.NullValue enum. */
60
- export type NullValue = (typeof NullValue)[keyof typeof NullValue];
61
-
62
- /** A decoded google.protobuf.Struct message: a JSON object; see `structToJson`. */
63
- export interface Struct {
64
- fields: Record<string, Value>;
65
- }
66
-
67
- /** A google.protobuf.Struct message to send; see `structFromJson`. */
68
- export interface StructInit {
69
- fields?: Record<string, ValueInit>;
70
- }
71
-
72
- /**
73
- * A decoded google.protobuf.Value message: a JSON value, which sets one member of the `kind`
74
- * oneof; see `valueToJson`.
75
- */
76
- export interface Value {
77
- nullValue: NullValue;
78
- numberValue: number;
79
- stringValue: string;
80
- boolValue: boolean;
81
- structValue: Struct | null;
82
- listValue: ListValue | null;
83
- }
84
-
85
- /** A google.protobuf.Value message to send; see `valueFromJson`. */
86
- export interface ValueInit {
87
- nullValue?: NullValue;
88
- numberValue?: number;
89
- stringValue?: string;
90
- boolValue?: boolean;
91
- structValue?: StructInit | null;
92
- listValue?: ListValueInit | null;
93
- }
94
-
95
- /** A decoded google.protobuf.ListValue message: a JSON array. */
96
- export interface ListValue {
97
- values: Value[];
98
- }
99
-
100
- /** A google.protobuf.ListValue message to send. */
101
- export interface ListValueInit {
102
- values?: ValueInit[];
103
- }
104
-
105
- export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
106
- export type JsonObject = { [key: string]: JsonValue };
107
-
108
- /** Returns the Timestamp of a Date, to the millisecond. */
109
- export function timestampFromDate(date: Date): Timestamp;
110
- /** Returns the Timestamp of milliseconds since the Unix epoch. */
111
- export function timestampFromMs(ms: number): Timestamp;
112
- /** Returns the Date of a Timestamp, whatever the representation of its seconds. */
113
- export function timestampDate(timestamp: {
114
- seconds?: Int64Value | null;
115
- nanos?: number | null;
116
- }): Date;
117
- /** Returns the milliseconds since the Unix epoch of a Timestamp, rounded down. */
118
- export function timestampMs(timestamp: {
119
- seconds?: Int64Value | null;
120
- nanos?: number | null;
121
- }): number;
122
- /** Returns the Duration of a number of milliseconds. */
123
- export function durationFromMs(ms: number): Duration;
124
- /** Returns the milliseconds of a Duration, rounded towards zero. */
125
- export function durationMs(duration: {
126
- seconds?: Int64Value | null;
127
- nanos?: number | null;
128
- }): number;
129
- /** Returns the Struct of a JSON object. */
130
- export function structFromJson(json: JsonObject): StructInit;
131
- /** Returns the JSON object of a Struct. */
132
- export function structToJson(struct: StructInit): JsonObject;
133
- /** Returns the Value of a JSON value. */
134
- export function valueFromJson(json: JsonValue): ValueInit;
135
- /** Returns the JSON value of a Value; one with no member of `kind` set is null. */
136
- export function valueToJson(value: ValueInit): JsonValue;
1
+ import type { DescMessage, DescService, MessageInitShape, MessageShape } from "@bufbuild/protobuf";
137
2
 
138
3
  export const Code: Readonly<{
139
4
  Ok: 0;
@@ -197,7 +62,7 @@ export interface CallOptions {
197
62
  onResponseMetadata?(metadata: Metadata): void;
198
63
  }
199
64
 
200
- /** The kind of an RPC, as in its generated descriptor. */
65
+ /** The kind of an RPC. */
201
66
  export type CallKind = "unary" | "serverStreaming" | "clientStreaming" | "bidirectionalStreaming";
202
67
 
203
68
  /** What a call interceptor sees of a call before it starts. */
@@ -334,55 +199,6 @@ export interface Transport {
334
199
  }>;
335
200
  }
336
201
 
337
- export type RpcMethodKind =
338
- | "unary"
339
- | "clientStreaming"
340
- | "serverStreaming"
341
- | "bidirectionalStreaming";
342
-
343
- export interface RpcMethodDescriptor {
344
- name: string;
345
- kind: RpcMethodKind;
346
- inputType: string;
347
- outputType: string;
348
- /**
349
- * The method marks its streamed request messages unreliable (`unreliable_requests` in the
350
- * `trevrpc.method` option). They are sent as WebTransport datagrams when they fit and the
351
- * session has datagrams, and may be lost or reordered; otherwise they travel on the stream.
352
- */
353
- unreliableRequests?: boolean;
354
- /**
355
- * The method marks its streamed response messages unreliable (`unreliable_responses`). The
356
- * client accepts them as datagrams on any session that has them.
357
- */
358
- unreliableResponses?: boolean;
359
- /**
360
- * The method's request messages may be stream-compressed (`compression: COMPRESSION_STREAM` in
361
- * the `trevrpc.method` option, on a method that streams requests, whose request type has no
362
- * field marked `debug_redact`). Calls on a transport with compression then compress them with one zstd
363
- * context per call, once the server agrees.
364
- */
365
- compressStreamRequests?: boolean;
366
- /** The method's response messages may be stream-compressed; see `compressStreamRequests`. */
367
- compressStreamResponses?: boolean;
368
- /**
369
- * The method's request messages may be compressed without a dictionary: one at a time, from 256
370
- * bytes, where no dictionary applies to their type and the server decodes it. The generator
371
- * sets it for `COMPRESSION_MESSAGE` and `COMPRESSION_STREAM`, except for message types with a
372
- * field marked `debug_redact`.
373
- */
374
- compressRequests?: boolean;
375
- /** The method's response messages may be compressed without a dictionary; see `compressRequests`. */
376
- compressResponses?: boolean;
377
- }
378
-
379
- export interface RpcServiceDescriptor {
380
- name: string;
381
- fullName: string;
382
- exportName: string;
383
- methods: Record<string, RpcMethodDescriptor>;
384
- }
385
-
386
202
  /** The final status of a call that ended with OK. */
387
203
  export interface StreamStatus {
388
204
  readonly code: StatusCode;
@@ -442,8 +258,8 @@ export interface ClientStreamingCall<TRequest, TResponse> {
442
258
  }
443
259
 
444
260
  /**
445
- * On a method that marks a direction unreliable (`RpcMethodDescriptor.unreliableRequests` or
446
- * `unreliableResponses`), a call on a session with datagrams may send its request messages, and
261
+ * On a method whose `trevrpc.method` option marks a direction unreliable (`unreliable_requests` or
262
+ * `unreliable_responses`), a call on a session with datagrams may send its request messages, and
447
263
  * receive its response messages, as datagrams: they can be lost or reordered, never duplicated.
448
264
  * Response datagrams are delivered after the response headers, merged with the stream's messages
449
265
  * in arrival order, and dropped when the call's queue is full (32 messages or 1 MiB) or the call
@@ -498,21 +314,62 @@ export interface BidirectionalStreamingCall<TRequest, TResponse> extends AsyncIt
498
314
  close(reason?: unknown): Promise<void>;
499
315
  }
500
316
 
501
- export type ServiceClient = Record<
502
- string,
503
- (requestOrOptions?: unknown, options?: CallOptions) => Promise<unknown>
504
- >;
317
+ /** A unary method of a client: sends one request and resolves with the response. */
318
+ export type UnaryMethod<I extends DescMessage, O extends DescMessage> = (
319
+ request: MessageInitShape<I>,
320
+ options?: CallOptions,
321
+ ) => Promise<MessageShape<O>>;
322
+
323
+ /** A server-streaming method of a client: sends one request and resolves with the responses. */
324
+ export type ServerStreamingMethod<I extends DescMessage, O extends DescMessage> = (
325
+ request: MessageInitShape<I>,
326
+ options?: CallOptions,
327
+ ) => Promise<ResponseStream<MessageShape<O>>>;
328
+
329
+ /** A client-streaming method of a client: starts a call to send requests on. */
330
+ export type ClientStreamingMethod<I extends DescMessage, O extends DescMessage> = (
331
+ options?: CallOptions,
332
+ ) => Promise<ClientStreamingCall<MessageInitShape<I>, MessageShape<O>>>;
333
+
334
+ /** A bidirectional-streaming method of a client: starts a call to send and receive on. */
335
+ export type BidirectionalStreamingMethod<I extends DescMessage, O extends DescMessage> = (
336
+ options?: CallOptions,
337
+ ) => Promise<BidirectionalStreamingCall<MessageInitShape<I>, MessageShape<O>>>;
338
+
339
+ /**
340
+ * The client of a protobuf-es service descriptor: one function per method, named by its
341
+ * `localName`. Requests take a message or the init object `create()` takes; responses are
342
+ * messages.
343
+ */
344
+ export type Client<S extends DescService> = {
345
+ [K in keyof S["method"]]: S["method"][K] extends {
346
+ methodKind: infer Kind;
347
+ input: infer I extends DescMessage;
348
+ output: infer O extends DescMessage;
349
+ }
350
+ ? Kind extends "unary"
351
+ ? UnaryMethod<I, O>
352
+ : Kind extends "server_streaming"
353
+ ? ServerStreamingMethod<I, O>
354
+ : Kind extends "client_streaming"
355
+ ? ClientStreamingMethod<I, O>
356
+ : Kind extends "bidi_streaming"
357
+ ? BidirectionalStreamingMethod<I, O>
358
+ : never
359
+ : never;
360
+ };
505
361
 
506
362
  /**
507
- * Creates a service client from a generated service descriptor. Its calls run the transport's
508
- * interceptors, then those of `options.interceptors`.
363
+ * Creates a client for `service`, a service descriptor that protoc-gen-es generates (such as
364
+ * `GreeterService` from `greeter_pb.js`). The `trevrpc.method` option of each method decides
365
+ * what its calls send as datagrams and compress; one the method's kind cannot use throws
366
+ * InvalidArgument. Calls run the transport's interceptors, then those of `options.interceptors`.
509
367
  */
510
- export function createServiceClient<TClient extends ServiceClient = ServiceClient>(
368
+ export function createClient<S extends DescService>(
369
+ service: S,
511
370
  transport: Transport,
512
- service: RpcServiceDescriptor,
513
- root: Root,
514
371
  options?: ClientOptions,
515
- ): TClient;
372
+ ): Client<S>;
516
373
 
517
374
  export interface WebTransportCertificateHash {
518
375
  algorithm: string;
package/src/index.js CHANGED
@@ -1,18 +1,4 @@
1
1
  export { fetchServerCertificateHashes } from "./certificate-hashes.js";
2
2
  export { Channel, connect, createChannel } from "./channel.js";
3
- export { createServiceClient } from "./client.js";
3
+ export { createClient } from "./client.js";
4
4
  export { Code, TrevRpcError, codeName } from "./status.js";
5
- export { createRoot, protobuf } from "./wire.js";
6
- export {
7
- NullValue,
8
- durationFromMs,
9
- durationMs,
10
- structFromJson,
11
- structToJson,
12
- timestampDate,
13
- timestampFromDate,
14
- timestampFromMs,
15
- timestampMs,
16
- valueFromJson,
17
- valueToJson,
18
- } from "./wkt.js";
@@ -0,0 +1,170 @@
1
+ // How a client calls each method of a protobuf-es service descriptor: the method's kind, and what
2
+ // its `(trevrpc.method)` option (proto/trevrpc/options.proto) lets it send as datagrams and
3
+ // compress. protoc-gen-es keeps the option in the descriptors it embeds, where protobuf-es leaves
4
+ // it as an unknown field of the method's options, so it is read from there without the generated
5
+ // extension.
6
+ import { BinaryReader, WireType } from "@bufbuild/protobuf/wire";
7
+
8
+ import { invalidArgument } from "./status.js";
9
+
10
+ /** The field number of the `trevrpc.method` extension of google.protobuf.MethodOptions. */
11
+ const MethodOptionField = 58247;
12
+
13
+ /** `trevrpc.Compression` values. */
14
+ const Compression = Object.freeze({ Unspecified: 0, Message: 1, Stream: 2 });
15
+
16
+ /** The kind of each protobuf-es `methodKind`, as calls and interceptors name it. */
17
+ const Kinds = Object.freeze({
18
+ __proto__: null,
19
+ unary: "unary",
20
+ server_streaming: "serverStreaming",
21
+ client_streaming: "clientStreaming",
22
+ bidi_streaming: "bidirectionalStreaming",
23
+ });
24
+
25
+ /**
26
+ * The call settings of `method`, a protobuf-es DescMethod: `{ kind, unreliableRequests,
27
+ * unreliableResponses, compressRequests, compressResponses, compressStreamRequests,
28
+ * compressStreamResponses }`, with each flag true only where the option sets it for a direction
29
+ * that allows it. An option the method's kind cannot use throws InvalidArgument.
30
+ */
31
+ export function methodSettings(method) {
32
+ const kind = Kinds[method.methodKind];
33
+ const name = method.parent?.typeName ? `${method.parent.typeName}.${method.name}` : method.name;
34
+ if (kind === undefined) {
35
+ throw invalidArgument(`${name}: unsupported RPC kind ${JSON.stringify(method.methodKind)}`);
36
+ }
37
+ const options = trevrpcMethodOptions(method, name);
38
+ return {
39
+ kind,
40
+ ...unreliableDirections(name, kind, options),
41
+ ...compressionDirections(name, kind, options, method),
42
+ };
43
+ }
44
+
45
+ /** The method's `(trevrpc.method)` option, with unset fields false or zero. */
46
+ function trevrpcMethodOptions(method, name) {
47
+ const options = { unreliableRequests: false, unreliableResponses: false, compression: 0 };
48
+ for (const field of method.proto?.options?.$unknown ?? []) {
49
+ if (field.no !== MethodOptionField) {
50
+ continue;
51
+ }
52
+ try {
53
+ if (field.wireType !== WireType.LengthDelimited) {
54
+ throw new Error(`wire type ${field.wireType}`);
55
+ }
56
+ // A message-typed option may be split over several fields, which merge in order.
57
+ const reader = new BinaryReader(new BinaryReader(field.data).bytes());
58
+ while (reader.pos < reader.len) {
59
+ const [number, wireType] = reader.tag();
60
+ if (number === 1 || number === 2 || number === 5) {
61
+ if (wireType !== WireType.Varint) {
62
+ throw new Error(`field ${number} has wire type ${wireType}`);
63
+ }
64
+ if (number === 5) {
65
+ options.compression = reader.int32();
66
+ } else {
67
+ options[number === 1 ? "unreliableRequests" : "unreliableResponses"] = reader.bool();
68
+ }
69
+ } else {
70
+ reader.skip(wireType, number);
71
+ }
72
+ }
73
+ } catch (error) {
74
+ throw invalidArgument(`${name}: malformed trevrpc.method option: ${error.message}`, error);
75
+ }
76
+ }
77
+ return options;
78
+ }
79
+
80
+ /** The `unreliable_*` flags that are set; one on a direction that does not stream throws. */
81
+ function unreliableDirections(name, kind, options) {
82
+ const directions = {};
83
+ if (options.unreliableRequests) {
84
+ if (kind !== "clientStreaming" && kind !== "bidirectionalStreaming") {
85
+ throw invalidArgument(
86
+ `${name}: unreliable_requests requires a client-streaming or bidirectional method`,
87
+ );
88
+ }
89
+ directions.unreliableRequests = true;
90
+ }
91
+ if (options.unreliableResponses) {
92
+ if (kind !== "serverStreaming" && kind !== "bidirectionalStreaming") {
93
+ throw invalidArgument(
94
+ `${name}: unreliable_responses requires a server-streaming or bidirectional method`,
95
+ );
96
+ }
97
+ directions.unreliableResponses = true;
98
+ }
99
+ return directions;
100
+ }
101
+
102
+ /**
103
+ * The directions `compression` lets compress, leaving out message types with a field marked
104
+ * `debug_redact`. `COMPRESSION_MESSAGE` compresses messages without a dictionary in both
105
+ * directions. `COMPRESSION_STREAM` does too, for the messages outside the stream, and
106
+ * stream-compresses the directions that stream; on a unary method it throws, as does a value this
107
+ * runtime does not know.
108
+ */
109
+ function compressionDirections(name, kind, { compression }, method) {
110
+ if (compression === Compression.Unspecified) {
111
+ return {};
112
+ }
113
+ if (compression !== Compression.Message && compression !== Compression.Stream) {
114
+ throw invalidArgument(`${name}: unknown compression ${compression}`);
115
+ }
116
+ if (compression === Compression.Stream && kind === "unary") {
117
+ throw invalidArgument(`${name}: COMPRESSION_STREAM requires a streaming method`);
118
+ }
119
+ const requests = !hasRedactedField(method.input);
120
+ const responses = !hasRedactedField(method.output);
121
+ const directions = {};
122
+ if (requests) {
123
+ directions.compressRequests = true;
124
+ }
125
+ if (responses) {
126
+ directions.compressResponses = true;
127
+ }
128
+ if (compression === Compression.Stream) {
129
+ if (requests && (kind === "clientStreaming" || kind === "bidirectionalStreaming")) {
130
+ directions.compressStreamRequests = true;
131
+ }
132
+ if (responses && (kind === "serverStreaming" || kind === "bidirectionalStreaming")) {
133
+ directions.compressStreamResponses = true;
134
+ }
135
+ }
136
+ return directions;
137
+ }
138
+
139
+ /** Whether each message type checked so far has a redacted field. */
140
+ const redacted = new WeakMap();
141
+
142
+ /**
143
+ * Whether `message`, a DescMessage, has a field marked `debug_redact`, directly or in a
144
+ * message-typed field (singular, repeated or a map value), however deeply nested.
145
+ */
146
+ export function hasRedactedField(message) {
147
+ let found = redacted.get(message);
148
+ if (found !== undefined) {
149
+ return found;
150
+ }
151
+ // Every message type reachable from this one; a cycle is visited once.
152
+ const reachable = new Set([message]);
153
+ found = false;
154
+ for (const type of reachable) {
155
+ for (const field of type.fields) {
156
+ if (field.proto?.options?.debugRedact === true) {
157
+ found = true;
158
+ break;
159
+ }
160
+ if (field.message !== undefined) {
161
+ reachable.add(field.message);
162
+ }
163
+ }
164
+ if (found) {
165
+ break;
166
+ }
167
+ }
168
+ redacted.set(message, found);
169
+ return found;
170
+ }
@@ -69,6 +69,8 @@ export class QMuxWebSocketSession {
69
69
  /** The highest QX_PING request sequence received, -1 before the first: a peer may start at 0. */
70
70
  #pingReceived = -1;
71
71
  #closedError = null;
72
+ /** Whether the WebSocket opened with the QMux subprotocol. */
73
+ #open = false;
72
74
  #lastActivity = Date.now();
73
75
  #idleTimer;
74
76
  #handshakeTimer;
@@ -110,6 +112,20 @@ export class QMuxWebSocketSession {
110
112
  );
111
113
  }
112
114
 
115
+ /**
116
+ * How far the session got: "opening" until the WebSocket opens, "handshaking" until the server's
117
+ * transport parameters arrive, "ready" after them, and "closed" once the session ended.
118
+ */
119
+ get stage() {
120
+ if (this.#closedError) {
121
+ return "closed";
122
+ }
123
+ if (this.#peer !== null) {
124
+ return "ready";
125
+ }
126
+ return this.#open ? "handshaking" : "opening";
127
+ }
128
+
113
129
  /** Opens a bidirectional stream, waiting for the server's stream limit. */
114
130
  async createBidirectionalStream() {
115
131
  await this.ready;
@@ -154,6 +170,7 @@ export class QMuxWebSocketSession {
154
170
  );
155
171
  return;
156
172
  }
173
+ this.#open = true;
157
174
  // The transport parameters must be the first frame on the wire.
158
175
  writeTransportParameters(this.#pending, {
159
176
  initialMaxData: this.#window,