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