├── .gitignore ├── .jshintignore ├── .jshintrc ├── README.md ├── config └── default.json ├── package.json └── src ├── browser ├── app │ ├── example.json │ └── index.js └── styles │ └── index.scss ├── index.js ├── public ├── a.html ├── a.json ├── css │ └── style.css └── js │ └── app.js └── views └── index.jade /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | 4 | "curly": true, 5 | "latedef": true, 6 | "quotmark": true, 7 | "undef": true, 8 | "unused": true, 9 | "trailing": true 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | webrtc-explorer-visualizer 2 | ========================== 3 | 4 | ## Project Information 5 | 6 | > [David Dias MSc in Peer-to-Peer Networks by Technical University of Lisbon](https://github.com/diasdavid/browserCloudjs#research-and-development) 7 | 8 | [![](https://img.shields.io/badge/INESC-GSD-brightgreen.svg?style=flat-square)](http://www.gsd.inesc-id.pt/) [![](https://img.shields.io/badge/TÉCNICO-LISBOA-blue.svg?style=flat-square)](http://tecnico.ulisboa.pt/) [![](https://img.shields.io/badge/project-browserCloudjs-blue.svg?style=flat-square)](https://github.com/diasdavid/browserCloudjs) 9 | 10 | This work was developed by David Dias with supervision by Luís Veiga, all in INESC-ID Lisboa (Distributed Systems Group), Instituto Superior Técnico, Universidade de Lisboa, with the support of Fundação para a Ciência e Tecnologia. 11 | 12 | More info on the team's work at: 13 | - http://daviddias.me 14 | - http://www.gsd.inesc-id.pt/~lveiga 15 | 16 | If you use this project, please acknowledge it in your work by referencing the following document: 17 | 18 | David Dias and Luís Veiga. browserCloud.js A federated community cloud served by a P2P overlay network on top of the web platform. INESC-ID Tec. Rep. 14/2015, Apr. 2015 19 | 20 | 21 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8300 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-explorer-visualizer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "npm run sass && npm run browserify && node src/index.js", 9 | "sass": "node_modules/.bin/node-sass src/browser/styles/index.scss src/public/css/style.css", 10 | "browserify": "node_modules/.bin/browserify src/browser/app/index.js -o src/public/js/app.js" 11 | }, 12 | "author": "David Dias ", 13 | "license": "ISC", 14 | "dependencies": { 15 | "browserify": "^15.0.0", 16 | "config": "^1.10.0", 17 | "dht-id": "^1.0.2", 18 | "domready": "^1.0.7", 19 | "hapi": "^16.1.0", 20 | "jade": "^1.9.2", 21 | "node-sass": "^4.3.0", 22 | "xhr": "^2.0.1" 23 | }, 24 | "devDependencies": { 25 | "code": "^4.0.0", 26 | "jscs": "^3.0.7", 27 | "jshint": "^2.6.0", 28 | "lab": "^12.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/browser/app/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "21f34817480d":{ 3 | "socketId":"GIWCjqZiDZ9-Qf-cAAAA", 4 | "fingerTable":{ 5 | "1":{"ideal":"21f34817480f","current":"2da0201cb888"}, 6 | "2":{"ideal":"21f348174811","current":"1d85821b2299"}, 7 | "3":{"ideal":"21f348174815","current":"c7b7d61b1dd5"}, 8 | "4":{"ideal":"21f34817481d","current":"a1d11c2c4728"}}, 9 | "predecessorId":"1d85821b2299"}, 10 | "1d85821b2299":{ 11 | "socketId":"qqzXHtiydh1Hf0cvAAAB", 12 | "fingerTable":{ 13 | "1":{"ideal":"1d85821b229b","current":"21f34817480d"}, 14 | "2":{"ideal":"1d85821b229d","current":"21f34817480d"}, 15 | "3":{"ideal":"1d85821b22a1","current":"21f34817480d"}, 16 | "4":{"ideal":"1d85821b22a9","current":"21f34817480d"}}, 17 | "predecessorId":"c7b7d61b1dd5"}, 18 | "c7b7d61b1dd5":{ 19 | "socketId":"1KP1mUCEEvt_PF7pAAAC", 20 | "fingerTable":{ 21 | "1":{"ideal":"c7b7d61b1dd7","current":"1d85821b2299"}, 22 | "2":{"ideal":"c7b7d61b1dd9","current":"1d85821b2299"}, 23 | "3":{"ideal":"c7b7d61b1ddd","current":"1d85821b2299"}, 24 | "4":{"ideal":"c7b7d61b1de5","current":"1d85821b2299"}}, 25 | "predecessorId":"a1d11c2c4728"}, 26 | "a1d11c2c4728":{ 27 | "socketId":"hWB-UweuJKSxvAEKAAAD", 28 | "fingerTable":{ 29 | "1":{"ideal":"a1d11c2c472a","current":"c7b7d61b1dd5"}, 30 | "2":{"ideal":"a1d11c2c472c","current":"c7b7d61b1dd5"}, 31 | "3":{"ideal":"a1d11c2c4730","current":"c7b7d61b1dd5"}, 32 | "4":{"ideal":"a1d11c2c4738","current":"c7b7d61b1dd5"}}, 33 | "predecessorId":"73e42adb55d9"}, 34 | "2da0201cb888":{ 35 | "socketId":"vy2t1aUd0I5zkrHwAAAE", 36 | "fingerTable":{ 37 | "1":{"ideal":"2da0201cb88a","current":"535fc202f902"}, 38 | "2":{"ideal":"2da0201cb88c","current":"535fc202f902"}, 39 | "3":{"ideal":"2da0201cb890","current":"535fc202f902"}, 40 | "4":{"ideal":"2da0201cb898","current":"535fc202f902"}}, 41 | "predecessorId":"21f34817480d"}, 42 | "535fc202f902":{ 43 | "socketId":"cr0ZZfq5TPimi367AAAF", 44 | "fingerTable":{ 45 | "1":{"ideal":"535fc202f904","current":"5ffcc6e13d5d"}, 46 | "2":{"ideal":"535fc202f906","current":"5ffcc6e13d5d"}, 47 | "3":{"ideal":"535fc202f90a","current":"5ffcc6e13d5d"}, 48 | "4":{"ideal":"535fc202f912","current":"5ffcc6e13d5d"}}, 49 | "predecessorId":"2da0201cb888"}, 50 | "73e42adb55d9":{ 51 | "socketId":"ukJlQU6cK-Gz5aEWAAAG", 52 | "fingerTable":{ 53 | "1":{"ideal":"73e42adb55db","current":"a1d11c2c4728"}, 54 | "2":{"ideal":"73e42adb55dd","current":"a1d11c2c4728"}, 55 | "3":{"ideal":"73e42adb55e1","current":"a1d11c2c4728"}, 56 | "4":{"ideal":"73e42adb55e9","current":"a1d11c2c4728"}}, 57 | "predecessorId":"5ffcc6e13d5d"}, 58 | "5ffcc6e13d5d":{ 59 | "socketId":"anJGtnPHDVT09OP4AAAH", 60 | "fingerTable":{ 61 | "1":{"ideal":"5ffcc6e13d5f","current":"73e42adb55d9"}, 62 | "2":{"ideal":"5ffcc6e13d61","current":"73e42adb55d9"}, 63 | "3":{"ideal":"5ffcc6e13d65","current":"73e42adb55d9"}, 64 | "4":{"ideal":"5ffcc6e13d6d","current":"73e42adb55d9"}}, 65 | "predecessorId":"535fc202f902"} 66 | } 67 | -------------------------------------------------------------------------------- /src/browser/app/index.js: -------------------------------------------------------------------------------- 1 | var Id = require('dht-id'); 2 | // var dht = require('./example.json'); 3 | var domready = require('domready'); 4 | var xhr = require("xhr"); 5 | 6 | window.app = { 7 | init: function () { 8 | domready(function(){ 9 | 10 | document 11 | .getElementById('visualize') 12 | .addEventListener('click', fetchDHT); 13 | 14 | }); 15 | } 16 | }; 17 | 18 | window.app.init(); 19 | 20 | function fetchDHT(){ 21 | xhr({ 22 | uri: "http://localhost:9000/dht", 23 | headers: { 24 | "Content-Type": "application/json" 25 | } 26 | }, function (err, resp, body) { 27 | drawDHT(JSON.parse(body)); 28 | }); 29 | } 30 | 31 | function cartesianCoordinates(id, r) { 32 | var maxId = new Id(Id.spin()).toDec(); 33 | var radId = id / (maxId / (2 * Math.PI)); 34 | 35 | return { 36 | y: Math.sin(radId - Math.PI / 2) * r , 37 | x: Math.cos(radId - Math.PI / 2) * r 38 | }; 39 | 40 | } 41 | 42 | function drawDHT(dht) { 43 | 44 | var R = 200 45 | var peers = []; 46 | 47 | Object.keys(dht).map(function (key){ 48 | var peer = { 49 | peerId: key, 50 | fingerTable: dht[key].fingerTable, 51 | predecessorId: dht[key].predecessorId 52 | }; 53 | 54 | peers.push(peer); 55 | //Add the peer to the global table too to 56 | //make it easier to lookup coords by id 57 | console.log('fill in peer'); 58 | dht[key].peer = peer; 59 | }); 60 | 61 | // add their coordinates 62 | peers.forEach(function (peer) { 63 | peer.coordinates = cartesianCoordinates( 64 | new Id(peer.peerId).toDec(), R); 65 | }); 66 | 67 | var vis = d3.select('#dht-ring') 68 | .append('svg'); 69 | 70 | vis.attr("width", 600) 71 | .attr("height", 600); 72 | 73 | var plane = vis.append("g") 74 | //centering 75 | .attr("transform", function(peer, i){ 76 | return "translate(" + 1.2 * R + "," + 1.2 * R + ")"; 77 | }) 78 | 79 | //separate the overall peer selection, from the on-enter-groups 80 | var peer = plane.selectAll("peers") 81 | .data(peers); 82 | 83 | var gs = peer.enter() 84 | .append("g") 85 | 86 | gs.append("svg:circle") 87 | .attr("r", "4px") 88 | .attr("fill", "black") 89 | 90 | gs.append("svg:text") 91 | .attr("dx", 5) 92 | .attr("dy", ".35em") 93 | .attr("fill", "black") 94 | .text(function(peer) { return peer.peerId; }); 95 | 96 | gs.attr("transform", function(peer, i){ 97 | return "translate(" + (peer.coordinates.x ) + "," + peer.coordinates.y + ")"; 98 | }); 99 | 100 | var arcBetween = function (source, target) { 101 | var dx = target.coordinates.x - source.coordinates.x; 102 | var dy = target.coordinates.y - source.coordinates.y; 103 | var dr = Math.sqrt(dx * dx + dy * dy); 104 | 105 | //We want to draw the line from 0,0 of the group (which is where 106 | //the dot is rendered), to the delta of the too points, since we 107 | //are drawing relative to the source position, not the canvas 108 | return "M" + 0 + 109 | "," + 0 + 110 | "A" + dr + 111 | "," + dr + 112 | " 0 0,1 " + dx + 113 | "," + dy; 114 | }; 115 | 116 | //Create a new sub-selection joining source's to their fingers 117 | var links = peer.selectAll('.links') 118 | .data(function (d) { 119 | //Should return the nested dataset, in this case an array of 120 | // [arcId, sourcePeer, targetPeer] 121 | var arcs = Object.keys(d.fingerTable).map(function (key) { 122 | return { 123 | arcId: d.peerId + '-' + key, 124 | source: d, 125 | target: dht[d.fingerTable[key].current].peer 126 | }; 127 | }); 128 | 129 | return arcs; 130 | }); 131 | 132 | //for all the links (a nested selection across sources and targets 133 | //draw an arc 134 | links.enter() 135 | .append("path") 136 | .attr('class', 'link') 137 | .attr("d", function (link) { 138 | return arcBetween(link.source, link.target); 139 | }); 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/browser/styles/index.scss: -------------------------------------------------------------------------------- 1 | circle { 2 | color: black; 3 | } 4 | 5 | 6 | .link { 7 | fill: none; 8 | stroke: #666; 9 | stroke-width: 1.5px; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var config = require('config'); 3 | var Path = require('path'); 4 | 5 | var server = new Hapi.Server(); 6 | 7 | server.connection({ 8 | port: config.get('port'), 9 | }); 10 | 11 | server.views({ 12 | engines: { 13 | jade: require('jade') 14 | }, 15 | relativeTo: __dirname, 16 | path: './views' 17 | }); 18 | 19 | server.route({ 20 | method: 'GET', 21 | path: '/{param*}', 22 | handler: { 23 | directory: { 24 | path: Path.join(__dirname, 'public') 25 | } 26 | } 27 | }); 28 | 29 | server.route({ 30 | method: 'GET', 31 | path: '/', 32 | handler: function(request, reply) { 33 | reply.view('index'); 34 | } 35 | }); 36 | 37 | 38 | server.start(function(err) { 39 | if (err) { 40 | throw err; 41 | } 42 | console.log('server started on :', config.get('port')); 43 | }); 44 | -------------------------------------------------------------------------------- /src/public/a.html: -------------------------------------------------------------------------------- 1 | h1 aaaaa 2 | -------------------------------------------------------------------------------- /src/public/a.json: -------------------------------------------------------------------------------- 1 | aaaa 2 | -------------------------------------------------------------------------------- /src/public/css/style.css: -------------------------------------------------------------------------------- 1 | circle { 2 | color: black; } 3 | 4 | .link { 5 | fill: none; 6 | stroke: #666; 7 | stroke-width: 1.5px; } 8 | -------------------------------------------------------------------------------- /src/public/js/app.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 14 || (offset === 14 && shift < 24)) { 153 | processBlock(); 154 | } 155 | offset = 14; 156 | shift = 24; 157 | 158 | // 64-bit length big-endian 159 | write(0x00); // numbers this big aren't accurate in javascript anyway 160 | write(0x00); // ..So just hard-code to zero. 161 | write(totalLength > 0xffffffffff ? totalLength / 0x10000000000 : 0x00); 162 | write(totalLength > 0xffffffff ? totalLength / 0x100000000 : 0x00); 163 | for (var s = 24; s >= 0; s -= 8) { 164 | write(totalLength >> s); 165 | } 166 | 167 | // At this point one last processBlock() should trigger and we can pull out the result. 168 | return toHex(h0) + 169 | toHex(h1) + 170 | toHex(h2) + 171 | toHex(h3) + 172 | toHex(h4); 173 | } 174 | 175 | // We have a full block to process. Let's do it! 176 | function processBlock() { 177 | // Extend the sixteen 32-bit words into eighty 32-bit words: 178 | for (var i = 16; i < 80; i++) { 179 | var w = block[i - 3] ^ block[i - 8] ^ block[i - 14] ^ block[i - 16]; 180 | block[i] = (w << 1) | (w >>> 31); 181 | } 182 | 183 | // log(block); 184 | 185 | // Initialize hash value for this chunk: 186 | var a = h0; 187 | var b = h1; 188 | var c = h2; 189 | var d = h3; 190 | var e = h4; 191 | var f, k; 192 | 193 | // Main loop: 194 | for (i = 0; i < 80; i++) { 195 | if (i < 20) { 196 | f = d ^ (b & (c ^ d)); 197 | k = 0x5A827999; 198 | } 199 | else if (i < 40) { 200 | f = b ^ c ^ d; 201 | k = 0x6ED9EBA1; 202 | } 203 | else if (i < 60) { 204 | f = (b & c) | (d & (b | c)); 205 | k = 0x8F1BBCDC; 206 | } 207 | else { 208 | f = b ^ c ^ d; 209 | k = 0xCA62C1D6; 210 | } 211 | var temp = (a << 5 | a >>> 27) + f + e + k + (block[i]|0); 212 | e = d; 213 | d = c; 214 | c = (b << 30 | b >>> 2); 215 | b = a; 216 | a = temp; 217 | } 218 | 219 | // Add this chunk's hash to result so far: 220 | h0 = (h0 + a) | 0; 221 | h1 = (h1 + b) | 0; 222 | h2 = (h2 + c) | 0; 223 | h3 = (h3 + d) | 0; 224 | h4 = (h4 + e) | 0; 225 | 226 | // The block is now reusable. 227 | offset = 0; 228 | for (i = 0; i < 16; i++) { 229 | block[i] = 0; 230 | } 231 | } 232 | 233 | function toHex(word) { 234 | var hex = ""; 235 | for (var i = 28; i >= 0; i -= 4) { 236 | hex += ((word >> i) & 0xf).toString(16); 237 | } 238 | return hex; 239 | } 240 | 241 | } 242 | 243 | }).call(this,require('_process')) 244 | },{"_process":1}],3:[function(require,module,exports){ 245 | var sha1 = require('git-sha1'); 246 | 247 | exports = module.exports = Id; 248 | 249 | var maxHex = 'ffffffffffff'; 250 | var maxDec = parseInt(maxHex, 16); 251 | 252 | function Id(_id) { 253 | var dec; 254 | var hex; 255 | 256 | if (typeof _id === 'number') { 257 | dec = _id; 258 | var tmp = ('00000000000000' + _id.toString(16)); 259 | hex = tmp.substring(tmp.length - 12, tmp.length); 260 | } 261 | if (typeof _id === 'string') { 262 | dec = parseInt(_id, 16); 263 | hex = _id; 264 | } 265 | if (typeof _id === 'undefined') { 266 | hex = sha1((~~(Math.random() * 1e9)).toString(36) + Date.now()) 267 | .substring(0, 12); 268 | dec = parseInt(hex, 16); 269 | } 270 | 271 | this.toHex = function() { 272 | return hex; 273 | }; 274 | 275 | this.toDec = function() { 276 | return dec; 277 | }; 278 | 279 | this.next = function() { 280 | if (hex === maxHex) { 281 | return '000000000000'; 282 | } else { 283 | var a = ('000000000000' + ((dec + 1).toString(16))); 284 | return a.substring(a.length - 12, a.length); 285 | } 286 | }; 287 | 288 | return this; 289 | } 290 | 291 | // 292 | // bigger Id than available to make the message spin the ring 293 | // 294 | exports.spin = function() { 295 | return (maxDec + 1).toString(16); 296 | }; 297 | 298 | // 299 | // returns the Id in a hex value, which correspondes to the hash of the content 300 | // 301 | exports.hash = function(content) { 302 | return sha1(content).substring(0, 12); 303 | }; 304 | 305 | },{"git-sha1":2}],4:[function(require,module,exports){ 306 | /*! 307 | * domready (c) Dustin Diaz 2014 - License MIT 308 | */ 309 | !function (name, definition) { 310 | 311 | if (typeof module != 'undefined') module.exports = definition() 312 | else if (typeof define == 'function' && typeof define.amd == 'object') define(definition) 313 | else this[name] = definition() 314 | 315 | }('domready', function () { 316 | 317 | var fns = [], listener 318 | , doc = document 319 | , hack = doc.documentElement.doScroll 320 | , domContentLoaded = 'DOMContentLoaded' 321 | , loaded = (hack ? /^loaded|^c/ : /^loaded|^i|^c/).test(doc.readyState) 322 | 323 | 324 | if (!loaded) 325 | doc.addEventListener(domContentLoaded, listener = function () { 326 | doc.removeEventListener(domContentLoaded, listener) 327 | loaded = 1 328 | while (listener = fns.shift()) listener() 329 | }) 330 | 331 | return function (fn) { 332 | loaded ? fn() : fns.push(fn) 333 | } 334 | 335 | }); 336 | 337 | },{}],5:[function(require,module,exports){ 338 | "use strict"; 339 | var window = require("global/window") 340 | var once = require("once") 341 | var parseHeaders = require("parse-headers") 342 | 343 | 344 | var XHR = window.XMLHttpRequest || noop 345 | var XDR = "withCredentials" in (new XHR()) ? XHR : window.XDomainRequest 346 | 347 | module.exports = createXHR 348 | 349 | function createXHR(options, callback) { 350 | function readystatechange() { 351 | if (xhr.readyState === 4) { 352 | loadFunc() 353 | } 354 | } 355 | 356 | function getBody() { 357 | // Chrome with requestType=blob throws errors arround when even testing access to responseText 358 | var body = undefined 359 | 360 | if (xhr.response) { 361 | body = xhr.response 362 | } else if (xhr.responseType === "text" || !xhr.responseType) { 363 | body = xhr.responseText || xhr.responseXML 364 | } 365 | 366 | if (isJson) { 367 | try { 368 | body = JSON.parse(body) 369 | } catch (e) {} 370 | } 371 | 372 | return body 373 | } 374 | 375 | var failureResponse = { 376 | body: undefined, 377 | headers: {}, 378 | statusCode: 0, 379 | method: method, 380 | url: uri, 381 | rawRequest: xhr 382 | } 383 | 384 | function errorFunc(evt) { 385 | clearTimeout(timeoutTimer) 386 | if(!(evt instanceof Error)){ 387 | evt = new Error("" + (evt || "unknown") ) 388 | } 389 | evt.statusCode = 0 390 | callback(evt, failureResponse) 391 | } 392 | 393 | // will load the data & process the response in a special response object 394 | function loadFunc() { 395 | clearTimeout(timeoutTimer) 396 | 397 | var status = (xhr.status === 1223 ? 204 : xhr.status) 398 | var response = failureResponse 399 | var err = null 400 | 401 | if (status !== 0){ 402 | response = { 403 | body: getBody(), 404 | statusCode: status, 405 | method: method, 406 | headers: {}, 407 | url: uri, 408 | rawRequest: xhr 409 | } 410 | if(xhr.getAllResponseHeaders){ //remember xhr can in fact be XDR for CORS in IE 411 | response.headers = parseHeaders(xhr.getAllResponseHeaders()) 412 | } 413 | } else { 414 | err = new Error("Internal XMLHttpRequest Error") 415 | } 416 | callback(err, response, response.body) 417 | 418 | } 419 | 420 | if (typeof options === "string") { 421 | options = { uri: options } 422 | } 423 | 424 | options = options || {} 425 | if(typeof callback === "undefined"){ 426 | throw new Error("callback argument missing") 427 | } 428 | callback = once(callback) 429 | 430 | var xhr = options.xhr || null 431 | 432 | if (!xhr) { 433 | if (options.cors || options.useXDR) { 434 | xhr = new XDR() 435 | }else{ 436 | xhr = new XHR() 437 | } 438 | } 439 | 440 | var key 441 | var uri = xhr.url = options.uri || options.url 442 | var method = xhr.method = options.method || "GET" 443 | var body = options.body || options.data 444 | var headers = xhr.headers = options.headers || {} 445 | var sync = !!options.sync 446 | var isJson = false 447 | var timeoutTimer 448 | 449 | if ("json" in options) { 450 | isJson = true 451 | headers["Accept"] || (headers["Accept"] = "application/json") //Don't override existing accept header declared by user 452 | if (method !== "GET" && method !== "HEAD") { 453 | headers["Content-Type"] = "application/json" 454 | body = JSON.stringify(options.json) 455 | } 456 | } 457 | 458 | xhr.onreadystatechange = readystatechange 459 | xhr.onload = loadFunc 460 | xhr.onerror = errorFunc 461 | // IE9 must have onprogress be set to a unique function. 462 | xhr.onprogress = function () { 463 | // IE must die 464 | } 465 | xhr.ontimeout = errorFunc 466 | xhr.open(method, uri, !sync) 467 | //has to be after open 468 | xhr.withCredentials = !!options.withCredentials 469 | 470 | // Cannot set timeout with sync request 471 | // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly 472 | // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent 473 | if (!sync && options.timeout > 0 ) { 474 | timeoutTimer = setTimeout(function(){ 475 | xhr.abort("timeout"); 476 | }, options.timeout+2 ); 477 | } 478 | 479 | if (xhr.setRequestHeader) { 480 | for(key in headers){ 481 | if(headers.hasOwnProperty(key)){ 482 | xhr.setRequestHeader(key, headers[key]) 483 | } 484 | } 485 | } else if (options.headers) { 486 | throw new Error("Headers cannot be set on an XDomainRequest object") 487 | } 488 | 489 | if ("responseType" in options) { 490 | xhr.responseType = options.responseType 491 | } 492 | 493 | if ("beforeSend" in options && 494 | typeof options.beforeSend === "function" 495 | ) { 496 | options.beforeSend(xhr) 497 | } 498 | 499 | xhr.send(body) 500 | 501 | return xhr 502 | 503 | 504 | } 505 | 506 | 507 | function noop() {} 508 | 509 | },{"global/window":6,"once":7,"parse-headers":11}],6:[function(require,module,exports){ 510 | (function (global){ 511 | if (typeof window !== "undefined") { 512 | module.exports = window; 513 | } else if (typeof global !== "undefined") { 514 | module.exports = global; 515 | } else if (typeof self !== "undefined"){ 516 | module.exports = self; 517 | } else { 518 | module.exports = {}; 519 | } 520 | 521 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 522 | },{}],7:[function(require,module,exports){ 523 | module.exports = once 524 | 525 | once.proto = once(function () { 526 | Object.defineProperty(Function.prototype, 'once', { 527 | value: function () { 528 | return once(this) 529 | }, 530 | configurable: true 531 | }) 532 | }) 533 | 534 | function once (fn) { 535 | var called = false 536 | return function () { 537 | if (called) return 538 | called = true 539 | return fn.apply(this, arguments) 540 | } 541 | } 542 | 543 | },{}],8:[function(require,module,exports){ 544 | var isFunction = require('is-function') 545 | 546 | module.exports = forEach 547 | 548 | var toString = Object.prototype.toString 549 | var hasOwnProperty = Object.prototype.hasOwnProperty 550 | 551 | function forEach(list, iterator, context) { 552 | if (!isFunction(iterator)) { 553 | throw new TypeError('iterator must be a function') 554 | } 555 | 556 | if (arguments.length < 3) { 557 | context = this 558 | } 559 | 560 | if (toString.call(list) === '[object Array]') 561 | forEachArray(list, iterator, context) 562 | else if (typeof list === 'string') 563 | forEachString(list, iterator, context) 564 | else 565 | forEachObject(list, iterator, context) 566 | } 567 | 568 | function forEachArray(array, iterator, context) { 569 | for (var i = 0, len = array.length; i < len; i++) { 570 | if (hasOwnProperty.call(array, i)) { 571 | iterator.call(context, array[i], i, array) 572 | } 573 | } 574 | } 575 | 576 | function forEachString(string, iterator, context) { 577 | for (var i = 0, len = string.length; i < len; i++) { 578 | // no such thing as a sparse string. 579 | iterator.call(context, string.charAt(i), i, string) 580 | } 581 | } 582 | 583 | function forEachObject(object, iterator, context) { 584 | for (var k in object) { 585 | if (hasOwnProperty.call(object, k)) { 586 | iterator.call(context, object[k], k, object) 587 | } 588 | } 589 | } 590 | 591 | },{"is-function":9}],9:[function(require,module,exports){ 592 | module.exports = isFunction 593 | 594 | var toString = Object.prototype.toString 595 | 596 | function isFunction (fn) { 597 | var string = toString.call(fn) 598 | return string === '[object Function]' || 599 | (typeof fn === 'function' && string !== '[object RegExp]') || 600 | (typeof window !== 'undefined' && 601 | // IE8 and below 602 | (fn === window.setTimeout || 603 | fn === window.alert || 604 | fn === window.confirm || 605 | fn === window.prompt)) 606 | }; 607 | 608 | },{}],10:[function(require,module,exports){ 609 | 610 | exports = module.exports = trim; 611 | 612 | function trim(str){ 613 | return str.replace(/^\s*|\s*$/g, ''); 614 | } 615 | 616 | exports.left = function(str){ 617 | return str.replace(/^\s*/, ''); 618 | }; 619 | 620 | exports.right = function(str){ 621 | return str.replace(/\s*$/, ''); 622 | }; 623 | 624 | },{}],11:[function(require,module,exports){ 625 | var trim = require('trim') 626 | , forEach = require('for-each') 627 | , isArray = function(arg) { 628 | return Object.prototype.toString.call(arg) === '[object Array]'; 629 | } 630 | 631 | module.exports = function (headers) { 632 | if (!headers) 633 | return {} 634 | 635 | var result = {} 636 | 637 | forEach( 638 | trim(headers).split('\n') 639 | , function (row) { 640 | var index = row.indexOf(':') 641 | , key = trim(row.slice(0, index)).toLowerCase() 642 | , value = trim(row.slice(index + 1)) 643 | 644 | if (typeof(result[key]) === 'undefined') { 645 | result[key] = value 646 | } else if (isArray(result[key])) { 647 | result[key].push(value) 648 | } else { 649 | result[key] = [ result[key], value ] 650 | } 651 | } 652 | ) 653 | 654 | return result 655 | } 656 | },{"for-each":8,"trim":10}],12:[function(require,module,exports){ 657 | var Id = require('dht-id'); 658 | // var dht = require('./example.json'); 659 | var domready = require('domready'); 660 | var xhr = require("xhr"); 661 | 662 | window.app = { 663 | init: function () { 664 | domready(function(){ 665 | 666 | document 667 | .getElementById('visualize') 668 | .addEventListener('click', fetchDHT); 669 | 670 | }); 671 | } 672 | }; 673 | 674 | window.app.init(); 675 | 676 | function fetchDHT(){ 677 | xhr({ 678 | uri: "http://localhost:9000/dht", 679 | headers: { 680 | "Content-Type": "application/json" 681 | } 682 | }, function (err, resp, body) { 683 | drawDHT(JSON.parse(body)); 684 | }); 685 | } 686 | 687 | function cartesianCoordinates(id, r) { 688 | var maxId = new Id(Id.spin()).toDec(); 689 | var radId = id / (maxId / (2 * Math.PI)); 690 | 691 | return { 692 | y: Math.sin(radId - Math.PI / 2) * r , 693 | x: Math.cos(radId - Math.PI / 2) * r 694 | }; 695 | 696 | } 697 | 698 | function drawDHT(dht) { 699 | 700 | var R = 200 701 | var peers = []; 702 | 703 | Object.keys(dht).map(function (key){ 704 | var peer = { 705 | peerId: key, 706 | fingerTable: dht[key].fingerTable, 707 | predecessorId: dht[key].predecessorId 708 | }; 709 | 710 | peers.push(peer); 711 | //Add the peer to the global table too to 712 | //make it easier to lookup coords by id 713 | console.log('fill in peer'); 714 | dht[key].peer = peer; 715 | }); 716 | 717 | // add their coordinates 718 | peers.forEach(function (peer) { 719 | peer.coordinates = cartesianCoordinates( 720 | new Id(peer.peerId).toDec(), R); 721 | }); 722 | 723 | var vis = d3.select('#dht-ring') 724 | .append('svg'); 725 | 726 | vis.attr("width", 600) 727 | .attr("height", 600); 728 | 729 | var plane = vis.append("g") 730 | //centering 731 | .attr("transform", function(peer, i){ 732 | return "translate(" + 1.2 * R + "," + 1.2 * R + ")"; 733 | }) 734 | 735 | //separate the overall peer selection, from the on-enter-groups 736 | var peer = plane.selectAll("peers") 737 | .data(peers); 738 | 739 | var gs = peer.enter() 740 | .append("g") 741 | 742 | gs.append("svg:circle") 743 | .attr("r", "4px") 744 | .attr("fill", "black") 745 | 746 | gs.append("svg:text") 747 | .attr("dx", 5) 748 | .attr("dy", ".35em") 749 | .attr("fill", "black") 750 | .text(function(peer) { return peer.peerId; }); 751 | 752 | gs.attr("transform", function(peer, i){ 753 | return "translate(" + (peer.coordinates.x ) + "," + peer.coordinates.y + ")"; 754 | }); 755 | 756 | var arcBetween = function (source, target) { 757 | var dx = target.coordinates.x - source.coordinates.x; 758 | var dy = target.coordinates.y - source.coordinates.y; 759 | var dr = Math.sqrt(dx * dx + dy * dy); 760 | 761 | //We want to draw the line from 0,0 of the group (which is where 762 | //the dot is rendered), to the delta of the too points, since we 763 | //are drawing relative to the source position, not the canvas 764 | return "M" + 0 + 765 | "," + 0 + 766 | "A" + dr + 767 | "," + dr + 768 | " 0 0,1 " + dx + 769 | "," + dy; 770 | }; 771 | 772 | //Create a new sub-selection joining source's to their fingers 773 | var links = peer.selectAll('.links') 774 | .data(function (d) { 775 | //Should return the nested dataset, in this case an array of 776 | // [arcId, sourcePeer, targetPeer] 777 | var arcs = Object.keys(d.fingerTable).map(function (key) { 778 | return { 779 | arcId: d.peerId + '-' + key, 780 | source: d, 781 | target: dht[d.fingerTable[key].current].peer 782 | }; 783 | }); 784 | 785 | return arcs; 786 | }); 787 | 788 | //for all the links (a nested selection across sources and targets 789 | //draw an arc 790 | links.enter() 791 | .append("path") 792 | .attr('class', 'link') 793 | .attr("d", function (link) { 794 | return arcBetween(link.source, link.target); 795 | }); 796 | 797 | } 798 | 799 | },{"dht-id":3,"domready":4,"xhr":5}]},{},[12]); 800 | -------------------------------------------------------------------------------- /src/views/index.jade: -------------------------------------------------------------------------------- 1 | html(lang='en') 2 | head 3 | meta(charset='utf-8') 4 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 5 | meta(name='viewport', content='width=device-width, initial-scale=1') 6 | 7 | link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css') 8 | link(rel='stylesheet', href='/css/style.css') 9 | 10 | body 11 | 12 | .container 13 | .row 14 | h1 DHT Visualizer 15 | br 16 | button(type="button" id="visualize") visualize 17 | hr 18 | .row 19 | .col-md-3 20 | .row 21 | h4 peer detail 22 | #peer-detail 23 | .col-md-9 24 | h4 DHT Ring 25 | #dht-ring 26 | 27 | script(src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.1/d3.min.js') 28 | script(src='/js/app.js') 29 | --------------------------------------------------------------------------------