@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.
@@ -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 { 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 { 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
+ });
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 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);
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 streaming message frame carrying an already-encoded protobuf body.
49
- * This is byte-identical to encodeFrame(RpcStreamFrame, { body }) without building the
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 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
- }
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
- /** 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
- }
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
- /** 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;
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
- /** Reads length-prefixed frames from a byte stream reader. */
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
- /** 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));
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
- /** 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);
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
- /** Resolves when the stream ends cleanly; rejects if more data arrives first. */
116
- async expectEnd() {
117
- if (this.#pending.byteLength === 0) {
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
- return;
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
- throw unavailable("stream carried data after the final frame");
144
+ const bytes = await this.#readExact(1 << (this.#pending[0] >> 6));
145
+ return readVarint(bytes, 0).value;
125
146
  }
126
147
 
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);
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
- /** 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;
214
+ function frameSize(type, length) {
215
+ return varintLength(type) + varintLength(length) + length;
206
216
  }
207
217
 
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;
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
  }