@libp2p/webtransport 3.0.10 → 3.0.11-6cb80f7d

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/index.ts CHANGED
@@ -7,13 +7,14 @@ import { inertDuplex } from './utils/inert-duplex.js'
7
7
  import { isSubset } from './utils/is-subset.js'
8
8
  import { parseMultiaddr } from './utils/parse-multiaddr.js'
9
9
  import type { Connection, MultiaddrConnection, Stream } from '@libp2p/interface/connection'
10
+ import type { CounterGroup, Metrics } from '@libp2p/interface/metrics'
10
11
  import type { PeerId } from '@libp2p/interface/peer-id'
11
12
  import type { StreamMuxerFactory, StreamMuxerInit, StreamMuxer } from '@libp2p/interface/stream-muxer'
12
13
  import type { Source } from 'it-stream-types'
13
14
  import type { MultihashDigest } from 'multiformats/hashes/interface'
14
15
 
15
16
  interface WebTransportSessionCleanup {
16
- (closeInfo?: WebTransportCloseInfo): void
17
+ (metric: string): void
17
18
  }
18
19
 
19
20
  const log = logger('libp2p:webtransport')
@@ -24,17 +25,32 @@ export interface WebTransportInit {
24
25
 
25
26
  export interface WebTransportComponents {
26
27
  peerId: PeerId
28
+ metrics?: Metrics
29
+ }
30
+
31
+ export interface WebTransportMetrics {
32
+ dialerEvents: CounterGroup
27
33
  }
28
34
 
29
35
  class WebTransportTransport implements Transport {
30
36
  private readonly components: WebTransportComponents
31
37
  private readonly config: Required<WebTransportInit>
38
+ private readonly metrics?: WebTransportMetrics
32
39
 
33
40
  constructor (components: WebTransportComponents, init: WebTransportInit = {}) {
34
41
  this.components = components
35
42
  this.config = {
36
43
  maxInboundStreams: init.maxInboundStreams ?? 1000
37
44
  }
45
+
46
+ if (components.metrics != null) {
47
+ this.metrics = {
48
+ dialerEvents: components.metrics.registerCounterGroup('libp2p_webtransport_dialer_events_total', {
49
+ label: 'event',
50
+ help: 'Total count of WebTransport dialer events by type'
51
+ })
52
+ }
53
+ }
38
54
  }
39
55
 
40
56
  readonly [Symbol.toStringTag] = '@libp2p/webtransport'
@@ -65,10 +81,15 @@ class WebTransportTransport implements Transport {
65
81
  let abortListener: (() => void) | undefined
66
82
  let maConn: MultiaddrConnection | undefined
67
83
 
68
- let cleanUpWTSession: (closeInfo?: WebTransportCloseInfo) => void = () => {}
84
+ let cleanUpWTSession: WebTransportSessionCleanup = () => {}
85
+
86
+ let closed = false
87
+ let ready = false
88
+ let authenticated = false
69
89
 
70
90
  try {
71
- let closed = false
91
+ this.metrics?.dialerEvents.increment({ pending: true })
92
+
72
93
  const wt = new WebTransport(`${url}/.well-known/libp2p-webtransport?type=noise`, {
73
94
  serverCertificateHashes: certhashes.map(certhash => ({
74
95
  algorithm: 'sha-256',
@@ -76,74 +97,69 @@ class WebTransportTransport implements Transport {
76
97
  }))
77
98
  })
78
99
 
79
- cleanUpWTSession = (closeInfo?: WebTransportCloseInfo) => {
100
+ cleanUpWTSession = (metric: string) => {
101
+ if (closed) {
102
+ // already closed session
103
+ return
104
+ }
105
+
80
106
  try {
107
+ this.metrics?.dialerEvents.increment({ [metric]: true })
108
+ wt.close()
109
+ } catch (err) {
110
+ log.error('error closing wt session', err)
111
+ } finally {
112
+ // This is how we specify the connection is closed and shouldn't be used.
81
113
  if (maConn != null) {
82
- if (maConn.timeline.close != null) {
83
- // already closed session
84
- return
85
- }
86
-
87
- // This is how we specify the connection is closed and shouldn't be used.
88
114
  maConn.timeline.close = Date.now()
89
115
  }
90
116
 
91
- if (closed) {
92
- // already closed session
93
- return
94
- }
95
-
96
- wt.close(closeInfo)
97
- } catch (err) {
98
- log.error('error closing wt session', err)
99
- } finally {
100
117
  closed = true
101
118
  }
102
119
  }
103
120
 
104
- // this promise resolves/throws when the session is closed or failed to init
105
- wt.closed
106
- .then(async () => {
107
- await maConn?.close()
108
- })
109
- .catch((err: Error) => {
110
- log.error('error on remote wt session close', err)
111
- maConn?.abort(err)
112
- })
113
- .finally(() => {
114
- // if we never got as far as creating the maConn, just clean up the session
115
- if (maConn == null) {
116
- cleanUpWTSession()
117
- }
118
- })
119
-
120
121
  // if the dial is aborted before we are ready, close the WebTransport session
121
122
  abortListener = () => {
122
- if (abortListener != null) {
123
- options.signal?.removeEventListener('abort', abortListener)
123
+ if (ready) {
124
+ cleanUpWTSession('noise_timeout')
125
+ } else {
126
+ cleanUpWTSession('ready_timeout')
124
127
  }
125
-
126
- cleanUpWTSession()
127
128
  }
128
- options.signal?.addEventListener('abort', abortListener)
129
+ options.signal?.addEventListener('abort', abortListener, {
130
+ once: true
131
+ })
129
132
 
130
- await wt.ready
133
+ await Promise.race([
134
+ wt.closed,
135
+ wt.ready
136
+ ])
137
+
138
+ ready = true
139
+ this.metrics?.dialerEvents.increment({ ready: true })
140
+
141
+ // this promise resolves/throws when the session is closed
142
+ wt.closed.catch((err: Error) => {
143
+ log.error('error on remote wt session close', err)
144
+ })
145
+ .finally(() => {
146
+ cleanUpWTSession('remote_close')
147
+ })
131
148
 
132
149
  if (!await this.authenticateWebTransport(wt, localPeer, remotePeer, certhashes)) {
133
150
  throw new Error('Failed to authenticate webtransport')
134
151
  }
135
152
 
153
+ this.metrics?.dialerEvents.increment({ open: true })
154
+
136
155
  maConn = {
137
- close: async (options?: AbortOptions) => {
156
+ close: async () => {
138
157
  log('Closing webtransport')
139
- cleanUpWTSession()
158
+ cleanUpWTSession('close')
140
159
  },
141
160
  abort: (err: Error) => {
142
161
  log('aborting webtransport due to passed err', err)
143
- cleanUpWTSession({
144
- closeCode: 0,
145
- reason: err.message
146
- })
162
+ cleanUpWTSession('abort')
147
163
  },
148
164
  remoteAddr: ma,
149
165
  timeline: {
@@ -153,16 +169,19 @@ class WebTransportTransport implements Transport {
153
169
  ...inertDuplex()
154
170
  }
155
171
 
156
- options?.signal?.throwIfAborted()
172
+ authenticated = true
157
173
 
158
- return await options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt, cleanUpWTSession), skipProtection: true })
174
+ return await options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt), skipProtection: true })
159
175
  } catch (err: any) {
160
176
  log.error('caught wt session err', err)
161
177
 
162
- cleanUpWTSession({
163
- closeCode: 0,
164
- reason: err.message
165
- })
178
+ if (authenticated) {
179
+ cleanUpWTSession('upgrade_error')
180
+ } else if (ready) {
181
+ cleanUpWTSession('noise_error')
182
+ } else {
183
+ cleanUpWTSession('ready_error')
184
+ }
166
185
 
167
186
  throw err
168
187
  } finally {
@@ -220,7 +239,7 @@ class WebTransportTransport implements Transport {
220
239
  return true
221
240
  }
222
241
 
223
- webtransportMuxer (wt: WebTransport, cleanUpWTSession: WebTransportSessionCleanup): StreamMuxerFactory {
242
+ webtransportMuxer (wt: WebTransport): StreamMuxerFactory {
224
243
  let streamIDCounter = 0
225
244
  const config = this.config
226
245
  return {
@@ -281,14 +300,17 @@ class WebTransportTransport implements Transport {
281
300
  */
282
301
  close: async (options?: AbortOptions) => {
283
302
  log('Closing webtransport muxer')
284
- cleanUpWTSession()
303
+
304
+ await Promise.all(
305
+ activeStreams.map(async s => s.close(options))
306
+ )
285
307
  },
286
308
  abort: (err: Error) => {
287
309
  log('Aborting webtransport muxer with err:', err)
288
- cleanUpWTSession({
289
- closeCode: 0,
290
- reason: err.message
291
- })
310
+
311
+ for (const stream of activeStreams) {
312
+ stream.abort(err)
313
+ }
292
314
  },
293
315
  // This stream muxer is webtransport native. Therefore it doesn't plug in with any other duplex.
294
316
  ...inertDuplex()
@@ -1,5 +1,7 @@
1
+ import { CodeError } from '@libp2p/interface/errors'
1
2
  import { peerIdFromString } from '@libp2p/peer-id'
2
3
  import { type Multiaddr, protocols } from '@multiformats/multiaddr'
4
+ import { WebTransport } from '@multiformats/multiaddr-matcher'
3
5
  import { bases, digest } from 'multiformats/basics'
4
6
  import type { PeerId } from '@libp2p/interface/peer-id'
5
7
  import type { MultihashDigest } from 'multiformats/hashes/interface'
@@ -11,73 +13,46 @@ function decodeCerthashStr (s: string): MultihashDigest {
11
13
  return digest.decode(multibaseDecoder.decode(s))
12
14
  }
13
15
 
14
- export function parseMultiaddr (ma: Multiaddr): { url: string, certhashes: MultihashDigest[], remotePeer?: PeerId } {
16
+ export interface ParsedMultiaddr {
17
+ url: string
18
+ certhashes: MultihashDigest[]
19
+ remotePeer?: PeerId
20
+ }
21
+
22
+ export function parseMultiaddr (ma: Multiaddr): ParsedMultiaddr {
23
+ if (!WebTransport.matches(ma)) {
24
+ throw new CodeError('Invalid multiaddr, was not a WebTransport address', 'ERR_INVALID_MULTIADDR')
25
+ }
26
+
15
27
  const parts = ma.stringTuples()
28
+ const certhashes = parts
29
+ .filter(([name, _]) => name === protocols('certhash').code)
30
+ .map(([_, value]) => decodeCerthashStr(value ?? ''))
31
+
32
+ // only take the first peer id in the multiaddr as it may be a relay
33
+ const remotePeer = parts
34
+ .filter(([name, _]) => name === protocols('p2p').code)
35
+ .map(([_, value]) => peerIdFromString(value ?? ''))[0]
36
+
37
+ const opts = ma.toOptions()
38
+ let host = opts.host
16
39
 
17
- // This is simpler to have inline than extract into a separate function
18
- // eslint-disable-next-line complexity
19
- const { url, certhashes, remotePeer } = parts.reduce((state: { url: string, certhashes: MultihashDigest[], seenHost: boolean, seenPort: boolean, remotePeer?: PeerId }, [proto, value]) => {
20
- switch (proto) {
21
- case protocols('ip6').code:
22
- // @ts-expect-error - ts error on switch fallthrough
23
- case protocols('dns6').code:
24
- if (value?.includes(':') === true) {
25
- /**
26
- * This resolves cases where `new globalThis.WebTransport` fails to construct because of an invalid URL being passed.
27
- *
28
- * `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL`
29
- * `new URL('https://[::1]:4001/blah')` is valid and will not.
30
- *
31
- * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
32
- */
33
- value = `[${value}]`
34
- }
35
- // eslint-disable-next-line no-fallthrough
36
- case protocols('ip4').code:
37
- case protocols('dns4').code:
38
- if (state.seenHost || state.seenPort) {
39
- throw new Error('Invalid multiaddr, saw host and already saw the host or port')
40
- }
41
- return {
42
- ...state,
43
- url: `${state.url}${value ?? ''}`,
44
- seenHost: true
45
- }
46
- case protocols('quic').code:
47
- case protocols('quic-v1').code:
48
- case protocols('webtransport').code:
49
- if (!state.seenHost || !state.seenPort) {
50
- throw new Error("Invalid multiaddr, Didn't see host and port, but saw quic/webtransport")
51
- }
52
- return state
53
- case protocols('udp').code:
54
- if (state.seenPort) {
55
- throw new Error('Invalid multiaddr, saw port but already saw the port')
56
- }
57
- return {
58
- ...state,
59
- url: `${state.url}:${value ?? ''}`,
60
- seenPort: true
61
- }
62
- case protocols('certhash').code:
63
- if (!state.seenHost || !state.seenPort) {
64
- throw new Error('Invalid multiaddr, saw the certhash before seeing the host and port')
65
- }
66
- return {
67
- ...state,
68
- certhashes: state.certhashes.concat([decodeCerthashStr(value ?? '')])
69
- }
70
- case protocols('p2p').code:
71
- return {
72
- ...state,
73
- remotePeer: peerIdFromString(value ?? '')
74
- }
75
- default:
76
- throw new Error(`unexpected component in multiaddr: ${proto} ${protocols(proto).name} ${value ?? ''} `)
77
- }
78
- },
79
- // All webtransport urls are https
80
- { url: 'https://', seenHost: false, seenPort: false, certhashes: [] })
40
+ if (opts.family === 6 && host?.includes(':')) {
41
+ /**
42
+ * This resolves cases where `new WebTransport()` fails to construct because of an invalid URL being passed.
43
+ *
44
+ * `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL`
45
+ * `new URL('https://[::1]:4001/blah')` is valid and will not.
46
+ *
47
+ * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
48
+ */
49
+ host = `[${host}]`
50
+ }
81
51
 
82
- return { url, certhashes, remotePeer }
52
+ return {
53
+ // All webtransport urls are https
54
+ url: `https://${host}:${opts.port}`,
55
+ certhashes,
56
+ remotePeer
57
+ }
83
58
  }
@@ -1,8 +0,0 @@
1
- {
2
- "WebTransportComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webtransport.WebTransportComponents.html",
3
- ".:WebTransportComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webtransport.WebTransportComponents.html",
4
- "WebTransportInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webtransport.WebTransportInit.html",
5
- ".:WebTransportInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webtransport.WebTransportInit.html",
6
- "webTransport": "https://libp2p.github.io/js-libp2p/functions/_libp2p_webtransport.webTransport.html",
7
- ".:webTransport": "https://libp2p.github.io/js-libp2p/functions/_libp2p_webtransport.webTransport.html"
8
- }