@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/datagram.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// Unreliable RPC messages over WebTransport datagrams. One DatagramEndpoint per session owns the
|
|
2
|
+
// session's single datagram reader loop, its single shared writer, the flow ID allocator, and the
|
|
3
|
+
// demultiplexer from flow IDs to calls. Calls that stream an unreliable direction open a
|
|
4
|
+
// DatagramFlow on the endpoint of the session that carries them. QMux over WebSocket has no
|
|
5
|
+
// datagrams, so those calls never get a flow and everything stays on the stream.
|
|
6
|
+
import { byteView } from "./bytes.js";
|
|
7
|
+
import { readVarint, varintLength, writeVarint } from "./varint.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Largest datagram assumed when the browser does not report `maxDatagramSize`. Browsers silently
|
|
11
|
+
* drop datagrams above the real limit, so this stays below a typical path MTU.
|
|
12
|
+
*/
|
|
13
|
+
export const FallbackMaxDatagramSize = 1100;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How much a flow buffers for a call that is not ready to take datagrams yet: at most this many
|
|
17
|
+
* messages or bytes, then further datagrams are dropped.
|
|
18
|
+
*/
|
|
19
|
+
export const FlowQueueMessages = 32;
|
|
20
|
+
export const FlowQueueBytes = 1 << 20;
|
|
21
|
+
|
|
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;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Encodes a datagram payload: the flow ID as a QUIC varint, then one MESSAGE frame per message,
|
|
31
|
+
* in order.
|
|
32
|
+
*/
|
|
33
|
+
export function encodeDatagram(flow, messages) {
|
|
34
|
+
let size = varintLength(flow);
|
|
35
|
+
for (const message of messages) {
|
|
36
|
+
size += messageFrameSize(message);
|
|
37
|
+
}
|
|
38
|
+
const datagram = new Uint8Array(size);
|
|
39
|
+
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;
|
|
45
|
+
}
|
|
46
|
+
return datagram;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 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
|
+
*/
|
|
54
|
+
export function decodeDatagram(datagram) {
|
|
55
|
+
const bytes = byteView(datagram);
|
|
56
|
+
const flow = readVarint(bytes, 0);
|
|
57
|
+
if (flow === null) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const messages = [];
|
|
61
|
+
let offset = flow.next;
|
|
62
|
+
while (offset < bytes.byteLength) {
|
|
63
|
+
const type = readVarint(bytes, offset);
|
|
64
|
+
const length = type === null ? null : readVarint(bytes, type.next);
|
|
65
|
+
if (length === null || typeof length.value !== "number") {
|
|
66
|
+
return { flow: flow.value, messages: [] };
|
|
67
|
+
}
|
|
68
|
+
const end = length.next + length.value;
|
|
69
|
+
if (end > bytes.byteLength) {
|
|
70
|
+
return { flow: flow.value, messages: [] };
|
|
71
|
+
}
|
|
72
|
+
if (type.value === MessageFrameType) {
|
|
73
|
+
messages.push(bytes.subarray(length.next, end));
|
|
74
|
+
}
|
|
75
|
+
offset = end;
|
|
76
|
+
}
|
|
77
|
+
return { flow: flow.value, messages };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const endpoints = new WeakMap();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The shared endpoint of a session's `datagrams` object, or undefined if the session has none or
|
|
84
|
+
* its datagram reader is unusable. The endpoint lives as long as the object does.
|
|
85
|
+
*/
|
|
86
|
+
export function datagramEndpoint(datagrams) {
|
|
87
|
+
if (datagrams == null || typeof datagrams !== "object") {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
let endpoint = endpoints.get(datagrams);
|
|
91
|
+
if (endpoint === undefined) {
|
|
92
|
+
endpoint = new DatagramEndpoint(datagrams);
|
|
93
|
+
endpoints.set(datagrams, endpoint);
|
|
94
|
+
}
|
|
95
|
+
return endpoint.usable ? endpoint : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class DatagramEndpoint {
|
|
99
|
+
#datagrams;
|
|
100
|
+
/** Flows that accept datagrams, by ID. */
|
|
101
|
+
#flows = new Map();
|
|
102
|
+
#nextFlow = 1;
|
|
103
|
+
#reading = false;
|
|
104
|
+
#closed;
|
|
105
|
+
/** The shared writer: undefined until first needed, null once unavailable. */
|
|
106
|
+
#writer;
|
|
107
|
+
|
|
108
|
+
constructor(datagrams) {
|
|
109
|
+
this.#datagrams = datagrams;
|
|
110
|
+
this.#closed = typeof datagrams.readable?.getReader !== "function";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get usable() {
|
|
114
|
+
return !this.#closed;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 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.
|
|
121
|
+
*/
|
|
122
|
+
openFlow({ receive }) {
|
|
123
|
+
if (!this.#start()) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const flow = new DatagramFlow(this, this.#nextFlow);
|
|
127
|
+
this.#nextFlow += 1;
|
|
128
|
+
if (receive) {
|
|
129
|
+
this.#flows.set(flow.id, flow);
|
|
130
|
+
}
|
|
131
|
+
return flow;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Stops routing datagrams to a flow; its ID is never reused. */
|
|
135
|
+
closeFlow(id) {
|
|
136
|
+
this.#flows.delete(id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* How many bytes of MESSAGE frames fit in a datagram of `flow` right now, or 0 if this session
|
|
141
|
+
* cannot send datagrams.
|
|
142
|
+
*/
|
|
143
|
+
frameRoom(flow) {
|
|
144
|
+
return this.#writerOrNull() === null
|
|
145
|
+
? 0
|
|
146
|
+
: Math.max(0, this.maxDatagramSize - varintLength(flow));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sends messages as one datagram of `flow`. Resolves true once the datagram was handed to the
|
|
151
|
+
* transport, and false if the messages have to go on the stream instead: they do not fit, there
|
|
152
|
+
* is no writer, or the write failed. Waits for send-buffer space until `signal` aborts, then
|
|
153
|
+
* rejects with the abort reason.
|
|
154
|
+
*/
|
|
155
|
+
async send(flow, messages, signal) {
|
|
156
|
+
const writer = this.#writerOrNull();
|
|
157
|
+
const datagram = writer === null ? null : encodeDatagram(flow, messages);
|
|
158
|
+
if (datagram === null || datagram.byteLength > this.maxDatagramSize) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
if (!(writer.desiredSize > 0)) {
|
|
163
|
+
await abortable(writer.ready, signal);
|
|
164
|
+
}
|
|
165
|
+
await abortable(writer.write(datagram), signal);
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
if (signal.aborted) {
|
|
169
|
+
throw signal.reason;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The largest datagram payload the session accepts right now, flow ID included. */
|
|
176
|
+
get maxDatagramSize() {
|
|
177
|
+
const size = this.#datagrams.maxDatagramSize;
|
|
178
|
+
return Number.isFinite(size) && size > 0 ? size : FallbackMaxDatagramSize;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#start() {
|
|
182
|
+
if (this.#closed) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (!this.#reading) {
|
|
186
|
+
let reader;
|
|
187
|
+
try {
|
|
188
|
+
reader = this.#datagrams.readable.getReader();
|
|
189
|
+
} catch {
|
|
190
|
+
this.#closed = true;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
this.#reading = true;
|
|
194
|
+
void this.#read(reader);
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The reader loop. It never blocks on a call: delivery is a synchronous, bounded push. */
|
|
200
|
+
async #read(reader) {
|
|
201
|
+
try {
|
|
202
|
+
for (;;) {
|
|
203
|
+
const { value, done } = await reader.read();
|
|
204
|
+
if (done) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const datagram = decodeDatagram(value);
|
|
208
|
+
const flow = datagram === null ? undefined : this.#flows.get(datagram.flow);
|
|
209
|
+
// 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
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// A failed session ends its calls through their streams.
|
|
218
|
+
} finally {
|
|
219
|
+
this.#closed = true;
|
|
220
|
+
this.#flows.clear();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#writerOrNull() {
|
|
225
|
+
if (this.#writer === undefined) {
|
|
226
|
+
this.#writer = this.#openWriter();
|
|
227
|
+
}
|
|
228
|
+
return this.#writer;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Locks the session's datagram writable once; a lost writer disables sending datagrams. */
|
|
232
|
+
#openWriter() {
|
|
233
|
+
try {
|
|
234
|
+
const datagrams = this.#datagrams;
|
|
235
|
+
const writable =
|
|
236
|
+
typeof datagrams.createWritable === "function"
|
|
237
|
+
? datagrams.createWritable()
|
|
238
|
+
: datagrams.writable;
|
|
239
|
+
const writer = writable.getWriter();
|
|
240
|
+
writer.closed.catch(() => {
|
|
241
|
+
this.#writer = null;
|
|
242
|
+
});
|
|
243
|
+
return writer;
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* One call's datagram flow. Datagrams for the flow wait in a bounded buffer until the call
|
|
252
|
+
* connects, then go straight to its handler. The call releases the flow when it ends.
|
|
253
|
+
*/
|
|
254
|
+
export class DatagramFlow {
|
|
255
|
+
#endpoint;
|
|
256
|
+
#handler;
|
|
257
|
+
#buffered = [];
|
|
258
|
+
#bufferedBytes = 0;
|
|
259
|
+
#released = false;
|
|
260
|
+
|
|
261
|
+
constructor(endpoint, id) {
|
|
262
|
+
this.#endpoint = endpoint;
|
|
263
|
+
this.id = id;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Delivers one received message. It must not block or throw. Without a handler it is buffered,
|
|
268
|
+
* and dropped when the buffer is full.
|
|
269
|
+
*/
|
|
270
|
+
deliver(message) {
|
|
271
|
+
if (this.#handler !== undefined) {
|
|
272
|
+
this.#handler(message);
|
|
273
|
+
} else if (
|
|
274
|
+
this.#buffered.length < FlowQueueMessages &&
|
|
275
|
+
this.#bufferedBytes + message.byteLength <= FlowQueueBytes
|
|
276
|
+
) {
|
|
277
|
+
this.#buffered.push(message);
|
|
278
|
+
this.#bufferedBytes += message.byteLength;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Hands the buffered messages, then all later ones, to `handler`. Only the first call counts. */
|
|
283
|
+
connect(handler) {
|
|
284
|
+
if (this.#handler !== undefined || this.#released) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.#handler = handler;
|
|
288
|
+
const buffered = this.#buffered;
|
|
289
|
+
this.#buffered = [];
|
|
290
|
+
this.#bufferedBytes = 0;
|
|
291
|
+
for (const message of buffered) {
|
|
292
|
+
handler(message);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** See DatagramEndpoint.frameRoom; 0 once the flow is released. */
|
|
297
|
+
get frameRoom() {
|
|
298
|
+
return this.#released ? 0 : this.#endpoint.frameRoom(this.id);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** See DatagramEndpoint.send. */
|
|
302
|
+
send(messages, signal) {
|
|
303
|
+
return this.#released ? Promise.resolve(false) : this.#endpoint.send(this.id, messages, signal);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Ends the flow: later datagrams for it are dropped. */
|
|
307
|
+
release() {
|
|
308
|
+
if (!this.#released) {
|
|
309
|
+
this.#released = true;
|
|
310
|
+
this.#handler = undefined;
|
|
311
|
+
this.#buffered = [];
|
|
312
|
+
this.#endpoint.closeFlow(this.id);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Settles like `promise`, or rejects with the abort reason as soon as `signal` aborts. */
|
|
318
|
+
function abortable(promise, signal) {
|
|
319
|
+
if (signal.aborted) {
|
|
320
|
+
return Promise.reject(signal.reason);
|
|
321
|
+
}
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const onAbort = () => reject(signal.reason);
|
|
324
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
325
|
+
promise.then(
|
|
326
|
+
(value) => {
|
|
327
|
+
signal.removeEventListener("abort", onAbort);
|
|
328
|
+
resolve(value);
|
|
329
|
+
},
|
|
330
|
+
(error) => {
|
|
331
|
+
signal.removeEventListener("abort", onAbort);
|
|
332
|
+
reject(error);
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
}
|
package/src/framing.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { byteView } from "./bytes.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { internal, invalidArgument, resourceExhausted, unavailable } from "./status.js";
|
|
3
|
+
import { readVarint, varintLength, writeVarint } from "./varint.js";
|
|
4
|
+
import { RpcHeaders, RpcRequest, RpcStatus, preflightWireMessage } from "./wire.js";
|
|
4
5
|
|
|
5
6
|
export const DefaultMaxFrameSize = 4 * 1024 * 1024;
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** Frame types; every other type is unknown and skipped by receivers. */
|
|
9
|
+
export const FrameType = Object.freeze({
|
|
10
|
+
Message: 0,
|
|
11
|
+
Request: 1,
|
|
12
|
+
Headers: 2,
|
|
13
|
+
Status: 3,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const KnownFrameTypes = new Set(Object.values(FrameType));
|
|
10
17
|
const EmptyBytes = new Uint8Array(0);
|
|
11
18
|
|
|
12
19
|
/** Encodes a protobuf message body. */
|
|
@@ -34,63 +41,51 @@ export function unmarshalMessage(messageType, body) {
|
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
/** Encodes
|
|
38
|
-
export function encodeFrame(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
new DataView(frame.buffer).setUint32(0, body.byteLength, false);
|
|
43
|
-
frame.set(body, HeaderLength);
|
|
44
|
+
/** Encodes one frame carrying an already-encoded payload. */
|
|
45
|
+
export function encodeFrame(type, payload, maxFrameSize = DefaultMaxFrameSize) {
|
|
46
|
+
checkFrameLength(payload.byteLength, maxFrameSize);
|
|
47
|
+
const frame = new Uint8Array(frameSize(type, payload.byteLength));
|
|
48
|
+
writeFrame(frame, 0, type, payload);
|
|
44
49
|
return frame;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
/** Encodes a MESSAGE frame; the payload is the raw protobuf message. */
|
|
53
|
+
export function encodeMessageFrame(body, maxFrameSize = DefaultMaxFrameSize) {
|
|
54
|
+
return encodeFrame(FrameType.Message, body, maxFrameSize);
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
/**
|
|
48
|
-
* Encodes a
|
|
49
|
-
*
|
|
50
|
-
* intermediate message.
|
|
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.
|
|
51
60
|
*/
|
|
52
|
-
export function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
frame.set(body, offset);
|
|
64
|
-
}
|
|
61
|
+
export function encodeRequestFrame(request, message, maxFrameSize = DefaultMaxFrameSize) {
|
|
62
|
+
const payload = marshalMessage(RpcRequest, request);
|
|
63
|
+
checkFrameLength(payload.byteLength, maxFrameSize);
|
|
64
|
+
if (message === undefined) {
|
|
65
|
+
return encodeFrame(FrameType.Request, payload, maxFrameSize);
|
|
66
|
+
}
|
|
67
|
+
checkFrameLength(message.byteLength, maxFrameSize);
|
|
68
|
+
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);
|
|
71
|
+
writeFrame(frame, 0, FrameType.Request, payload);
|
|
65
72
|
return frame;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
/**
|
|
69
|
-
export function
|
|
70
|
-
|
|
71
|
-
const messageBody = bareMessageBody(bytes);
|
|
72
|
-
if (messageBody !== undefined) {
|
|
73
|
-
return { kind: RpcStreamFrameKind.Message, status: Code.Ok, message: "", body: messageBody };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const frame = unmarshalMessage(RpcStreamFrame, bytes);
|
|
77
|
-
if (frame.kind !== RpcStreamFrameKind.Message && frame.kind !== RpcStreamFrameKind.Status) {
|
|
78
|
-
throw invalidArgument(`unsupported stream frame kind ${frame.kind}`);
|
|
79
|
-
}
|
|
80
|
-
return frame;
|
|
75
|
+
/** Encodes a HEADERS frame from RpcHeaders fields. */
|
|
76
|
+
export function encodeHeadersFrame(headers, maxFrameSize = DefaultMaxFrameSize) {
|
|
77
|
+
return encodeFrame(FrameType.Headers, marshalMessage(RpcHeaders, headers), maxFrameSize);
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
/**
|
|
84
|
-
export function
|
|
85
|
-
|
|
86
|
-
0,
|
|
87
|
-
false,
|
|
88
|
-
);
|
|
89
|
-
checkFrameLength(length, maxFrameSize);
|
|
90
|
-
return length;
|
|
80
|
+
/** Encodes a STATUS frame from RpcStatus fields. */
|
|
81
|
+
export function encodeStatusFrame(status, maxFrameSize = DefaultMaxFrameSize) {
|
|
82
|
+
return encodeFrame(FrameType.Status, marshalMessage(RpcStatus, status), maxFrameSize);
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Reads frames from a byte stream reader. Frame headers may be split across chunks; payloads
|
|
87
|
+
* inside one chunk are returned as views without copying. Unknown frame types are skipped.
|
|
88
|
+
*/
|
|
94
89
|
export class FrameReader {
|
|
95
90
|
#reader;
|
|
96
91
|
/** Bytes received from the reader but not yet consumed. */
|
|
@@ -101,44 +96,61 @@ export class FrameReader {
|
|
|
101
96
|
this.#reader = reader;
|
|
102
97
|
}
|
|
103
98
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Reads the next known frame as `{ type, payload }`, or returns null when the stream ended
|
|
101
|
+
* cleanly between frames. The declared length of every frame, unknown ones included, is checked
|
|
102
|
+
* against maxFrameSize before any payload is read or allocated.
|
|
103
|
+
*/
|
|
104
|
+
async readFrame(maxFrameSize = DefaultMaxFrameSize) {
|
|
105
|
+
for (;;) {
|
|
106
|
+
const type = await this.#readVarint(true);
|
|
107
|
+
if (type === null) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const length = await this.#readVarint(false);
|
|
111
|
+
checkFrameLength(length, maxFrameSize);
|
|
112
|
+
if (!KnownFrameTypes.has(type)) {
|
|
113
|
+
// Types of 2^53 and above are bigints, which are never known.
|
|
114
|
+
await this.#skip(length);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
return { type, payload: await this.#readExact(length) };
|
|
118
|
+
}
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Resolves when the rest of the stream consists of unknown frames and a clean end; rejects if
|
|
123
|
+
* a known frame or a malformed frame arrives first.
|
|
124
|
+
*/
|
|
125
|
+
async expectEnd(maxFrameSize = DefaultMaxFrameSize) {
|
|
126
|
+
const frame = await this.readFrame(maxFrameSize);
|
|
127
|
+
if (frame !== null) {
|
|
128
|
+
throw internal(`frame of type ${frame.type} after the final STATUS frame`);
|
|
129
|
+
}
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
/**
|
|
116
|
-
async
|
|
117
|
-
|
|
132
|
+
/** Reads a varint; null if allowEnd and the stream ended before its first byte. */
|
|
133
|
+
async #readVarint(allowEnd) {
|
|
134
|
+
while (this.#pending.byteLength === 0) {
|
|
118
135
|
const { value, done } = await this.#nextChunk();
|
|
119
136
|
if (done) {
|
|
120
|
-
|
|
137
|
+
if (allowEnd) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
throw unavailable("stream ended before a complete frame");
|
|
121
141
|
}
|
|
122
142
|
this.#pending = value;
|
|
123
143
|
}
|
|
124
|
-
|
|
144
|
+
const bytes = await this.#readExact(1 << (this.#pending[0] >> 6));
|
|
145
|
+
return readVarint(bytes, 0).value;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (header === null) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
return await this.#readExact(frameBodyLength(header, maxFrameSize), false);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Returns exactly size bytes, or null if allowEnd and the stream ended before the first byte. */
|
|
136
|
-
async #readExact(size, allowEnd) {
|
|
148
|
+
/** Returns exactly size bytes; copies only when the read spans chunks. */
|
|
149
|
+
async #readExact(size) {
|
|
137
150
|
if (this.#pending.byteLength >= size) {
|
|
138
151
|
return this.#take(size);
|
|
139
152
|
}
|
|
140
153
|
|
|
141
|
-
// Copies are needed only when a read spans chunks; whole frames inside one chunk are views.
|
|
142
154
|
const out = new Uint8Array(size);
|
|
143
155
|
let filled = this.#pending.byteLength;
|
|
144
156
|
out.set(this.#pending);
|
|
@@ -146,9 +158,6 @@ export class FrameReader {
|
|
|
146
158
|
while (filled < size) {
|
|
147
159
|
const { value, done } = await this.#nextChunk();
|
|
148
160
|
if (done) {
|
|
149
|
-
if (allowEnd && filled === 0) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
161
|
throw unavailable("stream ended before a complete frame");
|
|
153
162
|
}
|
|
154
163
|
const count = Math.min(value.byteLength, size - filled);
|
|
@@ -159,6 +168,23 @@ export class FrameReader {
|
|
|
159
168
|
return out;
|
|
160
169
|
}
|
|
161
170
|
|
|
171
|
+
/** Discards size bytes without buffering them. */
|
|
172
|
+
async #skip(size) {
|
|
173
|
+
let left = size;
|
|
174
|
+
while (left > 0) {
|
|
175
|
+
if (this.#pending.byteLength === 0) {
|
|
176
|
+
const { value, done } = await this.#nextChunk();
|
|
177
|
+
if (done) {
|
|
178
|
+
throw unavailable("stream ended before a complete frame");
|
|
179
|
+
}
|
|
180
|
+
this.#pending = value;
|
|
181
|
+
}
|
|
182
|
+
const count = Math.min(this.#pending.byteLength, left);
|
|
183
|
+
this.#pending = this.#pending.subarray(count);
|
|
184
|
+
left -= count;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
162
188
|
async #nextChunk() {
|
|
163
189
|
for (;;) {
|
|
164
190
|
const { value, done } = await this.#reader.read();
|
|
@@ -178,47 +204,18 @@ export class FrameReader {
|
|
|
178
204
|
}
|
|
179
205
|
}
|
|
180
206
|
|
|
207
|
+
/** Rejects payload lengths above the limit; length may be a bigint for huge varints. */
|
|
181
208
|
function checkFrameLength(length, maxFrameSize) {
|
|
182
209
|
if (length > maxFrameSize) {
|
|
183
210
|
throw resourceExhausted(`frame length ${length} exceeds maximum ${maxFrameSize}`);
|
|
184
211
|
}
|
|
185
212
|
}
|
|
186
213
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (bytes.byteLength === 0) {
|
|
190
|
-
return EmptyBytes;
|
|
191
|
-
}
|
|
192
|
-
if (bytes[0] !== StreamFrameBodyTag) {
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
let length = 0;
|
|
197
|
-
let offset = 1;
|
|
198
|
-
for (let shift = 0; shift < 35 && offset < bytes.byteLength; shift += 7) {
|
|
199
|
-
const byte = bytes[offset++];
|
|
200
|
-
length += (byte & 0x7f) * 2 ** shift;
|
|
201
|
-
if (byte < 0x80) {
|
|
202
|
-
return offset + length === bytes.byteLength ? bytes.subarray(offset) : undefined;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return undefined;
|
|
214
|
+
function frameSize(type, length) {
|
|
215
|
+
return varintLength(type) + varintLength(length) + length;
|
|
206
216
|
}
|
|
207
217
|
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
output[offset++] = (remaining % 0x80) | 0x80;
|
|
212
|
-
remaining = Math.floor(remaining / 0x80);
|
|
213
|
-
}
|
|
214
|
-
output[offset++] = remaining;
|
|
215
|
-
return offset;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function varintLength(value) {
|
|
219
|
-
let length = 1;
|
|
220
|
-
for (let remaining = value; remaining >= 0x80; remaining = Math.floor(remaining / 0x80)) {
|
|
221
|
-
length += 1;
|
|
222
|
-
}
|
|
223
|
-
return length;
|
|
218
|
+
function writeFrame(output, offset, type, payload) {
|
|
219
|
+
const next = writeVarint(output, writeVarint(output, offset, type), payload.byteLength);
|
|
220
|
+
output.set(payload, next);
|
|
224
221
|
}
|