libp2p 2.8.14 → 2.9.0-79473c99a

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.
@@ -0,0 +1,395 @@
1
+ import { connectionSymbol, LimitedConnectionError, ConnectionClosedError, ConnectionClosingError, TooManyOutboundProtocolStreamsError, TooManyInboundProtocolStreamsError } from '@libp2p/interface'
2
+ import * as mss from '@libp2p/multistream-select'
3
+ import { setMaxListeners } from 'main-event'
4
+ import { PROTOCOL_NEGOTIATION_TIMEOUT } from './connection-manager/constants.defaults.ts'
5
+ import { MuxerUnavailableError } from './errors.ts'
6
+ import { DEFAULT_MAX_INBOUND_STREAMS, DEFAULT_MAX_OUTBOUND_STREAMS } from './registrar.ts'
7
+ import type { AbortOptions, Logger, Direction, Connection as ConnectionInterface, Stream, ConnectionTimeline, ConnectionStatus, NewStreamOptions, PeerId, ConnectionLimits, StreamMuxerFactory, StreamMuxer, Metrics, PeerStore, MultiaddrConnection } from '@libp2p/interface'
8
+ import type { Registrar } from '@libp2p/interface-internal'
9
+ import type { Multiaddr } from '@multiformats/multiaddr'
10
+
11
+ const CLOSE_TIMEOUT = 500
12
+
13
+ export interface ConnectionComponents {
14
+ peerStore: PeerStore
15
+ registrar: Registrar
16
+ metrics?: Metrics
17
+ }
18
+
19
+ export interface ConnectionInit {
20
+ id: string
21
+ maConn: MultiaddrConnection
22
+ remotePeer: PeerId
23
+ direction?: Direction
24
+ muxerFactory?: StreamMuxerFactory
25
+ encryption?: string
26
+ limits?: ConnectionLimits
27
+ outboundStreamProtocolNegotiationTimeout?: number
28
+ inboundStreamProtocolNegotiationTimeout?: number
29
+ }
30
+
31
+ /**
32
+ * An implementation of the js-libp2p connection.
33
+ * Any libp2p transport should use an upgrader to return this connection.
34
+ */
35
+ export class Connection implements ConnectionInterface {
36
+ public readonly id: string
37
+ public readonly remoteAddr: Multiaddr
38
+ public readonly remotePeer: PeerId
39
+ public direction: Direction
40
+ public timeline: ConnectionTimeline
41
+ public multiplexer?: string
42
+ public encryption?: string
43
+ public status: ConnectionStatus
44
+ public limits?: ConnectionLimits
45
+ public readonly log: Logger
46
+ public tags: string[]
47
+
48
+ private readonly maConn: MultiaddrConnection
49
+ private readonly muxer?: StreamMuxer
50
+ private readonly components: ConnectionComponents
51
+ private readonly outboundStreamProtocolNegotiationTimeout: number
52
+ private readonly inboundStreamProtocolNegotiationTimeout: number
53
+
54
+ constructor (components: ConnectionComponents, init: ConnectionInit) {
55
+ this.components = components
56
+
57
+ this.id = init.id
58
+ this.remoteAddr = init.maConn.remoteAddr
59
+ this.remotePeer = init.remotePeer
60
+ this.direction = init.direction ?? 'outbound'
61
+ this.status = 'open'
62
+ this.timeline = init.maConn.timeline
63
+ this.encryption = init.encryption
64
+ this.limits = init.limits
65
+ this.maConn = init.maConn
66
+ this.log = init.maConn.log
67
+ this.outboundStreamProtocolNegotiationTimeout = init.outboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT
68
+ this.inboundStreamProtocolNegotiationTimeout = init.inboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT
69
+
70
+ if (this.remoteAddr.getPeerId() == null) {
71
+ this.remoteAddr = this.remoteAddr.encapsulate(`/p2p/${this.remotePeer}`)
72
+ }
73
+
74
+ this.tags = []
75
+
76
+ if (init.muxerFactory != null) {
77
+ this.multiplexer = init.muxerFactory.protocol
78
+
79
+ this.muxer = init.muxerFactory.createStreamMuxer({
80
+ direction: this.direction,
81
+ log: this.log,
82
+ // Run anytime a remote stream is created
83
+ onIncomingStream: (stream) => {
84
+ this.onIncomingStream(stream)
85
+ }
86
+ })
87
+
88
+ // Pipe all data through the muxer
89
+ void Promise.all([
90
+ this.muxer.sink(this.maConn.source),
91
+ this.maConn.sink(this.muxer.source)
92
+ ]).catch(err => {
93
+ this.log.error('error piping data through muxer - %e', err)
94
+ })
95
+ }
96
+ }
97
+
98
+ readonly [Symbol.toStringTag] = 'Connection'
99
+
100
+ readonly [connectionSymbol] = true
101
+
102
+ get streams (): Stream[] {
103
+ return this.muxer?.streams ?? []
104
+ }
105
+
106
+ /**
107
+ * Create a new stream over this connection
108
+ */
109
+ newStream = async (protocols: string[], options: NewStreamOptions = {}): Promise<Stream> => {
110
+ if (this.status === 'closing') {
111
+ throw new ConnectionClosingError('the connection is being closed')
112
+ }
113
+
114
+ if (this.status === 'closed') {
115
+ throw new ConnectionClosedError('the connection is closed')
116
+ }
117
+
118
+ if (!Array.isArray(protocols)) {
119
+ protocols = [protocols]
120
+ }
121
+
122
+ if (this.limits != null && options?.runOnLimitedConnection !== true) {
123
+ throw new LimitedConnectionError('Cannot open protocol stream on limited connection')
124
+ }
125
+
126
+ if (this.muxer == null) {
127
+ throw new MuxerUnavailableError('Connection is not multiplexed')
128
+ }
129
+
130
+ this.log.trace('starting new stream for protocols %s', protocols)
131
+ const muxedStream = await this.muxer.newStream()
132
+ this.log.trace('started new stream %s for protocols %s', muxedStream.id, protocols)
133
+
134
+ try {
135
+ if (options.signal == null) {
136
+ muxedStream.log('no abort signal was passed while trying to negotiate protocols %s falling back to default timeout', protocols)
137
+
138
+ const signal = AbortSignal.timeout(this.outboundStreamProtocolNegotiationTimeout)
139
+ setMaxListeners(Infinity, signal)
140
+
141
+ options = {
142
+ ...options,
143
+ signal
144
+ }
145
+ }
146
+
147
+ muxedStream.log.trace('selecting protocol from protocols %s', protocols)
148
+
149
+ const {
150
+ stream,
151
+ protocol
152
+ } = await mss.select(muxedStream, protocols, {
153
+ ...options,
154
+ log: muxedStream.log,
155
+ yieldBytes: true
156
+ })
157
+
158
+ muxedStream.log('selected protocol %s', protocol)
159
+
160
+ const outgoingLimit = findOutgoingStreamLimit(protocol, this.components.registrar, options)
161
+ const streamCount = countStreams(protocol, 'outbound', this)
162
+
163
+ if (streamCount >= outgoingLimit) {
164
+ const err = new TooManyOutboundProtocolStreamsError(`Too many outbound protocol streams for protocol "${protocol}" - ${streamCount}/${outgoingLimit}`)
165
+ muxedStream.abort(err)
166
+
167
+ throw err
168
+ }
169
+
170
+ // If a protocol stream has been successfully negotiated and is to be passed to the application,
171
+ // the peer store should ensure that the peer is registered with that protocol
172
+ await this.components.peerStore.merge(this.remotePeer, {
173
+ protocols: [protocol]
174
+ })
175
+
176
+ // after the handshake the returned stream can have early data so override
177
+ // the source/sink
178
+ muxedStream.source = stream.source
179
+ muxedStream.sink = stream.sink
180
+ muxedStream.protocol = protocol
181
+
182
+ // allow closing the write end of a not-yet-negotiated stream
183
+ if (stream.closeWrite != null) {
184
+ muxedStream.closeWrite = stream.closeWrite
185
+ }
186
+
187
+ // allow closing the read end of a not-yet-negotiated stream
188
+ if (stream.closeRead != null) {
189
+ muxedStream.closeRead = stream.closeRead
190
+ }
191
+
192
+ // make sure we don't try to negotiate a stream we are closing
193
+ if (stream.close != null) {
194
+ muxedStream.close = stream.close
195
+ }
196
+
197
+ this.components.metrics?.trackProtocolStream(muxedStream, this)
198
+
199
+ muxedStream.direction = 'outbound'
200
+
201
+ return muxedStream
202
+ } catch (err: any) {
203
+ this.log.error('could not create new outbound stream on connection %s %a for protocols %s - %e', this.direction === 'inbound' ? 'from' : 'to', this.remoteAddr, protocols, err)
204
+
205
+ if (muxedStream.timeline.close == null) {
206
+ muxedStream.abort(err)
207
+ }
208
+
209
+ throw err
210
+ }
211
+ }
212
+
213
+ private onIncomingStream (muxedStream: Stream): void {
214
+ const signal = AbortSignal.timeout(this.inboundStreamProtocolNegotiationTimeout)
215
+ setMaxListeners(Infinity, signal)
216
+
217
+ void Promise.resolve()
218
+ .then(async () => {
219
+ const protocols = this.components.registrar.getProtocols()
220
+
221
+ const { stream, protocol } = await mss.handle(muxedStream, protocols, {
222
+ signal,
223
+ log: muxedStream.log,
224
+ yieldBytes: false
225
+ })
226
+
227
+ this.log('incoming %s stream opened', protocol)
228
+
229
+ const incomingLimit = findIncomingStreamLimit(protocol, this.components.registrar)
230
+ const streamCount = countStreams(protocol, 'inbound', this)
231
+
232
+ if (streamCount === incomingLimit) {
233
+ const err = new TooManyInboundProtocolStreamsError(`Too many inbound protocol streams for protocol "${protocol}" - limit ${incomingLimit}`)
234
+ muxedStream.abort(err)
235
+
236
+ throw err
237
+ }
238
+
239
+ // after the handshake the returned stream can have early data so override
240
+ // the source/sink
241
+ muxedStream.source = stream.source
242
+ muxedStream.sink = stream.sink
243
+ muxedStream.protocol = protocol
244
+
245
+ // allow closing the write end of a not-yet-negotiated stream
246
+ if (stream.closeWrite != null) {
247
+ muxedStream.closeWrite = stream.closeWrite
248
+ }
249
+
250
+ // allow closing the read end of a not-yet-negotiated stream
251
+ if (stream.closeRead != null) {
252
+ muxedStream.closeRead = stream.closeRead
253
+ }
254
+
255
+ // make sure we don't try to negotiate a stream we are closing
256
+ if (stream.close != null) {
257
+ muxedStream.close = stream.close
258
+ }
259
+
260
+ // If a protocol stream has been successfully negotiated and is to be passed to the application,
261
+ // the peer store should ensure that the peer is registered with that protocol
262
+ await this.components.peerStore.merge(this.remotePeer, {
263
+ protocols: [protocol]
264
+ }, {
265
+ signal
266
+ })
267
+
268
+ this.components.metrics?.trackProtocolStream(muxedStream, this)
269
+
270
+ const { handler, options } = this.components.registrar.getHandler(protocol)
271
+
272
+ if (this.limits != null && options.runOnLimitedConnection !== true) {
273
+ throw new LimitedConnectionError('Cannot open protocol stream on limited connection')
274
+ }
275
+
276
+ handler({ connection: this, stream: muxedStream })
277
+ })
278
+ .catch(async err => {
279
+ this.log.error('error handling incoming stream id %s - %e', muxedStream.id, err)
280
+
281
+ if (muxedStream.timeline.close == null) {
282
+ await muxedStream.close({
283
+ signal
284
+ })
285
+ .catch(err => muxedStream.abort(err))
286
+ }
287
+ })
288
+ }
289
+
290
+ /**
291
+ * Close the connection
292
+ */
293
+ async close (options: AbortOptions = {}): Promise<void> {
294
+ if (this.status === 'closed' || this.status === 'closing') {
295
+ return
296
+ }
297
+
298
+ this.log('closing connection to %a', this.remoteAddr)
299
+
300
+ this.status = 'closing'
301
+
302
+ if (options.signal == null) {
303
+ const signal = AbortSignal.timeout(CLOSE_TIMEOUT)
304
+ setMaxListeners(Infinity, signal)
305
+
306
+ options = {
307
+ ...options,
308
+ signal
309
+ }
310
+ }
311
+
312
+ try {
313
+ this.log.trace('closing underlying transport')
314
+
315
+ // ensure remaining streams are closed gracefully
316
+ await this.muxer?.close(options)
317
+
318
+ // close the underlying transport
319
+ await this.maConn.close(options)
320
+
321
+ this.log.trace('updating timeline with close time')
322
+
323
+ this.status = 'closed'
324
+ this.timeline.close = Date.now()
325
+ } catch (err: any) {
326
+ this.log.error('error encountered during graceful close of connection to %a', this.remoteAddr, err)
327
+ this.abort(err)
328
+ }
329
+ }
330
+
331
+ abort (err: Error): void {
332
+ if (this.status === 'closed') {
333
+ return
334
+ }
335
+
336
+ this.log.error('aborting connection to %a due to error', this.remoteAddr, err)
337
+
338
+ this.status = 'closing'
339
+
340
+ // ensure remaining streams are aborted
341
+ this.muxer?.abort(err)
342
+
343
+ // abort the underlying transport
344
+ this.maConn.abort(err)
345
+
346
+ this.status = 'closed'
347
+ this.timeline.close = Date.now()
348
+ }
349
+ }
350
+
351
+ export function createConnection (components: ConnectionComponents, init: ConnectionInit): ConnectionInterface {
352
+ return new Connection(components, init)
353
+ }
354
+
355
+ function findIncomingStreamLimit (protocol: string, registrar: Registrar): number | undefined {
356
+ try {
357
+ const { options } = registrar.getHandler(protocol)
358
+
359
+ return options.maxInboundStreams
360
+ } catch (err: any) {
361
+ if (err.name !== 'UnhandledProtocolError') {
362
+ throw err
363
+ }
364
+ }
365
+
366
+ return DEFAULT_MAX_INBOUND_STREAMS
367
+ }
368
+
369
+ function findOutgoingStreamLimit (protocol: string, registrar: Registrar, options: NewStreamOptions = {}): number {
370
+ try {
371
+ const { options } = registrar.getHandler(protocol)
372
+
373
+ if (options.maxOutboundStreams != null) {
374
+ return options.maxOutboundStreams
375
+ }
376
+ } catch (err: any) {
377
+ if (err.name !== 'UnhandledProtocolError') {
378
+ throw err
379
+ }
380
+ }
381
+
382
+ return options.maxOutboundStreams ?? DEFAULT_MAX_OUTBOUND_STREAMS
383
+ }
384
+
385
+ function countStreams (protocol: string, direction: 'inbound' | 'outbound', connection: Connection): number {
386
+ let streamCount = 0
387
+
388
+ connection.streams.forEach(stream => {
389
+ if (stream.direction === direction && stream.protocol === protocol) {
390
+ streamCount++
391
+ }
392
+ })
393
+
394
+ return streamCount
395
+ }
package/src/index.ts CHANGED
@@ -212,3 +212,23 @@ export async function createLibp2p <T extends ServiceMap = ServiceMap> (options:
212
212
 
213
213
  return node
214
214
  }
215
+
216
+ // a non-exhaustive list of methods found on the libp2p object
217
+ const LIBP2P_METHODS = ['dial', 'dialProtocol', 'hangUp', 'handle', 'unhandle', 'getMultiaddrs', 'getProtocols']
218
+
219
+ /**
220
+ * Returns true if the passed object is a libp2p node - this can be used for
221
+ * type guarding in TypeScript.
222
+ */
223
+ export function isLibp2p <T extends ServiceMap = ServiceMap> (obj?: any): obj is Libp2p<T> {
224
+ if (obj == null) {
225
+ return false
226
+ }
227
+
228
+ if (obj instanceof Libp2pClass) {
229
+ return true
230
+ }
231
+
232
+ // if these are all functions it's probably a libp2p object
233
+ return LIBP2P_METHODS.every(m => typeof obj[m] === 'function')
234
+ }