UNPKG

@udarapremadasa/iwa-web-server

Version:

A TypeScript library for creating web servers compatible with IWA (Isolated Web Apps) using the Direct Sockets API

277 lines 10.6 kB
/** * IWAServer - Main web server class for Isolated Web Apps * * This is the primary API for creating and managing a web server * within an IWA using the Direct Sockets API. */ import { TCPServer } from './core/TCPServer'; import { ConnectionManager } from './core/ConnectionManager'; import { RequestHandler } from './core/RequestHandler'; import { Router } from './http/Router'; import { StaticFileHandler } from './http/StaticFileHandler'; import { WebSocketHandler } from './websocket/WebSocketHandler'; import { Logger } from './utils/Logger'; export class IWAServer { constructor(options = {}) { this.options = { port: options.port || 33000, // Default port above 32678 host: options.host || 'localhost', staticDir: options.staticDir || '/static', enableWebSocket: options.enableWebSocket !== false, maxConnections: options.maxConnections || 100, timeout: options.timeout || 30000, logLevel: options.logLevel || 'info', maxHeaderSize: options.maxHeaderSize || 8192, maxBodySize: options.maxBodySize || 1048576, maxCacheSize: options.maxCacheSize || 50, maxFileSize: options.maxFileSize || 1024 * 1024, enableCache: options.enableCache !== false }; this.logger = new Logger(this.options.logLevel); this.router = new Router(); this.tcpServer = new TCPServer(this.options); this.connectionManager = new ConnectionManager(this.options); this.requestHandler = new RequestHandler(this.router, this.options); this.staticFileHandler = new StaticFileHandler(this.options.staticDir); if (this.options.enableWebSocket) { this.webSocketHandler = new WebSocketHandler(); } this.isRunning = false; this.actualHost = null; this.actualPort = null; this.startTime = null; this.setupDefaultRoutes(); } /** * Start the server */ async start() { try { this.logger.info(`Starting IWA Server on ${this.options.host}:${this.options.port}`); await this.tcpServer.listen(this.options.port, this.options.host); this.isRunning = true; this.startTime = Date.now(); // Get the actual assigned port const serverInfo = this.tcpServer.getInfo(); this.actualHost = serverInfo.actualHost || this.options.host; this.actualPort = serverInfo.actualPort || this.options.port; // Convert 0.0.0.0 to localhost for display purposes const displayHost = this.actualHost === '0.0.0.0' ? 'localhost' : this.actualHost; // Handle incoming connections this.tcpServer.onConnection((socket) => { this.handleConnection(socket); }); this.logger.info(`Server running at http://${displayHost}:${this.actualPort}`); return true; } catch (error) { this.logger.error('Failed to start server:', error); throw error; } } /** * Stop the server */ async stop() { try { this.logger.info('Stopping IWA Server...'); this.isRunning = false; await this.connectionManager.closeAllConnections(); await this.tcpServer.close(); this.logger.info('Server stopped'); return true; } catch (error) { this.logger.error('Error stopping server:', error); throw error; } } /** * Add a route handler */ get(path, handler) { this.router.get(path, handler); return this; } post(path, handler) { this.router.post(path, handler); return this; } put(path, handler) { this.router.put(path, handler); return this; } delete(path, handler) { this.router.delete(path, handler); return this; } /** * Add middleware */ use(middleware) { this.router.use(middleware); return this; } /** * Handle WebSocket connections */ onWebSocket(path, handler) { if (!this.webSocketHandler) { throw new Error('WebSocket support is disabled'); } this.webSocketHandler.addHandler(path, handler); return this; } /** * Serve static files from a directory */ static(path, directory) { this.router.get(`${path}/*`, (req, res) => { const filePath = req.url.replace(path, ''); return this.staticFileHandler.serve(filePath, res); }); return this; } /** * Handle incoming TCP connection */ async handleConnection(socket) { let connectionId = null; let reader = null; let writer = null; let isWebSocketConnection = false; let wsConnection = null; try { connectionId = this.connectionManager.addConnection(socket); this.logger.debug(`New connection: ${connectionId}`); // Wait for the socket to be opened and get the streams const { readable, writable } = await socket.opened; reader = readable.getReader(); writer = writable.getWriter(); this.logger.debug(`Starting to read from connection: ${connectionId}`); while (this.isRunning && reader && writer) { try { const { value, done } = await reader.read(); if (done) { this.logger.debug(`Connection ${connectionId} closed by client`); break; } this.logger.debug(`Received ${value.length} bytes from ${connectionId}`); if (isWebSocketConnection && wsConnection) { // Process as WebSocket frames await wsConnection.processData(value); } else { // Process as HTTP request const result = await this.processRequest(value, writer, connectionId); // Check if this was a WebSocket upgrade if (result && result.type === 'websocket_upgrade' && result.connection) { isWebSocketConnection = true; wsConnection = result.connection; this.logger.debug(`Connection ${connectionId} upgraded to WebSocket`); } } } catch (readError) { this.logger.error(`Read error for connection ${connectionId}:`, readError); break; } } } catch (error) { this.logger.error(`Connection error for ${connectionId || 'unknown'}:`, { message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, name: error instanceof Error ? error.name : 'Unknown' }); } finally { // Clean up resources try { if (wsConnection) { await wsConnection.close(1001, 'Server shutdown'); } if (reader) { reader.releaseLock(); } if (writer) { writer.releaseLock(); } if (connectionId) { this.connectionManager.removeConnection(connectionId); this.logger.debug(`Connection cleaned up: ${connectionId}`); } } catch (cleanupError) { this.logger.error('Cleanup error:', cleanupError); } } } /** * Process incoming request data */ async processRequest(data, writer, connectionId) { try { this.logger.debug(`Processing ${data.length} bytes for connection ${connectionId}`); const result = await this.requestHandler.handle(data, writer, { connectionId, staticHandler: this.staticFileHandler, webSocketHandler: this.webSocketHandler }); this.logger.debug(`Request processed successfully for connection ${connectionId}`); return result; } catch (error) { this.logger.error(`Request processing error for connection ${connectionId}:`, { message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, name: error instanceof Error ? error.name : 'Unknown' }); try { // Send 500 error response const errorResponse = 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n'; await writer.write(new TextEncoder().encode(errorResponse)); } catch (writeError) { this.logger.error(`Failed to send error response:`, writeError); } return null; } } /** * Setup default routes */ setupDefaultRoutes() { // Health check endpoint this.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: Date.now() }); }); // Server info endpoint this.get('/info', (req, res) => { res.json({ name: 'IWA Web Server', version: '1.0.0', port: this.options.port, webSocketEnabled: this.options.enableWebSocket }); }); } /** * Get server status */ getStatus() { // Convert 0.0.0.0 to localhost for display purposes const displayHost = (this.actualHost === '0.0.0.0') ? 'localhost' : (this.actualHost || this.options.host); return { running: this.isRunning, port: this.actualPort || this.options.port, host: displayHost, requestedPort: this.options.port, requestedHost: this.options.host, connections: this.connectionManager.getConnectionCount(), uptime: this.isRunning ? Date.now() - (this.startTime || 0) : 0 }; } } export default IWAServer; //# sourceMappingURL=IWAServer.js.map