@libp2p/webrtc 3.2.1 → 3.2.2-62a56b54

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 (67) hide show
  1. package/dist/index.min.js +13 -13
  2. package/dist/src/index.d.ts +29 -0
  3. package/dist/src/index.d.ts.map +1 -1
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/maconn.d.ts.map +1 -1
  6. package/dist/src/maconn.js +5 -2
  7. package/dist/src/maconn.js.map +1 -1
  8. package/dist/src/muxer.d.ts +10 -13
  9. package/dist/src/muxer.d.ts.map +1 -1
  10. package/dist/src/muxer.js +44 -29
  11. package/dist/src/muxer.js.map +1 -1
  12. package/dist/src/pb/message.d.ts +2 -1
  13. package/dist/src/pb/message.d.ts.map +1 -1
  14. package/dist/src/pb/message.js +2 -0
  15. package/dist/src/pb/message.js.map +1 -1
  16. package/dist/src/private-to-private/initiate-connection.d.ts +25 -0
  17. package/dist/src/private-to-private/initiate-connection.d.ts.map +1 -0
  18. package/dist/src/private-to-private/initiate-connection.js +145 -0
  19. package/dist/src/private-to-private/initiate-connection.js.map +1 -0
  20. package/dist/src/private-to-private/listener.d.ts +6 -2
  21. package/dist/src/private-to-private/listener.d.ts.map +1 -1
  22. package/dist/src/private-to-private/listener.js +6 -3
  23. package/dist/src/private-to-private/listener.js.map +1 -1
  24. package/dist/src/private-to-private/signaling-stream-handler.d.ts +10 -0
  25. package/dist/src/private-to-private/signaling-stream-handler.d.ts.map +1 -0
  26. package/dist/src/private-to-private/signaling-stream-handler.js +97 -0
  27. package/dist/src/private-to-private/signaling-stream-handler.js.map +1 -0
  28. package/dist/src/private-to-private/transport.d.ts +12 -2
  29. package/dist/src/private-to-private/transport.d.ts.map +1 -1
  30. package/dist/src/private-to-private/transport.js +67 -56
  31. package/dist/src/private-to-private/transport.js.map +1 -1
  32. package/dist/src/private-to-private/util.d.ts +6 -5
  33. package/dist/src/private-to-private/util.d.ts.map +1 -1
  34. package/dist/src/private-to-private/util.js +72 -21
  35. package/dist/src/private-to-private/util.js.map +1 -1
  36. package/dist/src/private-to-public/transport.d.ts +2 -2
  37. package/dist/src/private-to-public/transport.d.ts.map +1 -1
  38. package/dist/src/private-to-public/transport.js +2 -2
  39. package/dist/src/private-to-public/transport.js.map +1 -1
  40. package/dist/src/stream.d.ts +39 -19
  41. package/dist/src/stream.d.ts.map +1 -1
  42. package/dist/src/stream.js +135 -39
  43. package/dist/src/stream.js.map +1 -1
  44. package/dist/src/util.d.ts +6 -0
  45. package/dist/src/util.d.ts.map +1 -1
  46. package/dist/src/util.js +46 -0
  47. package/dist/src/util.js.map +1 -1
  48. package/package.json +17 -11
  49. package/src/index.ts +34 -0
  50. package/src/maconn.ts +7 -2
  51. package/src/muxer.ts +58 -44
  52. package/src/pb/message.proto +6 -1
  53. package/src/pb/message.ts +4 -2
  54. package/src/private-to-private/initiate-connection.ts +191 -0
  55. package/src/private-to-private/listener.ts +12 -4
  56. package/src/private-to-private/signaling-stream-handler.ts +129 -0
  57. package/src/private-to-private/transport.ts +87 -59
  58. package/src/private-to-private/util.ts +89 -24
  59. package/src/private-to-public/transport.ts +4 -4
  60. package/src/stream.ts +163 -61
  61. package/src/util.ts +60 -0
  62. package/dist/src/private-to-private/handler.d.ts +0 -26
  63. package/dist/src/private-to-private/handler.d.ts.map +0 -1
  64. package/dist/src/private-to-private/handler.js +0 -137
  65. package/dist/src/private-to-private/handler.js.map +0 -1
  66. package/dist/typedoc-urls.json +0 -6
  67. package/src/private-to-private/handler.ts +0 -177
package/src/index.ts CHANGED
@@ -3,6 +3,40 @@ import { WebRTCDirectTransport, type WebRTCTransportDirectInit, type WebRTCDirec
3
3
  import type { WebRTCTransportComponents, WebRTCTransportInit } from './private-to-private/transport.js'
4
4
  import type { Transport } from '@libp2p/interface/transport'
5
5
 
6
+ export interface DataChannelOptions {
7
+ /**
8
+ * The maximum message size sendable over the channel in bytes (default 16KB)
9
+ */
10
+ maxMessageSize?: number
11
+
12
+ /**
13
+ * If the channel's `bufferedAmount` grows over this amount in bytes, wait
14
+ * for it to drain before sending more data (default: 16MB)
15
+ */
16
+ maxBufferedAmount?: number
17
+
18
+ /**
19
+ * When `bufferedAmount` is above `maxBufferedAmount`, we pause sending until
20
+ * the `bufferedAmountLow` event fires - this controls how long we wait for
21
+ * that event in ms (default: 30s)
22
+ */
23
+ bufferedAmountLowEventTimeout?: number
24
+
25
+ /**
26
+ * When closing a stream, we wait for `bufferedAmount` to become 0 before
27
+ * closing the underlying RTCDataChannel - this controls how long we wait
28
+ * in ms (default: 30s)
29
+ */
30
+ drainTimeout?: number
31
+
32
+ /**
33
+ * When closing a stream we first send a FIN flag to the remote and wait
34
+ * for a FIN_ACK reply before closing the underlying RTCDataChannel - this
35
+ * controls how long we wait for the acknowledgement in ms (default: 5s)
36
+ */
37
+ closeTimeout?: number
38
+ }
39
+
6
40
  /**
7
41
  * @param {WebRTCTransportDirectInit} init - WebRTC direct transport configuration
8
42
  * @param init.dataChannel - DataChannel configurations
package/src/maconn.ts CHANGED
@@ -5,7 +5,7 @@ import type { CounterGroup } from '@libp2p/interface/metrics'
5
5
  import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr'
6
6
  import type { Source, Sink } from 'it-stream-types'
7
7
 
8
- const log = logger('libp2p:webrtc:connection')
8
+ const log = logger('libp2p:webrtc:maconn')
9
9
 
10
10
  interface WebRTCMultiaddrConnectionInit {
11
11
  /**
@@ -65,8 +65,13 @@ export class WebRTCMultiaddrConnection implements MultiaddrConnection {
65
65
  this.timeline = init.timeline
66
66
  this.peerConnection = init.peerConnection
67
67
 
68
+ const initialState = this.peerConnection.connectionState
69
+
68
70
  this.peerConnection.onconnectionstatechange = () => {
69
- if (this.peerConnection.connectionState === 'closed' || this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed') {
71
+ log.trace('peer connection state change', this.peerConnection.connectionState, 'initial state', initialState)
72
+
73
+ if (this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed' || this.peerConnection.connectionState === 'closed') {
74
+ // nothing else to do but close the connection
70
75
  this.timeline.close = Date.now()
71
76
  }
72
77
  }
package/src/muxer.ts CHANGED
@@ -1,6 +1,7 @@
1
+ import { logger } from '@libp2p/logger'
1
2
  import { createStream } from './stream.js'
2
- import { nopSink, nopSource } from './util.js'
3
- import type { DataChannelOpts } from './stream.js'
3
+ import { drainAndClose, nopSink, nopSource } from './util.js'
4
+ import type { DataChannelOptions } from './index.js'
4
5
  import type { Stream } from '@libp2p/interface/connection'
5
6
  import type { CounterGroup } from '@libp2p/interface/metrics'
6
7
  import type { StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface/stream-muxer'
@@ -8,6 +9,8 @@ import type { AbortOptions } from '@multiformats/multiaddr'
8
9
  import type { Source, Sink } from 'it-stream-types'
9
10
  import type { Uint8ArrayList } from 'uint8arraylist'
10
11
 
12
+ const log = logger('libp2p:webrtc:muxer')
13
+
11
14
  const PROTOCOL = '/webrtc'
12
15
 
13
16
  export interface DataChannelMuxerFactoryInit {
@@ -17,19 +20,16 @@ export interface DataChannelMuxerFactoryInit {
17
20
  peerConnection: RTCPeerConnection
18
21
 
19
22
  /**
20
- * Optional metrics for this data channel muxer
23
+ * The protocol to use
21
24
  */
22
- metrics?: CounterGroup
25
+ protocol?: string
23
26
 
24
27
  /**
25
- * Data channel options
28
+ * Optional metrics for this data channel muxer
26
29
  */
27
- dataChannelOptions?: Partial<DataChannelOpts>
30
+ metrics?: CounterGroup
28
31
 
29
- /**
30
- * The protocol to use
31
- */
32
- protocol?: string
32
+ dataChannelOptions?: DataChannelOptions
33
33
  }
34
34
 
35
35
  export class DataChannelMuxerFactory implements StreamMuxerFactory {
@@ -41,23 +41,23 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory {
41
41
  private readonly peerConnection: RTCPeerConnection
42
42
  private streamBuffer: Stream[] = []
43
43
  private readonly metrics?: CounterGroup
44
- private readonly dataChannelOptions?: Partial<DataChannelOpts>
44
+ private readonly dataChannelOptions?: DataChannelOptions
45
45
 
46
46
  constructor (init: DataChannelMuxerFactoryInit) {
47
47
  this.peerConnection = init.peerConnection
48
48
  this.metrics = init.metrics
49
49
  this.protocol = init.protocol ?? PROTOCOL
50
- this.dataChannelOptions = init.dataChannelOptions
50
+ this.dataChannelOptions = init.dataChannelOptions ?? {}
51
51
 
52
52
  // store any datachannels opened before upgrade has been completed
53
53
  this.peerConnection.ondatachannel = ({ channel }) => {
54
54
  const stream = createStream({
55
55
  channel,
56
56
  direction: 'inbound',
57
- dataChannelOptions: init.dataChannelOptions,
58
57
  onEnd: () => {
59
58
  this.streamBuffer = this.streamBuffer.filter(s => s.id !== stream.id)
60
- }
59
+ },
60
+ ...this.dataChannelOptions
61
61
  })
62
62
  this.streamBuffer.push(stream)
63
63
  }
@@ -90,34 +90,15 @@ export class DataChannelMuxer implements StreamMuxer {
90
90
  public protocol: string
91
91
 
92
92
  private readonly peerConnection: RTCPeerConnection
93
- private readonly dataChannelOptions?: DataChannelOpts
93
+ private readonly dataChannelOptions: DataChannelOptions
94
94
  private readonly metrics?: CounterGroup
95
95
 
96
- /**
97
- * Gracefully close all tracked streams and stop the muxer
98
- */
99
- close: (options?: AbortOptions) => Promise<void> = async () => { }
100
-
101
- /**
102
- * Abort all tracked streams and stop the muxer
103
- */
104
- abort: (err: Error) => void = () => { }
105
-
106
- /**
107
- * The stream source, a no-op as the transport natively supports multiplexing
108
- */
109
- source: AsyncGenerator<Uint8Array, any, unknown> = nopSource()
110
-
111
- /**
112
- * The stream destination, a no-op as the transport natively supports multiplexing
113
- */
114
- sink: Sink<Source<Uint8Array | Uint8ArrayList>, Promise<void>> = nopSink
115
-
116
96
  constructor (readonly init: DataChannelMuxerInit) {
117
97
  this.streams = init.streams
118
98
  this.peerConnection = init.peerConnection
119
99
  this.protocol = init.protocol ?? PROTOCOL
120
100
  this.metrics = init.metrics
101
+ this.dataChannelOptions = init.dataChannelOptions ?? {}
121
102
 
122
103
  /**
123
104
  * Fired when a data channel has been added to the connection has been
@@ -129,19 +110,19 @@ export class DataChannelMuxer implements StreamMuxer {
129
110
  const stream = createStream({
130
111
  channel,
131
112
  direction: 'inbound',
132
- dataChannelOptions: this.dataChannelOptions,
133
113
  onEnd: () => {
114
+ log.trace('stream %s %s %s onEnd', stream.direction, stream.id, stream.protocol)
115
+ drainAndClose(channel, `inbound ${stream.id} ${stream.protocol}`, this.dataChannelOptions.drainTimeout)
134
116
  this.streams = this.streams.filter(s => s.id !== stream.id)
135
117
  this.metrics?.increment({ stream_end: true })
136
118
  init?.onStreamEnd?.(stream)
137
- }
119
+ },
120
+ ...this.dataChannelOptions
138
121
  })
139
122
 
140
123
  this.streams.push(stream)
141
- if ((init?.onIncomingStream) != null) {
142
- this.metrics?.increment({ incoming_stream: true })
143
- init.onIncomingStream(stream)
144
- }
124
+ this.metrics?.increment({ incoming_stream: true })
125
+ init?.onIncomingStream?.(stream)
145
126
  }
146
127
 
147
128
  const onIncomingStream = init?.onIncomingStream
@@ -150,19 +131,52 @@ export class DataChannelMuxer implements StreamMuxer {
150
131
  }
151
132
  }
152
133
 
134
+ /**
135
+ * Gracefully close all tracked streams and stop the muxer
136
+ */
137
+ async close (options?: AbortOptions): Promise<void> {
138
+ try {
139
+ await Promise.all(
140
+ this.streams.map(async stream => stream.close(options))
141
+ )
142
+ } catch (err: any) {
143
+ this.abort(err)
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Abort all tracked streams and stop the muxer
149
+ */
150
+ abort (err: Error): void {
151
+ for (const stream of this.streams) {
152
+ stream.abort(err)
153
+ }
154
+ }
155
+
156
+ /**
157
+ * The stream source, a no-op as the transport natively supports multiplexing
158
+ */
159
+ source: AsyncGenerator<Uint8Array, any, unknown> = nopSource()
160
+
161
+ /**
162
+ * The stream destination, a no-op as the transport natively supports multiplexing
163
+ */
164
+ sink: Sink<Source<Uint8Array | Uint8ArrayList>, Promise<void>> = nopSink
165
+
153
166
  newStream (): Stream {
154
167
  // The spec says the label SHOULD be an empty string: https://github.com/libp2p/specs/blob/master/webrtc/README.md#rtcdatachannel-label
155
168
  const channel = this.peerConnection.createDataChannel('')
156
169
  const stream = createStream({
157
170
  channel,
158
171
  direction: 'outbound',
159
- dataChannelOptions: this.dataChannelOptions,
160
172
  onEnd: () => {
161
- channel.close() // Stream initiator is responsible for closing the channel
173
+ log.trace('stream %s %s %s onEnd', stream.direction, stream.id, stream.protocol)
174
+ drainAndClose(channel, `outbound ${stream.id} ${stream.protocol}`, this.dataChannelOptions.drainTimeout)
162
175
  this.streams = this.streams.filter(s => s.id !== stream.id)
163
176
  this.metrics?.increment({ stream_end: true })
164
177
  this.init?.onStreamEnd?.(stream)
165
- }
178
+ },
179
+ ...this.dataChannelOptions
166
180
  })
167
181
  this.streams.push(stream)
168
182
  this.metrics?.increment({ outgoing_stream: true })
@@ -2,7 +2,8 @@ syntax = "proto3";
2
2
 
3
3
  message Message {
4
4
  enum Flag {
5
- // The sender will no longer send messages on the stream.
5
+ // The sender will no longer send messages on the stream. The recipient
6
+ // should send a FIN_ACK back to the sender.
6
7
  FIN = 0;
7
8
 
8
9
  // The sender will no longer read messages on the stream. Incoming data is
@@ -12,6 +13,10 @@ message Message {
12
13
  // The sender abruptly terminates the sending part of the stream. The
13
14
  // receiver can discard any data that it already received on that stream.
14
15
  RESET = 2;
16
+
17
+ // The sender previously received a FIN.
18
+ // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=1484907
19
+ FIN_ACK = 3;
15
20
  }
16
21
 
17
22
  optional Flag flag = 1;
package/src/pb/message.ts CHANGED
@@ -17,13 +17,15 @@ export namespace Message {
17
17
  export enum Flag {
18
18
  FIN = 'FIN',
19
19
  STOP_SENDING = 'STOP_SENDING',
20
- RESET = 'RESET'
20
+ RESET = 'RESET',
21
+ FIN_ACK = 'FIN_ACK'
21
22
  }
22
23
 
23
24
  enum __FlagValues {
24
25
  FIN = 0,
25
26
  STOP_SENDING = 1,
26
- RESET = 2
27
+ RESET = 2,
28
+ FIN_ACK = 3
27
29
  }
28
30
 
29
31
  export namespace Flag {
@@ -0,0 +1,191 @@
1
+ import { CodeError } from '@libp2p/interface/errors'
2
+ import { logger } from '@libp2p/logger'
3
+ import { peerIdFromString } from '@libp2p/peer-id'
4
+ import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'
5
+ import { pbStream } from 'it-protobuf-stream'
6
+ import pDefer, { type DeferredPromise } from 'p-defer'
7
+ import { type RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js'
8
+ import { Message } from './pb/message.js'
9
+ import { SIGNALING_PROTO_ID, splitAddr, type WebRTCTransportMetrics } from './transport.js'
10
+ import { parseRemoteAddress, readCandidatesUntilConnected, resolveOnConnected } from './util.js'
11
+ import type { DataChannelOptions } from '../index.js'
12
+ import type { Connection } from '@libp2p/interface/connection'
13
+ import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager'
14
+ import type { IncomingStreamData } from '@libp2p/interface-internal/registrar'
15
+ import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
16
+
17
+ const log = logger('libp2p:webrtc:initiate-connection')
18
+
19
+ export interface IncomingStreamOpts extends IncomingStreamData {
20
+ rtcConfiguration?: RTCConfiguration
21
+ dataChannelOptions?: Partial<DataChannelOptions>
22
+ signal: AbortSignal
23
+ }
24
+
25
+ export interface ConnectOptions {
26
+ peerConnection: RTCPeerConnection
27
+ multiaddr: Multiaddr
28
+ connectionManager: ConnectionManager
29
+ transportManager: TransportManager
30
+ dataChannelOptions?: Partial<DataChannelOptions>
31
+ signal?: AbortSignal
32
+ metrics?: WebRTCTransportMetrics
33
+ }
34
+
35
+ export async function initiateConnection ({ peerConnection, signal, metrics, multiaddr: ma, connectionManager, transportManager }: ConnectOptions): Promise<{ remoteAddress: Multiaddr }> {
36
+ const { baseAddr, peerId } = splitAddr(ma)
37
+
38
+ metrics?.dialerEvents.increment({ open: true })
39
+
40
+ log.trace('dialing base address: %a', baseAddr)
41
+
42
+ const relayPeer = baseAddr.getPeerId()
43
+
44
+ if (relayPeer == null) {
45
+ throw new CodeError('Relay peer was missing', 'ERR_INVALID_ADDRESS')
46
+ }
47
+
48
+ const connections = connectionManager.getConnections(peerIdFromString(relayPeer))
49
+ let connection: Connection
50
+ let shouldCloseConnection = false
51
+
52
+ if (connections.length === 0) {
53
+ // use the transport manager to open a connection. Initiating a WebRTC
54
+ // connection takes place in the context of a dial - if we use the
55
+ // connection manager instead we can end up joining our own dial context
56
+ connection = await transportManager.dial(baseAddr, {
57
+ signal
58
+ })
59
+ // this connection is unmanaged by the connection manager so we should
60
+ // close it when we are done
61
+ shouldCloseConnection = true
62
+ } else {
63
+ connection = connections[0]
64
+ }
65
+
66
+ try {
67
+ const stream = await connection.newStream(SIGNALING_PROTO_ID, {
68
+ signal,
69
+ runOnTransientConnection: true
70
+ })
71
+
72
+ const messageStream = pbStream(stream).pb(Message)
73
+ const connectedPromise: DeferredPromise<void> = pDefer()
74
+ const sdpAbortedListener = (): void => {
75
+ connectedPromise.reject(new CodeError('SDP handshake aborted', 'ERR_SDP_HANDSHAKE_ABORTED'))
76
+ }
77
+
78
+ try {
79
+ resolveOnConnected(peerConnection, connectedPromise)
80
+
81
+ // reject the connectedPromise if the signal aborts
82
+ signal?.addEventListener('abort', sdpAbortedListener)
83
+
84
+ // we create the channel so that the RTCPeerConnection has a component for
85
+ // which to collect candidates. The label is not relevant to connection
86
+ // initiation but can be useful for debugging
87
+ const channel = peerConnection.createDataChannel('init')
88
+
89
+ // setup callback to write ICE candidates to the remote peer
90
+ peerConnection.onicecandidate = ({ candidate }) => {
91
+ // a null candidate means end-of-candidates, an empty string candidate
92
+ // means end-of-candidates for this generation, otherwise this should
93
+ // be a valid candidate object
94
+ // see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent
95
+ const data = JSON.stringify(candidate?.toJSON() ?? null)
96
+
97
+ log.trace('initiator sending ICE candidate %s', data)
98
+
99
+ void messageStream.write({
100
+ type: Message.Type.ICE_CANDIDATE,
101
+ data
102
+ }, {
103
+ signal
104
+ })
105
+ .catch(err => {
106
+ log.error('error sending ICE candidate', err)
107
+ })
108
+ }
109
+ peerConnection.onicecandidateerror = (event) => {
110
+ log('initiator ICE candidate error', event)
111
+ }
112
+
113
+ // create an offer
114
+ const offerSdp = await peerConnection.createOffer()
115
+
116
+ log.trace('initiator send SDP offer %s', offerSdp.sdp)
117
+
118
+ // write the offer to the stream
119
+ await messageStream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp }, {
120
+ signal
121
+ })
122
+
123
+ // set offer as local description
124
+ await peerConnection.setLocalDescription(offerSdp).catch(err => {
125
+ log.error('could not execute setLocalDescription', err)
126
+ throw new CodeError('Failed to set localDescription', 'ERR_SDP_HANDSHAKE_FAILED')
127
+ })
128
+
129
+ // read answer
130
+ const answerMessage = await messageStream.read({
131
+ signal
132
+ })
133
+
134
+ if (answerMessage.type !== Message.Type.SDP_ANSWER) {
135
+ throw new CodeError('remote should send an SDP answer', 'ERR_SDP_HANDSHAKE_FAILED')
136
+ }
137
+
138
+ log.trace('initiator receive SDP answer %s', answerMessage.data)
139
+
140
+ const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data })
141
+ await peerConnection.setRemoteDescription(answerSdp).catch(err => {
142
+ log.error('could not execute setRemoteDescription', err)
143
+ throw new CodeError('Failed to set remoteDescription', 'ERR_SDP_HANDSHAKE_FAILED')
144
+ })
145
+
146
+ log.trace('initiator read candidates until connected')
147
+
148
+ await readCandidatesUntilConnected(connectedPromise, peerConnection, messageStream, {
149
+ direction: 'initiator',
150
+ signal
151
+ })
152
+
153
+ log.trace('initiator connected, closing init channel')
154
+ channel.close()
155
+
156
+ log.trace('initiator closing signalling stream')
157
+ await messageStream.unwrap().unwrap().close({
158
+ signal
159
+ })
160
+
161
+ const remoteAddress = parseRemoteAddress(peerConnection.currentRemoteDescription?.sdp ?? '')
162
+
163
+ log.trace('initiator connected to remote address %s', remoteAddress)
164
+
165
+ return {
166
+ remoteAddress: multiaddr(remoteAddress).encapsulate(`/p2p/${peerId.toString()}`)
167
+ }
168
+ } catch (err: any) {
169
+ peerConnection.close()
170
+ stream.abort(err)
171
+ throw err
172
+ } finally {
173
+ // remove event listeners
174
+ signal?.removeEventListener('abort', sdpAbortedListener)
175
+ peerConnection.onicecandidate = null
176
+ peerConnection.onicecandidateerror = null
177
+ }
178
+ } finally {
179
+ // if we had to open a connection to perform the SDP handshake
180
+ // close it because it's not tracked by the connection manager
181
+ if (shouldCloseConnection) {
182
+ try {
183
+ await connection.close({
184
+ signal
185
+ })
186
+ } catch (err: any) {
187
+ connection.abort(err)
188
+ }
189
+ }
190
+ }
191
+ }
@@ -5,20 +5,27 @@ import type { ListenerEvents, Listener } from '@libp2p/interface/transport'
5
5
  import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
6
6
  import type { Multiaddr } from '@multiformats/multiaddr'
7
7
 
8
- export interface ListenerOptions {
8
+ export interface WebRTCPeerListenerComponents {
9
9
  peerId: PeerId
10
10
  transportManager: TransportManager
11
11
  }
12
12
 
13
+ export interface WebRTCPeerListenerInit {
14
+ shutdownController: AbortController
15
+ }
16
+
13
17
  export class WebRTCPeerListener extends EventEmitter<ListenerEvents> implements Listener {
14
18
  private readonly peerId: PeerId
15
19
  private readonly transportManager: TransportManager
20
+ private readonly shutdownController: AbortController
16
21
 
17
- constructor (opts: ListenerOptions) {
22
+ constructor (components: WebRTCPeerListenerComponents, init: WebRTCPeerListenerInit) {
18
23
  super()
19
24
 
20
- this.peerId = opts.peerId
21
- this.transportManager = opts.transportManager
25
+ this.peerId = components.peerId
26
+ this.transportManager = components.transportManager
27
+
28
+ this.shutdownController = init.shutdownController
22
29
  }
23
30
 
24
31
  async listen (): Promise<void> {
@@ -39,6 +46,7 @@ export class WebRTCPeerListener extends EventEmitter<ListenerEvents> implements
39
46
  }
40
47
 
41
48
  async close (): Promise<void> {
49
+ this.shutdownController.abort()
42
50
  this.safeDispatchEvent('close', {})
43
51
  }
44
52
  }
@@ -0,0 +1,129 @@
1
+ import { CodeError } from '@libp2p/interface/errors'
2
+ import { logger } from '@libp2p/logger'
3
+ import { pbStream } from 'it-protobuf-stream'
4
+ import pDefer, { type DeferredPromise } from 'p-defer'
5
+ import { type RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js'
6
+ import { Message } from './pb/message.js'
7
+ import { parseRemoteAddress, readCandidatesUntilConnected, resolveOnConnected } from './util.js'
8
+ import type { IncomingStreamData } from '@libp2p/interface-internal/registrar'
9
+
10
+ const log = logger('libp2p:webrtc:signaling-stream-handler')
11
+
12
+ export interface IncomingStreamOpts extends IncomingStreamData {
13
+ peerConnection: RTCPeerConnection
14
+ signal: AbortSignal
15
+ }
16
+
17
+ export async function handleIncomingStream ({ peerConnection, stream, signal, connection }: IncomingStreamOpts): Promise<{ remoteAddress: string }> {
18
+ log.trace('new inbound signaling stream')
19
+
20
+ const messageStream = pbStream(stream).pb(Message)
21
+
22
+ try {
23
+ const connectedPromise: DeferredPromise<void> = pDefer()
24
+ const answerSentPromise: DeferredPromise<void> = pDefer()
25
+
26
+ signal.onabort = () => {
27
+ connectedPromise.reject(new CodeError('Timed out while trying to connect', 'ERR_TIMEOUT'))
28
+ }
29
+
30
+ // candidate callbacks
31
+ peerConnection.onicecandidate = ({ candidate }) => {
32
+ answerSentPromise.promise.then(
33
+ async () => {
34
+ // a null candidate means end-of-candidates, an empty string candidate
35
+ // means end-of-candidates for this generation, otherwise this should
36
+ // be a valid candidate object
37
+ // see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent
38
+ const data = JSON.stringify(candidate?.toJSON() ?? null)
39
+
40
+ log.trace('recipient sending ICE candidate %s', data)
41
+
42
+ await messageStream.write({
43
+ type: Message.Type.ICE_CANDIDATE,
44
+ data
45
+ }, {
46
+ signal
47
+ })
48
+ },
49
+ (err) => {
50
+ log.error('cannot set candidate since sending answer failed', err)
51
+ connectedPromise.reject(err)
52
+ }
53
+ )
54
+ }
55
+
56
+ resolveOnConnected(peerConnection, connectedPromise)
57
+
58
+ // read an SDP offer
59
+ const pbOffer = await messageStream.read({
60
+ signal
61
+ })
62
+
63
+ if (pbOffer.type !== Message.Type.SDP_OFFER) {
64
+ throw new CodeError(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `, 'ERR_SDP_HANDSHAKE_FAILED')
65
+ }
66
+
67
+ log.trace('recipient receive SDP offer %s', pbOffer.data)
68
+
69
+ const offer = new RTCSessionDescription({
70
+ type: 'offer',
71
+ sdp: pbOffer.data
72
+ })
73
+
74
+ await peerConnection.setRemoteDescription(offer).catch(err => {
75
+ log.error('could not execute setRemoteDescription', err)
76
+ throw new CodeError('Failed to set remoteDescription', 'ERR_SDP_HANDSHAKE_FAILED')
77
+ })
78
+
79
+ // create and write an SDP answer
80
+ const answer = await peerConnection.createAnswer().catch(err => {
81
+ log.error('could not execute createAnswer', err)
82
+ answerSentPromise.reject(err)
83
+ throw new CodeError('Failed to create answer', 'ERR_SDP_HANDSHAKE_FAILED')
84
+ })
85
+
86
+ log.trace('recipient send SDP answer %s', answer.sdp)
87
+
88
+ // write the answer to the remote
89
+ await messageStream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp }, {
90
+ signal
91
+ })
92
+
93
+ await peerConnection.setLocalDescription(answer).catch(err => {
94
+ log.error('could not execute setLocalDescription', err)
95
+ answerSentPromise.reject(err)
96
+ throw new CodeError('Failed to set localDescription', 'ERR_SDP_HANDSHAKE_FAILED')
97
+ })
98
+
99
+ answerSentPromise.resolve()
100
+
101
+ log.trace('recipient read candidates until connected')
102
+
103
+ // wait until candidates are connected
104
+ await readCandidatesUntilConnected(connectedPromise, peerConnection, messageStream, {
105
+ direction: 'recipient',
106
+ signal
107
+ })
108
+
109
+ log.trace('recipient connected, closing signaling stream')
110
+ await messageStream.unwrap().unwrap().close({
111
+ signal
112
+ })
113
+ } catch (err: any) {
114
+ if (peerConnection.connectionState !== 'connected') {
115
+ log.error('error while handling signaling stream from peer %a', connection.remoteAddr, err)
116
+
117
+ peerConnection.close()
118
+ throw err
119
+ } else {
120
+ log('error while handling signaling stream from peer %a, ignoring as the RTCPeerConnection is already connected', connection.remoteAddr, err)
121
+ }
122
+ }
123
+
124
+ const remoteAddress = parseRemoteAddress(peerConnection.currentRemoteDescription?.sdp ?? '')
125
+
126
+ log.trace('recipient connected to remote address %s', remoteAddress)
127
+
128
+ return { remoteAddress }
129
+ }