A migration guide for refactoring your application code from libp2p v0.45
to v0.46
.
The identify protocol is used by libp2p to discover which protocols a remote peer supports. By default it runs on every connection after it opens.
Applications interested in peers that support certain protocols can register topology callbacks to be notified when network peers that support those protocols connect or disconnect.
libp2p@0.46.x
adds the ability for the user to fine-tune their identify usage and to run the identify protocol manually:
import { createLibp2p } from 'libp2p'
import { identifyService } from 'libp2p/identify'
const node = await createLibp2p({
services: {
identify: identifyService({
// identify has stream limits so to prevent remote peers from closing
// streams due to too many identify streams being opened in parallel,
// so use this setting to disable running identify automatically.
//
// Note that this means you will need to run identify manually for
// every connection that opens in order for topologies to work.
//
// Some modules such as KAD-DHT and Circuit Relay rely on this being
// the case.
runOnConnectionOpen: false
})
}
})
const conn = await node.dial('/ip4/123.123...')
const identifyResult = await node.services.identify.identify(conn)
Note that this is an advanced option and is not necessary for the vast majority of users.
Most users will want to configure a topology instead to be notified when new peers are discovered that support a given protocol:
import { createLibp2p } from 'libp2p'
import { identifyService } from 'libp2p/identify'
const node = await createLibp2p({
services: {
identify: identifyService()
}
})
node.register('/my/protocol', {
onConnect (peer, connection) {
// this is called after identify has completed and the peer has confirmed
// which protocols it supports
},
onDisconnect (peer, connection) {
// handle disconnect
}
})
Some connections have limits applied to them by the remote. For example, as part of the Circuit Relay v2 protocol relay servers are allowed to limit the amount of data transferred over a relayed connection and for how long.
These connections are not expected to be long-lived and must be treated as slightly fragile as the remote may close them at any time. To detect this, these types of connections have a boolean .transient
property set to true
.
import { createLibp2p } from 'libp2p'
import { identifyService } from 'libp2p/identify'
const node = await createLibp2p({ /* ... */ })
// make a direct connection to a peer
const conn1 = await node.dial('/ip4/123.123.123.123/tcp/123')
console.info(conn1.transient) // false
// make a connection to a peer via a relay server
const conn2 = await node.dial('/ip4/.../p2p-circuit/...')
console.info(conn2.transient) // true
By default no protocols may run over a transient connection - to allow this protocols must explicitly opt-in to being run. This is in order to prevent high-bandwidth protocols from accidentally causing the remote to close the connection.
import { createLibp2p } from 'libp2p'
import { identifyService } from 'libp2p/identify'
const node = await createLibp2p({
// config here
})
// register an incoming stream handler for a protocol that is allowed to run over
// transient connections
await node.register('/my/protocol', () => {}, {
runOnTransientConnection: true
})
// open a stream and allow the protocol to run over a transient connection
const stream = await node.dialProtocol('/ip4/.../p2p-circuit/...', '/my/protocol', {
runOnTransientConnection: true
})
// the same flag can be passed to the `newStream` method on the connection itself
const conn = await node.dial()
conn.newStream('/my/protocol', {
runOnTransientConnection: true
})
Streams can either be closed gracefully, where we wait for any unsent data to be sent, or aborted in which case any unsent data is discarded and a reset message is sent, notifying the remote of the abnormal termination.
To close a stream gracefully we call the .close
method (or .closeRead
/.closeWrite
for when we want half-closed streams). To abort a stream we call .abort
and pass an error object.
In previous versions the .close
method was synchronous which meant it could not wait for existing data to be sent which made nodes behave unpredictably.
From 0.46.x
the .close
/.closeRead
/.closeWrite
methods on the Stream interface are now asynchronous. .abort
is a synchronous method that accepts an Error object.
Similarly the Connection interface now has asynchronous .close
and synchronous .abort
methods.
The .reset
method has been removed from the Stream interface as it is only to be invoked internally by stream multiplexers when a remote stream reset has occurred.
Before
const stream = await libp2p.dialProtocol(multiaddr, '/my-protocol/1.0.0')
// send some data
await stream.sink([data])
// close the stream - previously this may not have waited for the data to be sent
stream.close()
// alternatively cause the stream to error on the remote
stream.abort(new Error('Oh no!'))
After
const stream = await libp2p.dialProtocol(multiaddr, '/my-protocol/1.0.0')
// send some data
await stream.sink([data])
// close the stream - this method is now async
await stream.close()
// alternatively cause the stream to error on the remote
stream.abort(new Error('Oh no!'))
The properties on the stream.stat
and connection.stat
objects are now stored on the stream/connection itself.
Before
// stream.stat properties
console.info(stream.stat.direction)
console.info(stream.stat.timeline)
console.info(stream.stat.protocol)
// connection.stat properties
console.info(connection.stat.direction)
console.info(connection.stat.timeline)
console.info(connection.stat.multiplexer)
console.info(connection.stat.encryption)
console.info(connection.stat.status)
After
// stream.stat properties
console.info(stream.direction)
console.info(stream.timeline)
console.info(stream.protocol)
// connection.stat properties
console.info(connection.direction)
console.info(connection.timeline)
console.info(connection.multiplexer)
console.info(connection.encryption)
console.info(connection.status)
In an effort to prevent breaking changes affecting unrelated modules, libp2p prior to 0.46.x had a large number of single-issue interface modules for internal and external types - @libp2p/address-manager
, @libp2p/connection-gater
, @libp2p/connection-manager
and so on.
This meant that although we could release a new version of the address manager interface without impacting modules that only depended on the connection manager, releasing any change became a multiple-step process during which there was a time window sometimes lasting several days when the latest versions of modules would be incompatible with each other.
Adding new methods and types to interfaces also became a breaking change since the existing released implementations of those interfaces would not implement the new methods which complicated matters further.
Since libp2p/js-libp2p#1792 converted libp2p into a monorepo project, a lot of these problems have gone away since we can now release multiple libp2p modules simultaneously.
The urgency that required multiple interface modules has also subsided somewhat so now all libp2p interfaces are collected into two modules - @lib2p2p/interface
for public-facing APIs and @libp2p/interface-internal
for APIs designed to be consumed by libp2p components.
Before
import type { Libp2p } from '@libp2p/interface-libp2p'
import type { AddressManager } from '@libp2p/interface-address-manager'
import type { ConnectionManager } from '@libp2p/interface-connection-manager'
// etc
After
import type { Libp2p } from '@libp2p/interface'
import type { AddressManager } from '@libp2p/interface-internal/address-manager'
import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager'
// etc