@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
JavaScript
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