├── .gitignore ├── bin ├── server ├── node └── root ├── .travis.yml ├── public └── index.html ├── src ├── browser.js └── index.js ├── LICENSE ├── package.json ├── test └── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | public/browsernode.js 3 | -------------------------------------------------------------------------------- /bin/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Server = require('webrtc-bootstrap').Server 3 | var path = require('path') 4 | 5 | var secret = process.env.SECRET || 'SECRET' 6 | var port = process.env.PORT || 5000 7 | var publicDir = path.join(__dirname, '..', 'public') 8 | 9 | console.log(publicDir) 10 | 11 | var server = new Server(secret, { 12 | port: port, 13 | public: publicDir 14 | }) 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "node" 5 | - "7" 6 | - "6" 7 | - "5" 8 | - "4" 9 | 10 | addons: 11 | apt: 12 | sources: 13 | - ubuntu-toolchain-r-test 14 | packages: 15 | - g++-4.8 16 | env: 17 | global: 18 | - CXX=g++-4.8 19 | 20 | branches: 21 | only: 22 | - master 23 | notifications: 24 | email: 25 | recipients: 26 | - github@ericklavoie.com 27 | on_success: change 28 | on_failure: always 29 | -------------------------------------------------------------------------------- /bin/node: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var argv = require('minimist')(process.argv) 3 | var Client = require('webrtc-bootstrap') 4 | var Node = require('..') 5 | var debug = require('debug') 6 | var wrtc = require('wrtc') 7 | var log = debug('webrtc-tree-overlay:node') 8 | var host = argv.host ? argv.host : 'genet.herokuapp.com' 9 | var origin = argv.origin ? argv.origin : process.env.PWD 10 | 11 | log('connecting to ' + host) 12 | var node = new Node(new Client(host), { peerOpts: { wrtc: wrtc } }).join() 13 | 14 | node.on('parent-connect', function (channel) { 15 | log('connected to root') 16 | 17 | channel.on('data', function (data) { 18 | log('child received: ' + String(data)) 19 | channel.send(origin) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 |

Genet: WebRTC Tree Overlay Server

22 | 24 | 25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | var Client = require('webrtc-bootstrap') 2 | var Node = require('..') 3 | var debug = require('debug') 4 | var _log = debug('webrtc-tree-overlay:browser') 5 | 6 | function log (s) { 7 | var text = document.getElementById('log').textContent 8 | document.getElementById('log').textContent = text + s + '\n' 9 | _log(s) 10 | } 11 | 12 | module.exports = function (host, origin, secure) { 13 | log('connecting to ' + host + ' from ' + origin) 14 | var node = new Node(new Client(host, { secure: secure })).join() 15 | 16 | node.on('parent-connect', function (channel) { 17 | log('connected to root') 18 | 19 | channel.on('data', function (data) { 20 | log('child received: ' + String(data)) 21 | channel.send(origin) 22 | }) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /bin/root: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var argv = require('minimist')(process.argv) 3 | var Client = require('webrtc-bootstrap') 4 | var electronWebRTC = require('electron-webrtc') 5 | var Node = require('..') 6 | var debug = require('debug') 7 | var log = debug('webrtc-tree-overlay:root') 8 | var secret = argv.secret ? argv.secret : 'SECRET' 9 | var host = argv.host ? argv.host : 'genet.herokuapp.com' 10 | 11 | if (argv['wrtc']) { 12 | var wrtc = require('wrtc') 13 | } else { // including --electron-webrtc 14 | var wrtc = electronWebRTC({ headless: false }) 15 | } 16 | 17 | log('connecting to ' + host) 18 | var root = new Node(new Client(host), { peerOpts: { wrtc: wrtc } }).becomeRoot(secret, function () { 19 | log('connected to ' + host) 20 | }) 21 | 22 | root.on('child-connect', function (channel) { 23 | log('root connected to child ' + channel.id) 24 | 25 | channel.on('data', function (data) { 26 | console.log('child ' + channel.id.slice(0,5) + ' connected from ' + String(data)) 27 | }) 28 | 29 | log('root sending ping to child ' + channel.id) 30 | channel.send('ping') 31 | }) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Erick Lavoie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-tree-overlay", 3 | "version": "1.2.0", 4 | "description": "Dynamically maintain a tree overlay with WebRTC", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "DEBUG='webrtc-tree-overlay*,webrtc-bootstrap*' bin/server", 8 | "postinstall": "browserify src/browser.js -r -s browsernode -o public/browsernode.js;", 9 | "test": "tape test/index.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/elavoie/webrtc-tree-overlay.git" 14 | }, 15 | "keywords": [ 16 | "webrtc", 17 | "overlay" 18 | ], 19 | "author": "Erick Lavoie", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/elavoie/webrtc-tree-overlay/issues" 23 | }, 24 | "homepage": "https://github.com/elavoie/webrtc-tree-overlay#readme", 25 | "dependencies": { 26 | "browserify": "^16.2.3", 27 | "debug": "^2.6.9", 28 | "electron-webrtc": "^0.3.0", 29 | "event-emitter": "^0.3.5", 30 | "minimist": "^1.2.0", 31 | "webrtc-bootstrap": "^4.1.2" 32 | }, 33 | "devDependencies": { 34 | "tape": "^4.10.1", 35 | "wrtc": "^0.3.7" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var Client = require('webrtc-bootstrap') 3 | var Server = require('webrtc-bootstrap').Server 4 | var wrtc = require('wrtc') 5 | var Node = require('..') 6 | var debug = require('debug') 7 | var log = debug('test') 8 | var secret = 'secret' 9 | var port = 5000 10 | 11 | tape('Connection test', function (t) { 12 | t.timeoutAfter(5 * 1000 /* MS */) 13 | var server = new Server(secret, { 14 | port: port, 15 | timeout: 5000 16 | }) 17 | t.ok(server) 18 | var bootstrap = new Client('localhost:' + port) 19 | t.ok(bootstrap) 20 | 21 | var root = new Node(bootstrap, { peerOpts: { wrtc: wrtc } }).becomeRoot(secret) 22 | var node = new Node(bootstrap, { peerOpts: { wrtc: wrtc } }).join() 23 | 24 | var rootConnected = new Promise(function (resolve, reject) { 25 | root.on('child-connect', function (channel) { 26 | log('root connected to child') 27 | t.ok(channel) 28 | resolve(channel) 29 | }) 30 | }) 31 | 32 | var nodeConnected = new Promise(function (resolve, reject) { 33 | node.on('parent-connect', function (channel) { 34 | log('child connected to root') 35 | t.ok(channel) 36 | resolve(channel) 37 | }) 38 | }) 39 | 40 | Promise.all([rootConnected, nodeConnected]).then(function () { 41 | log('closing root and node') 42 | node.close() 43 | root.close() 44 | server.close() 45 | bootstrap.close() 46 | t.end() 47 | }) 48 | }) 49 | 50 | tape('README example', function (t) { 51 | t.timeoutAfter(5 * 1000 /* MS */) 52 | var server = new Server(secret, { 53 | port: port, 54 | timeout: 5000 55 | }) 56 | t.ok(server) 57 | var bootstrap = new Client('localhost:' + port) 58 | t.ok(bootstrap) 59 | 60 | var root = new Node(bootstrap, { peerOpts: { wrtc: wrtc } }).becomeRoot(secret) 61 | var node = new Node(bootstrap, { peerOpts: { wrtc: wrtc } }).join() 62 | 63 | var rootReceivedPong = new Promise(function (resolve, reject) { 64 | root.on('child-connect', function (channel) { 65 | log('root connected to child') 66 | t.ok(channel) 67 | 68 | channel.on('data', function (data) { 69 | log('root received: ' + data.toString()) 70 | t.equal(data.toString(), 'pong') 71 | resolve(true) 72 | }) 73 | channel.send('ping') 74 | }) 75 | }) 76 | 77 | var nodeReceivedPing = new Promise(function (resolve, reject) { 78 | node.on('parent-connect', function (channel) { 79 | log('child connected to root') 80 | t.ok(channel) 81 | 82 | channel.on('data', function (data) { 83 | log('child received: ' + data.toString()) 84 | t.equal(data.toString(), 'ping') 85 | channel.send('pong') 86 | resolve(true) 87 | }) 88 | }) 89 | }) 90 | 91 | Promise.all([rootReceivedPong, nodeReceivedPing]).then(function () { 92 | log('closing root and node') 93 | node.close() 94 | root.close() 95 | server.close() 96 | bootstrap.close() 97 | t.end() 98 | }) 99 | }) 100 | 101 | tape('Maximum Degree Property', function (t) { 102 | var server = new Server(secret, { 103 | port: port, 104 | timeout: 5000 105 | }) 106 | t.ok(server) 107 | var bootstrap = new Client('localhost:' + port) 108 | t.ok(bootstrap) 109 | 110 | var parentConnections = 0 111 | function onParentConnect (node) { 112 | return new Promise(function (resolve, reject) { 113 | node.on('parent-connect', function (channel) { 114 | log('connected to parent') 115 | parentConnections++ 116 | t.ok(channel) 117 | resolve(true) 118 | }) 119 | }) 120 | } 121 | 122 | var MAX_DEGREE = 2 123 | var NB_NODES = 10 124 | t.timeoutAfter(NB_NODES * 2000 /* MS */) 125 | var startTime = Date.now() 126 | var root = new Node(bootstrap, { maxDegree: MAX_DEGREE, peerOpts: { wrtc: wrtc } }).becomeRoot(secret) 127 | 128 | var nodes = [] 129 | for (var i = 0; i < NB_NODES; ++i) { 130 | nodes.push( 131 | new Node(bootstrap, { maxDegree: MAX_DEGREE, peerOpts: { wrtc: wrtc } }) 132 | .join()) 133 | } 134 | 135 | var childrenConnections = 0 136 | var childrenConnected = new Promise(function (resolve, reject) { 137 | function onChildConnect (node) { 138 | node.on('child-connect', function (channel) { 139 | log(++childrenConnections + ' connected child') 140 | t.ok(channel) 141 | 142 | if (childrenConnections === NB_NODES) { 143 | resolve(true) 144 | } 145 | }) 146 | } 147 | 148 | nodes.map(onChildConnect) 149 | onChildConnect(root) 150 | }) 151 | 152 | Promise.all([Promise.all(nodes.map(onParentConnect)), childrenConnected]) 153 | .then(function () { 154 | log('all nodes connected') 155 | t.equal(parentConnections, NB_NODES) 156 | 157 | log('root has ' + root.childrenNb + ' children, ' + 158 | root.candidateNb + ' candidate(s), ' + 159 | root._storedRequests.length + ' stored request(s)') 160 | 161 | for (var i = 0; i < NB_NODES; ++i) { 162 | log('node(' + nodes[i].id + ') has ' + nodes[i].childrenNb + ' children and ' + 163 | nodes[i].candidateNb + ' candidate(s), ' + 164 | nodes[i]._storedRequests.length + ' stored request(s)') 165 | t.ok(nodes[i].childrenNb <= MAX_DEGREE) 166 | } 167 | 168 | log('closing root and nodes') 169 | nodes.map(function (n) { n.close() }) 170 | root.close() 171 | server.close() 172 | bootstrap.close() 173 | console.log('Test took ' + (Date.now() - startTime) + ' ms to execute for ' + NB_NODES + ' nodes of maximum degree ' + MAX_DEGREE) 174 | t.end() 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webrtc-tree-overlay 2 | 3 | Dynamically maintains a tree overlay topology from nodes connected by WebRTC as 4 | they join and leave. Each node accepts a maximum number of new connections after 5 | which the newer connection requests are delegated to children. 6 | 7 | Requires a publicly accessible server for bootstrapping the connections, 8 | provided by [webrtc-bootstrap](https://github.com/elavoie/webrtc-bootstrap). 9 | 10 | # Usage 11 | 12 | // On the root node 13 | 14 | var bootstrap = require('webrtc-bootstrap')('server hostname or ip:port') 15 | var Node = require('webrtc-tree-overlay') 16 | 17 | var root = new Node(bootstrap).becomeRoot('secret') 18 | root.on('child-connect', function (channel) { 19 | channel.send('ping') 20 | channel.on('data', function (data) { 21 | console.log(data) 22 | }) 23 | }) 24 | 25 | 26 | // On a child node 27 | 28 | var boostrap = ... 29 | var Node = ... 30 | 31 | var node = new Node(bootstrap).join() 32 | node.on('parent-connect', function (channel) { 33 | channel.on('data', function (data) { 34 | console.log(data) 35 | channel.send('pong') 36 | }) 37 | }) 38 | node.on('child-connect', function (channel) { 39 | channel.send('ping') 40 | }) 41 | 42 | # Example Application 43 | 44 | Server process: 45 | ```` 46 | DEBUG='webrtc-tree-overlay*,webrtc-bootstrap*' bin/server 47 | ```` 48 | 49 | Root process: 50 | ```` 51 | DEBUG='webrtc-tree-overlay*,webrtc-bootstrap*' bin/root --host localhost:5000 52 | ```` 53 | 54 | Node process(es): 55 | ```` 56 | DEBUG='webrtc-tree-overlay*,webrtc-bootstrap*' bin/node --host localhost:5000 --origin nodejs_node 57 | ```` 58 | 59 | Browser process(s): 60 | ```` 61 | open http://localhost:5000/#browser_node 62 | ```` 63 | 64 | # API 65 | 66 | ## Node(bootstrap, [opts]) 67 | 68 | *bootstrap* is a 69 | [webrtc-bootstrap](https://github.com/elavoie/webrtc-bootstrap) connected 70 | client. 71 | 72 | *opts* has the following defaults: 73 | 74 | { 75 | peerOpts: {}, 76 | maxDegree: 10, 77 | requestTimeoutInMs: 30 * 1000 78 | } 79 | 80 | where 81 | - `opts.peerOpts` are options passed to [SimplePeer](https://github.com/feross/simple-peer) 82 | - `opts.maxDegree` is the maximum number of children a node will keep 83 | - `opts.requestTimeoutInMs` the upper bound on the time a candidate will be considered for joining. If the connection handshake has not been successfully before the end of the interval, the candidate is rejected. 84 | 85 | ## Node.children 86 | 87 | The current dictionary of children channels. 88 | 89 | ## Node.childrenNb 90 | 91 | The current number of children. 92 | 93 | ## Node.maxDegree 94 | 95 | The maximum number of children and candidates that are kept. If a join request arrives while it will either be passed to one of the children, or kept until one a candidate has become a connected child. 96 | 97 | ## Node.parent 98 | 99 | The parent channel, null if the node has not joined yet or is the root. 100 | 101 | ### Node.becomeRoot(secret[, cb]) 102 | 103 | Become root (through the bootstrap client), after which the node will automatically handle join requests. 104 | 105 | ### Node.join() 106 | 107 | Join an existing tree (through the bootstrap client). 108 | 109 | ### Node.close() 110 | 111 | Close the node and all associated channels. 112 | 113 | ### Node.on('data', function (data, channel, isParent)) 114 | - *data* received from of the direct neighbours, either parent or children; 115 | - *channel*: on which it was received; 116 | - *isParent* whether the channel is from our parent (`true`) or one of our children (`false`). 117 | 118 | ### Node.on('parent-connect', function (channel)) 119 | 120 | When the node is ready to communicate with its parent through *channel*. 121 | 122 | ### Node.on('parent-close', function (channel)) 123 | 124 | When the parent *channel* has closed. 125 | 126 | ### Node.on('parent-error', function (channel, err)) 127 | 128 | When the parent *channel* has failed become of an error *err*. 129 | 130 | ### Node.on('child-connect', function (channel)) 131 | 132 | When the node is ready to communicate with new child through *channel*. 133 | 134 | ### Node.on('child-close', function (channel)) 135 | 136 | When the child *channel* has closed. 137 | 138 | ### Node.on('child-error', function (channel, err)) 139 | 140 | When then child *channel* has failed because of *err*. 141 | 142 | 143 | ## Channel 144 | 145 | Abstracts the underlying WebRTC channel to multiplex the tree join protocol with application-level messages. Lightweight messages can be sent on the topology on those channels. However, for any heavy data traffic a dedicated SimplePeer connection should be established. The channel can be used for that purpose. 146 | 147 | The constructor is called internally by a Node during the joining process and it is not available publicly. 148 | 149 | ### Channel.id 150 | 151 | Identifier of the channel, different from the Node.id on either side. From the point of view of a child, the id of its parent is always null. From the point of view of a parent, each child has a non-null unique id. 152 | 153 | ### Channel.destroy() 154 | 155 | Closes the channel. 156 | 157 | ### Channel.isParent() 158 | 159 | Return true if the channel is used to communicate with the Node's parent. 160 | 161 | ### Channel.send(data) 162 | 163 | Sends *data* to the neighbour (parent or child) through the channel. 164 | 165 | 166 | ### Channel.on('close', function ()) 167 | 168 | Fires when the channel has closed. 169 | 170 | ### Channel.on('data', function (data)) 171 | 172 | Fires when data has arrived. 173 | 174 | ### Channel.on('error', function (err)) 175 | 176 | Fires an error aborted the channel. 177 | 178 | ### Channel.on('join-request', function (req)) 179 | 180 | Used internally. Fires when a join request has arrived. An application should not need to bother with those. 181 | 182 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var EE = require('event-emitter') 2 | var debug = require('debug') 3 | 4 | function hash (value) { 5 | // Robert Jenkins' 32 bit integer hash function, adapted to return 31-bit number 6 | value = ((value + 0x7ed55d16) + (value << 12)) & 0x7fffffff 7 | value = ((value ^ 0xc761c23c) ^ (value >>> 19)) & 0x7fffffff 8 | value = ((value + 0x165667b1) + (value << 5)) & 0x7fffffff 9 | value = ((value + 0xd3a2646c) ^ (value << 9)) & 0x7fffffff 10 | value = ((value + 0xfd7046c5) + (value << 3)) & 0x7fffffff 11 | value = ((value ^ 0xb55a4f09) ^ (value >>> 16)) & 0x7fffffff 12 | return value 13 | } 14 | 15 | // Wraps the WebRTC socket inside a channel to encapsulate 16 | // the join-request protocol while allowing application-defined control 17 | // protocols to be multiplexed 18 | function Channel (id, socket) { 19 | var log = debug('webrtc-tree-overlay:channel(' + id + ')') 20 | this._log = log 21 | var self = this 22 | this.id = id 23 | this._socket = socket 24 | .on('data', function (data) { 25 | log('received data:') 26 | log(data.toString()) 27 | var message = JSON.parse(data) 28 | if (message.type === 'DATA') { 29 | log('data: ' + message.data) 30 | self.emit('data', message.data) 31 | } else if (message.type === 'JOIN-REQUEST') { 32 | log('join-request: ' + JSON.stringify(message)) 33 | self.emit('join-request', message) 34 | } else { 35 | throw new Error('Invalid message type on channel(' + id + ')') 36 | } 37 | }) 38 | .on('connect', function () { 39 | self.emit('connect', self) 40 | }) 41 | .on('close', function () { 42 | log('closing') 43 | self.emit('close') 44 | }) 45 | .on('error', function (err) { 46 | log(err.message) 47 | log(err.stack) 48 | }) 49 | } 50 | EE(Channel.prototype) 51 | 52 | Channel.prototype.send = function (data) { 53 | var message = JSON.stringify({ 54 | type: 'DATA', 55 | data: data 56 | }) 57 | this._log('sending:') 58 | this._log(message) 59 | this._socket.send(message) 60 | } 61 | 62 | Channel.prototype._sendJoinRequest = function (req) { 63 | this._log('sending join request from ' + req.origin) 64 | if (req.type !== 'JOIN-REQUEST') { 65 | throw new Error('Invalid join request') 66 | } 67 | 68 | this._socket.send(JSON.stringify(req)) 69 | } 70 | 71 | Channel.prototype.isParent = function () { 72 | return this.id === null 73 | } 74 | 75 | Channel.prototype.destroy = function () { 76 | this._socket.destroy() 77 | } 78 | 79 | function Node (bootstrap, opts) { 80 | if (!bootstrap) { 81 | throw new Error('Missing bootstrap client argument') 82 | } 83 | this.bootstrap = bootstrap 84 | 85 | opts = opts || {} 86 | 87 | this.id = hash(Math.floor(Math.random() * 4294967296)).toString().slice(0, 6) 88 | this._log = debug('webrtc-tree-overlay:node(' + this.id + ')') 89 | this.parent = null 90 | this.children = {} 91 | this.childrenNb = 0 92 | this._candidates = {} 93 | this._candidateNb = 0 94 | this.peerOpts = opts.peerOpts || {} 95 | this.maxDegree = opts.maxDegree || 10 96 | this._REQUEST_TIMEOUT_IN_MS = opts.requestTimeoutInMs || 30 * 1000 97 | 98 | this._storedRequests = {} 99 | for (var i = 0; i < this.maxDegree; i++) { 100 | this._storedRequests[i] = [] 101 | } 102 | } 103 | EE(Node.prototype) 104 | 105 | Node.prototype.join = function (cb) { 106 | var self = this 107 | 108 | self._log('creating a peer connection with options:') 109 | self._log(this.peerOpts) 110 | 111 | this.parent = new Channel( 112 | null, 113 | this.bootstrap.connect(null, { 114 | peerOpts: this.peerOpts, 115 | timeout: self._REQUEST_TIMEOUT_IN_MS, 116 | cb: function (err) { 117 | if (err) { 118 | self._log('connection to parent failed') 119 | self.parent.destroy() 120 | self.parent = null 121 | if (cb) { cb(err) } 122 | } else { 123 | if (cb) { cb(null) } 124 | } 125 | } 126 | })) 127 | 128 | this.parent 129 | .on('join-request', this._handleJoinRequest.bind(this)) 130 | .on('data', function (data) { 131 | self.emit('data', data, self.parent, true) 132 | }) 133 | .on('connect', function () { 134 | self._log('connected to parent') 135 | self.emit('parent-connect', self.parent) 136 | if (cb) { cb(null) } 137 | }) 138 | .on('close', function () { 139 | self._log('parent closed') 140 | self.emit('parent-close', self.parent) 141 | self.parent = null 142 | }) 143 | .on('error', function (err) { 144 | self._log('parent error: ' + err) 145 | self.emit('parent-error', self.parent, err) 146 | self.parent = null 147 | }) 148 | 149 | return this 150 | } 151 | 152 | Node.prototype._handleJoinRequest = function (req) { 153 | var self = this 154 | self._log('_handleJoinRequest(' + req.origin + ')') 155 | self._log( 156 | 'childrenNb: ' + this.childrenNb + 157 | ', _candidateNb: ' + this._candidateNb + 158 | ', maxDegree: ' + this.maxDegree) 159 | if (this._candidates.hasOwnProperty(req.origin)) { 160 | self._log('forwarding request to one of our candidates (' + req.origin.slice(0, 4) + ')') 161 | // A candidate is sending us more signal information 162 | this._candidates[req.origin]._socket.signal(req.signal) 163 | } else if (this.childrenNb + this._candidateNb < this.maxDegree) { 164 | self._log('creating a new candidate (' + req.origin + ')') 165 | // We have connections available for a new candidate 166 | this.createCandidate(req) 167 | } else { 168 | // Let one of our children handle this candidate 169 | this._delegate(req) 170 | } 171 | } 172 | 173 | Node.prototype._addChild = function (child) { 174 | this.childrenNb++ 175 | 176 | var childIdx = null 177 | for (var i = 0; i < this.maxDegree; ++i) { 178 | if (!this.children[i]) { 179 | childIdx = i 180 | this.children[i] = child 181 | break 182 | } 183 | } 184 | 185 | if (childIdx === null) { 186 | this._log('children:') 187 | this._log(this.children) 188 | throw new Error('No space found for adding new child') 189 | } 190 | 191 | this._removeCandidate(child.id) 192 | return childIdx 193 | } 194 | 195 | Node.prototype._removeChild = function (child) { 196 | 197 | var childIdx = null 198 | for (var i = 0; i < this.maxDegree; ++i) { 199 | if (this.children[i] === child) { 200 | childIdx = i 201 | delete this.children[i] 202 | this.childrenNb-- 203 | } 204 | } 205 | 206 | return childIdx 207 | } 208 | 209 | Node.prototype.createCandidate = function (req) { 210 | var self = this 211 | // Use the ID assigned by the bootstrap server to the originator 212 | // for routing requests 213 | var child = new Channel( 214 | req.origin, 215 | this.bootstrap.connect(req, { peerOpts: this.peerOpts }) 216 | ) 217 | .on('connect', function () { 218 | self._log('child (' + JSON.stringify(child.id) + ') connected') 219 | clearTimeout(timeout) 220 | var childIdx = self._addChild(child) 221 | 222 | // Process stored requests that belong to this child 223 | var storedRequests = self._storedRequests[childIdx].slice(0) 224 | self._storedRequests[childIdx] = [] 225 | 226 | storedRequests.forEach(function (req) { 227 | child._sendJoinRequest(req) 228 | }) 229 | self.emit('child-connect', child) 230 | }) 231 | .on('data', function (data) { 232 | self.emit('data', data, child, false) 233 | }) 234 | .on('join-request', function (req) { 235 | self._handleJoinRequest(req) 236 | }) 237 | .on('close', function () { 238 | self._log('child (' + JSON.stringify(child.id) + ') closed') 239 | self._removeChild(child) 240 | self._removeCandidate(child.id) 241 | self.emit('child-close', child) 242 | }) 243 | .on('error', function (err) { 244 | self._log('child (' + JSON.stringify(child.id) + ') error: ') 245 | self._log(err) 246 | self._removeChild(child) 247 | self._removeCandidate(child.id) 248 | self.emit('child-error', child, err) 249 | }) 250 | 251 | var timeout = setTimeout(function () { 252 | self._log('connection to child(' + child.id + ') failed') 253 | child.destroy() 254 | self._removeCandidate(child.id) 255 | }, self._REQUEST_TIMEOUT_IN_MS) 256 | 257 | this._addCandidate(child) 258 | return child 259 | } 260 | 261 | Node.prototype._addCandidate = function (peer) { 262 | var self = this 263 | if (this._candidates.hasOwnProperty(peer.id)) { 264 | if (this._candidates[peer.id] !== peer) { 265 | throw new Error('Adding a different candidate with the same identifier as an existing one') 266 | } else { 267 | self._log('WARNING: re-adding the same candidate ' + peer.id) 268 | } 269 | } else { 270 | self._log('added candidate (' + peer.id + ')') 271 | this._candidates[peer.id] = peer 272 | this._candidateNb++ 273 | } 274 | } 275 | 276 | Node.prototype._removeCandidate = function (id) { 277 | var self = this 278 | if (this._candidates.hasOwnProperty(id)) { 279 | delete this._candidates[id] 280 | this._candidateNb-- 281 | self._log('removed candidate (' + id + ')') 282 | } else { 283 | self._log('candidate (' + id + ') not found, it may have been removed already') 284 | } 285 | } 286 | 287 | Node.prototype._delegateIndex = function (req) { 288 | // Deterministically choose one of our children, regardless of whether it is 289 | // connected or not at the moment. 290 | // 291 | // We combine the first bytes of the origin address with the node 292 | // id to derive the index of the child to use. 293 | var origin = Number.parseInt(req.origin.slice(0, 6), 16) 294 | var id = Number.parseInt(this.id) 295 | var childIndex = hash(origin ^ id) % this.maxDegree 296 | this._log('_delegateIndex: ' + childIndex + ', computed from origin ' + origin + ' and node.id ' + id) 297 | return childIndex 298 | } 299 | 300 | Node.prototype._delegate = function (req) { 301 | var self = this 302 | var childIndex = self._delegateIndex(req) 303 | self._log('delegating request (' + req.origin + ') to child[' + childIndex + ']') 304 | var child = this.children[childIndex] 305 | if (child) { 306 | self._log('forwarding request (' + req.origin + ') to child (' + child.id + ')') 307 | child._sendJoinRequest(req) 308 | } else { 309 | // Defer until the corresponding candidate 310 | // has joined 311 | this._storedRequests[childIndex].push(req) 312 | } 313 | } 314 | 315 | Node.prototype.becomeRoot = function (secret, cb) { 316 | var self = this 317 | this.bootstrap.root(secret, function (req) { 318 | if (!req.type) { 319 | req.type = 'JOIN-REQUEST' 320 | } else if (req.type !== 'JOIN-REQUEST') { 321 | throw new Error('Invalid request type') 322 | } 323 | self._handleJoinRequest(req) 324 | }, function (err) { 325 | if (err) { 326 | self._log('connection to server failed') 327 | } 328 | if (cb) { return cb(err) } 329 | }) 330 | return this 331 | } 332 | 333 | Node.prototype.close = function () { 334 | if (this.parent) { 335 | this.parent.destroy() 336 | this.parent = null 337 | } 338 | 339 | for (var i = 0; i < this.children.length; ++i) { 340 | this.children[i].destroy() 341 | } 342 | this.children = [] 343 | 344 | for (var c in this._candidates) { 345 | this._candidates[c].destroy() 346 | } 347 | this._candidates = {} 348 | this._candidateNb = 0 349 | 350 | this.emit('close') 351 | } 352 | 353 | module.exports = Node 354 | --------------------------------------------------------------------------------