@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.
- package/README.md +30 -1
- package/package.json +12 -4
- package/src/call.js +401 -58
- package/src/channel.js +47 -10
- package/src/client.js +93 -22
- package/src/compression.js +330 -0
- package/src/connection-metadata.js +114 -0
- package/src/datagram.js +48 -27
- package/src/framing.js +23 -6
- package/src/generator.js +107 -10
- package/src/index.d.ts +75 -14
- package/src/qmux/session.js +2 -1
- package/src/stream-compression.js +298 -0
- package/src/wire.js +43 -11
- package/src/zstd.d.ts +35 -0
- package/src/zstd.js +407 -0
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
41
|
-
datagram[offset] =
|
|
42
|
-
offset = writeVarint(datagram, offset + 1,
|
|
43
|
-
datagram.set(
|
|
44
|
-
offset +=
|
|
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
|
|
52
|
-
* frames do not exactly fill it
|
|
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 ===
|
|
73
|
-
|
|
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
|
|
120
|
-
*
|
|
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
|
-
|
|
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
|
|
59
|
-
*
|
|
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.
|
|
79
|
+
checkFrameLength(message.size, maxFrameSize);
|
|
68
80
|
const requestSize = frameSize(FrameType.Request, payload.byteLength);
|
|
69
|
-
const frame = new Uint8Array(requestSize + frameSize(
|
|
70
|
-
writeFrame(frame, requestSize,
|
|
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) =>
|
|
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
|
|
416
|
-
outputType
|
|
417
|
-
...unreliableDirections(
|
|
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
|
-
*
|
|
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
|
|
96
|
-
*
|
|
97
|
-
* never mixes the stream of one session
|
|
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
|
-
|
|
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
|
|
152
|
-
*
|
|
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
|
|
201
|
-
*
|
|
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
|
|
341
|
-
|
|
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>;
|
package/src/qmux/session.js
CHANGED
|
@@ -66,7 +66,8 @@ export class QMuxWebSocketSession {
|
|
|
66
66
|
#pending = new FrameWriter();
|
|
67
67
|
#flushScheduled = false;
|
|
68
68
|
#waiters = new Set();
|
|
69
|
-
|
|
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;
|