werift 0.22.8 → 0.23.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.
Files changed (69) hide show
  1. package/lib/common/src/crc.d.ts +4 -0
  2. package/lib/common/src/crc.js +124 -0
  3. package/lib/common/src/crc.js.map +1 -0
  4. package/lib/common/src/index.d.ts +1 -0
  5. package/lib/common/src/index.js +1 -0
  6. package/lib/common/src/index.js.map +1 -1
  7. package/lib/dtls/src/flight/server/flight6.js +12 -2
  8. package/lib/dtls/src/flight/server/flight6.js.map +1 -1
  9. package/lib/dtls/src/server.js +6 -1
  10. package/lib/dtls/src/server.js.map +1 -1
  11. package/lib/dtls/src/socket.d.ts +1 -0
  12. package/lib/dtls/src/socket.js +3 -0
  13. package/lib/dtls/src/socket.js.map +1 -1
  14. package/lib/ice/src/stun/message.js +2 -10
  15. package/lib/ice/src/stun/message.js.map +1 -1
  16. package/lib/index.mjs +705 -113
  17. package/lib/nonstandard/index.mjs +308 -44
  18. package/lib/rtp/src/index.d.ts +1 -0
  19. package/lib/rtp/src/index.js +1 -0
  20. package/lib/rtp/src/index.js.map +1 -1
  21. package/lib/rtp/src/srtp/cipher/ctr.d.ts +3 -3
  22. package/lib/rtp/src/srtp/cipher/ctr.js +25 -14
  23. package/lib/rtp/src/srtp/cipher/ctr.js.map +1 -1
  24. package/lib/rtp/src/srtp/cipher/gcm.d.ts +3 -3
  25. package/lib/rtp/src/srtp/cipher/gcm.js +57 -20
  26. package/lib/rtp/src/srtp/cipher/gcm.js.map +1 -1
  27. package/lib/rtp/src/srtp/cipher/index.d.ts +1 -1
  28. package/lib/rtp/src/srtp/cipher/index.js +1 -1
  29. package/lib/rtp/src/srtp/cipher/index.js.map +1 -1
  30. package/lib/rtp/src/srtp/context/srtp.d.ts +2 -1
  31. package/lib/rtp/src/srtp/context/srtp.js +22 -5
  32. package/lib/rtp/src/srtp/context/srtp.js.map +1 -1
  33. package/lib/rtp/src/srtp/error.d.ts +3 -0
  34. package/lib/rtp/src/srtp/error.js +11 -0
  35. package/lib/rtp/src/srtp/error.js.map +1 -0
  36. package/lib/rtp/src/srtp/packet.d.ts +5 -0
  37. package/lib/rtp/src/srtp/packet.js +48 -0
  38. package/lib/rtp/src/srtp/packet.js.map +1 -0
  39. package/lib/sctp/src/chunk.js +3 -6
  40. package/lib/sctp/src/chunk.js.map +1 -1
  41. package/lib/sctp/src/sctp.d.ts +19 -0
  42. package/lib/sctp/src/sctp.js +259 -23
  43. package/lib/sctp/src/sctp.js.map +1 -1
  44. package/lib/webrtc/src/dataChannel.d.ts +1 -0
  45. package/lib/webrtc/src/dataChannel.js +5 -2
  46. package/lib/webrtc/src/dataChannel.js.map +1 -1
  47. package/lib/webrtc/src/peerConnection.d.ts +4 -1
  48. package/lib/webrtc/src/peerConnection.js +26 -5
  49. package/lib/webrtc/src/peerConnection.js.map +1 -1
  50. package/lib/webrtc/src/sctpManager.d.ts +1 -1
  51. package/lib/webrtc/src/sctpManager.js +3 -2
  52. package/lib/webrtc/src/sctpManager.js.map +1 -1
  53. package/lib/webrtc/src/sdpManager.d.ts +1 -1
  54. package/lib/webrtc/src/sdpManager.js +3 -4
  55. package/lib/webrtc/src/sdpManager.js.map +1 -1
  56. package/lib/webrtc/src/secureTransportManager.js +1 -1
  57. package/lib/webrtc/src/secureTransportManager.js.map +1 -1
  58. package/lib/webrtc/src/transceiverManager.js +3 -2
  59. package/lib/webrtc/src/transceiverManager.js.map +1 -1
  60. package/lib/webrtc/src/transport/dtls.d.ts +1 -0
  61. package/lib/webrtc/src/transport/dtls.js +117 -12
  62. package/lib/webrtc/src/transport/dtls.js.map +1 -1
  63. package/lib/webrtc/src/transport/sctp.d.ts +9 -3
  64. package/lib/webrtc/src/transport/sctp.js +35 -9
  65. package/lib/webrtc/src/transport/sctp.js.map +1 -1
  66. package/lib/webrtc/src/utils.d.ts +2 -0
  67. package/lib/webrtc/src/utils.js +20 -0
  68. package/lib/webrtc/src/utils.js.map +1 -1
  69. package/package.json +1 -3
@@ -21,6 +21,8 @@ export declare class SCTP {
21
21
  started: boolean;
22
22
  state: SCTPConnectionState;
23
23
  isServer: boolean;
24
+ private isStopping;
25
+ private isClosed;
24
26
  private hmacKey;
25
27
  private localPartialReliability;
26
28
  private localPort;
@@ -30,6 +32,8 @@ export declare class SCTP {
30
32
  private remotePort?;
31
33
  private remoteVerificationTag;
32
34
  private advertisedRwnd;
35
+ private peerAdvertisedRwnd;
36
+ private get peerRwnd();
33
37
  private inboundStreams;
34
38
  _inboundStreamsCount: number;
35
39
  _inboundStreamsMax: number;
@@ -37,6 +41,9 @@ export declare class SCTP {
37
41
  private sackDuplicates;
38
42
  private sackMisOrdered;
39
43
  private sackNeeded;
44
+ private sackPacketCount;
45
+ private sackHasNewDataInPacket;
46
+ private sackImmediate;
40
47
  private sackTimeout;
41
48
  private cwnd;
42
49
  private fastRecoveryExit?;
@@ -52,6 +59,8 @@ export declare class SCTP {
52
59
  private advancedPeerAckTsn;
53
60
  private partialBytesAcked;
54
61
  private sentQueue;
62
+ private transmitting;
63
+ private transmitRequested;
55
64
  /**初期TSNと同じ値に初期化される単調に増加する数です. これは、新しいre-configuration requestパラメーターを送信するたびに1ずつ増加します */
56
65
  reconfigRequestSeq: number;
57
66
  /**このフィールドは、incoming要求のre-configuration requestシーケンス番号を保持します. 他の場合では、次に予想されるre-configuration requestシーケンス番号から1を引いた値が保持されます */
@@ -74,12 +83,16 @@ export declare class SCTP {
74
83
  /**Re-configuration Timer */
75
84
  private timerReconfigHandle?;
76
85
  private timerReconfigFailures;
86
+ private timerHeartbeatHandle?;
87
+ private heartbeatInterval;
77
88
  private ssthresh?;
78
89
  constructor(transport: Transport, port?: number);
90
+ private get isStopped();
79
91
  get maxChannels(): number | undefined;
80
92
  static client(transport: Transport, port?: number): SCTP;
81
93
  static server(transport: Transport, port?: number): SCTP;
82
94
  private handleData;
95
+ private scheduleSack;
83
96
  private sendSack;
84
97
  private receiveChunk;
85
98
  private getExtensions;
@@ -97,6 +110,7 @@ export declare class SCTP {
97
110
  ordered?: boolean;
98
111
  }) => Promise<void>;
99
112
  private transmit;
113
+ private transmitOnce;
100
114
  transmitReconfigRequest(): Promise<void>;
101
115
  sendReconfigParam(param: StreamParam): Promise<void>;
102
116
  private sendResetRequest;
@@ -119,6 +133,11 @@ export declare class SCTP {
119
133
  private timerReconfigHandleStart;
120
134
  private timerReconfigHandleExpired;
121
135
  private timerReconfigCancel;
136
+ private heartbeatStart;
137
+ private heartbeatRestart;
138
+ private heartbeatCancel;
139
+ private timerHeartbeatExpired;
140
+ setHeartbeatInterval(interval: number): void;
122
141
  private updateAdvancedPeerAckPoint;
123
142
  private maybeAbandon;
124
143
  static getCapabilities(): RTCSctpCapabilities;
@@ -23,10 +23,16 @@ const SCTP_MAX_ASSOCIATION_RETRANS = 10;
23
23
  const SCTP_MAX_INIT_RETRANS = 8;
24
24
  const SCTP_RTO_ALPHA = 1 / 8;
25
25
  const SCTP_RTO_BETA = 1 / 4;
26
+ // RFC 9260 RTO.Initial/RTO.Min/RTO.Max defaults and bounds are in seconds.
26
27
  const SCTP_RTO_INITIAL = 3;
27
28
  const SCTP_RTO_MIN = 1;
28
29
  const SCTP_RTO_MAX = 60;
29
30
  const SCTP_TSN_MODULO = 2 ** 32;
31
+ // RFC 9260 delayed ACK guidance: send SACK within 200ms at the latest.
32
+ // Use a small default delay to keep request/response latency low while still delaying ACKs.
33
+ const SCTP_SACK_DELAY_MS = 200;
34
+ // RFC 9260 HB.interval recommended default is 30 seconds.
35
+ const SCTP_HEARTBEAT_INTERVAL = 30;
30
36
  const RECONFIG_MAX_STREAMS = 135;
31
37
  // # parameters
32
38
  const SCTP_STATE_COOKIE = 0x0007;
@@ -39,6 +45,10 @@ const SCTPConnectionStates = [
39
45
  "connecting",
40
46
  ];
41
47
  class SCTP {
48
+ get peerRwnd() {
49
+ // RFC 9260 §6.2.1(D-ii): available receiver window = a_rwnd - outstanding DATA.
50
+ return Math.max(0, this.peerAdvertisedRwnd - this.flightSize);
51
+ }
42
52
  constructor(transport, port = 5000) {
43
53
  Object.defineProperty(this, "transport", {
44
54
  enumerable: true,
@@ -107,6 +117,18 @@ class SCTP {
107
117
  writable: true,
108
118
  value: true
109
119
  });
120
+ Object.defineProperty(this, "isStopping", {
121
+ enumerable: true,
122
+ configurable: true,
123
+ writable: true,
124
+ value: false
125
+ });
126
+ Object.defineProperty(this, "isClosed", {
127
+ enumerable: true,
128
+ configurable: true,
129
+ writable: true,
130
+ value: false
131
+ });
110
132
  Object.defineProperty(this, "hmacKey", {
111
133
  enumerable: true,
112
134
  configurable: true,
@@ -162,6 +184,12 @@ class SCTP {
162
184
  writable: true,
163
185
  value: 1024 * 1024
164
186
  }); // Receiver Window
187
+ Object.defineProperty(this, "peerAdvertisedRwnd", {
188
+ enumerable: true,
189
+ configurable: true,
190
+ writable: true,
191
+ value: this.advertisedRwnd
192
+ });
165
193
  Object.defineProperty(this, "inboundStreams", {
166
194
  enumerable: true,
167
195
  configurable: true,
@@ -204,6 +232,24 @@ class SCTP {
204
232
  writable: true,
205
233
  value: false
206
234
  });
235
+ Object.defineProperty(this, "sackPacketCount", {
236
+ enumerable: true,
237
+ configurable: true,
238
+ writable: true,
239
+ value: 0
240
+ });
241
+ Object.defineProperty(this, "sackHasNewDataInPacket", {
242
+ enumerable: true,
243
+ configurable: true,
244
+ writable: true,
245
+ value: false
246
+ });
247
+ Object.defineProperty(this, "sackImmediate", {
248
+ enumerable: true,
249
+ configurable: true,
250
+ writable: true,
251
+ value: false
252
+ });
207
253
  Object.defineProperty(this, "sackTimeout", {
208
254
  enumerable: true,
209
255
  configurable: true,
@@ -290,6 +336,18 @@ class SCTP {
290
336
  writable: true,
291
337
  value: []
292
338
  });
339
+ Object.defineProperty(this, "transmitting", {
340
+ enumerable: true,
341
+ configurable: true,
342
+ writable: true,
343
+ value: false
344
+ });
345
+ Object.defineProperty(this, "transmitRequested", {
346
+ enumerable: true,
347
+ configurable: true,
348
+ writable: true,
349
+ value: false
350
+ });
293
351
  // # reconfiguration
294
352
  /**初期TSNと同じ値に初期化される単調に増加する数です. これは、新しいre-configuration requestパラメーターを送信するたびに1ずつ増加します */
295
353
  Object.defineProperty(this, "reconfigRequestSeq", {
@@ -395,6 +453,18 @@ class SCTP {
395
453
  writable: true,
396
454
  value: 0
397
455
  });
456
+ Object.defineProperty(this, "timerHeartbeatHandle", {
457
+ enumerable: true,
458
+ configurable: true,
459
+ writable: true,
460
+ value: void 0
461
+ });
462
+ Object.defineProperty(this, "heartbeatInterval", {
463
+ enumerable: true,
464
+ configurable: true,
465
+ writable: true,
466
+ value: SCTP_HEARTBEAT_INTERVAL
467
+ });
398
468
  // etc
399
469
  Object.defineProperty(this, "ssthresh", {
400
470
  enumerable: true,
@@ -441,17 +511,9 @@ class SCTP {
441
511
  if (ordered) {
442
512
  this.outboundStreamSeq[streamId] = (0, common_1.uint16Add)(streamSeqNum, 1);
443
513
  }
444
- if (!this.timer3Handle) {
445
- await this.transmit();
446
- }
447
- else {
448
- if (this.outboundQueue.length) {
449
- await this.flush.asPromise();
450
- }
451
- else {
452
- // unreachable?
453
- await new Promise((r) => setImmediate(r));
454
- }
514
+ await this.transmit();
515
+ while (this.outboundQueue.length) {
516
+ await this.flush.asPromise();
455
517
  }
456
518
  }
457
519
  });
@@ -460,6 +522,8 @@ class SCTP {
460
522
  configurable: true,
461
523
  writable: true,
462
524
  value: () => {
525
+ if (this.isStopped)
526
+ return;
463
527
  this.timer1Failures++;
464
528
  this.timer1Handle = undefined;
465
529
  if (this.timer1Failures > SCTP_MAX_INIT_RETRANS) {
@@ -467,10 +531,14 @@ class SCTP {
467
531
  }
468
532
  else {
469
533
  setImmediate(() => {
534
+ if (this.isStopped)
535
+ return;
470
536
  this.sendChunk(this.timer1Chunk).catch((err) => {
471
537
  log("send timer1 chunk failed", err.message);
472
538
  });
473
539
  });
540
+ if (this.isStopped)
541
+ return;
474
542
  this.timer1Handle = setTimeout(this.timer1Expired, this.rto * 1000);
475
543
  }
476
544
  }
@@ -480,6 +548,8 @@ class SCTP {
480
548
  configurable: true,
481
549
  writable: true,
482
550
  value: () => {
551
+ if (this.isStopped)
552
+ return;
483
553
  this.timer2Failures++;
484
554
  this.timer2Handle = undefined;
485
555
  if (this.timer2Failures > SCTP_MAX_ASSOCIATION_RETRANS) {
@@ -487,10 +557,14 @@ class SCTP {
487
557
  }
488
558
  else {
489
559
  setImmediate(() => {
560
+ if (this.isStopped)
561
+ return;
490
562
  this.sendChunk(this.timer2Chunk).catch((err) => {
491
563
  log("send timer2Chunk failed", err.message);
492
564
  });
493
565
  });
566
+ if (this.isStopped)
567
+ return;
494
568
  this.timer2Handle = setTimeout(this.timer2Expired, this.rto * 1000);
495
569
  }
496
570
  }
@@ -500,6 +574,8 @@ class SCTP {
500
574
  configurable: true,
501
575
  writable: true,
502
576
  value: () => {
577
+ if (this.isStopped)
578
+ return;
503
579
  this.timer3Handle = undefined;
504
580
  // # mark retransmit or abandoned chunks
505
581
  this.sentQueue.forEach((chunk) => {
@@ -522,6 +598,8 @@ class SCTP {
522
598
  configurable: true,
523
599
  writable: true,
524
600
  value: async () => {
601
+ if (this.isStopped)
602
+ return;
525
603
  this.timerReconfigFailures++;
526
604
  // back off
527
605
  this.rto = Math.ceil(this.rto * 1.5);
@@ -533,15 +611,41 @@ class SCTP {
533
611
  else if (this.reconfigRequest) {
534
612
  log("timerReconfigHandleExpired", this.timerReconfigFailures, this.rto);
535
613
  await this.sendReconfigParam(this.reconfigRequest);
614
+ if (this.isStopped)
615
+ return;
536
616
  this.timerReconfigHandle = setTimeout(this.timerReconfigHandleExpired, this.rto * 1000);
537
617
  }
538
618
  }
539
619
  });
620
+ Object.defineProperty(this, "timerHeartbeatExpired", {
621
+ enumerable: true,
622
+ configurable: true,
623
+ writable: true,
624
+ value: async () => {
625
+ this.timerHeartbeatHandle = undefined;
626
+ if (this.associationState !== const_1.SCTP_STATE.ESTABLISHED)
627
+ return;
628
+ if (this.flightSize === 0 && this.outboundQueue.length === 0) {
629
+ const heartbeat = new chunk_1.HeartbeatChunk();
630
+ heartbeat.params.push([
631
+ 1,
632
+ Buffer.from(jspack_1.jspack.Pack("!L", [Math.floor(Date.now() / 1000)])),
633
+ ]);
634
+ await this.sendChunk(heartbeat).catch((err) => {
635
+ log("send heartbeat failed", err.message);
636
+ });
637
+ }
638
+ this.heartbeatStart();
639
+ }
640
+ });
540
641
  this.localPort = this.port;
541
642
  this.transport.onData = (buf) => {
542
643
  this.handleData(buf);
543
644
  };
544
645
  }
646
+ get isStopped() {
647
+ return this.isStopping || this.isClosed;
648
+ }
545
649
  get maxChannels() {
546
650
  if (this._inboundStreamsCount > 0) {
547
651
  return Math.min(this._inboundStreamsCount, this._outboundStreamsCount);
@@ -560,6 +664,8 @@ class SCTP {
560
664
  }
561
665
  // call from dtls transport
562
666
  async handleData(data) {
667
+ if (this.isStopped)
668
+ return;
563
669
  let expectedTag;
564
670
  const [, , verificationTag, chunks] = (0, chunk_1.parsePacket)(data);
565
671
  const initChunk = chunks.filter((v) => v.type === chunk_1.InitChunk.type).length;
@@ -575,18 +681,47 @@ class SCTP {
575
681
  if (verificationTag !== expectedTag) {
576
682
  return;
577
683
  }
684
+ this.sackHasNewDataInPacket = false;
578
685
  for (const chunk of chunks) {
579
686
  await this.receiveChunk(chunk);
580
687
  }
581
688
  if (this.sackNeeded) {
689
+ // RFC 9260 delayed ACK guidance is per packet ("every second packet").
690
+ if (this.sackHasNewDataInPacket) {
691
+ this.sackPacketCount++;
692
+ }
693
+ if (this.sackPacketCount >= 2) {
694
+ this.sackImmediate = true;
695
+ }
696
+ await this.scheduleSack();
697
+ }
698
+ }
699
+ async scheduleSack() {
700
+ if (this.isStopped)
701
+ return;
702
+ if (!this.sackNeeded)
703
+ return;
704
+ if (this.sackImmediate) {
705
+ if (this.sackTimeout) {
706
+ clearTimeout(this.sackTimeout);
707
+ this.sackTimeout = undefined;
708
+ }
582
709
  await this.sendSack();
710
+ return;
583
711
  }
712
+ if (this.sackTimeout)
713
+ return;
714
+ // RFC 9260 delayed ACK path: defer SACK, but never beyond 200ms.
715
+ this.sackTimeout = setTimeout(() => {
716
+ this.sackTimeout = undefined;
717
+ this.sendSack().catch((err) => {
718
+ log("send delayed sack failed", err.message);
719
+ });
720
+ }, SCTP_SACK_DELAY_MS);
584
721
  }
585
722
  async sendSack() {
586
- if (this.sackTimeout)
723
+ if (this.isStopped)
587
724
  return;
588
- await new Promise((r) => (this.sackTimeout = setImmediate(r)));
589
- this.sackTimeout = undefined;
590
725
  if (!this.sackNeeded)
591
726
  return;
592
727
  const gaps = [];
@@ -611,6 +746,8 @@ class SCTP {
611
746
  });
612
747
  this.sackDuplicates = [];
613
748
  this.sackNeeded = false;
749
+ this.sackPacketCount = 0;
750
+ this.sackImmediate = false;
614
751
  }
615
752
  async receiveChunk(chunk) {
616
753
  switch (chunk.type) {
@@ -629,6 +766,7 @@ class SCTP {
629
766
  this.reconfigResponseSeq = tsnMinusOne(init.initialTsn);
630
767
  this.remoteVerificationTag = init.initiateTag;
631
768
  this.ssthresh = init.advertisedRwnd;
769
+ this.peerAdvertisedRwnd = init.advertisedRwnd;
632
770
  this.getExtensions(init.params);
633
771
  this._inboundStreamsCount = Math.min(init.outboundStreams, this._inboundStreamsMax);
634
772
  this._outboundStreamsCount = Math.min(this._outboundStreamsCount, init.inboundStreams);
@@ -662,6 +800,7 @@ class SCTP {
662
800
  this.reconfigResponseSeq = tsnMinusOne(initAck.initialTsn);
663
801
  this.remoteVerificationTag = initAck.initiateTag;
664
802
  this.ssthresh = initAck.advertisedRwnd;
803
+ this.peerAdvertisedRwnd = initAck.advertisedRwnd;
665
804
  this.getExtensions(initAck.params);
666
805
  this._inboundStreamsCount = Math.min(initAck.outboundStreams, this._inboundStreamsMax);
667
806
  this._outboundStreamsCount = Math.min(this._outboundStreamsCount, initAck.inboundStreams);
@@ -862,8 +1001,21 @@ class SCTP {
862
1001
  }
863
1002
  receiveDataChunk(chunk) {
864
1003
  this.sackNeeded = true;
865
- if (this.markReceived(chunk.tsn))
1004
+ if (this.markReceived(chunk.tsn)) {
1005
+ this.sackImmediate = true;
866
1006
  return;
1007
+ }
1008
+ this.sackHasNewDataInPacket = true;
1009
+ // RFC 9260 delayed ACK is optional; prefer immediate DATA ACK to keep ACK-clock responsive.
1010
+ this.sackImmediate = true;
1011
+ if ((chunk.flags & SCTP_DATA_LAST_FRAG) === 0) {
1012
+ // Keep fragmented-message feedback prompt to avoid long tail-loss recovery.
1013
+ this.sackImmediate = true;
1014
+ }
1015
+ if (this.sackMisOrdered.size > 0) {
1016
+ // RFC 9260 permits immediate SACK on gap/loss indicators.
1017
+ this.sackImmediate = true;
1018
+ }
867
1019
  const inboundStream = this.getInboundStream(chunk.streamId);
868
1020
  inboundStream.addChunk(chunk);
869
1021
  this.advertisedRwnd -= chunk.userData.length;
@@ -972,12 +1124,15 @@ class SCTP {
972
1124
  else if (done > 0) {
973
1125
  this.timer3Restart();
974
1126
  }
1127
+ // RFC 9260 §6.2.1(D-ii): keep latest peer a_rwnd; available window is derived via flightSize.
1128
+ this.peerAdvertisedRwnd = chunk.advertisedRwnd;
975
1129
  this.updateAdvancedPeerAckPoint();
976
1130
  await this.onSackReceived();
977
1131
  await this.transmit();
978
1132
  }
979
1133
  receiveForwardTsnChunk(chunk) {
980
1134
  this.sackNeeded = true;
1135
+ this.sackImmediate = true;
981
1136
  if ((0, common_1.uint32Gte)(this.lastReceivedTsn, chunk.cumulativeTsn)) {
982
1137
  return;
983
1138
  }
@@ -1053,6 +1208,26 @@ class SCTP {
1053
1208
  return false;
1054
1209
  }
1055
1210
  async transmit() {
1211
+ if (this.isStopped)
1212
+ return;
1213
+ if (this.transmitting) {
1214
+ this.transmitRequested = true;
1215
+ return;
1216
+ }
1217
+ this.transmitting = true;
1218
+ try {
1219
+ do {
1220
+ this.transmitRequested = false;
1221
+ await this.transmitOnce();
1222
+ } while (this.transmitRequested);
1223
+ }
1224
+ finally {
1225
+ this.transmitting = false;
1226
+ }
1227
+ }
1228
+ async transmitOnce() {
1229
+ if (this.isStopped)
1230
+ return;
1056
1231
  // """
1057
1232
  // Transmit outbound data.
1058
1233
  // """
@@ -1077,7 +1252,7 @@ class SCTP {
1077
1252
  this.fastRecoveryTransmit = false;
1078
1253
  }
1079
1254
  else if (this.flightSize >= cwnd) {
1080
- return;
1255
+ break;
1081
1256
  }
1082
1257
  this.flightSizeIncrease(dataChunk);
1083
1258
  dataChunk.misses = 0;
@@ -1092,11 +1267,17 @@ class SCTP {
1092
1267
  }
1093
1268
  retransmitEarliest = false;
1094
1269
  }
1095
- // for performance todo fix
1096
- while (this.outboundQueue.length > 0) {
1270
+ // Drain queued outbound DATA; retransmission pacing is controlled by RTO/T3.
1271
+ while (this.outboundQueue.length > 0 &&
1272
+ this.flightSize < cwnd &&
1273
+ this.peerRwnd > 0) {
1097
1274
  const chunk = this.outboundQueue.shift();
1098
1275
  if (!chunk)
1099
- return;
1276
+ break;
1277
+ if (chunk.bookSize > this.peerRwnd && this.flightSize > 0) {
1278
+ this.outboundQueue.unshift(chunk);
1279
+ break;
1280
+ }
1100
1281
  this.sentQueue.push(chunk);
1101
1282
  this.flightSizeIncrease(chunk);
1102
1283
  // # update counters
@@ -1109,8 +1290,10 @@ class SCTP {
1109
1290
  this.timer3Start();
1110
1291
  }
1111
1292
  }
1112
- // Resetting the queue to empty array mitigates this.
1113
- this.outboundQueue = [];
1293
+ if (!this.outboundQueue.length) {
1294
+ // Resetting the queue to empty array mitigates this.
1295
+ this.outboundQueue = [];
1296
+ }
1114
1297
  this.flush.execute();
1115
1298
  }
1116
1299
  async transmitReconfigRequest() {
@@ -1185,14 +1368,19 @@ class SCTP {
1185
1368
  }
1186
1369
  /**t3 is wait for data sack */
1187
1370
  timer3Start() {
1371
+ if (this.isStopped)
1372
+ return;
1188
1373
  if (this.timer3Handle)
1189
1374
  throw new Error();
1375
+ // RFC 9260 T3-rtx runs on current RTO; rto is stored in seconds.
1190
1376
  this.timer3Handle = setTimeout(this.timer3Expired, this.rto * 1000);
1191
1377
  }
1192
1378
  timer3Restart() {
1379
+ if (this.isStopped)
1380
+ return;
1193
1381
  this.timer3Cancel();
1194
- // for performance
1195
- this.timer3Handle = setTimeout(this.timer3Expired, this.rto);
1382
+ // RFC 9260 requires restarting T3-rtx with current RTO (seconds -> JS ms).
1383
+ this.timer3Handle = setTimeout(this.timer3Expired, this.rto * 1000);
1196
1384
  }
1197
1385
  timer3Cancel() {
1198
1386
  if (this.timer3Handle) {
@@ -1202,6 +1390,8 @@ class SCTP {
1202
1390
  }
1203
1391
  /**Re-configuration Timer */
1204
1392
  timerReconfigHandleStart() {
1393
+ if (this.isStopped)
1394
+ return;
1205
1395
  if (this.timerReconfigHandle)
1206
1396
  return;
1207
1397
  log("timerReconfigHandleStart", { rto: this.rto });
@@ -1217,6 +1407,31 @@ class SCTP {
1217
1407
  this.timerReconfigFailures = 0;
1218
1408
  }
1219
1409
  }
1410
+ heartbeatStart() {
1411
+ if (this.timerHeartbeatHandle ||
1412
+ this.associationState !== const_1.SCTP_STATE.ESTABLISHED) {
1413
+ return;
1414
+ }
1415
+ // RFC 9260 heartbeat timer is based on RTO + HB.interval for idle paths.
1416
+ this.timerHeartbeatHandle = setTimeout(this.timerHeartbeatExpired, (this.rto + this.heartbeatInterval) * 1000);
1417
+ }
1418
+ heartbeatRestart() {
1419
+ this.heartbeatCancel();
1420
+ this.heartbeatStart();
1421
+ }
1422
+ heartbeatCancel() {
1423
+ if (this.timerHeartbeatHandle) {
1424
+ clearTimeout(this.timerHeartbeatHandle);
1425
+ this.timerHeartbeatHandle = undefined;
1426
+ }
1427
+ }
1428
+ setHeartbeatInterval(interval) {
1429
+ if (interval <= 0) {
1430
+ throw new Error("heartbeat interval must be > 0");
1431
+ }
1432
+ this.heartbeatInterval = interval;
1433
+ this.heartbeatRestart();
1434
+ }
1220
1435
  updateAdvancedPeerAckPoint() {
1221
1436
  if ((0, common_1.uint32Gt)(this.lastSackedTsn, this.advancedPeerAckTsn)) {
1222
1437
  this.advancedPeerAckTsn = this.lastSackedTsn;
@@ -1330,13 +1545,22 @@ class SCTP {
1330
1545
  this.associationState = state;
1331
1546
  }
1332
1547
  if (state === const_1.SCTP_STATE.ESTABLISHED) {
1548
+ this.isStopping = false;
1549
+ this.isClosed = false;
1333
1550
  this.setConnectionState("connected");
1551
+ this.heartbeatStart();
1334
1552
  }
1335
1553
  else if (state === const_1.SCTP_STATE.CLOSED) {
1554
+ this.isClosed = true;
1336
1555
  this.timer1Cancel();
1337
1556
  this.timer2Cancel();
1338
1557
  this.timer3Cancel();
1339
1558
  this.timerReconfigCancel();
1559
+ this.heartbeatCancel();
1560
+ if (this.sackTimeout) {
1561
+ clearTimeout(this.sackTimeout);
1562
+ this.sackTimeout = undefined;
1563
+ }
1340
1564
  this.setConnectionState("closed");
1341
1565
  this.removeAllListeners();
1342
1566
  }
@@ -1347,6 +1571,16 @@ class SCTP {
1347
1571
  this.stateChanged[state].execute();
1348
1572
  }
1349
1573
  async stop() {
1574
+ if (this.isStopped) {
1575
+ this.setState(const_1.SCTP_STATE.CLOSED);
1576
+ return;
1577
+ }
1578
+ this.isStopping = true;
1579
+ this.transport.onData = undefined;
1580
+ if (this.sackTimeout) {
1581
+ clearTimeout(this.sackTimeout);
1582
+ this.sackTimeout = undefined;
1583
+ }
1350
1584
  if (this.associationState !== const_1.SCTP_STATE.CLOSED) {
1351
1585
  await this.abort();
1352
1586
  }
@@ -1355,6 +1589,8 @@ class SCTP {
1355
1589
  clearTimeout(this.timer2Handle);
1356
1590
  clearTimeout(this.timer3Handle);
1357
1591
  clearTimeout(this.timerReconfigHandle);
1592
+ clearTimeout(this.timerHeartbeatHandle);
1593
+ clearTimeout(this.sackTimeout);
1358
1594
  }
1359
1595
  async abort() {
1360
1596
  const abort = new chunk_1.AbortChunk();