miniswap
Version:
Miniature Bitswap implementation, remote read only.
280 lines (240 loc) • 7.11 kB
JavaScript
import { CID } from 'multiformats/cid'
import * as gen from './gen/message.js'
import { encode as encodePrefix } from './prefix.js'
const MAX_PRIORITY = Math.pow(2, 31) - 1
const MAX_MESSAGE_SIZE = 4 * 1024 * 1024 // 4 MB
/**
* Overhead added to the message when either block and blockPresence are non
* empty.
* - 2 is the size of the varint used to declare the new embedded messages
* - 8 (4 x 2) is the size of the varint used to declare embedded messages
* payload when the total message size is 4 MB
*/
const NON_EMPTY_OVERHEAD = 2 + 8
/**
* Overhead added by a new block (without considering the data field size)
* - 1 is the size of the varint which declares the new embedded message
* - 1 is the size of the varint which declares the prefix field
* - 4 is the size of the varint used to declare the data field message
* payload when the total message size is 4 MB
* - 4 is the size of the CI prefix
*/
const NEW_BLOCK_OVERHEAD = 1 + 1 + 4 + 4
/**
* overhead added by a new presence (without considering the cid field size)
* - 1 is the varint which declare the new embedded message
* - 1 is the varint which declare the cid field
* - 1 is the varint which declare the type field
* - 1 is the varint of the type field value
*/
const NEW_PRESENCE_OVERHEAD = 1 + 1 + 1 + 1
/**
* An arbitrary percentage added to minimize the probability of false negatives
* since this is an estimated algorithm.
*/
const ADDRED_ESTIMATION_PERCENTAGE = 0.1
export class Entry {
/**
* @param {CID} cid
* @param {number} priority
* @param {boolean} cancel
* @param {gen.Message.Wantlist.WantType} wantType
* @param {boolean} sendDontHave
*/
constructor (cid, priority, cancel, wantType, sendDontHave) {
this.cid = cid
this.priority = priority
this.cancel = Boolean(cancel)
this.wantType = wantType
this.sendDontHave = Boolean(sendDontHave)
if (this.priority == null || this.priority < 0) {
this.priority = 1
} else if (this.priority > MAX_PRIORITY) {
this.priority = MAX_PRIORITY
}
if (!gen.Message.Wantlist.WantType[this.wantType]) {
this.wantType = 0
}
}
/**
* @param {gen.Message.Wantlist.Entry} raw
*/
static fromRaw (raw) {
const wantType = raw.wantType
const sendDontHave = raw.sendDontHave
const cid = CID.decode(raw.block)
return new Entry(cid, raw.priority, raw.cancel, wantType, sendDontHave)
}
serialize () {
const { cid, priority, cancel, wantType, sendDontHave } = this
return {
block: cid.bytes,
priority,
cancel,
wantType,
sendDontHave
}
}
encode () {
return gen.Message.Wantlist.Entry.encode(this.serialize()).finish()
}
}
export class Wantlist {
/**
* @param {Object} [options]
* @param {Entry[]} [options.entries]
* @param {boolean} [options.full]
*/
constructor ({ entries, full } = {}) {
this.entries = entries || []
this.full = Boolean(full)
}
/**
* @param {gen.Message.Wantlist} raw
*/
static fromRaw (raw) {
return new Wantlist({
entries: raw.entries.map(e => Entry.fromRaw(e)),
full: raw.full
})
}
serialize () {
return {
entries: this.entries.map(e => e.serialize()),
full: this.full
}
}
encode () {
return gen.Message.Wantlist.encode(this.serialize()).finish()
}
}
export class Block {
/**
* @param {Uint8Array|CID} prefixOrCid
* @param {Uint8Array} data
*/
constructor (prefixOrCid, data) {
if (prefixOrCid instanceof CID) {
prefixOrCid = encodePrefix(prefixOrCid)
}
this.prefix = prefixOrCid
this.data = data
}
/**
* @param {gen.Message.Block} raw
*/
static fromRaw (raw) {
return new Block(raw.prefix, raw.data)
}
serialize () {
return { prefix: this.prefix, data: this.data }
}
encode () {
return gen.Message.Block.encode(this.serialize()).finish()
}
}
export class BlockPresence {
/**
* @param {CID} cid
* @param {gen.Message.BlockPresenceType} type
*/
constructor (cid, type) {
this.cid = cid
this.type = type
if (!gen.Message.BlockPresenceType[this.type]) {
this.type = 0
}
}
/**
* @param {gen.Message.BlockPresence} raw
*/
static fromRaw (raw) {
return new BlockPresence(CID.decode(raw.cid), raw.type)
}
serialize () {
return { cid: this.cid.bytes, type: this.type }
}
encode () {
return gen.Message.BlockPresence.encode(this.serialize()).finish()
}
}
/**
* As specified in the constants above, each Message can be 4MB maximum (after
* serialization).
* Each block can be at most 2 MB.
* Each CID is roughly 40 byte.
*/
export class Message {
/**
* @param {Object} [options]
* @param {Wantlist} [options.wantlist]
* @param {Block[]} [options.blocks]
* @param {BlockPresence[]} [options.blockPresences]
* @param {number} [options.pendingBytes]
*/
constructor ({ wantlist, blocks, blockPresences, pendingBytes } = {}) {
this.wantlist = wantlist || new Wantlist()
this.blocks = blocks || []
this.blockPresences = blockPresences || []
this.pendingBytes = pendingBytes || 0
// Validate pendingBytes
if (this.pendingBytes < 0) {
this.pendingBytes = 0
}
this.estimatedLength = this.encode().length + NON_EMPTY_OVERHEAD
}
/**
* @param {Uint8Array} encoded
*/
static decode (encoded) {
const decoded = gen.Message.decode(encoded)
return new Message({
wantlist: Wantlist.fromRaw(decoded.wantlist),
blocks: decoded.payload.map(b => Block.fromRaw(b)),
blockPresences: decoded.blockPresences.map(b => BlockPresence.fromRaw(b)),
pendingBytes: decoded.pendingBytes
})
}
serialize () {
const { wantlist, blocks, blockPresences } = this
return {
wantlist: wantlist.serialize(),
payload: blocks.map(b => b.serialize()),
blockPresences: blockPresences.map(b => b.serialize()),
pendingBytes: this.pendingBytes * Number.MAX_SAFE_INTEGER
}
}
encode () {
return gen.Message.encode(this.serialize()).finish()
}
/**
* @param {Block} block
*/
addBlock (block) {
// Use module.exports here so that tests can eventually override
const newBlockSize = NEW_BLOCK_OVERHEAD + block.data.length
if (this.estimateNewSizeAfter(newBlockSize) > MAX_MESSAGE_SIZE) {
return false
}
this.blocks.push(block)
this.estimatedLength += newBlockSize
return true
}
/**
* @param {BlockPresence} presence
*/
addBlockPresence (presence) {
const newPresenceSize = NEW_PRESENCE_OVERHEAD + presence.cid.byteLength
if (this.estimateNewSizeAfter(newPresenceSize) > MAX_MESSAGE_SIZE) {
return false
}
this.blockPresences.push(presence)
this.estimatedLength += newPresenceSize
return true
}
estimateNewSizeAfter (newElement) {
return (this.estimatedLength + newElement) * (1 + ADDRED_ESTIMATION_PERCENTAGE)
}
}
export const { WantType } = gen.Message.Wantlist
export const { BlockPresenceType } = gen.Message