UNPKG

ipfs-bitswap

Version:

JavaScript implementation of the Bitswap data exchange protocol used by IPFS

373 lines (318 loc) 11.1 kB
import { anySignal } from 'any-signal' import forEach from 'it-foreach' import { CID } from 'multiformats/cid' import { DecisionEngine, type PeerLedger } from './decision-engine/index.js' import { Network } from './network.js' import { Notifications } from './notifications.js' import { Stats } from './stats/index.js' import { logger } from './utils/index.js' import { WantManager } from './want-manager/index.js' import type { BitswapOptions, Bitswap, MultihashHasherLoader, WantListEntry, BitswapWantProgressEvents, BitswapNotifyProgressEvents } from './index.js' import type { BitswapMessage } from './message/index.js' import type { Libp2p, PeerId } from '@libp2p/interface' import type { Logger } from '@libp2p/logger' import type { AbortOptions } from '@multiformats/multiaddr' import type { Blockstore, Pair } from 'interface-blockstore' import type { AwaitIterable } from 'interface-store' import type { ProgressOptions } from 'progress-events' const hashLoader: MultihashHasherLoader = { async getHasher () { throw new Error('Not implemented') } } const defaultOptions: Required<BitswapOptions> = { maxInboundStreams: 1024, maxOutboundStreams: 1024, incomingStreamTimeout: 30000, hashLoader, statsEnabled: false, statsComputeThrottleTimeout: 1000, statsComputeThrottleMaxQueueSize: 1000 } const statsKeys = [ 'blocksReceived', 'dataReceived', 'dupBlksReceived', 'dupDataReceived', 'blocksSent', 'dataSent', 'providesBufferLength', 'wantListLength', 'peerCount' ] /** * JavaScript implementation of the Bitswap 'data exchange' protocol * used by IPFS. */ export class DefaultBitswap implements Bitswap { private readonly _libp2p: Libp2p private readonly _log: Logger public readonly stats: Stats public network: Network public blockstore: Blockstore public engine: DecisionEngine public wm: WantManager public notifications: Notifications private started: boolean constructor (libp2p: Libp2p, blockstore: Blockstore, options: BitswapOptions = {}) { this._libp2p = libp2p this._log = logger(this.peerId) options = Object.assign({}, defaultOptions, options) // stats this.stats = new Stats(libp2p, statsKeys, { enabled: options.statsEnabled, computeThrottleTimeout: options.statsComputeThrottleTimeout, computeThrottleMaxQueueSize: options.statsComputeThrottleMaxQueueSize }) // the network delivers messages this.network = new Network(libp2p, this, this.stats, { hashLoader: options.hashLoader, maxInboundStreams: options.maxInboundStreams, maxOutboundStreams: options.maxOutboundStreams, incomingStreamTimeout: options.incomingStreamTimeout }) // local database this.blockstore = blockstore this.engine = new DecisionEngine(this.peerId, blockstore, this.network, this.stats, libp2p) // handle message sending this.wm = new WantManager(this.peerId, this.network, this.stats, libp2p) this.notifications = new Notifications(this.peerId) this.started = false } isStarted (): boolean { return this.started } get peerId (): PeerId { return this._libp2p.peerId } /** * handle messages received through the network */ async _receiveMessage (peerId: PeerId, incoming: BitswapMessage): Promise<void> { try { // Note: this allows the engine to respond to any wants in the message. // Processing of the blocks in the message happens below, after the // blocks have been added to the blockstore. await this.engine.messageReceived(peerId, incoming) } catch (err) { // Log instead of throwing an error so as to process as much as // possible of the message. Currently `messageReceived` does not // throw any errors, but this could change in the future. this._log('failed to receive message', incoming) } if (incoming.blocks.size === 0) { return } /** @type { { cid: CID, wasWanted: boolean, data: Uint8Array }[] } */ const received = [] for (const [cidStr, data] of incoming.blocks.entries()) { const cid = CID.parse(cidStr) received.push({ wasWanted: this.wm.wantlist.contains(cid), cid, data }) } // quickly send out cancels, reduces chances of duplicate block receives this.wm.cancelWants( received .filter(({ wasWanted }) => wasWanted) .map(({ cid }) => cid) ) await Promise.all( received.map( async ({ cid, wasWanted, data }) => { await this._handleReceivedBlock(peerId, cid, data, wasWanted) } ) ) } async _handleReceivedBlock (peerId: PeerId, cid: CID, data: Uint8Array, wasWanted: boolean): Promise<void> { this._log('received block') const has = await this.blockstore.has(cid) this._updateReceiveCounters(peerId.toString(), cid, data, has) if (!wasWanted) { return } await this.put(cid, data) } _updateReceiveCounters (peerIdStr: string, cid: CID, data: Uint8Array, exists: boolean): void { this.stats.push(peerIdStr, 'blocksReceived', 1) this.stats.push(peerIdStr, 'dataReceived', data.length) if (exists) { this.stats.push(peerIdStr, 'dupBlksReceived', 1) this.stats.push(peerIdStr, 'dupDataReceived', data.length) } } /** * handle errors on the receiving channel */ _receiveError (err: Error): void { this._log.error('ReceiveError', err) } /** * handle new peers */ _onPeerConnected (peerId: PeerId): void { this.wm.connected(peerId) } /** * handle peers being disconnected */ _onPeerDisconnected (peerId: PeerId): void { this.wm.disconnected(peerId) this.engine.peerDisconnected(peerId) this.stats.disconnected(peerId) } enableStats (): void { this.stats.enable() } disableStats (): void { this.stats.disable() } /** * Return the current wantlist for a given `peerId` */ wantlistForPeer (peerId: PeerId, _options?: any): Map<string, WantListEntry> { return this.engine.wantlistForPeer(peerId) } /** * Return ledger information for a given `peerId` */ ledgerForPeer (peerId: PeerId): PeerLedger | undefined { return this.engine.ledgerForPeer(peerId) } /** * Fetch a given block by cid. If the block is in the local * blockstore it is returned, otherwise the block is added to the wantlist and returned once another node sends it to us. */ async want (cid: CID, options: AbortOptions & ProgressOptions<BitswapWantProgressEvents> = {}): Promise<Uint8Array> { const fetchFromNetwork = async (cid: CID, options: AbortOptions & ProgressOptions<BitswapWantProgressEvents>): Promise<Uint8Array> => { // add it to the want list - n.b. later we will abort the AbortSignal // so no need to remove the blocks from the wantlist after we have it this.wm.wantBlocks([cid], options) return this.notifications.wantBlock(cid, options) } let promptedNetwork = false const loadOrFetchFromNetwork = async (cid: CID, options: AbortOptions & ProgressOptions<BitswapWantProgressEvents>): Promise<Uint8Array> => { try { // have to await here as we want to handle ERR_NOT_FOUND const block = await this.blockstore.get(cid, options) return block } catch (err: any) { if (err.code !== 'ERR_NOT_FOUND') { throw err } if (!promptedNetwork) { promptedNetwork = true this.network.findAndConnect(cid, options) .catch((err) => { this._log.error(err) }) } // we don't have the block locally so fetch it from the network return await fetchFromNetwork(cid, options) } } // depending on implementation it's possible for blocks to come in while // we do the async operations to get them from the blockstore leading to // a race condition, so register for incoming block notifications as well // as trying to get it from the datastore const controller = new AbortController() const signal = anySignal([controller.signal, options.signal]) try { const block = await Promise.race([ this.notifications.wantBlock(cid, { ...options, signal }), loadOrFetchFromNetwork(cid, { ...options, signal }) ]) return block } finally { // since we have the block we can now abort any outstanding attempts to // fetch it controller.abort() signal.clear() } } /** * Removes the given CIDs from the wantlist independent of any ref counts. * * This will cause all outstanding promises for a given block to reject. * * If you want to cancel the want for a block without doing that, pass an * AbortSignal in to `.get` or `.getMany` and abort it. */ unwant (cids: CID[] | CID): void { const cidsArray = Array.isArray(cids) ? cids : [cids] this.wm.unwantBlocks(cidsArray) cidsArray.forEach((cid) => { this.notifications.unwantBlock(cid) }) } /** * Removes the given keys from the want list. This may cause pending promises * for blocks to never resolve. If you wish these promises to abort instead * call `unwant(cids)` instead. */ cancelWants (cids: CID[] | CID): void { this.wm.cancelWants(Array.isArray(cids) ? cids : [cids]) } /** * Put the given block to the underlying blockstore and * send it to nodes that have it in their wantlist. */ async put (cid: CID, block: Uint8Array, _options?: any): Promise<void> { await this.blockstore.put(cid, block) this.notify(cid, block) } /** * Put the given blocks to the underlying blockstore and * send it to nodes that have it them their wantlist. */ async * putMany (source: Iterable<Pair> | AsyncIterable<Pair>, options?: AbortOptions): AwaitIterable<CID> { yield * this.blockstore.putMany(forEach(source, ({ cid, block }) => { this.notify(cid, block) }), options) } /** * Sends notifications about the arrival of a block */ notify (cid: CID, block: Uint8Array, options: ProgressOptions<BitswapNotifyProgressEvents> = {}): void { this.notifications.hasBlock(cid, block) this.engine.receivedBlocks([{ cid, block }]) // Note: Don't wait for provide to finish before returning this.network.provide(cid, options).catch((err) => { this._log.error('Failed to provide: %s', err.message) }) } /** * Get the current list of wants */ getWantlist (): IterableIterator<[string, WantListEntry]> { return this.wm.wantlist.entries() } /** * Get the current list of partners */ get peers (): PeerId[] { return this.engine.peers() } /** * Start the bitswap node */ async start (): Promise<void> { this.wm.start() await this.network.start() this.engine.start() this.started = true } /** * Stop the bitswap node */ async stop (): Promise<void> { this.stats.stop() this.wm.stop() await this.network.stop() this.engine.stop() this.started = false } }