@libp2p/webrtc 3.2.11-9c67c5b3d → 3.2.11-adea7bbbf

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.
package/src/muxer.ts CHANGED
@@ -51,6 +51,7 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory {
51
51
  private readonly metrics?: CounterGroup
52
52
  private readonly dataChannelOptions?: DataChannelOptions
53
53
  private readonly components: DataChannelMuxerFactoryComponents
54
+ private readonly log: Logger
54
55
 
55
56
  constructor (components: DataChannelMuxerFactoryComponents, init: DataChannelMuxerFactoryInit) {
56
57
  this.components = components
@@ -58,9 +59,20 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory {
58
59
  this.metrics = init.metrics
59
60
  this.protocol = init.protocol ?? PROTOCOL
60
61
  this.dataChannelOptions = init.dataChannelOptions ?? {}
62
+ this.log = components.logger.forComponent('libp2p:webrtc:datachannelmuxerfactory')
61
63
 
62
64
  // store any datachannels opened before upgrade has been completed
63
65
  this.peerConnection.ondatachannel = ({ channel }) => {
66
+ this.log.trace('incoming early datachannel with channel id %d and label "%s"', channel.id)
67
+
68
+ // 'init' channel is only used during connection establishment
69
+ if (channel.label === 'init') {
70
+ this.log.trace('closing early init channel')
71
+ channel.close()
72
+
73
+ return
74
+ }
75
+
64
76
  // @ts-expect-error fields are set below
65
77
  const bufferedStream: BufferedStream = {}
66
78
 
@@ -136,10 +148,21 @@ export class DataChannelMuxer implements StreamMuxer {
136
148
  * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/datachannel_event}
137
149
  */
138
150
  this.peerConnection.ondatachannel = ({ channel }) => {
151
+ this.log.trace('incoming datachannel with channel id %d', channel.id)
152
+
153
+ // 'init' channel is only used during connection establishment
154
+ if (channel.label === 'init') {
155
+ this.log.trace('closing init channel')
156
+ channel.close()
157
+
158
+ return
159
+ }
160
+
139
161
  const stream = createStream({
140
162
  channel,
141
163
  direction: 'inbound',
142
164
  onEnd: () => {
165
+ this.log('incoming channel %s ended with state %s', channel.id, channel.readyState)
143
166
  this.#onStreamEnd(stream, channel)
144
167
  },
145
168
  logger: this.logger,
@@ -161,6 +184,7 @@ export class DataChannelMuxer implements StreamMuxer {
161
184
  queueMicrotask(() => {
162
185
  this.init.streams.forEach(bufferedStream => {
163
186
  bufferedStream.onEnd = () => {
187
+ this.log('incoming early channel %s ended with state %s', bufferedStream.channel.id, bufferedStream.channel.readyState)
164
188
  this.#onStreamEnd(bufferedStream.stream, bufferedStream.channel)
165
189
  }
166
190
 
@@ -220,10 +244,14 @@ export class DataChannelMuxer implements StreamMuxer {
220
244
  newStream (): Stream {
221
245
  // The spec says the label SHOULD be an empty string: https://github.com/libp2p/specs/blob/master/webrtc/README.md#rtcdatachannel-label
222
246
  const channel = this.peerConnection.createDataChannel('')
247
+
248
+ this.log.trace('opened outgoing datachannel with channel id %s', channel.id)
249
+
223
250
  const stream = createStream({
224
251
  channel,
225
252
  direction: 'outbound',
226
253
  onEnd: () => {
254
+ this.log('outgoing channel %s ended with state %s', channel.id, channel.readyState)
227
255
  this.#onStreamEnd(stream, channel)
228
256
  },
229
257
  logger: this.logger,
@@ -1,8 +1,6 @@
1
1
  import { CodeError } from '@libp2p/interface/errors'
2
- import { abortableSource } from 'abortable-iterator'
2
+ import { closeSource } from '@libp2p/utils/close-source'
3
3
  import { anySignal } from 'any-signal'
4
- import * as lp from 'it-length-prefixed'
5
- import { AbortError, raceSignal } from 'race-signal'
6
4
  import { isFirefox } from '../util.js'
7
5
  import { RTCIceCandidate } from '../webrtc/index.js'
8
6
  import { Message } from './pb/message.js'
@@ -29,41 +27,40 @@ export const readCandidatesUntilConnected = async (connectedPromise: DeferredPro
29
27
  options.signal
30
28
  ])
31
29
 
32
- const source = abortableSource(stream.unwrap().unwrap().source, signal, {
33
- returnOnAbort: true
34
- })
30
+ const abortListener = (): void => {
31
+ closeSource(stream.unwrap().unwrap().source, options.log)
32
+ }
33
+
34
+ signal.addEventListener('abort', abortListener)
35
35
 
36
36
  try {
37
37
  // read candidates until we are connected or we reach the end of the stream
38
- for await (const buf of lp.decode(source)) {
39
- const message = Message.decode(buf)
38
+ while (true) {
39
+ const message = await Promise.race([
40
+ connectedPromise.promise,
41
+ stream.read()
42
+ ])
43
+
44
+ // stream ended or we became connected
45
+ if (message == null) {
46
+ break
47
+ }
40
48
 
41
49
  if (message.type !== Message.Type.ICE_CANDIDATE) {
42
50
  throw new CodeError('ICE candidate message expected', 'ERR_NOT_ICE_CANDIDATE')
43
51
  }
44
52
 
45
- let candidateInit = JSON.parse(message.data ?? 'null')
46
-
47
- if (candidateInit === '') {
48
- options.log.trace('end-of-candidates for this generation received')
49
- candidateInit = {
50
- candidate: '',
51
- sdpMid: '0',
52
- sdpMLineIndex: 0
53
- }
54
- }
53
+ const candidateInit = JSON.parse(message.data ?? 'null')
55
54
 
56
- if (candidateInit === null) {
55
+ // an empty string means this generation of candidates is complete, a null
56
+ // candidate means candidate gathering has finished
57
+ // see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent
58
+ if (candidateInit === '' || candidateInit === null) {
57
59
  options.log.trace('end-of-candidates received')
58
- candidateInit = {
59
- candidate: null,
60
- sdpMid: '0',
61
- sdpMLineIndex: 0
62
- }
60
+
61
+ continue
63
62
  }
64
63
 
65
- // a null candidate means end-of-candidates
66
- // see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent
67
64
  const candidate = new RTCIceCandidate(candidateInit)
68
65
 
69
66
  options.log.trace('%s received new ICE candidate', options.direction, candidate)
@@ -71,24 +68,15 @@ export const readCandidatesUntilConnected = async (connectedPromise: DeferredPro
71
68
  try {
72
69
  await pc.addIceCandidate(candidate)
73
70
  } catch (err) {
74
- options.log.error('%s bad candidate received', options.direction, err)
71
+ options.log.error('%s bad candidate received', options.direction, candidateInit, err)
75
72
  }
76
73
  }
77
74
  } catch (err) {
78
75
  options.log.error('%s error parsing ICE candidate', options.direction, err)
79
76
  } finally {
77
+ signal.removeEventListener('abort', abortListener)
80
78
  signal.clear()
81
79
  }
82
-
83
- if (options.signal?.aborted === true) {
84
- throw new AbortError('Aborted while reading ICE candidates', 'ERR_ICE_CANDIDATES_READ_ABORTED')
85
- }
86
-
87
- // read all available ICE candidates, wait for connection state change
88
- await raceSignal(connectedPromise.promise, options.signal, {
89
- errorMessage: 'Aborted before connected',
90
- errorCode: 'ERR_ABORTED_BEFORE_CONNECTED'
91
- })
92
80
  }
93
81
 
94
82
  export function resolveOnConnected (pc: RTCPeerConnection, promise: DeferredPromise<void>): void {
package/src/stream.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CodeError } from '@libp2p/interface/errors'
2
- import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface/stream-muxer/stream'
2
+ import { AbstractStream, type AbstractStreamInit } from '@libp2p/utils/abstract-stream'
3
3
  import * as lengthPrefixed from 'it-length-prefixed'
4
4
  import { type Pushable, pushable } from 'it-pushable'
5
5
  import pDefer from 'p-defer'
@@ -57,6 +57,12 @@ export const MAX_MESSAGE_SIZE = 16 * 1024
57
57
  */
58
58
  export const FIN_ACK_TIMEOUT = 5000
59
59
 
60
+ /**
61
+ * When sending data messages, if the channel is not in the "open" state, wait
62
+ * this long for the "open" event to fire.
63
+ */
64
+ export const OPEN_TIMEOUT = 5000
65
+
60
66
  export class WebRTCStream extends AbstractStream {
61
67
  /**
62
68
  * The data channel used to send and receive data
@@ -69,8 +75,6 @@ export class WebRTCStream extends AbstractStream {
69
75
  */
70
76
  private readonly incomingData: Pushable<Uint8Array>
71
77
 
72
- private messageQueue?: Uint8ArrayList
73
-
74
78
  private readonly maxBufferedAmount: number
75
79
 
76
80
  private readonly bufferedAmountLowEventTimeout: number
@@ -85,6 +89,7 @@ export class WebRTCStream extends AbstractStream {
85
89
  */
86
90
  private readonly receiveFinAck: DeferredPromise<void>
87
91
  private readonly finAckTimeout: number
92
+ private readonly openTimeout: number
88
93
 
89
94
  constructor (init: WebRTCStreamInit) {
90
95
  // override onEnd to send/receive FIN_ACK before closing the stream
@@ -122,17 +127,18 @@ export class WebRTCStream extends AbstractStream {
122
127
 
123
128
  this.channel = init.channel
124
129
  this.channel.binaryType = 'arraybuffer'
125
- this.incomingData = pushable()
126
- this.messageQueue = new Uint8ArrayList()
130
+ this.incomingData = pushable<Uint8Array>()
127
131
  this.bufferedAmountLowEventTimeout = init.bufferedAmountLowEventTimeout ?? BUFFERED_AMOUNT_LOW_TIMEOUT
128
132
  this.maxBufferedAmount = init.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT
129
133
  this.maxMessageSize = (init.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD - VARINT_LENGTH
130
134
  this.receiveFinAck = pDefer()
131
135
  this.finAckTimeout = init.closeTimeout ?? FIN_ACK_TIMEOUT
136
+ this.openTimeout = init.openTimeout ?? OPEN_TIMEOUT
132
137
 
133
138
  // set up initial state
134
139
  switch (this.channel.readyState) {
135
140
  case 'open':
141
+ this.timeline.open = new Date().getTime()
136
142
  break
137
143
 
138
144
  case 'closed':
@@ -153,19 +159,6 @@ export class WebRTCStream extends AbstractStream {
153
159
  // handle RTCDataChannel events
154
160
  this.channel.onopen = (_evt) => {
155
161
  this.timeline.open = new Date().getTime()
156
-
157
- if (this.messageQueue != null && this.messageQueue.byteLength > 0) {
158
- this.log.trace('dataChannel opened, sending queued messages', this.messageQueue.byteLength, this.channel.readyState)
159
-
160
- // send any queued messages
161
- this._sendMessage(this.messageQueue)
162
- .catch(err => {
163
- this.log.error('error sending queued messages', err)
164
- this.abort(err)
165
- })
166
- }
167
-
168
- this.messageQueue = undefined
169
162
  }
170
163
 
171
164
  this.channel.onclose = (_evt) => {
@@ -218,6 +211,7 @@ export class WebRTCStream extends AbstractStream {
218
211
  async _sendMessage (data: Uint8ArrayList, checkBuffer: boolean = true): Promise<void> {
219
212
  if (checkBuffer && this.channel.bufferedAmount > this.maxBufferedAmount) {
220
213
  try {
214
+ this.log('channel buffer is %d, wait for "bufferedamountlow" event', this.channel.bufferedAmount)
221
215
  await pEvent(this.channel, 'bufferedamountlow', { timeout: this.bufferedAmountLowEventTimeout })
222
216
  } catch (err: any) {
223
217
  if (err instanceof TimeoutError) {
@@ -232,22 +226,14 @@ export class WebRTCStream extends AbstractStream {
232
226
  throw new CodeError(`Invalid datachannel state - ${this.channel.readyState}`, 'ERR_INVALID_STATE')
233
227
  }
234
228
 
235
- if (this.channel.readyState === 'open') {
236
- // send message without copying data
237
- for (const buf of data) {
238
- this.channel.send(buf)
239
- }
240
- } else if (this.channel.readyState === 'connecting') {
241
- // queue message for when we are open
242
- if (this.messageQueue == null) {
243
- this.messageQueue = new Uint8ArrayList()
244
- }
245
-
246
- this.messageQueue.append(data)
247
- } else {
248
- this.log.error('unknown datachannel state %s', this.channel.readyState)
249
- throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE')
229
+ if (this.channel.readyState !== 'open') {
230
+ this.log('channel state is "%s" and not "open", waiting for "open" event before sending data', this.channel.readyState)
231
+ await pEvent(this.channel, 'open', { timeout: this.openTimeout })
232
+ this.log('channel state is now "%s", sending data', this.channel.readyState)
250
233
  }
234
+
235
+ // send message without copying data
236
+ this.channel.send(data.subarray())
251
237
  }
252
238
 
253
239
  async sendData (data: Uint8ArrayList): Promise<void> {
@@ -342,7 +328,7 @@ export class WebRTCStream extends AbstractStream {
342
328
  // flags can be sent while we or the remote are closing the datachannel so
343
329
  // if the channel isn't open, don't try to send it but return false to let
344
330
  // the caller know and act if they need to
345
- this.log.trace('not sending flag %s because channel is not open', flag.toString())
331
+ this.log.trace('not sending flag %s because channel is "%s" and not "open"', this.channel.readyState, flag.toString())
346
332
  return false
347
333
  }
348
334