@helia/bitswap
Version:
JavaScript implementation of the Bitswap data exchange protocol used by Helia
135 lines (106 loc) • 4.22 kB
text/typescript
/* eslint-disable complexity */
import { CodeError } from '@libp2p/interface'
import { encodingLength } from 'uint8-varint'
import { BitswapMessage, Block, BlockPresence, WantlistEntry } from '../pb/message.js'
import type { QueuedBitswapMessage } from './bitswap-message.js'
/**
* https://github.com/ipfs/kubo/issues/4473#issuecomment-350390693
*/
export const MAX_BLOCK_SIZE = 4193648
const MAX_ENCODED_BLOCK_SIZE = MAX_BLOCK_SIZE + 16
/**
* Split the passed Bitswap message into multiple smaller messages that when
* serialized will be under the maximum message size.
*
* Since blocks are the largest thing to send, we first try to fit as many
* blocks as possible into the message, then add (smaller) block presences and
* wants until the max size is reached.
*
* If a block is encountered that is larger than the max message size an error
* will be thrown.
*/
export function * splitMessage (message: QueuedBitswapMessage, maxSize: number): Generator<Uint8Array> {
const wantListEntries = [...message.wantlist.values()]
const blockPresences = [...message.blockPresences.values()]
const blocks = [...message.blocks.values()]
let wantListIndex = 0
let blockPresencesIndex = 0
let blocksIndex = 0
let doneSending = false
while (true) {
const subMessage: Required<BitswapMessage> = {
wantlist: {
full: message.full ?? false,
entries: []
},
blockPresences: [],
blocks: [],
pendingBytes: 0
}
let size = BitswapMessage.encode(subMessage).byteLength
let { added, hasMore, newSize } = addToMessage(blocks, subMessage.blocks, blocksIndex, maxSize, size, calculateEncodedBlockSize)
blocksIndex += added
size = newSize
const haveMoreBlocks = hasMore
;({ added, hasMore, newSize } = addToMessage(blockPresences, subMessage.blockPresences, blockPresencesIndex, maxSize, size, calculateEncodedBlockPresenceSize))
blockPresencesIndex += added
size = newSize
const haveMorePresences = hasMore
;({ added, hasMore, newSize } = addToMessage(wantListEntries, subMessage.wantlist.entries, wantListIndex, maxSize, size, calculateEncodedWantlistEntrySize))
wantListIndex += added
size = newSize
const haveMoreWantlistEntries = hasMore
doneSending = !haveMoreBlocks && !haveMorePresences && !haveMoreWantlistEntries
// if we're sending multiple messages this is no longer the full wantlist
if (!doneSending) {
subMessage.wantlist.full = false
}
yield BitswapMessage.encode(subMessage)
if (doneSending) {
break
}
}
}
interface AddResult {
hasMore: boolean
added: number
newSize: number
}
function addToMessage <T> (input: T[], output: T[], start: number, maxSize: number, size: number, calculateSize: (arg: T) => number): AddResult {
let added = 0
let hasMore = false
// try to send as many blocks as possible
for (let i = start; i < input.length; i++) {
const item = input[i]
const itemSize = calculateSize(item)
if (itemSize > MAX_ENCODED_BLOCK_SIZE) {
throw new CodeError('Cannot send block as after encoding it is over the max message size', 'ERR_BLOCK_TOO_LARGE')
}
const newSize = size + itemSize
if (newSize > maxSize) {
hasMore = true
break
}
output.push(item)
added++
size = newSize
}
return { hasMore, added, newSize: size }
}
function calculateEncodedBlockSize (block: Block): number {
// 3 is the "blocks" field number in message.proto
return calculateLength(3, Block.encode(block))
}
function calculateEncodedBlockPresenceSize (blockPresence: BlockPresence): number {
// 4 is the "blockPresences" field number in message.proto
return calculateLength(4, BlockPresence.encode(blockPresence))
}
function calculateEncodedWantlistEntrySize (entry: WantlistEntry): number {
// 1 is the "entries" field number in message.proto
return calculateLength(1, WantlistEntry.encode(entry))
}
function calculateLength (fieldNumber: number, data: Uint8Array): number {
const fieldNumberLength = encodingLength(fieldNumber)
const dataLengthLength = encodingLength(data.byteLength)
return fieldNumberLength + dataLengthLength + data.byteLength
}