fcn-wrtc
Version:
Fully Connected Network over WebRTC connection
229 lines (209 loc) • 6.69 kB
JavaScript
const EventEmitter = require('events');
const uuid = require('uuid/v4');
const _ = require('lodash');
const N2N = require('n2n-overlay-wrtc');
const io = require('socket.io-client');
const Q = require('q');
const debug = require('debug')('fcn-wrtc');
/**
* Fully Connected Network Main Class
*/
class Fcn extends N2N {
/**
* Constructor of the FCN main class
* @param {object} options Options object
* @see https://github.com/ran3d/n2n-overlay-wrtc.git for Options
*/
constructor(options = {}){
super(_.merge({
pid: 'fullmesh',
signalingAdress: 'http://localhost:3000',
room: 'fullmesh-default',
verbose: false,
origins:'*'
}, options));
this.id = uuid();
this.connected = 'disconnected';
// Connection to the signaling server
this.signaling = io.connect(this.options.signalingAdress, {origins: options.origins});
this.directCallback = (src, dest) => {
this._log(`Direct connection between: `, src.getOutviewId(), ` and `, dest.getOutviewId());
return (offer) => {
dest.connect( (answer) => {
src.connect(answer);
}, offer);
};
};
this.signalingInit = () => {
return (offer) => {
this.signaling.emit('new', {offer, room: this.options.room});
};
};
this.signaling.on('new_spray', (data) => {
const signalingAccept = (offer) => {
this.signaling.emit('accept', { offer, room: this.options.room });
};
this.connect(signalingAccept, data);
});
this.signaling.on('accept_spray', (data) => {
this.connect(data);
});
this.on('receive', (id, message) => {
if(message && message.signal && message.inviewId && message.outviewId && message.signal === 'connect'){
this._log('Message received: ' , id, message);
// connect the neighbor to all other neighbour by bridge
this._connect2AllNeighbours(id, message);
}else {
// handle message not for us...
this._receive(id, message);
}
});
}
/**
* Handle incoming message
* @param {string} id id of the sender
* @param {object} message message
* @return {void}
*/
_receive(id, message){
throw new Error('[FCN-WRTC] Message not handled from:' +id);
}
/**
* Get an array of outview IDs. (i.e. outgoing connections)
* @return {array} array of outview IDs
*/
getNeighbours () {
return this.getPeers().o;
}
/**
* Get an object containing outgoing/ingoing arcs/connections
* @return {object} { i: [...], o[...]}
*/
getPeers () {
let resO = [], resI = [];
this.getOutview().forEach( (v, k) => resO.push(k));
this.getInview().forEach( (v, k) => resI.push(k));
return {i: _.without(resI, this.getOutviewId()), o: _.without(resO, this.getInviewId())};
}
/**
* Connection the socket to another socket or use the signalingAdress to contact the signaling server to connect the socket to the network
* @param {Fcn} socket Optionnal
* @param {number} timeout Optionnal, connection timeout = 60 seconds
* @param {string} joinRoomEmit Optionnal, signal to emit join order to the socket.io server
* @param {string} joinRommOn Optionnal, signal to listening join event from the socket.io server
*/
connection (socket = undefined, timeout = 60000, joinRoomEmit = 'joinRoom', joinRoomOn = 'joinedRoom' ) {
return this._connection(socket, timeout, joinRoomEmit, joinRoomOn);
}
/**
* @private
*/
_connection (socket = undefined, timeout = 60000, joinRoomEmit = 'joinRoom', joinRoomOn = 'joinedRoom' ) {
this._log('Pending connection...');
const self = this;
return Q.Promise(function (resolve, reject) {
try {
if(socket) {
self.join(self.directCallback(self, socket)).then(() => {
self.emit('connected', { room: self.options.room });
}).catch(error => {
self._log(error);
if(error === 'connected') {
resolve(true);
} else {
reject(error);
}
});
} else {
self.signaling.emit(joinRoomEmit, { room: self.options.room });
self.signaling.once(joinRoomOn, () => {
self._log(' Joined the room', self.options.room);
self.join(self.signalingInit()).then(() => {
self.emit('connected', { room: self.options.room });
}).catch(error => {
self._log(error);
if(error === 'connected') {
resolve(true);
} else {
reject(error);
}
});
});
}
self.once('connected', () => {
self._log(`@${self.id} is now connected`);
self.connected = 'connected';
resolve(true);
});
setTimeout(() => {
reject();
}, timeout);
} catch (error) {
reject(error);
}
});
}
/**
* Joining a network.
* @param {callback} sender Function that will be called each time an offer
* arrives to this peer. It is the responsability of the caller to send
* these offer (using sender) to the contact inside the network.
* @returns {Promise} A promise that is resolved when the peer joins the
* network -- the resolve contains the peerId; rejected after a timeout, or
* already connected state.
*/
join (sender) {
let result = new Promise( (resolve, reject) => {
this.once('open', (peerId) => {
(this.connected === 'connected') && reject(new Error('Already connected.'));
this._sendConnectMessage(peerId);
resolve(peerId);
});
});
// #3 engage the very first connection of this peer
this.connect(sender);
return result;
};
/**
* Send a message from the new peer to a neighbor in the network to connect us to the whole network.
* @private
*/
_sendConnectMessage(id){
const m = {
signal: 'connect',
inviewId: this.getInviewId(),
outviewId: this.getOutviewId()
};
this.send(id, m);
}
/**
* Connect the from peer to the whole network.
* @private
*/
_connect2AllNeighbours (from, message) {
this._log('[Connection2AllNeighbours] ', this.getPeers());
// we have to established the connection to our neighbour
this.connect(null, from);
// now for all peers except this one, we have to connect our neighbor to our other neighbors
let neigh = this.getNeighbours();
neigh = _.without(neigh, from);
if(neigh.length > 0) {
neigh.forEach(id => {
const entry1 = from;
const entry2 = id;
if(entry1 && entry2){
this._log(`Bridge between: (${entry1}, ${entry2}))`);
this.connect(entry1, entry2);
this.connect(entry2, entry1);
}
});
}
}
/**
* @private
*/
_log(...args){
if(this.options.verbose) debug('[FCN@'+this.getOutviewId()+'] ', args);
}
}
module.exports = { Fcn, uuid };