webpeerjs
Version:
WebPEER.js is Decentralized P2P network overlay inside browser.
1,739 lines (1,467 loc) • 61.4 kB
JavaScript
//! WebPEER.js -- https://github.com/nuzulul/webpeerjs
import * as config from './config'
import {
mkErr,
PBPeer,
uint8ArrayToString,
uint8ArrayFromString,
first,
Key,
msgIdFnStrictNoSign,
metrics,
//getDigest,
mkDebug,
multiaddr,
pipe,
lp,
map
} from './utils'
import { createDelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client'
import { createLibp2p } from 'libp2p'
import { IDBDatastore } from 'datastore-idb'
import { webTransport } from '@libp2p/webtransport'
import { webSockets } from '@libp2p/websockets'
import { webRTC } from '@libp2p/webrtc'
import { dcutr } from '@libp2p/dcutr'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery'
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { identify, identifyPush } from '@libp2p/identify'
import { peerIdFromString } from '@libp2p/peer-id'
import { kadDHT, removePrivateAddressesMapper } from '@libp2p/kad-dht'
import { simpleMetrics } from '@libp2p/simple-metrics'
class webpeerjs{
//libp2p instance
#libp2p
//map [id,addrs] of discovered peers (addrs is array of address)
#discoveredPeers
//array of all webpeers id has been found
#webPeersId
//map [id,addrs]
#webPeersAddrs
//database of best peers has been found
#dbstore
#dbstoreData
//map of [id,number_of_dialed] of good peers on #connectionTracker
#dialedGoodPeers
//boolean is dial websocket
#isDialWebsocket
//map [id,mddrs] of dialed bootstrap address
#dialedKnownBootstrap
//array of dialed discovered peers id
//#dialedDiscoveredPeers
//object from joinRoom()
#rooms
//map [id,addrs] of webpeers currently connected (addrs is array of address)
#connectedPeers
//array of we peers id proxy of #connectedPeers
#connectedPeersArr
//map [id,number_of_dialed] of #connectionTracker object store
#connectionTrackerStore
//map [id,addr] of all peers connections (addr is string of address)
#connections
//track disconnect event
#trackDisconnect
//list of dial multiaddress queue
#dialQueue
//is dial enabled
#isDialEnabled
//message tracker avoid double
#msgIdtracker
//inbound message time tracker
#msgTimeTracker
//map of peer exchange data
#peerexchangedata
//track last connect to webpeer network
#lastTimeConnectToNetwork
#lastTimeReceiveData
//arr to track on connect event
#onConnectQueue
//callback to websocket dialable
#onWebsocketFn
//time tracker for sending message
#sendMessageTimeTracker
id
status
IPFS
address
peers
constructor(libp2p,dbstore,onMetrics,onWebsocketFn,onDialFn){
this.#libp2p = libp2p
this.#dbstore = dbstore
this.#onWebsocketFn = onWebsocketFn
this.#dbstoreData = new Map()
this.#discoveredPeers = new Map()
this.#webPeersId = []
this.#webPeersAddrs = new Map()
this.#dialedGoodPeers = new Map()
this.#isDialWebsocket = false
this.#dialedKnownBootstrap = new Map()
//this.#dialedDiscoveredPeers = []
this.address = []
this.#rooms = {}
this.#connectedPeers = new Map()
this.#connectedPeersArr = []
this.#connectionTrackerStore = new Map()
this.#connections = new Map()
this.#trackDisconnect = new Map()
this.#dialQueue = []
this.#isDialEnabled = true
this.#msgIdtracker = new Map()
this.#peerexchangedata = new Map()
this.#lastTimeConnectToNetwork = new Date().getTime()
this.#lastTimeReceiveData = new Date().getTime()
this.#onConnectQueue = []
this.#msgTimeTracker = new Map()
this.#sendMessageTimeTracker = 0
this.peers = (function(f) {
return f
})(this.#connectedPeersArr);
this.status = (function(libp2p) {
return libp2p.status
})(this.#libp2p);
this.status = 'unconnected'
this.IPFS = (function(libp2p,discoveredPeers) {
const obj = {libp2p,discoveredPeers}
return obj
})(this.#libp2p,this.#discoveredPeers);
this.id = this.#libp2p.peerId.toString()
for(const topic of config.CONFIG_PUPSUB_PEER_DATA){
this.#libp2p.services.pubsub.subscribe(topic)
}
for(const topic of config.CONFIG_PUBSUB_PEER_DISCOVERY_HYBRID){
this.#libp2p.services.pubsub.subscribe(topic)
}
//listen to peer connect event
this.#libp2p.addEventListener("peer:connect",async (evt) => {
//const connection = evt.detail;
const id = evt.detail.toString()
//console.log('peer:connect '+id,evt)
const connections = this.#libp2p.getConnections().map((con)=>{return {id:con.remotePeer.toString(),addr:con.remoteAddr.toString()}})
const connect = connections.find((con)=>con.id == id)
const addr = connect.addr
if(config.CONFIG_KNOWN_BOOTSTRAP_PEERS_IDS.includes(id)){
if((!this.#connections.has(id) || (this.#connections.get(id).includes('/wss/')))&&addr.includes('webtransport')){
await this.#dbstore.put(new Key(id), new TextEncoder().encode(addr))
}
}
this.#connections.set(id,addr)
//required by joinRoom version 1 to announce via universal connectivity
if(config.CONFIG_KNOWN_BOOTSTRAP_HYBRID_IDS.includes(id)){
setTimeout(()=>{
this.#peerDiscoveryHybrid()
this.#announce()
setTimeout(()=>{
this.#peerDiscoveryHybrid()
this.#announce()
},5000)
},1000)
}
if(this.#webPeersId.includes(id)){
setTimeout(()=>{
this.#ping()
},10000)
setTimeout(()=>{
this.#ping()
},15000)
let address = [addr]
if(this.#connectedPeers.has(id)){
if(this.#lastTimeReceiveData < 10*1000){
//reset this last seen
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(id,metadata)
}
}
else {
if(this.#lastTimeReceiveData < 10*1000){
//add to connected webpeers
this.#onConnectFnUpdate(id)
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(id,metadata)
this.#updatePeers()
}
}
}
});
//listen message from subscribed pupsub topic
this.#libp2p.services.pubsub.addEventListener('message', event => {
//console.log('on:'+event.detail.topic,event.detail.data)
//console.log('from '+event.detail.from.toString(),event)
//console.log('ontopic:'+event.detail.topic)
if (event.detail.type !== 'signed') {
return
}
if(config.CONFIG_JOIN_ROOM_VERSION == 1){
const topic = event.detail.topic
const senderPeerId = event.detail.from.toString()
try{
if(topic === config.CONFIG_PUPSUB_PEER_DATA || config.CONFIG_PUBSUB_PEER_DISCOVERY_WEBPEER.includes(topic) || config.CONFIG_RUN_ON_TRANSIENT_CONNECTION){
this.#lastTimeReceiveData = new Date().getTime()
}
//track last connect to webpeer network
if(config.CONFIG_PUBSUB_PEER_DISCOVERY_WEBPEER.includes(topic)){
const now = new Date().getTime()
const limit = 15*1000
const lastConnectTime = now - this.#lastTimeConnectToNetwork
if(lastConnectTime > limit){
//console.log('need re announce')
this.#announce()
}
this.#lastTimeConnectToNetwork = now
}
//if it is webpeer
if(this.#webPeersId.includes(senderPeerId)){
if(this.#connectedPeers.has(senderPeerId)){
if(this.#lastTimeReceiveData < 10*1000){
//reset this last seen
const address = this.#connectedPeers.get(senderPeerId).addrs
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(senderPeerId,metadata)
}
}
else {
if(this.#lastTimeReceiveData < 10*1000){
//add to connected webpeers
this.#onConnectFnUpdate(senderPeerId)
const address = this.#webPeersAddrs.get(senderPeerId)
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(senderPeerId,metadata)
this.#updatePeers()
}
}
//dial if not connected
if(!this.#isConnected(senderPeerId)){
if(this.#connections.has(senderPeerId)){
let mddrs = []
const addr = this.#connections.get(senderPeerId)
const mddr = multiaddr(addr)
mddrs.push(mddr)
this.#dialMultiaddress(mddrs)
}
else if(this.#discoveredPeers.has(senderPeerId)){
const addrs = this.#discoveredPeers.get(senderPeerId)
let mddrs = []
for(const addr of addrs){
if(!addr.includes('webrtc'))continue
const mddr = multiaddr(addr)
mddrs.push(mddr)
}
this.#dialMultiaddress(mddrs)
}
else if(this.#connectedPeers.has(senderPeerId)){
const addrs = this.#connectedPeers.get(senderPeerId).addrs
let mddrs = []
for(const addr of addrs){
if(!addr.includes('webrtc'))continue
const mddr = multiaddr(addr)
mddrs.push(mddr)
}
this.#dialMultiaddress(mddrs)
}
}
}
//parse the message over pupsub peer discovery
const peer = PBPeer.decode(event.detail.data)
//detect libp2p pupsub peer discovery
if(peer.addrs.length > 1 && this.#libp2p.getPeers().length < config.CONFIG_MAX_CONNECTIONS){
let mddrs = []
for(const uaddr of peer.addrs){
const mddr = multiaddr(uaddr)
if(mddr.toString().includes('webtransport') && mddr.toString().includes('certhash')){
mddrs.push(mddr)
}
}
this.#dialMultiaddress(mddrs)
//console.log('dial '+senderPeerId,mddrs.toString())
}
const msg = uint8ArrayToString(peer.addrs[0])
const json = JSON.parse(msg)
const prefix = json.prefix
const room = json.room
const rooms = json.rooms
const message = json.message
const msgId = json.msgId
const signal = json.signal
const id = json.id
const address = json.address
//console.log(`from ${id}:${signal} = ${msg}`)
if(id != senderPeerId)return
//detect special webpeer identity
if(prefix === config.CONFIG_PREFIX){
this.#lastTimeReceiveData = new Date().getTime()
//add to webpeers id
if(!this.#webPeersId.includes(id))this.#webPeersId.push(id)
//add to connected webpeers
if(!this.#connectedPeers.has(id)){
this.#onConnectFnUpdate(id)
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(id,metadata)
if(address){
if(address.length > 0){
this.#webPeersAddrs.set(id,address)
}
}
this.#updatePeers()
}else{
//reset this last seen
let addr = this.#connectedPeers.get(id).addrs
if(address)addr = address
const now = new Date().getTime()
const metadata = {addrs:addr,last:now}
this.#connectedPeers.set(id,metadata)
//update address
if(address){
if(address.length > 0){
this.#webPeersAddrs.set(id,address)
}
}
}
if(room){
if(this.#rooms[room]){
//update room members
if(!this.#rooms[room].members.includes(id)){
if(this.#connectedPeers.has(id)){
this.#rooms[room].members.push(id)
this.#rooms[room].onMembers(this.#rooms[room].members)
}
}
//inbound message
//use #msgIdtracker to prevent double message
//use #msgTimeTracker to limit inbound message 1msg/s
if(message){
const msgID = msgId+id
let oldmsgtime = 0
let newmsgtime = new Date().getTime()
if(this.#msgTimeTracker.has(id))oldmsgtime = this.#msgTimeTracker.get(id)
const msgtimelimit = 1000
if(!this.#msgIdtracker.has(msgID) && newmsgtime-oldmsgtime>msgtimelimit){
this.#msgIdtracker.set(msgID,newmsgtime)
this.#msgTimeTracker.set(id,newmsgtime)
this.#rooms[room].onMessage(message,id)
}
}
}
}
if(rooms){
for(const room of rooms){
if(this.#rooms[room]){
//update room members
if(!this.#rooms[room].members.includes(id)){
if(this.#connectedPeers.has(id)){
this.#rooms[room].members.push(id)
this.#rooms[room].onMembers(this.#rooms[room].members)
}
}
}
}
}
if(signal){
//repply announce with ping
if(signal == 'announce'){
setTimeout(()=>{this.#ping('')},1000)
//console.log('rooms',rooms)
}
if(signal == 'ping'){
//console.log('rooms',rooms)
}
}
}
else if(prefix === 'hybrid'){
//console.log('hybrid' +id,address)
const limit = config.CONFIG_MAX_CONNECTIONS / 2
if(address.length>0 && ((!this.#connections.has(id))||(this.#connectedPeers.size < limit))){
let mddrs = []
for(const addr of address){
const mddr = multiaddr(addr)
mddrs.push(mddr)
}
this.#dialMultiaddress(mddrs)
//console.log('dial '+id,mddrs.toString())
}
}
}catch(err){
//console.log('from '+event.detail.from.toString())
mkDebug(err)
}
}
})
//listen to peer discovery event
this.#libp2p.addEventListener('peer:discovery', (evt) => {
//console.log('Discovered:', evt.detail.id.toString())
//console.log('Discovered: '+evt.detail.id.toString(), evt.detail.multiaddrs.toString())
//save peer discover
const multiaddrs = evt.detail.multiaddrs
const id = evt.detail.id
if(multiaddrs.length != 0){
let addrs = []
for(const addr of multiaddrs){
let peeraddr
if(multiaddrs.toString().includes(evt.detail.id.toString())){
//console.log('Discovered:', evt.detail.multiaddrs.toString())
//peer from pupsub peer discovery already has included self id
peeraddr = addr.toString()
}
else{
//other need to add Id
peeraddr = addr.toString()+'/p2p/'+id
}
addrs.push(peeraddr)
}
//save the new format multiaddrs
this.#discoveredPeers.set(id.toString(), addrs)
//track if peer come from relay then dial it because there is a chance it is from other browser node
if(multiaddrs.toString().includes('certhash')&& multiaddrs.toString().includes('webtransport') && multiaddrs.toString().includes('p2p-circuit')){
//console.log(addrs)
if(!this.#connections.has(id)){
let mddrs = []
for(const addr of addrs){
if(!addr.includes('webrtc'))continue
const mddr = multiaddr(addr)
mddrs.push(mddr)
}
this.#dialMultiaddress(mddrs)
}
}
}
else{
//peer with empty address (multiaddrs = [])
//this.#discoveredPeers.set(id.toString(), multiaddrs)
}
})
//listen to peer disconnect event
this.#libp2p.addEventListener("peer:disconnect",async (evt) => {
//const connection = evt.detail;
//console.log(`Disconnected from ${connection.toCID().toString()}`);
const id = evt.detail.string
//track disconnect event
if(this.#trackDisconnect.has(id)){
let count = this.#trackDisconnect.get(id)
count++
this.#trackDisconnect.set(id,count)
//console.log(this.#trackDisconnect)
if(count>5){
if(config.CONFIG_KNOWN_BOOTSTRAP_PUBLIC_IDS.includes(id)){
return
}
}
else if(count>10){
if(this.#dbstoreData.has(id)){
this.#dbstoreData.delete(id)
}
if(!this.#webPeersId.includes(id) && !this.#dialedKnownBootstrap.has(id)){
return
}
}
}
else{
this.#trackDisconnect.set(id,0)
}
let peerexchangelist = []
for(const peer of this.#peerexchangedata.values()){
peerexchangelist.push(peer.id)
}
//if this disconnected peer is web peer redial it
if(this.#webPeersId.includes(id)){
const addr = this.#connections.get(id)
let mddrs = []
const mddr = multiaddr(addr)
mddrs.push(mddr)
this.#dialMultiaddress(mddrs)
}
//if this disconnected peer is known bootstrap redial it
else if(this.#dialedKnownBootstrap.has(id)){
const addr = this.#connections.get(id)
if(addr.includes('/wss/'))return
let mddrs = []
const addrs = multiaddr(addr)
mddrs.push(addrs)
this.#dialMultiaddress(mddrs)
}
else if(peerexchangelist.includes(id)){
const addr = this.#connections.get(id)
let mddrs = []
const addrs = multiaddr(addr)
mddrs.push(addrs)
this.#dialMultiaddress(mddrs)
}
//redial if this disconnected peer is regular peer
else{
const addr = this.#connections.get(id)
let mddrs = []
const addrs = multiaddr(addr)
mddrs.push(addrs)
//this.#dialMultiaddress(mddrs)
}
});
//listen to self peer update
this.#libp2p.addEventListener('self:peer:update', ({ detail: { peer } }) => {
//const multiaddrs = peer.addresses.map(({ multiaddr }) => multiaddr)
//console.log(`changed multiaddrs: peer ${peer.id.toString()} multiaddrs: ${multiaddrs}`)
const id = peer.id.toString()
const addrs = []
peer.addresses.forEach((address)=>{
const addr = address.multiaddr.toString()+'/p2p/'+id
if(addr.includes('webtransport') && addr.includes('certhash')){
addrs.push(addr)
}
})
if(!config.CONFIG_RUN_ON_TRANSIENT_CONNECTION)addrs.reverse()
//this.#ListenAddressChange(addrs)
this.address = addrs
this.#ping()
this.#peerDiscoveryHybrid()
})
this.#libp2p.addEventListener('peer:identify', async (evt) => {
//console.log('peer:identify '+evt.detail.peerId.toString(),evt.detail)
const id = evt.detail.peerId.toString()
if(config.CONFIG_KNOWN_BOOTSTRAP_PUBLIC_IDS.includes(id)){
const remoteAddr = evt.detail.connection.remoteAddr.toString()
if(remoteAddr.includes('/wss/')){
let addrs = []
let mddrs = []
for(const peer of evt.detail.listenAddrs){
if(!peer.toString().includes('webtransport'))continue
const addr = peer.toString()+'/p2p/'+id
const mddr = multiaddr(addr)
addrs.push(addr)
mddrs.push(mddr)
}
this.#dialedKnownBootstrap.set(id,addrs)
await this.#libp2p.hangUp(peerIdFromString(id))
this.#dialMultiaddress(mddrs)
}
}
if(evt.detail.protocols.includes(config.CONFIG_PROTOCOL)){
//console.log('peer:identify '+evt.detail.peerId.toString(),evt)
const id = evt.detail.peerId.toString()
let address = []
let mddrs = []
for(const addrs of evt.detail.listenAddrs){
const addr = addrs.toString()+'/p2p/'+id
const mddr = multiaddr(addr)
if(addr.includes('webtransport')){
address.push(addr)
mddrs.push(mddr)
}
}
if(this.#connectedPeers.has(id)){
//reset this last seen
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(id,metadata)
}
const command = 'peer-exchange'
this.#dialProtocol(id,command)
}
})
//dial known peers from configuration
this.#dialKnownPeers()
//watch connection every 30s if none dial known peers again from configuration
this.#watchConnection()
//if found good peers save to storage and reconnect if disconnect
this.#connectionTracker()
//periodically dial saved bootstrap address if disconnect
this.#dialRandomBootstrap()
//dial random discovered peers
//this.#dialdiscoveredpeers()
this.#registerProtocol()
onMetrics((data)=>{
const signal = metrics(data)
this.#isDialEnabled = signal.isDialEnabled
onDialFn(signal.isAutoDialEnabled)
})
setTimeout(()=>{
this.#dialQueueList()
setInterval(()=>{
this.#dialQueueList()
},5e3)
},10e3)
setInterval(()=>{
this.#trackLastSeen()
},5e3)
setInterval(()=>{
this.#peerDiscoveryHybrid()
this.#trackHybridPeersConnection()
this.#trackWebpeerConnection()
this.#garbageCollectionMsgIdTracker()
},10e3)
/*setTimeout(async()=>{
try{
//console.log('getClosestPeers')
const digest = await getDigest()
//console.log('digest',digest)
for await (const event of this.#libp2p.services.aminoDHT.getClosestPeers(digest)){
if (event.name === 'FINAL_PEER'){
//event.peer.multiaddrs.forEach((ma) => {
//console.log(event.peer.id.toString(),ma.toString())
//})
//console.log(event.peer.id.toString(),event.peer.multiaddrs.toString())
}
}
}
catch(err){
console.error('query error', err)
}
},60e3)*/
/*setTimeout(async()=>{
const key = uint8ArrayFromString(config.CONFIG_PREFIX)
const value = uint8ArrayFromString(this.id)
for await (const event of this.#libp2p.services.aminoDHT.put(key,value)){
console.log('put',event)
}
},30e3)*/
}
/*
PUBLIC FUNCTION
*/
//Listen on new peer connection
#onConnectFn = () => {}
onConnect = f => (this.#onConnectFn = f)
//Listen on peer disconnect
#onDisconnectFn = () => {}
onDisconnect = f => (this.#onDisconnectFn = f)
joinRoom = room => {
if (this.#rooms[room]) {
return [
this.#rooms[room].sendMessage,
this.#rooms[room].listenMessage,
this.#rooms[room].onMembersChange
]
}
if (!room) {
throw mkErr('room is required')
}
//join room version 1 user pupsub via pupsub peer discovery
if(config.CONFIG_JOIN_ROOM_VERSION == 1){
let topics = []
if(config.CONFIG_RUN_ON_TRANSIENT_CONNECTION == true){
topics = config.CONFIG_PUBSUB_PEER_DISCOVERY_HYBRID
}
else{
topics = config.CONFIG_PUPSUB_PEER_DATA
}
this.#rooms[room] = {
onMessage : () => {},
listenMessage : f => (this.#rooms[room] = {...this.#rooms[room], onMessage: f}),
sendMessage : async (message) => {
const now = (new Date()).getTime()
if(now-this.#sendMessageTimeTracker < 1000){
throw mkErr('can not send more than 1 message/s')
}
this.#sendMessageTimeTracker = now
const msgId = (new Date()).getTime()
const data = JSON.stringify({prefix:config.CONFIG_PREFIX,room,message,id:this.#libp2p.peerId.toString(),msgId})
const arr = uint8ArrayFromString(data)
const sizelimit = config.CONFIG_MESSAGE_SIZE_LIMIT
if(arr.byteLength > sizelimit){
throw mkErr('message too large')
}
const peer = {
publicKey: this.#libp2p.peerId.publicKey,
addrs: [arr],
}
const encodedPeer = PBPeer.encode(peer)
for(const topic of topics){
await this.#libp2p.services.pubsub.publish(topic, encodedPeer)
}
},
members : [this.id],
onMembers : () => {},
onMembersChange : f => {this.#rooms[room] = {...this.#rooms[room], onMembers: f};this.#rooms[room].onMembers(this.#rooms[room].members);this.#ping()},
}
}
return [
this.#rooms[room].sendMessage,
this.#rooms[room].listenMessage,
this.#rooms[room].onMembersChange
]
}
dial(addr){
let mddrs = []
const mddr = multiaddr(addr)
mddrs.push(mddr)
this.#dialMultiaddress(mddrs)
}
plugin(callback){
callback(this)
}
/*
PRIVATE FUNCTION
*/
#garbageCollectionMsgIdTracker(){
const limit = 60*1000
for(const msg of this.#msgIdtracker){
if(msg[1] > limit){
this.#msgIdtracker.delete(msg[0])
}
}
}
//track hybrid peer connection and try to dial if not connected at least 1
#trackHybridPeersConnection(){
let isConnectedToHybridPeers = false
for(const id of config.CONFIG_KNOWN_BOOTSTRAP_HYBRID_IDS){
if(this.#isConnected(id))isConnectedToHybridPeers = true
}
if(!isConnectedToHybridPeers){
for(const id of config.CONFIG_KNOWN_BOOTSTRAP_HYBRID_IDS){
if(this.status !== 'connected' && this.#connections.has(id))
{
let mddrs = []
const addr = this.#connections.get(id)
const mddr = multiaddr(addr)
mddrs.push(mddr)
this.#dialMultiaddress(mddrs)
}
}
}
}
//track webpeer connection and try to dial if not connected to network using saved webpeer address
#trackWebpeerConnection(){
if(this.status === 'connected')return
if(this.#webPeersAddrs.size < 1)return
const keys = Array.from(this.#webPeersAddrs.keys())
const randomKey = Math.floor(Math.random() * keys.length)
const key = keys[randomKey]
const addrs = this.#webPeersAddrs.get(key)
let mddrs = []
for(const addr of addrs){
const mddr = multiaddr(addr)
mddrs.push(mddr)
}
this.#dialMultiaddress(mddrs)
}
//prevent double on connect event
#onConnectFnUpdate(id){
if(!this.#onConnectQueue.includes(id)){
this.#onConnectQueue.push(id)
this.#onConnectFn(id)
setTimeout(()=>{
const index = this.#onConnectQueue.indexOf(id)
if (index > -1) {
this.#onConnectQueue.splice(index, 1)
}
},5000)
}
}
#updatePeers(){
this.#connectedPeersArr.length = 0
for(const peer of this.#connectedPeers){
const item = {id:peer[0],address:peer[1].addrs}
this.#connectedPeersArr.push(item)
}
if(this.#connectedPeers.size > 0){
this.status = 'connected'
}
else{
this.status = 'unconnected'
}
this.#ping()
}
async #registerProtocol(){
const handler = async ({ connection, stream }) => {
try{
const output = await pipe(
stream.source,
(source) => lp.decode(source),
(source) => map(source, (buf) => uint8ArrayToString(buf.subarray())),
async function (source) {
let string = ''
for await (const msg of source) {
string += msg.toString()
}
return string
}
)
const id = connection.remotePeer.toString()
let json = JSON.parse(output)
let jsonMessage = {
protocol:config.CONFIG_PROTOCOL,
command:null,
data:null
}
if(json.command === 'peer-exchange'){
if(json.protocol == config.CONFIG_PROTOCOL){
const address = [connection.remoteAddr.toString()]
if(this.#connectedPeers.has(id)){
//reset this last seen
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(id,metadata)
}
else{
if(!this.#webPeersId.includes(id))this.#webPeersId.push(id)
//add to connected webpeers
this.#onConnectFnUpdate(id)
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(id,metadata)
this.#updatePeers()
}
}
if(json.data != null){
this.#peerexchangedata.set(id,json.data)
let mddrs = []
const dataaddr = json.data.addr
const datamddr = multiaddr(dataaddr)
const dataid = json.data.id
mddrs.push(datamddr)
this.#dialMultiaddress(mddrs)
if(!this.#dbstoreData.has(dataid)){
//await this.#dbstore.put(new Key(dataid), new TextEncoder().encode(dataaddr))
//this.#dbstoreData.set(dataid,dataaddr)
}
}
const keys = Array.from(this.#dbstoreData.keys())
const randomKey = Math.floor(Math.random() * keys.length)
const key = keys[randomKey]
const addr = this.#dbstoreData.get(key)
jsonMessage.command = json.command
jsonMessage.data = {id:key,addr}
}
const message = JSON.stringify(jsonMessage)
//console.log('answer message '+id,message)
pipe(
message,
(source) => map(source, (string) => uint8ArrayFromString(string)),
(source) => lp.encode(source),
stream.sink
)
}
catch(err){
mkDebug(err)
}
}
await this.#libp2p.handle(config.CONFIG_PROTOCOL, handler, {
maxInboundStreams: 100,
maxOutboundStreams: 100,
runOnTransientConnection:config.CONFIG_RUN_ON_TRANSIENT_CONNECTION
})
await this.#libp2p.register(config.CONFIG_PROTOCOL, {
/*onConnect: (peer, connection) => {
//handle connect
//console.log('handle connect',peer)
},
onDisconnect: (peer, connection) => {
//handle disconnect
//console.log('handle disconnect',peer)
}*/
})
}
async #dialProtocol(id,command){
const connections = this.#libp2p.getConnections().map((con)=>{return {id:con.remotePeer.toString(),addr:con.remoteAddr.toString()}})
const connect = connections.find((con)=>con.id == id)
const addr = connect.addr
const mddr = multiaddr(addr)
let jsonMessage = {
protocol:config.CONFIG_PROTOCOL,
command:null,
data:null
}
if(command === 'peer-exchange'){
if(this.#peerexchangedata.has(id))return
const keys = Array.from(this.#dbstoreData.keys())
const randomKey = Math.floor(Math.random() * keys.length)
const key = keys[randomKey]
const addr = this.#dbstoreData.get(key)
jsonMessage.command = command
jsonMessage.data = {id:key,addr}
}
const message = JSON.stringify(jsonMessage)
//console.log('ask message '+id,message)
try{
const stream = await this.#libp2p.dialProtocol(mddr, config.CONFIG_PROTOCOL,{runOnTransientConnection:config.CONFIG_RUN_ON_TRANSIENT_CONNECTION})
const output = await pipe(
message,
(source) => map(source, (string) => uint8ArrayFromString(string)),
(source) => lp.encode(source),
stream,
(source) => lp.decode(source),
(source) => map(source, (buf) => uint8ArrayToString(buf.subarray())),
async function (source) {
let string = ''
for await (const msg of source) {
string += msg.toString()
}
return string
}
)
//console.log(output)
const json = JSON.parse(output)
if(json.protocol == config.CONFIG_PROTOCOL){
const address = [addr]
if(this.#connectedPeers.has(id)){
//reset this last seen
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(id,metadata)
}
else{
if(!this.#webPeersId.includes(id))this.#webPeersId.push(id)
//add to connected webpeers
this.#onConnectFnUpdate(id)
const now = new Date().getTime()
const metadata = {addrs:address,last:now}
this.#connectedPeers.set(id,metadata)
this.#updatePeers()
}
}
if(json.command == 'peer-exchange'){
if(json.data != null){
this.#peerexchangedata.set(id,json.data)
let mddrs = []
const dataaddr = json.data.addr
const datamddr = multiaddr(dataaddr)
const dataid = json.data.id
mddrs.push(datamddr)
this.#dialMultiaddress(mddrs)
if(!this.#dbstoreData.has(dataid)){
//await this.#dbstore.put(new Key(dataid), new TextEncoder().encode(dataaddr))
//this.#dbstoreData.set(dataid,dataaddr)
}
}
}
}
catch(err){
mkDebug(err)
}
}
async #findHybridPeer(){
if(!navigator.onLine)return
if(!this.#isDialEnabled)return
if(this.#connectedPeers.size > 5)return
for(const target of config.CONFIG_KNOWN_BOOTSTRAP_HYBRID_IDS){
if(!this.#isConnected(target) && !this.#connections.has(target)){
//console.log('findPeer',target)
const peerId = peerIdFromString(target)
//const peerInfo = await this.#libp2p.services.aminoDHT.findPeer(peerId)
//console.info(peerInfo)
for await (const event of this.#libp2p.services.aminoDHT.findPeer(peerId)){
//console.info('findPeer',event)
if (event.name === 'FINAL_PEER'){
//console.log(event.peer.id.toString(),event.peer.multiaddrs.toString())
let mddrs = []
let addrs = []
const id = event.peer.id.toString()
for(const mddr of event.peer.multiaddrs){
const peeraddr = mddr.toString()+'/p2p/'+id
const peermddr = multiaddr(peeraddr)
addrs.push(peeraddr)
mddrs.push(peermddr)
}
this.#dialedKnownBootstrap.set(id,addrs)
if(!this.#isConnected(id)){
this.#dialMultiaddress(mddrs)
}
}
}
}
}
}
//check the last seen in web peer
#trackLastSeen(){
const timeout = 20*1000
const forcetimeout = 60*1000
const now = new Date().getTime()
//if webpeer last seen grather then timeout send onDisconnect
for(const peer of this.#connectedPeers){
const id = peer[0]
const last = peer[1].last
const time = now-last
if((time>timeout && !this.#isConnected(id))||(time>forcetimeout)){
//console.log('time',time)
this.#connectedPeers.delete(id)
this.#updatePeers()
this.#onDisconnectFn(id)
//remove id from room member
const rooms = Object.keys(this.#rooms)
for(const room of rooms){
if(this.#rooms[room].members.includes(id)){
const index = this.#rooms[room].members.indexOf(id)
this.#rooms[room].members.splice(index,1)
this.#rooms[room].onMembers(this.#rooms[room].members)
}
}
}
}
}
//check if this id is connected
#isConnected(id){
let peers = []
for(const peer of this.#libp2p.getPeers()){
peers.push(peer.toString())
}
if(peers.includes(id)){
return true
}
else{
return false
}
}
//add multiaddr address to queue list
async #dialMultiaddress(mddrs){
if(mddrs.length>0){
const id = mddrs[0].toString().split('/').pop()
const queueids = this.#dialQueue.map((arr)=> arr[0].toString().split('/').pop())
//if peer id is already in the queque cancel queque
if(queueids.includes(id)){
return
}
if(this.status === 'connected' && config.CONFIG_KNOWN_BOOTSTRAP_PUBLIC_IDS.includes(id))return
const webPeerCount = this.#connectedPeers.size
const allPeerCount = this.#libp2p.getPeers().length
const nodePeerCount = allPeerCount - webPeerCount
const limitCount = config.CONFIG_MAX_CONNECTIONS / 2
if(this.#webPeersId.includes(id)){
if(webPeerCount>limitCount){
return
}
}
else{
if((nodePeerCount>(limitCount-5))||(allPeerCount>(config.CONFIG_MAX_CONNECTIONS-5))){
return
}
}
if(this.#webPeersId.includes(id) || config.CONFIG_KNOWN_BOOTSTRAP_HYBRID_IDS.includes(id)){
this.#dialQueue.unshift(mddrs)
}
else{
this.#dialQueue.push(mddrs)
}
}
}
//dial multiaddr address in queue list
#dialQueueList(){
if(!this.#isDialEnabled || !navigator.onLine || (document.visibilityState === 'hidden' && ('ontouchstart' in document.documentElement)) )return
const mddrsToDial = 5
let queue = []
for(const item of this.#libp2p.getDialQueue()){
const id = item.peerId.string
queue.push(id)
}
if (queue.length > mddrsToDial)return
for(let i = 0; i < mddrsToDial; i++){
const mddrs = this.#dialQueue.shift()
if(mddrs != undefined && mddrs.length>0){
const id = mddrs[0].toString().split('/').pop()
if(this.#isConnected(id))continue
if(queue.includes(id)){continue;}
//console.log('dial',id)
//dial with webtransport
this.#dialWebtransport(mddrs)
//fallback dial with websocket if enabled
if(this.#isDialWebsocket){
this.#dialWebsocket(mddrs)
}
}
else{
break
}
}
}
//announce and ping via pupsub peer discovery hybrid
async #peerDiscoveryHybrid(){
const topics = config.CONFIG_PUBSUB_PEER_DISCOVERY_HYBRID
const data = JSON.stringify({prefix:'hybrid',id:this.#libp2p.peerId.toString(),address:this.address})
const peer = {
publicKey: this.#libp2p.peerId.publicKey,
addrs: [uint8ArrayFromString(data)],
}
const encodedPeer = PBPeer.encode(peer)
for(const topic of topics){
await this.#libp2p.services.pubsub.publish(topic, encodedPeer)
}
}
async #announce(){
let topics = []
if(config.CONFIG_RUN_ON_TRANSIENT_CONNECTION == true){
topics = config.CONFIG_PUBSUB_PEER_DISCOVERY_HYBRID
}
else{
topics = config.CONFIG_PUPSUB_PEER_DATA
}
const data = JSON.stringify({prefix:config.CONFIG_PREFIX,signal:'announce',id:this.#libp2p.peerId.toString(),address:this.address,rooms:Object.keys(this.#rooms)})
const peer = {
publicKey: this.#libp2p.peerId.publicKey,
addrs: [uint8ArrayFromString(data)],
}
const encodedPeer = PBPeer.encode(peer)
for(const topic of topics){
await this.#libp2p.services.pubsub.publish(topic, encodedPeer)
}
}
async #ping(){
let topics = []
if(config.CONFIG_RUN_ON_TRANSIENT_CONNECTION == true){
topics = config.CONFIG_PUBSUB_PEER_DISCOVERY_HYBRID
}
else{
topics = config.CONFIG_PUPSUB_PEER_DATA
}
const data = JSON.stringify({prefix:config.CONFIG_PREFIX,signal:'ping',id:this.#libp2p.peerId.toString(),address:this.address,rooms:Object.keys(this.#rooms)})
const peer = {
publicKey: this.#libp2p.peerId.publicKey,
addrs: [uint8ArrayFromString(data)],
}
const encodedPeer = PBPeer.encode(peer)
for(const topic of topics){
await this.#libp2p.services.pubsub.publish(topic, encodedPeer)
}
}
//dial discovered peers
/*#dialdiscoveredpeers(){
setInterval(()=>{
const keys = Array.from(this.#discoveredPeers.keys())
for(const key of keys){
if(!this.#dialedDiscoveredPeers.includes(key)){
this.#dialedDiscoveredPeers.push(key)
const addrs = this.#discoveredPeers.get(key)
let mddrs = []
for(const addr of addrs){
const mddr = multiaddr(addr)
mddrs.push(mddr)
}
this.#dialMultiaddress(mddrs)
break
}
}
},30*1000)
}*/
//dial random known bootstrap periodically
#dialRandomBootstrap(){
setInterval(()=>{
//const keys = Array.from(this.#dialedKnownBootstrap.keys())
const keys = config.CONFIG_KNOWN_BOOTSTRAP_PEERS_IDS
const randomKey = Math.floor(Math.random() * keys.length)
let ids = []
ids.push(keys[randomKey])
//universal connectivity id for webpeer discovery and joinRoom version 1 to work
for(const id of config.CONFIG_KNOWN_BOOTSTRAP_HYBRID_IDS){
ids.push(id)
}
for(const id of ids){
if(id == undefined)continue
//const addrs = this.#dialedKnownBootstrap.get(id)
if(!this.#isConnected(id)){
if(this.#connections.has(id))
{
let mddrs = []
const addr = this.#connections.get(id)
const mddr = multiaddr(addr)
mddrs.push(mddr)
this.#dialMultiaddress(mddrs)
}
else if (this.#dialedKnownBootstrap.has(id)){
let mddrs = []
const addrs = this.#dialedKnownBootstrap.get(id)
for(const addr of addrs){
const mddr = multiaddr(addr)
mddrs.push(mddr)
}
this.#dialMultiaddress(mddrs)
}
else{
const bootstrap = config.CONFIG_KNOWN_BOOTSTRAP_PEERS_ADDRS
const index = bootstrap.findIndex((peer)=>peer.Peers[0].ID == id)
if(index > -1){
const addrs = bootstrap[index].Peers[0].Addrs
let mddrs = []
for(const addr of addrs){
const peeraddr = addr+'/p2p/'+id
const mddr = multiaddr(peeraddr)
mddrs.push(mddr)
}
this.#dialMultiaddress(mddrs)
}
}
}
}
},45*1000)
}
async #checkConnectionAll(){
//save peer address if connection is good
const connections = this.#libp2p.getConnections()
for(const connect of connections){
const peer = connect.remotePeer
const remote = connect.remoteAddr
const upgraded = connect.timeline.upgraded
const bestlimit = 5*60*1000
const now = new Date().getTime()
const besttime = now-upgraded
if(besttime>bestlimit){
const addr = remote.toString()
const id = peer.toString()
if(!this.#webPeersId.includes(id) && !config.CONFIG_KNOWN_BOOTSTRAP_PEERS_IDS.includes(id) && !this.#dbstoreData.has(id) && !addr.includes('p2p-circuit') && addr.includes('webtransport')){
await this.#dbstore.put(new Key(id), new TextEncoder().encode(addr))
this.#dbstoreData.set(id,addr)
}
}
const goodlimit = 60*1000
const goodtime = now-upgraded
if(goodtime>goodlimit){
const id = peer.toString()
if(!this.#dialedGoodPeers.has(id))this.#dialedGoodPeers.set(id,0)
}
}
const shuffle = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
//connect to saved best peer address
//working great
const bestall = Array.from(this.#dbstoreData.keys())
const bestshuffle = shuffle(bestall)
const best = bestshuffle.slice(0, 10)
for(const hybrid of config.CONFIG_KNOWN_BOOTSTRAP_HYBRID_IDS){
if(best.includes(hybrid))continue
if(!bestall.includes(hybrid))continue
best.unshift(hybrid)
}
for(const id of best){
const addr = this.#dbstoreData.get(id)
if(this.#isConnected(id)){
this.#connectionTrackerStore.set(id,0)
continue
}else{
if(this.#connectionTrackerStore.has(id)){
let current = this.#connectionTrackerStore.get(id)
current++
this.#connectionTrackerStore.set(id,current)
if(current>5){
if(!this.#connections.has(id) && !config.CONFIG_KNOWN_BOOTSTRAP_PEERS_IDS.includes(id) && navigator.onLine)
{
setTimeout(async ()=> {
if(this.#dbstoreData.has(id) && !this.#connections.has(id)){
this.#dbstoreData.delete(id)
await this.#dbstore.delete(new Key(id))
}
},60*1000)
}
continue
}
}
else{
this.#connectionTrackerStore.set(id,0)
}
let mddrs = []
const mddr = multiaddr(addr)
mddrs.push(mddr)
//console.log(id,addr)
this.#dialMultiaddress(mddrs)
}
}
//connect to good peer address if it is disconnected
const goods = Array.from(this.#dialedGoodPeers.keys())
for(const id of goods){
if(this.#isConnected(id)){
this.#dialedGoodPeers.set(id,0)
continue
}
else{
let count = this.#dialedGoodPeers.get(id)
if (count < 15 || (count < 25 && this.#dialedKnownBootstrap.has(id))){
const addr = this.#connections.get(id)
let mddrs = []
const mddr = multiaddr(addr)
mddrs.push(mddr)
this.#dialMultiaddress(mddrs)
count++
this.#dialedGoodPeers.set(id,count)
}
}
}
}
//track for good connection
async #connectionTracker(){
for await (const { key, value } of this.#dbstore.query({})) {
const id = key.toString().split('/')[1]
const addr = new TextDecoder().decode(value)
this.#dbstoreData.set(id,addr)
}
this.#checkConnectionAll()
setInterval(async ()=>{
this.#checkConnectionAll()
},15*1000)
}
//update listen address on change
//#ListenAddressChange = () => {}
//#onSelfAddress = f => (this.#ListenAddressChange = f)
//Periodically watch for connection
#watchConnection(){
setInterval(()=>{
const peers = this.#libp2p.getPeers().length
if(peers == 0){
this.#dialKnownPeers()
}
},120*1000)
}
//dial to all known bootstrap peers and DNS
#dialKnownPeers(){
setTimeout(()=>{
this.#dialSavedKnownID()
setTimeout(()=>{this.#dialUpdateSavedKnownID()},20000)
setTimeout(()=>{this.#findHybridPeer()},30000)
setTimeout(()=>{
const peers = this.#libp2p.getPeers().length
if(peers == 0){
this.#dialKnownID()
setTimeout(()=>{this.#findHybridPeer()},30000)
setTimeout(()=>{
const peers = this.#libp2p.getPeers().length
if(peers == 0){
this.#dialKnownBootstrap()
setTimeout(()=>{this.#findHybridPeer()},15000)
setTimeout(()=>{
const peers = this.#libp2p.getPeers().length
if(peers == 0){
this.#dialKnownDNS()
setTimeout(()=>{this.#findHybridPeer()},15000)
setTimeout(()=>{
const peers = this.#libp2p.getPeers().length
if(peers == 0){
this.#dialKnownDNSonly()
setTimeout(()=>{this.#findHybridPeer()},15000)
}
},config.CONFIG_TIMEOUT_DIAL_KNOWN_PEERS)
}
},config.CONFIG_TIMEOUT_DIAL_KNOWN_PEERS)
}
},config.CONFIG_TIMEOUT_DIAL_KNOWN_PEERS)
}
},config.CONFIG_TIMEOUT_DIAL_KNOWN_PEERS)
},5000)
}
async #dialSavedKnownID(){
if(!navigator.onLine)return
if(!this.#isDialEnabled)return
let firsttime = true
for(const target of config.CONFIG_KNOWN_BOOTSTRAP_PEERS_IDS){
if(this.#dbstoreData.has(target)){
firsttime = false
let mddrs = []
let addrs = []
const id = target
const peeraddr = this.#dbstoreData.get(target)
const peermddr = multiaddr(peeraddr)
addrs.push(peeraddr)
mddrs.push(peermddr)
this.#dialedKnownBootstrap.set(id,addrs)
if(!this.#isConnected(id)){
this.#dialMultiaddress(mddrs)
}
}
}
if(firsttime){
for(const target of config.CONFIG_KNOWN_BOOTSTRAP_PEERS_IDS){
const api = config.CONFIG_DELEGATED_API
const delegatedClient = createDelegatedRoutingV1HttpApiClient(api)
const peer = await first(delegatedClient.getPeers(peerIdFromString(target)))
if(!peer)continue
const address = peer.Addrs
const id = peer.ID
let mddrs = []
let addrs = []
for(const addr of address){
const peeraddr = addr.toString()+'/p2p/'+id.toString()
const peermddr = multiaddr(peeraddr)
addrs.push(peeraddr)
mddrs.push(peermddr)
}
this.#dialedKnownBootstrap.set(id,addrs)
if(!this.#isConnected(id)){
this.#dialMultiaddress(mddrs)
}
}
}
}
async #dialUpdateSavedKnownID(){
if(!navigator.onLine)return
if(!this.#isDialEnabled)return
let firsttime = true
for(const target of config.CONFIG_KNOWN_BOOTSTRAP_PEERS_IDS){
if(this.#dbstoreData.has(target)){
firsttime = false
}
if(!this.#connections.has(target) && (this.#dbstoreData.has(target) || firsttime)){
//console.log('#dialUpdateSavedKnownID()',target)
const api = config.CONFIG_DELEGATED_API
const delegatedClient = createDelegatedRoutingV1HttpApiClient(api)
const peer = await first(delegatedClient.getPeers(peerIdFromString(target)))
if(!peer){
if (navigator.onLine) {
await this.#dbstore.delete(new Key(target))
}
continue
}
const address = peer.Addrs
const id = peer.ID
let mddrs = []
let addrs = []
for(const addr of address){
const peeraddr = addr.toString()+'/p2p/'+id.toString()
const peermddr = multiaddr(peeraddr)
addrs.push(peeraddr)
mddrs.push(peermddr)
}
this.#dialedKnownBootstrap.set(id,addrs)
if(!this.#isConnected(id)){
this.#dialMultiaddress(mddrs)
}
}
}
}
//dial based on known peers ID
async #dialKnownID(){
if(!navigator.onLine)return
if(!this.#isDialEnabled)return
//console.log('#dialKnownID()')
const api = config.CONFIG_DELEGATED_API
const delegatedClient = createDelegatedRoutingV1HttpApiClient(api)
const BOOTSTRAP_PEER_IDS = config.CONFIG_KNOWN_BOOTSTRAP_PEERS_IDS
const peers = await Promise.all(
BOOTSTRAP_PEER_IDS.map((peerId) => first(delegatedClient.getPeers(peerIdFromString(peerId)))),
)
for(const peer of peers){
if(!peer)return
const address = peer.Addrs
const id = peer.ID
let mddrs = []
let addrs = []
for(const addr of address){
const peeraddr = addr.toString()+'/p2p/'+id.toString()
const peermddr = multiaddr(peeraddr)
addrs.push(peeraddr)
mddrs.push(peermddr)
}
this.#dialedKnownBootstrap.set(id,addrs)
if(!this.#isConnected(id)){
this.#dialMultiaddress(mddrs)
}
}
}
//dial based on known bootsrap peers address using Websocket expected
#dialKnownBootstrap(){
//console.log('#dialKnownBootstrap()')
if(!navigator.onLine)return
if(!this.#isDialEnabled)return
const bootstrap = config.CONFIG_KNOWN_BOOTSTRAP_PEERS_ADDRS
for(const peer of bootstrap){
const address = peer.Peers[0].Addrs
const id = peer.Peers[0].ID
let mddrs = []
let addrs = []
for(const addr of address){
if(!addr.includes('wss'))continue
const peeraddr = addr+'/p2p/'+id
const peermddr = multiaddr(peeraddr)
addrs.push(peeraddr)
mddrs.push(p