@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.
@@ -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 { Code, invalidArgument, resourceExhausted, unavailable } from "./status.js";
3
- import { RpcStreamFrame, RpcStreamFrameKind, preflightWireMessage } from "./wire.js";
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
- const HeaderLength = 4;
8
- /** Tag of RpcStreamFrame.body: field 4, length-delimited. */
9
- const StreamFrameBodyTag = (4 << 3) | 2;
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 a protobuf message into a length-prefixed frame. */
38
- export function encodeFrame(messageType, message, maxFrameSize = DefaultMaxFrameSize) {
39
- const body = marshalMessage(messageType, message);
40
- checkFrameLength(body.byteLength, maxFrameSize);
41
- const frame = new Uint8Array(HeaderLength + body.byteLength);
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 a streaming message frame carrying an already-encoded protobuf body.
49
- * This is byte-identical to encodeFrame(RpcStreamFrame, { body }) without building the
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 encodeMessageFrame(body, maxFrameSize = DefaultMaxFrameSize) {
53
- const frameLength =
54
- body.byteLength === 0 ? 0 : 1 + varintLength(body.byteLength) + body.byteLength;
55
- checkFrameLength(frameLength, maxFrameSize);
56
-
57
- const frame = new Uint8Array(HeaderLength + frameLength);
58
- new DataView(frame.buffer).setUint32(0, frameLength, false);
59
- if (body.byteLength > 0) {
60
- let offset = HeaderLength;
61
- frame[offset++] = StreamFrameBodyTag;
62
- offset = writeVarint(frame, offset, body.byteLength);
63
- frame.set(body, offset);
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
- /** Decodes the body of a response stream frame, validating its kind. */
69
- export function decodeStreamFrame(body) {
70
- const bytes = byteView(body);
71
- const messageBody = bareMessageBody(bytes);
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
- 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;
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
- /** Validates the body length stored in a frame header. */
84
- export function frameBodyLength(header, maxFrameSize = DefaultMaxFrameSize) {
85
- const length = new DataView(header.buffer, header.byteOffset, header.byteLength).getUint32(
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
- /** Reads length-prefixed frames from a byte stream reader. */
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
- /** Reads and decodes one frame; a stream that ends first is an error. */
105
- async readFrame(messageType, maxFrameSize = DefaultMaxFrameSize) {
106
- return unmarshalMessage(messageType, await this.#readBody(maxFrameSize, false));
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
- /** Reads one streaming RPC frame, or returns null when the stream ended cleanly. */
110
- async readStreamFrame(maxFrameSize = DefaultMaxFrameSize) {
111
- const body = await this.#readBody(maxFrameSize, true);
112
- return body === null ? null : decodeStreamFrame(body);
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
- /** Resolves when the stream ends cleanly; rejects if more data arrives first. */
116
- async expectEnd() {
117
- if (this.#pending.byteLength === 0) {
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
- return;
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
- throw unavailable("stream carried data after the final frame");
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, or null if allowEnd and the stream ended before the first byte. */
136
- async #readExact(size, allowEnd) {
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
- /** Returns the body of a message-only stream frame, or undefined if it carries anything else. */
188
- function bareMessageBody(bytes) {
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 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;
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
  }