@trevrpc/trevrpc-js 1.1.0 → 1.2.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.
@@ -0,0 +1,114 @@
1
+ // Connection metadata is request metadata a client sets once for all its calls, such as a bearer
2
+ // token, so that it crosses each connection once instead of in every REQUEST. A call offers the
3
+ // set (RpcRequest.connection_metadata with entries) until an ANSWER confirms that the server
4
+ // stored it for the session, then refers to it by ID. Calls on different streams reach the server
5
+ // in any order, so a reference only follows a confirmation. The server merges the set into each
6
+ // call's metadata, the call's own entries winning, so the client just sends both; see the wiki's
7
+ // Connection Metadata section. This module holds the channel's set and what each session
8
+ // confirmed; call.js puts them on the REQUEST and learns the confirmation from the ANSWER.
9
+ import { normalizeMetadata } from "./metadata.js";
10
+
11
+ /** The property of a transport that holds its ConnectionMetadataSource. */
12
+ export const ConnectionMetadataKey = Symbol("trevrpc.connectionMetadata");
13
+
14
+ /** The connection metadata of a transport, or undefined without a source. */
15
+ export function transportConnectionMetadata(transport) {
16
+ return transport[ConnectionMetadataKey];
17
+ }
18
+
19
+ /** A request that carries no connection metadata. */
20
+ const NoConnectionMetadata = Object.freeze({ field: undefined, offered: 0 });
21
+
22
+ /**
23
+ * A channel's connection metadata, shared by its sessions: the current set, whose ID changes with
24
+ * it, and the state of each session.
25
+ */
26
+ export class ConnectionMetadataSource {
27
+ /** The current set, `{ id, metadata }`, or undefined without connection metadata. */
28
+ #current;
29
+ #ids = 0;
30
+ #sessions = new WeakMap();
31
+
32
+ /** Throws InvalidArgument if `metadata` is invalid, like call metadata. */
33
+ constructor(metadata) {
34
+ this.set(metadata);
35
+ }
36
+
37
+ /** The current set, or undefined. */
38
+ get current() {
39
+ return this.#current;
40
+ }
41
+
42
+ /**
43
+ * Replaces the connection metadata, with keys normalized like a call's; empty or missing
44
+ * metadata removes it. The new set has a new ID, so each session offers it again.
45
+ */
46
+ set(metadata) {
47
+ const normalized = metadata == null ? {} : normalizeMetadata(metadata);
48
+ const keys = Object.keys(normalized);
49
+ if (keys.length === 0) {
50
+ this.#current = undefined;
51
+ return;
52
+ }
53
+ // Copied, so a caller changing its bytes later does not change what is sent.
54
+ for (const key of keys) {
55
+ normalized[key] = normalized[key].slice();
56
+ }
57
+ // IDs are uint32 on the wire and 0 means none: they run from 1 to 2^32 - 1 and wrap.
58
+ this.#ids = (this.#ids % 0xffffffff) + 1;
59
+ this.#current = { id: this.#ids, metadata: normalized };
60
+ }
61
+
62
+ /**
63
+ * The state of one session, keyed like the compression state: what the server confirmed on the
64
+ * connection that `key` identifies, dropped with it.
65
+ */
66
+ session(key) {
67
+ let state = this.#sessions.get(key);
68
+ if (state === undefined) {
69
+ state = new SessionConnectionMetadata(this);
70
+ this.#sessions.set(key, state);
71
+ }
72
+ return state;
73
+ }
74
+ }
75
+
76
+ class SessionConnectionMetadata {
77
+ #source;
78
+ /** The ID of the set the server confirmed on this session, or zero. */
79
+ #confirmed = 0;
80
+
81
+ constructor(source) {
82
+ this.#source = source;
83
+ }
84
+
85
+ /**
86
+ * The connection metadata of one REQUEST as `{ field, offered }`: `field` is the value of
87
+ * `connectionMetadata`, a reference to the current set once the session confirmed it and an
88
+ * offer of it otherwise, or undefined without a set; `offered` is the ID the REQUEST offers,
89
+ * which `learn` checks the ANSWER against, or zero.
90
+ */
91
+ prepare() {
92
+ const set = this.#source.current;
93
+ if (set === undefined) {
94
+ return NoConnectionMetadata;
95
+ }
96
+ if (this.#confirmed === set.id) {
97
+ return { field: { id: set.id }, offered: 0 };
98
+ }
99
+ return { field: { id: set.id, metadata: set.metadata }, offered: set.id };
100
+ }
101
+
102
+ /** Records that the server stored the `offered` set, once the call's ANSWER confirms it. */
103
+ learn(offered, answer) {
104
+ // Only the current set counts: a late confirmation of one since replaced would make calls
105
+ // offer the current one again.
106
+ if (
107
+ offered !== 0 &&
108
+ answer.connectionMetadata === offered &&
109
+ this.#source.current?.id === offered
110
+ ) {
111
+ this.#confirmed = offered;
112
+ }
113
+ }
114
+ }
package/src/datagram.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // DatagramFlow on the endpoint of the session that carries them. QMux over WebSocket has no
5
5
  // datagrams, so those calls never get a flow and everything stays on the stream.
6
6
  import { byteView } from "./bytes.js";
7
+ import { FrameType } from "./framing.js";
7
8
  import { readVarint, varintLength, writeVarint } from "./varint.js";
8
9
 
9
10
  /**
@@ -19,16 +20,17 @@ export const FallbackMaxDatagramSize = 1100;
19
20
  export const FlowQueueMessages = 32;
20
21
  export const FlowQueueBytes = 1 << 20;
21
22
 
22
- const MessageFrameType = 0;
23
-
24
- /** Encoded size of one message as a MESSAGE frame inside a datagram. */
25
- export function messageFrameSize(message) {
26
- return 1 + varintLength(message.byteLength) + message.byteLength;
23
+ /**
24
+ * Encoded size of one outgoing message (`{ type, payload }` from encodeMessage) as a MESSAGE or
25
+ * ZSTD_MESSAGE frame inside a datagram.
26
+ */
27
+ export function messageFrameSize({ payload }) {
28
+ return 1 + varintLength(payload.byteLength) + payload.byteLength;
27
29
  }
28
30
 
29
31
  /**
30
- * Encodes a datagram payload: the flow ID as a QUIC varint, then one MESSAGE frame per message,
31
- * in order.
32
+ * Encodes a datagram payload: the flow ID as a QUIC varint, then the frame of each outgoing
33
+ * message, in order.
32
34
  */
33
35
  export function encodeDatagram(flow, messages) {
34
36
  let size = varintLength(flow);
@@ -37,19 +39,20 @@ export function encodeDatagram(flow, messages) {
37
39
  }
38
40
  const datagram = new Uint8Array(size);
39
41
  let offset = writeVarint(datagram, 0, flow);
40
- for (const message of messages) {
41
- datagram[offset] = MessageFrameType;
42
- offset = writeVarint(datagram, offset + 1, message.byteLength);
43
- datagram.set(message, offset);
44
- offset += message.byteLength;
42
+ for (const { type, payload } of messages) {
43
+ datagram[offset] = type;
44
+ offset = writeVarint(datagram, offset + 1, payload.byteLength);
45
+ datagram.set(payload, offset);
46
+ offset += payload.byteLength;
45
47
  }
46
48
  return datagram;
47
49
  }
48
50
 
49
51
  /**
50
52
  * Decodes a datagram payload as `{ flow, messages }`, or null if it has no complete flow ID. The
51
- * messages are the payloads of its MESSAGE frames; every other frame is skipped. A datagram whose
52
- * frames do not exactly fill it decodes to no messages at all.
53
+ * messages are its MESSAGE and ZSTD_MESSAGE frames as `{ type, payload }`; every other frame is
54
+ * skipped. A datagram whose frames do not exactly fill it, or that holds a ZSTD_STREAM_MESSAGE,
55
+ * which needs a stream's ordered delivery, decodes to no messages at all.
53
56
  */
54
57
  export function decodeDatagram(datagram) {
55
58
  const bytes = byteView(datagram);
@@ -69,8 +72,11 @@ export function decodeDatagram(datagram) {
69
72
  if (end > bytes.byteLength) {
70
73
  return { flow: flow.value, messages: [] };
71
74
  }
72
- if (type.value === MessageFrameType) {
73
- messages.push(bytes.subarray(length.next, end));
75
+ if (type.value === FrameType.ZstdStreamMessage) {
76
+ return { flow: flow.value, messages: [] };
77
+ }
78
+ if (type.value === FrameType.Message || type.value === FrameType.ZstdMessage) {
79
+ messages.push({ type: type.value, payload: bytes.subarray(length.next, end) });
74
80
  }
75
81
  offset = end;
76
82
  }
@@ -116,14 +122,15 @@ class DatagramEndpoint {
116
122
 
117
123
  /**
118
124
  * Allocates the next flow of this session. A flow that `receive`s is registered with the
119
- * reader, so datagrams for its ID reach it. Returns undefined if the session cannot read
120
- * datagrams.
125
+ * reader, so datagrams for its ID reach it; `inflate` maps each received `{ type, payload }`
126
+ * message to its payload, decompressed, and throws if it is invalid. Returns undefined if the
127
+ * session cannot read datagrams.
121
128
  */
122
- openFlow({ receive }) {
129
+ openFlow({ receive, inflate }) {
123
130
  if (!this.#start()) {
124
131
  return undefined;
125
132
  }
126
- const flow = new DatagramFlow(this, this.#nextFlow);
133
+ const flow = new DatagramFlow(this, this.#nextFlow, inflate);
127
134
  this.#nextFlow += 1;
128
135
  if (receive) {
129
136
  this.#flows.set(flow.id, flow);
@@ -207,11 +214,7 @@ class DatagramEndpoint {
207
214
  const datagram = decodeDatagram(value);
208
215
  const flow = datagram === null ? undefined : this.#flows.get(datagram.flow);
209
216
  // Unknown and ended flows drop the datagram, and so do malformed ones (no messages).
210
- if (flow !== undefined) {
211
- for (const message of datagram.messages) {
212
- flow.deliver(message);
213
- }
214
- }
217
+ flow?.deliverAll(datagram.messages);
215
218
  }
216
219
  } catch {
217
220
  // A failed session ends its calls through their streams.
@@ -253,21 +256,39 @@ class DatagramEndpoint {
253
256
  */
254
257
  export class DatagramFlow {
255
258
  #endpoint;
259
+ #inflate;
256
260
  #handler;
257
261
  #buffered = [];
258
262
  #bufferedBytes = 0;
259
263
  #released = false;
260
264
 
261
- constructor(endpoint, id) {
265
+ constructor(endpoint, id, inflate) {
262
266
  this.#endpoint = endpoint;
267
+ this.#inflate = inflate;
263
268
  this.id = id;
264
269
  }
265
270
 
271
+ /**
272
+ * Delivers the messages of one received datagram, decompressed. A datagram with an invalid
273
+ * message is dropped whole.
274
+ */
275
+ deliverAll(messages) {
276
+ let payloads;
277
+ try {
278
+ payloads = messages.map(this.#inflate);
279
+ } catch {
280
+ return;
281
+ }
282
+ for (const payload of payloads) {
283
+ this.#deliver(payload);
284
+ }
285
+ }
286
+
266
287
  /**
267
288
  * Delivers one received message. It must not block or throw. Without a handler it is buffered,
268
289
  * and dropped when the buffer is full.
269
290
  */
270
- deliver(message) {
291
+ #deliver(message) {
271
292
  if (this.#handler !== undefined) {
272
293
  this.#handler(message);
273
294
  } else if (
package/src/framing.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { byteView } from "./bytes.js";
2
2
  import { internal, invalidArgument, resourceExhausted, unavailable } from "./status.js";
3
3
  import { readVarint, varintLength, writeVarint } from "./varint.js";
4
- import { RpcHeaders, RpcRequest, RpcStatus, preflightWireMessage } from "./wire.js";
4
+ import { RpcAnswer, RpcHeaders, RpcRequest, RpcStatus, preflightWireMessage } from "./wire.js";
5
5
 
6
6
  export const DefaultMaxFrameSize = 4 * 1024 * 1024;
7
7
 
@@ -11,6 +11,9 @@ export const FrameType = Object.freeze({
11
11
  Request: 1,
12
12
  Headers: 2,
13
13
  Status: 3,
14
+ ZstdMessage: 4,
15
+ ZstdStreamMessage: 5,
16
+ Answer: 6,
14
17
  });
15
18
 
16
19
  const KnownFrameTypes = new Set(Object.values(FrameType));
@@ -55,8 +58,17 @@ export function encodeMessageFrame(body, maxFrameSize = DefaultMaxFrameSize) {
55
58
  }
56
59
 
57
60
  /**
58
- * Encodes a REQUEST frame, followed by a MESSAGE frame when message is given, in one buffer so
59
- * both go out in a single write.
61
+ * Encodes an outgoing message, `{ type, payload, size }` from encodeMessage, as its MESSAGE or
62
+ * ZSTD_MESSAGE frame. The message size counts against the limit, not just the payload's.
63
+ */
64
+ export function encodeOutgoingFrame(message, maxFrameSize = DefaultMaxFrameSize) {
65
+ checkFrameLength(message.size, maxFrameSize);
66
+ return encodeFrame(message.type, message.payload, maxFrameSize);
67
+ }
68
+
69
+ /**
70
+ * Encodes a REQUEST frame, followed by the frame of message, an outgoing message from
71
+ * encodeMessage, when it is given, in one buffer so both go out in a single write.
60
72
  */
61
73
  export function encodeRequestFrame(request, message, maxFrameSize = DefaultMaxFrameSize) {
62
74
  const payload = marshalMessage(RpcRequest, request);
@@ -64,10 +76,10 @@ export function encodeRequestFrame(request, message, maxFrameSize = DefaultMaxFr
64
76
  if (message === undefined) {
65
77
  return encodeFrame(FrameType.Request, payload, maxFrameSize);
66
78
  }
67
- checkFrameLength(message.byteLength, maxFrameSize);
79
+ checkFrameLength(message.size, maxFrameSize);
68
80
  const requestSize = frameSize(FrameType.Request, payload.byteLength);
69
- const frame = new Uint8Array(requestSize + frameSize(FrameType.Message, message.byteLength));
70
- writeFrame(frame, requestSize, FrameType.Message, message);
81
+ const frame = new Uint8Array(requestSize + frameSize(message.type, message.payload.byteLength));
82
+ writeFrame(frame, requestSize, message.type, message.payload);
71
83
  writeFrame(frame, 0, FrameType.Request, payload);
72
84
  return frame;
73
85
  }
@@ -77,6 +89,11 @@ export function encodeHeadersFrame(headers, maxFrameSize = DefaultMaxFrameSize)
77
89
  return encodeFrame(FrameType.Headers, marshalMessage(RpcHeaders, headers), maxFrameSize);
78
90
  }
79
91
 
92
+ /** Encodes an ANSWER frame from RpcAnswer fields. */
93
+ export function encodeAnswerFrame(answer, maxFrameSize = DefaultMaxFrameSize) {
94
+ return encodeFrame(FrameType.Answer, marshalMessage(RpcAnswer, answer), maxFrameSize);
95
+ }
96
+
80
97
  /** Encodes a STATUS frame from RpcStatus fields. */
81
98
  export function encodeStatusFrame(status, maxFrameSize = DefaultMaxFrameSize) {
82
99
  return encodeFrame(FrameType.Status, marshalMessage(RpcStatus, status), maxFrameSize);
package/src/generator.js CHANGED
@@ -62,6 +62,7 @@ message FieldDescriptorProto {
62
62
  optional int32 oneof_index = 9;
63
63
  optional string json_name = 10;
64
64
  optional bool proto3_optional = 17;
65
+ optional FieldOptions options = 8;
65
66
  }
66
67
 
67
68
  message OneofDescriptorProto {
@@ -101,6 +102,12 @@ message MethodOptions {
101
102
  message TrevrpcMethodOptions {
102
103
  optional bool unreliable_requests = 1;
103
104
  optional bool unreliable_responses = 2;
105
+ // trevrpc.Compression, read as its number so that unknown values reach the check.
106
+ optional int32 compression = 5;
107
+ }
108
+
109
+ message FieldOptions {
110
+ optional bool debug_redact = 16;
104
111
  }
105
112
 
106
113
  message FileOptions {
@@ -236,6 +243,7 @@ export function generate(request) {
236
243
  const protoFiles = request.protoFile ?? [];
237
244
  const rootJson = buildProtobufJson(protoFiles);
238
245
  const typeNames = declarationTypeNames(protoFiles);
246
+ const redacted = redactedTypes(protoFiles);
239
247
  const files = [];
240
248
 
241
249
  for (const file of protoFiles) {
@@ -246,11 +254,11 @@ export function generate(request) {
246
254
  const fileName = outputFileName(file.name, options.fileSuffix);
247
255
  files.push({
248
256
  name: fileName,
249
- content: generateFile(file, rootJson, options),
257
+ content: generateFile(file, rootJson, redacted, options),
250
258
  });
251
259
  files.push({
252
260
  name: declarationFileName(fileName),
253
- content: generateDeclarationFile(protoFiles, file, typeNames, options),
261
+ content: generateDeclarationFile(protoFiles, file, typeNames, redacted, options),
254
262
  });
255
263
  }
256
264
 
@@ -323,8 +331,10 @@ export function buildProtobufJson(files) {
323
331
  return root;
324
332
  }
325
333
 
326
- function generateFile(file, rootJson, options) {
327
- const services = (file.service ?? []).map((service) => serviceDescriptor(file, service));
334
+ function generateFile(file, rootJson, redacted, options) {
335
+ const services = (file.service ?? []).map((service) =>
336
+ serviceDescriptor(file, service, redacted),
337
+ );
328
338
  const serviceExports = services.map((service) => generateService(service)).join("\n");
329
339
 
330
340
  return `// Code generated by protoc-gen-trevrpc-js. DO NOT EDIT.\n\nimport { createRoot, createServiceClient } from ${JSON.stringify(
@@ -332,13 +342,13 @@ function generateFile(file, rootJson, options) {
332
342
  )};\n\nexport const root = createRoot(${JSON.stringify(rootJson, null, 2)});\n\n${serviceExports}`;
333
343
  }
334
344
 
335
- function generateDeclarationFile(protoFiles, file, typeNames, options) {
345
+ function generateDeclarationFile(protoFiles, file, typeNames, redacted, options) {
336
346
  const messages = collectMessages(protoFiles)
337
347
  .filter(({ message }) => !message.options?.mapEntry)
338
348
  .map((message) => generateMessageDeclaration(message, typeNames))
339
349
  .join("\n\n");
340
350
  const services = (file.service ?? [])
341
- .map((service) => serviceDescriptor(file, service))
351
+ .map((service) => serviceDescriptor(file, service, redacted))
342
352
  .map((service) => generateServiceDeclaration(service, typeNames))
343
353
  .join("\n\n");
344
354
  const sections = [messages, services].filter(Boolean).join("\n\n");
@@ -400,7 +410,7 @@ function generateMethodDeclaration(jsName, method, typeNames) {
400
410
  }
401
411
  }
402
412
 
403
- function serviceDescriptor(file, service) {
413
+ function serviceDescriptor(file, service, redacted) {
404
414
  const fullName =
405
415
  file.package === "" || file.package == null ? service.name : `${file.package}.${service.name}`;
406
416
  const methods = {};
@@ -409,12 +419,17 @@ function serviceDescriptor(file, service) {
409
419
  for (const method of service.method ?? []) {
410
420
  const jsName = uniqueName(lowerCamelIdentifier(method.name), usedNames);
411
421
  const kind = methodKind(method);
422
+ const inputType = stripLeadingDot(method.inputType);
423
+ const outputType = stripLeadingDot(method.outputType);
424
+ const methodName = `${fullName}.${method.name}`;
425
+ const options = method.options?.trevrpcMethod;
412
426
  methods[jsName] = {
413
427
  name: method.name,
414
428
  kind,
415
- inputType: stripLeadingDot(method.inputType),
416
- outputType: stripLeadingDot(method.outputType),
417
- ...unreliableDirections(`${fullName}.${method.name}`, kind, method.options?.trevrpcMethod),
429
+ inputType,
430
+ outputType,
431
+ ...unreliableDirections(methodName, kind, options),
432
+ ...compressionDirections(methodName, kind, options, { inputType, outputType }, redacted),
418
433
  };
419
434
  }
420
435
 
@@ -451,6 +466,88 @@ function unreliableDirections(methodName, kind, options) {
451
466
  return directions;
452
467
  }
453
468
 
469
+ /** `trevrpc.Compression` values, from proto/trevrpc/options.proto. */
470
+ const Compression = { unspecified: 0, message: 1, stream: 2 };
471
+
472
+ /**
473
+ * The directions a method's `compression` lets compress, as descriptor fields, leaving out message
474
+ * types with a field marked `debug_redact`. `COMPRESSION_MESSAGE` compresses messages without a
475
+ * dictionary in both directions. `COMPRESSION_STREAM` does too, for the messages outside the
476
+ * stream, and stream-compresses the directions that stream; on a unary method it is an error, and
477
+ * so is a value this generator does not know.
478
+ */
479
+ function compressionDirections(methodName, kind, options, { inputType, outputType }, redacted) {
480
+ const compression = options?.compression ?? Compression.unspecified;
481
+ if (compression === Compression.unspecified) {
482
+ return {};
483
+ }
484
+ if (compression !== Compression.message && compression !== Compression.stream) {
485
+ throw new Error(`${methodName}: unknown compression ${compression}`);
486
+ }
487
+ const directions = {};
488
+ if (!redacted.has(inputType)) {
489
+ directions.compressRequests = true;
490
+ }
491
+ if (!redacted.has(outputType)) {
492
+ directions.compressResponses = true;
493
+ }
494
+ if (compression === Compression.message) {
495
+ return directions;
496
+ }
497
+ if (kind === "unary") {
498
+ throw new Error(`${methodName}: COMPRESSION_STREAM requires a streaming method`);
499
+ }
500
+ if (
501
+ (kind === "clientStreaming" || kind === "bidirectionalStreaming") &&
502
+ !redacted.has(inputType)
503
+ ) {
504
+ directions.compressStreamRequests = true;
505
+ }
506
+ if (
507
+ (kind === "serverStreaming" || kind === "bidirectionalStreaming") &&
508
+ !redacted.has(outputType)
509
+ ) {
510
+ directions.compressStreamResponses = true;
511
+ }
512
+ return directions;
513
+ }
514
+
515
+ /**
516
+ * The full names of the message types that contain a field marked `debug_redact`, directly or in
517
+ * a message-typed field, however deeply nested.
518
+ */
519
+ function redactedTypes(files) {
520
+ const messages = new Map(
521
+ collectMessages(files).map(({ fullName, message }) => [fullName, message]),
522
+ );
523
+ const redacted = new Set();
524
+ const visiting = new Set();
525
+ const visit = (name) => {
526
+ if (redacted.has(name)) {
527
+ return true;
528
+ }
529
+ const message = messages.get(name);
530
+ if (message === undefined || visiting.has(name)) {
531
+ return false;
532
+ }
533
+ visiting.add(name);
534
+ const found = (message.field ?? []).some(
535
+ (field) =>
536
+ field.options?.debugRedact === true ||
537
+ (field.typeName != null && visit(stripLeadingDot(field.typeName))),
538
+ );
539
+ visiting.delete(name);
540
+ if (found) {
541
+ redacted.add(name);
542
+ }
543
+ return found;
544
+ };
545
+ for (const name of messages.keys()) {
546
+ visit(name);
547
+ }
548
+ return redacted;
549
+ }
550
+
454
551
  function declarationTypeNames(files) {
455
552
  const names = new Map();
456
553
  const usedNames = new Set();
package/src/index.d.ts CHANGED
@@ -53,8 +53,8 @@ export interface CallOptions {
53
53
  /** Cancels the call. */
54
54
  signal?: AbortSignal;
55
55
  /**
56
- * Called with the response headers, empty when the server sent none, as soon as the first
57
- * response frame arrives. A callback that throws fails the call.
56
+ * Called with the response headers, empty when the server sent none, as soon as they, the first
57
+ * message or the final status arrive. A callback that throws fails the call.
58
58
  */
59
59
  onResponseHeaders?(headers: Metadata): void;
60
60
  /**
@@ -82,6 +82,16 @@ export interface DatagramTransport {
82
82
  readonly writable?: WritableStream<Uint8Array>;
83
83
  }
84
84
 
85
+ declare const compressionBrand: unique symbol;
86
+
87
+ /**
88
+ * Compression settings, made by `createCompression()` from `@trevrpc/trevrpc-js/zstd`: the zstd
89
+ * dictionaries a channel holds and the message types each compresses. See `ConnectOptions`.
90
+ */
91
+ export interface Compression {
92
+ readonly [compressionBrand]: true;
93
+ }
94
+
85
95
  /**
86
96
  * Anything that opens a bidirectional byte stream per call, such as a Channel or WebTransport.
87
97
  * Calls to methods that mark a direction unreliable use the `datagrams` of the session that opens
@@ -91,14 +101,17 @@ export interface Transport {
91
101
  createBidirectionalStream(): Promise<TransportStream>;
92
102
  /** Datagrams of the session that the next stream opens on; absent without datagrams. */
93
103
  readonly datagrams?: DatagramTransport;
104
+ /** Compression settings for calls on this transport; absent without compression. */
105
+ readonly compression?: Compression;
94
106
  /**
95
- * Like `createBidirectionalStream()`, but also returns the datagrams of the session that opened
96
- * the stream. Transports whose session can change, such as a Channel, implement it so a call
97
- * never mixes the stream of one session with the datagrams of another.
107
+ * Like `createBidirectionalStream()`, but also returns the session that opened the stream.
108
+ * Calls negotiate dictionaries and open datagram flows per session, so transports whose session
109
+ * can change, such as a Channel, implement it so a call never mixes the stream of one session
110
+ * with the state of another.
98
111
  */
99
- createBidirectionalStreamWithDatagrams?(): Promise<{
112
+ createBidirectionalStreamWithSession?(): Promise<{
100
113
  stream: TransportStream;
101
- datagrams?: DatagramTransport;
114
+ session: { readonly datagrams?: DatagramTransport };
102
115
  }>;
103
116
  }
104
117
 
@@ -124,6 +137,24 @@ export interface RpcMethodDescriptor {
124
137
  * client accepts them as datagrams on any session that has them.
125
138
  */
126
139
  unreliableResponses?: boolean;
140
+ /**
141
+ * The method's request messages may be stream-compressed (`compression: COMPRESSION_STREAM` in
142
+ * the `trevrpc.method` option, on a method that streams requests, whose request type has no
143
+ * field marked `debug_redact`). Calls on a transport with compression then compress them with one zstd
144
+ * context per call, once the server agrees.
145
+ */
146
+ compressStreamRequests?: boolean;
147
+ /** The method's response messages may be stream-compressed; see `compressStreamRequests`. */
148
+ compressStreamResponses?: boolean;
149
+ /**
150
+ * The method's request messages may be compressed without a dictionary: one at a time, from 256
151
+ * bytes, where no dictionary applies to their type and the server decodes it. The generator
152
+ * sets it for `COMPRESSION_MESSAGE` and `COMPRESSION_STREAM`, except for message types with a
153
+ * field marked `debug_redact`.
154
+ */
155
+ compressRequests?: boolean;
156
+ /** The method's response messages may be compressed without a dictionary; see `compressRequests`. */
157
+ compressResponses?: boolean;
127
158
  }
128
159
 
129
160
  export interface RpcServiceDescriptor {
@@ -148,8 +179,9 @@ export interface ResponseStream<TResponse> extends AsyncIterable<TResponse> {
148
179
  /** Settles with the final status, or rejects with the error that ended the call. */
149
180
  readonly status: Promise<StreamStatus>;
150
181
  /**
151
- * The response headers, empty when the server sent none. Settles when the first response frame
152
- * arrives, before the first message is received, or rejects with the error that ended the call.
182
+ * The response headers, empty when the server sent none. Settles when they, the first message or
183
+ * the final status arrive, before the first message is received, or rejects with the error that
184
+ * ended the call.
153
185
  */
154
186
  readonly headers: Promise<Metadata>;
155
187
  /** Cancels the call unless it already finished. */
@@ -197,8 +229,9 @@ export interface BidirectionalStreamingCall<TRequest, TResponse> extends AsyncIt
197
229
  /** Settles with the final status, or rejects with the error that ended the call. */
198
230
  readonly status: Promise<StreamStatus>;
199
231
  /**
200
- * The response headers, empty when the server sent none. Settles when the first response frame
201
- * arrives, before the first message is received, or rejects with the error that ended the call.
232
+ * The response headers, empty when the server sent none. Settles when they, the first message or
233
+ * the final status arrive, before the first message is received, or rejects with the error that
234
+ * ended the call.
202
235
  */
203
236
  readonly headers: Promise<Metadata>;
204
237
  /**
@@ -298,6 +331,25 @@ export interface ConnectOptions {
298
331
  * `protocols: ["trevrpc-1"]`, the wire version, which cannot be changed.
299
332
  */
300
333
  WebTransport?: new (url: string | URL, options?: object) => WebTransportSessionLike;
334
+ /**
335
+ * Compresses messages with the dictionaries these settings hold, from `createCompression()` in
336
+ * `@trevrpc/trevrpc-js/zstd`. Dictionaries are negotiated with the server per session, so a
337
+ * message whose dictionary the server lacks travels uncompressed. Compression settings also
338
+ * enable stream compression on methods that allow it, even without dictionaries.
339
+ */
340
+ compression?: Compression;
341
+ /**
342
+ * The memory, in bytes, the stream compression contexts of this channel's calls may hold.
343
+ * Defaults to 64 MiB; 0 turns stream compression off. Calls beyond it compress per message.
344
+ */
345
+ streamCompressionMemory?: number;
346
+ /**
347
+ * Request metadata for all of the channel's calls, such as a bearer token, which crosses each
348
+ * connection once instead of in every request. A call's own `metadata` wins over an entry with
349
+ * the same key. Keys are lowercase ASCII and the `trevrpc-` prefix is reserved, as for call
350
+ * metadata. Replace it with `Channel.setConnectionMetadata()`.
351
+ */
352
+ connectionMetadata?: MetadataInput;
301
353
  /** Cancels the initial connection attempt only. */
302
354
  signal?: AbortSignal;
303
355
  /** Receives channel state transitions. */
@@ -336,11 +388,20 @@ export class Channel implements Transport {
336
388
  readonly state: ChannelState;
337
389
  /** The transport of the current or last session. */
338
390
  readonly transport: TransportKind;
391
+ /** The compression settings from the connect options. */
392
+ readonly compression?: Compression;
393
+ /**
394
+ * Replaces the channel's connection metadata (see `ConnectOptions.connectionMetadata`); empty or
395
+ * missing metadata removes it. Calls started after this use the new metadata, and each
396
+ * connection sends it again before referring to it. Throws if it is invalid, and then keeps the
397
+ * old.
398
+ */
399
+ setConnectionMetadata(metadata?: MetadataInput): void;
339
400
  createBidirectionalStream(): Promise<TransportStream>;
340
- /** Opens a stream together with the datagrams of the session that carries it, if any. */
341
- createBidirectionalStreamWithDatagrams(): Promise<{
401
+ /** Opens a stream together with the session that carries it. */
402
+ createBidirectionalStreamWithSession(): Promise<{
342
403
  stream: TransportStream;
343
- datagrams?: DatagramTransport;
404
+ session: { readonly datagrams?: DatagramTransport };
344
405
  }>;
345
406
  /** Resolves once the channel is ready, immediately if it already is. */
346
407
  waitUntilReady(): Promise<void>;
@@ -66,7 +66,8 @@ export class QMuxWebSocketSession {
66
66
  #pending = new FrameWriter();
67
67
  #flushScheduled = false;
68
68
  #waiters = new Set();
69
- #pingReceived = 0;
69
+ /** The highest QX_PING request sequence received, -1 before the first: a peer may start at 0. */
70
+ #pingReceived = -1;
70
71
  #closedError = null;
71
72
  #lastActivity = Date.now();
72
73
  #idleTimer;