UNPKG

api-websocket-bridge

Version:

API WebSocket Bridge is a Node.js application that provides a WebSocket server for real-time communication. It allows clients to connect and exchange data using the WebSocket protocol

279 lines (250 loc) 8.7 kB
const WebSocket = require('ws'); const EventEmitter = require('events'); const axios = require('axios'); const https = require('https'); const fs = require('fs'); /** * WebSocketDriver class for managing a WebSocket server and API updates. */ class WebSocketDriver { /** * Creates an instance of WebSocketDriver. * @param {Object} config - Configuration object for WebSocketDriver. */ constructor(config) { /** * Configuration object for WebSocketDriver. * @type {Object} */ this.config = config; this.wss = null; this.eventEmitter = new EventEmitter(); this.intervalId = null; this.previousData = {}; this.validateConfig(); } /** * Starts the WebSocket server and API update interval. * @returns {Promise<void>} - A promise that resolves when the server is started. */ async start() { await this.checkSSLFilesExist(); await this.checkAPIUpdates(); this.startWebSocketServer(); this.startAPIUpdateInterval(); } /** * Checks if the SSL certificate and key files exist and are readable. * @returns {Promise<void>} - A promise that resolves if the files exist and are readable, otherwise rejects with an error. * @throws {Error} - SSL certificate or key file not found or not readable. */ async checkSSLFilesExist() { try { await Promise.all([ fs.promises.access(this.config.certPath, fs.constants.R_OK), fs.promises.access(this.config.keyPath, fs.constants.R_OK) ]); } catch (error) { throw new Error('SSL certificate or key file not found or not readable'); } } /** * Checks for API updates and sets the initial data. * @returns {Promise<void>} - A promise that resolves when the API data is fetched and set. * @throws {Error} - Failed to fetch API data. */ async checkAPIUpdates() { try { const response = await axios.get(this.config.apiUrl); this.previousData = response.data; } catch (error) { throw new Error(`Failed to fetch API data: ${error.message}`); } } /** * Starts the WebSocket server with SSL configuration. */ startWebSocketServer() { const serverConfig = { cert: fs.readFileSync(this.config.certPath), key: fs.readFileSync(this.config.keyPath), }; const server = https.createServer(serverConfig); this.wss = new WebSocket.Server({ server }); this.wss.on('connection', this.handleConnection.bind(this)); server.listen(this.config.port, () => { this.log(`WebSocket server is running on port ${this.config.port}`, 'info'); }); server.on('error', (error) => { this.log(`Server error: ${error.message}`, 'error'); }); } /** * Handles a new WebSocket connection. * @param {WebSocket} ws - The WebSocket instance representing the connection. */ handleConnection(ws) { this.log('New WebSocket client connected', 'info'); ws.send(JSON.stringify({ type: 'dataUpdate', data: this.previousData })); ws.on('message', (message) => { let data; try { data = JSON.parse(message); } catch (error) { this.log('Invalid JSON message received', 'error'); return; } switch (data.type) { case 'subscribe': this.handleSubscription(ws, data.event); break; default: this.log(`Unknown message type: ${data.type}`, 'warn'); } }); } /** * Starts the API update interval. */ startAPIUpdateInterval() { this.intervalId = setInterval(async () => { try { const response = await axios.get(this.config.apiUrl); const newData = response.data; const updatedEvents = this.getUpdatedEvents(newData); updatedEvents.forEach((event) => { const eventData = this.getEventData(event, newData); this.eventEmitter.emit(event.emitEvent, eventData); }); this.previousData = newData; } catch (error) { this.log(`Error updating API data: ${error.message}`, 'error'); } }, this.config.updateInterval); } /** * Gets the events that have updated data compared to the previous data. * @param {Object} newData - The new API data. * @returns {Array} - An array of event configurations that have updated data. */ getUpdatedEvents(newData) { return Object.entries(this.config.events) .filter(([event, config]) => { const { params } = config; return params.some((param) => newData[param] !== this.previousData[param]); }) .map(([event]) => this.config.events[event]); } /** * Gets the event data for a specific event from the API data. * @param {Object} event - The event configuration. * @param {Object} data - The API data. * @returns {Object} - The event data. */ getEventData(event, data) { const eventData = {}; const { params } = event; params.forEach((param) => { const nestedParamValue = param.split('.').reduce((obj, key) => obj && obj[key], data); this.setNestedProperty(eventData, param, nestedParamValue); }); return eventData; } /** * Sets a nested property value in an object. * @param {Object} obj - The object to set the property value on. * @param {string} path - The dot-separated path of the property. * @param {*} value - The value to set. */ setNestedProperty(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); let currentObj = obj; for (const key of keys) { currentObj[key] = currentObj[key] || {}; currentObj = currentObj[key]; } currentObj[lastKey] = value; } /** * Handles a subscription request from a WebSocket client. * @param {WebSocket} ws - The WebSocket instance representing the connection. * @param {string} event - The event type to subscribe to. */ handleSubscription(ws, event) { if (event === 'all') { for (const eventConfig of Object.values(this.config.events)) { this.eventEmitter.on(eventConfig.emitEvent, (eventData) => { ws.send(JSON.stringify({ type: eventConfig.emitEvent, ...eventData })); }); } this.log('Client subscribed to all events', 'info'); } else { const eventConfig = this.config.events[event]; if (!eventConfig) { this.log(`Unknown event type: ${event}`, 'warn'); return; } this.eventEmitter.on(eventConfig.emitEvent, (eventData) => { ws.send(JSON.stringify({ type: eventConfig.emitEvent, ...eventData })); }); this.log(`Client subscribed to event: ${event}`, 'info'); } } /** * Stops the WebSocket server and API update interval. */ stop() { clearInterval(this.intervalId); this.wss.close(); this.log('WebSocket server stopped', 'info'); } /** * Validates the configuration object. * @throws {Error} - If the configuration object is invalid or missing required properties. */ validateConfig() { const { certPath, keyPath, port, updateInterval, apiUrl, events } = this.config; if (!certPath || typeof certPath !== 'string') { throw new Error('Invalid or missing certificate path in the configuration'); } if (!keyPath || typeof keyPath !== 'string') { throw new Error('Invalid or missing key path in the configuration'); } if (!port || typeof port !== 'number') { throw new Error('Invalid or missing port in the configuration'); } if (!updateInterval || typeof updateInterval !== 'number' || updateInterval <= 0) { throw new Error('Invalid or missing update interval in the configuration'); } if (!apiUrl || typeof apiUrl !== 'string') { throw new Error('Invalid or missing API URL in the configuration'); } if (!events || typeof events !== 'object' || Array.isArray(events)) { throw new Error('Invalid or missing events configuration in the configuration'); } } /** * Logs a message with the specified log level. * @param {string} message - The message to log. * @param {string} [level='info'] - The log level (info, warn, error). */ log(message, level = 'info') { const logLevel = level.toLowerCase(); const timestamp = new Date().toISOString(); switch (logLevel) { case 'info': console.log(`[${timestamp}] INFO: ${message}`); break; case 'warn': console.log(`[${timestamp}] WARN: ${message}`); break; case 'error': console.error(`[${timestamp}] ERROR: ${message}`); break; default: console.log(`[${timestamp}] ${message}`); } } } module.exports = WebSocketDriver;