UNPKG

seaport

Version:

service registry and port assignment for clusters

346 lines (304 loc) 9.44 kB
var inherits = require('inherits'); var EventEmitter = require('events').EventEmitter; var indexOf = require('indexof'); var objectKeys = require('object-keys'); var semver = require('semver'); var defined = require('defined'); var secure = require('secure-peer'); var merge = require('merge'); var generateId = require('./id.js'); var Protocol = require('./protocol.js'); module.exports = Seaport; inherits(Seaport, EventEmitter); function Seaport (opts) { var self = this; if (!(this instanceof Seaport)) return new Seaport(opts); if (!opts) opts = {}; this.endpoints = []; this.services = {}; this.heartbeat = defined(opts.heartbeat, 15 * 1000); this.timeout = defined(opts.timeout, this.heartbeat * 3); this.known = {}; this.doc = this; // for legacy .doc.set() syntax, deprecated if (opts.private) { var keys = { private: opts.private, public: opts.public }; this.secure = secure(keys); if (opts.authorized) { this.authorized = opts.authorized.map(normalizeKey); } } } // each stream creates a new endpoint Seaport.prototype.createStream = function (addr) { var self = this; var p = new Protocol({ heartbeat: this.heartbeat, timeout: this.timeout, }); this.endpoints.push(p); p.on('end', onend); p.on('register', function onregister (id, meta) { // prevents loops when peers connect from both sides if (self.known[id] && self.query(meta).length) return; if (!meta.host) meta.host = self._host; if (self.services[id]) { self.services[id] = meta; } else { self.known[id] = meta; } for (var i = 0; i < self.endpoints.length; i++) { var e = self.endpoints[i]; if (e === p) continue; e.send([ 'register', id, meta ]); } self.emit('register', meta, id); }); p.on('timeout', function () { onend(); stream.emit('timeout'); }); p.on('free', function (id) { var obj = self.known[id] ? self.known : self.services; var meta = obj[id]; if (!meta) return; for (var i = 0; i < self.endpoints.length; i++) { var e = self.endpoints[i]; e.send(['free', id]); } delete obj[id]; self.emit('free', meta, id); }); p.on('synced', function () { stream.emit('synced'); self.emit('synced', stream); }); if (!addr && !self._host) { p.on('helo', function (addr) { self._host = addr; self.emit('address', addr); }); } this.emit('endpoint', p); var stream; if (this.secure) { stream = this.secure(function (s) { s.pipe(p.createStream()).pipe(s); }); stream.on('identify', function (id) { if (self._isAuthorized(id.key.public)) { id.accept(); self.emit('accept', id); } else { id.reject(); self.emit('reject', id); } }); } else { stream = p.createStream(); } stream.on('error', onend); stream.on('close', onend); p.once('close', function () { p.destroy(); var keys = objectKeys(p.known); for (var i = 0; i < keys.length; i++) { var key = keys[i]; var meta = self.known[key]; if (!meta) continue; delete self.known[key]; self.emit('free', meta, key); } }); if (addr) p.send([ 'helo', addr ]); registerServices(merge(this.known, this.services), p); self.emit('stream', stream); self.emit('protocol', p); return stream; function onend () { if (p._ended) return; p._ended = true; p.destroy(); var ix = indexOf(self.endpoints, p); self.endpoints.splice(ix, 1); var keys = objectKeys(p.known); for (var i = 0; i < keys.length; i++) { var key = keys[i]; var meta = self.known[key]; if (!meta) continue; delete self.known[key]; for (var j = 0; j < self.endpoints.length; j++) { var e = self.endpoints[j]; if (e === p) continue; e.send([ 'free', key ]); } self.emit('free', meta, key); } } }; Seaport.prototype.register = function () { return this.registerMeta.apply(this, arguments).port; }; Seaport.prototype.registerMeta = function (meta, port) { var self = this; meta = fixMeta(meta, port); if (!meta.port) { meta.port = 10000 + Math.floor(Math.random() * 55000); } var id = meta.id || generateId(); meta.id = id; this.services[id] = meta; if (!meta.host && this._host) { meta.host = this._host; register(); } else if (!meta.host) { this.once('address', function (addr) { meta.host = addr; register(); }); } else register() return meta; function register () { var mserv = {}; mserv[id] = meta; for (var i = 0; i < self.endpoints.length; i++) { registerServices(mserv, self.endpoints[i]); } } }; Seaport.prototype.free = function (meta) { var id; if (typeof meta === 'number') { meta = { port: meta }; } if (typeof meta === 'object') { var keys = objectKeys(this.services).concat(objectKeys(this.known)); var mkeys = objectKeys(meta); for (var i = 0; i < keys.length; i++) { var s = this.services[keys[i]] || this.known[keys[i]]; for (var j = 0; j < mkeys.length; j++) { if (meta[mkeys[j]] !== s[mkeys[j]]) break; } if (j === mkeys.length) { id = keys[i]; meta = this.services[id]; break; } } if (i === keys.length) return; } else { id = meta; meta = this.services[id] || this.known[id]; } if (this.services[id]) { meta = this.services[id]; delete this.services[id]; } else if (this.known[id]) { meta = this.known[id]; delete this.known[id]; } else { return; } for (var i = 0; i < this.endpoints.length; i++) { var e = this.endpoints[i]; e.send([ 'free', id ]); } this.emit('free', meta, id); }; Seaport.prototype.close = function () { this.closed = true; for (var i = 0; i < this.endpoints.length; i++) { var e = this.endpoints[i]; e.emit('close'); e.end(); } this.emit('close'); }; Seaport.prototype.get = function get (meta, cb) { var self = this; var ps = this.query(meta); if (ps.length > 0) return cb(ps); this.once('register', function () { self.get(meta, cb) }); }; Seaport.prototype.set = function (id, meta) { var records = this.services[id] ? this.services : this.known; if (!records[id]) return; records[id] = meta; if (this.services[id]) return; for (var i = 0; i < this.endpoints.length; i++) { var e = this.endpoints[i]; e.send([ 'register', id, meta ]); } }; Seaport.prototype.query = function (meta) { meta = fixMeta(meta); var mkeys = objectKeys(meta); var skeys = objectKeys(this.services); var keys = objectKeys(this.known); var rows = []; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var row = this.known[key]; if (matches(row)) rows.push(row); } for (var i = 0; i < skeys.length; i++) { var key = skeys[i]; var row = this.services[key]; if (matches(row)) rows.push(row); } return rows; function matches (row) { for (var i = 0; i < mkeys.length; i++) { var mkey = mkeys[i]; if (mkey === 'version') { if (!semver.satisfies(row.version, meta.version)) { return false; } } else if (row[mkey] !== meta[mkey]) return false; } return true; } }; Seaport.prototype._isAuthorized = function (id) { if (!this.authorized) return true; return indexOf(this.authorized, normalizeKey(id)) >= 0; }; function registerServices (services, p) { var keys = objectKeys(services); for (var i = 0; i < keys.length; i++) { var key = keys[i]; p.send([ 'register', key, services[key] ]); } p.send([ 'synced' ]); } function fixMeta (meta, port) { if (!meta) return {}; if (typeof meta === 'string') { if (typeof port === 'object') { port.role = meta; meta = port; } else meta = { role: meta }; } if (typeof port === 'number') { meta.port = port; } if (/@/.test(meta.role)) { meta.version = meta.role.split('@')[1]; meta.role = meta.role.split('@')[0]; } return meta; } function normalizeKey (id) { return (typeof id === 'string' ? id : String(id)) .replace(/^-----BEGIN \w+ \w+ KEY-----\s*/, '') .replace(/^-----END \w+ \w+ KEY-----\s*/, '') .replace(/\s+/g, '') ; }