UNPKG

@zerodensity/realityhub-api

Version:
3 lines (2 loc) 15.3 kB
"use strict";var e=require("uuid"),t=require("events"),s=require("ws");class o extends Error{}class r extends Error{constructor(e){super(e),this.code="TIMEOUT",this.name=this.constructor.name}}function i(e,t,s=null){return new Promise((o,i)=>{let n,a,c;c=()=>{for(const s of t)e.removeListener(s,a)},a=(...e)=>{c(),clearTimeout(n),o(...e)};for(const s of t)e.once(s,a);s&&(n=setTimeout(()=>{c(),i(new r("Timeout exceeded."))},s))})}const n=52428800;class a extends t{constructor(e){super(),this.moduleName=e.moduleName,this.maxPacketSize=e.maxPacketSize||n,this.logger=e.logger,this.events=new Map,this.apiHandlers=new Map,this.messageTimeout=2e3,this.overridenTimeout=NaN;try{"undefined"!=typeof window&&"undefined"!=typeof localStorage?this.overridenTimeout=Number(localStorage.getItem("BROKER_TIMEOUT"))||NaN:process&&process.env&&(this.overridenTimeout=Number(process.env.BROKER_TIMEOUT)||NaN)}catch(e){}if(this.overridenTimeout){(this.logger||console).warn(`Broker Timeout is overriden to ${this.overridenTimeout} milliseconds!`)}let t=n;try{"undefined"!=typeof window&&"undefined"!=typeof localStorage?t||=Number(localStorage.getItem("MAX_WS_PACKET_SIZE")):process&&process.env&&(t||=Number(process.env.MAX_WS_PACKET_SIZE))}catch(e){(this.logger||console).error(`Cannot read maxPacketSize from either localStorage or env, defaulting to ${t}`)}if(this.maxPacketSize=Math.max(this.maxPacketSize,t),this.maxPacketSize!==n){(this.logger||console).log(`BrokerBase is created with maxPacketSize: ${this.maxPacketSize}`)}this.initProxy()}getMethodProxy(e,t,s){return s={timeout:2e3,excludedClients:[],...s},new Proxy({},{get:(o,r)=>{if("emit"===r&&this.moduleName!==`${e}.${t}`)throw new Error("A module can only emit its own events.");return(...o)=>{switch(r){case"emit":this.emitMessage(o,e,t,s);break;case"on":{const s=o.shift(),r=o.shift();if("string"!=typeof s)throw new Error("eventName must be a string");if("function"!=typeof r)throw new Error("eventHandler must be a function");const i=`${e}.${t}.${s}`;this.subscribeToAPIEvent(i,r).catch(e=>{console.error(`Couldn't subscribe to ${i}`),"TIMEOUT"!==e.code&&console.trace(e)});break}case"once":{const s=o.shift(),r=o.shift(),i=o.shift()||3e5;if("string"!=typeof s)throw new Error("eventName must be a string");if("function"!=typeof r)throw new Error("eventHandler must be a function");if("number"!=typeof i||isNaN(i))throw new Error("timeout must be a number");const n=`${e}.${t}.${s}`;this.subscribeToAPIEvent(n,r,{once:!0}).catch(e=>{console.error(`Couldn't subscribe to ${n}`),"TIMEOUT"!==e.code&&console.trace(e)});break}case"off":{const s=o.shift(),r=o.shift(),i=`${e}.${t}.${s}`;if("string"!=typeof s)throw new Error("eventName must be a string");if(r&&"function"!=typeof r)throw new Error("eventHandler must be a function");this.unsubscribeFromAPIEvent(i,r).catch(e=>{console.error(`Couldn't unsubscribe from ${i}`),"TIMEOUT"!==e.code&&console.trace(e)});break}case"callTimeout":{const r=o[0];if("number"!=typeof r)throw new Error("callTimeout: timeout is required.");const i=JSON.parse(JSON.stringify(s));return i.timeout=r,this.getMethodProxy(e,t,i)}case"excludeClients":{const r=o[0]||[];if(!(r instanceof Array))throw new Error("excludedClients requires 1 parameter: an array of strings");const i=JSON.parse(JSON.stringify(s));return i.excludedClients=i.excludedClients.concat(r),this.getMethodProxy(e,t,i)}default:return this.sendMessage({data:o,timeout:s.timeout,type:`${e}.${t}.${r}`,targetModuleName:`${e}.${t}`,excludedClients:s.excludedClients})}}},set:(s,o,r)=>{if(this.moduleName!==`${e}.${t}`)throw new Error("Cannot register methods to other modules.");if("function"!=typeof r)throw new Error("Handler must be a function.");if(["emit","on","off"].includes(o))throw new Error(`${o} is a reserved method name.`);return this.registerAPIHandler(o,r)}})}initProxy(){this.api=new Proxy({},{get:(e,t)=>new Proxy({},{get:(e,s)=>this.getMethodProxy(t,s),set:(e,s,o)=>{if(this.moduleName!==`${t}.${s}`)throw new Error("Cannot register methods to other modules.");if("object"!=typeof o)throw new Error("API must be set to an object.");for(const[e,t]of Object.entries(o)){if("function"!=typeof t)throw new Error("Handler must be a function.");if(["emit","on","off"].includes(e))throw new Error(`${e} is a reserved method name.`);this.registerAPIHandler(e,t)}return!0}}),set:function(){return console.warn("Module name and method name are required."),!1}})}sendResponse(e,t,s,o=[],r=!1){if(!e)return;const{id:i,moduleName:n,timeout:a,instigatorId:c}=t,l={type:"response",targetModuleName:n,instigatorId:c,requestId:i,timeout:a,success:s,data:o};return r&&(l.moduleName=t.targetModuleName),this.sendMessage(l,e,r)}registerAPIHandler(e,t){return e=`${this.moduleName}.${e}`,!this.apiHandlers.has(e)&&(this.apiHandlers.set(e,{relay:!1,messageHandler:t}),!0)}subscribeToAPIEvent(e,t,s){s={sendMessage:!0,once:!1,...s};const o=this.events.get(e)||[];if(o.push({eventHandler:t,once:s.once}),this.events.set(e,o),s.sendMessage){const t=e.split(".").slice(0,2).join(".");return this.sendMessage({type:"subscribe",eventName:e,targetModuleName:t})}}unsubscribeFromAPIEvent(e,t,s=!0){if(t){const s=(this.events.get(e)||[]).filter(e=>e.eventHandler!==t);this.events.set(e,s)}else this.events.delete(e);if(s){const t=e.split(".").slice(0,2).join(".");return this.sendMessage({type:"unsubscribe",eventName:e,targetModuleName:t})}}async sendMessage(t,s,r=!1){t.id=e.v4(),r||(t.moduleName=this.moduleName),t.time=(new Date).valueOf();const n=JSON.stringify(t);if(n.length>this.maxPacketSize&&this.logger.trace(new Error("MAX_WS_PACKET_SIZE")),s.send(n),!["event","response"].includes(t.type)){let e;try{e=await i(this,[`response::${t.id}`],this.overridenTimeout||t.timeout||this.messageTimeout)}catch(e){return void(this.logger||console).debug(`${this.moduleName} failed to send message ${t.type} to ${t.targetModuleName||""}`)}if(!e)return;if(e.success)return e.data;{let s=`${t.moduleName}'s "${t.type}" request has failed.`;throw e.data instanceof Array&&e.data.length&&e.data[0].error&&(s=e.data[0].error),this.logger.error(s),new o(s)}}}ping(e){return this.sendMessage({type:`${e}.ping`})}emitMessage(e,t,s,o={}){const r=e.shift();if("string"!=typeof r)throw new Error("eventName must be a string");const i=`${t}.${s}.${r}`;this.sendMessage({type:"event",eventName:i,data:e,excludedClients:o.excludedClients||[]}).catch(e=>{console.error(`Couldn't emit ${i}`),"TIMEOUT"!==e.code&&console.trace(e)})}destroy(){this.removeAllListeners()}}class c{setCallback(e){if("function"!=typeof e)throw new Error("callback must be a function.");this.callback=e}setAncillaryData(e){this.ancillaryData=e||{}}getAncillaryData(){return this.ancillaryData||{}}call(...e){if(this.callback)return this.callback(...e)}}const l="undefined"!=typeof window?window.WebSocket:s;class h extends a{constructor(e={}){super(e),this.setMaxListeners(20);for(const e of[this.onOpen,this.onClose,this.onError,this.connect,this.onSocketMessage]){this[e.name]=e.bind(this)}this.logger=e.logger||function(e,t={silent:!1}){const{silent:s}=t;return["log","info","warn","error","trace","debug"].reduce((t,o)=>(t[o]=(...t)=>{if(s||(t=[`NOTICE (${o}): Current module: ${e} has no Logger object available, outputting to the console`,...t]),["trace","debug"].includes(o))for(const e of t||[])console.log(e);return console[o](...t)},t),{})}(this.moduleName,{silent:!0}),this.isDuplicate=e.isDuplicate,this.parent=e.parent,this.ssl=e.ssl,this.duplicates=new Set,this.serverModuleName=null,this.registrars=new Set,this.webSocketURL=e.webSocketURL,this.connected=!1,this.isDuplicate&&(this.connected=this.isConnected())}getConnectPromise(){return new Promise(e=>{const t=setInterval(()=>{this.connected&&(clearInterval(t),e())},0)})}forceReconnect(){this.removeSocketListeners(),this.socket=null,this.connect(this.connectOptions)}connect(e){if(this.isDuplicate)return;let t;this.connectOptions=e,t=e?{hostname:e.host,port:e.port}:new URL(location.href);const s="https:"===t.protocol||this.ssl?"wss":"ws",o=t.port?`${s}://${t.hostname}:${t.port}${this.webSocketURL}`:`${s}://${t.hostname}${this.webSocketURL}`;if(this.logger.info(`BrokerClient is connecting to ${o}`),"undefined"!=typeof process&&null!=process.versions&&null!=process.versions.node){const e=process.env.NODE_TLS_REJECT_UNAUTHORIZED;process.env.NODE_TLS_REJECT_UNAUTHORIZED=0,this.socket=new l(`${o}?module=true`),process.env.NODE_TLS_REJECT_UNAUTHORIZED=e}else this.socket=new l(o);this.addSocketListeners()}duplicate(e){const{moduleName:t}=e;let s,o;return this.isDuplicate?(s=this.parent.duplicates,o=new h({parent:this.parent,isDuplicate:!0,webSocketURL:this.webSocketURL,logger:this.logger,moduleName:t})):(s=this.duplicates,o=new h({parent:this,isDuplicate:!0,webSocketURL:this.webSocketURL,logger:this.logger,moduleName:t})),o.on("destroy",()=>s.delete(o)),s.add(o),o.ping().catch(e=>{console.error("Failed to send ping"),console.trace(e.message)}),o}isConnected(){return this.isDuplicate?this.parent.isConnected():this.connected}ping(){return this.sendMessage({type:"ping"})}getSocket(){return this.socket}async onSocketMessage(e){try{await this.handleMessage(e.data,this.socket)}catch(e){console.trace(e)}}async handleMessage(e){let t;try{t=JSON.parse(e);const s=this.isDuplicate?this.parent.getSocket():this.socket;switch(t.type){case"response":if(this.emit(`response::${t.requestId}`,t),!this.isDuplicate)for(const t of this.duplicates)t.handleMessage(e);break;case"event":for(const[e,s]of this.events.entries())if(e===t.eventName)for(const o of s)try{o.eventHandler(...t.data)}catch(e){this.logger.warn(e)}finally{o.once&&this.unsubscribeFromAPIEvent(e,o.eventHandler)}if(!this.isDuplicate)for(const t of this.duplicates)t.handleMessage(e);break;case"subscribe":{const[o,r,...i]=t.eventName.split("."),n=i.join("."),a=[o,r].join(".");if(this.moduleName===a)this.emit("subscribe",{eventName:n}),await this.sendResponse(s,t,!0);else{if(!this.isDuplicate)for(const t of this.duplicates)if(t.moduleName===a)return void t.handleMessage(e);await this.sendResponse(s,t,!1,[{error:`${t.eventName} sent to ${this.moduleName}. This is probably a mistake.`}])}break}case"unsubscribe":{const[o,r,...i]=t.eventName.split("."),n=i.join("."),a=[o,r].join(".");if(this.moduleName===a)this.emit("unsubscribe",{eventName:n}),await this.sendResponse(s,t,!0);else{if(!this.isDuplicate)for(const t of this.duplicates)if(t.moduleName===a)return void t.handleMessage(e);await this.sendResponse(s,t,!1,[{error:`${t.eventName} sent to ${this.moduleName}. This is probably a mistake.`}])}break}case"ping":if(this.serverModuleName=t.moduleName,!this.isDuplicate&&t.targetModuleName!==this.moduleName)for(const s of this.duplicates)if(s.moduleName===t.targetModuleName)return void s.handleMessage(e);await Promise.all([this.sendResponse(s,t,!0),this.resubscribeModuleEvents(),this.subscribeToAPIEvent(`${t.moduleName}.moduleconnect`,({moduleName:e})=>{this.emit("moduleconnect",{moduleName:e}),this.resubscribeModuleEvents()}),this.subscribeToAPIEvent(`${t.moduleName}.moduledisconnect`,({moduleName:e})=>{this.emit("moduledisconnect",{moduleName:e})})]);break;default:{if(!this.apiHandlers.has(t.type))return void await this.sendResponse(s,t,!1,[{error:`There is no handler registered for this type of message: ${t.type}`}]);const{messageHandler:e,relay:r}=this.apiHandlers.get(t.type);try{let o=await e(...t.data);if(o instanceof c){const e=o;e.setAncillaryData({...t.ancillaryData,caller:{moduleName:t.moduleName}}),o=await e.call(...t.data)}await this.sendResponse(s,t,!0,o,r)}catch(e){if(e instanceof o)return this.logger.error(e.message),void await this.sendResponse(s,t,!1,[{error:e.message}],r);this.logger.trace(e),await this.sendResponse(s,t,!1,[{error:"ERROR"}],r)}break}}}catch(e){if("TIMEOUT"===e.code)return console.warn("Message timed out."),void console.log(t);console.trace(e)}}async sendMessage(t){const s=e.v4(),o=this.isDuplicate?this.parent.getSocket():this.socket,r=Object.assign({},t,{id:s,time:(new Date).valueOf(),moduleName:this.moduleName});if(o.readyState!==l.OPEN)try{await i(this,["connect"],r.timeout||this.messageTimeout)}catch(e){throw console.error("Timeout: Socket is not ready"),e}let n;try{n=super.sendMessage(r,o)}catch(e){throw console.error("BrokerBase::sendMessage throwed an exception"),e}return n}addSocketListeners(){this.isDuplicate||(this.socket.addEventListener("open",this.onOpen),this.socket.addEventListener("message",this.onSocketMessage),this.socket.addEventListener("error",this.onError),this.socket.addEventListener("close",this.onClose))}removeSocketListeners(){this.socket&&(this.socket.removeEventListener("open",this.onOpen),this.socket.removeEventListener("message",this.onSocketMessage),this.socket.removeEventListener("error",this.onError),this.socket.removeEventListener("close",this.onClose))}resubscribeModuleEvents(){for(const e of this.events.keys()){const t=e.split(".").slice(0,2).join(".");this.sendMessage({type:"subscribe",eventName:e,targetModuleName:t}).catch(new Function)}}async onOpen(){try{this.connected=!0,this.emit("connect"),this.isDuplicate&&await this.ping();for(const e of this.registrars)await this.registerHandlersToRemote(e);if(!this.isDuplicate)for(const e of this.duplicates)e.onOpen()}catch(e){console.trace(e.message)}}onError(e){if(e.error instanceof Error&&(e=e.error.message),this.isDuplicate||this.logger.trace(e),!this.isDuplicate){this.logger.warn(`${this.moduleName} couldn't connect to WebSocket server.`);for(const t of this.duplicates)t.onError(e)}}onClose(e){if(this.connected){if(this.connected=!1,this.emit("disconnect",e),this.events.delete(`${this.serverModuleName}.moduleconnect`),!this.isDuplicate)for(const e of this.duplicates)e.onClose()}else this.emit("reconnectfailure");setTimeout(()=>this.connect(this.connectOptions),1e3)}registerHandlersToRemote(e){return this.registrars.add(e),this.sendMessage({type:`${e}.registerAPIHandlers`,data:Array.from(this.apiHandlers.keys()),targetModuleName:e})}async deregisterHandlersFromRemotes(){try{const e=[];for(const t of this.registrars)e.push(this.sendMessage({type:`${targetModuleName}.deregisterAPIHandlers`,data:Array.from(this.apiHandlers.keys()),targetModuleName:t}));return Promise.all(e)}catch(e){console.trace(e.message)}}unsubscribeFromAllEvents(){const e=[];for(const t of this.events.keys())e.push(this.unsubscribeFromAPIEvent(t));return Promise.all(e)}async destroy(){try{this.isDuplicate?(await Promise.all([this.deregisterHandlersFromRemotes(),this.unsubscribeFromAllEvents(),this.sendMessage({type:"event",eventName:`${this.moduleName}.disconnect`,targetModuleName:this.serverModuleName})]),this.emit("destroy")):(this.socket.close(),this.removeSocketListeners(),super.destroy())}catch(e){console.trace(e.message)}}async registerAPIHandlers(e,t=null,s="hub.core"){for(const[s,o]of Object.entries(e))this.registerAPIHandler(s,o.bind(t));return this.registerHandlersToRemote(s)}static async initModule(e){const{moduleName:t,serverURL:s,hub:o,webSocketURL:r="/core",clientModuleName:i,menuTitle:n}=e,a=new h({moduleName:t,webSocketURL:r});return a.connect(o),await a.getConnectPromise(),s?(await a.api.hub.core.registerProxyURL({moduleName:t,serverURL:s,clientModuleName:i,menuTitle:n}),a):a}}exports.BrokerBase=a,exports.BrokerClient=h,exports.BrokerError=o,exports.RawRequest=c; //# sourceMappingURL=index.cjs.map