UNPKG

thalassa-crowsnest

Version:
208 lines (172 loc) 6.29 kB
var assert = require('assert') , f = require('util').format , Thalassa = require('thalassa') , pkg = require('../package.json') , shoe = require('shoe') , crdt = require('crdt') , MuxDemux = require('mux-demux') , split = require('split') , EventEmitter = require('events').EventEmitter , util = require('util') ; global.WebSocket = require('ws'); var websocket = require('websocket-stream'); var ThalassaAgent = module.exports = function ThalassaAgent (opts) { var self = this; if (typeof opts !== 'object') opts = {}; assert(opts.host, 'opts.host must be passed!'); assert(opts.port, 'opts.port must be passed!'); assert(opts.apiport, 'opts.apiport must be passed!'); this.log = (typeof opts.log === 'function') ? opts.log : function (){}; var client = this.client = new Thalassa.Client(opts); this.log('debug', f('connecting to Thalassa server %s:%s', opts.host, opts.port)); client.start(); client.on('online', this.handleThalassaOnline.bind(this)); client.on('offline', this.handleThalassaOffline.bind(this)); client.subscribe(); client.getRegistrations(function (err, regs) { if (err) { self.log('error', 'Thalassa.getRegistrations', err); } else { regs.forEach(self.handleThalassaOnline.bind(self)); } }); this.aqueductDocs = {}; this.serviceDoc = new crdt.Doc(); this.servicesSet = this.serviceDoc.createSet('type', 'service'); this._streams = {}; this._clientMuxes = []; }; util.inherits(ThalassaAgent, EventEmitter); ThalassaAgent.prototype.handleThalassaOnline = function (service) { var self = this; var activity = { type: 'activity', time: Date.now(), verb: 'online', object: service.id }; self.emit('activity', activity); self.log('debug', 'ONLINE', service.id); self.serviceDoc.add({ id: service.id, type: 'service', service: service}); if (service.id.indexOf('/thalassa-aqueduct/') === 0) { self.log('debug', 'aqueduct server ONLINE', service); self._connectToAqueduct(service); } }; ThalassaAgent.prototype._connectToAqueduct = function(service) { var self = this; // in case the service registration has gone offline if (!self.servicesSet.has(service.id)) return; var stream = self._streams[service.id] = new websocket(util.format('ws://%s:%s/readstream', service.host, service.port)); stream.on('end', function () { mx.destroy(); // if the reference to the stream still exists, try to reconnect // otherwise it was removed because the service was unregistered if (self._streams[service.id]) self._connectToAqueduct(service); }); stream.on('error', function () { mx.destroy(); }); var doc = new crdt.Doc(); // just hack on doc.service for now doc.service = service; self.aqueductDocs[service.id] = doc; var mx = new MuxDemux(function (s) { if (s.meta.type === 'config') { s.pipe(doc.createStream()).pipe(s); //s.on('data', console.log.bind(console)) s.on('error', function () { s.destroy(); }); } else if(s.meta.type === 'stat') { s.pipe(split()) .on('data', function (line) { try { var stat = JSON.parse(line); // tack on the aqueduct host Id because we'll use it to filter stat.hostId = service.id; self.emit('stat', stat); } catch(err) { self.log('error', 'error parsing stat from stream ' + line, String(err)); } }) .on('error', function () { s.destroy(); }); } else if(s.meta.type === 'activity') { s.pipe(split()) .on('data', function (line) { try { var activityObj = JSON.parse(line); self.emit('activity', activityObj); } catch(err) { self.log('error', 'error parsing activity from stream ' + line, String(err)); } }) .on('error', function () { s.destroy(); }); } else { self.log('error', 'ignored aqueduct stream ', s.meta); } }); stream.pipe(mx).pipe(stream); mx.on('error', function () { stream.destroy(); }); for (var id in this._clientMuxes) { var s = doc.createStream(); var cx = this._clientMuxes[id]; s.pipe(cx.createStream({ type: 'aqueduct', id: doc.id, service: doc.service })).pipe(s); } stream.once('end', function () { mx.destroy(); delete self._streams[service.id]; }); stream.on('error', function (err) { self.log('error', 'error from aqueduct connection ' + service.id + ': ' + err.message); if (self._streams[service.id]) self._streams[service.id].destroy(); mx.destroy(); // try to reconnect setTimeout(function () { self._connectToAqueduct(service); }, 2000); }); stream.on('connect', function () { self.log('debug', service.id + ' connected!!!'); }); }; ThalassaAgent.prototype.handleThalassaOffline = function (id) { var self = this; var activity = { type: 'activity', time: Date.now(), verb: 'offline', object: id }; self.emit('activity', activity); self.serviceDoc.rm(id); self.log('debug', 'OFFLINE', id); if (id.indexOf('/thalassa-aqueduct/') === 0) { self.log('debug', 'aqueduct server OFFLINE', id); var doc = self.aqueductDocs[id]; if (doc) { doc.dispose(); doc.removeAllListeners(); delete self.aqueductDocs[id]; } var stream = self._streams[id]; if (stream) { // delete the referenc to the stream first so we'll know it was explicitely disconnected delete self._streams[id]; stream.destroy(); } } }; ThalassaAgent.prototype.createReadableMuxStream = function() { var self = this; var mx = new MuxDemux(); var id = Date.now() + String(Math.ceil(Math.random()*9999999)); self._clientMuxes[id] = mx; Object.keys(self.aqueductDocs).forEach(function (id) { var doc = self.aqueductDocs[id]; var mxs = mx.createStream({ id: doc.id, type: 'aqueduct', service: doc.service }); var stream = doc.createStream(); mxs.pipe(stream).pipe(mxs); }); mx.on('close', function () { delete self._clientMuxes[id]; }); // services crdt var serviceStream = self.serviceDoc.createStream({writable: false, sendClock: true}); var mxs = mx.createStream({ type: 'thalassa' }); serviceStream.pipe(mxs).pipe(serviceStream); return mx; };