@helia/bitswap
Version:
JavaScript implementation of the Bitswap data exchange protocol used by Helia
170 lines (146 loc) • 5.25 kB
text/typescript
/* eslint-disable max-depth */
import { DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK } from '../constants.js'
import { BlockPresenceType, WantType } from '../pb/message.js'
import { QueuedBitswapMessage } from '../utils/bitswap-message.js'
import { cidToPrefix } from '../utils/cid-prefix.js'
import type { Network } from '../network.js'
import type { ComponentLogger, Logger, PeerId } from '@libp2p/interface'
import type { Blockstore } from 'interface-blockstore'
import type { AbortOptions } from 'it-length-prefixed-stream'
import type { CID } from 'multiformats/cid'
export interface LedgerComponents {
peerId: PeerId
blockstore: Blockstore
network: Network
logger: ComponentLogger
}
export interface LedgerInit {
maxSizeReplaceHasWithBlock?: number
}
export interface PeerWantListEntry {
/**
* The CID the peer has requested
*/
cid: CID
/**
* The priority with which the remote should return the block
*/
priority: number
/**
* If we want the block or if we want the remote to tell us if they have the
* block - note if the block is small they'll send it to us anyway.
*/
wantType: WantType
/**
* Whether the remote should tell us if they have the block or not
*/
sendDontHave: boolean
/**
* If we don't have the block and we've told them we don't have the block
*/
sentDontHave?: boolean
}
export class Ledger {
public peerId: PeerId
private readonly blockstore: Blockstore
private readonly network: Network
public wants: Map<string, PeerWantListEntry>
public exchangeCount: number
public bytesSent: number
public bytesReceived: number
public lastExchange?: number
private readonly maxSizeReplaceHasWithBlock: number
private readonly log: Logger
constructor (components: LedgerComponents, init: LedgerInit) {
this.peerId = components.peerId
this.blockstore = components.blockstore
this.network = components.network
this.wants = new Map()
this.log = components.logger.forComponent(`helia:bitswap:ledger:${components.peerId}`)
this.exchangeCount = 0
this.bytesSent = 0
this.bytesReceived = 0
this.maxSizeReplaceHasWithBlock = init.maxSizeReplaceHasWithBlock ?? DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK
}
sentBytes (n: number): void {
this.exchangeCount++
this.lastExchange = (new Date()).getTime()
this.bytesSent += n
}
receivedBytes (n: number): void {
this.exchangeCount++
this.lastExchange = (new Date()).getTime()
this.bytesReceived += n
}
debtRatio (): number {
return (this.bytesSent / (this.bytesReceived + 1)) // +1 is to prevent division by zero
}
public async sendBlocksToPeer (options?: AbortOptions): Promise<void> {
const message = new QueuedBitswapMessage()
const sentBlocks = new Set<string>()
for (const [key, entry] of this.wants.entries()) {
try {
const block = await this.blockstore.get(entry.cid, options)
// do they want the block or just us to tell them we have the block
if (entry.wantType === WantType.WantHave) {
if (block.byteLength < this.maxSizeReplaceHasWithBlock) {
this.log('sending have and block for %c', entry.cid)
// if the block is small we just send it to them
sentBlocks.add(key)
message.addBlock(entry.cid, {
data: block,
prefix: cidToPrefix(entry.cid)
})
} else {
this.log('sending have for %c', entry.cid)
// otherwise tell them we have the block
message.addBlockPresence(entry.cid, {
cid: entry.cid.bytes,
type: BlockPresenceType.HaveBlock
})
}
} else {
this.log('sending block for %c', entry.cid)
// they want the block, send it to them
sentBlocks.add(key)
message.addBlock(entry.cid, {
data: block,
prefix: cidToPrefix(entry.cid)
})
}
} catch (err: any) {
if (err.code !== 'ERR_NOT_FOUND') {
throw err
}
this.log('do not have block for %c', entry.cid)
// we don't have the requested block and the remote is not interested
// in us telling them that
if (!entry.sendDontHave) {
continue
}
// we have already told them we don't have the block
if (entry.sentDontHave === true) {
continue
}
entry.sentDontHave = true
message.addBlockPresence(entry.cid, {
cid: entry.cid.bytes,
type: BlockPresenceType.DontHaveBlock
})
}
}
// only send the message if we actually have something to send
if (message.blocks.size > 0 || message.blockPresences.size > 0) {
this.log('sending message')
await this.network.sendMessage(this.peerId, message, options)
this.log('sent message')
// update accounting
this.sentBytes([...message.blocks.values()].reduce((acc, curr) => acc + curr.data.byteLength, 0))
// remove sent blocks from local copy of their want list - they can still
// re-request if required
for (const key of sentBlocks) {
this.wants.delete(key)
}
}
}
}