@libp2p/peer-store
Version:
Stores information about peers libp2p knows on the network
219 lines (177 loc) • 6.69 kB
text/typescript
import { NotFoundError } from '@libp2p/interface'
import { peerIdFromCID } from '@libp2p/peer-id'
import mortice, { type Mortice } from 'mortice'
import { base32 } from 'multiformats/bases/base32'
import { CID } from 'multiformats/cid'
import { MAX_ADDRESS_AGE, MAX_PEER_AGE } from './constants.js'
import { Peer as PeerPB } from './pb/peer.js'
import { bytesToPeer, pbToPeer } from './utils/bytes-to-peer.js'
import { peerEquals } from './utils/peer-equals.js'
import { NAMESPACE_COMMON, peerIdToDatastoreKey } from './utils/peer-id-to-datastore-key.js'
import { toPeerPB } from './utils/to-peer-pb.js'
import type { AddressFilter, PersistentPeerStoreComponents, PersistentPeerStoreInit } from './index.js'
import type { PeerUpdate as PeerUpdateExternal, PeerId, Peer, PeerData, PeerQuery, Logger } from '@libp2p/interface'
import type { Datastore, Key, Query } from 'interface-datastore'
/**
* Event detail emitted when peer data changes
*/
export interface PeerUpdate extends PeerUpdateExternal {
updated: boolean
}
export interface ExistingPeer {
peerPB: PeerPB
peer: Peer
}
function keyToPeerId (key: Key): PeerId {
// /peers/${peer-id-as-libp2p-key-cid-string-in-base-32}
const base32Str = key.toString().split('/')[2]
const buf = CID.parse(base32Str, base32)
return peerIdFromCID(buf)
}
function decodePeer (key: Key, value: Uint8Array, maxAddressAge: number): Peer {
const peerId = keyToPeerId(key)
return bytesToPeer(peerId, value, maxAddressAge)
}
function mapQuery (query: PeerQuery, maxAddressAge: number): Query {
return {
prefix: NAMESPACE_COMMON,
filters: (query.filters ?? []).map(fn => ({ key, value }) => {
return fn(decodePeer(key, value, maxAddressAge))
}),
orders: (query.orders ?? []).map(fn => (a, b) => {
return fn(decodePeer(a.key, a.value, maxAddressAge), decodePeer(b.key, b.value, maxAddressAge))
})
}
}
export class PersistentStore {
private readonly peerId: PeerId
private readonly datastore: Datastore
public readonly lock: Mortice
private readonly addressFilter?: AddressFilter
private readonly log: Logger
private readonly maxAddressAge: number
private readonly maxPeerAge: number
constructor (components: PersistentPeerStoreComponents, init: PersistentPeerStoreInit = {}) {
this.log = components.logger.forComponent('libp2p:peer-store')
this.peerId = components.peerId
this.datastore = components.datastore
this.addressFilter = init.addressFilter
this.lock = mortice({
name: 'peer-store',
singleProcess: true
})
this.maxAddressAge = init.maxAddressAge ?? MAX_ADDRESS_AGE
this.maxPeerAge = init.maxPeerAge ?? MAX_PEER_AGE
}
async has (peerId: PeerId): Promise<boolean> {
try {
await this.load(peerId)
return true
} catch (err: any) {
if (err.name !== 'NotFoundError') {
throw err
}
}
return false
}
async delete (peerId: PeerId): Promise<void> {
if (this.peerId.equals(peerId)) {
return
}
await this.datastore.delete(peerIdToDatastoreKey(peerId))
}
async load (peerId: PeerId): Promise<Peer> {
const key = peerIdToDatastoreKey(peerId)
const buf = await this.datastore.get(key)
const peer = PeerPB.decode(buf)
if (this.#peerIsExpired(peerId, peer)) {
await this.datastore.delete(key)
throw new NotFoundError()
}
return pbToPeer(peerId, peer, this.peerId.equals(peerId) ? Infinity : this.maxAddressAge)
}
async save (peerId: PeerId, data: PeerData): Promise<PeerUpdate> {
const existingPeer = await this.#findExistingPeer(peerId)
const peerPb: PeerPB = await toPeerPB(peerId, data, 'patch', {
addressFilter: this.addressFilter
})
return this.#saveIfDifferent(peerId, peerPb, existingPeer)
}
async patch (peerId: PeerId, data: Partial<PeerData>): Promise<PeerUpdate> {
const existingPeer = await this.#findExistingPeer(peerId)
const peerPb: PeerPB = await toPeerPB(peerId, data, 'patch', {
addressFilter: this.addressFilter,
existingPeer
})
return this.#saveIfDifferent(peerId, peerPb, existingPeer)
}
async merge (peerId: PeerId, data: PeerData): Promise<PeerUpdate> {
const existingPeer = await this.#findExistingPeer(peerId)
const peerPb: PeerPB = await toPeerPB(peerId, data, 'merge', {
addressFilter: this.addressFilter,
existingPeer
})
return this.#saveIfDifferent(peerId, peerPb, existingPeer)
}
async * all (query?: PeerQuery): AsyncGenerator<Peer, void, unknown> {
for await (const { key, value } of this.datastore.query(mapQuery(query ?? {}, this.maxAddressAge))) {
const peerId = keyToPeerId(key)
// skip self peer if present
if (peerId.equals(this.peerId)) {
continue
}
const peer = PeerPB.decode(value)
// remove expired peer
if (this.#peerIsExpired(peerId, peer)) {
await this.datastore.delete(key)
continue
}
yield pbToPeer(peerId, peer, this.peerId.equals(peerId) ? Infinity : this.maxAddressAge)
}
}
async #findExistingPeer (peerId: PeerId): Promise<ExistingPeer | undefined> {
try {
const key = peerIdToDatastoreKey(peerId)
const buf = await this.datastore.get(key)
const peerPB = PeerPB.decode(buf)
// remove expired peer
if (this.#peerIsExpired(peerId, peerPB)) {
await this.datastore.delete(key)
throw new NotFoundError()
}
return {
peerPB,
peer: bytesToPeer(peerId, buf, this.maxAddressAge)
}
} catch (err: any) {
if (err.name !== 'NotFoundError') {
this.log.error('invalid peer data found in peer store - %e', err)
}
}
}
async #saveIfDifferent (peerId: PeerId, peer: PeerPB, existingPeer?: ExistingPeer): Promise<PeerUpdate> {
// record last update
peer.updated = Date.now()
const buf = PeerPB.encode(peer)
await this.datastore.put(peerIdToDatastoreKey(peerId), buf)
return {
peer: bytesToPeer(peerId, buf, this.maxAddressAge),
previous: existingPeer?.peer,
updated: existingPeer == null || !peerEquals(peer, existingPeer.peerPB)
}
}
#peerIsExpired (peerId: PeerId, peer: PeerPB): boolean {
if (peer.updated == null) {
return true
}
if (this.peerId.equals(peerId)) {
return false
}
const expired = peer.updated < (Date.now() - this.maxPeerAge)
const minAddressObserved = Date.now() - this.maxAddressAge
const addrs = peer.addresses.filter(addr => {
return addr.observed != null && addr.observed > minAddressObserved
})
return expired && addrs.length === 0
}
}