├── .idea ├── .name ├── .gitignore ├── misc.xml ├── vcs.xml ├── jsLibraryMappings.xml ├── modules.xml └── b2bjs.iml ├── signalling_server.js ├── stats ├── FileStatsRecorder.js ├── StatsRecorder.js ├── HTTPStatsRecorder.js ├── ProximityLinkChangeProbe.js ├── Stats Objects Standard.txt ├── NodeStatsProbe.js ├── StatsObjURLTagger.js └── EncounterIntervalProbe.js ├── controllers ├── NodeController.js ├── SearchRequest.js ├── ProximityLinkBooster.js ├── SearchRelay.js └── SearchResponder.js ├── config.js ├── services └── ClientInfoService.js ├── test.js ├── constants.js ├── index.html ├── package.json ├── index.js ├── test └── ListManagerTest.js ├── proximity ├── ProximityList.js └── ListManager.js ├── README.md ├── thirdparty └── adapter.js └── node.js /.idea/.name: -------------------------------------------------------------------------------- 1 | b2bjs -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /signalling_server.js: -------------------------------------------------------------------------------- 1 | process.env.PORT = process.argv[2]; 2 | let server = require("cyclon.p2p-rtc-server/server"); 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /stats/FileStatsRecorder.js: -------------------------------------------------------------------------------- 1 | let StatsRecorder = require('./StatsRecorder'); 2 | 3 | class FileStatsRecorder extends StatsRecorder{ 4 | __handleStats(statsObj) { 5 | super.__handleStats(); 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /stats/StatsRecorder.js: -------------------------------------------------------------------------------- 1 | const constants = require('../constants'); 2 | class StatsRecorder{ 3 | constructor(statsRecorder){ 4 | this.eventEmitters = []; 5 | this.eventsBacklog = []; 6 | } 7 | addEventEmitter(ee){ 8 | this.eventEmitters.push(ee) ; 9 | ee.on("stats",(statsObj)=>{ 10 | this.__handleStats(statsObj); 11 | }); 12 | } 13 | 14 | __handleStats(statsObj){ 15 | //override in subclass 16 | } 17 | } 18 | 19 | module.exports = StatsRecorder; 20 | 21 | -------------------------------------------------------------------------------- /.idea/b2bjs.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /controllers/NodeController.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require("events").EventEmitter; 2 | 3 | class NodeController extends EventEmitter{ 4 | constructor(node) { 5 | super(); 6 | this.node = node; 7 | } 8 | 9 | /** 10 | * 11 | * @param packet 12 | * @return {Boolean} true if packet could be handled, false if this controller can't handle the given packet 13 | */ 14 | handlePacket(packet){ 15 | // override in subclass 16 | throw new Error("should handle in subclass"); 17 | } 18 | 19 | /** 20 | * 21 | * @param packet 22 | * 23 | */ 24 | sendOutPacket(packet,targetNode){ 25 | packet["date"] = new Date().getDate(); 26 | this.node.sendObjectToNode(packet, targetNode); 27 | } 28 | 29 | 30 | } 31 | 32 | module.exports = NodeController; 33 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | let CONFIG = { 2 | NEIGHBOR_SIZE: 7, 3 | SHUFFLE_SIZE: 3, 4 | TICK_INTERVAL: 20000, 5 | DEFAULT_SIGNALLING_SERVERS: [ 6 | { 7 | "socket": { 8 | "server": "http://localhost:12345" 9 | }, 10 | "signallingApiBase": "http://localhost:12345" 11 | }, 12 | { 13 | "socket": { 14 | "server": "http://localhost:12346" 15 | }, 16 | "signallingApiBase": "http://localhost:12346" 17 | } 18 | ], 19 | DEFAULT_BATCHING_DELAY_MS: 300, 20 | DEFAULT_ICE_SERVERS: [ 21 | // The public Google STUN server 22 | {urls: ['stun:stun.l.google.com:19302']}, 23 | ], 24 | DEFAULT_CHANNEL_STATE_TIMEOUT_MS: 30000, DEFAULT_SIGNALLING_SERVER_RECONNECT_DELAY_MS: 5000 25 | }; 26 | 27 | module.exports = CONFIG; 28 | -------------------------------------------------------------------------------- /services/ClientInfoService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ClientInfoService(StorageService) { 4 | 5 | var infoKey = "cyclonDemoClientInfo"; 6 | var neighbourCacheKey = "cyclonDemoNeighbourCacheKey"; 7 | 8 | return { 9 | 10 | getClientInfo: function () { 11 | return StorageService.getItem(infoKey); 12 | }, 13 | 14 | setClientInfo: function (value) { 15 | StorageService.setItem(infoKey, value); 16 | }, 17 | 18 | getStoredNeighbourCache: function() { 19 | var storedValue = StorageService.getItem(neighbourCacheKey); 20 | return storedValue ? JSON.parse(storedValue) : null; 21 | }, 22 | 23 | setStoredNeighbourCache: function(value) { 24 | StorageService.setItem(neighbourCacheKey, JSON.stringify(value)); 25 | } 26 | }; 27 | } 28 | 29 | module.exports = ClientInfoService; -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | function hello(callback) { 2 | callback(); 3 | } 4 | 5 | class Test { 6 | constructor(){ 7 | } 8 | test2(){ 9 | const a = []; 10 | a.push("a1"); 11 | console.log(a); 12 | } 13 | 14 | test(){ 15 | let entries = [4]; 16 | let a = [1, 2, 3]; 17 | entries.push(...a); 18 | console.info(entries); 19 | } 20 | 21 | hello2(){ 22 | console.log("hello2"); 23 | } 24 | } 25 | 26 | // console.log(new Date().getTime()/1000); 27 | 28 | let a = new Date().getTime()/1000; 29 | 30 | console.log(1569569502.117 -1569569499.472 >1 ); 31 | console.log(5.007999897003174 - 1> 1); 32 | setTimeout(()=>{ 33 | console.log((new Date().getTime()/1000)-a); 34 | },5000); 35 | 36 | // new Test().test(); 37 | // let a = [3,2,7,0]; 38 | // a.splice(1, 0, 80); 39 | // console.log(a); 40 | // console.log(a.slice(0, a.length)); 41 | 42 | let {x,y=10} = {x:3} 43 | console.log(y); 44 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | module.exports.PACKET_TYPE = { 2 | SEARCH_REQ:"search_req", 3 | SEARCH_RES:"search_res", 4 | PROXIMITY_LINKS: "proximity_links" 5 | }; 6 | 7 | module.exports.PACKET_FIELD = { 8 | DATE_TIME: "date_time", 9 | PACKET_TYPE: "packet_type", 10 | PACKET_ID: "packet_id", 11 | REQUEST_ID: "request_id", 12 | PACKET_SOURCE: "packet_source", 13 | QUERY: "query", 14 | HOPS: "hops", 15 | BODY:"body", 16 | LIST: "list", 17 | POINTERS: "pointers", 18 | ENTRY: "" 19 | }; 20 | 21 | module.exports.EVENTS = { 22 | SEARCH_START: 'start', 23 | SEARCH_RELAY: 'relay', 24 | SEARCH_DISCARDED: 'discard', 25 | SEARCH_REVISITED: 'revisit', 26 | SEARCH_RESPOND: 'respond', 27 | PROX_LINK_CHANGED: 'prox_change', 28 | NODE_STATS: 'node_stats', 29 | ENC_INTERVAL: 'enc_interval' 30 | }; 31 | 32 | module.exports.STATS_RECORDER = { 33 | HTTP: "http", 34 | FILE: "file", 35 | }; 36 | 37 | module.exports.STATS_FILE_PATH = './stats.txt'; 38 | module.exports.STATS_HTTP_URL = "http://localhost:3500/stats"; 39 | -------------------------------------------------------------------------------- /stats/HTTPStatsRecorder.js: -------------------------------------------------------------------------------- 1 | let StatsRecorder = require('./StatsRecorder'); 2 | let StatsTagger = require('./StatsObjURLTagger'); 3 | let cyclonRtc = require('cyclon.p2p-rtc-client'); 4 | 5 | class HTTPStatsRecorder extends StatsRecorder{ 6 | constructor(){ 7 | super(); 8 | this.tagger = new StatsTagger(); 9 | } 10 | __handleStats(statsObj) { 11 | this.tagger.tagStatsObj(statsObj); 12 | this.__sendStats(statsObj); 13 | } 14 | 15 | /** 16 | * tries sending the obj over the network if it fails adds it to backlog 17 | * @param statsObj 18 | * @private 19 | */ 20 | __sendStats(statsObj){ 21 | let httpReq = new cyclonRtc.HttpRequestService(); 22 | httpReq.get(statsObj.url).then((resolve)=>{ 23 | // sent successfully 24 | },(reject)=>{ 25 | // couldnt send save for later 26 | console.error("couldnt send stats obj to url: "+statsObj.url); 27 | console.error(reject); 28 | this.eventsBacklog.push(statsObj); 29 | }); 30 | } 31 | } 32 | 33 | 34 | 35 | 36 | module.exports = HTTPStatsRecorder; 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cyclon node test 7 | 24 | 25 | 26 |
name:
27 | 28 | 29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meshp2p", 3 | "version": "1.0.30", 4 | "main": "node.js", 5 | "scripts": { 6 | "test": "mocha 'test/*' || true", 7 | "build": "browserify index.js -o bundle.js", 8 | "server": "node signalling_server.js" 9 | }, 10 | "keywords": [ 11 | "browser", 12 | "network", 13 | "mesh", 14 | "search", 15 | "distributed", 16 | "distributed search", 17 | "discovery", 18 | "vicinity", 19 | "p2p", 20 | "webrtc", 21 | "cyclon", 22 | "browser p2p", 23 | "gossip", 24 | "rtc" 25 | ], 26 | "author": "Hadi Modarres (github.com/HadiModarres)", 27 | "license": "ISC", 28 | "description": "https://github.com/HadiModarres/MeshP2P", 29 | "dependencies": { 30 | "angular": "^1.7.9", 31 | "browserify": "^16.5.0", 32 | "chai": "^4.2.0", 33 | "cyclon.p2p": "^1.0.0", 34 | "cyclon.p2p-common": "^0.1.11", 35 | "cyclon.p2p-rtc-client": "^1.0.2", 36 | "cyclon.p2p-rtc-comms": "^1.1.1", 37 | "cyclon.p2p-rtc-server": "^1.0.2", 38 | "faker": "^4.1.0", 39 | "mocha": "^6.2.2", 40 | "string-similarity": "^3.0.0", 41 | "uuid": "^3.3.3", 42 | "watchify": "^3.11.1", 43 | "webrtc-adapter": "^7.3.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /stats/ProximityLinkChangeProbe.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events").EventEmitter; 2 | var cyclonRtc = require('cyclon.p2p-rtc-client'); 3 | let constants = require("../constants"); 4 | /** 5 | * Has the responsibility to gather and report number of shuffles between encounters of other nodes. This data 6 | * can possibly be used to measure network size. 7 | */ 8 | class ProximityLinkChangeProbe extends EventEmitter{ 9 | constructor(node){ 10 | super(); 11 | this.node = node; 12 | this._gatherData(); 13 | } 14 | 15 | _gatherData(){ 16 | this.node.on("neighbors_updated", (beforeKeys, afterKeys) => { 17 | let httpReq = new cyclonRtc.HttpRequestService(); 18 | let changed = this.__getDifferenceCount(beforeKeys,afterKeys); 19 | httpReq.get(`http://localhost:3500/stats/link_changed?count=${changed}`); 20 | }); 21 | } 22 | 23 | __getDifferenceCount(arr1,arr2){ 24 | let min = Math.min(arr1.length, arr2.length); 25 | let diffCount = 0; 26 | for (let i =0;i EVENTS enum 4 | 5 | Search Request: 6 | { 7 | event 8 | id: search request id 9 | source_name: requesting node name 10 | query: search query 11 | } 12 | 13 | 14 | Search Relay: 15 | { 16 | event 17 | id: search request id 18 | source_name: requesting node name 19 | target_name: target node name 20 | } 21 | 22 | 23 | Search Response: 24 | { 25 | event 26 | id: search request id 27 | source_name: requesting node name 28 | } 29 | 30 | Search Discard: 31 | { 32 | event 33 | id: search request id 34 | source_name: requesting node name 35 | } 36 | 37 | Search Revisit: 38 | { 39 | event 40 | id: search request id 41 | source_name: requesting node name 42 | } 43 | 44 | Encounter Interval: 45 | { 46 | event 47 | sample_size 48 | interval_count 49 | total_avg 50 | source_name: requesting node name 51 | } 52 | 53 | Node Stats: 54 | { 55 | id: node name, 56 | neighbors: [node_name] : array of other neighbor names, 57 | shuffles: shuffle count 58 | timeouts: timeout count 59 | errors: error count 60 | enc_interval: encounter interval total average 61 | } 62 | 63 | 64 | Proximity List Changed: 65 | { 66 | event 67 | source_name 68 | } 69 | -------------------------------------------------------------------------------- /stats/NodeStatsProbe.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events").EventEmitter; 2 | let IntervalProbe = require("./EncounterIntervalProbe"); 3 | 4 | class NodeStatsProbe extends EventEmitter{ 5 | constructor(node,reportInterval){ 6 | super(); 7 | this.node = node; 8 | this.intervalProbe = new IntervalProbe(this.node,1,4); 9 | this._initStats(); 10 | this._setupListeners(); 11 | this._startEmitting(); 12 | this.reportInterval = reportInterval; 13 | } 14 | 15 | _startEmitting(){ 16 | if (!this.node.listManager.getAllProximityLists("list#name")){ 17 | setTimeout(()=>{this._startEmitting()},this.reportInterval); 18 | return; 19 | } 20 | 21 | let proxList = this.node.listManager.getAllProximityLists("list#name")[0]; 22 | let neighbors = proxList.getAllElements(); 23 | neighbors = neighbors.concat(this.node.listManager.getInboundListForGlobalList("list#name")); 24 | neighbors = neighbors.map((value) => { 25 | return value.key; 26 | }); 27 | let statsObj = {event: "node_stats",id:this.node.name,neighbors,shuffles:this.shuffleCount,timeouts:this.timeoutCount, 28 | errors:this.errorCount,enc_interval: this.enc_interval}; 29 | this.emit("stats", statsObj); 30 | setTimeout(()=>{this._startEmitting()},this.reportInterval); 31 | } 32 | 33 | _setupListeners(){ 34 | this.intervalProbe.on("stats", (statsObj) => { 35 | this.enc_interval = statsObj.total_avg; 36 | }); 37 | this.node.__cyclonNode.on("shuffleCompleted",(direction)=>{ 38 | this.shuffleCount++; 39 | }); 40 | 41 | this.node.__cyclonNode.on("shuffleError", (direction) => { 42 | this.errorCount++; 43 | }); 44 | 45 | this.node.__cyclonNode.on("shuffleTimeout", (direction) => { 46 | this.timeoutCount++; 47 | }); 48 | } 49 | 50 | _initStats(){ 51 | this.neighborNames = []; 52 | this.shuffleCount = 0; 53 | this.timeoutCount = 0; 54 | this.errorCount = 0; 55 | this.enc_interval = 0; 56 | } 57 | } 58 | 59 | module.exports = NodeStatsProbe; 60 | -------------------------------------------------------------------------------- /stats/StatsObjURLTagger.js: -------------------------------------------------------------------------------- 1 | let constants = require("../constants"); 2 | /** 3 | * responsibility: tags stats objects with the url they should be sent to. 4 | */ 5 | class StatsObjURLTagger { 6 | tagStatsObj(obj){ 7 | switch (obj.event) { 8 | case constants.EVENTS.SEARCH_RELAY: 9 | this.__handleSearchRelay(obj); 10 | break; 11 | case constants.EVENTS.SEARCH_START: 12 | this.__handleSearchReq(obj); 13 | break; 14 | case constants.EVENTS.SEARCH_RESPOND: 15 | this.__handleSearchRes(obj); 16 | break; 17 | case constants.EVENTS.SEARCH_DISCARDED: 18 | this.__handleSearchDiscard(obj); 19 | break; 20 | case constants.EVENTS.SEARCH_REVISITED: 21 | this.__handleSearchRevisit(obj); 22 | break; 23 | case constants.EVENTS.NODE_STATS: 24 | this.__handleNodeStats(obj); 25 | break; 26 | 27 | } 28 | } 29 | 30 | __handleNodeStats(obj){ 31 | let url = `${constants.STATS_HTTP_URL}/node_stats?json=${JSON.stringify(obj)}`; 32 | obj.url = url; 33 | } 34 | __handleSearchReq(obj){ 35 | let url = `${constants.STATS_HTTP_URL}/search_started?id=${obj.id}&source_name=${obj.source_name}&query=${obj.query}`; 36 | obj.url = url; 37 | } 38 | __handleSearchRes(obj){ 39 | let url = `${constants.STATS_HTTP_URL}/search_responded?id=${obj.id}&node_name=${obj.source_name}`; 40 | obj.url = url; 41 | } 42 | __handleSearchRelay(obj){ 43 | let url = `${constants.STATS_HTTP_URL}/search_relayed?id=${obj.id}&node_name=${obj.source_name}&target_name=${obj.target_name}`; 44 | obj.url = url; 45 | } 46 | __handleSearchDiscard(obj){ 47 | let url = `${constants.STATS_HTTP_URL}/search_discarded?id=${obj.id}&node_name=${obj.source_name}`; 48 | obj.url = url; 49 | } 50 | __handleSearchRevisit(obj){ 51 | let url = `${constants.STATS_HTTP_URL}/search_revisited?id=${obj.id}&node_name=${obj.source_name}`; 52 | obj.url = url; 53 | } 54 | } 55 | 56 | module.exports = StatsObjURLTagger; 57 | -------------------------------------------------------------------------------- /controllers/SearchRequest.js: -------------------------------------------------------------------------------- 1 | const NodeController = require("./NodeController"); 2 | const constants = require("../constants"); 3 | const uuid = require("uuid/v4"); 4 | var cyclonRtc = require('cyclon.p2p-rtc-client'); 5 | 6 | 7 | class SearchRequest extends NodeController{ 8 | constructor(node,searchTerm,list){ 9 | super(node); 10 | this.searchTerm = searchTerm; 11 | this.id = uuid(); 12 | this.responses = []; 13 | this.list = list; 14 | } 15 | 16 | initiateSearch(){ 17 | let neighborIds = this.node.getRandomSamplePointers(); 18 | let packet = {}; 19 | packet[constants.PACKET_FIELD.PACKET_ID] = this.id; 20 | packet[constants.PACKET_FIELD.PACKET_SOURCE] = this.node.__cyclonNode.createNewPointer(); 21 | packet[constants.PACKET_FIELD.PACKET_TYPE] = constants.PACKET_TYPE.SEARCH_REQ; 22 | packet[constants.PACKET_FIELD.HOPS] = 1; 23 | packet[constants.PACKET_FIELD.QUERY]= this.searchTerm; 24 | packet[constants.PACKET_FIELD.LIST]= this.list; 25 | 26 | let stats_obj = {event:constants.EVENTS.SEARCH_START,id:this.id, 27 | source_name:this.node.name,query:this.searchTerm }; 28 | this.emit("stats", stats_obj); 29 | // this.emit("stats", constants.EVENTS.SEARCH_START, this.id, this.node.name, this.searchTerm, neighborIds.length); 30 | for(let neighborId of neighborIds) { 31 | this.sendOutPacket(packet, neighborId); 32 | 33 | // let httpReq = new cyclonRtc.HttpRequestService(); 34 | // httpReq.get(`http://localhost:3500/stats/search_started?id=${this.id}&source_name=${this.node.name}&query=${this.searchTerm}`); 35 | } 36 | if (neighborIds.length===0){ 37 | for (let elem of this.node.listManager.getAllProximityLists(this.list)[0].getAllElements()){ 38 | this.sendOutPacket(packet,elem.value); 39 | } 40 | } 41 | this.node.handledPacketIds.push(packet[constants.PACKET_FIELD.PACKET_ID]); 42 | 43 | } 44 | 45 | handlePacket(packet){ 46 | if (packet[constants.PACKET_FIELD.PACKET_TYPE] !== constants.PACKET_TYPE.SEARCH_RES) 47 | return false; 48 | if (this.id !== packet[constants.PACKET_FIELD.REQUEST_ID]) 49 | return false; 50 | console.info("received response for request: " + this.id +" response from: "+packet[constants.PACKET_FIELD.PACKET_SOURCE]); 51 | this.responses.push(packet); 52 | this.emit("search_result",packet); 53 | return true; 54 | } 55 | } 56 | 57 | module.exports = SearchRequest; 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | let Node = require("./node").Node; 2 | let faker = require("faker"); 3 | let SearchRequest = require("./controllers/SearchRequest"); 4 | let SearchResponder = require("./controllers/SearchResponder"); 5 | let stringSimilarity = require("string-similarity"); 6 | 7 | 8 | 9 | let node = new Node((channel)=>{ 10 | console.info("connection received!"); 11 | console.info(channel); 12 | },{}); 13 | // let name = faker.name.firstName(); 14 | // while (name.charAt(0) !== 'K') { 15 | // name = faker.name.firstName(); 16 | // } 17 | let name = `${Math.floor(Math.random()*200)},${Math.floor(Math.random()*200)}`; 18 | node.registerList("list#name", (a, b) =>{ 19 | let x1 = a.split(",")[0]; 20 | let y1 = a.split(",")[1]; 21 | let x2 = b.split(",")[0]; 22 | let y2 = b.split(",")[1]; 23 | return 1/Math.sqrt(Math.pow(Math.abs(x1-x2),2)+Math.pow(Math.abs(y1-y2),2)); 24 | },0.2); 25 | node.setEntries("list#name", [name]); 26 | node.name= name; 27 | window.document.title = name; 28 | node.startNode(); 29 | 30 | // let clientInfoService = new ClientInfoService(persistentStorage); 31 | let neighbourSet = node.__cyclonNode.getNeighbourSet(); 32 | 33 | 34 | 35 | let currWindow = []; 36 | 37 | window.onload = function () { 38 | document.getElementById("name").innerText = name; 39 | }; 40 | 41 | let response_ids = {}; 42 | global.runTest = function () { 43 | console.info("running search"); 44 | // let searchRequest = new SearchRequest(node, document.getElementById("new_name").value,"list#name"); 45 | document.getElementById("results").innerHTML=""; 46 | // response_ids = {}; 47 | // searchRequest.on("search_result", (packet) => { 48 | // if (!response_ids[packet.packet_id]){ 49 | // response_ids[packet.packet_id]= 1; 50 | // }else { 51 | // response_ids[packet.packet_id]++; 52 | // } 53 | // }); 54 | // node.attachController(searchRequest); 55 | // node.statsRecorder.addEventEmitter(searchRequest); 56 | // searchRequest.initiateSearch(); 57 | node.search("list#name", document.getElementById("new_name").value, 60, (value) => { 58 | console.info("called"); 59 | let point = JSON.stringify(value); 60 | document.getElementById("results").innerHTML += 61 | `
${JSON.stringify(value)}
`; 62 | }); 63 | }; 64 | 65 | global.connectBtnClicked = function () { 66 | // console.info(document.getElementById("pointer").innerText); 67 | let pointer = JSON.parse(document.getElementById("pointer").value); 68 | node.connectToNode(pointer).then((rtcDataChannel) => { 69 | 70 | console.info("connected!"); 71 | console.info(rtcDataChannel); 72 | }); 73 | }; 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /controllers/ProximityLinkBooster.js: -------------------------------------------------------------------------------- 1 | const NodeController = require("./NodeController"); 2 | const constants = require("../constants"); 3 | 4 | class ProximityLinkBooster extends NodeController{ 5 | constructor(node,globalList){ 6 | super(node); 7 | this._globalList = globalList; 8 | this._startSendTimer(); 9 | } 10 | 11 | /** 12 | * 13 | * @param packet 14 | * @return {Boolean} true if packet could be handled, false if this controller can't handle the given packet 15 | */ 16 | handlePacket(packet){ 17 | if (packet[constants.PACKET_FIELD.PACKET_TYPE] !== constants.PACKET_TYPE.PROXIMITY_LINKS || 18 | packet[constants.PACKET_FIELD.LIST !== this._globalList]) { 19 | return false; 20 | } 21 | this.node._handlePointerSet(packet[constants.PACKET_FIELD.POINTERS]); 22 | let entry = {key: packet[constants.PACKET_FIELD.ENTRY], value: packet[constants.PACKET_FIELD.PACKET_SOURCE]}; 23 | this.node.listManager.addInboundElementToList(packet[constants.PACKET_FIELD.LIST], entry); 24 | return true; 25 | } 26 | 27 | _startSendTimer(){ 28 | // return; 29 | // console.log("sending"); 30 | // this._sendProximityLinks(); 31 | let proxLists = this.node.listManager.getAllProximityLists(this._globalList); 32 | if (proxLists) { 33 | let randProxList = proxLists[Math.floor(Math.random() * proxLists.length)]; 34 | if (randProxList.list.length>1) { 35 | let randomProximateNodePointer = randProxList.list[Math.floor(Math.random() * randProxList.list.length)].value; 36 | let packet = this._createPacket(randomProximateNodePointer, randProxList); 37 | this.sendOutPacket(packet, randomProximateNodePointer); 38 | } 39 | } 40 | setTimeout(()=>{this._startSendTimer()},15000); 41 | } 42 | 43 | _createPacket(targetPointer, proxList){ 44 | let pointers = proxList.list.map((proximityListEntry)=>{ 45 | return proximityListEntry.value; 46 | }); 47 | pointers = pointers.filter((pointer)=>{ 48 | if (pointer !== targetPointer){ 49 | return true; 50 | } 51 | console.log("same!"); 52 | return false; 53 | }); 54 | let packet = {}; 55 | packet[constants.PACKET_FIELD.PACKET_TYPE] = constants.PACKET_TYPE.PROXIMITY_LINKS; 56 | packet[constants.PACKET_FIELD.POINTERS] = pointers; 57 | packet[constants.PACKET_FIELD.LIST] = this._globalList; 58 | packet[constants.PACKET_FIELD.ENTRY] = proxList.referenceElement.key; 59 | packet[constants.PACKET_FIELD.PACKET_SOURCE] = this.node.__cyclonNode.createNewPointer(); 60 | return packet; 61 | } 62 | 63 | } 64 | 65 | module.exports = ProximityLinkBooster; -------------------------------------------------------------------------------- /controllers/SearchRelay.js: -------------------------------------------------------------------------------- 1 | const NodeController = require("./NodeController"); 2 | const constants = require("../constants"); 3 | var cyclonRtc = require('cyclon.p2p-rtc-client'); 4 | 5 | class SearchRelay extends NodeController{ 6 | constructor(node){ 7 | super(node); 8 | this.handledPacketIds = []; 9 | this.maximumHops = 5; 10 | } 11 | handlePacket(packet){ 12 | console.info("testing relay"); 13 | if (packet[constants.PACKET_FIELD.PACKET_TYPE] !== constants.PACKET_TYPE.SEARCH_REQ) { 14 | return false; 15 | } 16 | 17 | packet[constants.PACKET_FIELD.HOPS] ++; 18 | let n = Math.pow(2, (this.maximumHops - packet[constants.PACKET_FIELD.HOPS])) ; 19 | if (n===1){ 20 | return false; 21 | }else { 22 | let bestProxList = this.node.listManager.proxListWithClosestRefToElement( 23 | packet[constants.PACKET_FIELD.QUERY], packet[constants.PACKET_FIELD.LIST]); 24 | let randomEntries = this.node.__getRandomEntriesForList(packet[constants.PACKET_FIELD.LIST]); 25 | randomEntries = randomEntries.map((value => { 26 | return {key:value.key,value:value.pointer}; 27 | })); 28 | let allEntries = bestProxList.getAllElements().concat(randomEntries); 29 | allEntries = allEntries.concat(this.node.listManager.getInboundListForGlobalList(packet[constants.PACKET_FIELD.LIST])); 30 | let sortedList = bestProxList.sortListOnProximityToElement(allEntries, {key: packet[constants.PACKET_FIELD.QUERY]}); 31 | 32 | // let nearNodes = bestProxList.nearestNodesTo({key:packet[constants.PACKET_FIELD.QUERY]}, n); 33 | let nearNodes = sortedList.slice(0, 2); 34 | let refScore = bestProxList.proximityScoreComparedToRef(packet[constants.PACKET_FIELD.QUERY]); 35 | 36 | nearNodes = nearNodes.filter((value) => { 37 | if (value.score>=refScore){ 38 | return true; 39 | }else{ 40 | return false; 41 | } 42 | }); 43 | 44 | console.info("near nodes: "); 45 | console.info(nearNodes); 46 | // let httpReq = new cyclonRtc.HttpRequestService(); 47 | // httpReq.get(`http://localhost:3500/stats/search_relayed?id=${packet[constants.PACKET_FIELD.PACKET_ID]}&node_name=${this.node.name}`); 48 | for (let node of nearNodes) { 49 | let stats_obj = {event:constants.EVENTS.SEARCH_RELAY,id:packet[constants.PACKET_FIELD.PACKET_ID], 50 | source_name:this.node.name,target_name: node.key}; 51 | this.emit("stats", stats_obj); 52 | this.sendOutPacket(packet, node.value); 53 | } 54 | return true; 55 | } 56 | } 57 | } 58 | 59 | module.exports = SearchRelay; 60 | -------------------------------------------------------------------------------- /controllers/SearchResponder.js: -------------------------------------------------------------------------------- 1 | const NodeController = require("./NodeController"); 2 | var cyclonRtc = require('cyclon.p2p-rtc-client'); 3 | const constants = require("../constants"); 4 | const uuid = require("uuid/v4"); 5 | 6 | class SearchResponder extends NodeController{ 7 | constructor(node){ 8 | super(node); 9 | this.handledPacketIds = []; 10 | } 11 | handlePacket(packet){ 12 | console.info("testing responder"); 13 | if (packet[constants.PACKET_FIELD.PACKET_TYPE] !== constants.PACKET_TYPE.SEARCH_REQ) { 14 | return false; 15 | } 16 | // if (this.handledPacketIds.includes(packet[constants.PACKET_FIELD.PACKET_ID])) { 17 | // let stats_obj = {event:constants.EVENTS.SEARCH_REVISITED,id:packet[constants.PACKET_FIELD.PACKET_ID],source_name:this.node.name}; 18 | // this.emit("stats", stats_obj); 19 | // return false; 20 | // }else{ 21 | // if (this.handledPacketIds.length>200){ 22 | // this.handledPacketIds = []; 23 | // } 24 | // this.handledPacketIds.push(packet[constants.PACKET_FIELD.PACKET_ID]); 25 | // } 26 | 27 | let response = this.__responseForPacket(packet); 28 | if (!response){ 29 | return false; 30 | } 31 | 32 | this.__sendResponseFor(packet,response); 33 | 34 | let stats_obj = {event:constants.EVENTS.SEARCH_RESPOND,id:packet[constants.PACKET_FIELD.PACKET_ID],source_name:this.node.name}; 35 | this.emit("stats", stats_obj); 36 | // let httpReq = new cyclonRtc.HttpRequestService(); 37 | // httpReq.get(`http://localhost:3500/stats/search_responded?id=${packet[constants.PACKET_FIELD.PACKET_ID]}&node_name=${this.node.name}`); 38 | return true; 39 | } 40 | 41 | __responseForPacket(packet){ 42 | let proxLists = this.node.listManager.getAllProximityLists(packet[constants.PACKET_FIELD.LIST]); 43 | for (let proxList of proxLists){ 44 | let match = proxList.queryHit(packet[constants.PACKET_FIELD.QUERY]); 45 | if(match){ 46 | console.info("query was hit"); 47 | return {key: proxList.referenceElement.key,value:this.node.__cyclonNode.createNewPointer()}; 48 | } 49 | } 50 | return undefined; 51 | } 52 | 53 | __sendResponseFor(packet,responseBody){ 54 | console.info("search responder: handling packet"); 55 | let response = {}; 56 | response[constants.PACKET_FIELD.PACKET_ID] = uuid(); 57 | response[constants.PACKET_FIELD.REQUEST_ID] = packet[constants.PACKET_FIELD.PACKET_ID]; 58 | response[constants.PACKET_FIELD.PACKET_TYPE] = constants.PACKET_TYPE.SEARCH_RES; 59 | response[constants.PACKET_FIELD.BODY] = responseBody; 60 | this.node.sendObjectToNode(response,packet[constants.PACKET_FIELD.PACKET_SOURCE]); 61 | } 62 | } 63 | 64 | 65 | module.exports = SearchResponder; 66 | -------------------------------------------------------------------------------- /stats/EncounterIntervalProbe.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events").EventEmitter; 2 | let constants = require("../constants"); 3 | /** 4 | * Has the responsibility to gather and report number of shuffles between encounters of other nodes. This data 5 | * can possibly be used to measure network size. 6 | */ 7 | class EncounterIntervalProbe extends EventEmitter{ 8 | get node() { 9 | return this._node; 10 | } 11 | 12 | get sampleSize() { 13 | return this._sampleSize; 14 | } 15 | 16 | get encounterCounts() { 17 | return this._encounterCounts; 18 | } 19 | /** 20 | * 21 | * @param sampleSize number of nodes to check encounters for. 22 | * @param encounterCounts number of encounters to average and report at once 23 | */ 24 | constructor(node,sampleSize,encounterCounts){ 25 | super(); 26 | this._node = node; 27 | this._sampleSize = sampleSize; 28 | this._encounterCounts = encounterCounts; 29 | this.samples= []; 30 | this.currentIndex= 0; 31 | this._gatherData(); 32 | } 33 | 34 | _gatherData(){ 35 | this._node.__cyclonNode.on("shuffleCompleted", (direction,pointer)=> { 36 | this.currentIndex++; 37 | if (this.currentIndex===1){ 38 | this._initializeSamples(); 39 | }else{ 40 | this._updateSamples(); 41 | this._checkFinishCriteria(); 42 | } 43 | }); 44 | } 45 | _checkFinishCriteria(){ 46 | for (let sample of this.samples){ 47 | if (sample.encounters.length!== this._encounterCounts){ 48 | return false; 49 | } 50 | } 51 | // all samples have gathered 3 encounters, finish criteria triggered 52 | console.info(JSON.stringify(this.samples)); 53 | let totalAvg = this.samples.map((value)=>{ 54 | return (value.encounters[value.encounters.length-1]-1)/this._encounterCounts; 55 | }).reduce(((previousValue, currentValue) => { 56 | return previousValue+currentValue; 57 | }),0); 58 | let statsObj = {sample_size: this._sampleSize,interval_count: this._encounterCounts, 59 | total_avg: totalAvg,source_name: this._node.name}; 60 | this.emit("stats",statsObj); 61 | this.currentIndex = 0; 62 | console.info(statsObj); 63 | } 64 | 65 | 66 | _updateSamples(){ 67 | let neighbors = this._node.getRandomSamplePointers(); 68 | let neighborIds = this._node.getRandomSampleIds(); 69 | for (let sample of this.samples){ 70 | if (neighborIds.includes(sample.id)){ 71 | console.info("encountered"); 72 | sample.encounters.push(this.currentIndex); 73 | } 74 | } 75 | } 76 | 77 | _initializeSamples(){ 78 | let neighbors = this._node.getRandomSamplePointers(); 79 | let size = Math.min(neighbors.length, this._sampleSize); 80 | this.samples=[]; 81 | for (let i=0;i { 22 | return a + b; 23 | }); 24 | listManager.addGlobalList("files", (a, b) => { 25 | return ''; 26 | }); 27 | }).to.throw(Error); 28 | }) 29 | 30 | // it('should add neighbour to list', function () { 31 | // let listManager = new ListManager(); 32 | // listManager.addGlobalList("sampleList", (a, b) => { 33 | // return a + b; 34 | // }); 35 | // listManager.addEntry("sampleList", {key: "dkf", value: "dff"}); 36 | // listManager.addNeighbor("sampleList", {key: {obj: 'hello'}, value: "34fd344"}); 37 | // let lists = listManager.getAllProximityLists("sampleList"); 38 | // assert.equal(lists.length, 1); 39 | // assert.equal(lists[0].getElementCount(), 1); 40 | // }); 41 | 42 | it('should remove a proximity list from global list', function () { 43 | let listManager = new ListManager(); 44 | listManager.addGlobalList("sampleList", (a, b) => { 45 | return a + b; 46 | }); 47 | listManager.addEntry("sampleList", {key: "dkf", value: "dff"}); 48 | listManager.removeEntry("sampleList", {key: "dkf"}); 49 | assert.equal(listManager.getAllProximityLists("sampleList").length, 0); 50 | }); 51 | 52 | // it('should maintain multiple lists', function () { 53 | // let listManager = new ListManager(); 54 | // listManager.proximityListSize=12; 55 | // listManager.addGlobalList("Files", (a, b) => { 56 | // return a + b; 57 | // }); 58 | // listManager.addGlobalList("Names", (a, b) => { 59 | // return a + b; 60 | // }); 61 | // listManager.addEntry("Files", {key: "file1.jpg", value: "23adf324"}); 62 | // listManager.addEntry("Files", {key: "file2.jpg", value: "23adf324"}); 63 | // listManager.addEntry("Names", {key: "Jack", value: "23adf324"}); 64 | // 65 | // listManager.addNeighbor("Files", {key:"f.jpg"}); 66 | // listManager.addNeighbor("Files", {key:"b.txt"}); 67 | // listManager.addNeighbor("Names", {key:"Jo"}); 68 | // listManager.addNeighbor("Names", {key:"Jimmy"}); 69 | // listManager.addNeighbor("Names", {key:"Jacky"}); 70 | // 71 | // let fileCounts = listManager.getAllProximityLists("Files").map((value) => { 72 | // return value.getElementCount(); 73 | // }); 74 | // let nameCounts = listManager.getAllProximityLists("Names").map((value) => { 75 | // return value.getElementCount(); 76 | // }); 77 | // 78 | // assert.equal(fileCounts.reduce((previous, current) => { 79 | // return previous + current 80 | // }, 0), 4); 81 | // assert.equal(nameCounts.reduce((previous, current) => { 82 | // return previous + current 83 | // }, 0), 3); 84 | // 85 | // }); 86 | }); 87 | -------------------------------------------------------------------------------- /proximity/ProximityList.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events").EventEmitter; 2 | const constants = require("../constants"); 3 | var cyclonRtc = require('cyclon.p2p-rtc-client'); 4 | /** 5 | * Maintains a list of most similar items to a given reference element, the similarity between elements is determined 6 | * by the proximity function (a,b) => c, higher values of c mean that a and b are more similar 7 | */ 8 | class ProximityList extends EventEmitter{ 9 | /** 10 | * 11 | * @param maximumListSize 12 | * @param referenceElement 13 | * @param proximityFunc (a,b) => float n, 0 this.minResponseScore; 37 | } 38 | 39 | 40 | perfectMatchForElement(key) { 41 | for (let e of [this.referenceElement, ...this.list]) { 42 | if (JSON.stringify(e.key) === JSON.stringify(key)) { 43 | return e; 44 | } 45 | } 46 | return undefined; 47 | } 48 | 49 | /** 50 | * Sorts a list of external elements according to how close they are to external element 51 | * @param extList 52 | * @param extElem 53 | */ 54 | sortListOnProximityToElement(extList, extElem) { 55 | for (let elem of extList) { 56 | let score = this.proximityFunc(elem.key, extElem.key); 57 | elem.score = score; 58 | } 59 | return extList.sort((a, b) => { 60 | if (a.score > b.score) { 61 | return -1; 62 | } else if (a.score === b.score) { 63 | return 0; 64 | } else { 65 | return 1; 66 | } 67 | }); 68 | } 69 | 70 | 71 | nearestNodesTo(element, count) { 72 | let scores = this.list.map((value => { 73 | let score = this.proximityFunc(element, value); 74 | return {score, elem: value}; 75 | })); 76 | scores = scores.sort((a, b) => { 77 | if (a.score > b.score) { 78 | return -1; 79 | } else if (a.score === b.score) { 80 | return 0; 81 | } else { 82 | return 1; 83 | } 84 | }); 85 | return scores.slice(0, count).map((value => { 86 | return value.elem; 87 | })); 88 | } 89 | 90 | 91 | addElement(element) { 92 | if (!element.key && element.key!==0){ 93 | // bad element -> no key 94 | throw Error("Proximity List: element to be added should have a key"); 95 | } 96 | element.proximityScore = this.proximityFunc(this.referenceElement.key, element.key).toFixed(3); 97 | this._putElement(element); 98 | this.list = this.list.slice(0, this.maximumListSize); 99 | // console.info("new list: " + this.list); 100 | } 101 | 102 | addElements(elements) { 103 | for (let elem of elements) { 104 | this.addElement(elem); 105 | } 106 | } 107 | 108 | // addInboundElement(element){ 109 | // this.inboundList.unshift(element); 110 | // if (this.inboundList.length >= this.inboundListSize) { 111 | // this.inboundList.pop(); 112 | // } 113 | // } 114 | 115 | /** 116 | * 117 | * @return {null|*} most similar element object or null if list is empty 118 | */ 119 | getMostSimilarElement() { 120 | if (this.list.length > 0) { 121 | return this.list[0]; 122 | } 123 | return null; 124 | } 125 | 126 | /** 127 | * Put the element in the list according to its score 128 | * @param element 129 | * @private 130 | */ 131 | _putElement(element) { 132 | for (let i = 0; i < this.list.length; i++) { 133 | if (this.list[i].proximityScore < element.proximityScore) { 134 | this.list.splice(i, 0, element); 135 | return true; 136 | } 137 | } 138 | this.list.push(element); 139 | return false; 140 | } 141 | 142 | getKeys(){ 143 | return this.list.map((value => { 144 | return value.key; 145 | })); 146 | } 147 | 148 | getAllElements() { 149 | return this.list; 150 | } 151 | 152 | getElementCount() { 153 | return this.list.length; 154 | } 155 | 156 | 157 | } 158 | 159 | // let proxList = new ProximityList(4,{key: 8},(a,b)=>{return 1/Math.abs(a-b)}) 160 | // for (let i=0;i<20;i++){ 161 | // proxList.addElement({key: i,value:i}); 162 | // } 163 | // console.log(proxList); 164 | 165 | module.exports = ProximityList; 166 | 167 | -------------------------------------------------------------------------------- /proximity/ListManager.js: -------------------------------------------------------------------------------- 1 | let ProximityList = require("./ProximityList"); 2 | 3 | /** 4 | * List manager works with entries of type {key,value} 5 | */ 6 | class ListManager { 7 | constructor() { 8 | this.proximityListSize = 6; 9 | this.lists = []; 10 | this.inboundListSize = 5; 11 | } 12 | 13 | addGlobalList(globalList, proximityFunction, responseMinScore) { 14 | for (let list of this.lists) { 15 | if (list.listName === globalList) { 16 | throw new Error(`global list "${globalList}" exists already`); 17 | } 18 | } 19 | let newGlobalList = 20 | {listName: globalList, 21 | lists: [], 22 | proximityFunction: proximityFunction, 23 | inboundList: [], 24 | responseMinScore: responseMinScore}; 25 | this.lists.push(newGlobalList); 26 | 27 | } 28 | 29 | addInboundElementToList(globalList,entry){ 30 | if (!entry.key && entry.key!==0){ 31 | // bad element -> no key 32 | throw Error("List Manager: entry to be added should have a key"); 33 | } 34 | let list = this.getGlobalList(globalList); 35 | if (!list) { 36 | console.warn(entry); 37 | } else { 38 | list.inboundList.unshift(entry); 39 | if (list.inboundList.length >= this.inboundListSize) { 40 | list.inboundList.pop(); 41 | } 42 | } 43 | } 44 | getInboundListForGlobalList(globalList){ 45 | let list = this.getGlobalList(globalList); 46 | if (!list) { 47 | console.warn(entry); 48 | } else { 49 | return list.inboundList; 50 | } 51 | } 52 | 53 | /** 54 | * 55 | * @param globalList 56 | * @return list or undefined if the list doesnt exist 57 | */ 58 | getGlobalList(globalList) { 59 | for (let list of this.lists){ 60 | if (list.listName === globalList) { 61 | return list; 62 | } 63 | } 64 | return undefined; 65 | } 66 | 67 | 68 | removeEntry(globalList,entry){ 69 | if (!entry.key && entry.key!==0){ 70 | // bad element -> no key 71 | throw Error("List Manager: entry to be removed should have a key"); 72 | } 73 | let list = this.getGlobalList(globalList); 74 | if (!list){ 75 | console.warn(`tried to remove entry ${entry} from non-existent global list: ${globalList}`); 76 | return; 77 | } 78 | list.lists = list.lists.filter((value) => { 79 | return (value.referenceElement.key !== entry.key); 80 | }); 81 | } 82 | 83 | addEntry(globalList, entry) { 84 | if (!entry.key && entry.key!==0){ 85 | // bad element -> no key 86 | throw Error("List Manager: entry to be added should have a key"); 87 | } 88 | let list = this.getGlobalList(globalList); 89 | if (!list) { 90 | console.warn(`tried to add entry ${entry} to non-existent global list: ${globalList}. Ignoring.`); 91 | console.warn(entry); 92 | } else { 93 | for (let l of list.lists){ 94 | if (l.referenceElement.key === entry.key){ 95 | console.warn(`a proximity list for entry ${entry} already exists.`); 96 | return; 97 | } 98 | } 99 | let proxList = new ProximityList(this.proximityListSize, entry, list.proximityFunction, list.responseMinScore); 100 | list.lists.push(proxList); 101 | } 102 | } 103 | 104 | removeAllRecordsFromAllLists(filterFunc){ 105 | for (let globalList of this.lists) { 106 | for (let proxList of globalList.lists) { 107 | proxList.list = proxList.list.filter(filterFunc); 108 | } 109 | } 110 | } 111 | 112 | addElementToAllProximityLists(globalList, element) { 113 | let list = this.getGlobalList(globalList); 114 | if (!list) { 115 | console.warn(`tried to add list element ${element} to non-existent global list: ${globalList}`); 116 | return undefined; 117 | } 118 | for (let l of list.lists){ 119 | l.addElement(element); 120 | } 121 | } 122 | 123 | 124 | getAllProximityLists(globalList) { 125 | let list = this.getGlobalList(globalList); 126 | if (!list) { 127 | return undefined; 128 | } 129 | return list.lists; 130 | } 131 | 132 | 133 | /** 134 | * @return return all entries in the form of {key,list} 135 | */ 136 | getAllLocalEntries(){ 137 | const allEntries = []; 138 | for (let list of this.lists){ 139 | for (let proxList of list.lists){ 140 | allEntries.push({list: list.listName, key: proxList.referenceElement.key}); 141 | } 142 | } 143 | return allEntries 144 | } 145 | 146 | /** 147 | * returns the proximity list that has the closest reference element key to in 148 | */ 149 | proxListWithClosestRefToElement(key,globalList){ 150 | let proxLists = this.getAllProximityLists(globalList); 151 | let bestProxList = undefined; 152 | let bestScore = Number.NEGATIVE_INFINITY; 153 | for (let proxList of proxLists){ 154 | let score = proxList.proximityScoreComparedToRef(key); 155 | if (score>bestScore){ 156 | bestScore = score; 157 | bestProxList = proxList; 158 | } 159 | } 160 | return bestProxList; 161 | } 162 | 163 | } 164 | 165 | 166 | module.exports = ListManager; 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # MeshP2P 6 | #### Create P2P apps between browsers 7 | 8 | MeshP2P is a framework that allows bootstrapping the development of a distributed application over web browsers using WebRTC. 9 | It allows for discovery of other peers without requiring a central server. This is done through regular exchange of information between peers. 10 | 11 | 12 | ## Usage 13 | ``` 14 | npm install meshp2p --save 15 | ``` 16 | 17 | #### Importing in your js code 18 | ``` 19 | const Node = require("meshp2p").Node; 20 | ``` 21 | 22 | ## API 23 | 24 | 25 | ### Node Constructor 26 | As a minimum a callback for incoming rtc connections, and an array of signalling servers should be provided to the Node constructor. 27 | 28 | ##### Arguments 29 | 1. InboundCb (func): a function that is called when another peer makes a connection to this node. The rtcDataChannel will provided 30 | as an argument to the callback function. 31 | 2. Options (Obj): parameters, DEFAULT_SIGNALLING_SERVERS is required 32 | 33 | ##### Example 34 | Assuming two signalling servers are running locally on ports 12345 and 12346: 35 | ``` 36 | node = new Node((rtcDataChannel)=>{ 37 | // do something with inbound connection 38 | },{DEFAULT_SIGNALLING_SERVERS:[ 39 | { 40 | "socket": { 41 | "server": "http://127.0.0.1:12345" 42 | }, 43 | "signallingApiBase": "http://127.0.0.1:12345" 44 | }, 45 | { 46 | "socket": { 47 | "server": "http://127.0.0.1:12346" 48 | }, 49 | "signallingApiBase": "http://127.0.0.1:12346" 50 | } 51 | ]}); 52 | ``` 53 | 54 | See [Signalling Servers](#signalling-servers) on how to start signalling servers. 55 | 56 | ### registerList 57 | 58 | MeshP2P allows for multiple lists to exist in the network, and each node is able to register multiple entries in each list. 59 | Make a global list in the network using registerList. 60 | 61 | ##### Arguments 62 | 1. list (string): The name of the list 63 | 2. proximityFunction (func): The proximity function specifies how closeness between nodes is defined. 64 | It has the form (entry1, entry2) => float. The higher returned score from proximity function means the 65 | two entries are more identical, and a score of 0 means least identical. 66 | 3. responseMinScore (float): this is the minimum closeness score between entries to consider it a hit, and a node would respond to the query. 67 | 68 | ##### Returns 69 | 70 | Void. 71 | 72 | ##### Example 1 73 | 74 | Consider a network with a list of names of peers. In this case it's natural to consider closeness between entries to be string similarity: 75 | 76 | 77 | ``` 78 | registerList("list#name", (str1, str2) => {return stringSimilarity(str1,str2)}, 0.7) 79 | ``` 80 | 81 | ##### Example 2 82 | 83 | Consider a network of peers with each peer having a coordinate in 2D space. In this case one can define the closeness to be inverse of their euclidean distance, and a distance of less than 4 to consider a hit: 84 | 85 | ``` 86 | registerList("list#coordinates", (entry1,entry2) => {return 1/euclideanDist(entry1,entry2)}, 1/4) 87 | ``` 88 | 89 | ### setEntries 90 | 91 | Set the entries for the current node in the specified network list. 92 | 93 | ##### Arguments 94 | 1. list (string): The global list 95 | 2. entries (object[]): the entries for this node 96 | 97 | ##### Returns 98 | Void. 99 | 100 | ##### Examples 101 | ``` 102 | setEntries("list#names", ["Jack"]) 103 | ``` 104 | 105 | ``` 106 | setEntries("list#coordinates",[{x: 3,y:12}]) 107 | ``` 108 | 109 | ### Search 110 | 111 | Search the network. 112 | 113 | ##### Arguments 114 | 1. list(string): The global list to search 115 | 2. query(obj): The query. The query should have the form of list entry, and will be fed to the provided proximityFunction provided. 116 | 3. timeout(int): Number of seconds to wait for responses from the network. After that, the resources are freed and responses for this query aren't handled. 117 | 4. searchResultCallback(func): The callback is called each time a response is received from the network. The response is passed to the callback function and has the form: {key,value}. key is the entry that caused a match, and the value is the nodePointer of the peer that has responded to the query. 118 | 119 | ##### Examples 120 | ``` 121 | search("list#names", "jacky", 60, (response)=>{ 122 | // do something with the response 123 | }); 124 | ``` 125 | 126 | ``` 127 | search("list#coordinates", {x:2,y:2}, 60, (response)=>{ 128 | // do something with the response 129 | }); 130 | ``` 131 | 132 | ### connectToNode 133 | 134 | ##### Arguments 135 | nodePointer(nodePointerObj): The node pointer of the peer to connect to. 136 | 137 | ##### Returns 138 | A promise that is resolved with an rtcDataChannel (https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel) when successfully connected to the target peer. 139 | 140 | ##### Examples 141 | 142 | ``` 143 | node.search("list#names", "jacky", 60, (response)=>{ 144 | node.connectToNode(response.value).then(rtcDataChannel)=>{ 145 | rtcDataChannel.send("hello"); }) 146 | }); 147 | ``` 148 | 149 | ### startNode 150 | Starts the node. Start node only after specifying the global list and the node's entries in the list. 151 | 152 | ##### Arguments 153 | None 154 | 155 | 156 | ## Signalling Servers 157 | 158 | WebRTC requires the use of signalling servers so that peers can negotiate for a connection. Signalling servers are provided as part of MeshP2P 159 | and can be easily started using: 160 | ``` 161 | npm run server -- 12345 162 | ``` 163 | This runs a signalling server on port:12345 164 | 165 | Peers in the network should have access to at least one signalling server, and this should be specified in the node constructor when 166 | creating peers. 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /thirdparty/adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | /* global mozRTCIceCandidate, mozRTCPeerConnection, 11 | mozRTCSessionDescription, webkitRTCPeerConnection */ 12 | /* exported trace */ 13 | 14 | 'use strict'; 15 | 16 | var RTCPeerConnection = null; 17 | var getUserMedia = null; 18 | var attachMediaStream = null; 19 | var reattachMediaStream = null; 20 | var webrtcDetectedBrowser = null; 21 | var webrtcDetectedVersion = null; 22 | 23 | function trace(text) { 24 | // This function is used for logging. 25 | if (text[text.length - 1] === '\n') { 26 | text = text.substring(0, text.length - 1); 27 | } 28 | console.log((window.performance.now() / 1000).toFixed(3) + ': ' + text); 29 | } 30 | 31 | function maybeFixConfiguration(pcConfig) { 32 | if (!pcConfig) { 33 | return; 34 | } 35 | for (var i = 0; i < pcConfig.iceServers.length; i++) { 36 | if (pcConfig.iceServers[i].hasOwnProperty('urls')) { 37 | pcConfig.iceServers[i].url = pcConfig.iceServers[i].urls; 38 | delete pcConfig.iceServers[i].urls; 39 | } 40 | } 41 | } 42 | 43 | if (navigator.mozGetUserMedia) { 44 | console.log('This appears to be Firefox'); 45 | 46 | webrtcDetectedBrowser = 'firefox'; 47 | 48 | webrtcDetectedVersion = 49 | parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); 50 | 51 | // The RTCPeerConnection object. 52 | RTCPeerConnection = function (pcConfig, pcConstraints) { 53 | // .urls is not supported in FF yet. 54 | maybeFixConfiguration(pcConfig); 55 | return new mozRTCPeerConnection(pcConfig, pcConstraints); 56 | }; 57 | 58 | // The RTCSessionDescription object. 59 | window.RTCSessionDescription = mozRTCSessionDescription; 60 | 61 | // The RTCIceCandidate object. 62 | window.RTCIceCandidate = mozRTCIceCandidate; 63 | 64 | // getUserMedia shim (only difference is the prefix). 65 | // Code from Adam Barth. 66 | getUserMedia = navigator.mozGetUserMedia.bind(navigator); 67 | navigator.getUserMedia = getUserMedia; 68 | 69 | // Creates ICE server from the URL for FF. 70 | window.createIceServer = function (url, username, password) { 71 | var iceServer = null; 72 | var urlParts = url.split(':'); 73 | if (urlParts[0].indexOf('stun') === 0) { 74 | // Create ICE server with STUN URL. 75 | iceServer = { 76 | 'url': url 77 | }; 78 | } else if (urlParts[0].indexOf('turn') === 0) { 79 | if (webrtcDetectedVersion < 27) { 80 | // Create iceServer with turn url. 81 | // Ignore the transport parameter from TURN url for FF version <=27. 82 | var turnUrlParts = url.split('?'); 83 | // Return null for createIceServer if transport=tcp. 84 | if (turnUrlParts.length === 1 || 85 | turnUrlParts[1].indexOf('transport=udp') === 0) { 86 | iceServer = { 87 | 'url': turnUrlParts[0], 88 | 'credential': password, 89 | 'username': username 90 | }; 91 | } 92 | } else { 93 | // FF 27 and above supports transport parameters in TURN url, 94 | // So passing in the full url to create iceServer. 95 | iceServer = { 96 | 'url': url, 97 | 'credential': password, 98 | 'username': username 99 | }; 100 | } 101 | } 102 | return iceServer; 103 | }; 104 | 105 | window.createIceServers = function (urls, username, password) { 106 | var iceServers = []; 107 | // Use .url for FireFox. 108 | for (var i = 0; i < urls.length; i++) { 109 | var iceServer = 110 | window.createIceServer(urls[i], username, password); 111 | if (iceServer !== null) { 112 | iceServers.push(iceServer); 113 | } 114 | } 115 | return iceServers; 116 | }; 117 | 118 | // Attach a media stream to an element. 119 | attachMediaStream = function (element, stream) { 120 | console.log('Attaching media stream'); 121 | element.mozSrcObject = stream; 122 | }; 123 | 124 | reattachMediaStream = function (to, from) { 125 | console.log('Reattaching media stream'); 126 | to.mozSrcObject = from.mozSrcObject; 127 | }; 128 | 129 | } else if (navigator.webkitGetUserMedia) { 130 | console.log('This appears to be Chrome'); 131 | 132 | webrtcDetectedBrowser = 'chrome'; 133 | // Temporary fix until crbug/374263 is fixed. 134 | // Setting Chrome version to 999, if version is unavailable. 135 | var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); 136 | if (result !== null) { 137 | webrtcDetectedVersion = parseInt(result[2], 10); 138 | } else { 139 | webrtcDetectedVersion = 999; 140 | } 141 | 142 | // Creates iceServer from the url for Chrome M33 and earlier. 143 | window.createIceServer = function (url, username, password) { 144 | var iceServer = null; 145 | var urlParts = url.split(':'); 146 | if (urlParts[0].indexOf('stun') === 0) { 147 | // Create iceServer with stun url. 148 | iceServer = { 149 | 'url': url 150 | }; 151 | } else if (urlParts[0].indexOf('turn') === 0) { 152 | // Chrome M28 & above uses below TURN format. 153 | iceServer = { 154 | 'url': url, 155 | 'credential': password, 156 | 'username': username 157 | }; 158 | } 159 | return iceServer; 160 | }; 161 | 162 | // Creates iceServers from the urls for Chrome M34 and above. 163 | window.createIceServers = function (urls, username, password) { 164 | var iceServers = []; 165 | if (webrtcDetectedVersion >= 34) { 166 | // .urls is supported since Chrome M34. 167 | iceServers = { 168 | 'urls': urls, 169 | 'credential': password, 170 | 'username': username 171 | }; 172 | } else { 173 | for (var i = 0; i < urls.length; i++) { 174 | var iceServer = 175 | window.createIceServer(urls[i], username, password); 176 | if (iceServer !== null) { 177 | iceServers.push(iceServer); 178 | } 179 | } 180 | } 181 | return iceServers; 182 | }; 183 | 184 | // The RTCPeerConnection object. 185 | RTCPeerConnection = function (pcConfig, pcConstraints) { 186 | // .urls is supported since Chrome M34. 187 | if (webrtcDetectedVersion < 34) { 188 | maybeFixConfiguration(pcConfig); 189 | } 190 | return new webkitRTCPeerConnection(pcConfig, pcConstraints); 191 | }; 192 | 193 | // Get UserMedia (only difference is the prefix). 194 | // Code from Adam Barth. 195 | getUserMedia = navigator.webkitGetUserMedia.bind(navigator); 196 | navigator.getUserMedia = getUserMedia; 197 | 198 | // Attach a media stream to an element. 199 | attachMediaStream = function (element, stream) { 200 | if (typeof element.srcObject !== 'undefined') { 201 | element.srcObject = stream; 202 | } else if (typeof element.mozSrcObject !== 'undefined') { 203 | element.mozSrcObject = stream; 204 | } else if (typeof element.src !== 'undefined') { 205 | element.src = URL.createObjectURL(stream); 206 | } else { 207 | console.log('Error attaching stream to element.'); 208 | } 209 | }; 210 | 211 | reattachMediaStream = function (to, from) { 212 | to.src = from.src; 213 | }; 214 | } else { 215 | console.log('Browser does not appear to be WebRTC-capable'); 216 | } -------------------------------------------------------------------------------- /node.js: -------------------------------------------------------------------------------- 1 | var cyclon = require('cyclon.p2p'); 2 | var cyclonRtc = require('cyclon.p2p-rtc-client'); 3 | var cyclonRtcComms = require('cyclon.p2p-rtc-comms'); 4 | var Utils = require("cyclon.p2p-common"); 5 | let SearchResponder = require("./controllers/SearchResponder"); 6 | let SearchRelay = require("./controllers/SearchRelay"); 7 | let SearchRequest = require("./controllers/SearchRequest"); 8 | const ListManager = require("./proximity/ListManager"); 9 | let StatsRecorder = require("./stats/HTTPStatsRecorder"); 10 | var EventEmitter = require("events").EventEmitter; 11 | let NodeStatsProbe = require("./stats/NodeStatsProbe"); 12 | let ProximityLinkChangePrope = require("./stats/ProximityLinkChangeProbe"); 13 | const constants = require("./constants"); 14 | let ProximityLinkBooster = require("./controllers/ProximityLinkBooster"); 15 | 16 | let logger = console; 17 | 18 | class Node extends EventEmitter{ 19 | constructor(inboundConnectionCallback, 20 | { 21 | NEIGHBOR_SIZE= 7, 22 | SHUFFLE_SIZE= 3, 23 | TICK_INTERVAL= 20000, 24 | DEFAULT_SIGNALLING_SERVERS= [ 25 | { 26 | "socket": { 27 | "server": "http://localhost:12345" 28 | }, 29 | "signallingApiBase": "http://localhost:12345" 30 | }, 31 | { 32 | "socket": { 33 | "server": "http://localhost:12346" 34 | }, 35 | "signallingApiBase": "http://localhost:12346" 36 | } 37 | ], 38 | DEFAULT_BATCHING_DELAY_MS= 300, 39 | DEFAULT_ICE_SERVERS= [ 40 | // The public Google STUN server 41 | {urls: ['stun:stun.l.google.com:19302']}, 42 | ], 43 | DEFAULT_CHANNEL_STATE_TIMEOUT_MS= 30000, 44 | DEFAULT_SIGNALLING_SERVER_RECONNECT_DELAY_MS= 5000, 45 | ANALYTICS= false 46 | } 47 | ) { 48 | super(); 49 | this.inboundCb = inboundConnectionCallback; 50 | 51 | this._config = {}; 52 | //how to do following assignments in one statement? 53 | this._config.NEIGHBOR_SIZE= NEIGHBOR_SIZE; 54 | this._config.SHUFFLE_SIZE= SHUFFLE_SIZE; 55 | this._config.TICK_INTERVAL= TICK_INTERVAL; 56 | this._config.DEFAULT_SIGNALLING_SERVERS= DEFAULT_SIGNALLING_SERVERS; 57 | this._config.DEFAULT_BATCHING_DELAY_MS= DEFAULT_BATCHING_DELAY_MS; 58 | this._config.DEFAULT_ICE_SERVERS= DEFAULT_ICE_SERVERS; 59 | this._config.DEFAULT_CHANNEL_STATE_TIMEOUT_MS= DEFAULT_CHANNEL_STATE_TIMEOUT_MS; 60 | this._config.DEFAULT_SIGNALLING_SERVER_RECONNECT_DELAY_MS= DEFAULT_SIGNALLING_SERVER_RECONNECT_DELAY_MS; 61 | this._config.ANALYTICS = ANALYTICS; 62 | 63 | this.__controllers = []; 64 | this.listManager = new ListManager(); 65 | this.name = ''; 66 | this.__initCyclonNode(); 67 | this.__initSearchControllers(); 68 | this.proximityLinkBooster = new ProximityLinkBooster(this,"list#name"); 69 | this.__controllers.push(this.proximityLinkBooster); 70 | if (this._config.ANALYTICS) { 71 | this.statsRecorder = new StatsRecorder(); 72 | this.statsProbe = new NodeStatsProbe(this, 4000); 73 | this.linkChangeProbe = new ProximityLinkChangePrope(this); 74 | this.__addEventListeners(); 75 | } 76 | 77 | this.__setupConnectionListener(); 78 | this.handledPacketIds = []; 79 | } 80 | 81 | /** 82 | * Register a global list on this node 83 | * @param list 84 | * @param proximityFunction (a,b)->float 0 to 1, 1 being identical and 0 least similar 85 | * 86 | * @param responseMinScore 87 | */ 88 | registerList(list,proximityFunction, responseMinScore){ 89 | this.listManager.addGlobalList(list, proximityFunction, responseMinScore); 90 | } 91 | 92 | /** 93 | * Set the entries for the global list 94 | * @param list 95 | * @param entries 96 | */ 97 | setEntries(list,entries){ 98 | for (let entry of entries) { 99 | this.listManager.addEntry(list,{key:entry}); 100 | } 101 | 102 | } 103 | 104 | /** 105 | * Search the global list for the object , searchResultCallback is called with the corresponding result 106 | * every time a response is received. 107 | * 108 | * @param list 109 | * @param query 110 | * @param timeout seconds after search request expires 111 | * @param searchResultCallback 112 | */ 113 | search(list,query,timeout=60,searchResultCallback){ 114 | let searchRequest = new SearchRequest(this, query,list); 115 | searchRequest.on("search_result", (packet) => { 116 | searchResultCallback(packet.body); 117 | }); 118 | this.attachController(searchRequest); 119 | setTimeout(() => { 120 | this._removeController(searchRequest); 121 | }, timeout * 1000); 122 | if (this._config.ANALYTICS) { 123 | this.statsRecorder.addEventEmitter(searchRequest); 124 | } 125 | searchRequest.initiateSearch(); 126 | } 127 | 128 | /** 129 | * Connects to the node specified by nodePointer and returns an rtc data channel 130 | * @param nodePointer 131 | */ 132 | async connectToNode(nodePointer){ 133 | let channel = await this.rtc.openChannel("data", nodePointer); 134 | return channel.rtcDataChannel; 135 | } 136 | 137 | 138 | 139 | startNode(){ 140 | // this.__cyclonNode.on("shuffleCompleted",(direction)=>{ 141 | // console.info("shuffle completed"); 142 | // }); 143 | 144 | this.__cyclonNode.on("shuffleError", (direction) => { 145 | console.error("shuffle error"); 146 | }); 147 | 148 | this.__cyclonNode.on("shuffleTimeout", (direction) => { 149 | console.error("shuffle timeout"); 150 | }); 151 | 152 | console.info("starting node"); 153 | this.__cyclonNode.start(); 154 | console.info(this.__cyclonNode.createNewPointer()); 155 | this.__setupHandlerForNewRandomNeighborSet(); 156 | this.__listenForPackets(); 157 | } 158 | 159 | 160 | __setupConnectionListener(){ 161 | let self = this; 162 | this.rtc.onChannel("data", function (data) { 163 | self.inboundCb(data.rtcDataChannel); 164 | }); 165 | } 166 | 167 | __addEventListeners(){ 168 | for (let c of this.__controllers) { 169 | this.statsRecorder.addEventEmitter(c); 170 | } 171 | 172 | this.statsRecorder.addEventEmitter(this); 173 | this.statsRecorder.addEventEmitter(this.statsProbe); 174 | } 175 | 176 | __initSearchControllers() { 177 | let searchResponder = new SearchResponder(this); 178 | this.attachController(searchResponder); 179 | let searchRelay = new SearchRelay(this); 180 | this.attachController(searchRelay); 181 | } 182 | 183 | 184 | __initCyclonNode() { 185 | let self = this; 186 | let persistentStorage = sessionStorage; 187 | let inMemoryStorage = Utils.newInMemoryStorage(); 188 | let timingService = new cyclonRtc.TimingService(); 189 | 190 | //level 5 191 | let signallingServerService = new cyclonRtc.StaticSignallingServerService(this._config.DEFAULT_SIGNALLING_SERVERS); 192 | let socketFactory = new cyclonRtc.SocketFactory(); 193 | let signallingServerSelector = new cyclonRtc.SignallingServerSelector(signallingServerService, persistentStorage, timingService, this._config.DEFAULT_SIGNALLING_SERVER_RECONNECT_DELAY_MS); 194 | 195 | 196 | //level4 197 | // let rtcObjectFactory = new cyclonRtc.AdapterJsRTCObjectFactory(logger); 198 | let rtcObjectFactory = new cyclonRtc.NativeRTCObjectFactory(logger); 199 | let signallingSocket = new cyclonRtc.RedundantSignallingSocket(signallingServerService, socketFactory, logger, Utils.asyncExecService(), signallingServerSelector); 200 | let httpRequestService = new cyclonRtc.HttpRequestService(); 201 | 202 | 203 | //level3 204 | let signallingService = new cyclonRtc.SocketIOSignallingService(signallingSocket, logger, httpRequestService, persistentStorage); 205 | let peerConnectionFactory = new cyclonRtc.PeerConnectionFactory(rtcObjectFactory, logger, this._config.DEFAULT_ICE_SERVERS, this._config.DEFAULT_CHANNEL_STATE_TIMEOUT_MS); 206 | 207 | 208 | //level 2 209 | let iceCandidateBatchingSignalling = new cyclonRtc.IceCandidateBatchingSignallingService(Utils.asyncExecService(), 210 | signallingService, this._config.DEFAULT_BATCHING_DELAY_MS); 211 | let channelFactory = new cyclonRtc.ChannelFactory(peerConnectionFactory, iceCandidateBatchingSignalling, logger,this._config.DEFAULT_CHANNEL_STATE_TIMEOUT_MS); 212 | let shuffleStateFactory = new cyclonRtcComms.ShuffleStateFactory(logger, Utils.asyncExecService()); 213 | 214 | 215 | // level 1 216 | this.rtc = new cyclonRtc.RTC(iceCandidateBatchingSignalling, channelFactory); 217 | this.comms = new cyclonRtcComms.WebRTCComms(this.rtc, shuffleStateFactory, logger,["meshp2p"]); 218 | this.bootStrap = new cyclonRtcComms.SignallingServerBootstrap(signallingSocket, httpRequestService,["meshp2p"]); 219 | 220 | 221 | // level 0 222 | 223 | this.__cyclonNode = cyclon.builder(this.comms, this.bootStrap) 224 | .withNumNeighbours(this._config.NEIGHBOR_SIZE) 225 | .withMetadataProviders({ 226 | "clientInfo": () => { 227 | return this.listManager.getAllLocalEntries(); 228 | }, 229 | } 230 | ) 231 | .withShuffleSize(this._config.SHUFFLE_SIZE) 232 | .withTickIntervalMs(this._config.TICK_INTERVAL) 233 | .build(); 234 | 235 | 236 | } 237 | 238 | 239 | /** 240 | * 241 | * @param nodePointers these are pointers defined in cyclon.p2p 242 | * @private 243 | */ 244 | __extractListEntriesFromPointers(nodePointers){ 245 | let listEntries = []; 246 | for (let pointer of nodePointers){ 247 | let entries = pointer["metadata"]["clientInfo"].map((value) => { 248 | return {key:value.key ,list:value.list ,pointer:pointer} 249 | }); 250 | listEntries.push(...entries); 251 | } 252 | return listEntries; 253 | } 254 | 255 | __extractNodeIdsFromPointers(nodePointers){ 256 | let nodeIds = []; 257 | for (let pointer of nodePointers){ 258 | if (!nodeIds.includes(pointer.id)){ 259 | nodeIds.push(pointer.id); 260 | } 261 | } 262 | return nodeIds; 263 | } 264 | 265 | __getRandomEntriesForList(list){ 266 | let randomPointers = this.getRandomSamplePointers(); 267 | let randomEntries = this.__extractListEntriesFromPointers(randomPointers); 268 | return randomEntries.filter((value => { 269 | if (value.list === list) { 270 | return true; 271 | } else { 272 | return false; 273 | } 274 | })); 275 | } 276 | 277 | __setupHandlerForNewRandomNeighborSet(){ 278 | this.__cyclonNode.on("shuffleCompleted", (direction,pointer)=> { 279 | console.info(`${direction} shuffle complete. ${JSON.stringify(pointer)}`); 280 | let namesProxList = this.listManager.getAllProximityLists("list#name")[0]; 281 | let beforeKeys = namesProxList.getAllElements().map((value) => { 282 | return value.key; 283 | }); 284 | let pointerSet = this.getRandomSamplePointers(); 285 | this._handlePointerSet(pointerSet); 286 | this.__sendNeighborsToStatsServer(); 287 | let afterKeys = namesProxList.getAllElements().map((value) => { 288 | return value.key; 289 | }); 290 | this.emit("neighbors_updated",beforeKeys,afterKeys); 291 | }); 292 | } 293 | 294 | _handlePointerSet(pointerSet){ 295 | let entries = this.__extractListEntriesFromPointers(pointerSet); 296 | let nodeIds = this.__extractNodeIdsFromPointers(pointerSet); 297 | for (let nodeId of nodeIds){ 298 | this.__removeNeighbour({pointer: {id: nodeId}}); 299 | } 300 | this.__incorporateNeighbourList(entries); 301 | } 302 | 303 | 304 | __incorporateNeighbourList(neighbourList) { 305 | for (let neighbor of neighbourList){ 306 | let changed = this.listManager.addElementToAllProximityLists(neighbor.list, 307 | {key:neighbor.key,value:neighbor.pointer}); 308 | } 309 | } 310 | 311 | __removeNeighbour(neighbour){ 312 | let filterFunc = function (elem) { 313 | return (neighbour.pointer.id !== elem.value.id); 314 | }; 315 | this.listManager.removeAllRecordsFromAllLists(filterFunc); 316 | } 317 | 318 | __sendNeighborsToStatsServer(){ 319 | let httpReq = new cyclonRtc.HttpRequestService(); 320 | let proxList = this.listManager.getAllProximityLists("list#name")[0]; 321 | let neighbors = proxList.getAllElements(); 322 | neighbors = neighbors.map((value) => { 323 | return `"${value.key}"`; 324 | }); 325 | 326 | // let localEntry = this.listManager.getAllLocalEntries()[0].key; 327 | // httpReq.get(`http://localhost:3500/stats/neighbors_updated?json={"id":"${localEntry}","neighbors":[${neighbors}]}`); 328 | } 329 | 330 | 331 | __listenForPackets(){ 332 | let self =this; 333 | this.rtc.onChannel("search", function (data) { 334 | data.receive("unionp2p", 20000).then((message) => { 335 | console.info("data received!"); 336 | console.info(message); 337 | self.__handleReceivedPacket(message.data); 338 | data.rtcDataChannel.close(); 339 | data.close(); 340 | self.__listenForPackets(); 341 | },(reason => { 342 | data.rtcDataChannel.close(); 343 | data.close(); 344 | })); 345 | }); 346 | } 347 | 348 | __handleReceivedPacket(packet) { 349 | if (this.handledPacketIds.includes(packet[constants.PACKET_FIELD.PACKET_ID])) { 350 | return; 351 | } 352 | console.log(this.__controllers.length); 353 | console.info(`handling packet ${JSON.stringify(packet)}`); 354 | for (let controller of this.__controllers) { 355 | console.info("testing controller"); 356 | // if (controller.handlePacket(packet)) 357 | // return; 358 | controller.handlePacket(packet); 359 | } 360 | if (this.handledPacketIds.length>500){ 361 | this.handledPacketIds = []; 362 | } 363 | this.handledPacketIds.push(packet[constants.PACKET_FIELD.PACKET_ID]); 364 | // console.error(packet[constants.PACKET_FIELD.PACKET_TYPE]); 365 | // let stats_obj = {event:constants.EVENTS.SEARCH_DISCARDED,id:packet[constants.PACKET_FIELD.PACKET_ID],source_name:this.name}; 366 | // this.emit("stats", stats_obj); 367 | // let httpReq = new cyclonRtc.HttpRequestService(); 368 | // httpReq.get(`http://localhost:3500/stats/search_discarded?id=${packet[constants.PACKET_FIELD.PACKET_ID]}&node_name=${this.name}`); 369 | } 370 | 371 | /** 372 | * 373 | * @param obj 374 | * @param {uuid} targetNodeId 375 | */ 376 | sendObjectToNode(obj, targetNodePointer) { 377 | this.rtc.openChannel("search", targetNodePointer).then((channel) => { 378 | channel.send("unionp2p", { 379 | data: obj 380 | }); 381 | setTimeout(()=>{ 382 | channel.close(); 383 | },5000) 384 | }); 385 | } 386 | 387 | 388 | 389 | __getNodePointerForNodeUUID(id) { 390 | for (let pointer of this.proximityList.getAllElements()) { 391 | if (pointer["id"] === id) { 392 | return pointer; 393 | } 394 | } 395 | console.info(id); 396 | return undefined; 397 | } 398 | 399 | 400 | attachController(controller) { 401 | this.__controllers.push(controller); 402 | } 403 | 404 | _removeController(controller){ 405 | console.log("removing search request controller"); 406 | this.__controllers = this.__controllers.filter((value => { 407 | return value !== controller; 408 | })); 409 | } 410 | 411 | getRandomSamplePointers(){ 412 | return Array.from(this.__cyclonNode.getNeighbourSet().getContents().values()); 413 | } 414 | getRandomSampleIds(){ 415 | return Array.from(this.__cyclonNode.getNeighbourSet().getContents().keys()); 416 | // return Object.values(this.__cyclonNode.getNeighbourSet().getContents()).map((value => { 417 | // return value.id; 418 | // })); 419 | } 420 | getId() { 421 | return this.__cyclonNode.getId(); 422 | } 423 | 424 | } 425 | 426 | module.exports = {Node}; 427 | 428 | 429 | 430 | --------------------------------------------------------------------------------