├── .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 |
--------------------------------------------------------------------------------