@zerodensity/realityhub-api
Version:
RealityHub API Javascript Implementation
1 lines • 60.9 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/lib/BrokerError.js","../src/lib/onceMultiple.js","../src/lib/BrokerBase.js","../src/lib/RawRequest.js","../src/lib/BrokerClient.js","../src/lib/consoleLogger.js"],"sourcesContent":["// Copyright (c) 2019-2021 Zero Density Inc.\n//\n// This file is part of realityhub-api.\n//\n// realityhub-api is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License version 2, as published by\n// the Free Software Foundation.\n//\n// realityhub-api is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with realityhub-api. If not, see <https://www.gnu.org/licenses/>.\n\nexport default class BrokerError extends Error {}\n","// Copyright (c) 2019-2021 Zero Density Inc.\n//\n// This file is part of realityhub-api.\n//\n// realityhub-api is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License version 2, as published by\n// the Free Software Foundation.\n//\n// realityhub-api is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with realityhub-api. If not, see <https://www.gnu.org/licenses/>.\n\nclass TimeoutError extends Error {\n constructor(params) {\n super(params);\n this.code = 'TIMEOUT';\n this.name = this.constructor.name;\n }\n}\n\nexport default function onceMultiple(target, eventNames, timeout = null) {\n return new Promise((resolve, reject) => {\n let timer;\n let handler;\n let removeListeners;\n\n removeListeners = () => {\n for (const eventName of eventNames) {\n target.removeListener(eventName, handler);\n }\n };\n\n handler = (...args) => {\n removeListeners();\n clearTimeout(timer);\n resolve(...args);\n };\n\n for (const eventName of eventNames) {\n target.once(eventName, handler);\n }\n\n if (timeout) {\n timer = setTimeout(() => {\n removeListeners();\n reject(new TimeoutError('Timeout exceeded.'));\n }, timeout);\n }\n });\n}\n","// Copyright (c) 2019-2021 Zero Density Inc.\n//\n// This file is part of realityhub-api.\n//\n// realityhub-api is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License version 2, as published by\n// the Free Software Foundation.\n//\n// realityhub-api is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with realityhub-api. If not, see <https://www.gnu.org/licenses/>.\n\nimport { v4 as uuid } from 'uuid';\nimport EventEmitter from 'events';\nimport BrokerError from './BrokerError.js';\nimport onceMultiple from './onceMultiple.js';\n\nconst DEFAULT_MAX_WS_PACKET_SIZE = 50 /*MB*/ * 1024 * 1024;\n\n/**\n * BrokerBase constructor\n * @param {object} params Parameters\n * @param {string} [params.moduleName] Module name\n * @param {number} [params.maxPacketSize] Maximum websocket packet size\n * @param {Logger} [params.logger] Logger instance\n */\nexport default class BrokerBase extends EventEmitter {\n constructor(params) {\n super();\n this.moduleName = params.moduleName;\n this.maxPacketSize = params.maxPacketSize || DEFAULT_MAX_WS_PACKET_SIZE;\n\n this.logger = params.logger;\n\n this.events = new Map();\n this.apiHandlers = new Map();\n\n this.messageTimeout = 2000;\n\n this.overridenTimeout = NaN; // NaN = use the implementation\n\n try {\n if (typeof window != 'undefined' && typeof localStorage != 'undefined') {\n this.overridenTimeout = Number(localStorage.getItem('BROKER_TIMEOUT')) || NaN;\n } else if (process && process.env) {\n this.overridenTimeout = Number(process.env.BROKER_TIMEOUT) || NaN;\n }\n } catch (ex) {}\n\n if (this.overridenTimeout) {\n const logger = this.logger || console;\n logger.warn(`Broker Timeout is overriden to ${this.overridenTimeout} milliseconds!`);\n }\n\n let maxPacketSizeRead = DEFAULT_MAX_WS_PACKET_SIZE;\n\n try {\n if (typeof window != 'undefined' && typeof localStorage != 'undefined') {\n maxPacketSizeRead ||= Number(localStorage.getItem('MAX_WS_PACKET_SIZE'));\n } else if (process && process.env) {\n maxPacketSizeRead ||= Number(process.env.MAX_WS_PACKET_SIZE);\n }\n } catch (ex) {\n const logger = this.logger || console;\n logger.error(`Cannot read maxPacketSize from either localStorage or env, defaulting to ${maxPacketSizeRead}`);\n }\n\n this.maxPacketSize = Math.max(this.maxPacketSize, maxPacketSizeRead);\n\n if (this.maxPacketSize !== DEFAULT_MAX_WS_PACKET_SIZE) {\n const logger = this.logger || console;\n logger.log(`BrokerBase is created with maxPacketSize: ${this.maxPacketSize}`);\n }\n\n this.initProxy();\n }\n\n getMethodProxy(vendorName, moduleName, options) {\n options = {\n timeout: 2000,\n excludedClients: [],\n ...options,\n };\n\n return new Proxy(\n {},\n {\n get: (_, methodName) => {\n if (methodName === 'emit' && this.moduleName !== `${vendorName}.${moduleName}`) {\n throw new Error('A module can only emit its own events.');\n }\n\n return (...args) => {\n switch (methodName) {\n case 'emit': {\n this.emitMessage(args, vendorName, moduleName, options);\n break;\n }\n\n case 'on': {\n const eventName = args.shift();\n const eventHandler = args.shift();\n\n if (typeof eventName !== 'string') {\n throw new Error('eventName must be a string');\n }\n\n if (typeof eventHandler !== 'function') {\n throw new Error('eventHandler must be a function');\n }\n\n const fullyQualifiedName = `${vendorName}.${moduleName}.${eventName}`;\n\n this.subscribeToAPIEvent(fullyQualifiedName, eventHandler).catch((err) => {\n console.error(`Couldn't subscribe to ${fullyQualifiedName}`);\n\n if (err.code !== 'TIMEOUT') {\n console.trace(err);\n }\n });\n\n break;\n }\n\n case 'once': {\n const eventName = args.shift();\n const eventHandler = args.shift();\n\n /**\n * If the subscribed event is not emitted within the given timeout then the\n * event handler will be removed automatically to prevent memory leak.\n * If a timeout is not provided by caller than a default timeout of 5 minutes is set.\n */\n const timeout = args.shift() || 60 * 1000 * 5;\n\n if (typeof eventName !== 'string') {\n throw new Error('eventName must be a string');\n }\n\n if (typeof eventHandler !== 'function') {\n throw new Error('eventHandler must be a function');\n }\n\n if (typeof timeout !== 'number' || isNaN(timeout)) {\n throw new Error('timeout must be a number');\n }\n\n const fullyQualifiedName = `${vendorName}.${moduleName}.${eventName}`;\n\n this.subscribeToAPIEvent(fullyQualifiedName, eventHandler, {\n once: true,\n }).catch((err) => {\n console.error(`Couldn't subscribe to ${fullyQualifiedName}`);\n\n if (err.code !== 'TIMEOUT') {\n console.trace(err);\n }\n });\n break;\n }\n\n case 'off': {\n const eventName = args.shift();\n const eventHandler = args.shift();\n const fullyQualifiedName = `${vendorName}.${moduleName}.${eventName}`;\n\n if (typeof eventName !== 'string') {\n throw new Error('eventName must be a string');\n }\n\n if (eventHandler && typeof eventHandler !== 'function') {\n throw new Error('eventHandler must be a function');\n }\n\n this.unsubscribeFromAPIEvent(fullyQualifiedName, eventHandler).catch((err) => {\n console.error(`Couldn't unsubscribe from ${fullyQualifiedName}`);\n\n if (err.code !== 'TIMEOUT') {\n console.trace(err);\n }\n });\n\n break;\n }\n\n case 'callTimeout': {\n const timeout = args[0];\n\n if (typeof timeout !== 'number') {\n throw new Error('callTimeout: timeout is required.');\n }\n\n const clonedOptions = JSON.parse(JSON.stringify(options));\n clonedOptions.timeout = timeout;\n return this.getMethodProxy(vendorName, moduleName, clonedOptions);\n }\n\n case 'excludeClients': {\n const excludedClients = args[0] || [];\n\n if (!(excludedClients instanceof Array)) {\n throw new Error('excludedClients requires 1 parameter: an array of strings');\n }\n\n const clonedOptions = JSON.parse(JSON.stringify(options));\n clonedOptions.excludedClients = clonedOptions.excludedClients.concat(excludedClients);\n return this.getMethodProxy(vendorName, moduleName, clonedOptions);\n }\n\n default: {\n return this.sendMessage({\n data: args,\n timeout: options.timeout,\n type: `${vendorName}.${moduleName}.${methodName}`,\n targetModuleName: `${vendorName}.${moduleName}`,\n excludedClients: options.excludedClients,\n });\n }\n }\n };\n },\n set: (_, methodName, handler) => {\n if (this.moduleName !== `${vendorName}.${moduleName}`) {\n throw new Error('Cannot register methods to other modules.');\n }\n\n if (typeof handler !== 'function') {\n throw new Error('Handler must be a function.');\n }\n\n if (['emit', 'on', 'off'].includes(methodName)) {\n throw new Error(`${methodName} is a reserved method name.`);\n }\n\n return this.registerAPIHandler(methodName, handler);\n },\n }\n );\n }\n\n /**\n * Initializes the Proxy object.\n * @private\n */\n initProxy() {\n // These nested proxies allow us to get vendorName, moduleName and methodName.\n // e.g. const pong = await this.api.hub.core.ping();\n this.api = new Proxy(\n {},\n {\n get: (_, vendorName) => {\n return new Proxy(\n {},\n {\n get: (_, moduleName) => {\n return this.getMethodProxy(vendorName, moduleName);\n },\n set: (_, moduleName, api) => {\n if (this.moduleName !== `${vendorName}.${moduleName}`) {\n throw new Error('Cannot register methods to other modules.');\n }\n\n if (typeof api !== 'object') {\n throw new Error('API must be set to an object.');\n }\n\n for (const [methodName, handler] of Object.entries(api)) {\n if (typeof handler !== 'function') {\n throw new Error('Handler must be a function.');\n }\n\n if (['emit', 'on', 'off'].includes(methodName)) {\n throw new Error(`${methodName} is a reserved method name.`);\n }\n\n this.registerAPIHandler(methodName, handler);\n }\n\n return true;\n },\n }\n );\n },\n set: function () {\n console.warn('Module name and method name are required.');\n return false;\n },\n }\n );\n }\n\n /**\n * Send a response message through `socket`.\n * @private\n * @param {WebSocket} socket Target socket.\n * @param {object} message The message to respond.\n * @param {boolean} success Whether the request was successfully processed or not.\n * @param {array} [data] Additional payload\n * @param {boolean} [relayedMessage=false]\n * @returns {Promise.<array, Error>}\n */\n sendResponse(socket, message, success, data = [], relayedMessage = false) {\n if (!socket) return;\n\n const { id: requestId, moduleName: targetModuleName, timeout, instigatorId } = message;\n const websocketMessage = {\n type: 'response',\n targetModuleName,\n instigatorId,\n requestId,\n timeout,\n success,\n data,\n };\n\n if (relayedMessage) {\n websocketMessage.moduleName = message.targetModuleName;\n }\n\n return this.sendMessage(websocketMessage, socket, relayedMessage);\n }\n\n /**\n * Registers an API request handler.\n * @param {string} messageType Message type.\n * @param {function} messageHandler Handler function.\n * @returns {boolean} `false` if a handler has already been assigned to the `messageType`.\n */\n registerAPIHandler(messageType, messageHandler) {\n messageType = `${this.moduleName}.${messageType}`;\n\n if (this.apiHandlers.has(messageType)) return false;\n\n this.apiHandlers.set(messageType, {\n relay: false,\n messageHandler,\n });\n\n return true;\n }\n\n /**\n * Subscribe to an API event.\n * @param {string} eventName Fully qualified event name.\n * @param {function} eventHandler A function which will be called when the event is received.\n * @param {object} [options] Options\n * @param {boolean} [options.sendMessage=true] If set to `true`, it will send a subscription message over WebSocket.\n * Otherwise the message will only be registered internally.\n * @param {boolean} [options.once=false] If true then the handler will be invoked only once and it won't be invoked for\n * the future events that are emitted.\n * @returns {Promise.<array, Error>}\n */\n subscribeToAPIEvent(eventName, eventHandler, options) {\n options = {\n sendMessage: true,\n once: false,\n ...options,\n };\n\n // Add handler to handlers map\n const handlerArray = this.events.get(eventName) || [];\n handlerArray.push({ eventHandler, once: options.once });\n this.events.set(eventName, handlerArray);\n\n // Send a subscription message over WebSocket\n if (options.sendMessage) {\n const targetModuleName = eventName.split('.').slice(0, 2).join('.');\n\n return this.sendMessage({\n type: 'subscribe',\n eventName,\n targetModuleName,\n });\n }\n }\n\n /**\n * Unsubscribe from an API event.\n * @param {string} eventName Fully qualified event name.\n * @param {function} [eventHandler] A previously registered handler function. All handlers of the event will\n * be removed unless `eventHandler` is provided.\n * @param {boolean} [sendMessage=true] Will send an unsubscription request when set to `true`.\n * @returns {Promise.<array, Error>}\n */\n unsubscribeFromAPIEvent(eventName, eventHandler, sendMessage = true) {\n if (eventHandler) {\n const handlerArray = (this.events.get(eventName) || []).filter((entry) => entry.eventHandler !== eventHandler);\n this.events.set(eventName, handlerArray);\n } else {\n this.events.delete(eventName);\n }\n\n if (sendMessage) {\n const targetModuleName = eventName.split('.').slice(0, 2).join('.');\n\n return this.sendMessage({\n type: 'unsubscribe',\n eventName,\n targetModuleName,\n });\n }\n }\n\n /**\n * Send an API message through a socket.\n * @async\n * @private\n * @param {object} message Message object. `time`, `id`, `moduleName` and `data` keys will be added\n * to the message object. Unlike other mentioned fields `data` will not get overridden when provided.\n * @param {object} socket Socket instance.\n * @param {boolean} [relayedMessage=false]\n * @returns {Promise.<array, Error>}\n */\n async sendMessage(message, socket, relayedMessage = false) {\n message.id = uuid();\n\n if (!relayedMessage) {\n message.moduleName = this.moduleName;\n }\n\n message.time = new Date().valueOf();\n const packet = JSON.stringify(message);\n\n if (packet.length > this.maxPacketSize) {\n this.logger.trace(new Error('MAX_WS_PACKET_SIZE'));\n }\n\n socket.send(packet);\n\n if (!['event', 'response'].includes(message.type)) {\n let responseMessage;\n\n try {\n responseMessage = await onceMultiple(\n this,\n [`response::${message.id}`],\n this.overridenTimeout || message.timeout || this.messageTimeout\n );\n } catch (ex) {\n const logger = this.logger || console;\n logger.debug(`${this.moduleName} failed to send message ${message.type} to ${message.targetModuleName || ''}`);\n return;\n }\n\n if (!responseMessage) return;\n\n if (responseMessage.success) {\n return responseMessage.data;\n } else {\n let errorMessage = `${message.moduleName}'s \"${message.type}\" request has failed.`;\n\n if (responseMessage.data instanceof Array && responseMessage.data.length && responseMessage.data[0].error) {\n errorMessage = responseMessage.data[0].error;\n }\n\n this.logger.error(errorMessage);\n throw new BrokerError(errorMessage);\n }\n }\n }\n\n /**\n * Send a ping request.\n * @param {string} targetModuleName\n * @private\n */\n ping(targetModuleName) {\n return this.sendMessage({ type: `${targetModuleName}.ping` });\n }\n\n /**\n * @private\n * @param {array} args\n * @param {string} vendorName\n * @param {string} moduleName\n */\n emitMessage(args, vendorName, moduleName, options = {}) {\n const eventName = args.shift();\n\n if (typeof eventName !== 'string') {\n throw new Error('eventName must be a string');\n }\n\n const fullyQualifiedName = `${vendorName}.${moduleName}.${eventName}`;\n\n this.sendMessage({\n type: 'event',\n eventName: fullyQualifiedName,\n data: args,\n excludedClients: options.excludedClients || [],\n }).catch((err) => {\n console.error(`Couldn't emit ${fullyQualifiedName}`);\n\n if (err.code !== 'TIMEOUT') {\n console.trace(err);\n }\n });\n }\n\n destroy() {\n this.removeAllListeners();\n }\n}\n","// Copyright (c) 2019-2021 Zero Density Inc.\n//\n// This file is part of realityhub-api.\n//\n// realityhub-api is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License version 2, as published by\n// the Free Software Foundation.\n//\n// realityhub-api is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with realityhub-api. If not, see <https://www.gnu.org/licenses/>.\n\nexport default class RawRequest {\n setCallback(callback) {\n if (typeof callback !== 'function') {\n throw new Error('callback must be a function.');\n }\n\n this.callback = callback;\n }\n\n setAncillaryData(ancillaryData) {\n this.ancillaryData = ancillaryData || {};\n }\n\n getAncillaryData() {\n return this.ancillaryData || {};\n }\n\n call(...args) {\n if (this.callback) {\n return this.callback(...args);\n }\n }\n}\n","// Copyright (c) 2019-2021 Zero Density Inc.\n//\n// This file is part of realityhub-api.\n//\n// realityhub-api is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License version 2, as published by\n// the Free Software Foundation.\n//\n// realityhub-api is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with realityhub-api. If not, see <https://www.gnu.org/licenses/>.\n\nimport { v4 as uuid } from 'uuid';\nimport BrokerBase from './BrokerBase.js';\nimport BrokerError from './BrokerError.js';\nimport RawRequest from './RawRequest.js';\nimport consoleLogger from './consoleLogger.js';\nimport onceMultiple from './onceMultiple.js';\nimport WebSocket from 'ws';\n\nconst WS = typeof window !== 'undefined' ? window.WebSocket : WebSocket;\n\nexport default class BrokerClient extends BrokerBase {\n /**\n * BrokerClient constructor\n * @param {object} params Parameters\n * @param {string} params.webSocketURL WebSocket URL\n * @param {string} [params.moduleName] Module name\n * @param {number} [params.maxPacketSize] Maximum websocket packet size\n * @param {Logger} [params.logger] Logger instance\n * @param {boolean} [params.isDuplicate] [Private property, used internally]\n * @param {boolean} [params.parent] [Private property, used internally]\n */\n constructor(params = {}) {\n super(params);\n\n this.setMaxListeners(20);\n\n for (const func of [this.onOpen, this.onClose, this.onError, this.connect, this.onSocketMessage]) {\n const name = func.name;\n this[name] = func.bind(this);\n }\n\n this.logger = params.logger || consoleLogger(this.moduleName, { silent: true });\n\n this.isDuplicate = params.isDuplicate;\n this.parent = params.parent;\n this.ssl = params.ssl;\n this.duplicates = new Set();\n\n // moduleName of the server (will be set when we receive a ping message)\n this.serverModuleName = null;\n\n /**\n * A set of module names\n * Each time registerHandlersToRemote is called, the `remote` is added to this set.\n * BrokerClient uses the list of the remote endpoints for re-registering after a\n * reconnect.\n * @type {Set<string>}\n */\n this.registrars = new Set();\n\n this.webSocketURL = params.webSocketURL;\n this.connected = false;\n\n if (this.isDuplicate) {\n this.connected = this.isConnected();\n }\n }\n\n /**\n * Returns a `Promise` that will resolve once a `connect` event is emitted.\n * If BrokerClient is already connected then it will resolve in the next\n * event loop.\n * @returns {Promise}\n */\n getConnectPromise() {\n return new Promise((resolve) => {\n const looper = setInterval(() => {\n if (this.connected) {\n clearInterval(looper);\n resolve();\n }\n }, 0);\n });\n }\n\n forceReconnect() {\n this.removeSocketListeners();\n this.socket = null;\n this.connect(this.connectOptions);\n }\n\n /**\n * Connect to server.\n * @param {object} options Options\n * @param {string} options.host Hostname or IP of the target server.\n * @param {number} options.port WebSocket port of the target server.\n */\n connect(options) {\n if (this.isDuplicate) return;\n\n this.connectOptions = options;\n let url;\n\n if (!options) {\n url = new URL(location.href);\n } else {\n url = { hostname: options.host, port: options.port };\n }\n\n const scheme = url.protocol === 'https:' || this.ssl ? 'wss' : 'ws';\n\n const webSocketURL = url.port\n ? `${scheme}://${url.hostname}:${url.port}${this.webSocketURL}`\n : `${scheme}://${url.hostname}${this.webSocketURL}`;\n\n this.logger.info(`BrokerClient is connecting to ${webSocketURL}`);\n\n if (typeof process !== 'undefined' && process.versions != null && process.versions.node != null) {\n const NODE_TLS_REJECT_UNAUTHORIZED = process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;\n this.socket = new WS(`${webSocketURL}?module=true`);\n process.env.NODE_TLS_REJECT_UNAUTHORIZED = NODE_TLS_REJECT_UNAUTHORIZED;\n } else {\n this.socket = new WS(webSocketURL);\n }\n\n this.addSocketListeners();\n }\n\n /**\n * Duplicates a BrokerClient in order to share the same WebSocket.\n * @param {object} params Parameters\n * @param {string} params.moduleName Module name of the duplicate BrokerClient (a duplicate\n * can have a different name than its parent)\n * @returns {BrokerClient}\n */\n duplicate(params) {\n const { moduleName } = params;\n let duplicates;\n let duplicate;\n\n if (this.isDuplicate) {\n duplicates = this.parent.duplicates;\n duplicate = new BrokerClient({\n parent: this.parent,\n isDuplicate: true,\n webSocketURL: this.webSocketURL,\n logger: this.logger,\n moduleName,\n });\n } else {\n duplicates = this.duplicates;\n duplicate = new BrokerClient({\n parent: this,\n isDuplicate: true,\n webSocketURL: this.webSocketURL,\n logger: this.logger,\n moduleName,\n });\n }\n\n duplicate.on('destroy', () => duplicates.delete(duplicate));\n duplicates.add(duplicate);\n\n duplicate.ping().catch((ex) => {\n console.error(`Failed to send ping`);\n console.trace(ex.message);\n });\n\n return duplicate;\n }\n\n /**\n * Returns `true` if WebSocket connection is established.\n * @returns {boolean}\n */\n isConnected() {\n if (this.isDuplicate) {\n return this.parent.isConnected();\n }\n\n return this.connected;\n }\n\n /**\n * Sends a ping message to server.\n * @private\n */\n ping() {\n return this.sendMessage({ type: 'ping' });\n }\n\n /**\n * Returns the WebSocket instance.\n * @private\n */\n getSocket() {\n return this.socket;\n }\n\n async onSocketMessage(event) {\n try {\n await this.handleMessage(event.data, this.socket);\n } catch (ex) {\n console.trace(ex);\n }\n }\n\n /**\n * Handles incoming messages.\n * @param {string} rawMessage Raw message\n * @async\n * @private\n */\n async handleMessage(rawMessage) {\n let message;\n\n try {\n message = JSON.parse(rawMessage);\n const socket = this.isDuplicate ? this.parent.getSocket() : this.socket;\n\n switch (message.type) {\n case 'response': {\n this.emit(`response::${message.requestId}`, message);\n\n // Send the response to other duplicates (if we are parent)\n if (!this.isDuplicate) {\n for (const duplicate of this.duplicates) {\n duplicate.handleMessage(rawMessage);\n }\n }\n\n break;\n }\n\n case 'event': {\n // Run previously registered event handlers\n for (const [subscribedEvent, entries] of this.events.entries()) {\n if (subscribedEvent === message.eventName) {\n for (const entry of entries) {\n try {\n entry.eventHandler(...message.data);\n } catch (ex) {\n this.logger.warn(ex);\n } finally {\n if (entry.once) {\n this.unsubscribeFromAPIEvent(subscribedEvent, entry.eventHandler);\n }\n }\n }\n }\n }\n\n // Send the event to other duplicates (if we are parent)\n if (!this.isDuplicate) {\n for (const duplicate of this.duplicates) {\n duplicate.handleMessage(rawMessage);\n }\n }\n\n break;\n }\n\n case 'subscribe': {\n const [vendor, moduleName, ...rest] = message.eventName.split('.');\n const eventName = rest.join('.');\n const targetModuleName = [vendor, moduleName].join('.');\n\n if (this.moduleName === targetModuleName) {\n this.emit('subscribe', { eventName });\n await this.sendResponse(socket, message, true);\n } else {\n // Check if the target is one of the duplicates (if we are parent)\n if (!this.isDuplicate) {\n for (const duplicate of this.duplicates) {\n if (duplicate.moduleName === targetModuleName) {\n duplicate.handleMessage(rawMessage);\n return;\n }\n }\n }\n\n await this.sendResponse(socket, message, false, [\n {\n error: `${message.eventName} sent to ${this.moduleName}. This is probably a mistake.`,\n },\n ]);\n }\n\n break;\n }\n\n case 'unsubscribe': {\n const [vendor, moduleName, ...rest] = message.eventName.split('.');\n const eventName = rest.join('.');\n const targetModuleName = [vendor, moduleName].join('.');\n\n if (this.moduleName === targetModuleName) {\n this.emit('unsubscribe', { eventName });\n await this.sendResponse(socket, message, true);\n } else {\n // Check if the target is one of the duplicates (if we are parent)\n if (!this.isDuplicate) {\n for (const duplicate of this.duplicates) {\n if (duplicate.moduleName === targetModuleName) {\n duplicate.handleMessage(rawMessage);\n return;\n }\n }\n }\n\n await this.sendResponse(socket, message, false, [\n {\n error: `${message.eventName} sent to ${this.moduleName}. This is probably a mistake.`,\n },\n ]);\n }\n\n break;\n }\n\n case 'ping': {\n this.serverModuleName = message.moduleName;\n\n if (!this.isDuplicate && message.targetModuleName !== this.moduleName) {\n for (const duplicate of this.duplicates) {\n if (duplicate.moduleName === message.targetModuleName) {\n duplicate.handleMessage(rawMessage);\n return;\n }\n }\n }\n\n await Promise.all([\n this.sendResponse(socket, message, true),\n this.resubscribeModuleEvents(),\n this.subscribeToAPIEvent(`${message.moduleName}.moduleconnect`, ({ moduleName }) => {\n this.emit('moduleconnect', { moduleName });\n this.resubscribeModuleEvents();\n }),\n this.subscribeToAPIEvent(`${message.moduleName}.moduledisconnect`, ({ moduleName }) => {\n this.emit('moduledisconnect', { moduleName });\n }),\n ]);\n\n break;\n }\n\n default: {\n if (!this.apiHandlers.has(message.type)) {\n await this.sendResponse(socket, message, false, [\n {\n error: `There is no handler registered for this type of message: ${message.type}`,\n },\n ]);\n return;\n }\n\n const { messageHandler, relay } = this.apiHandlers.get(message.type);\n\n try {\n let responseMessage = await messageHandler(...message.data);\n\n if (responseMessage instanceof RawRequest) {\n const rawRequest = responseMessage;\n rawRequest.setAncillaryData({\n ...message.ancillaryData,\n caller: {\n moduleName: message.moduleName,\n },\n });\n responseMessage = await rawRequest.call(...message.data);\n }\n\n await this.sendResponse(socket, message, true, responseMessage, relay);\n } catch (ex) {\n if (ex instanceof BrokerError) {\n this.logger.error(ex.message);\n await this.sendResponse(socket, message, false, [{ error: ex.message }], relay);\n return;\n }\n\n this.logger.trace(ex);\n await this.sendResponse(socket, message, false, [{ error: 'ERROR' }], relay);\n }\n\n break;\n }\n }\n } catch (ex) {\n if (ex.code === 'TIMEOUT') {\n console.warn('Message timed out.');\n console.log(message);\n return;\n }\n\n console.trace(ex);\n }\n }\n\n /**\n * Sends a message.\n * @async\n * @param {object} message\n * @returns {Promise.<Array, Error>}\n */\n async sendMessage(message) {\n const id = uuid();\n const socket = this.isDuplicate ? this.parent.getSocket() : this.socket;\n const webSocketMessage = Object.assign({}, message, {\n id,\n time: new Date().valueOf(),\n moduleName: this.moduleName,\n });\n\n if (socket.readyState !== WS.OPEN) {\n try {\n await onceMultiple(this, ['connect'], webSocketMessage.timeout || this.messageTimeout);\n } catch (ex) {\n console.error(`Timeout: Socket is not ready`);\n throw ex;\n }\n }\n\n let ret;\n\n try {\n ret = super.sendMessage(webSocketMessage, socket);\n } catch (ex) {\n console.error(`BrokerBase::sendMessage throwed an exception`);\n throw ex;\n }\n\n return ret;\n }\n\n /**\n * @private\n */\n addSocketListeners() {\n if (this.isDuplicate) return;\n\n this.socket.addEventListener('open', this.onOpen);\n this.socket.addEventListener('message', this.onSocketMessage);\n this.socket.addEventListener('error', this.onError);\n this.socket.addEventListener('close', this.onClose);\n }\n\n removeSocketListeners() {\n if (!this.socket) return;\n\n this.socket.removeEventListener('open', this.onOpen);\n this.socket.removeEventListener('message', this.onSocketMessage);\n this.socket.removeEventListener('error', this.onError);\n this.socket.removeEventListener('close', this.onClose);\n }\n\n /**\n * @private\n */\n resubscribeModuleEvents() {\n for (const eventName of this.events.keys()) {\n const moduleName = eventName.split('.').slice(0, 2).join('.');\n\n this.sendMessage({\n type: 'subscribe',\n eventName,\n targetModuleName: moduleName,\n }).catch(new Function());\n }\n }\n\n /**\n * @async\n * @private\n */\n async onOpen() {\n try {\n this.connected = true;\n this.emit('connect');\n\n if (this.isDuplicate) {\n await this.ping();\n }\n\n for (const registrar of this.registrars) {\n await this.registerHandlersToRemote(registrar);\n }\n\n if (!this.isDuplicate) {\n for (const duplicate of this.duplicates) {\n duplicate.onOpen();\n }\n }\n } catch (ex) {\n console.trace(ex.message);\n }\n }\n\n /**\n * @private\n * @param {ErrorEvent} err\n */\n onError(err) {\n if (err.error instanceof Error) {\n err = err.error.message;\n }\n\n if (!this.isDuplicate) {\n this.logger.trace(err);\n }\n\n if (!this.isDuplicate) {\n this.logger.warn(`${this.moduleName} couldn't connect to WebSocket server.`);\n\n for (const duplicate of this.duplicates) {\n duplicate.onError(err);\n }\n }\n }\n\n /**\n * @private\n */\n onClose(e) {\n if (this.connected) {\n this.connected = false;\n this.emit('disconnect', e);\n this.events.delete(`${this.serverModuleName}.moduleconnect`);\n\n if (!this.isDuplicate) {\n for (const duplicate of this.duplicates) {\n duplicate.onClose();\n }\n }\n } else {\n this.emit('reconnectfailure');\n }\n\n setTimeout(() => this.connect(this.connectOptions), 1000);\n }\n\n /**\n * Send the local API handler list to the `targetModuleName` so it will relay\n * messages targeting those handlers.\n * @param {string} targetModuleName Module name of the API Server\n * @returns {Promise.<Array, Error>}\n */\n registerHandlersToRemote(targetModuleName) {\n this.registrars.add(targetModuleName);\n\n return this.sendMessage({\n type: `${targetModuleName}.registerAPIHandlers`,\n data: Array.from(this.apiHandlers.keys()),\n targetModuleName,\n });\n }\n\n /**\n * Send a message to all remote endpoints telling them not to relay any messages\n * to this module anymore.\n * @async\n * @returns {Promise.<Array, Error>[]}\n */\n async deregisterHandlersFromRemotes() {\n try {\n const promises = [];\n\n for (const registrar of this.registrars) {\n promises.push(\n this.sendMessage({\n type: `${targetModuleName}.deregisterAPIHandlers`,\n data: Array.from(this.apiHandlers.keys()),\n targetModuleName: registrar,\n })\n );\n }\n\n return Promise.all(promises);\n } catch (ex) {\n console.trace(ex.message);\n }\n }\n\n /**\n * Unsubscribes from all subscriptions.\n * @returns {Promise.<Array, Error>[]}\n */\n unsubscribeFromAllEvents() {\n const promises = [];\n\n for (const eventName of this.events.keys()) {\n promises.push(this.unsubscribeFromAPIEvent(eventName));\n }\n\n return Promise.all(promises);\n }\n\n /**\n * Performs cleanup.\n * @async\n */\n async destroy() {\n try {\n if (this.isDuplicate) {\n await Promise.all([\n this.deregisterHandlersFromRemotes(),\n this.unsubscribeFromAllEvents(),\n this.sendMessage({\n type: 'event',\n eventName: `${this.moduleName}.disconnect`,\n targetModuleName: this.serverModuleName,\n }),\n ]);\n this.emit('destroy');\n } else {\n this.socket.close();\n this.removeSocketListeners();\n super.destroy();\n }\n } catch (ex) {\n console.trace(ex.message);\n }\n }\n\n /**\n * Register a module's API handlers to RealityHub\n * @async\n * @param {Object.<string, function>} handlers Key will be registered to the API tree.\n * @param {*} [context=null] Handlers' `this` will be set to this context.\n * The value (function) will handle the API calls.\n * @param {string} [remote='hub.core'] Remote\n * @example\n * // server.js\n * brokerClient.registerAPIHandlers(this, {\n * addNumbers: function (number1, number2) {\n * return number1 + number2;\n * },\n * }).catch((ex) => console.trace(ex));\n *\n * // client.js\n * brokerClient.api.moduleVendor.moduleName.addNumber(3, 5)\n * .then((result) => {\n * // Will log 15\n * console.log(result);\n * })\n * .catch((ex) => console.trace(ex));\n * @returns {Promise}\n */\n async registerAPIHandlers(handlers, context = null, remote = 'hub.core') {\n for (const [handlerName, handler] of Object.entries(handlers)) {\n this.registerAPIHandler(handlerName, handler.bind(context));\n }\n\n return this.registerHandlersToRemote(remote);\n }\n\n /**\n * Third-party modules can use this method to initialize a BrokerClient\n * and register themselves to RealityHub.\n * @async\n * @static\n * @param {{ clientModuleName?: string, menuTitle?: string, moduleName: string, serverURL: string, webSocketURL?: string, hub: {host: string, port: number }}} params Parameters\n * @param {string} [params.clientModuleName] Client Module Name (`<vendor>.<client module name>`)\n * @param {string} [params.menuTitle] Menu Title\n * @param {string} params.moduleName Backend Module Name (`<vendor>.<backend module name>`)\n * @param {string} params.serverURL Your module has to serve your client files over HTTP or HTTPS.\n * RealityHub will look for an `index.js` file in this path. This script file will be imported\n * by RealityHub's `index.html` via a `<script type=\"module\">` tag. Relative paths in your scripts\n * will be proxied by RealityHub.\n * @param {string} [params.webSocketURL=\"/core\"] WebSocket URL to connect. RealityHub's API Server\n * is serving at `/core` by default. *(Default: /core)*\n * @param {{ host: string, port: number }} params.hub RealityHub connection parameters\n * @param {string} params.hub.host RealityHub hostname or IP address\n * @param {string} params.hub.port RealityHub port\n * @returns {Promise<BrokerClient, Error>} A BrokerClient instance.\n */\n static async initModule(params) {\n const { moduleName, serverURL, hub, webSocketURL = '/core', clientModuleName, menuTitle } = params;\n const hubClient = new BrokerClient({ moduleName, webSocketURL });\n\n hubClient.connect(hub);\n await hubClient.getConnectPromise();\n if (!serverURL) return hubClient;\n\n await hubClient.api.hub.core.registerProxyURL({\n moduleName,\n serverURL,\n clientModuleName,\n menuTitle,\n });\n return hubClient;\n }\n}\n","// Copyright (c) 2019-2021 Zero Density Inc.\n//\n// This file is part of realityhub-api.\n//\n// realityhub-api is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License version 2, as published by\n// the Free Software Foundation.\n//\n// realityhub-api is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with realityhub-api. If not, see <https://www.gnu.org/licenses/>.\n\nexport default function consoleLogger(moduleName, options = { silent: false }) {\n const { silent } = options;\n\n return ['log', 'info', 'warn', 'error', 'trace', 'debug'].reduce((o, item) => {\n o[item] = (...args) => {\n if (!silent) {\n args = [\n `NOTICE (${item}): Current module: ${moduleName} has no Logger object available, outputting to the console`,\n ...args,\n ];\n }\n\n if (['trace', 'debug'].includes(item)) {\n for (const arg of args || []) {\n console.log(arg);\n }\n }\n\n return console[item](...args);\n };\n return o;\n }, {});\n}\n"],"names":["BrokerError","Error","TimeoutError","constructor","params","super","this","code","name","onceMultiple","target","eventNames","timeout","Promise","resolve","reject","timer","handler","removeListeners","eventName","removeListener","args","clearTimeout","once","setTimeout","DEFAULT_MAX_WS_PACKET_SIZE","BrokerBase","EventEmitter","moduleName","maxPacketSize","logger","events","Map","apiHandlers","messageTimeout","overridenTimeout","NaN","window","localStorage","Number","getItem","process","env","BROKER_TIMEOUT","ex","console","warn","maxPacketSizeRead","MAX_WS_PACKET_SIZE","error","Math","max","log","initProxy","getMethodProxy","vendorName","options","excludedClients","Proxy","get","_","methodName","emitMessage","shift","eventHandler","fullyQualifiedName","subscribeToAPIEvent","catch","err","trace","isNaN","unsubscribeFromAPIEvent","clonedOptions","JSON","parse","stringify","Array","concat","sendMessage","data","type","targetModuleName","set","includes","registerAPIHandler","api","Object","entries","sendResponse","socket","message","success","relayedMessage","id","requestId","instigatorId","websocketMessage","messageType","messageHandler","has","relay","handlerArray","push","split","slice","join","filter","entry","delete","uuid","time","Date","valueOf","packet","length","send","responseMessage","debug","errorMessage","ping","destroy","removeAllListeners","RawRequest","setCallback","callback","setAncillaryData","ancillaryData","getAncillaryData","call","WS","WebSocket","BrokerClient","setMaxListeners","func","onOpen","onClose","onError","connect","onSocketMessage","bind","silent","reduce","o","item","arg","consoleLogger","isDuplicate","parent","ssl","duplicates","Set","serverModuleName","registrars","webSocketURL","connected","isConnected","getConnectPromise","looper","setInterval","clearInterval","forceReconnect","removeSocketListeners","connectOptions","url","hostname","host","port","URL","location","href","scheme","protocol","info","versions","node","NODE_TLS_REJECT_UNAUTHORIZED","addSocketListeners","duplicate","on","add","getSocket","event","handleMessage","rawMessage","emit","subscribedEvent","vendor","rest","all","resubscribeModuleEvents","rawRequest","caller","webSocketMessage","assign","readyState","OPEN","ret","addEventListener","removeEventListener","keys","Function","registrar","registerHandlersToRemote","e","from","deregisterHandlersFromRemotes","promises","unsubscribeFromAllEvents","close","registerAPIHandlers","handlers","context","remote","handlerName","initModule","serverURL","hub","clientModuleName","menuTitle","hubClient","core","registerProxyURL"],"mappings":"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