UNPKG

@dyniqo/ts-websocket

Version:

A robust and flexible TypeScript library for managing WebSocket connections alongside an HTTP server. This library provides seamless integration into Node.js applications with built-in support for dependency injection, modular design, and extensibility.

627 lines (607 loc) 23.5 kB
/*! * ts-websocket * by LorestaniMe <dyniqo@gmail.com> * https://github.com/Dyniqo/ts-websocket * License MIT */ import 'reflect-metadata'; import { injectable, inject, Container } from 'inversify'; import jwt from 'jsonwebtoken'; import { WebSocketServer, WebSocket } from 'ws'; import express from 'express'; import http from 'http'; const TYPES = { ILogger: 'ILogger', IAuthService: 'IAuthService', IWebSocketService: 'IWebSocketService', WebSocketController: 'WebSocketController', IConfig: 'IConfig', }; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function __param(paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } } function __metadata(metadataKey, metadataValue) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * LoggerService: A service class for managing application logging. * * This service implements the `ILogger` interface and provides methods for logging errors, * warnings, and general informational messages. The logging functionality can be controlled * through the configuration to enable or disable logging. * * ## Dependencies: * - **IConfig**: Supplies configuration settings, including the `enableLogging` flag. * * ## Responsibilities: * - Logs error messages unconditionally. * - Logs warnings and informational messages conditionally, based on the `enableLogging` flag. */ let LoggerService = class LoggerService { /** * Constructor: Initializes the LoggerService with configuration settings. * * @param {IConfig} config - The configuration object providing logging settings. */ constructor(config) { var _a; this.config = config; this.enableLogging = (_a = this.config.enableLogging) !== null && _a !== void 0 ? _a : true; } /** * Logs an error message. * * @param {string} message - The error message to log. */ error(message) { console.error(`[WS ERROR]: ${message}`); } /** * Logs a warning message if logging is enabled. * * @param {string} message - The warning message to log. */ warn(message) { if (this.enableLogging) { console.warn(`[WS WARN]: ${message}`); } } /** * Logs an informational message if logging is enabled. * * @param {string} message - The message to log. */ log(message) { if (this.enableLogging) { console.log(`[WS LOG]: ${message}`); } } }; LoggerService = __decorate([ injectable(), __param(0, inject(TYPES.IConfig)), __metadata("design:paramtypes", [Object]) ], LoggerService); /** * AuthService: A service class for managing authentication. * * This service implements the `IAuthService` interface and provides functionality * for generating and verifying authentication tokens using JSON Web Tokens (JWT). * * ## Dependencies: * - **IConfig**: Provides the secret key and token expiry settings for token management. * * ## Responsibilities: * - Generate JWT tokens for user authentication. * - Verify the validity of JWT tokens and extract the associated username. */ let AuthService = class AuthService { /** * Constructor: Initializes the AuthService with configuration settings. * * @param {IConfig} config - The configuration object providing the secret key and token expiry. */ constructor(config) { this.config = config; this.secretKey = this.config.secretKey; this.tokenExpiry = this.config.tokenExpiry; } /** * Generates a JWT token for the given username. * * @param {string} username - The username for which the token is generated. * @returns {string} - A JWT token representing the authenticated user. */ generateToken(username) { return jwt.sign({ username }, this.secretKey, { expiresIn: this.tokenExpiry }); } /** * Verifies the provided JWT token and retrieves the associated username. * * @param {string} token - The JWT token to verify. * @returns {string | null} - The username if the token is valid, or null if invalid. */ verifyToken(token) { try { const decoded = jwt.verify(token, this.secretKey); return decoded.username; } catch { return null; } } }; AuthService = __decorate([ injectable(), __param(0, inject(TYPES.IConfig)), __metadata("design:paramtypes", [Object]) ], AuthService); /** * WebSocketService: A service class for managing WebSocket operations. * * This service implements the `IWebSocketService` interface and provides functionality * for broadcasting messages to connected WebSocket clients. * * ## Dependencies: * - **ILogger**: Used for logging message broadcasting activities. * * ## Responsibilities: * - Logs the broadcasting of messages using the injected logger service. */ let WebSocketService = class WebSocketService { /** * Constructor: Initializes the WebSocketService with a logger dependency. * * @param {ILogger} logger - The logger service for logging operations. */ constructor(logger) { this.logger = logger; } /** * Logs a message broadcasting activity. * * @param {IMessage<any>} message - The message to be broadcasted. */ broadcastMessage(message) { this.logger.log(`Broadcasting message from ${message.sender}`); } }; WebSocketService = __decorate([ injectable(), __param(0, inject(TYPES.ILogger)), __metadata("design:paramtypes", [Object]) ], WebSocketService); /** * WebSocketController: A class for managing WebSocket server and client interactions. * * This controller initializes a WebSocket server, handles client authentication and messaging, * and supports custom lifecycle hooks for message processing. It is designed to integrate * with dependency-injected services for logging, authentication, and configuration. * * ## Responsibilities: * - Initializes a WebSocket server (`wss`) using provided configuration options. * - Manages WebSocket client connections and tracks them in a `Map`. * - Authenticates clients using token-based authentication. * - Processes and broadcasts messages between connected clients. * - Provides hooks for pre-processing and post-processing messages. * - Logs key events such as connections, disconnections, and errors. * * ## Dependencies: * - `IWebSocketService`: Provides utility functions for WebSocket operations. * - `ILogger`: Handles logging of events and errors. * - `IAuthService`: Manages token-based client authentication. * - `IConfig`: Supplies configuration for the WebSocket server and hooks. */ let WebSocketController = class WebSocketController { /** * Constructor: Initializes the WebSocketController and sets up the WebSocket server. * * @param {IWebSocketService} webSocketService - Utility service for WebSocket operations. * @param {ILogger} logger - Logger service for monitoring and debugging. * @param {IAuthService} authService - Authentication service for verifying tokens. * @param {IConfig} config - Configuration object for WebSocket server and hooks. */ constructor(webSocketService, logger, authService, config) { var _a, _b; this.webSocketService = webSocketService; this.logger = logger; this.authService = authService; this.config = config; /** * * @private * @type {Map<WebSocket, string>} * @memberof WebSocketController */ this.clients = new Map(); this.wss = new WebSocketServer({ noServer: true, ...this.config.wsOptions }); this.wss.on('connection', this.onConnection.bind(this)); this.beforeSend = (_a = this.config.hooks) === null || _a === void 0 ? void 0 : _a.beforeSend; this.afterSend = (_b = this.config.hooks) === null || _b === void 0 ? void 0 : _b.afterSend; this.onMessage = this.config.onMessage; } /** * Dynamically sets lifecycle hooks for message processing. * * @param {Function} [beforeSend] - Hook executed before a message is broadcasted. * @param {Function} [afterSend] - Hook executed after a message is broadcasted. */ setLifecycleHooks(beforeSend, afterSend) { this.beforeSend = beforeSend; this.afterSend = afterSend; } /** * Handles HTTP-to-WebSocket upgrade requests. * * @param {IncomingMessage} request - The incoming HTTP upgrade request. * @param {Socket} socket - The network socket for the connection. * @param {Buffer} head - The first packet of the upgraded stream. */ handleUpgrade(request, socket, head) { this.wss.handleUpgrade(request, socket, head, (ws) => { this.wss.emit('connection', ws, request); }); } /** * Handles a new WebSocket connection, authenticates the client, and sets up event listeners. * * @private * @param {WebSocket} ws - The WebSocket instance for the connected client. * @param {IncomingMessage} request - The incoming HTTP request for the connection. */ onConnection(ws, request) { const token = this.extractToken(request.url); const username = token ? this.authService.verifyToken(token) : null; if (token && !username) { ws.send(JSON.stringify({ error: 'Authentication Failed' })); ws.close(1008, 'Authentication Failed'); this.logger.warn('WebSocket connection closed due to failed authentication.'); return; } const user = username || 'Anonymous'; this.clients.set(ws, user); this.logger.log(`User connected: ${user}`); ws.on('message', (data) => { let message; try { message = JSON.parse(data); message.sender = user; } catch (error) { this.logger.error(`Error parsing message: ${error.message}`); ws.send(JSON.stringify({ error: 'Invalid JSON format' })); ws.close(); return; } try { if (this.onMessage) { this.onMessage(message); } else { if (this.beforeSend) { this.beforeSend(message); } this.broadcastMessage(message); if (this.afterSend) { this.afterSend(message); } } } catch (error) { this.logger.error(`Error processing message: ${error.message}`); ws.send(JSON.stringify({ error: 'Internal server error' })); } }); ws.on('close', () => { this.clients.delete(ws); this.logger.log(`User disconnected: ${user}`); }); ws.on('error', (error) => { this.logger.error(`WebSocket error: ${error.message}`); }); } /** * Broadcasts a message to all connected WebSocket clients. * * @private * @param {IMessage<any>} message - The message to be broadcasted. */ broadcastMessage(message) { this.wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); } }); this.logger.log(`Broadcasted message from ${message.sender}`); } /** * Extracts the token from a WebSocket request URL for authentication. * * @private * @param {string | undefined} url - The URL from which to extract the token. * @returns {string | null} - The extracted token or null if not found. */ extractToken(url) { if (!url) return null; try { const params = new URLSearchParams(url.split('?')[1]); return params.get('token'); } catch { return null; } } /** * Gracefully shuts down the WebSocket server and logs the closure. */ close() { this.clients.forEach((username, ws) => { ws.close(1001, 'Server shutting down'); }); this.wss.close(() => { this.logger.warn('WebSocket server closed.'); }); } }; WebSocketController = __decorate([ injectable(), __param(0, inject(TYPES.IWebSocketService)), __param(1, inject(TYPES.ILogger)), __param(2, inject(TYPES.IAuthService)), __param(3, inject(TYPES.IConfig)), __metadata("design:paramtypes", [Object, Object, Object, Object]) ], WebSocketController); /** * Config: A class implementing the `IConfig` interface to provide configuration settings for the application. * * This class encapsulates various configuration options, including server settings, lifecycle hooks, * and logging preferences. It allows default values to be overridden using a partial configuration object. * * ## Properties: * - **port**: The port number on which the server will run (default: 8080). * - **secretKey**: The secret key used for token generation and authentication (default: 'default_secret_key'). * - **tokenExpiry**: The duration for which generated tokens remain valid (default: '1h'). * - **wsOptions**: Optional WebSocket server options. * - **hooks**: Optional lifecycle hooks for WebSocket message processing. * - **enableLogging**: Whether to enable logging (default: `true`). * - **setupRoutes**: Optional callback for setting up application routes. * - **onMessage**: Optional custom handler for processing incoming WebSocket messages. */ let Config = class Config { /** * Constructor: Initializes the Config class with provided options or defaults. * * @param {Partial<IConfig>} options - A partial configuration object to override defaults. */ constructor(options = {}) { var _a; /** * Assign the port, with a default of 8080 if not provided. */ this.port = options.port || 8080; /** * Assign the secret key, with a default of 'default_secret_key' if not provided. */ this.secretKey = options.secretKey || 'default_secret_key'; /** * Assign the token expiry, with a default of '1h' if not provided. */ this.tokenExpiry = options.tokenExpiry || '1h'; /** * Assign optional WebSocket options. */ this.wsOptions = options.wsOptions; /** * Assign optional lifecycle hooks for WebSocket messages. */ this.hooks = options.hooks; /** * Enable or disable logging. Defaults to `true` if not specified. */ this.enableLogging = (_a = options.enableLogging) !== null && _a !== void 0 ? _a : true; /** * Assign optional route setup callback. */ this.setupRoutes = options.setupRoutes; /** * Assign an optional custom handler for incoming WebSocket messages. */ this.onMessage = options.onMessage; } }; Config = __decorate([ injectable(), __metadata("design:paramtypes", [Object]) ], Config); /** * Creates and configures a Dependency Injection (DI) container. * This container is responsible for managing and injecting dependencies * across the application using the InversifyJS library. * * @param {Partial<IConfig>} configOptions - Configuration options for initializing the application. * @returns {Container} - A configured DI container instance. * * The function performs the following bindings: * - Binds `IConfig` to a dynamic instance of `Config` with the provided configuration options. * - Binds `ILogger` to a singleton instance of `LoggerService` for logging functionalities. * - Binds `IAuthService` to a singleton instance of `AuthService` for authentication-related operations. * - Binds `IWebSocketService` to a singleton instance of `WebSocketService` for WebSocket handling. * - Binds `WebSocketController` to a singleton instance for managing WebSocket events and connections. * * Each dependency is registered with a singleton scope to ensure one shared instance across the application. */ function createContainer(configOptions) { const container = new Container(); container .bind(TYPES.IConfig) .toDynamicValue(() => new Config(configOptions)) .inSingletonScope(); container.bind(TYPES.ILogger).to(LoggerService).inSingletonScope(); container .bind(TYPES.IAuthService) .to(AuthService) .inSingletonScope(); container .bind(TYPES.IWebSocketService) .to(WebSocketService) .inSingletonScope(); container .bind(TYPES.WebSocketController) .to(WebSocketController) .inSingletonScope(); return container; } /** * User: A class representing a user in the system. * * This class encapsulates the basic properties of a user, such as the username, * and provides a simple structure for managing user-related information. * * ## Properties: * - **username** (string): The unique identifier or name of the user. */ class User { /** * Creates a new instance of the User class. * * @param {string} username - The unique identifier or name of the user. */ constructor(username) { this.username = username; } } /** * WebSocketManager: A class for managing WebSocket server and HTTP server interactions. * * This class is responsible for initializing the HTTP and WebSocket servers, configuring middleware, * managing lifecycle hooks, and delegating WebSocket operations to the WebSocketController. * * ## Responsibilities: * - Initializes the HTTP server and Express application. * - Configures WebSocket functionality and integrates WebSocketController for handling WebSocket events. * - Manages lifecycle hooks for WebSocket message processing. * - Provides methods to start, stop the server, and generate authentication tokens. * * ## Dependencies: * - Uses `createContainer` to resolve and inject dependencies including: * - `ILogger`: For logging server and WebSocket events. * - `IAuthService`: For token generation and authentication. * - `IWebSocketService`: For WebSocket operations. * - `WebSocketController`: To handle WebSocket connections and messaging. * - Accepts `IWebSocketManagerOptions` to customize behavior and configuration. */ class WebSocketManager { /** * Constructor: Initializes the WebSocketManager with provided options. * * @param {IWebSocketManagerOptions} [options={}] - Optional configuration for the WebSocket manager. */ constructor(options = {}) { var _a; this.options = options; const container = createContainer({ port: options.port || 8080, secretKey: options.secretKey || 'default_secret', tokenExpiry: options.tokenExpiry || '1h', wsOptions: options.wsOptions, hooks: { beforeSend: options.beforeSend, afterSend: options.afterSend, }, enableLogging: (_a = options.enableLogging) !== null && _a !== void 0 ? _a : true, setupRoutes: options.setupRoutes, onMessage: options.onMessage, }); this.logger = container.get(TYPES.ILogger); this.authService = container.get(TYPES.IAuthService); this.webSocketService = container.get(TYPES.IWebSocketService); this.webSocketController = container.get(TYPES.WebSocketController); this.app = express(); this.server = http.createServer(this.app); this.port = options.port || 8080; this.setupMiddleware(); if (options.setupRoutes) { options.setupRoutes(this.app); } this.setupWebSocket(); if (options.beforeSend || options.afterSend) { this.webSocketController.setLifecycleHooks(options.beforeSend, options.afterSend); } } /** *Configures middleware for the Express application. * * @private * @memberof WebSocketManager */ setupMiddleware() { this.app.use(express.json()); } /** * Configures WebSocket upgrade handling and delegates it to WebSocketController. * * @private * @memberof WebSocketManager */ setupWebSocket() { this.server.on('upgrade', (request, socket, head) => { this.webSocketController.handleUpgrade(request, socket, head); }); } /** * Starts the HTTP server and WebSocket server. * * @memberof WebSocketManager */ start() { this.server.listen(this.port, () => { this.logger.log(`Server is running on port ${this.port}`); }); } /** * Stops the HTTP server and closes the WebSocket server. * * @memberof WebSocketManager */ stop() { this.webSocketController.close(); this.server.close(() => { this.logger.log('Server has been stopped.'); }); } /** * Generates an authentication token for the given username. * * @param {string} username - The username for which the token is generated. * @returns {string} - A JWT token representing the authenticated user. */ generateToken(username) { return this.authService.generateToken(username); } } export { AuthService, Config, LoggerService, TYPES, User, WebSocketController, WebSocketManager, WebSocketService, createContainer }; //# sourceMappingURL=ts-websocket.esm.js.map