@libp2p/webrtc 5.2.24-6059227cb → 5.2.24-87bc8d4fb

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 (90) hide show
  1. package/README.md +20 -10
  2. package/dist/index.min.js +18 -18
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/constants.d.ts +23 -4
  5. package/dist/src/constants.d.ts.map +1 -1
  6. package/dist/src/constants.js +23 -4
  7. package/dist/src/constants.js.map +1 -1
  8. package/dist/src/index.d.ts +22 -20
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +22 -12
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/maconn.d.ts +58 -0
  13. package/dist/src/maconn.d.ts.map +1 -0
  14. package/dist/src/maconn.js +56 -0
  15. package/dist/src/maconn.js.map +1 -0
  16. package/dist/src/muxer.d.ts +46 -14
  17. package/dist/src/muxer.d.ts.map +1 -1
  18. package/dist/src/muxer.js +138 -30
  19. package/dist/src/muxer.js.map +1 -1
  20. package/dist/src/private-to-private/initiate-connection.d.ts +3 -2
  21. package/dist/src/private-to-private/initiate-connection.d.ts.map +1 -1
  22. package/dist/src/private-to-private/initiate-connection.js +5 -37
  23. package/dist/src/private-to-private/initiate-connection.js.map +1 -1
  24. package/dist/src/private-to-private/signaling-stream-handler.d.ts +4 -4
  25. package/dist/src/private-to-private/signaling-stream-handler.d.ts.map +1 -1
  26. package/dist/src/private-to-private/signaling-stream-handler.js +7 -19
  27. package/dist/src/private-to-private/signaling-stream-handler.js.map +1 -1
  28. package/dist/src/private-to-private/transport.d.ts +9 -2
  29. package/dist/src/private-to-private/transport.d.ts.map +1 -1
  30. package/dist/src/private-to-private/transport.js +15 -30
  31. package/dist/src/private-to-private/transport.js.map +1 -1
  32. package/dist/src/private-to-private/util.d.ts +2 -3
  33. package/dist/src/private-to-private/util.d.ts.map +1 -1
  34. package/dist/src/private-to-private/util.js +14 -26
  35. package/dist/src/private-to-private/util.js.map +1 -1
  36. package/dist/src/private-to-public/listener.d.ts.map +1 -1
  37. package/dist/src/private-to-public/listener.js +15 -21
  38. package/dist/src/private-to-public/listener.js.map +1 -1
  39. package/dist/src/private-to-public/transport.d.ts +8 -0
  40. package/dist/src/private-to-public/transport.d.ts.map +1 -1
  41. package/dist/src/private-to-public/transport.js +2 -3
  42. package/dist/src/private-to-public/transport.js.map +1 -1
  43. package/dist/src/private-to-public/utils/connect.d.ts +1 -1
  44. package/dist/src/private-to-public/utils/connect.d.ts.map +1 -1
  45. package/dist/src/private-to-public/utils/connect.js +14 -17
  46. package/dist/src/private-to-public/utils/connect.js.map +1 -1
  47. package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts +4 -4
  48. package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts.map +1 -1
  49. package/dist/src/private-to-public/utils/get-rtcpeerconnection.js +2 -13
  50. package/dist/src/private-to-public/utils/get-rtcpeerconnection.js.map +1 -1
  51. package/dist/src/private-to-public/utils/sdp.d.ts.map +1 -1
  52. package/dist/src/private-to-public/utils/sdp.js +13 -25
  53. package/dist/src/private-to-public/utils/sdp.js.map +1 -1
  54. package/dist/src/private-to-public/utils/stun-listener.js +1 -1
  55. package/dist/src/private-to-public/utils/stun-listener.js.map +1 -1
  56. package/dist/src/stream.d.ts +26 -14
  57. package/dist/src/stream.d.ts.map +1 -1
  58. package/dist/src/stream.js +204 -134
  59. package/dist/src/stream.js.map +1 -1
  60. package/dist/src/util.d.ts +1 -3
  61. package/dist/src/util.d.ts.map +1 -1
  62. package/dist/src/util.js +0 -19
  63. package/dist/src/util.js.map +1 -1
  64. package/dist/src/webrtc/index.d.ts +1 -1
  65. package/dist/src/webrtc/index.d.ts.map +1 -1
  66. package/dist/src/webrtc/index.js +1 -1
  67. package/dist/src/webrtc/index.js.map +1 -1
  68. package/package.json +29 -26
  69. package/src/constants.ts +28 -5
  70. package/src/index.ts +22 -21
  71. package/src/maconn.ts +101 -0
  72. package/src/muxer.ts +169 -39
  73. package/src/private-to-private/initiate-connection.ts +8 -46
  74. package/src/private-to-private/signaling-stream-handler.ts +10 -23
  75. package/src/private-to-private/transport.ts +25 -33
  76. package/src/private-to-private/util.ts +16 -33
  77. package/src/private-to-public/listener.ts +15 -22
  78. package/src/private-to-public/transport.ts +12 -3
  79. package/src/private-to-public/utils/connect.ts +15 -18
  80. package/src/private-to-public/utils/get-rtcpeerconnection.ts +4 -16
  81. package/src/private-to-public/utils/sdp.ts +13 -29
  82. package/src/private-to-public/utils/stun-listener.ts +1 -1
  83. package/src/stream.ts +237 -153
  84. package/src/util.ts +1 -22
  85. package/src/webrtc/index.ts +1 -1
  86. package/dist/src/rtcpeerconnection-to-conn.d.ts +0 -12
  87. package/dist/src/rtcpeerconnection-to-conn.d.ts.map +0 -1
  88. package/dist/src/rtcpeerconnection-to-conn.js +0 -46
  89. package/dist/src/rtcpeerconnection-to-conn.js.map +0 -1
  90. package/src/rtcpeerconnection-to-conn.ts +0 -66
@@ -1,10 +1,10 @@
1
+ import { PeerConnection } from '@ipshipyard/node-datachannel'
2
+ import { RTCPeerConnection } from '@ipshipyard/node-datachannel/polyfill'
1
3
  import { Crypto } from '@peculiar/webcrypto'
2
- import { PeerConnection } from 'node-datachannel'
3
- import { RTCPeerConnection } from 'node-datachannel/polyfill'
4
4
  import { DEFAULT_ICE_SERVERS, MAX_MESSAGE_SIZE } from '../../constants.js'
5
5
  import { generateTransportCertificate } from './generate-certificates.js'
6
6
  import type { TransportCertificate } from '../../index.js'
7
- import type { CertificateFingerprint } from 'node-datachannel'
7
+ import type { CertificateFingerprint } from '@ipshipyard/node-datachannel'
8
8
 
9
9
  const crypto = new Crypto()
10
10
 
@@ -14,7 +14,7 @@ interface DirectRTCPeerConnectionInit extends RTCConfiguration {
14
14
  }
15
15
 
16
16
  export class DirectRTCPeerConnection extends RTCPeerConnection {
17
- private peerConnection: PeerConnection
17
+ private readonly peerConnection: PeerConnection
18
18
  private readonly ufrag: string
19
19
 
20
20
  constructor (init: DirectRTCPeerConnectionInit) {
@@ -22,18 +22,6 @@ export class DirectRTCPeerConnection extends RTCPeerConnection {
22
22
 
23
23
  this.peerConnection = init.peerConnection
24
24
  this.ufrag = init.ufrag
25
-
26
- // make sure C++ peer connection is garbage collected
27
- // https://github.com/murat-dogan/node-datachannel/issues/366#issuecomment-3228453155
28
- this.addEventListener('connectionstatechange', () => {
29
- switch (this.connectionState) {
30
- case 'closed':
31
- this.peerConnection.close()
32
- break
33
- default:
34
- break
35
- }
36
- })
37
25
  }
38
26
 
39
27
  async createOffer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
@@ -1,11 +1,10 @@
1
1
  import { InvalidParametersError } from '@libp2p/interface'
2
- import { getNetConfig } from '@libp2p/utils'
3
- import { CODE_CERTHASH, multiaddr } from '@multiformats/multiaddr'
2
+ import { multiaddr } from '@multiformats/multiaddr'
4
3
  import { base64url } from 'multiformats/bases/base64'
5
4
  import { bases, digest } from 'multiformats/basics'
6
5
  import * as Digest from 'multiformats/hashes/digest'
7
6
  import { sha256 } from 'multiformats/hashes/sha2'
8
- import { MAX_MESSAGE_SIZE } from '../../constants.js'
7
+ import { CODEC_CERTHASH, MAX_MESSAGE_SIZE } from '../../constants.js'
9
8
  import { InvalidFingerprintError, UnsupportedHashAlgorithmError } from '../../error.js'
10
9
  import type { Multiaddr } from '@multiformats/multiaddr'
11
10
  import type { MultihashDigest } from 'multiformats/hashes/interface'
@@ -28,8 +27,8 @@ export function getFingerprintFromSdp (sdp: string | undefined): string | undefi
28
27
 
29
28
  // Extract the certhash from a multiaddr
30
29
  export function certhash (ma: Multiaddr): string {
31
- const components = ma.getComponents()
32
- const certhash = components.find(c => c.code === CODE_CERTHASH)?.value
30
+ const tups = ma.stringTuples()
31
+ const certhash = tups.filter((tup) => tup[0] === CODEC_CERTHASH).map((tup) => tup[1])[0]
33
32
 
34
33
  if (certhash === undefined || certhash === '') {
35
34
  throw new InvalidParametersError(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`)
@@ -101,20 +100,15 @@ export function toSupportedHashFunction (code: number): 'sha-1' | 'sha-256' | 's
101
100
  * ice-lite mode and DTLS active mode.
102
101
  */
103
102
  export function serverAnswerFromMultiaddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
104
- const { host, port, type } = getNetConfig(ma)
105
-
106
- if (type !== 'ip4' && type !== 'ip6') {
107
- throw new InvalidParametersError(`Multiaddr ${ma} was not an IPv4 or IPv6 address`)
108
- }
109
-
103
+ const { host, port, family } = ma.toOptions()
110
104
  const fingerprint = ma2Fingerprint(ma)
111
105
  const sdp = `v=0
112
- o=- 0 0 IN IP${type === 'ip4' ? 4 : 6} ${host}
106
+ o=- 0 0 IN IP${family} ${host}
113
107
  s=-
114
108
  t=0 0
115
109
  a=ice-lite
116
110
  m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
117
- c=IN IP${type === 'ip4' ? 4 : 6} ${host}
111
+ c=IN IP${family} ${host}
118
112
  a=mid:0
119
113
  a=ice-options:ice2
120
114
  a=ice-ufrag:${ufrag}
@@ -137,16 +131,11 @@ a=end-of-candidates
137
131
  * Create an offer SDP message from a multiaddr
138
132
  */
139
133
  export function clientOfferFromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
140
- const { host, port, type } = getNetConfig(ma)
141
-
142
- if (type !== 'ip4' && type !== 'ip6') {
143
- throw new InvalidParametersError(`Multiaddr ${ma} was not an IPv4 or IPv6 address`)
144
- }
145
-
134
+ const { host, port, family } = ma.toOptions()
146
135
  const sdp = `v=0
147
- o=- 0 0 IN IP${type === 'ip4' ? 4 : 6} ${host}
136
+ o=- 0 0 IN IP${family} ${host}
148
137
  s=-
149
- c=IN IP${type === 'ip4' ? 4 : 6} ${host}
138
+ c=IN IP${family} ${host}
150
139
  t=0 0
151
140
  a=ice-options:ice2,trickle
152
141
  m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
@@ -177,13 +166,8 @@ export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessi
177
166
 
178
167
  const lineBreak = desc.sdp.includes('\r\n') ? '\r\n' : '\n'
179
168
 
180
- try {
181
- desc.sdp = desc.sdp
182
- .replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + lineBreak)
183
- .replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + lineBreak)
184
- } catch {
185
- // fails under Node.js
186
- }
187
-
169
+ desc.sdp = desc.sdp
170
+ .replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + lineBreak)
171
+ .replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + lineBreak)
188
172
  return desc
189
173
  }
@@ -1,5 +1,5 @@
1
1
  import { isIPv4 } from '@chainsafe/is-ip'
2
- import { IceUdpMuxListener } from 'node-datachannel'
2
+ import { IceUdpMuxListener } from '@ipshipyard/node-datachannel'
3
3
  import type { Logger } from '@libp2p/interface'
4
4
  import type { AddressInfo } from 'node:net'
5
5
 
package/src/stream.ts CHANGED
@@ -1,17 +1,20 @@
1
- import { StreamResetError, StreamStateError } from '@libp2p/interface'
2
- import { AbstractStream } from '@libp2p/utils'
1
+ import { StreamStateError, TimeoutError } from '@libp2p/interface'
2
+ import { AbstractStream } from '@libp2p/utils/abstract-stream'
3
+ import { anySignal } from 'any-signal'
3
4
  import * as lengthPrefixed from 'it-length-prefixed'
4
5
  import { pushable } from 'it-pushable'
5
- import { pEvent } from 'p-event'
6
+ import pDefer from 'p-defer'
7
+ import pTimeout from 'p-timeout'
8
+ import { raceEvent } from 'race-event'
6
9
  import { raceSignal } from 'race-signal'
7
10
  import { Uint8ArrayList } from 'uint8arraylist'
8
- import { DEFAULT_FIN_ACK_TIMEOUT, MAX_BUFFERED_AMOUNT, MAX_MESSAGE_SIZE, PROTOBUF_OVERHEAD } from './constants.js'
11
+ import { BUFFERED_AMOUNT_LOW_TIMEOUT, FIN_ACK_TIMEOUT, MAX_BUFFERED_AMOUNT, MAX_MESSAGE_SIZE, OPEN_TIMEOUT, PROTOBUF_OVERHEAD } from './constants.js'
9
12
  import { Message } from './private-to-public/pb/message.js'
10
- import { isFirefox } from './util.js'
11
13
  import type { DataChannelOptions } from './index.js'
12
- import type { AbortOptions, MessageStreamDirection, Logger } from '@libp2p/interface'
13
- import type { AbstractStreamInit, SendResult } from '@libp2p/utils'
14
+ import type { AbortOptions, Direction, Logger } from '@libp2p/interface'
15
+ import type { AbstractStreamInit } from '@libp2p/utils/abstract-stream'
14
16
  import type { Pushable } from 'it-pushable'
17
+ import type { DeferredPromise } from 'p-defer'
15
18
 
16
19
  export interface WebRTCStreamInit extends AbstractStreamInit, DataChannelOptions {
17
20
  /**
@@ -36,40 +39,124 @@ export class WebRTCStream extends AbstractStream {
36
39
  * and then the protobuf decoder.
37
40
  */
38
41
  private readonly incomingData: Pushable<Uint8Array>
42
+
39
43
  private readonly maxBufferedAmount: number
40
- private receivedFinAck?: PromiseWithResolvers<void>
41
- private finAckTimeout: number
44
+
45
+ private readonly bufferedAmountLowEventTimeout: number
46
+
47
+ /**
48
+ * The maximum size of a message in bytes
49
+ */
50
+ private readonly maxMessageSize: number
51
+
52
+ /**
53
+ * When this promise is resolved, the remote has sent us a FIN flag
54
+ */
55
+ private readonly receiveFinAck: DeferredPromise<void>
56
+ private readonly finAckTimeout: number
57
+ private readonly openTimeout: number
58
+ private readonly closeController: AbortController
42
59
 
43
60
  constructor (init: WebRTCStreamInit) {
44
- super({
45
- ...init,
46
- maxMessageSize: (init.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD
47
- })
61
+ // override onEnd to send/receive FIN_ACK before closing the stream
62
+ const originalOnEnd = init.onEnd
63
+ init.onEnd = (err?: Error): void => {
64
+ this.log.trace('readable and writeable ends closed with status "%s"', this.status)
65
+
66
+ void Promise.resolve(async () => {
67
+ if (this.timeline.abort != null || this.timeline.reset !== null) {
68
+ return
69
+ }
70
+
71
+ // wait for FIN_ACK if we haven't received it already
72
+ try {
73
+ await pTimeout(this.receiveFinAck.promise, {
74
+ milliseconds: this.finAckTimeout
75
+ })
76
+ } catch (err) {
77
+ this.log.error('error receiving FIN_ACK', err)
78
+ }
79
+ })
80
+ .then(() => {
81
+ // stop processing incoming messages
82
+ this.incomingData.end()
83
+
84
+ // final cleanup
85
+ originalOnEnd?.(err)
86
+ })
87
+ .catch(err => {
88
+ this.log.error('error ending stream', err)
89
+ })
90
+ .finally(() => {
91
+ this.channel.close()
92
+ })
93
+ }
94
+
95
+ super(init)
48
96
 
49
97
  this.channel = init.channel
50
98
  this.channel.binaryType = 'arraybuffer'
51
99
  this.incomingData = pushable<Uint8Array>()
100
+ this.bufferedAmountLowEventTimeout = init.bufferedAmountLowEventTimeout ?? BUFFERED_AMOUNT_LOW_TIMEOUT
52
101
  this.maxBufferedAmount = init.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT
53
- this.finAckTimeout = init.finAckTimeout ?? DEFAULT_FIN_ACK_TIMEOUT
102
+ this.maxMessageSize = (init.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD
103
+ this.receiveFinAck = pDefer()
104
+ this.finAckTimeout = init.closeTimeout ?? FIN_ACK_TIMEOUT
105
+ this.openTimeout = init.openTimeout ?? OPEN_TIMEOUT
106
+ this.closeController = new AbortController()
107
+
108
+ // set up initial state
109
+ switch (this.channel.readyState) {
110
+ case 'open':
111
+ this.timeline.open = new Date().getTime()
112
+ break
113
+
114
+ case 'closed':
115
+ case 'closing':
116
+ if (this.timeline.close === undefined || this.timeline.close === 0) {
117
+ this.timeline.close = Date.now()
118
+ }
119
+ break
120
+ case 'connecting':
121
+ // noop
122
+ break
123
+
124
+ default:
125
+ this.log.error('unknown datachannel state %s', this.channel.readyState)
126
+ throw new StreamStateError('Unknown datachannel state')
127
+ }
54
128
 
55
129
  // handle RTCDataChannel events
56
- this.channel.onclose = () => {
57
- this.log.trace('received datachannel close event')
130
+ this.channel.onopen = (_evt) => {
131
+ this.timeline.open = new Date().getTime()
132
+ }
133
+
134
+ this.channel.onclose = (_evt) => {
135
+ this.log.trace('received onclose event')
58
136
 
59
- this.onRemoteCloseWrite()
60
- this.onTransportClosed()
137
+ // stop any in-progress writes
138
+ this.closeController.abort()
139
+
140
+ // if the channel has closed we'll never receive a FIN_ACK so resolve the
141
+ // promise so we don't try to wait later
142
+ this.receiveFinAck.resolve()
143
+
144
+ void this.close().catch(err => {
145
+ this.log.error('error closing stream after channel closed', err)
146
+ })
61
147
  }
62
148
 
63
149
  this.channel.onerror = (evt) => {
64
- const err = (evt as RTCErrorEvent).error
150
+ this.log.trace('received onerror event')
65
151
 
66
- this.log.trace('received datachannel error event - %e', err)
152
+ // stop any in-progress writes
153
+ this.closeController.abort()
67
154
 
155
+ const err = (evt as RTCErrorEvent).error
68
156
  this.abort(err)
69
157
  }
70
158
 
71
159
  this.channel.onmessage = async (event: MessageEvent<ArrayBuffer>) => {
72
- this.log('incoming message %d bytes', event.data.byteLength)
73
160
  const { data } = event
74
161
 
75
162
  if (data === null || data.byteLength === 0) {
@@ -79,186 +166,187 @@ export class WebRTCStream extends AbstractStream {
79
166
  this.incomingData.push(new Uint8Array(data, 0, data.byteLength))
80
167
  }
81
168
 
82
- // dispatch drain event when the buffered amount drops to zero
83
- this.channel.bufferedAmountLowThreshold = 0
84
-
85
- this.channel.onbufferedamountlow = () => {
86
- if (this.writableNeedsDrain) {
87
- this.safeDispatchEvent('drain')
88
- }
89
- }
90
-
91
- if (this.channel.readyState !== 'open') {
92
- this.log('channel ready state is "%s" and not "open", waiting for "open" event before sending data', this.channel.readyState)
93
- pEvent(this.channel, 'open', {
94
- rejectionEvents: [
95
- 'close',
96
- 'error'
97
- ]
98
- })
99
- .then(() => {
100
- this.log('channel ready state is now "%s", dispatching drain', this.channel.readyState)
101
- this.safeDispatchEvent('drain')
102
- })
103
- .catch(err => {
104
- this.abort(err.error ?? err)
105
- })
106
- }
169
+ const self = this
107
170
 
108
171
  // pipe framed protobuf messages through a length prefixed decoder, and
109
172
  // surface data from the `Message.message` field through a source.
110
173
  Promise.resolve().then(async () => {
111
174
  for await (const buf of lengthPrefixed.decode(this.incomingData)) {
112
- this.processIncomingProtobuf(buf)
175
+ const message = self.processIncomingProtobuf(buf)
176
+
177
+ if (message != null) {
178
+ self.sourcePush(new Uint8ArrayList(message))
179
+ }
113
180
  }
114
181
  })
115
182
  .catch(err => {
116
183
  this.log.error('error processing incoming data channel messages', err)
117
184
  })
118
-
119
- // close when both writable ends are closed or an error occurs
120
- const cleanUpDatachannelOnClose = (): void => {
121
- if (this.channel.readyState === 'open') {
122
- this.log.trace('stream closed, closing underlying datachannel')
123
- this.channel.close()
124
- }
125
- }
126
- this.addEventListener('close', cleanUpDatachannelOnClose)
127
185
  }
128
186
 
129
187
  sendNewStream (): void {
130
188
  // opening new streams is handled by WebRTC so this is a noop
131
189
  }
132
190
 
133
- _sendMessage (data: Uint8ArrayList): void {
134
- if (this.channel.readyState !== 'open') {
191
+ async _sendMessage (data: Uint8ArrayList, checkBuffer: boolean = true): Promise<void> {
192
+ if (this.channel.readyState === 'closed' || this.channel.readyState === 'closing') {
135
193
  throw new StreamStateError(`Invalid datachannel state - ${this.channel.readyState}`)
136
194
  }
137
195
 
138
- this.log.trace('sending message, channel state "%s"', this.channel.readyState)
139
-
140
- if (isFirefox) {
141
- // TODO: firefox can deliver small messages out of order - remove once a
142
- // browser with https://bugzilla.mozilla.org/show_bug.cgi?id=1983831 is
143
- // available in playwright-test
144
- this.channel.send(data.subarray())
145
- return
146
- }
196
+ if (this.channel.readyState !== 'open') {
197
+ const timeout = AbortSignal.timeout(this.openTimeout)
198
+ const signal = anySignal([
199
+ this.closeController.signal,
200
+ timeout
201
+ ])
202
+
203
+ try {
204
+ this.log('channel state is "%s" and not "open", waiting for "open" event before sending data', this.channel.readyState)
205
+ await raceEvent(this.channel, 'open', signal)
206
+ } finally {
207
+ signal.clear()
208
+ }
147
209
 
148
- // send message without copying data
149
- for (const buf of data) {
150
- this.channel.send(buf)
210
+ this.log('channel state is now "%s", sending data', this.channel.readyState)
151
211
  }
152
- }
153
212
 
154
- sendData (data: Uint8ArrayList): SendResult {
155
- if (this.channel.readyState !== 'open') {
156
- return {
157
- sentBytes: 0,
158
- canSendMore: false
213
+ if (checkBuffer && this.channel.bufferedAmount > this.maxBufferedAmount) {
214
+ const timeout = AbortSignal.timeout(this.bufferedAmountLowEventTimeout)
215
+ const signal = anySignal([
216
+ this.closeController.signal,
217
+ timeout
218
+ ])
219
+
220
+ try {
221
+ this.log('channel buffer is %d, wait for "bufferedamountlow" event', this.channel.bufferedAmount)
222
+ await raceEvent(this.channel, 'bufferedamountlow', signal)
223
+ } catch (err: any) {
224
+ if (timeout.aborted) {
225
+ throw new TimeoutError(`Timed out waiting for DataChannel buffer to clear after ${this.bufferedAmountLowEventTimeout}ms`)
226
+ }
227
+
228
+ throw err
229
+ } finally {
230
+ signal.clear()
159
231
  }
160
232
  }
161
233
 
162
- // TODO: firefox can deliver small messages out of order - remove once a
163
- // browser with https://bugzilla.mozilla.org/show_bug.cgi?id=1983831 is
164
- // available in playwright-test
165
- // ----
166
- // this is also necessary to work with rust-libp2p 0.54 though 0.53 seems ok
167
- this._sendMessage(
168
- lengthPrefixed.encode.single(Message.encode({
169
- message: data.subarray()
170
- }))
171
- )
172
-
173
- /*
174
- // TODO: enable this when FF and rust-libp2p are not broken
175
- // send message without copying data
176
- for (const message of data) {
177
- this._sendMessage(
178
- lengthPrefixed.encode.single(Message.encode({
179
- message
180
- }))
181
- )
182
- }
234
+ try {
235
+ this.log.trace('sending message, channel state "%s"', this.channel.readyState)
236
+ // send message without copying data
237
+ this.channel.send(data.subarray())
238
+ } catch (err: any) {
239
+ this.log.error('error while sending message', err)
183
240
  }
184
- */
241
+ }
185
242
 
186
- return {
187
- sentBytes: data.byteLength,
188
- canSendMore: this.channel.bufferedAmount < this.maxBufferedAmount
243
+ async sendData (data: Uint8ArrayList): Promise<void> {
244
+ const bytesTotal = data.byteLength
245
+ // sending messages is an async operation so use a copy of the list as it
246
+ // may be changed beneath us
247
+ data = data.sublist()
248
+
249
+ while (data.byteLength > 0) {
250
+ const toSend = Math.min(data.byteLength, this.maxMessageSize)
251
+ const buf = data.subarray(0, toSend)
252
+ const messageBuf = Message.encode({ message: buf })
253
+ const sendBuf = lengthPrefixed.encode.single(messageBuf)
254
+ this.log.trace('sending %d/%d bytes on channel', buf.byteLength, bytesTotal)
255
+ await this._sendMessage(sendBuf)
256
+
257
+ data.consume(toSend)
189
258
  }
259
+
260
+ this.log.trace('finished sending data, channel state "%s"', this.channel.readyState)
190
261
  }
191
262
 
192
- sendReset (err: Error): void {
263
+ async sendReset (): Promise<void> {
193
264
  try {
194
- this.log.error('sending reset - %e', err)
195
- this._sendFlag(Message.Flag.RESET)
196
- this.receivedFinAck?.reject(err)
265
+ await this._sendFlag(Message.Flag.RESET)
197
266
  } catch (err) {
198
267
  this.log.error('failed to send reset - %e', err)
268
+ } finally {
269
+ this.channel.close()
199
270
  }
200
271
  }
201
272
 
202
- async sendCloseWrite (options?: AbortOptions): Promise<void> {
203
- this._sendFlag(Message.Flag.FIN)
204
- options?.signal?.throwIfAborted()
205
- this.receivedFinAck = Promise.withResolvers<void>()
206
-
207
- await Promise.any([
208
- raceSignal(this.receivedFinAck.promise, options?.signal),
209
- new Promise<void>(resolve => {
210
- AbortSignal.timeout(this.finAckTimeout)
211
- .addEventListener('abort', () => {
212
- resolve()
213
- })
214
- })
215
- ])
273
+ async sendCloseWrite (options: AbortOptions): Promise<void> {
274
+ if (this.channel.readyState !== 'open') {
275
+ this.receiveFinAck.resolve()
276
+ return
277
+ }
278
+
279
+ const sent = await this._sendFlag(Message.Flag.FIN)
280
+
281
+ if (sent) {
282
+ this.log.trace('awaiting FIN_ACK')
283
+ try {
284
+ await raceSignal(this.receiveFinAck.promise, options?.signal, {
285
+ errorMessage: 'sending close-write was aborted before FIN_ACK was received',
286
+ errorName: 'FinAckNotReceivedError'
287
+ })
288
+ } catch (err) {
289
+ this.log.error('failed to await FIN_ACK', err)
290
+ }
291
+ } else {
292
+ this.log.trace('sending FIN failed, not awaiting FIN_ACK')
293
+ }
294
+
295
+ // if we've attempted to receive a FIN_ACK, do not try again
296
+ this.receiveFinAck.resolve()
216
297
  }
217
298
 
218
- async sendCloseRead (options?: AbortOptions): Promise<void> {
219
- this._sendFlag(Message.Flag.STOP_SENDING)
220
- options?.signal?.throwIfAborted()
299
+ async sendCloseRead (): Promise<void> {
300
+ if (this.channel.readyState !== 'open') {
301
+ return
302
+ }
303
+
304
+ await this._sendFlag(Message.Flag.STOP_SENDING)
221
305
  }
222
306
 
223
307
  /**
224
308
  * Handle incoming
225
309
  */
226
- private processIncomingProtobuf (buffer: Uint8ArrayList): void {
310
+ private processIncomingProtobuf (buffer: Uint8ArrayList): Uint8Array | undefined {
227
311
  const message = Message.decode(buffer)
228
312
 
229
- // ignore data messages if we've closed the readable end already
230
- if (message.message != null && (this.readStatus === 'readable' || this.readStatus === 'paused')) {
231
- this.onData(new Uint8ArrayList(message.message))
232
- }
233
-
234
313
  if (message.flag !== undefined) {
235
314
  this.log.trace('incoming flag %s, write status "%s", read status "%s"', message.flag, this.writeStatus, this.readStatus)
236
315
 
237
316
  if (message.flag === Message.Flag.FIN) {
238
- // we should expect no more data from the remote, stop reading
239
- this._sendFlag(Message.Flag.FIN_ACK)
240
- this.onRemoteCloseWrite()
317
+ // We should expect no more data from the remote, stop reading
318
+ this.remoteCloseWrite()
319
+
320
+ this.log.trace('sending FIN_ACK')
321
+ void this._sendFlag(Message.Flag.FIN_ACK)
322
+ .catch(err => {
323
+ this.log.error('error sending FIN_ACK immediately', err)
324
+ })
241
325
  }
242
326
 
243
327
  if (message.flag === Message.Flag.RESET) {
244
- // stop reading and writing to the stream immediately
245
- this.receivedFinAck?.reject(new StreamResetError('The stream was reset'))
246
- this.onRemoteReset()
328
+ // Stop reading and writing to the stream immediately
329
+ this.reset()
247
330
  }
248
331
 
249
332
  if (message.flag === Message.Flag.STOP_SENDING) {
250
- // the remote has stopped reading
251
- this.onRemoteCloseRead()
333
+ // The remote has stopped reading
334
+ this.remoteCloseRead()
252
335
  }
253
336
 
254
337
  if (message.flag === Message.Flag.FIN_ACK) {
255
- // remote received our FIN
256
- this.receivedFinAck?.resolve()
338
+ this.log.trace('received FIN_ACK')
339
+ this.receiveFinAck.resolve()
257
340
  }
258
341
  }
342
+
343
+ // ignore data messages if we've closed the readable end already
344
+ if (this.readStatus === 'ready') {
345
+ return message.message
346
+ }
259
347
  }
260
348
 
261
- private _sendFlag (flag: Message.Flag): boolean {
349
+ private async _sendFlag (flag: Message.Flag): Promise<boolean> {
262
350
  if (this.channel.readyState !== 'open') {
263
351
  // flags can be sent while we or the remote are closing the datachannel so
264
352
  // if the channel isn't open, don't try to send it but return false to let
@@ -272,7 +360,7 @@ export class WebRTCStream extends AbstractStream {
272
360
  const prefixedBuf = lengthPrefixed.encode.single(messageBuf)
273
361
 
274
362
  try {
275
- this._sendMessage(prefixedBuf)
363
+ await this._sendMessage(prefixedBuf, false)
276
364
 
277
365
  return true
278
366
  } catch (err: any) {
@@ -281,14 +369,6 @@ export class WebRTCStream extends AbstractStream {
281
369
 
282
370
  return false
283
371
  }
284
-
285
- sendPause (): void {
286
- // TODO: read backpressure?
287
- }
288
-
289
- sendResume (): void {
290
- // TODO: read backpressure?
291
- }
292
372
  }
293
373
 
294
374
  export interface WebRTCStreamOptions extends DataChannelOptions {
@@ -303,7 +383,12 @@ export interface WebRTCStreamOptions extends DataChannelOptions {
303
383
  /**
304
384
  * The stream direction
305
385
  */
306
- direction: MessageStreamDirection
386
+ direction: Direction
387
+
388
+ /**
389
+ * A callback invoked when the channel ends
390
+ */
391
+ onEnd?(err?: Error | undefined): void
307
392
 
308
393
  /**
309
394
  * The logger to create a scope from
@@ -314,16 +399,15 @@ export interface WebRTCStreamOptions extends DataChannelOptions {
314
399
  * If true the underlying datachannel is being used to perform the noise
315
400
  * handshake during connection establishment
316
401
  */
317
- isHandshake?: boolean
402
+ handshake?: boolean
318
403
  }
319
404
 
320
405
  export function createStream (options: WebRTCStreamOptions): WebRTCStream {
321
- const { channel, direction, isHandshake } = options
406
+ const { channel, direction, handshake } = options
322
407
 
323
408
  return new WebRTCStream({
324
409
  ...options,
325
410
  id: `${channel.id}`,
326
- log: options.log.newScope(`${isHandshake === true ? 'handshake' : direction}:${channel.id}`),
327
- protocol: ''
411
+ log: options.log.newScope(`${handshake === true ? 'handshake' : direction}:${channel.id}`)
328
412
  })
329
413
  }