@libp2p/tcp 10.0.6-24fa1d5af → 10.0.6-d9c7e0f7e

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.
@@ -1,14 +1,11 @@
1
- import { InvalidParametersError, TimeoutError } from '@libp2p/interface'
1
+ import { AbortError, InvalidParametersError, TimeoutError } from '@libp2p/interface'
2
2
  import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr'
3
- import pDefer from 'p-defer'
4
- import { raceEvent } from 'race-event'
5
3
  import { duplex } from 'stream-to-it'
6
4
  import { CLOSE_TIMEOUT, SOCKET_TIMEOUT } from './constants.js'
7
5
  import { multiaddrToNetConfig } from './utils.js'
8
6
  import type { ComponentLogger, MultiaddrConnection, CounterGroup } from '@libp2p/interface'
9
7
  import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr'
10
8
  import type { Socket } from 'net'
11
- import type { DeferredPromise } from 'p-defer'
12
9
 
13
10
  interface ToConnectionOptions {
14
11
  listeningAddr?: Multiaddr
@@ -19,7 +16,6 @@ interface ToConnectionOptions {
19
16
  metrics?: CounterGroup
20
17
  metricPrefix?: string
21
18
  logger: ComponentLogger
22
- direction: 'inbound' | 'outbound'
23
19
  }
24
20
 
25
21
  /**
@@ -27,15 +23,12 @@ interface ToConnectionOptions {
27
23
  * https://github.com/libp2p/interface-transport#multiaddrconnection
28
24
  */
29
25
  export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptions): MultiaddrConnection => {
30
- let closePromise: DeferredPromise<void>
26
+ let closePromise: Promise<void> | null = null
31
27
  const log = options.logger.forComponent('libp2p:tcp:socket')
32
- const direction = options.direction
33
28
  const metrics = options.metrics
34
29
  const metricPrefix = options.metricPrefix ?? ''
35
30
  const inactivityTimeout = options.socketInactivityTimeout ?? SOCKET_TIMEOUT
36
31
  const closeTimeout = options.socketCloseTimeout ?? CLOSE_TIMEOUT
37
- let timedout = false
38
- let errored = false
39
32
 
40
33
  // Check if we are connected on a unix path
41
34
  if (options.listeningAddr?.getPath() != null) {
@@ -46,19 +39,6 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
46
39
  options.localAddr = options.remoteAddr
47
40
  }
48
41
 
49
- // handle socket errors
50
- socket.on('error', err => {
51
- errored = true
52
-
53
- if (!timedout) {
54
- log.error('%s socket error - %e', direction, err)
55
- metrics?.increment({ [`${metricPrefix}error`]: true })
56
- }
57
-
58
- socket.destroy()
59
- maConn.timeline.close = Date.now()
60
- })
61
-
62
42
  let remoteAddr: Multiaddr
63
43
 
64
44
  if (options.remoteAddr != null) {
@@ -79,37 +59,37 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
79
59
 
80
60
  // by default there is no timeout
81
61
  // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketsettimeouttimeout-callback
82
- socket.setTimeout(inactivityTimeout)
83
-
84
- socket.once('timeout', () => {
85
- timedout = true
86
- log('%s %s socket read timeout', direction, lOptsStr)
62
+ socket.setTimeout(inactivityTimeout, () => {
63
+ log('%s socket read timeout', lOptsStr)
87
64
  metrics?.increment({ [`${metricPrefix}timeout`]: true })
88
65
 
66
+ // only destroy with an error if the remote has not sent the FIN message
67
+ let err: Error | undefined
68
+ if (socket.readable) {
69
+ err = new TimeoutError('Socket read timeout')
70
+ }
71
+
89
72
  // if the socket times out due to inactivity we must manually close the connection
90
73
  // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-timeout
91
- socket.destroy(new TimeoutError())
92
- maConn.timeline.close = Date.now()
74
+ socket.destroy(err)
93
75
  })
94
76
 
95
77
  socket.once('close', () => {
96
- // record metric for clean exit
97
- if (!timedout && !errored) {
98
- log('%s %s socket close', direction, lOptsStr)
99
- metrics?.increment({ [`${metricPrefix}close`]: true })
100
- }
78
+ log('%s socket close', lOptsStr)
79
+ metrics?.increment({ [`${metricPrefix}close`]: true })
101
80
 
102
81
  // In instances where `close` was not explicitly called,
103
82
  // such as an iterable stream ending, ensure we have set the close
104
83
  // timeline
105
- socket.destroy()
106
- maConn.timeline.close = Date.now()
84
+ if (maConn.timeline.close == null) {
85
+ maConn.timeline.close = Date.now()
86
+ }
107
87
  })
108
88
 
109
89
  socket.once('end', () => {
110
90
  // the remote sent a FIN packet which means no more data will be sent
111
91
  // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-end
112
- log('%s %s socket end', direction, lOptsStr)
92
+ log('%s socket end', lOptsStr)
113
93
  metrics?.increment({ [`${metricPrefix}end`]: true })
114
94
  })
115
95
 
@@ -131,7 +111,7 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
131
111
  // If the source errored the socket will already have been destroyed by
132
112
  // duplex(). If the socket errored it will already be
133
113
  // destroyed. There's nothing to do here except log the error & return.
134
- log.error('%s %s error in sink - %e', direction, lOptsStr, err)
114
+ log.error('%s error in sink', lOptsStr, err)
135
115
  }
136
116
  }
137
117
 
@@ -148,66 +128,96 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
148
128
 
149
129
  async close (options: AbortOptions = {}) {
150
130
  if (socket.closed) {
151
- log('the %s %s socket is already closed', direction, lOptsStr)
131
+ log('The %s socket is already closed', lOptsStr)
152
132
  return
153
133
  }
154
134
 
155
135
  if (socket.destroyed) {
156
- log('the %s %s socket is already destroyed', direction, lOptsStr)
136
+ log('The %s socket is already destroyed', lOptsStr)
157
137
  return
158
138
  }
159
139
 
160
- if (closePromise != null) {
161
- return closePromise.promise
140
+ const abortSignalListener = (): void => {
141
+ socket.destroy(new AbortError('Destroying socket after timeout'))
162
142
  }
163
143
 
164
144
  try {
165
- closePromise = pDefer()
145
+ if (closePromise != null) {
146
+ log('The %s socket is already closing', lOptsStr)
147
+ await closePromise
148
+ return
149
+ }
166
150
 
167
- // close writable end of socket
168
- socket.end()
151
+ if (options.signal == null) {
152
+ const signal = AbortSignal.timeout(closeTimeout)
169
153
 
170
- // convert EventEmitter to EventTarget
171
- const eventTarget = socketToEventTarget(socket)
154
+ options = {
155
+ ...options,
156
+ signal
157
+ }
158
+ }
172
159
 
173
- // don't wait forever to close
174
- const signal = options.signal ?? AbortSignal.timeout(closeTimeout)
160
+ options.signal?.addEventListener('abort', abortSignalListener)
175
161
 
176
- // wait for any unsent data to be sent
177
- if (socket.writableLength > 0) {
178
- log('%s %s draining socket', direction, lOptsStr)
179
- await raceEvent(eventTarget, 'drain', signal, {
180
- errorEvent: 'error'
162
+ log('%s closing socket', lOptsStr)
163
+ closePromise = new Promise<void>((resolve, reject) => {
164
+ socket.once('close', () => {
165
+ // socket completely closed
166
+ log('%s socket closed', lOptsStr)
167
+ resolve()
181
168
  })
182
- log('%s %s socket drained', direction, lOptsStr)
183
- }
169
+ socket.once('error', (err: Error) => {
170
+ log('%s socket error', lOptsStr, err)
184
171
 
185
- await Promise.all([
186
- raceEvent(eventTarget, 'close', signal, {
187
- errorEvent: 'error'
188
- }),
172
+ if (!socket.destroyed) {
173
+ reject(err)
174
+ }
175
+ // if socket is destroyed, 'closed' event will be emitted later to resolve the promise
176
+ })
177
+
178
+ // shorten inactivity timeout
179
+ socket.setTimeout(closeTimeout)
180
+
181
+ // close writable end of the socket
182
+ socket.end()
183
+
184
+ if (socket.writableLength > 0) {
185
+ // there are outgoing bytes waiting to be sent
186
+ socket.once('drain', () => {
187
+ log('%s socket drained', lOptsStr)
189
188
 
190
- // all bytes have been sent we can destroy the socket
191
- socket.destroy()
192
- ])
189
+ // all bytes have been sent we can destroy the socket (maybe) before the timeout
190
+ socket.destroy()
191
+ })
192
+ } else {
193
+ // nothing to send, destroy immediately, no need for the timeout
194
+ socket.destroy()
195
+ }
196
+ })
197
+
198
+ await closePromise
193
199
  } catch (err: any) {
194
200
  this.abort(err)
195
201
  } finally {
196
- closePromise.resolve()
202
+ options.signal?.removeEventListener('abort', abortSignalListener)
197
203
  }
198
204
  },
199
205
 
200
206
  abort: (err: Error) => {
201
- log('%s %s socket abort due to error - %e', direction, lOptsStr, err)
207
+ log('%s socket abort due to error', lOptsStr, err)
202
208
 
203
209
  // the abortSignalListener may already destroyed the socket with an error
204
- socket.destroy()
210
+ if (!socket.destroyed) {
211
+ socket.destroy(err)
212
+ }
205
213
 
206
214
  // closing a socket is always asynchronous (must wait for "close" event)
207
215
  // but the tests expect this to be a synchronous operation so we have to
208
216
  // set the close time here. the tests should be refactored to reflect
209
217
  // reality.
210
- maConn.timeline.close = Date.now()
218
+ if (maConn.timeline.close == null) {
219
+ maConn.timeline.close = Date.now()
220
+ }
211
221
  },
212
222
 
213
223
  log
@@ -215,17 +225,3 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
215
225
 
216
226
  return maConn
217
227
  }
218
-
219
- function socketToEventTarget (obj?: any): EventTarget {
220
- const eventTarget = {
221
- addEventListener: (type: any, cb: any) => {
222
- obj.addListener(type, cb)
223
- },
224
- removeEventListener: (type: any, cb: any) => {
225
- obj.removeListener(type, cb)
226
- }
227
- }
228
-
229
- // @ts-expect-error partial implementation
230
- return eventTarget
231
- }
package/src/tcp.ts CHANGED
@@ -36,7 +36,7 @@ import { TCPListener } from './listener.js'
36
36
  import { toMultiaddrConnection } from './socket-to-conn.js'
37
37
  import { multiaddrToNetConfig } from './utils.js'
38
38
  import type { TCPComponents, TCPCreateListenerOptions, TCPDialEvents, TCPDialOptions, TCPMetrics, TCPOptions } from './index.js'
39
- import type { Logger, Connection, Transport, Listener, MultiaddrConnection } from '@libp2p/interface'
39
+ import type { Logger, Connection, Transport, Listener } from '@libp2p/interface'
40
40
  import type { Multiaddr } from '@multiformats/multiaddr'
41
41
  import type { Socket, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net'
42
42
 
@@ -53,11 +53,7 @@ export class TCP implements Transport<TCPDialEvents> {
53
53
 
54
54
  if (components.metrics != null) {
55
55
  this.metrics = {
56
- events: components.metrics.registerCounterGroup('libp2p_tcp_dialer_events_total', {
57
- label: 'event',
58
- help: 'Total count of TCP dialer events by type'
59
- }),
60
- errors: components.metrics.registerCounterGroup('libp2p_tcp_dialer_errors_total', {
56
+ dialerEvents: components.metrics.registerCounterGroup('libp2p_tcp_dialer_events_total', {
61
57
  label: 'event',
62
58
  help: 'Total count of TCP dialer events by type'
63
59
  })
@@ -80,28 +76,23 @@ export class TCP implements Transport<TCPDialEvents> {
80
76
  // options.signal destroys the socket before 'connect' event
81
77
  const socket = await this._connect(ma, options)
82
78
 
83
- let maConn: MultiaddrConnection
79
+ // Avoid uncaught errors caused by unstable connections
80
+ socket.on('error', err => {
81
+ this.log('socket error', err)
82
+ })
84
83
 
85
- try {
86
- maConn = toMultiaddrConnection(socket, {
87
- remoteAddr: ma,
88
- socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout,
89
- socketCloseTimeout: this.opts.socketCloseTimeout,
90
- metrics: this.metrics?.events,
91
- logger: this.components.logger,
92
- direction: 'outbound'
93
- })
94
- } catch (err: any) {
95
- this.metrics?.errors.increment({ outbound_to_connection: true })
96
- socket.destroy(err)
97
- throw err
98
- }
84
+ const maConn = toMultiaddrConnection(socket, {
85
+ remoteAddr: ma,
86
+ socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout,
87
+ socketCloseTimeout: this.opts.socketCloseTimeout,
88
+ metrics: this.metrics?.dialerEvents,
89
+ logger: this.components.logger
90
+ })
99
91
 
100
92
  try {
101
93
  this.log('new outbound connection %s', maConn.remoteAddr)
102
94
  return await options.upgrader.upgradeOutbound(maConn, options)
103
95
  } catch (err: any) {
104
- this.metrics?.errors.increment({ outbound_upgrade: true })
105
96
  this.log.error('error upgrading outbound connection', err)
106
97
  maConn.abort(err)
107
98
  throw err
@@ -112,8 +103,6 @@ export class TCP implements Transport<TCPDialEvents> {
112
103
  options.signal?.throwIfAborted()
113
104
  options.onProgress?.(new CustomProgressEvent('tcp:open-connection'))
114
105
 
115
- let rawSocket: Socket
116
-
117
106
  return new Promise<Socket>((resolve, reject) => {
118
107
  const start = Date.now()
119
108
  const cOpts = multiaddrToNetConfig(ma, {
@@ -122,34 +111,35 @@ export class TCP implements Transport<TCPDialEvents> {
122
111
  }) as (IpcSocketConnectOpts & TcpSocketConnectOpts)
123
112
 
124
113
  this.log('dialing %a', ma)
125
- rawSocket = net.connect(cOpts)
114
+ const rawSocket = net.connect(cOpts)
126
115
 
127
116
  const onError = (err: Error): void => {
128
- this.log.error('dial to %a errored - %e', ma, err)
129
117
  const cOptsStr = cOpts.path ?? `${cOpts.host ?? ''}:${cOpts.port}`
130
118
  err.message = `connection error ${cOptsStr}: ${err.message}`
131
- this.metrics?.events.increment({ error: true })
119
+ this.metrics?.dialerEvents.increment({ error: true })
120
+
132
121
  done(err)
133
122
  }
134
123
 
135
124
  const onTimeout = (): void => {
136
125
  this.log('connection timeout %a', ma)
137
- this.metrics?.events.increment({ timeout: true })
126
+ this.metrics?.dialerEvents.increment({ timeout: true })
138
127
 
139
- const err = new TimeoutError(`Connection timeout after ${Date.now() - start}ms`)
128
+ const err = new TimeoutError(`connection timeout after ${Date.now() - start}ms`)
140
129
  // Note: this will result in onError() being called
141
130
  rawSocket.emit('error', err)
142
131
  }
143
132
 
144
133
  const onConnect = (): void => {
145
134
  this.log('connection opened %a', ma)
146
- this.metrics?.events.increment({ connect: true })
135
+ this.metrics?.dialerEvents.increment({ connect: true })
147
136
  done()
148
137
  }
149
138
 
150
139
  const onAbort = (): void => {
151
140
  this.log('connection aborted %a', ma)
152
- this.metrics?.events.increment({ abort: true })
141
+ this.metrics?.dialerEvents.increment({ abort: true })
142
+ rawSocket.destroy()
153
143
  done(new AbortError())
154
144
  }
155
145
 
@@ -177,10 +167,6 @@ export class TCP implements Transport<TCPDialEvents> {
177
167
  options.signal.addEventListener('abort', onAbort)
178
168
  }
179
169
  })
180
- .catch(err => {
181
- rawSocket?.destroy()
182
- throw err
183
- })
184
170
  }
185
171
 
186
172
  /**