@trevrpc/trevrpc-js 1.4.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;
@@ -166,6 +31,8 @@ export type Metadata = Record<string, Uint8Array>;
166
31
 
167
32
  /** Error carrying a TrevRPC status code, message, and metadata. */
168
33
  export class TrevRpcError extends Error {
34
+ /** Always `"TrevRpcError"`, for matching errors by tag, as Effect's `catchTag` does. */
35
+ readonly _tag: "TrevRpcError";
169
36
  readonly code: StatusCode;
170
37
  readonly statusMessage: string;
171
38
  readonly metadata: Metadata;
@@ -195,7 +62,7 @@ export interface CallOptions {
195
62
  onResponseMetadata?(metadata: Metadata): void;
196
63
  }
197
64
 
198
- /** The kind of an RPC, as in its generated descriptor. */
65
+ /** The kind of an RPC. */
199
66
  export type CallKind = "unary" | "serverStreaming" | "clientStreaming" | "bidirectionalStreaming";
200
67
 
201
68
  /** What a call interceptor sees of a call before it starts. */
@@ -332,55 +199,6 @@ export interface Transport {
332
199
  }>;
333
200
  }
334
201
 
335
- export type RpcMethodKind =
336
- | "unary"
337
- | "clientStreaming"
338
- | "serverStreaming"
339
- | "bidirectionalStreaming";
340
-
341
- export interface RpcMethodDescriptor {
342
- name: string;
343
- kind: RpcMethodKind;
344
- inputType: string;
345
- outputType: string;
346
- /**
347
- * The method marks its streamed request messages unreliable (`unreliable_requests` in the
348
- * `trevrpc.method` option). They are sent as WebTransport datagrams when they fit and the
349
- * session has datagrams, and may be lost or reordered; otherwise they travel on the stream.
350
- */
351
- unreliableRequests?: boolean;
352
- /**
353
- * The method marks its streamed response messages unreliable (`unreliable_responses`). The
354
- * client accepts them as datagrams on any session that has them.
355
- */
356
- unreliableResponses?: boolean;
357
- /**
358
- * The method's request messages may be stream-compressed (`compression: COMPRESSION_STREAM` in
359
- * the `trevrpc.method` option, on a method that streams requests, whose request type has no
360
- * field marked `debug_redact`). Calls on a transport with compression then compress them with one zstd
361
- * context per call, once the server agrees.
362
- */
363
- compressStreamRequests?: boolean;
364
- /** The method's response messages may be stream-compressed; see `compressStreamRequests`. */
365
- compressStreamResponses?: boolean;
366
- /**
367
- * The method's request messages may be compressed without a dictionary: one at a time, from 256
368
- * bytes, where no dictionary applies to their type and the server decodes it. The generator
369
- * sets it for `COMPRESSION_MESSAGE` and `COMPRESSION_STREAM`, except for message types with a
370
- * field marked `debug_redact`.
371
- */
372
- compressRequests?: boolean;
373
- /** The method's response messages may be compressed without a dictionary; see `compressRequests`. */
374
- compressResponses?: boolean;
375
- }
376
-
377
- export interface RpcServiceDescriptor {
378
- name: string;
379
- fullName: string;
380
- exportName: string;
381
- methods: Record<string, RpcMethodDescriptor>;
382
- }
383
-
384
202
  /** The final status of a call that ended with OK. */
385
203
  export interface StreamStatus {
386
204
  readonly code: StatusCode;
@@ -391,6 +209,9 @@ export interface StreamStatus {
391
209
  /**
392
210
  * Responses of a server-streaming call. Iteration ends after the final OK status and throws a
393
211
  * TrevRpcError for any other outcome. Leaving the loop early cancels the call.
212
+ *
213
+ * Buffered messages are delivered without returning to the event loop, so a page that renders a
214
+ * long stream must yield to let the browser render; see "Rendering long streams" in the guide.
394
215
  */
395
216
  export interface ResponseStream<TResponse> extends AsyncIterable<TResponse> {
396
217
  /** Settles with the final status, or rejects with the error that ended the call. */
@@ -437,12 +258,15 @@ export interface ClientStreamingCall<TRequest, TResponse> {
437
258
  }
438
259
 
439
260
  /**
440
- * On a method that marks a direction unreliable (`RpcMethodDescriptor.unreliableRequests` or
441
- * `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
442
263
  * receive its response messages, as datagrams: they can be lost or reordered, never duplicated.
443
264
  * Response datagrams are delivered after the response headers, merged with the stream's messages
444
265
  * in arrival order, and dropped when the call's queue is full (32 messages or 1 MiB) or the call
445
266
  * has ended. Without datagrams, such as over WebSocket, everything stays on the stream.
267
+ *
268
+ * Like a `ResponseStream`, iteration delivers buffered messages without returning to the event
269
+ * loop.
446
270
  */
447
271
  export interface BidirectionalStreamingCall<TRequest, TResponse> extends AsyncIterable<TResponse> {
448
272
  /** Settles with the final status, or rejects with the error that ended the call. */
@@ -490,21 +314,62 @@ export interface BidirectionalStreamingCall<TRequest, TResponse> extends AsyncIt
490
314
  close(reason?: unknown): Promise<void>;
491
315
  }
492
316
 
493
- export type ServiceClient = Record<
494
- string,
495
- (requestOrOptions?: unknown, options?: CallOptions) => Promise<unknown>
496
- >;
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
+ };
497
361
 
498
362
  /**
499
- * Creates a service client from a generated service descriptor. Its calls run the transport's
500
- * 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`.
501
367
  */
502
- export function createServiceClient<TClient extends ServiceClient = ServiceClient>(
368
+ export function createClient<S extends DescService>(
369
+ service: S,
503
370
  transport: Transport,
504
- service: RpcServiceDescriptor,
505
- root: Root,
506
371
  options?: ClientOptions,
507
- ): TClient;
372
+ ): Client<S>;
508
373
 
509
374
  export interface WebTransportCertificateHash {
510
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,
package/src/status.js CHANGED
@@ -37,6 +37,10 @@ export class TrevRpcError extends Error {
37
37
  }
38
38
  }
39
39
 
40
+ // A discriminant for libraries that match errors by tag, such as Effect's `catchTag`. It is on the
41
+ // prototype, so errors carry no extra own property.
42
+ Object.defineProperty(TrevRpcError.prototype, "_tag", { value: "TrevRpcError" });
43
+
40
44
  /** Converts a number into a known status code, defaulting unknown values to Unknown. */
41
45
  export function codeFromNumber(code) {
42
46
  return Number.isInteger(code) && code >= 0 && code < CodeNames.length ? code : Code.Unknown;