@trevrpc/trevrpc-js 0.2.3 → 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 +127 -46
- package/package.json +12 -45
- package/src/bytes.js +10 -0
- package/src/call.js +520 -0
- package/src/channel.js +258 -288
- package/src/client.js +543 -1254
- package/src/datagram.js +336 -0
- package/src/framing.js +151 -548
- package/src/generator.js +66 -109
- package/src/index.d.ts +235 -463
- package/src/index.js +4 -3
- 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 +24 -70
- package/src/varint.js +73 -0
- package/src/wire.js +21 -89
- package/examples/greeter/README.md +0 -35
- package/examples/greeter/client.js +0 -172
- package/examples/greeter/greeter.node.trevrpc.d.ts +0 -53
- package/examples/greeter/greeter.node.trevrpc.js +0 -9
- package/examples/greeter/greeter.proto +0 -18
- package/examples/greeter/greeter.trevrpc.d.ts +0 -55
- package/examples/greeter/greeter.trevrpc.js +0 -103
- package/examples/greeter/index.html +0 -140
- package/examples/greeter/static-server.js +0 -168
- package/src/advanced.d.ts +0 -29
- package/src/advanced.js +0 -1
- package/src/browser-channel.js +0 -49
- package/src/connect.js +0 -6
- package/src/connect.node.js +0 -6
- package/src/index.node.d.ts +0 -10
- package/src/index.node.js +0 -3
- package/src/native-loader.js +0 -118
- package/src/node-advanced.d.ts +0 -27
- package/src/node-advanced.js +0 -1
- package/src/node-generated.d.ts +0 -24
- package/src/node-generated.js +0 -126
- package/src/node-index.d.ts +0 -1
- package/src/node-index.js +0 -7
- package/src/node.d.ts +0 -298
- package/src/node.js +0 -1456
- package/src/runtime.js +0 -66
- package/src/server-response.js +0 -49
- package/src/timer.js +0 -29
- package/src/webtransport.js +0 -639
package/src/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
1
|
+
export { Channel, connect } from "./channel.js";
|
|
2
|
+
export { createServiceClient } from "./client.js";
|
|
3
|
+
export { Code, TrevRpcError, codeName } from "./status.js";
|
|
4
|
+
export { createRoot, protobuf } from "./wire.js";
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readVarint, varintLength } from "../varint.js";
|
|
2
|
+
// QX_TRANSPORT_PARAMETERS (draft-ietf-quic-qmux-02 §6): the subset of QUIC transport parameters
|
|
3
|
+
// that QMux keeps, plus max_record_size.
|
|
4
|
+
import { FrameType, FrameWriter, QMuxProtocolError, TransportError } from "./frames.js";
|
|
5
|
+
|
|
6
|
+
/** The max_record_size a peer may assume until told otherwise, and the smallest it may set. */
|
|
7
|
+
export const DefaultMaxRecordSize = 16382;
|
|
8
|
+
|
|
9
|
+
const Param = Object.freeze({
|
|
10
|
+
MaxIdleTimeout: 0x01,
|
|
11
|
+
InitialMaxData: 0x04,
|
|
12
|
+
InitialMaxStreamDataBidiLocal: 0x05,
|
|
13
|
+
InitialMaxStreamDataBidiRemote: 0x06,
|
|
14
|
+
InitialMaxStreamDataUni: 0x07,
|
|
15
|
+
InitialMaxStreamsBidi: 0x08,
|
|
16
|
+
InitialMaxStreamsUni: 0x09,
|
|
17
|
+
MaxRecordSize: 0x0571_c594_29cd_0845n,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** Parameters that only make sense for QUIC packets. */
|
|
21
|
+
const Forbidden = new Set([0x00, 0x02, 0x03, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10]);
|
|
22
|
+
|
|
23
|
+
const Fields = new Map([
|
|
24
|
+
[Param.MaxIdleTimeout, "maxIdleTimeoutMs"],
|
|
25
|
+
[Param.InitialMaxData, "initialMaxData"],
|
|
26
|
+
[Param.InitialMaxStreamDataBidiLocal, "initialMaxStreamDataBidiLocal"],
|
|
27
|
+
[Param.InitialMaxStreamDataBidiRemote, "initialMaxStreamDataBidiRemote"],
|
|
28
|
+
[Param.InitialMaxStreamDataUni, "initialMaxStreamDataUni"],
|
|
29
|
+
[Param.InitialMaxStreamsBidi, "initialMaxStreamsBidi"],
|
|
30
|
+
[Param.InitialMaxStreamsUni, "initialMaxStreamsUni"],
|
|
31
|
+
[Param.MaxRecordSize, "maxRecordSize"],
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/** Appends the QX_TRANSPORT_PARAMETERS frame advertising `params`; zero values are omitted. */
|
|
35
|
+
export function writeTransportParameters(writer, params) {
|
|
36
|
+
const body = new FrameWriter();
|
|
37
|
+
for (const [id, field] of Fields) {
|
|
38
|
+
const value = params[field] ?? (id === Param.MaxRecordSize ? DefaultMaxRecordSize : 0);
|
|
39
|
+
if (value !== 0) {
|
|
40
|
+
body.varint(id).varint(varintLength(value)).varint(value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const bytes = body.take();
|
|
44
|
+
return writer.varint(FrameType.TransportParameters).varint(bytes.length).bytes(bytes);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parameterError(message) {
|
|
48
|
+
return new QMuxProtocolError(
|
|
49
|
+
TransportError.TransportParameterError,
|
|
50
|
+
message,
|
|
51
|
+
FrameType.TransportParameters,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Decodes a peer's transport parameters; unknown ones are ignored. */
|
|
56
|
+
export function decodeTransportParameters(bytes) {
|
|
57
|
+
const params = {
|
|
58
|
+
maxIdleTimeoutMs: 0,
|
|
59
|
+
initialMaxData: 0,
|
|
60
|
+
initialMaxStreamDataBidiLocal: 0,
|
|
61
|
+
initialMaxStreamDataBidiRemote: 0,
|
|
62
|
+
initialMaxStreamDataUni: 0,
|
|
63
|
+
initialMaxStreamsBidi: 0,
|
|
64
|
+
initialMaxStreamsUni: 0,
|
|
65
|
+
maxRecordSize: DefaultMaxRecordSize,
|
|
66
|
+
};
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
let offset = 0;
|
|
69
|
+
while (offset < bytes.length) {
|
|
70
|
+
const id = readVarint(bytes, offset);
|
|
71
|
+
const length = id && readVarint(bytes, id.next);
|
|
72
|
+
if (!length || typeof length.value === "bigint" || length.next + length.value > bytes.length) {
|
|
73
|
+
throw parameterError("truncated transport parameters");
|
|
74
|
+
}
|
|
75
|
+
const value = bytes.subarray(length.next, length.next + length.value);
|
|
76
|
+
offset = length.next + length.value;
|
|
77
|
+
if (seen.has(id.value)) {
|
|
78
|
+
throw parameterError(`duplicate transport parameter 0x${id.value.toString(16)}`);
|
|
79
|
+
}
|
|
80
|
+
seen.add(id.value);
|
|
81
|
+
if (Forbidden.has(id.value)) {
|
|
82
|
+
throw parameterError(`transport parameter 0x${id.value.toString(16)} is not allowed in QMux`);
|
|
83
|
+
}
|
|
84
|
+
const field = Fields.get(id.value);
|
|
85
|
+
if (field === undefined) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const parsed = readVarint(value, 0);
|
|
89
|
+
if (!parsed || parsed.next !== value.length) {
|
|
90
|
+
throw parameterError(`malformed transport parameter 0x${id.value.toString(16)}`);
|
|
91
|
+
}
|
|
92
|
+
// Limits beyond 2^53 are as good as unlimited.
|
|
93
|
+
params[field] = typeof parsed.value === "bigint" ? Number.MAX_SAFE_INTEGER : parsed.value;
|
|
94
|
+
}
|
|
95
|
+
if (params.maxRecordSize < DefaultMaxRecordSize) {
|
|
96
|
+
throw parameterError(
|
|
97
|
+
`max_record_size ${params.maxRecordSize} is below ${DefaultMaxRecordSize}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (params.initialMaxStreamsBidi > 2 ** 60) {
|
|
101
|
+
throw parameterError("initial_max_streams_bidi exceeds 2^60");
|
|
102
|
+
}
|
|
103
|
+
return params;
|
|
104
|
+
}
|