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