ipfs-bitswap
Version:
JavaScript implementation of the Bitswap data exchange protocol used by IPFS
307 lines (260 loc) • 10 kB
text/typescript
import { CodeError } from '@libp2p/interface'
import drain from 'it-drain'
import * as lp from 'it-length-prefixed'
import map from 'it-map'
import { pipe } from 'it-pipe'
import take from 'it-take'
import { type ProgressEvent, CustomProgressEvent, type ProgressOptions } from 'progress-events'
import { TimeoutController } from 'timeout-abort-controller'
import * as CONSTANTS from './constants.js'
import { BitswapMessage as Message } from './message/index.js'
import { logger } from './utils/index.js'
import type { DefaultBitswap } from './bitswap.js'
import type { MultihashHasherLoader } from './index.js'
import type { Stats } from './stats/index.js'
import type { Libp2p, AbortOptions, Connection, PeerId, PeerInfo, IncomingStreamData, Topology } from '@libp2p/interface'
import type { Logger } from '@libp2p/logger'
import type { Multiaddr } from '@multiformats/multiaddr'
import type { CID } from 'multiformats/cid'
export interface Provider {
id: PeerId
multiaddrs: Multiaddr[]
}
export type BitswapNetworkProgressEvents =
ProgressEvent<'bitswap:network:dial', PeerId>
export type BitswapNetworkWantProgressEvents =
ProgressEvent<'bitswap:network:send-wantlist', PeerId> |
ProgressEvent<'bitswap:network:send-wantlist:error', { peer: PeerId, error: Error }> |
ProgressEvent<'bitswap:network:find-providers', CID> |
BitswapNetworkProgressEvents
export type BitswapNetworkNotifyProgressEvents =
ProgressEvent<'bitswap:network:provide', CID> |
BitswapNetworkProgressEvents
const BITSWAP100 = '/ipfs/bitswap/1.0.0'
const BITSWAP110 = '/ipfs/bitswap/1.1.0'
const BITSWAP120 = '/ipfs/bitswap/1.2.0'
const DEFAULT_MAX_INBOUND_STREAMS = 1024
const DEFAULT_MAX_OUTBOUND_STREAMS = 1024
const DEFAULT_INCOMING_STREAM_TIMEOUT = 30000
export interface NetworkOptions {
b100Only?: boolean
hashLoader?: MultihashHasherLoader
maxInboundStreams?: number
maxOutboundStreams?: number
incomingStreamTimeout?: number
}
export class Network {
private readonly _log: Logger
private readonly _libp2p: Libp2p
private readonly _bitswap: DefaultBitswap
public _protocols: string[]
private readonly _stats: Stats
private _running: boolean
private readonly _hashLoader: MultihashHasherLoader
private readonly _maxInboundStreams: number
private readonly _maxOutboundStreams: number
private readonly _incomingStreamTimeout: number
private _registrarIds?: string[]
constructor (libp2p: Libp2p, bitswap: DefaultBitswap, stats: Stats, options: NetworkOptions = {}) {
this._log = logger(libp2p.peerId, 'network')
this._libp2p = libp2p
this._bitswap = bitswap
this._protocols = [BITSWAP100]
if (options.b100Only !== true) {
// Latest bitswap first
this._protocols.unshift(BITSWAP110)
this._protocols.unshift(BITSWAP120)
}
this._stats = stats
this._running = false
// bind event listeners
this._onPeerConnect = this._onPeerConnect.bind(this)
this._onPeerDisconnect = this._onPeerDisconnect.bind(this)
this._onConnection = this._onConnection.bind(this)
this._hashLoader = options.hashLoader ?? {
async getHasher () {
throw new Error('Not implemented')
}
}
this._maxInboundStreams = options.maxInboundStreams ?? DEFAULT_MAX_INBOUND_STREAMS
this._maxOutboundStreams = options.maxOutboundStreams ?? DEFAULT_MAX_OUTBOUND_STREAMS
this._incomingStreamTimeout = options.incomingStreamTimeout ?? DEFAULT_INCOMING_STREAM_TIMEOUT
}
async start (): Promise<void> {
this._running = true
await this._libp2p.handle(this._protocols, this._onConnection, {
maxInboundStreams: this._maxInboundStreams,
maxOutboundStreams: this._maxOutboundStreams
})
// register protocol with topology
const topology: Topology = {
onConnect: this._onPeerConnect,
onDisconnect: this._onPeerDisconnect
}
/** @type {string[]} */
this._registrarIds = []
for (const protocol of this._protocols) {
this._registrarIds.push(await this._libp2p.register(protocol, topology))
}
// All existing connections are like new ones for us
this._libp2p.getConnections().forEach(conn => {
this._onPeerConnect(conn.remotePeer)
})
}
async stop (): Promise<void> {
this._running = false
// Unhandle both, libp2p doesn't care if it's not already handled
await this._libp2p.unhandle(this._protocols)
// unregister protocol and handlers
if (this._registrarIds != null) {
for (const id of this._registrarIds) {
this._libp2p.unregister(id)
}
this._registrarIds = []
}
}
/**
* Handles both types of incoming bitswap messages
*/
_onConnection (info: IncomingStreamData): void {
if (!this._running) {
return
}
const { stream, connection } = info
const controller = new TimeoutController(this._incomingStreamTimeout)
Promise.resolve().then(async () => {
this._log('incoming new bitswap %s connection from %p', stream.protocol, connection.remotePeer)
const abortListener = (): void => {
stream.abort(new CodeError('Incoming Bitswap stream timed out', 'ERR_TIMEOUT'))
}
let signal = AbortSignal.timeout(this._incomingStreamTimeout)
signal.addEventListener('abort', abortListener)
await pipe(
stream,
(source) => lp.decode(source),
async (source) => {
for await (const data of source) {
try {
const message = await Message.deserialize(data.subarray(), this._hashLoader)
await this._bitswap._receiveMessage(connection.remotePeer, message)
} catch (err: any) {
this._bitswap._receiveError(err)
break
}
// we have received some data so reset the timeout controller
signal.removeEventListener('abort', abortListener)
signal = AbortSignal.timeout(this._incomingStreamTimeout)
signal.addEventListener('abort', abortListener)
}
}
)
await stream.close({
signal
})
})
.catch(err => {
this._log(err)
stream.abort(err)
})
.finally(() => {
controller.clear()
})
}
_onPeerConnect (peerId: PeerId): void {
this._bitswap._onPeerConnected(peerId)
}
_onPeerDisconnect (peerId: PeerId): void {
this._bitswap._onPeerDisconnected(peerId)
}
/**
* Find providers given a `cid`.
*/
findProviders (cid: CID, options: AbortOptions & ProgressOptions<BitswapNetworkWantProgressEvents> = {}): AsyncIterable<PeerInfo> {
options.onProgress?.(new CustomProgressEvent<PeerId>('bitswap:network:find-providers', cid))
return this._libp2p.contentRouting.findProviders(cid, options)
}
/**
* Find the providers of a given `cid` and connect to them.
*/
async findAndConnect (cid: CID, options?: AbortOptions & ProgressOptions<BitswapNetworkWantProgressEvents>): Promise<void> {
await drain(
take(
map(this.findProviders(cid, options), async provider => this.connectTo(provider.id, options)
.catch(err => {
// Prevent unhandled promise rejection
this._log.error(err)
})),
CONSTANTS.maxProvidersPerRequest
)
)
.catch(err => {
this._log.error(err)
})
}
/**
* Tell the network we can provide content for the passed CID
*/
async provide (cid: CID, options: AbortOptions & ProgressOptions<BitswapNetworkNotifyProgressEvents> = {}): Promise<void> {
options.onProgress?.(new CustomProgressEvent<PeerId>('bitswap:network:provide', cid))
await this._libp2p.contentRouting.provide(cid, options)
}
/**
* Connect to the given peer
* Send the given msg (instance of Message) to the given peer
*/
async sendMessage (peer: PeerId, msg: Message, options: ProgressOptions<BitswapNetworkWantProgressEvents> = {}): Promise<void> {
if (!this._running) throw new Error('network isn\'t running')
const stringId = peer.toString()
this._log('sendMessage to %s', stringId, msg)
options.onProgress?.(new CustomProgressEvent<PeerId>('bitswap:network:send-wantlist', peer))
await this._writeMessage(peer, msg, options)
this._updateSentStats(peer, msg.blocks)
}
/**
* Connects to another peer
*/
async connectTo (peer: PeerId, options: AbortOptions & ProgressOptions<BitswapNetworkProgressEvents> = {}): Promise<Connection> { // eslint-disable-line require-await
if (!this._running) {
throw new Error('network isn\'t running')
}
options.onProgress?.(new CustomProgressEvent<PeerId>('bitswap:network:dial', peer))
return this._libp2p.dial(peer, options)
}
_updateSentStats (peer: PeerId, blocks: Map<string, Uint8Array>): void {
const peerId = peer.toString()
if (this._stats != null) {
for (const block of blocks.values()) {
this._stats.push(peerId, 'dataSent', block.length)
}
this._stats.push(peerId, 'blocksSent', blocks.size)
}
}
async _writeMessage (peerId: PeerId, msg: Message, options: ProgressOptions<BitswapNetworkWantProgressEvents> = {}): Promise<void> {
const stream = await this._libp2p.dialProtocol(peerId, [BITSWAP120, BITSWAP110, BITSWAP100])
try {
/** @type {Uint8Array} */
let serialized
switch (stream.protocol) {
case BITSWAP100:
serialized = msg.serializeToBitswap100()
break
case BITSWAP110:
case BITSWAP120:
serialized = msg.serializeToBitswap110()
break
default:
throw new Error(`Unknown protocol: ${stream.protocol}`)
}
await pipe(
[serialized],
(source) => lp.encode(source),
stream
)
await stream.close()
} catch (err: any) {
options.onProgress?.(new CustomProgressEvent<{ peer: PeerId, error: Error }>('bitswap:network:send-wantlist:error', { peer: peerId, error: err }))
this._log(err)
stream.abort(err)
}
}
}