UNPKG

fcn-wrtc

Version:

Fully Connected Network over WebRTC connection

229 lines (209 loc) 6.69 kB
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 };