@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,740 @@
1
+ // TrevRPC over QMux over WebSocket (draft-ietf-quic-qmux-02, draft-lcurley-qmux-websocket-00):
2
+ // the browser fallback when WebTransport is unavailable or blocked. A session exposes the part of
3
+ // the WebTransport API that the channel uses, so calls run on it unchanged.
4
+ import { DefaultMaxFrameSize } from "../framing.js";
5
+ import {
6
+ FrameParser,
7
+ FrameType,
8
+ FrameWriter,
9
+ QMuxProtocolError,
10
+ TransportError,
11
+ streamFrameOverhead,
12
+ writeConnectionClose,
13
+ writeMaxData,
14
+ writeMaxStreamData,
15
+ writePing,
16
+ writeResetStream,
17
+ writeStopSending,
18
+ writeStreamFrame,
19
+ } from "./frames.js";
20
+ import { decodeTransportParameters, writeTransportParameters } from "./params.js";
21
+
22
+ /** The WebSocket subprotocol of TrevRPC over QMux. */
23
+ export const QMuxProtocol = "qmux-02.trevrpc-1";
24
+
25
+ /** The largest record this endpoint accepts and sends. */
26
+ const MaxRecordSize = 64 * 1024;
27
+ /** Stream error code for streams abandoned locally. */
28
+ const CancelledCode = 1;
29
+ /** Writes wait while the WebSocket buffers more than this many bytes. */
30
+ const SendBufferLimit = 1024 * 1024;
31
+ const SendBufferPollMs = 5;
32
+ /** Bounds opening the WebSocket and receiving the server's transport parameters. */
33
+ const HandshakeTimeoutMs = 10_000;
34
+
35
+ /** An error shaped like WebTransportError, so callers treat both transports alike. */
36
+ function transportError(message, { source = "session", streamErrorCode = null, cause } = {}) {
37
+ const error = new Error(message, cause === undefined ? undefined : { cause });
38
+ error.name = "WebTransportError";
39
+ error.source = source;
40
+ error.streamErrorCode = streamErrorCode;
41
+ return error;
42
+ }
43
+
44
+ function protocolError(code, message, frameType) {
45
+ return new QMuxProtocolError(code, message, frameType);
46
+ }
47
+
48
+ /** A QMux session over one WebSocket. */
49
+ export class QMuxWebSocketSession {
50
+ /** Resolves once the server's transport parameters have arrived. */
51
+ ready;
52
+ /** Resolves with `{ closeCode, reason }` after a clean close and rejects otherwise. */
53
+ closed;
54
+
55
+ #ws;
56
+ #window;
57
+ #peer = null;
58
+ #streams = new Map();
59
+ #openedLocal = 0;
60
+ #maxLocal = 0;
61
+ #connSendMax = 0;
62
+ #connSent = 0;
63
+ #connRecvMax;
64
+ #connReceived = 0;
65
+ #connConsumed = 0;
66
+ #pending = new FrameWriter();
67
+ #flushScheduled = false;
68
+ #waiters = new Set();
69
+ /** The highest QX_PING request sequence received, -1 before the first: a peer may start at 0. */
70
+ #pingReceived = -1;
71
+ #closedError = null;
72
+ #lastActivity = Date.now();
73
+ #idleTimer;
74
+ #handshakeTimer;
75
+ #settleReady;
76
+ #settleClosed;
77
+
78
+ /**
79
+ * Opens a session to a `wss://` URL. `options.WebSocket` replaces the global WebSocket
80
+ * constructor; `options.maxFrameSize` sizes the receive windows like the native clients.
81
+ */
82
+ constructor(url, options = {}) {
83
+ const WebSocketConstructor = options.WebSocket ?? globalThis.WebSocket;
84
+ if (typeof WebSocketConstructor !== "function") {
85
+ throw transportError("WebSocket is not available in this JavaScript runtime");
86
+ }
87
+ // A frame header is at most 16 bytes, so the window fits one maximum-size frame.
88
+ this.#window = (options.maxFrameSize ?? DefaultMaxFrameSize) + 16;
89
+ this.#connRecvMax = this.#window;
90
+ this.ready = new Promise((resolve, reject) => {
91
+ this.#settleReady = { resolve, reject };
92
+ });
93
+ this.closed = new Promise((resolve, reject) => {
94
+ this.#settleClosed = { resolve, reject };
95
+ });
96
+ this.ready.catch(() => {});
97
+ this.closed.catch(() => {});
98
+
99
+ const ws = new WebSocketConstructor(url, [QMuxProtocol]);
100
+ ws.binaryType = "arraybuffer";
101
+ ws.onopen = () => this.#opened();
102
+ ws.onmessage = (event) => this.#received(event.data);
103
+ ws.onerror = () => this.#finish(transportError("WebSocket error"), false);
104
+ ws.onclose = (event) =>
105
+ this.#finish(transportError(`WebSocket closed with code ${event.code}`), false);
106
+ this.#ws = ws;
107
+ this.#handshakeTimer = setTimeout(
108
+ () => this.#finish(transportError("QMux handshake timed out"), false),
109
+ options.handshakeTimeoutMs ?? HandshakeTimeoutMs,
110
+ );
111
+ }
112
+
113
+ /** Opens a bidirectional stream, waiting for the server's stream limit. */
114
+ async createBidirectionalStream() {
115
+ await this.ready;
116
+ for (;;) {
117
+ if (this.#closedError) {
118
+ throw this.#closedError;
119
+ }
120
+ if (this.#openedLocal < this.#maxLocal) {
121
+ break;
122
+ }
123
+ await this.#wait();
124
+ }
125
+ const id = this.#openedLocal * 4;
126
+ this.#openedLocal += 1;
127
+ const stream = new QMuxStream(
128
+ this,
129
+ id,
130
+ this.#peer.initialMaxStreamDataBidiRemote,
131
+ this.#window,
132
+ );
133
+ this.#streams.set(id, stream);
134
+ return { readable: stream.readable, writable: stream.writable };
135
+ }
136
+
137
+ /** Closes the session with an application CONNECTION_CLOSE. */
138
+ close(closeInfo = {}) {
139
+ if (this.#closedError) {
140
+ return;
141
+ }
142
+ const code = closeInfo.closeCode ?? 0;
143
+ const reason = closeInfo.reason ?? "";
144
+ writeConnectionClose(this.#pending, { code, reason });
145
+ this.#flush();
146
+ this.#finish(transportError("session closed locally"), true, { closeCode: code, reason });
147
+ }
148
+
149
+ #opened() {
150
+ if (this.#ws.protocol !== QMuxProtocol) {
151
+ this.#finish(
152
+ transportError(`server selected WebSocket subprotocol "${this.#ws.protocol}"`),
153
+ false,
154
+ );
155
+ return;
156
+ }
157
+ // The transport parameters must be the first frame on the wire.
158
+ writeTransportParameters(this.#pending, {
159
+ initialMaxData: this.#window,
160
+ initialMaxStreamDataBidiLocal: this.#window,
161
+ maxRecordSize: MaxRecordSize,
162
+ });
163
+ this.#flush();
164
+ }
165
+
166
+ #received(data) {
167
+ if (this.#closedError) {
168
+ return;
169
+ }
170
+ this.#lastActivity = Date.now();
171
+ try {
172
+ if (typeof data === "string") {
173
+ throw protocolError(TransportError.ProtocolViolation, "text message", 0);
174
+ }
175
+ const record = new Uint8Array(data);
176
+ if (record.length > MaxRecordSize) {
177
+ throw protocolError(TransportError.FrameEncodingError, "record exceeds max_record_size", 0);
178
+ }
179
+ const parser = new FrameParser(record);
180
+ for (let frame = parser.next(); frame !== null && !this.#closedError; frame = parser.next()) {
181
+ if (this.#peer === null && frame.type !== FrameType.TransportParameters) {
182
+ throw protocolError(
183
+ TransportError.ProtocolViolation,
184
+ "first frame is not QX_TRANSPORT_PARAMETERS",
185
+ frame.type,
186
+ );
187
+ }
188
+ this.#handle(frame);
189
+ }
190
+ } catch (error) {
191
+ if (!(error instanceof QMuxProtocolError)) {
192
+ throw error;
193
+ }
194
+ writeConnectionClose(this.#pending, { protocolError: error });
195
+ this.#flush();
196
+ this.#finish(
197
+ transportError(`QMux protocol error: ${error.message}`, { cause: error }),
198
+ false,
199
+ );
200
+ }
201
+ }
202
+
203
+ #handle(frame) {
204
+ switch (frame.type) {
205
+ case FrameType.TransportParameters:
206
+ this.#parameters(frame);
207
+ return;
208
+ case FrameType.Padding:
209
+ case FrameType.DataBlocked:
210
+ return;
211
+ case FrameType.ResetStream:
212
+ this.#stream(frame)?.onReset(frame.code, frame.finalSize);
213
+ return;
214
+ case FrameType.StopSending:
215
+ this.#stream(frame)?.onStopSending(frame.code);
216
+ return;
217
+ case FrameType.MaxData:
218
+ if (frame.maximum > this.#connSendMax) {
219
+ this.#connSendMax = frame.maximum;
220
+ this.#wake();
221
+ }
222
+ return;
223
+ case FrameType.MaxStreamData: {
224
+ const stream = this.#stream(frame);
225
+ if (stream && frame.maximum > stream.sendMax) {
226
+ stream.sendMax = frame.maximum;
227
+ this.#wake();
228
+ }
229
+ return;
230
+ }
231
+ case FrameType.MaxStreamsBidi:
232
+ case FrameType.MaxStreamsUni:
233
+ case FrameType.StreamsBlockedBidi:
234
+ case FrameType.StreamsBlockedUni:
235
+ if (frame.maximum > 2 ** 60) {
236
+ throw protocolError(
237
+ TransportError.FrameEncodingError,
238
+ "stream count exceeds 2^60",
239
+ frame.type,
240
+ );
241
+ }
242
+ if (frame.type === FrameType.MaxStreamsBidi && frame.maximum > this.#maxLocal) {
243
+ this.#maxLocal = frame.maximum;
244
+ this.#wake();
245
+ }
246
+ return;
247
+ case FrameType.StreamDataBlocked:
248
+ this.#stream(frame);
249
+ return;
250
+ case FrameType.ConnectionClose:
251
+ case FrameType.ApplicationClose: {
252
+ const clean = frame.type === FrameType.ApplicationClose && frame.code === 0;
253
+ const message = `session closed by peer with code ${frame.code}${frame.reason ? `: ${frame.reason}` : ""}`;
254
+ this.#finish(transportError(message), clean, {
255
+ closeCode: frame.code,
256
+ reason: frame.reason,
257
+ });
258
+ return;
259
+ }
260
+ case FrameType.Ping:
261
+ if (frame.sequence <= this.#pingReceived) {
262
+ throw protocolError(
263
+ TransportError.ProtocolViolation,
264
+ "QX_PING sequence is not increasing",
265
+ frame.type,
266
+ );
267
+ }
268
+ this.#pingReceived = frame.sequence;
269
+ writePing(this.#pending, FrameType.PingResponse, frame.sequence);
270
+ this.#scheduleFlush();
271
+ return;
272
+ case FrameType.PingResponse:
273
+ throw protocolError(
274
+ TransportError.ProtocolViolation,
275
+ "QX_PING response was never requested",
276
+ frame.type,
277
+ );
278
+ default:
279
+ if (frame.data !== undefined) {
280
+ this.#stream(frame)?.onData(frame.offset, frame.data, frame.fin);
281
+ }
282
+ }
283
+ }
284
+
285
+ #parameters(frame) {
286
+ if (this.#peer !== null) {
287
+ throw protocolError(
288
+ TransportError.ProtocolViolation,
289
+ "repeated QX_TRANSPORT_PARAMETERS",
290
+ frame.type,
291
+ );
292
+ }
293
+ this.#peer = decodeTransportParameters(frame.params);
294
+ this.#maxLocal = this.#peer.initialMaxStreamsBidi;
295
+ this.#connSendMax = this.#peer.initialMaxData;
296
+ const idleMs = this.#peer.maxIdleTimeoutMs;
297
+ if (idleMs > 0) {
298
+ this.#idleTimer = setInterval(
299
+ () => {
300
+ if (Date.now() - this.#lastActivity >= idleMs) {
301
+ this.#finish(transportError("QMux session idle timeout"), false);
302
+ }
303
+ },
304
+ Math.max(10, idleMs / 4),
305
+ );
306
+ }
307
+ clearTimeout(this.#handshakeTimer);
308
+ this.#settleReady.resolve();
309
+ }
310
+
311
+ /** The stream a frame refers to, or undefined once it has completed. */
312
+ #stream(frame) {
313
+ const id = frame.streamId;
314
+ if (id % 4 !== 0) {
315
+ // The server may open no streams of either kind.
316
+ throw protocolError(
317
+ TransportError.StreamLimitError,
318
+ `stream ${id} exceeds the stream limit`,
319
+ frame.type,
320
+ );
321
+ }
322
+ if (id / 4 >= this.#openedLocal) {
323
+ throw protocolError(
324
+ TransportError.StreamStateError,
325
+ `stream ${id} was never opened`,
326
+ frame.type,
327
+ );
328
+ }
329
+ return this.#streams.get(id);
330
+ }
331
+
332
+ // Called by streams.
333
+
334
+ /** Accounts for stream data received from the peer. */
335
+ received(length, frameType) {
336
+ this.#connReceived += length;
337
+ if (this.#connReceived > this.#connRecvMax) {
338
+ throw protocolError(
339
+ TransportError.FlowControlError,
340
+ "connection flow control limit exceeded",
341
+ frameType,
342
+ );
343
+ }
344
+ }
345
+
346
+ /** Returns connection credit for received data that was read or discarded. */
347
+ consumed(length) {
348
+ this.#connConsumed += length;
349
+ if (this.#connRecvMax - this.#connConsumed <= this.#window / 2) {
350
+ this.#connRecvMax = this.#connConsumed + this.#window;
351
+ writeMaxData(this.#pending, this.#connRecvMax);
352
+ this.#scheduleFlush();
353
+ }
354
+ }
355
+
356
+ maxStreamData(stream) {
357
+ writeMaxStreamData(this.#pending, stream.id, stream.recvMax);
358
+ this.#scheduleFlush();
359
+ }
360
+
361
+ stopSending(stream, code) {
362
+ writeStopSending(this.#pending, stream.id, code);
363
+ this.#scheduleFlush();
364
+ }
365
+
366
+ resetStream(stream, code) {
367
+ writeResetStream(this.#pending, stream.id, code, stream.sendOffset);
368
+ this.#scheduleFlush();
369
+ }
370
+
371
+ /** Bytes `stream` may send now, waiting for flow control credit and socket buffer space. */
372
+ async sendCredit(stream) {
373
+ for (;;) {
374
+ if (stream.writeError) {
375
+ throw stream.writeError;
376
+ }
377
+ if (this.#closedError) {
378
+ throw this.#closedError;
379
+ }
380
+ const credit = Math.min(
381
+ stream.sendMax - stream.sendOffset,
382
+ this.#connSendMax - this.#connSent,
383
+ );
384
+ if (credit > 0 && this.#ws.bufferedAmount <= SendBufferLimit) {
385
+ return Math.min(credit, this.#recordLimit() - 32);
386
+ }
387
+ if (credit > 0) {
388
+ await new Promise((resolve) => setTimeout(resolve, SendBufferPollMs));
389
+ } else {
390
+ await this.#wait();
391
+ }
392
+ }
393
+ }
394
+
395
+ sendStream(stream, data, fin) {
396
+ const overhead = streamFrameOverhead(stream.id, stream.sendOffset, data.length);
397
+ if (this.#pending.length + overhead + data.length > this.#recordLimit()) {
398
+ this.#flush();
399
+ }
400
+ writeStreamFrame(this.#pending, stream.id, stream.sendOffset, data, fin);
401
+ stream.sendOffset += data.length;
402
+ this.#connSent += data.length;
403
+ this.#scheduleFlush();
404
+ }
405
+
406
+ completed(stream) {
407
+ this.#streams.delete(stream.id);
408
+ }
409
+
410
+ /** Wakes writers waiting for credit so they notice that their stream failed. */
411
+ wakeSenders() {
412
+ this.#wake();
413
+ }
414
+
415
+ get closedError() {
416
+ return this.#closedError;
417
+ }
418
+
419
+ #recordLimit() {
420
+ return Math.min(this.#peer?.maxRecordSize ?? MaxRecordSize, MaxRecordSize);
421
+ }
422
+
423
+ /** Waits for a change in credit or session state. */
424
+ #wait() {
425
+ return new Promise((resolve) => this.#waiters.add(resolve));
426
+ }
427
+
428
+ #wake() {
429
+ const waiters = [...this.#waiters];
430
+ this.#waiters.clear();
431
+ for (const resolve of waiters) {
432
+ resolve();
433
+ }
434
+ }
435
+
436
+ #scheduleFlush() {
437
+ if (!this.#flushScheduled) {
438
+ this.#flushScheduled = true;
439
+ queueMicrotask(() => {
440
+ this.#flushScheduled = false;
441
+ this.#flush();
442
+ });
443
+ }
444
+ }
445
+
446
+ /** Sends the pending frames as one record. */
447
+ #flush() {
448
+ if (this.#pending.length === 0) {
449
+ return;
450
+ }
451
+ const record = this.#pending.take();
452
+ if (this.#ws.readyState === 1) {
453
+ this.#ws.send(record);
454
+ this.#lastActivity = Date.now();
455
+ }
456
+ }
457
+
458
+ #finish(error, clean, closeInfo) {
459
+ if (this.#closedError) {
460
+ return;
461
+ }
462
+ this.#closedError = error;
463
+ clearInterval(this.#idleTimer);
464
+ clearTimeout(this.#handshakeTimer);
465
+ for (const stream of this.#streams.values()) {
466
+ stream.onSessionClosed(error);
467
+ }
468
+ this.#streams.clear();
469
+ this.#settleReady.reject(error);
470
+ if (clean) {
471
+ this.#settleClosed.resolve(closeInfo);
472
+ } else {
473
+ this.#settleClosed.reject(error);
474
+ }
475
+ this.#wake();
476
+ try {
477
+ this.#ws.close(1000);
478
+ } catch {
479
+ // Already closing.
480
+ }
481
+ }
482
+ }
483
+
484
+ /** One bidirectional stream: WHATWG streams over STREAM, RESET_STREAM and STOP_SENDING. */
485
+ class QMuxStream {
486
+ readable;
487
+ writable;
488
+ sendOffset = 0;
489
+ sendMax;
490
+ recvMax;
491
+ writeError = null;
492
+
493
+ #session;
494
+ #window;
495
+ #received = 0;
496
+ #read = 0;
497
+ #chunks = [];
498
+ #finalSize = null;
499
+ #finReceived = false;
500
+ #readError = null;
501
+ #readCancelled = false;
502
+ #sendDone = false;
503
+ #pullWaiter = null;
504
+ #writeController;
505
+
506
+ constructor(session, id, sendMax, window) {
507
+ this.id = id;
508
+ this.#session = session;
509
+ this.sendMax = sendMax;
510
+ this.recvMax = window;
511
+ this.#window = window;
512
+ this.readable = new ReadableStream(
513
+ { pull: (controller) => this.#pull(controller), cancel: () => this.#cancelRead() },
514
+ { highWaterMark: 0 },
515
+ );
516
+ this.writable = new WritableStream({
517
+ start: (controller) => {
518
+ this.#writeController = controller;
519
+ // An abort waits for the write in flight, which may be waiting for credit that never
520
+ // comes; the controller's signal fires at once, so the stream is reset and the write fails.
521
+ controller.signal.addEventListener(
522
+ "abort",
523
+ () => this.#abortSend(controller.signal.reason),
524
+ {
525
+ once: true,
526
+ },
527
+ );
528
+ },
529
+ write: (chunk) => this.#write(chunk),
530
+ close: () => this.#finishSend(),
531
+ abort: () => this.#resetSend(CancelledCode),
532
+ });
533
+ }
534
+
535
+ onData(offset, data, fin) {
536
+ const end = offset + data.length;
537
+ if (this.#finalSize !== null && (end > this.#finalSize || (fin && end !== this.#finalSize))) {
538
+ throw protocolError(
539
+ TransportError.FinalSizeError,
540
+ `stream ${this.id} data beyond its final size`,
541
+ FrameType.Stream,
542
+ );
543
+ }
544
+ if (offset !== this.#received) {
545
+ throw protocolError(
546
+ TransportError.ProtocolViolation,
547
+ `stream ${this.id} data out of order`,
548
+ FrameType.Stream,
549
+ );
550
+ }
551
+ if (end > this.recvMax) {
552
+ throw protocolError(
553
+ TransportError.FlowControlError,
554
+ `stream ${this.id} exceeds its flow control limit`,
555
+ FrameType.Stream,
556
+ );
557
+ }
558
+ this.#session.received(data.length, FrameType.Stream);
559
+ this.#received = end;
560
+ if (fin) {
561
+ this.#finalSize = end;
562
+ this.#finReceived = true;
563
+ }
564
+ if (this.#readCancelled || this.#readError) {
565
+ this.#read = end;
566
+ this.#session.consumed(data.length);
567
+ } else if (data.length > 0) {
568
+ this.#chunks.push(data);
569
+ }
570
+ this.#wakePull();
571
+ this.#maybeComplete();
572
+ }
573
+
574
+ onReset(code, finalSize) {
575
+ if (this.#finReceived && finalSize === this.#finalSize) {
576
+ return;
577
+ }
578
+ if ((this.#finalSize !== null && finalSize !== this.#finalSize) || finalSize < this.#received) {
579
+ throw protocolError(
580
+ TransportError.FinalSizeError,
581
+ `stream ${this.id} reset with an inconsistent final size`,
582
+ FrameType.ResetStream,
583
+ );
584
+ }
585
+ if (finalSize > this.recvMax) {
586
+ throw protocolError(
587
+ TransportError.FlowControlError,
588
+ `stream ${this.id} final size exceeds its flow control limit`,
589
+ FrameType.ResetStream,
590
+ );
591
+ }
592
+ this.#session.received(finalSize - this.#received, FrameType.ResetStream);
593
+ this.#received = finalSize;
594
+ this.#finalSize = finalSize;
595
+ if (!this.#readCancelled && !this.#readError) {
596
+ this.#readError = transportError(`stream reset by peer with code ${code}`, {
597
+ source: "stream",
598
+ streamErrorCode: code,
599
+ });
600
+ }
601
+ this.#session.consumed(finalSize - this.#read);
602
+ this.#read = finalSize;
603
+ this.#chunks = [];
604
+ this.#wakePull();
605
+ this.#maybeComplete();
606
+ }
607
+
608
+ onStopSending(code) {
609
+ if (this.#sendDone) {
610
+ return;
611
+ }
612
+ this.writeError = transportError(`stream stopped by peer with code ${code}`, {
613
+ source: "stream",
614
+ streamErrorCode: code,
615
+ });
616
+ this.#resetSend(code);
617
+ this.#writeController.error(this.writeError);
618
+ }
619
+
620
+ /** Resets the send side because the writer was aborted, failing a write waiting for credit. */
621
+ #abortSend(reason) {
622
+ if (this.#sendDone) {
623
+ return;
624
+ }
625
+ this.writeError ??= transportError(`stream aborted: ${reason?.message ?? reason}`, {
626
+ source: "stream",
627
+ streamErrorCode: CancelledCode,
628
+ });
629
+ this.#resetSend(CancelledCode);
630
+ this.#session.wakeSenders();
631
+ }
632
+
633
+ onSessionClosed(error) {
634
+ this.#readError ??= error;
635
+ this.writeError ??= error;
636
+ this.#sendDone = true;
637
+ this.#wakePull();
638
+ try {
639
+ this.#writeController.error(error);
640
+ } catch {
641
+ // The writable already ended.
642
+ }
643
+ }
644
+
645
+ async #pull(controller) {
646
+ for (;;) {
647
+ if (this.#readError) {
648
+ controller.error(this.#readError);
649
+ return;
650
+ }
651
+ const chunk = this.#chunks.shift();
652
+ if (chunk !== undefined) {
653
+ controller.enqueue(chunk);
654
+ this.#consumed(chunk.length);
655
+ return;
656
+ }
657
+ if (this.#finReceived && this.#read === this.#finalSize) {
658
+ controller.close();
659
+ return;
660
+ }
661
+ await new Promise((resolve) => {
662
+ this.#pullWaiter = resolve;
663
+ });
664
+ }
665
+ }
666
+
667
+ #wakePull() {
668
+ const resolve = this.#pullWaiter;
669
+ this.#pullWaiter = null;
670
+ resolve?.();
671
+ }
672
+
673
+ #consumed(length) {
674
+ this.#read += length;
675
+ if (this.#finalSize === null && this.recvMax - this.#read <= this.#window / 2) {
676
+ this.recvMax = this.#read + this.#window;
677
+ this.#session.maxStreamData(this);
678
+ }
679
+ this.#session.consumed(length);
680
+ this.#maybeComplete();
681
+ }
682
+
683
+ #cancelRead() {
684
+ if (
685
+ this.#readCancelled ||
686
+ this.#readError ||
687
+ (this.#finReceived && this.#read === this.#finalSize)
688
+ ) {
689
+ return;
690
+ }
691
+ this.#readCancelled = true;
692
+ const unread = this.#received - this.#read;
693
+ this.#chunks = [];
694
+ this.#read = this.#received;
695
+ this.#session.consumed(unread);
696
+ if (this.#finalSize === null && !this.#session.closedError) {
697
+ this.#session.stopSending(this, CancelledCode);
698
+ }
699
+ this.#maybeComplete();
700
+ }
701
+
702
+ async #write(chunk) {
703
+ let data = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
704
+ while (data.length > 0) {
705
+ const allowed = await this.#session.sendCredit(this);
706
+ const piece = data.subarray(0, allowed);
707
+ this.#session.sendStream(this, piece, false);
708
+ data = data.subarray(piece.length);
709
+ }
710
+ }
711
+
712
+ #finishSend() {
713
+ if (this.#sendDone) {
714
+ return;
715
+ }
716
+ if (this.#session.closedError) {
717
+ throw this.#session.closedError;
718
+ }
719
+ this.#sendDone = true;
720
+ this.#session.sendStream(this, new Uint8Array(0), true);
721
+ this.#maybeComplete();
722
+ }
723
+
724
+ #resetSend(code) {
725
+ if (this.#sendDone) {
726
+ return;
727
+ }
728
+ this.#sendDone = true;
729
+ if (!this.#session.closedError) {
730
+ this.#session.resetStream(this, code);
731
+ }
732
+ this.#maybeComplete();
733
+ }
734
+
735
+ #maybeComplete() {
736
+ if (this.#sendDone && this.#finalSize !== null && this.#read === this.#finalSize) {
737
+ this.#session.completed(this);
738
+ }
739
+ }
740
+ }