UNPKG

@pulzar/core

Version:

Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support

632 lines 21.2 kB
import "reflect-metadata"; import { logger } from "../utils/logger"; import { EventEmitter } from "events"; export class UWSGateway extends EventEmitter { app = null; uWS = null; connections = new Map(); messageHandlers = new Map(); middlewares = []; guards = []; pipes = []; interceptors = []; options; heartbeatTimer; isListening = false; constructor(options = {}) { super(); this.options = { port: 3001, host: "0.0.0.0", compression: 0, // uWS.SHARED_COMPRESSOR maxBackpressure: 64 * 1024, idleTimeout: 120, maxPayloadLength: 16 * 1024, enableMetrics: true, enableHeartbeat: true, heartbeatInterval: 30000, ssl: undefined, ...options, }; } /** * Initialize uWebSockets.js */ async initialize() { try { // Dynamic import for uWebSockets.js this.uWS = await this.loadUWS(); // Create app instance if (this.options.ssl) { this.app = this.uWS.SSLApp({ cert_file_name: this.options.ssl.cert, key_file_name: this.options.ssl.key, passphrase: this.options.ssl.passphrase, }); } else { this.app = this.uWS.App(); } // Setup WebSocket behavior this.app.ws("/*", { compression: this.options.compression, maxBackpressure: this.options.maxBackpressure, idleTimeout: this.options.idleTimeout, maxPayloadLength: this.options.maxPayloadLength, upgrade: this.handleUpgrade.bind(this), open: this.handleOpen.bind(this), message: this.handleMessage.bind(this), close: this.handleClose.bind(this), drain: this.handleDrain.bind(this), ping: this.handlePing.bind(this), pong: this.handlePong.bind(this), }); // Setup HTTP routes for WebSocket info this.app.get("/ws/info", this.handleWSInfo.bind(this)); this.app.get("/ws/stats", this.handleWSStats.bind(this)); logger.info("uWebSockets.js gateway initialized (placeholder)", { port: this.options.port, host: this.options.host, }); } catch (error) { logger.error("Failed to initialize uWebSockets.js gateway", { error }); throw error; } } /** * Load uWebSockets.js dynamically */ async loadUWS() { try { // Try to load uWebSockets.js dynamically const uwsModule = await this.dynamicImportUWS(); if (uwsModule) { logger.info("uWebSockets.js loaded successfully"); return uwsModule; } else { logger.warn("uWebSockets.js not available, using mock implementation"); return this.createMockUWS(); } } catch (error) { logger.error("Failed to load uWebSockets.js", { error }); return this.createMockUWS(); } } async dynamicImportUWS() { try { // Try different possible module names const possibleNames = ["uws", "uWebSockets.js"]; for (const moduleName of possibleNames) { try { return await new Function(`return import("${moduleName}")`)(); } catch { continue; } } return null; } catch { return null; } } createMockUWS() { const connections = new Map(); const rooms = new Map(); return { App: () => ({ ws: (pattern, options) => { logger.debug("Mock UWS WebSocket route", { pattern }); return this; }, get: (pattern, handler) => { logger.debug("Mock UWS GET route", { pattern }); return this; }, post: (pattern, handler) => { logger.debug("Mock UWS POST route", { pattern }); return this; }, listen: (portOrHost, portOrCallback, callback) => { const port = typeof portOrHost === "number" ? portOrHost : this.options.port; const finalCallback = typeof portOrCallback === "function" ? portOrCallback : callback; setTimeout(() => { logger.info("Mock UWS server listening", { port }); finalCallback?.({ port }); }, 0); return this; }, publish: (topic, message, opCode, compress) => { const room = rooms.get(topic); if (room) { logger.debug("Mock UWS publish", { topic, subscribers: room.size }); // Simulate publishing to subscribers for (const connectionId of room) { const connection = connections.get(connectionId); if (connection && connection.send) { connection.send(message); } } return true; } return false; }, numSubscribers: (topic) => { return rooms.get(topic)?.size || 0; }, close: () => { connections.clear(); rooms.clear(); logger.info("Mock UWS server closed"); }, }), SSLApp: () => ({ // Same interface as App but for SSL ws: () => this, get: () => this, post: () => this, listen: () => this, publish: () => true, numSubscribers: () => 0, close: () => { }, }), SHARED_COMPRESSOR: 0, DEDICATED_COMPRESSOR: 1, DISABLED: 2, }; } /** * Start listening for WebSocket connections */ async listen() { if (!this.app) { throw new Error("Gateway not initialized. Call initialize() first."); } if (this.isListening) { logger.warn("Gateway already listening"); return; } // Start listening return new Promise((resolve, reject) => { this.app.listen(this.options.host, this.options.port, (token) => { if (token) { this.isListening = true; this.emit("listening"); logger.info("uWebSockets.js gateway listening", { host: this.options.host, port: this.options.port, compression: this.options.compression, heartbeat: this.options.enableHeartbeat, }); if (this.options.enableHeartbeat) { this.startHeartbeat(); } resolve(); } else { const error = new Error(`Failed to listen on ${this.options.host}:${this.options.port}`); reject(error); } }); }); } /** * Handle WebSocket upgrade */ handleUpgrade(res, req, context) { // WebSocket upgrade logic would go here // For now, just accept all upgrades res.upgrade({ url: req.getUrl() }, req.getHeader("sec-websocket-key"), req.getHeader("sec-websocket-protocol"), req.getHeader("sec-websocket-extensions"), context); } /** * Handle new WebSocket connection */ handleOpen(ws) { const connectionId = this.generateConnectionId(); const connectionInfo = { id: connectionId, connectedAt: new Date(), lastActivity: new Date(), subscriptions: new Set(), metadata: {}, authenticated: false, }; // Store connection info in WebSocket user data ws.getUserData = () => connectionInfo; this.connections.set(connectionId, connectionInfo); logger.debug("WebSocket connection opened", { connectionId, totalConnections: this.connections.size, }); this.emit("connection", { connection: connectionInfo, ws }); } /** * Handle WebSocket message */ async handleMessage(ws, messageBuffer, opCode) { const connectionInfo = ws.getUserData(); if (!connectionInfo) { logger.warn("Message from unknown connection"); return; } connectionInfo.lastActivity = new Date(); try { // Parse message const messageText = Buffer.from(messageBuffer).toString(); const message = JSON.parse(messageText); logger.debug("WebSocket message received", { connectionId: connectionInfo.id, type: message.type, size: messageBuffer.byteLength, }); // Apply middlewares await this.applyMiddlewares(connectionInfo, message); // Apply guards const canActivate = await this.applyGuards(connectionInfo, message); if (!canActivate) { this.sendError(ws, "Access denied", message.id); return; } // Apply pipes const transformedMessage = await this.applyPipes(message, connectionInfo); // Apply interceptors and handle message await this.applyInterceptors(connectionInfo, transformedMessage, async () => { await this.handleIncomingMessage(ws, connectionInfo, transformedMessage); }); } catch (error) { logger.error("Error processing WebSocket message", { connectionId: connectionInfo.id, error: error instanceof Error ? error.message : String(error), }); this.sendError(ws, "Message processing failed", undefined); } } /** * Handle incoming parsed message */ async handleIncomingMessage(ws, connection, message) { const handlers = this.messageHandlers.get(message.type) || []; for (const handler of handlers) { try { const result = await handler.handle(connection, message, ws); if (result !== undefined) { this.sendMessage(ws, { type: `${message.type}:response`, data: result, id: message.id, }); } } catch (error) { logger.error("Message handler error", { connectionId: connection.id, messageType: message.type, error: error instanceof Error ? error.message : String(error), }); this.sendError(ws, "Handler error", message.id); } } this.emit("message", { connection, message, ws }); } /** * Handle WebSocket connection close */ handleClose(ws, code, message) { const connectionInfo = ws.getUserData(); if (connectionInfo) { this.connections.delete(connectionInfo.id); logger.debug("WebSocket connection closed", { connectionId: connectionInfo.id, code, totalConnections: this.connections.size, }); this.emit("disconnect", { connection: connectionInfo, code, message }); } } /** * Handle WebSocket drain event */ handleDrain(ws) { const connectionInfo = ws.getUserData(); if (connectionInfo) { logger.debug("WebSocket drained", { connectionId: connectionInfo.id }); this.emit("drain", { connection: connectionInfo, ws }); } } /** * Handle ping */ handlePing(ws, message) { const connectionInfo = ws.getUserData(); if (connectionInfo) { connectionInfo.lastActivity = new Date(); this.emit("ping", { connection: connectionInfo, message }); } } /** * Handle pong */ handlePong(ws, message) { const connectionInfo = ws.getUserData(); if (connectionInfo) { connectionInfo.lastActivity = new Date(); this.emit("pong", { connection: connectionInfo, message }); } } /** * Apply middlewares */ async applyMiddlewares(connection, message) { for (const middleware of this.middlewares) { await new Promise((resolve, reject) => { let called = false; const next = () => { if (called) return; called = true; resolve(); }; try { const result = middleware(connection, message, next); if (result instanceof Promise) { result.catch(reject); } } catch (error) { reject(error); } }); } } /** * Apply guards */ async applyGuards(connection, message) { for (const guard of this.guards) { const canActivate = await guard.canActivate(connection, message); if (!canActivate) { return false; } } return true; } /** * Apply pipes */ async applyPipes(message, connection) { let result = message; for (const pipe of this.pipes) { result = await pipe.transform(result, connection); } return result; } /** * Apply interceptors */ async applyInterceptors(connection, message, next) { let index = 0; const executeInterceptor = async () => { if (index < this.interceptors.length) { const interceptor = this.interceptors[index++]; if (interceptor) { await interceptor.intercept(connection, message, executeInterceptor); } } else { await next(); } }; await executeInterceptor(); } /** * Send message to WebSocket connection */ sendMessage(ws, message) { try { const messageText = JSON.stringify({ ...message, timestamp: Date.now(), }); const result = ws.send(messageText); return result === 1; // uWS returns 1 for success } catch (error) { logger.error("Failed to send WebSocket message", { error }); return false; } } /** * Send error message */ sendError(ws, error, messageId) { this.sendMessage(ws, { type: "error", data: { error }, id: messageId, }); } /** * Broadcast message to all connections */ broadcast(message) { return this.connections.size; } /** * Publish to topic */ publishToTopic(topic, message) { if (!this.app) return false; const messageText = JSON.stringify({ ...message, timestamp: Date.now(), }); return this.app.publish(topic, messageText); } /** * Register message handler */ registerMessageHandler(type, handler) { if (!this.messageHandlers.has(type)) { this.messageHandlers.set(type, []); } this.messageHandlers.get(type).push(handler); logger.debug("WebSocket message handler registered", { type }); } /** * Add middleware */ use(middleware) { this.middlewares.push(middleware); } /** * Add guard */ addGuard(guard) { this.guards.push(guard); } /** * Add pipe */ addPipe(pipe) { this.pipes.push(pipe); } /** * Add interceptor */ addInterceptor(interceptor) { this.interceptors.push(interceptor); } /** * Start heartbeat timer */ startHeartbeat() { this.heartbeatTimer = setInterval(() => { const now = Date.now(); const timeout = this.options.idleTimeout * 1000; for (const [connectionId, connection] of this.connections) { if (now - connection.lastActivity.getTime() > timeout) { logger.debug("Connection timed out", { connectionId }); // In real implementation, close the connection this.connections.delete(connectionId); } } }, this.options.heartbeatInterval); } /** * Handle WebSocket info endpoint */ handleWSInfo(res, req) { const info = { connections: this.connections.size, messageTypes: Array.from(this.messageHandlers.keys()), middlewares: this.middlewares.length, guards: this.guards.length, pipes: this.pipes.length, interceptors: this.interceptors.length, }; res.writeHeader("Content-Type", "application/json"); res.end(JSON.stringify(info)); } /** * Handle WebSocket stats endpoint */ handleWSStats(res, req) { const stats = { totalConnections: this.connections.size, authenticatedConnections: Array.from(this.connections.values()).filter((c) => c.authenticated).length, subscriptions: Array.from(this.connections.values()).reduce((total, c) => total + c.subscriptions.size, 0), uptime: process.uptime(), memory: process.memoryUsage(), }; res.writeHeader("Content-Type", "application/json"); res.end(JSON.stringify(stats)); } /** * Generate unique connection ID */ generateConnectionId() { return `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Get connection statistics */ getStats() { return { isListening: this.isListening, totalConnections: this.connections.size, authenticatedConnections: Array.from(this.connections.values()).filter((c) => c.authenticated).length, messageHandlers: this.messageHandlers.size, middlewares: this.middlewares.length, guards: this.guards.length, pipes: this.pipes.length, interceptors: this.interceptors.length, }; } /** * Shutdown gateway */ async shutdown() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = undefined; } if (this.app) { this.app.close(); this.app = null; } this.connections.clear(); this.messageHandlers.clear(); this.middlewares.length = 0; this.guards.length = 0; this.pipes.length = 0; this.interceptors.length = 0; this.isListening = false; this.emit("shutdown"); logger.info("uWebSockets.js gateway shutdown"); } } /** * WebSocket gateway decorator */ export function WebSocketGateway(options = {}) { return function (target) { // Store gateway metadata Reflect.defineMetadata("ws:gateway", options, target); return target; }; } /** * WebSocket message handler decorator */ export function SubscribeMessage(type) { return function (target, propertyKey, descriptor) { // Store message handler metadata Reflect.defineMetadata("ws:message", { type }, target, propertyKey); return descriptor; }; } /** * WebSocket connection decorator for parameter injection */ export function ConnectedSocket() { return function (target, propertyKey, parameterIndex) { // Store parameter metadata Reflect.defineMetadata("ws:connection", { parameterIndex }, target, propertyKey); }; } /** * WebSocket message payload decorator */ export function MessageBody() { return function (target, propertyKey, parameterIndex) { // Store parameter metadata Reflect.defineMetadata("ws:payload", { parameterIndex }, target, propertyKey); }; } export default UWSGateway; //# sourceMappingURL=uws-gateway.js.map