UNPKG

webpeerjs

Version:

WebPEER.js is Decentralized P2P network overlay inside browser.

1,739 lines (1,467 loc) 61.4 kB
//! 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