@libp2p/webrtc 2.0.11 → 3.0.0-e66f4891

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 (91) hide show
  1. package/README.md +7 -2
  2. package/dist/index.min.js +18 -19
  3. package/dist/src/error.d.ts +2 -2
  4. package/dist/src/error.d.ts.map +1 -1
  5. package/dist/src/error.js +1 -1
  6. package/dist/src/error.js.map +1 -1
  7. package/dist/src/index.d.ts +1 -1
  8. package/dist/src/maconn.d.ts +5 -4
  9. package/dist/src/maconn.d.ts.map +1 -1
  10. package/dist/src/maconn.js +8 -5
  11. package/dist/src/maconn.js.map +1 -1
  12. package/dist/src/muxer.d.ts +10 -5
  13. package/dist/src/muxer.d.ts.map +1 -1
  14. package/dist/src/muxer.js +6 -2
  15. package/dist/src/muxer.js.map +1 -1
  16. package/dist/src/private-to-private/handler.d.ts +4 -3
  17. package/dist/src/private-to-private/handler.d.ts.map +1 -1
  18. package/dist/src/private-to-private/handler.js +104 -84
  19. package/dist/src/private-to-private/handler.js.map +1 -1
  20. package/dist/src/private-to-private/listener.d.ts +4 -3
  21. package/dist/src/private-to-private/listener.d.ts.map +1 -1
  22. package/dist/src/private-to-private/listener.js +1 -1
  23. package/dist/src/private-to-private/listener.js.map +1 -1
  24. package/dist/src/private-to-private/transport.d.ts +6 -5
  25. package/dist/src/private-to-private/transport.d.ts.map +1 -1
  26. package/dist/src/private-to-private/transport.js +13 -6
  27. package/dist/src/private-to-private/transport.js.map +1 -1
  28. package/dist/src/private-to-private/util.d.ts.map +1 -1
  29. package/dist/src/private-to-private/util.js +1 -0
  30. package/dist/src/private-to-private/util.js.map +1 -1
  31. package/dist/src/private-to-public/options.d.ts +1 -1
  32. package/dist/src/private-to-public/sdp.js +1 -1
  33. package/dist/src/private-to-public/transport.d.ts +4 -4
  34. package/dist/src/private-to-public/transport.d.ts.map +1 -1
  35. package/dist/src/private-to-public/transport.js +99 -92
  36. package/dist/src/private-to-public/transport.js.map +1 -1
  37. package/dist/src/private-to-public/util.d.ts.map +1 -1
  38. package/dist/src/private-to-public/util.js.map +1 -1
  39. package/dist/src/stream.d.ts +34 -3
  40. package/dist/src/stream.d.ts.map +1 -1
  41. package/dist/src/stream.js +24 -14
  42. package/dist/src/stream.js.map +1 -1
  43. package/dist/src/webrtc/index.browser.d.ts +15 -0
  44. package/dist/src/webrtc/index.browser.d.ts.map +1 -0
  45. package/dist/src/webrtc/index.browser.js +5 -0
  46. package/dist/src/webrtc/index.browser.js.map +1 -0
  47. package/dist/src/webrtc/index.d.ts +8 -0
  48. package/dist/src/webrtc/index.d.ts.map +1 -0
  49. package/dist/src/webrtc/index.js +11 -0
  50. package/dist/src/webrtc/index.js.map +1 -0
  51. package/dist/src/webrtc/rtc-data-channel.d.ts +29 -0
  52. package/dist/src/webrtc/rtc-data-channel.d.ts.map +1 -0
  53. package/dist/src/webrtc/rtc-data-channel.js +115 -0
  54. package/dist/src/webrtc/rtc-data-channel.js.map +1 -0
  55. package/dist/src/webrtc/rtc-events.d.ts +9 -0
  56. package/dist/src/webrtc/rtc-events.d.ts.map +1 -0
  57. package/dist/src/webrtc/rtc-events.js +15 -0
  58. package/dist/src/webrtc/rtc-events.js.map +1 -0
  59. package/dist/src/webrtc/rtc-ice-candidate.d.ts +22 -0
  60. package/dist/src/webrtc/rtc-ice-candidate.d.ts.map +1 -0
  61. package/dist/src/webrtc/rtc-ice-candidate.js +47 -0
  62. package/dist/src/webrtc/rtc-ice-candidate.js.map +1 -0
  63. package/dist/src/webrtc/rtc-peer-connection.d.ts +47 -0
  64. package/dist/src/webrtc/rtc-peer-connection.d.ts.map +1 -0
  65. package/dist/src/webrtc/rtc-peer-connection.js +245 -0
  66. package/dist/src/webrtc/rtc-peer-connection.js.map +1 -0
  67. package/dist/src/webrtc/rtc-session-description.d.ts +10 -0
  68. package/dist/src/webrtc/rtc-session-description.d.ts.map +1 -0
  69. package/dist/src/webrtc/rtc-session-description.js +18 -0
  70. package/dist/src/webrtc/rtc-session-description.js.map +1 -0
  71. package/package.json +29 -121
  72. package/src/error.ts +2 -2
  73. package/src/index.ts +1 -1
  74. package/src/maconn.ts +13 -8
  75. package/src/muxer.ts +11 -5
  76. package/src/private-to-private/handler.ts +124 -103
  77. package/src/private-to-private/listener.ts +4 -3
  78. package/src/private-to-private/transport.ts +20 -12
  79. package/src/private-to-private/util.ts +1 -0
  80. package/src/private-to-public/options.ts +1 -1
  81. package/src/private-to-public/sdp.ts +1 -1
  82. package/src/private-to-public/transport.ts +113 -107
  83. package/src/private-to-public/util.ts +0 -1
  84. package/src/stream.ts +29 -16
  85. package/src/webrtc/index.browser.ts +4 -0
  86. package/src/webrtc/index.ts +12 -0
  87. package/src/webrtc/rtc-data-channel.ts +140 -0
  88. package/src/webrtc/rtc-events.ts +19 -0
  89. package/src/webrtc/rtc-ice-candidate.ts +50 -0
  90. package/src/webrtc/rtc-peer-connection.ts +306 -0
  91. package/src/webrtc/rtc-session-description.ts +19 -0
@@ -1,17 +1,19 @@
1
- import { type CreateListenerOptions, type DialOptions, type Listener, symbol, type Transport, type Upgrader, type TransportManager } from '@libp2p/interface-transport'
2
- import { CodeError } from '@libp2p/interfaces/errors'
1
+ import { CodeError } from '@libp2p/interface/errors'
2
+ import { type CreateListenerOptions, type DialOptions, symbol, type Transport, type Listener, type Upgrader } from '@libp2p/interface/transport'
3
3
  import { logger } from '@libp2p/logger'
4
4
  import { peerIdFromString } from '@libp2p/peer-id'
5
5
  import { multiaddr, type Multiaddr, protocols } from '@multiformats/multiaddr'
6
6
  import { codes } from '../error.js'
7
7
  import { WebRTCMultiaddrConnection } from '../maconn.js'
8
+ import { cleanup } from '../webrtc/index.js'
8
9
  import { initiateConnection, handleIncomingStream } from './handler.js'
9
10
  import { WebRTCPeerListener } from './listener.js'
10
11
  import type { DataChannelOpts } from '../stream.js'
11
- import type { Connection } from '@libp2p/interface-connection'
12
- import type { PeerId } from '@libp2p/interface-peer-id'
13
- import type { IncomingStreamData, Registrar } from '@libp2p/interface-registrar'
14
- import type { Startable } from '@libp2p/interfaces/startable'
12
+ import type { Connection } from '@libp2p/interface/connection'
13
+ import type { PeerId } from '@libp2p/interface/peer-id'
14
+ import type { Startable } from '@libp2p/interface/startable'
15
+ import type { IncomingStreamData, Registrar } from '@libp2p/interface-internal/registrar'
16
+ import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
15
17
 
16
18
  const log = logger('libp2p:webrtc:peer')
17
19
 
@@ -48,12 +50,15 @@ export class WebRTCTransport implements Transport, Startable {
48
50
  async start (): Promise<void> {
49
51
  await this.components.registrar.handle(SIGNALING_PROTO_ID, (data: IncomingStreamData) => {
50
52
  this._onProtocol(data).catch(err => { log.error('failed to handle incoming connect from %p', data.connection.remotePeer, err) })
53
+ }, {
54
+ runOnTransientConnection: true
51
55
  })
52
56
  this._started = true
53
57
  }
54
58
 
55
59
  async stop (): Promise<void> {
56
60
  await this.components.registrar.unhandle(SIGNALING_PROTO_ID)
61
+ cleanup()
57
62
  this._started = false
58
63
  }
59
64
 
@@ -89,7 +94,10 @@ export class WebRTCTransport implements Transport, Startable {
89
94
  }
90
95
 
91
96
  const connection = await this.components.transportManager.dial(baseAddr, options)
92
- const signalingStream = await connection.newStream([SIGNALING_PROTO_ID], options)
97
+ const signalingStream = await connection.newStream(SIGNALING_PROTO_ID, {
98
+ ...options,
99
+ runOnTransientConnection: true
100
+ })
93
101
 
94
102
  try {
95
103
  const { pc, muxerFactory, remoteAddress } = await initiateConnection({
@@ -113,11 +121,11 @@ export class WebRTCTransport implements Transport, Startable {
113
121
  )
114
122
 
115
123
  // close the stream if SDP has been exchanged successfully
116
- signalingStream.close()
124
+ await signalingStream.close()
117
125
  return result
118
- } catch (err) {
126
+ } catch (err: any) {
119
127
  // reset the stream in case of any error
120
- signalingStream.reset()
128
+ signalingStream.abort(err)
121
129
  throw err
122
130
  } finally {
123
131
  // Close the signaling connection
@@ -143,8 +151,8 @@ export class WebRTCTransport implements Transport, Startable {
143
151
  skipProtection: true,
144
152
  muxerFactory
145
153
  })
146
- } catch (err) {
147
- stream.reset()
154
+ } catch (err: any) {
155
+ stream.abort(err)
148
156
  throw err
149
157
  } finally {
150
158
  // Close the signaling connection
@@ -1,5 +1,6 @@
1
1
  import { logger } from '@libp2p/logger'
2
2
  import { isFirefox } from '../util.js'
3
+ import { RTCIceCandidate } from '../webrtc/index.js'
3
4
  import { Message } from './pb/message.js'
4
5
  import type { DeferredPromise } from 'p-defer'
5
6
 
@@ -1,4 +1,4 @@
1
- import type { CreateListenerOptions, DialOptions } from '@libp2p/interface-transport'
1
+ import type { CreateListenerOptions, DialOptions } from '@libp2p/interface/transport'
2
2
 
3
3
  export interface WebRTCListenerOptions extends CreateListenerOptions {}
4
4
  export interface WebRTCDialOptions extends DialOptions {}
@@ -133,7 +133,7 @@ a=ice-ufrag:${ufrag}
133
133
  a=ice-pwd:${ufrag}
134
134
  a=fingerprint:${CERTFP}
135
135
  a=sctp-port:5000
136
- a=max-message-size:100000
136
+ a=max-message-size:16384
137
137
  a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host\r\n`
138
138
  }
139
139
 
@@ -1,5 +1,5 @@
1
1
  import { noise as Noise } from '@chainsafe/libp2p-noise'
2
- import { type CreateListenerOptions, type Listener, symbol, type Transport } from '@libp2p/interface-transport'
2
+ import { type CreateListenerOptions, symbol, type Transport, type Listener } from '@libp2p/interface/transport'
3
3
  import { logger } from '@libp2p/logger'
4
4
  import * as p from '@libp2p/peer-id'
5
5
  import { protocols } from '@multiformats/multiaddr'
@@ -11,13 +11,14 @@ import { WebRTCMultiaddrConnection } from '../maconn.js'
11
11
  import { DataChannelMuxerFactory } from '../muxer.js'
12
12
  import { createStream } from '../stream.js'
13
13
  import { isFirefox } from '../util.js'
14
+ import { RTCPeerConnection } from '../webrtc/index.js'
14
15
  import * as sdp from './sdp.js'
15
16
  import { genUfrag } from './util.js'
16
17
  import type { WebRTCDialOptions } from './options.js'
17
18
  import type { DataChannelOpts } from '../stream.js'
18
- import type { Connection } from '@libp2p/interface-connection'
19
- import type { CounterGroup, Metrics } from '@libp2p/interface-metrics'
20
- import type { PeerId } from '@libp2p/interface-peer-id'
19
+ import type { Connection } from '@libp2p/interface/connection'
20
+ import type { CounterGroup, Metrics } from '@libp2p/interface/metrics'
21
+ import type { PeerId } from '@libp2p/interface/peer-id'
21
22
  import type { Multiaddr } from '@multiformats/multiaddr'
22
23
 
23
24
  const log = logger('libp2p:webrtc:transport')
@@ -134,116 +135,121 @@ export class WebRTCDirectTransport implements Transport {
134
135
 
135
136
  const peerConnection = new RTCPeerConnection({ certificates: [certificate] })
136
137
 
137
- // create data channel for running the noise handshake. Once the data channel is opened,
138
- // the remote will initiate the noise handshake. This is used to confirm the identity of
139
- // the peer.
140
- const dataChannelOpenPromise = new Promise<RTCDataChannel>((resolve, reject) => {
141
- const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 })
142
- const handshakeTimeout = setTimeout(() => {
143
- const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}`
144
- log.error(error)
145
- this.metrics?.dialerEvents.increment({ open_error: true })
146
- reject(dataChannelError('data', error))
147
- }, HANDSHAKE_TIMEOUT_MS)
148
-
149
- handshakeDataChannel.onopen = (_) => {
150
- clearTimeout(handshakeTimeout)
151
- resolve(handshakeDataChannel)
152
- }
153
-
154
- // ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
155
- handshakeDataChannel.onerror = (event: Event) => {
156
- clearTimeout(handshakeTimeout)
157
- const errorTarget = event.target?.toString() ?? 'not specified'
158
- const error = `Error opening a data channel for handshaking: ${errorTarget}`
159
- log.error(error)
160
- // NOTE: We use unknown error here but this could potentially be considered a reset by some standards.
161
- this.metrics?.dialerEvents.increment({ unknown_error: true })
162
- reject(dataChannelError('data', error))
163
- }
164
- })
165
-
166
- const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32)
167
-
168
- // Create offer and munge sdp with ufrag == pwd. This allows the remote to
169
- // respond to STUN messages without performing an actual SDP exchange.
170
- // This is because it can infer the passwd field by reading the USERNAME
171
- // attribute of the STUN message.
172
- const offerSdp = await peerConnection.createOffer()
173
- const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
174
- await peerConnection.setLocalDescription(mungedOfferSdp)
175
-
176
- // construct answer sdp from multiaddr and ufrag
177
- const answerSdp = sdp.fromMultiAddr(ma, ufrag)
178
- await peerConnection.setRemoteDescription(answerSdp)
179
-
180
- // wait for peerconnection.onopen to fire, or for the datachannel to open
181
- const handshakeDataChannel = await dataChannelOpenPromise
182
-
183
- const myPeerId = this.components.peerId
184
-
185
- // Do noise handshake.
186
- // Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
187
- // <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
188
- const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma)
189
-
190
- // Since we use the default crypto interface and do not use a static key or early data,
191
- // we pass in undefined for these parameters.
192
- const noise = Noise({ prologueBytes: fingerprintsPrologue })()
193
-
194
- const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel })
195
- const wrappedDuplex = {
196
- ...wrappedChannel,
197
- sink: wrappedChannel.sink.bind(wrappedChannel),
198
- source: (async function * () {
199
- for await (const list of wrappedChannel.source) {
200
- for (const buf of list) {
201
- yield buf
202
- }
138
+ try {
139
+ // create data channel for running the noise handshake. Once the data channel is opened,
140
+ // the remote will initiate the noise handshake. This is used to confirm the identity of
141
+ // the peer.
142
+ const dataChannelOpenPromise = new Promise<RTCDataChannel>((resolve, reject) => {
143
+ const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 })
144
+ const handshakeTimeout = setTimeout(() => {
145
+ const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}`
146
+ log.error(error)
147
+ this.metrics?.dialerEvents.increment({ open_error: true })
148
+ reject(dataChannelError('data', error))
149
+ }, HANDSHAKE_TIMEOUT_MS)
150
+
151
+ handshakeDataChannel.onopen = (_) => {
152
+ clearTimeout(handshakeTimeout)
153
+ resolve(handshakeDataChannel)
203
154
  }
204
- }())
205
- }
206
155
 
207
- // Creating the connection before completion of the noise
208
- // handshake ensures that the stream opening callback is set up
209
- const maConn = new WebRTCMultiaddrConnection({
210
- peerConnection,
211
- remoteAddr: ma,
212
- timeline: {
213
- open: Date.now()
214
- },
215
- metrics: this.metrics?.dialerEvents
216
- })
217
-
218
- const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange'
219
-
220
- peerConnection.addEventListener(eventListeningName, () => {
221
- switch (peerConnection.connectionState) {
222
- case 'failed':
223
- case 'disconnected':
224
- case 'closed':
225
- maConn.close().catch((err) => {
226
- log.error('error closing connection', err)
227
- }).finally(() => {
228
- // Remove the event listener once the connection is closed
229
- controller.abort()
230
- })
231
- break
232
- default:
233
- break
156
+ // ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event
157
+ handshakeDataChannel.onerror = (event: Event) => {
158
+ clearTimeout(handshakeTimeout)
159
+ const errorTarget = event.target?.toString() ?? 'not specified'
160
+ const error = `Error opening a data channel for handshaking: ${errorTarget}`
161
+ log.error(error)
162
+ // NOTE: We use unknown error here but this could potentially be considered a reset by some standards.
163
+ this.metrics?.dialerEvents.increment({ unknown_error: true })
164
+ reject(dataChannelError('data', error))
165
+ }
166
+ })
167
+
168
+ const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32)
169
+
170
+ // Create offer and munge sdp with ufrag == pwd. This allows the remote to
171
+ // respond to STUN messages without performing an actual SDP exchange.
172
+ // This is because it can infer the passwd field by reading the USERNAME
173
+ // attribute of the STUN message.
174
+ const offerSdp = await peerConnection.createOffer()
175
+ const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
176
+ await peerConnection.setLocalDescription(mungedOfferSdp)
177
+
178
+ // construct answer sdp from multiaddr and ufrag
179
+ const answerSdp = sdp.fromMultiAddr(ma, ufrag)
180
+ await peerConnection.setRemoteDescription(answerSdp)
181
+
182
+ // wait for peerconnection.onopen to fire, or for the datachannel to open
183
+ const handshakeDataChannel = await dataChannelOpenPromise
184
+
185
+ const myPeerId = this.components.peerId
186
+
187
+ // Do noise handshake.
188
+ // Set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
189
+ // <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
190
+ const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma)
191
+
192
+ // Since we use the default crypto interface and do not use a static key or early data,
193
+ // we pass in undefined for these parameters.
194
+ const noise = Noise({ prologueBytes: fingerprintsPrologue })()
195
+
196
+ const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel })
197
+ const wrappedDuplex = {
198
+ ...wrappedChannel,
199
+ sink: wrappedChannel.sink.bind(wrappedChannel),
200
+ source: (async function * () {
201
+ for await (const list of wrappedChannel.source) {
202
+ for (const buf of list) {
203
+ yield buf
204
+ }
205
+ }
206
+ }())
234
207
  }
235
- }, { signal })
236
208
 
237
- // Track opened peer connection
238
- this.metrics?.dialerEvents.increment({ peer_connection: true })
209
+ // Creating the connection before completion of the noise
210
+ // handshake ensures that the stream opening callback is set up
211
+ const maConn = new WebRTCMultiaddrConnection({
212
+ peerConnection,
213
+ remoteAddr: ma,
214
+ timeline: {
215
+ open: Date.now()
216
+ },
217
+ metrics: this.metrics?.dialerEvents
218
+ })
219
+
220
+ const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange'
221
+
222
+ peerConnection.addEventListener(eventListeningName, () => {
223
+ switch (peerConnection.connectionState) {
224
+ case 'failed':
225
+ case 'disconnected':
226
+ case 'closed':
227
+ maConn.close().catch((err) => {
228
+ log.error('error closing connection', err)
229
+ }).finally(() => {
230
+ // Remove the event listener once the connection is closed
231
+ controller.abort()
232
+ })
233
+ break
234
+ default:
235
+ break
236
+ }
237
+ }, { signal })
239
238
 
240
- const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel })
239
+ // Track opened peer connection
240
+ this.metrics?.dialerEvents.increment({ peer_connection: true })
241
241
 
242
- // For outbound connections, the remote is expected to start the noise handshake.
243
- // Therefore, we need to secure an inbound noise connection from the remote.
244
- await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId)
242
+ const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel })
245
243
 
246
- return options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory })
244
+ // For outbound connections, the remote is expected to start the noise handshake.
245
+ // Therefore, we need to secure an inbound noise connection from the remote.
246
+ await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId)
247
+
248
+ return await options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory })
249
+ } catch (err) {
250
+ peerConnection.close()
251
+ throw err
252
+ }
247
253
  }
248
254
 
249
255
  /**
@@ -1,3 +1,2 @@
1
-
2
1
  const charset = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')
3
2
  export const genUfrag = (len: number): string => [...Array(len)].map(() => charset.at(Math.floor(Math.random() * charset.length))).join('')
package/src/stream.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface-stream-muxer/stream'
2
- import { CodeError } from '@libp2p/interfaces/errors'
1
+ import { CodeError } from '@libp2p/interface/errors'
2
+ import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface/stream-muxer/stream'
3
3
  import { logger } from '@libp2p/logger'
4
4
  import * as lengthPrefixed from 'it-length-prefixed'
5
5
  import { type Pushable, pushable } from 'it-pushable'
6
6
  import { pEvent, TimeoutError } from 'p-event'
7
7
  import { Uint8ArrayList } from 'uint8arraylist'
8
8
  import { Message } from './pb/message.js'
9
- import type { Direction, Stream } from '@libp2p/interface-connection'
9
+ import type { Direction } from '@libp2p/interface/connection'
10
10
 
11
11
  const log = logger('libp2p:webrtc:stream')
12
12
 
@@ -26,6 +26,8 @@ export interface WebRTCStreamInit extends AbstractStreamInit {
26
26
  channel: RTCDataChannel
27
27
 
28
28
  dataChannelOptions?: Partial<DataChannelOpts>
29
+
30
+ maxDataSize: number
29
31
  }
30
32
 
31
33
  // Max message size that can be sent to the DataChannel
@@ -40,7 +42,7 @@ const BUFFERED_AMOUNT_LOW_TIMEOUT = 30 * 1000
40
42
  // protobuf field definition overhead
41
43
  const PROTOBUF_OVERHEAD = 3
42
44
 
43
- class WebRTCStream extends AbstractStream {
45
+ export class WebRTCStream extends AbstractStream {
44
46
  /**
45
47
  * The data channel used to send and receive data
46
48
  */
@@ -58,6 +60,7 @@ class WebRTCStream extends AbstractStream {
58
60
  private readonly incomingData: Pushable<Uint8Array>
59
61
 
60
62
  private messageQueue?: Uint8ArrayList
63
+ private readonly maxDataSize: number
61
64
 
62
65
  constructor (init: WebRTCStreamInit) {
63
66
  super(init)
@@ -71,6 +74,7 @@ class WebRTCStream extends AbstractStream {
71
74
  maxBufferedAmount: init.dataChannelOptions?.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT,
72
75
  maxMessageSize: init.dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE
73
76
  }
77
+ this.maxDataSize = init.maxDataSize
74
78
 
75
79
  // set up initial state
76
80
  switch (this.channel.readyState) {
@@ -79,8 +83,8 @@ class WebRTCStream extends AbstractStream {
79
83
 
80
84
  case 'closed':
81
85
  case 'closing':
82
- if (this.stat.timeline.close === undefined || this.stat.timeline.close === 0) {
83
- this.stat.timeline.close = Date.now()
86
+ if (this.timeline.close === undefined || this.timeline.close === 0) {
87
+ this.timeline.close = Date.now()
84
88
  }
85
89
  break
86
90
  case 'connecting':
@@ -94,7 +98,7 @@ class WebRTCStream extends AbstractStream {
94
98
 
95
99
  // handle RTCDataChannel events
96
100
  this.channel.onopen = (_evt) => {
97
- this.stat.timeline.open = new Date().getTime()
101
+ this.timeline.open = new Date().getTime()
98
102
 
99
103
  if (this.messageQueue != null) {
100
104
  // send any queued messages
@@ -107,7 +111,9 @@ class WebRTCStream extends AbstractStream {
107
111
  }
108
112
 
109
113
  this.channel.onclose = (_evt) => {
110
- this.close()
114
+ void this.close().catch(err => {
115
+ log.error('error closing stream after channel closed', err)
116
+ })
111
117
  }
112
118
 
113
119
  this.channel.onerror = (evt) => {
@@ -153,7 +159,6 @@ class WebRTCStream extends AbstractStream {
153
159
  await pEvent(this.channel, 'bufferedamountlow', { timeout: this.dataChannelOptions.bufferedAmountLowEventTimeout })
154
160
  } catch (err: any) {
155
161
  if (err instanceof TimeoutError) {
156
- this.abort(err)
157
162
  throw new Error('Timed out waiting for DataChannel buffer to clear')
158
163
  }
159
164
 
@@ -184,10 +189,17 @@ class WebRTCStream extends AbstractStream {
184
189
  }
185
190
 
186
191
  async sendData (data: Uint8ArrayList): Promise<void> {
187
- const msgbuf = Message.encode({ message: data.subarray() })
188
- const sendbuf = lengthPrefixed.encode.single(msgbuf)
192
+ data = data.sublist()
189
193
 
190
- await this._sendMessage(sendbuf)
194
+ while (data.byteLength > 0) {
195
+ const toSend = Math.min(data.byteLength, this.maxDataSize)
196
+ const buf = data.subarray(0, toSend)
197
+ const msgbuf = Message.encode({ message: buf })
198
+ const sendbuf = lengthPrefixed.encode.single(msgbuf)
199
+ await this._sendMessage(sendbuf)
200
+
201
+ data.consume(toSend)
202
+ }
191
203
  }
192
204
 
193
205
  async sendReset (): Promise<void> {
@@ -212,7 +224,7 @@ class WebRTCStream extends AbstractStream {
212
224
  if (message.flag === Message.Flag.FIN) {
213
225
  // We should expect no more data from the remote, stop reading
214
226
  this.incomingData.end()
215
- this.closeRead()
227
+ this.remoteCloseWrite()
216
228
  }
217
229
 
218
230
  if (message.flag === Message.Flag.RESET) {
@@ -222,7 +234,7 @@ class WebRTCStream extends AbstractStream {
222
234
 
223
235
  if (message.flag === Message.Flag.STOP_SENDING) {
224
236
  // The remote has stopped reading
225
- this.closeWrite()
237
+ this.remoteCloseRead()
226
238
  }
227
239
  }
228
240
 
@@ -259,7 +271,7 @@ export interface WebRTCStreamOptions {
259
271
  onEnd?: (err?: Error | undefined) => void
260
272
  }
261
273
 
262
- export function createStream (options: WebRTCStreamOptions): Stream {
274
+ export function createStream (options: WebRTCStreamOptions): WebRTCStream {
263
275
  const { channel, direction, onEnd, dataChannelOptions } = options
264
276
 
265
277
  return new WebRTCStream({
@@ -268,6 +280,7 @@ export function createStream (options: WebRTCStreamOptions): Stream {
268
280
  maxDataSize: (dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD,
269
281
  dataChannelOptions,
270
282
  onEnd,
271
- channel
283
+ channel,
284
+ log: logger(`libp2p:mplex:stream:${direction}:${channel.id}`)
272
285
  })
273
286
  }
@@ -0,0 +1,4 @@
1
+ export const RTCPeerConnection = globalThis.RTCPeerConnection
2
+ export const RTCSessionDescription = globalThis.RTCSessionDescription
3
+ export const RTCIceCandidate = globalThis.RTCIceCandidate
4
+ export function cleanup (): void {}
@@ -0,0 +1,12 @@
1
+ import node from 'node-datachannel'
2
+ import { IceCandidate } from './rtc-ice-candidate.js'
3
+ import { PeerConnection } from './rtc-peer-connection.js'
4
+ import { SessionDescription } from './rtc-session-description.js'
5
+
6
+ export { SessionDescription as RTCSessionDescription }
7
+ export { IceCandidate as RTCIceCandidate }
8
+ export { PeerConnection as RTCPeerConnection }
9
+
10
+ export function cleanup (): void {
11
+ node.cleanup()
12
+ }
@@ -0,0 +1,140 @@
1
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
2
+ import type node from 'node-datachannel'
3
+
4
+ export class DataChannel extends EventTarget implements RTCDataChannel {
5
+ binaryType: BinaryType
6
+
7
+ readonly maxPacketLifeTime: number | null
8
+ readonly maxRetransmits: number | null
9
+ readonly negotiated: boolean
10
+ readonly ordered: boolean
11
+
12
+ onbufferedamountlow: ((this: RTCDataChannel, ev: Event) => any) | null
13
+ onclose: ((this: RTCDataChannel, ev: Event) => any) | null
14
+ onclosing: ((this: RTCDataChannel, ev: Event) => any) | null
15
+ onerror: ((this: RTCDataChannel, ev: Event) => any) | null
16
+ onmessage: ((this: RTCDataChannel, ev: MessageEvent) => any) | null
17
+ onopen: ((this: RTCDataChannel, ev: Event) => any) | null
18
+
19
+ #dataChannel: node.DataChannel
20
+ #bufferedAmountLowThreshold: number
21
+ #readyState: RTCDataChannelState
22
+
23
+ constructor (dataChannel: node.DataChannel, dataChannelDict: RTCDataChannelInit = {}) {
24
+ super()
25
+
26
+ this.#dataChannel = dataChannel
27
+ this.#readyState = 'connecting'
28
+ this.#bufferedAmountLowThreshold = 0
29
+
30
+ this.binaryType = 'arraybuffer'
31
+
32
+ this.#dataChannel.onOpen(() => {
33
+ this.#readyState = 'open'
34
+ this.dispatchEvent(new Event('open'))
35
+ })
36
+ this.#dataChannel.onClosed(() => {
37
+ this.#readyState = 'closed'
38
+ this.dispatchEvent(new Event('close'))
39
+ })
40
+ this.#dataChannel.onError((msg) => {
41
+ this.#readyState = 'closed'
42
+ this.dispatchEvent(new RTCErrorEvent('error', {
43
+ error: new RTCError({
44
+ errorDetail: 'data-channel-failure'
45
+ }, msg)
46
+ }))
47
+ })
48
+ this.#dataChannel.onBufferedAmountLow(() => {
49
+ this.dispatchEvent(new Event('bufferedamountlow'))
50
+ })
51
+ this.#dataChannel.onMessage((data: string | Uint8Array) => {
52
+ if (typeof data === 'string') {
53
+ data = uint8ArrayFromString(data)
54
+ }
55
+
56
+ this.dispatchEvent(new MessageEvent('message', { data }))
57
+ })
58
+
59
+ // forward events to properties
60
+ this.addEventListener('message', event => {
61
+ this.onmessage?.(event as MessageEvent<ArrayBuffer>)
62
+ })
63
+ this.addEventListener('bufferedamountlow', event => {
64
+ this.onbufferedamountlow?.(event)
65
+ })
66
+ this.addEventListener('error', event => {
67
+ this.onerror?.(event)
68
+ })
69
+ this.addEventListener('close', event => {
70
+ this.onclose?.(event)
71
+ })
72
+ this.addEventListener('closing', event => {
73
+ this.onclosing?.(event)
74
+ })
75
+ this.addEventListener('open', event => {
76
+ this.onopen?.(event)
77
+ })
78
+
79
+ this.onbufferedamountlow = null
80
+ this.onclose = null
81
+ this.onclosing = null
82
+ this.onerror = null
83
+ this.onmessage = null
84
+ this.onopen = null
85
+
86
+ this.maxPacketLifeTime = dataChannelDict.maxPacketLifeTime ?? null
87
+ this.maxRetransmits = dataChannelDict.maxRetransmits ?? null
88
+ this.negotiated = dataChannelDict.negotiated ?? false
89
+ this.ordered = dataChannelDict.ordered ?? true
90
+ }
91
+
92
+ get id (): number {
93
+ return this.#dataChannel.getId()
94
+ }
95
+
96
+ get label (): string {
97
+ return this.#dataChannel.getLabel()
98
+ }
99
+
100
+ get protocol (): string {
101
+ return this.#dataChannel.getProtocol()
102
+ }
103
+
104
+ get bufferedAmount (): number {
105
+ return this.#dataChannel.bufferedAmount()
106
+ }
107
+
108
+ set bufferedAmountLowThreshold (threshold: number) {
109
+ this.#bufferedAmountLowThreshold = threshold
110
+ this.#dataChannel.setBufferedAmountLowThreshold(threshold)
111
+ }
112
+
113
+ get bufferedAmountLowThreshold (): number {
114
+ return this.#bufferedAmountLowThreshold
115
+ }
116
+
117
+ get readyState (): RTCDataChannelState {
118
+ return this.#readyState
119
+ }
120
+
121
+ close (): void {
122
+ this.#readyState = 'closing'
123
+ this.dispatchEvent(new Event('closing'))
124
+
125
+ this.#dataChannel.close()
126
+ }
127
+
128
+ send (data: string): void
129
+ send (data: Blob): void
130
+ send (data: ArrayBuffer): void
131
+ send (data: ArrayBufferView): void
132
+ send (data: any): void {
133
+ // TODO: sending Blobs
134
+ if (typeof data === 'string') {
135
+ this.#dataChannel.sendMessage(data)
136
+ } else {
137
+ this.#dataChannel.sendMessageBinary(data)
138
+ }
139
+ }
140
+ }