@theatersoft/bus
Version:
Bus messaging connects distributed clients and services
59 lines (40 loc) • 11.3 kB
JavaScript
/*
Copyright (C) 2016-2018 Theatersoft
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var util = _interopDefault(require('util'));
var WebSocket = require('ws');
var WebSocket__default = _interopDefault(WebSocket);
const mixinEventEmitter=(a)=>{return class b extends a{constructor(...c){super(...c);this.events=new Map;}on(c,d){this.events.has(c)||this.events.set(c,[]);this.events.get(c).push(d);return this}off(c,d){const e=this.events.get(c);if(e&&e.length)this.events.set(c,e.filter((f)=>f!==d));return this}emit(c,...d){const e=this.events.get(c);if(e&&e.length){e.forEach((f)=>f(...d));return!0}return!1}}};class EventEmitter extends mixinEventEmitter(class{}){}
util.inspect.defaultOptions={breakLength:Infinity};let time; let tag$1;const format$1=(...a)=>[...(time?[new Date().toLocaleTimeString('en',{hour12:!1})]:[]),...(tag$1?[tag$1]:[]),...a];const setTag=(a)=>{tag$1=a.toUpperCase();};const setTime=(a)=>{time=a;};const debug$1=(...a)=>console.log(...format$1(...a));const log$1=(...a)=>console.log(...format$1(...a));const error$1=(...a)=>console.error(...format$1(...a));
const tag='BUS'; const format=(...a)=>[tag,...a];const error=(...a)=>error$1(...format(...a));
function proxy(a){let[b,c,d]=/^([/\d]+)(\w+)$/.exec(a)||[void 0,void 0,a];return new Proxy({},{get(e,f){return(...g)=>(c?Promise.resolve():manager.resolveName(d).then((h)=>{c=h;})).then(()=>c?node.request({path:c,intf:d,member:f,args:g}):error('Proxy interface not resolved'))}})}function methods(a){return Object.getOwnPropertyNames(Object.getPrototypeOf(a)).filter((b)=>'function'==typeof a[b]&&'constructor'!==b)}
const parentStartup=(a)=>class extends a{constructor(...b){super(...b);const{parent:{auth:c}}=((connection.context)),d=({hello:f})=>{if(f){this.name=`${f}0`;this.emit('connect',f);this.off('data',d);}},e=({auth:f})=>{this.send({auth:c});this.off('data',e);this.on('data',d);};this.on('data',c?e:d);}};const childStartup=(a)=>class extends a{constructor(...b){super(...b);const{children:{check:c}={}}=((connection.context));Promise.resolve().then(()=>{if(!c)this.emit('connect');else{const d=({auth:e})=>{c(e).then((f)=>{if(f){this.emit('connect');}else{error('childStartup check failed',e);}this.off('data',d);});};this.on('data',d);this.send({auth:''});}});}hello(){this.send({hello:`${this.name}/`});}};
class NodeConnection extends EventEmitter{constructor(a){super();this.ws=a.on('open',()=>this.emit('open')).on('message',(b,c)=>{this.emit('data',JSON.parse(b));}).on('close',()=>this.emit('close')).on('error',(b)=>this.emit('error',b));}send(a){this.ws.send(JSON.stringify(a));}close(){this.ws.close();}}class ChildConnection extends childStartup(NodeConnection){}class ParentConnection extends parentStartup(NodeConnection){}class Server$1 extends EventEmitter{constructor(a){super();a.on('connection',(b)=>{const c=new ChildConnection(b).on('connect',()=>{this.emit('child',c);});}).on('close',(b,c)=>this.emit('close')).on('error',(b)=>this.emit('error',b));}}let context;const defaultUrl=process.env.BUS||'ws://localhost:5453'; const defaultAuth=process.env.AUTH;var connection = {create(a={}){const{parent:{url:b,auth:c}={},children:{server:d,host:e,port:f,check:g}={}}=a;return Promise.resolve(c).then((h)=>{const i=d||e?{server:d,host:e,port:f,check:g}:void 0,j=b||h||!i?{url:b||defaultUrl,auth:h||defaultAuth}:void 0;context={parent:j,children:i};})},get context(){if(!context)throw new Error('Invalid bus context');return context},get hasParent(){return this.context.parent&&this.context.parent.url},get hasChildren(){return!!this.context.children},createParentConnection(){process.env.NODE_TLS_REJECT_UNAUTHORIZED='0';return new ParentConnection(new WebSocket__default(this.context.parent.url))},createServer(){let a,{host:b,port:c,server:d}=this.context.children;if(d){a=Promise.resolve(d).then((e)=>({server:e}));}else{a=Promise.resolve({host:b,port:c});}return a.then((e)=>new Server$1(new WebSocket.Server(e)))}};const type='node';
const dupNode=()=>Promise.reject('duplicate node'); const missingNode=()=>Promise.reject('missing node'); const dupName=()=>Promise.reject('duplicate name'); const missingName=()=>Promise.reject('missing name');class Manager{init(a){if(node.root){this.names=new Map;this.nodes=new Map;node.registerObject('Bus',this,methods(this),{sender:!0});}else this.proxy=proxy('/Bus');this.addNode(a);}addNode(a){if(this.proxy)return this.proxy.addNode(a);if(this.nodes.has(a))return dupNode();this.nodes.set(a,[]);return Promise.resolve()}removeNode(a){if(this.proxy)return this.proxy.removeNode(a);if(!this.nodes.has(a))return missingNode();return Promise.all((this.nodes.get(a)||[]).slice().map((b)=>this.removeName(b))).then(()=>{this.nodes.delete(a);})}addName(a,b){if(this.proxy)return this.proxy.addName(a);if(this.names.has(a))return dupName();this.names.set(a,b);const c=this.nodes.get(b);if(!c)return missingNode();c.push(a);return Promise.resolve()}resolveName(a){if(this.proxy)return this.proxy.resolveName(a);if(!this.names.has(a))return missingName();return Promise.resolve(this.names.get(a))}removeName(a,b){if(this.proxy)return this.proxy.removeName(a);if(!this.names.has(a))return missingName();const c=this.names.get(a);if(!c)return missingName();this.names.delete(a);if(!this.nodes.has(c))return missingNode();const d=this.nodes.get(c);if(!d)return missingName();const e=d.indexOf(a);if(-1===e)return missingName();d.splice(e,1);return Promise.resolve()}check(a){const{children:{check:b}={}}=connection.context;return b(a)}}var manager = new Manager;
const nodeIntrospect=(b)=>class extends b{introspect(d){if(d!==this.name)return this.request({path:d,intf:'*',member:'introspect',args:[d]});return{name:this.name,type,children:this.conns.reduce((e,f,g)=>(g&&f&&e.push(`${f.name}/`),e),[]),objects:Object.entries(this.objects).filter(([e])=>'*'!==e).reduce((e,[f,{intf:g,meta:h}])=>(e[f]={intf:g,meta:h},e),{})}}};
const logRequest=(a)=>void 0; const logResponse=(a)=>a.hasOwnProperty('err')?error(`<-${a.id} `,a.err,'FAILED'):void 0;class Node{constructor(){this.conns=[void 0];this.objects={};this.reqid=0;this.requests={};this.signals=new EventEmitter;this.status=new EventEmitter;}init(a,b){this.objects['*']={obj:this};if(b){b.id=0;b.registered=!0;this.conns[0]=this.bind(b);}this.name=a;this.root='/'===a;manager.init(a);if(!this.server&&connection.hasChildren){connection.createServer().then((d)=>{this.server=d.on('child',(f)=>{this.addChild(this.bind(f));}).on('error',(f)=>{error('server error',f.message);});});}}addChild(a){a.id=this.conns.length;a.name=`${this.name}${a.id}`;a.hello();this.conns.push(a);a.registered=!0;}route(a){let b=a.lastIndexOf('/');if(-1===b)throw new Error('Invalid name');let d=a.slice(0,b+1),f=d===this.name?null:d.startsWith(this.name)?this.conns[parseInt(d.slice(this.name.length))]:this.conns[0];return f}bind(a){return a.on('data',(b)=>{b.req?this._request(b.req):b.res?this._response(b.res,!1):b.sig&&this._signal(b.sig,a.id);}).on('close',()=>{if(!a.registered){return}this.conns[a.id]=void 0;if(0===a.id){this.closing||this.reconnect();}else Promise.resolve().then(()=>manager.removeNode(`${a.name}/`)).then(()=>void 0).catch((b)=>void 0);})}reconnect(a=1000){this.status.emit('disconnect');setTimeout(()=>{const b=connection.createParentConnection().on('open',()=>{}).on('close',()=>{}).on('connect',(d)=>{this.init(d,b);Object.keys(this.objects).filter((f)=>/^[A-Z]/.test(f)).forEach((f)=>manager.addName(f,this.name));this.status.emit('reconnect');}).on('error',(d)=>{error('reconnect parent error',d.message);this.reconnect(Math.min(2*a,32000));});},a);}_request(a){logRequest(a);const b=this.route(a.path);if(b){b.send({req:a});}else if(null===b){Promise.resolve().then(()=>{const{intf:d,member:f,sender:g}=a,h=this.objects[d];if(!h)throw`Error interface ${d} object not found`;const{obj:k,meta:l={}}=h;if(!k[f])throw`Error member ${f} not found`;let{args:m}=a;if(l.sender)m=m.concat(g);return k[f](...m)}).then((d)=>this._response({id:a.id,path:a.sender,res:d}),(d)=>this._response({id:a.id,path:a.sender,err:d}));}else{}}_response(a,b=!0){const d=this.route(a.path);if(d){if(b)logResponse(a);d.send({res:a});}else if(null===d){let{r:f,j:g,req:h}=this.requests[a.id];delete this.requests[a.id];logResponse(a);if(a.hasOwnProperty('err'))g(a.err);else f(a.res);}else{error('connection error',a);}}_signal(a,b){this.signals.emit(a.name,a.args);this.conns.filter((d)=>d&&d.id!==b).forEach((d)=>{d&&d.send({sig:a});});}request(a){return new Promise((b,d)=>{const f=Object.assign({},a,{sender:this.name,id:this.reqid++});this.requests[f.id]={r:b,j:d,req:f};this._request(f);})}close(){this.closing=!0;this.conns.forEach((a)=>a&&a.close());}registerObject(a,b,d,f){this.objects[a]={obj:b,intf:d,meta:f};}unregisterObject(a){delete this.objects[a];}}var node = new class extends nodeIntrospect(Node){};
function executor(a,b){return{promise:new Promise((c,d)=>{a=c;b=d;}),resolve:(c)=>a(c),reject:(c)=>b(c)}}
let start=executor(); let _started;class Bus{start(a){if(!_started){_started=!0;connection.create(a).then(()=>{if(connection.hasParent){const b=connection.createParentConnection().on('open',()=>{}).on('close',()=>{}).on('connect',(c)=>{node.init(c,b);start.resolve(this);}).on('error',(c)=>{error('parent error',c);start.reject(c);});}else{node.init('/');start.resolve(this);}});}return start.promise}started(){return start.promise}get root(){return node.root}get name(){return node.name}get proxy(){return proxy}registerObject(a,b,c,d){return(isBusName(a)?manager.addName(a,this.name):Promise.resolve()).then(()=>{node.registerObject(a,b,c,node.root?d:void 0);return{signal:(f,g)=>node._signal({name:`${a}.${f}`,args:g})}})}unregisterObject(a){return(isBusName(a)?manager.removeName(a,this.name):Promise.resolve()).then(()=>{node.unregisterObject(a);})}request(a,...b){const[,c,d,f]=/^([/\d]+)(\w+).(\w+)$/.exec(a);return node.request({path:c,intf:d,member:f,args:b}).catch((g)=>{error(`request ${a} rejected ${g}`);throw g})}registerListener(a,b){node.signals.on(a,b);}unregisterListener(a,b){node.signals.off(a,b);}on(a,b){node.status.on(a,b);return this}off(a,b){node.status.off(a,b);}close(){node.close();}introspectNode(a){return node.introspect(a)}resolveName(a){return manager.resolveName(a)}}var bus = new Bus;const isBusName=(a)=>a.charAt(0).toUpperCase()===a.charAt(0);
exports['default'] = bus;
exports.bus = bus;
exports.proxy = proxy;
exports.EventEmitter = EventEmitter;
exports.mixinEventEmitter = mixinEventEmitter;
exports.executor = executor;
exports.setTag = setTag;
exports.setTime = setTime;
exports.debug = debug$1;
exports.log = log$1;
exports.error = error$1;