@trevrpc/trevrpc-js 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -18
- package/bin/protoc-gen-trevrpc-js.js +1 -1
- package/package.json +7 -10
- package/src/call.js +269 -36
- package/src/channel.js +152 -17
- package/src/client.js +481 -103
- package/src/datagram.js +336 -0
- package/src/framing.js +108 -111
- package/src/generator.js +41 -7
- package/src/index.d.ts +162 -17
- package/src/qmux/frames.js +262 -0
- package/src/qmux/params.js +104 -0
- package/src/qmux/session.js +739 -0
- package/src/race.js +90 -0
- package/src/status.js +1 -1
- package/src/varint.js +73 -0
- package/src/wire.js +17 -30
package/src/generator.js
CHANGED
|
@@ -89,6 +89,18 @@ message MethodDescriptorProto {
|
|
|
89
89
|
optional string output_type = 3;
|
|
90
90
|
optional bool client_streaming = 5 [default = false];
|
|
91
91
|
optional bool server_streaming = 6 [default = false];
|
|
92
|
+
optional MethodOptions options = 4;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Only the trevrpc.method extension is modelled; protobuf.js keeps every other extension out of
|
|
96
|
+
// the decoded message. The numbers are those of proto/trevrpc/options.proto.
|
|
97
|
+
message MethodOptions {
|
|
98
|
+
optional TrevrpcMethodOptions trevrpc_method = 58247;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
message TrevrpcMethodOptions {
|
|
102
|
+
optional bool unreliable_requests = 1;
|
|
103
|
+
optional bool unreliable_responses = 2;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
message FileOptions {
|
|
@@ -171,7 +183,6 @@ const GeneratedTypeImports = [
|
|
|
171
183
|
"BidirectionalStreamingCall",
|
|
172
184
|
"ClientStreamingCall",
|
|
173
185
|
"ResponseStream",
|
|
174
|
-
"UnaryResponse",
|
|
175
186
|
];
|
|
176
187
|
|
|
177
188
|
const RESERVED_WORDS = new Set([
|
|
@@ -358,10 +369,6 @@ function generateServiceMethod(jsName, method) {
|
|
|
358
369
|
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(options = {}) {\n return this._client.${jsName}(options);\n }`;
|
|
359
370
|
}
|
|
360
371
|
|
|
361
|
-
if (method.kind === "unary") {
|
|
362
|
-
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request, options = {}) {\n return this._client.${jsName}(request, options);\n }\n\n /** Calls the ${method.name} RPC and returns response metadata. */\n ${jsName}WithResponse(request, options = {}) {\n return this._client.${jsName}WithResponse(request, options);\n }`;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
372
|
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request, options = {}) {\n return this._client.${jsName}(request, options);\n }`;
|
|
366
373
|
}
|
|
367
374
|
|
|
@@ -381,7 +388,7 @@ function generateMethodDeclaration(jsName, method, typeNames) {
|
|
|
381
388
|
|
|
382
389
|
switch (method.kind) {
|
|
383
390
|
case "unary":
|
|
384
|
-
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request: ${inputType}, options?: CallOptions): Promise<${outputType}
|
|
391
|
+
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request: ${inputType}, options?: CallOptions): Promise<${outputType}>;`;
|
|
385
392
|
case "serverStreaming":
|
|
386
393
|
return ` /** Calls the ${method.name} RPC. */\n ${jsName}(request: ${inputType}, options?: CallOptions): Promise<ResponseStream<${outputType}>>;`;
|
|
387
394
|
case "clientStreaming":
|
|
@@ -401,11 +408,13 @@ function serviceDescriptor(file, service) {
|
|
|
401
408
|
|
|
402
409
|
for (const method of service.method ?? []) {
|
|
403
410
|
const jsName = uniqueName(lowerCamelIdentifier(method.name), usedNames);
|
|
411
|
+
const kind = methodKind(method);
|
|
404
412
|
methods[jsName] = {
|
|
405
413
|
name: method.name,
|
|
406
|
-
kind
|
|
414
|
+
kind,
|
|
407
415
|
inputType: stripLeadingDot(method.inputType),
|
|
408
416
|
outputType: stripLeadingDot(method.outputType),
|
|
417
|
+
...unreliableDirections(`${fullName}.${method.name}`, kind, method.options?.trevrpcMethod),
|
|
409
418
|
};
|
|
410
419
|
}
|
|
411
420
|
|
|
@@ -417,6 +426,31 @@ function serviceDescriptor(file, service) {
|
|
|
417
426
|
};
|
|
418
427
|
}
|
|
419
428
|
|
|
429
|
+
/**
|
|
430
|
+
* The `(trevrpc.method)` flags of a method as descriptor fields, only for the ones that are set.
|
|
431
|
+
* A flag on a direction that does not stream is an error.
|
|
432
|
+
*/
|
|
433
|
+
function unreliableDirections(methodName, kind, options) {
|
|
434
|
+
const directions = {};
|
|
435
|
+
if (options?.unreliableRequests) {
|
|
436
|
+
if (kind !== "clientStreaming" && kind !== "bidirectionalStreaming") {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`${methodName}: unreliable_requests requires a client-streaming or bidirectional method`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
directions.unreliableRequests = true;
|
|
442
|
+
}
|
|
443
|
+
if (options?.unreliableResponses) {
|
|
444
|
+
if (kind !== "serverStreaming" && kind !== "bidirectionalStreaming") {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`${methodName}: unreliable_responses requires a server-streaming or bidirectional method`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
directions.unreliableResponses = true;
|
|
450
|
+
}
|
|
451
|
+
return directions;
|
|
452
|
+
}
|
|
453
|
+
|
|
420
454
|
function declarationTypeNames(files) {
|
|
421
455
|
const names = new Map();
|
|
422
456
|
const usedNames = new Set();
|
package/src/index.d.ts
CHANGED
|
@@ -52,6 +52,17 @@ export interface CallOptions {
|
|
|
52
52
|
metadata?: MetadataInput;
|
|
53
53
|
/** Cancels the call. */
|
|
54
54
|
signal?: AbortSignal;
|
|
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.
|
|
58
|
+
*/
|
|
59
|
+
onResponseHeaders?(headers: Metadata): void;
|
|
60
|
+
/**
|
|
61
|
+
* Called with the trailing metadata of the final status when the call succeeds, before its
|
|
62
|
+
* result is returned or its response stream ends. A failed call reports its metadata on the
|
|
63
|
+
* TrevRpcError instead. A callback that throws fails the call.
|
|
64
|
+
*/
|
|
65
|
+
onResponseMetadata?(metadata: Metadata): void;
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
export interface TransportStream {
|
|
@@ -59,9 +70,36 @@ export interface TransportStream {
|
|
|
59
70
|
writable: WritableStream<Uint8Array>;
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The datagram side of a WebTransport session (`WebTransport.datagrams`). TrevRPC needs only
|
|
75
|
+
* `readable`; sending also needs `createWritable()` (or the older `writable`), and
|
|
76
|
+
* `maxDatagramSize` bounds what is sent as a datagram (1100 bytes if it is missing).
|
|
77
|
+
*/
|
|
78
|
+
export interface DatagramTransport {
|
|
79
|
+
readonly readable: ReadableStream<Uint8Array>;
|
|
80
|
+
readonly maxDatagramSize?: number;
|
|
81
|
+
createWritable?(): WritableStream<Uint8Array>;
|
|
82
|
+
readonly writable?: WritableStream<Uint8Array>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Anything that opens a bidirectional byte stream per call, such as a Channel or WebTransport.
|
|
87
|
+
* Calls to methods that mark a direction unreliable use the `datagrams` of the session that opens
|
|
88
|
+
* their stream, when it has them.
|
|
89
|
+
*/
|
|
63
90
|
export interface Transport {
|
|
64
91
|
createBidirectionalStream(): Promise<TransportStream>;
|
|
92
|
+
/** Datagrams of the session that the next stream opens on; absent without datagrams. */
|
|
93
|
+
readonly datagrams?: DatagramTransport;
|
|
94
|
+
/**
|
|
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.
|
|
98
|
+
*/
|
|
99
|
+
createBidirectionalStreamWithDatagrams?(): Promise<{
|
|
100
|
+
stream: TransportStream;
|
|
101
|
+
datagrams?: DatagramTransport;
|
|
102
|
+
}>;
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
export type RpcMethodKind =
|
|
@@ -75,6 +113,17 @@ export interface RpcMethodDescriptor {
|
|
|
75
113
|
kind: RpcMethodKind;
|
|
76
114
|
inputType: string;
|
|
77
115
|
outputType: string;
|
|
116
|
+
/**
|
|
117
|
+
* The method marks its streamed request messages unreliable (`unreliable_requests` in the
|
|
118
|
+
* `trevrpc.method` option). They are sent as WebTransport datagrams when they fit and the
|
|
119
|
+
* session has datagrams, and may be lost or reordered; otherwise they travel on the stream.
|
|
120
|
+
*/
|
|
121
|
+
unreliableRequests?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* The method marks its streamed response messages unreliable (`unreliable_responses`). The
|
|
124
|
+
* client accepts them as datagrams on any session that has them.
|
|
125
|
+
*/
|
|
126
|
+
unreliableResponses?: boolean;
|
|
78
127
|
}
|
|
79
128
|
|
|
80
129
|
export interface RpcServiceDescriptor {
|
|
@@ -84,11 +133,6 @@ export interface RpcServiceDescriptor {
|
|
|
84
133
|
methods: Record<string, RpcMethodDescriptor>;
|
|
85
134
|
}
|
|
86
135
|
|
|
87
|
-
export interface UnaryResponse<TResponse> {
|
|
88
|
-
readonly message: TResponse;
|
|
89
|
-
readonly metadata: Metadata;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
136
|
/** The final status of a call that ended with OK. */
|
|
93
137
|
export interface StreamStatus {
|
|
94
138
|
readonly code: StatusCode;
|
|
@@ -103,29 +147,92 @@ export interface StreamStatus {
|
|
|
103
147
|
export interface ResponseStream<TResponse> extends AsyncIterable<TResponse> {
|
|
104
148
|
/** Settles with the final status, or rejects with the error that ended the call. */
|
|
105
149
|
readonly status: Promise<StreamStatus>;
|
|
150
|
+
/**
|
|
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.
|
|
153
|
+
*/
|
|
154
|
+
readonly headers: Promise<Metadata>;
|
|
106
155
|
/** Cancels the call unless it already finished. */
|
|
107
156
|
close(reason?: unknown): Promise<void>;
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
export interface ClientStreamingCall<TRequest, TResponse> {
|
|
111
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Sends one request message; the promise settles when the transport accepted it, which applies
|
|
162
|
+
* backpressure.
|
|
163
|
+
*
|
|
164
|
+
* Rejects as soon as the server ends the call before the upload is done: with the server's
|
|
165
|
+
* `TrevRpcError` if its final status is not OK, even if the server never reads the rest of the
|
|
166
|
+
* request, and with `Code.Cancelled` if the status is OK (the call succeeded without the rest
|
|
167
|
+
* of the upload; `closeAndRecv()` returns its response). A response that fails after the
|
|
168
|
+
* upload stopped, such as a lost connection, is reported instead of a generic closed-stream
|
|
169
|
+
* error. A failed send is not retried or reported twice: later sends fail the same way, and
|
|
170
|
+
* `closeAndRecv()` returns the call's outcome.
|
|
171
|
+
*
|
|
172
|
+
* Pending sends are rejected too when the call is cancelled or its deadline expires.
|
|
173
|
+
*/
|
|
112
174
|
send(request: TRequest): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Sends every message of `requests`, then closes the request stream. If the server ends the
|
|
177
|
+
* call first, the rest is not sent and the promise resolves; `closeAndRecv()` reports the
|
|
178
|
+
* outcome. A send that fails otherwise rejects as `send()` does, and requests that throw cancel
|
|
179
|
+
* the call and reject with what they threw.
|
|
180
|
+
*/
|
|
181
|
+
sendAll(requests: Iterable<TRequest> | AsyncIterable<TRequest>): Promise<void>;
|
|
113
182
|
/** Closes the request stream and returns the final response. */
|
|
114
183
|
closeAndRecv(): Promise<TResponse>;
|
|
115
|
-
/** Closes the request stream and returns the final response with its metadata. */
|
|
116
|
-
closeAndRecvWithResponse(): Promise<UnaryResponse<TResponse>>;
|
|
117
184
|
/** Cancels the call unless it already finished. */
|
|
118
185
|
close(reason?: unknown): Promise<void>;
|
|
119
186
|
}
|
|
120
187
|
|
|
188
|
+
/**
|
|
189
|
+
* On a method that marks a direction unreliable (`RpcMethodDescriptor.unreliableRequests` or
|
|
190
|
+
* `unreliableResponses`), a call on a session with datagrams may send its request messages, and
|
|
191
|
+
* receive its response messages, as datagrams: they can be lost or reordered, never duplicated.
|
|
192
|
+
* Response datagrams are delivered after the response headers, merged with the stream's messages
|
|
193
|
+
* in arrival order, and dropped when the call's queue is full (32 messages or 1 MiB) or the call
|
|
194
|
+
* has ended. Without datagrams, such as over WebSocket, everything stays on the stream.
|
|
195
|
+
*/
|
|
121
196
|
export interface BidirectionalStreamingCall<TRequest, TResponse> extends AsyncIterable<TResponse> {
|
|
122
197
|
/** Settles with the final status, or rejects with the error that ended the call. */
|
|
123
198
|
readonly status: Promise<StreamStatus>;
|
|
124
|
-
/**
|
|
199
|
+
/**
|
|
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.
|
|
202
|
+
*/
|
|
203
|
+
readonly headers: Promise<Metadata>;
|
|
204
|
+
/**
|
|
205
|
+
* Sends one request message; the promise settles when the transport accepted it, which applies
|
|
206
|
+
* backpressure.
|
|
207
|
+
*
|
|
208
|
+
* Rejects as soon as the server ends the call before the upload is done, even if nobody is
|
|
209
|
+
* calling `recv()` and the server never reads the rest of the request: with the server's
|
|
210
|
+
* `TrevRpcError` if its final status is not OK, and with `Code.Cancelled` if the status is OK
|
|
211
|
+
* (`recv()` then returns `undefined` after the responses that were sent). A response that
|
|
212
|
+
* fails after the upload stopped, such as a lost connection, is reported instead of a generic
|
|
213
|
+
* closed-stream error. Later sends fail the same way, `closeSend()` resolves, and `recv()` and
|
|
214
|
+
* `status` report the call's outcome.
|
|
215
|
+
*
|
|
216
|
+
* While the request side is open the response is read ahead of `recv()` to find the final
|
|
217
|
+
* status: at most 32 frames or 1 MiB (plus one frame of at most `maxFrameSize`), after which
|
|
218
|
+
* reading pauses until `recv()` takes messages. Responses read ahead are delivered in order.
|
|
219
|
+
*
|
|
220
|
+
* Pending sends are rejected too when the call is cancelled or its deadline expires.
|
|
221
|
+
*/
|
|
125
222
|
send(request: TRequest): Promise<void>;
|
|
223
|
+
/**
|
|
224
|
+
* Sends every message of `requests`, then closes the request stream. If the server ends the
|
|
225
|
+
* call first, the rest is not sent and the promise resolves; `recv()` and `status` report the
|
|
226
|
+
* outcome. A send that fails otherwise rejects as `send()` does, and requests that throw cancel
|
|
227
|
+
* the call and reject with what they threw.
|
|
228
|
+
*/
|
|
229
|
+
sendAll(requests: Iterable<TRequest> | AsyncIterable<TRequest>): Promise<void>;
|
|
126
230
|
/** Receives the next response message, or undefined after the final OK status. */
|
|
127
231
|
recv(): Promise<TResponse | undefined>;
|
|
128
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Closes the request stream while keeping the response stream readable. Resolves if the server
|
|
234
|
+
* already ended the call; its final status is reported by `recv()` and `status`.
|
|
235
|
+
*/
|
|
129
236
|
closeSend(): Promise<void>;
|
|
130
237
|
/** Cancels the call unless it already finished. */
|
|
131
238
|
close(reason?: unknown): Promise<void>;
|
|
@@ -151,17 +258,45 @@ export interface WebTransportCertificateHash {
|
|
|
151
258
|
|
|
152
259
|
export interface WebTransportSessionLike extends Transport {
|
|
153
260
|
readonly ready: Promise<unknown>;
|
|
261
|
+
/**
|
|
262
|
+
* The WebTransport protocol the server selected. An empty or missing value means wire version 1.
|
|
263
|
+
* Any other value than "trevrpc-1" fails the connection attempt with FailedPrecondition.
|
|
264
|
+
*/
|
|
265
|
+
readonly protocol?: string;
|
|
154
266
|
readonly closed: Promise<unknown>;
|
|
155
267
|
close(closeInfo?: { closeCode?: number; reason?: string }): void;
|
|
156
268
|
}
|
|
157
269
|
|
|
270
|
+
/** How a channel reaches the server: WebTransport, or QMux over WebSocket as the fallback. */
|
|
271
|
+
export type TransportKind = "webtransport" | "websocket";
|
|
272
|
+
|
|
158
273
|
export interface ConnectOptions {
|
|
274
|
+
/**
|
|
275
|
+
* Transports to use, in order of preference. The default, `["webtransport", "websocket"]`,
|
|
276
|
+
* starts QMux over WebSocket once WebTransport fails or has not connected within
|
|
277
|
+
* `fallbackDelayMs`, and uses whichever is ready first.
|
|
278
|
+
*/
|
|
279
|
+
transports?: readonly TransportKind[];
|
|
280
|
+
/** The `wss://` URL of the server's TCP listener. Defaults to `url` with a `wss:` scheme. */
|
|
281
|
+
webSocketUrl?: string | URL;
|
|
282
|
+
/** How long WebTransport may take before the WebSocket fallback starts. Defaults to 300. */
|
|
283
|
+
fallbackDelayMs?: number;
|
|
284
|
+
/** How long reconnects keep using WebSocket after it won, before trying WebTransport again. */
|
|
285
|
+
reprobeIntervalMs?: number;
|
|
286
|
+
/** WebSocket implementation to use instead of the global one. */
|
|
287
|
+
WebSocket?: new (url: string | URL, protocols?: string | string[]) => WebSocket;
|
|
159
288
|
allowPooling?: boolean;
|
|
160
289
|
congestionControl?: "default" | "low-latency" | "throughput";
|
|
161
290
|
requireUnreliable?: boolean;
|
|
162
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* Trusts a self-signed ECDSA certificate (at most 14 days old) by its SHA-256 hash. WebTransport
|
|
293
|
+
* only: the WebSocket fallback needs a certificate the browser trusts.
|
|
294
|
+
*/
|
|
163
295
|
serverCertificateHashes?: readonly WebTransportCertificateHash[];
|
|
164
|
-
/**
|
|
296
|
+
/**
|
|
297
|
+
* WebTransport implementation to use instead of the global one. It is constructed with
|
|
298
|
+
* `protocols: ["trevrpc-1"]`, the wire version, which cannot be changed.
|
|
299
|
+
*/
|
|
165
300
|
WebTransport?: new (url: string | URL, options?: object) => WebTransportSessionLike;
|
|
166
301
|
/** Cancels the initial connection attempt only. */
|
|
167
302
|
signal?: AbortSignal;
|
|
@@ -180,23 +315,33 @@ export interface ChannelStateChange {
|
|
|
180
315
|
delayMs?: number;
|
|
181
316
|
/** Why the session was lost or the previous attempt failed. */
|
|
182
317
|
error?: unknown;
|
|
318
|
+
/** The transport of the new session, when it becomes ready. */
|
|
319
|
+
transport?: TransportKind;
|
|
183
320
|
}
|
|
184
321
|
|
|
185
|
-
/** Opens a channel to a
|
|
322
|
+
/** Opens a channel to a TrevRPC server and waits for its session to be ready. */
|
|
186
323
|
export function connect(url: string | URL, options?: ConnectOptions): Promise<Channel>;
|
|
187
324
|
|
|
188
325
|
/**
|
|
189
|
-
* A
|
|
190
|
-
*
|
|
326
|
+
* A session that reconnects in the background after it is lost: WebTransport, or QMux over
|
|
327
|
+
* WebSocket when WebTransport is unavailable or blocked. Calls fail fast with Unavailable while it
|
|
328
|
+
* reconnects and are never replayed.
|
|
191
329
|
*/
|
|
192
330
|
export class Channel implements Transport {
|
|
193
331
|
private constructor();
|
|
194
332
|
|
|
195
|
-
/** Opens a channel to a
|
|
333
|
+
/** Opens a channel to a TrevRPC server and waits for its session to be ready. */
|
|
196
334
|
static connect(url: string | URL, options?: ConnectOptions): Promise<Channel>;
|
|
197
335
|
|
|
198
336
|
readonly state: ChannelState;
|
|
337
|
+
/** The transport of the current or last session. */
|
|
338
|
+
readonly transport: TransportKind;
|
|
199
339
|
createBidirectionalStream(): Promise<TransportStream>;
|
|
340
|
+
/** Opens a stream together with the datagrams of the session that carries it, if any. */
|
|
341
|
+
createBidirectionalStreamWithDatagrams(): Promise<{
|
|
342
|
+
stream: TransportStream;
|
|
343
|
+
datagrams?: DatagramTransport;
|
|
344
|
+
}>;
|
|
200
345
|
/** Resolves once the channel is ready, immediately if it already is. */
|
|
201
346
|
waitUntilReady(): Promise<void>;
|
|
202
347
|
/** Stops reconnecting and closes the current session. */
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// QMux frames (draft-ietf-quic-qmux-02): the QUIC v1 frames QMux carries plus its own
|
|
2
|
+
// QX_TRANSPORT_PARAMETERS and QX_PING frames.
|
|
3
|
+
import { readVarint, varintLength, writeVarint } from "../varint.js";
|
|
4
|
+
|
|
5
|
+
export const FrameType = Object.freeze({
|
|
6
|
+
Padding: 0x00,
|
|
7
|
+
ResetStream: 0x04,
|
|
8
|
+
StopSending: 0x05,
|
|
9
|
+
Stream: 0x08, // through 0x0f
|
|
10
|
+
MaxData: 0x10,
|
|
11
|
+
MaxStreamData: 0x11,
|
|
12
|
+
MaxStreamsBidi: 0x12,
|
|
13
|
+
MaxStreamsUni: 0x13,
|
|
14
|
+
DataBlocked: 0x14,
|
|
15
|
+
StreamDataBlocked: 0x15,
|
|
16
|
+
StreamsBlockedBidi: 0x16,
|
|
17
|
+
StreamsBlockedUni: 0x17,
|
|
18
|
+
ConnectionClose: 0x1c,
|
|
19
|
+
ApplicationClose: 0x1d,
|
|
20
|
+
TransportParameters: 0x3f51_5330_0d0a_0d0an,
|
|
21
|
+
Ping: 0x348c_6752_9ef8_c7bdn,
|
|
22
|
+
PingResponse: 0x348c_6752_9ef8_c7ben,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const StreamFin = 0x01;
|
|
26
|
+
const StreamLen = 0x02;
|
|
27
|
+
const StreamOff = 0x04;
|
|
28
|
+
|
|
29
|
+
/** QUIC transport error codes (RFC 9000 §20.1). */
|
|
30
|
+
export const TransportError = Object.freeze({
|
|
31
|
+
NoError: 0x00,
|
|
32
|
+
InternalError: 0x01,
|
|
33
|
+
FlowControlError: 0x03,
|
|
34
|
+
StreamLimitError: 0x04,
|
|
35
|
+
StreamStateError: 0x05,
|
|
36
|
+
FinalSizeError: 0x06,
|
|
37
|
+
FrameEncodingError: 0x07,
|
|
38
|
+
TransportParameterError: 0x08,
|
|
39
|
+
ProtocolViolation: 0x0a,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** A QMux protocol error that closes the session with a transport CONNECTION_CLOSE. */
|
|
43
|
+
export class QMuxProtocolError extends Error {
|
|
44
|
+
constructor(code, message, frameType = 0) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "QMuxProtocolError";
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.frameType = frameType;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function truncated(frameType) {
|
|
53
|
+
return new QMuxProtocolError(TransportError.FrameEncodingError, "truncated frame", frameType);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Builds the frames of one record. */
|
|
57
|
+
export class FrameWriter {
|
|
58
|
+
#bytes = new Uint8Array(256);
|
|
59
|
+
#length = 0;
|
|
60
|
+
|
|
61
|
+
get length() {
|
|
62
|
+
return this.#length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#reserve(extra) {
|
|
66
|
+
if (this.#length + extra > this.#bytes.length) {
|
|
67
|
+
const grown = new Uint8Array(Math.max(this.#bytes.length * 2, this.#length + extra));
|
|
68
|
+
grown.set(this.#bytes.subarray(0, this.#length));
|
|
69
|
+
this.#bytes = grown;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
varint(value) {
|
|
74
|
+
this.#reserve(8);
|
|
75
|
+
this.#length = writeVarint(this.#bytes, this.#length, value);
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
bytes(data) {
|
|
80
|
+
this.#reserve(data.length);
|
|
81
|
+
this.#bytes.set(data, this.#length);
|
|
82
|
+
this.#length += data.length;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Returns the frames written so far and starts over. */
|
|
87
|
+
take() {
|
|
88
|
+
const frames = this.#bytes.slice(0, this.#length);
|
|
89
|
+
this.#length = 0;
|
|
90
|
+
return frames;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Size of a STREAM frame header carrying `length` bytes at `offset`. */
|
|
95
|
+
export function streamFrameOverhead(streamId, offset, length) {
|
|
96
|
+
return (
|
|
97
|
+
1 + varintLength(streamId) + varintLength(length) + (offset > 0 ? varintLength(offset) : 0)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Appends a STREAM frame. It always carries a length so that frames may follow it. */
|
|
102
|
+
export function writeStreamFrame(writer, streamId, offset, data, fin) {
|
|
103
|
+
let type = FrameType.Stream | StreamLen;
|
|
104
|
+
if (offset > 0) {
|
|
105
|
+
type |= StreamOff;
|
|
106
|
+
}
|
|
107
|
+
if (fin) {
|
|
108
|
+
type |= StreamFin;
|
|
109
|
+
}
|
|
110
|
+
writer.varint(type).varint(streamId);
|
|
111
|
+
if (offset > 0) {
|
|
112
|
+
writer.varint(offset);
|
|
113
|
+
}
|
|
114
|
+
return writer.varint(data.length).bytes(data);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function writeResetStream(writer, streamId, code, finalSize) {
|
|
118
|
+
return writer.varint(FrameType.ResetStream).varint(streamId).varint(code).varint(finalSize);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function writeStopSending(writer, streamId, code) {
|
|
122
|
+
return writer.varint(FrameType.StopSending).varint(streamId).varint(code);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function writeMaxData(writer, maximum) {
|
|
126
|
+
return writer.varint(FrameType.MaxData).varint(maximum);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function writeMaxStreamData(writer, streamId, maximum) {
|
|
130
|
+
return writer.varint(FrameType.MaxStreamData).varint(streamId).varint(maximum);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Appends an application CONNECTION_CLOSE, or a transport one for a QMuxProtocolError. */
|
|
134
|
+
export function writeConnectionClose(writer, { code = 0, reason = "", protocolError } = {}) {
|
|
135
|
+
const reasonBytes = new TextEncoder().encode(reason).subarray(0, 256);
|
|
136
|
+
if (protocolError) {
|
|
137
|
+
writer
|
|
138
|
+
.varint(FrameType.ConnectionClose)
|
|
139
|
+
.varint(protocolError.code)
|
|
140
|
+
.varint(protocolError.frameType);
|
|
141
|
+
} else {
|
|
142
|
+
writer.varint(FrameType.ApplicationClose).varint(code);
|
|
143
|
+
}
|
|
144
|
+
return writer.varint(reasonBytes.length).bytes(reasonBytes);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function writePing(writer, type, sequence) {
|
|
148
|
+
return writer.varint(type).varint(sequence);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Decodes the frames of one record; `next()` returns null at its end. */
|
|
152
|
+
export class FrameParser {
|
|
153
|
+
#bytes;
|
|
154
|
+
#offset = 0;
|
|
155
|
+
|
|
156
|
+
constructor(bytes) {
|
|
157
|
+
this.#bytes = bytes;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#varint(frameType) {
|
|
161
|
+
const result = readVarint(this.#bytes, this.#offset);
|
|
162
|
+
if (result === null) {
|
|
163
|
+
throw truncated(frameType);
|
|
164
|
+
}
|
|
165
|
+
this.#offset = result.next;
|
|
166
|
+
return result.value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A varint that must fit a number: offsets, lengths, limits and stream IDs. */
|
|
170
|
+
#number(frameType) {
|
|
171
|
+
const value = this.#varint(frameType);
|
|
172
|
+
if (typeof value === "bigint") {
|
|
173
|
+
throw new QMuxProtocolError(
|
|
174
|
+
TransportError.FrameEncodingError,
|
|
175
|
+
"value exceeds what this endpoint supports",
|
|
176
|
+
frameType,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#take(frameType, length) {
|
|
183
|
+
if (length > this.#bytes.length - this.#offset) {
|
|
184
|
+
throw truncated(frameType);
|
|
185
|
+
}
|
|
186
|
+
const data = this.#bytes.subarray(this.#offset, this.#offset + length);
|
|
187
|
+
this.#offset += length;
|
|
188
|
+
return data;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
next() {
|
|
192
|
+
if (this.#offset >= this.#bytes.length) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const type = this.#varint(0);
|
|
196
|
+
if (typeof type === "bigint") {
|
|
197
|
+
if (type === FrameType.TransportParameters) {
|
|
198
|
+
return { type, params: this.#take(type, this.#number(type)) };
|
|
199
|
+
}
|
|
200
|
+
if (type === FrameType.Ping || type === FrameType.PingResponse) {
|
|
201
|
+
return { type, sequence: this.#number(type) };
|
|
202
|
+
}
|
|
203
|
+
throw new QMuxProtocolError(
|
|
204
|
+
TransportError.FrameEncodingError,
|
|
205
|
+
`frame type 0x${type.toString(16)} is not allowed in QMux`,
|
|
206
|
+
0,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
switch (type) {
|
|
210
|
+
case FrameType.Padding:
|
|
211
|
+
return { type };
|
|
212
|
+
case FrameType.ResetStream:
|
|
213
|
+
return {
|
|
214
|
+
type,
|
|
215
|
+
streamId: this.#number(type),
|
|
216
|
+
code: this.#number(type),
|
|
217
|
+
finalSize: this.#number(type),
|
|
218
|
+
};
|
|
219
|
+
case FrameType.StopSending:
|
|
220
|
+
return { type, streamId: this.#number(type), code: this.#number(type) };
|
|
221
|
+
case FrameType.MaxData:
|
|
222
|
+
case FrameType.DataBlocked:
|
|
223
|
+
case FrameType.MaxStreamsBidi:
|
|
224
|
+
case FrameType.MaxStreamsUni:
|
|
225
|
+
case FrameType.StreamsBlockedBidi:
|
|
226
|
+
case FrameType.StreamsBlockedUni:
|
|
227
|
+
return { type, maximum: this.#number(type) };
|
|
228
|
+
case FrameType.MaxStreamData:
|
|
229
|
+
case FrameType.StreamDataBlocked:
|
|
230
|
+
return { type, streamId: this.#number(type), maximum: this.#number(type) };
|
|
231
|
+
case FrameType.ConnectionClose:
|
|
232
|
+
case FrameType.ApplicationClose: {
|
|
233
|
+
const code = this.#number(type);
|
|
234
|
+
const frameType = type === FrameType.ConnectionClose ? this.#varint(type) : undefined;
|
|
235
|
+
const reason = new TextDecoder().decode(this.#take(type, this.#number(type)));
|
|
236
|
+
return { type, code, frameType, reason };
|
|
237
|
+
}
|
|
238
|
+
default:
|
|
239
|
+
if (type >= FrameType.Stream && type <= (FrameType.Stream | 0x07)) {
|
|
240
|
+
return this.#stream(type);
|
|
241
|
+
}
|
|
242
|
+
throw new QMuxProtocolError(
|
|
243
|
+
TransportError.FrameEncodingError,
|
|
244
|
+
`frame type 0x${type.toString(16)} is not allowed in QMux`,
|
|
245
|
+
type,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#stream(type) {
|
|
251
|
+
const streamId = this.#number(type);
|
|
252
|
+
const offset = type & StreamOff ? this.#number(type) : 0;
|
|
253
|
+
const length = type & StreamLen ? this.#number(type) : this.#bytes.length - this.#offset;
|
|
254
|
+
return {
|
|
255
|
+
type,
|
|
256
|
+
streamId,
|
|
257
|
+
offset,
|
|
258
|
+
data: this.#take(type, length),
|
|
259
|
+
fin: (type & StreamFin) !== 0,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|