UNPKG

@gati-framework/runtime

Version:

Gati runtime execution engine for running handler-based applications

470 lines 16.7 kB
/** * @module runtime/app-core * @description Main application orchestrator for Gati framework */ import { createServer } from 'http'; import { createRequest } from './request.js'; import { createResponse } from './response.js'; import { createGlobalContext, createLocalContext } from './context-manager.js'; import { createRouteManager } from './route-manager.js'; import { createMiddlewareManager } from './middleware.js'; import { executeHandler } from './handler-engine.js'; import { createLogger } from './logger.js'; /** * Generate instance ID for distributed deployment */ function generateInstanceId() { return process.env['INSTANCE_ID'] || process.env['HOSTNAME'] || `instance_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } /** * Generate trace ID for distributed tracing */ function generateTraceId() { return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 16)}`; } /** * Main Gati application class */ export class GatiApp { server = null; router; middleware; gctx; config; isShuttingDown = false; activeRequests = 0; logger; constructor(config = {}) { this.config = { port: config.port || 3000, host: config.host || 'localhost', timeout: config.timeout || 30000, logging: config.logging !== false, logger: config.logger, cluster: config.cluster, performance: config.performance, tracing: config.tracing, services: config.services, instance: config.instance, playground: config.playground, }; this.logger = createLogger({ name: 'gati-app', ...this.config.logger, }); // Create global context with instance metadata this.gctx = createGlobalContext({ instance: { id: this.config.instance?.id || generateInstanceId(), region: this.config.instance?.region || process.env['AWS_REGION'] || 'local', zone: this.config.instance?.zone || process.env['AWS_AVAILABILITY_ZONE'] || 'local-a', }, config: this.config, services: {}, // Will be populated by modules }); this.router = createRouteManager(); this.middleware = createMiddlewareManager(); // Add default logging middleware if enabled if (this.config.logging) { this.use(this.createLoggingMiddleware()); } // Add default error handling this.useError(this.createDefaultErrorHandler()); } /** * Register a middleware function */ use(middleware) { this.middleware.use(middleware); } /** * Register an error handling middleware */ useError(middleware) { this.middleware.useError(middleware); } /** * Register a GET route */ get(path, handler) { this.router.get(path, handler); } /** * Register a POST route */ post(path, handler) { this.router.post(path, handler); } /** * Register a PUT route */ put(path, handler) { this.router.put(path, handler); } /** * Register a PATCH route */ patch(path, handler) { this.router.patch(path, handler); } /** * Register a DELETE route */ delete(path, handler) { this.router.delete(path, handler); } /** * Register a route dynamically */ registerRoute(method, path, handler) { const methodUpper = method.toUpperCase(); switch (methodUpper) { case 'GET': this.router.get(path, handler); break; case 'POST': this.router.post(path, handler); break; case 'PUT': this.router.put(path, handler); break; case 'PATCH': this.router.patch(path, handler); break; case 'DELETE': this.router.delete(path, handler); break; } } /** * Unregister a route */ unregisterRoute(method, path) { this.router.unregister(method.toUpperCase(), path); } /** * Start the HTTP server */ async listen() { if (this.server) { throw new Error('Server is already running'); } return new Promise((resolve, reject) => { try { this.server = createServer((req, res) => { void this.handleRequest(req, res); }); this.server.timeout = this.config.timeout; this.server.listen(this.config.port, this.config.host, () => { if (this.config.logging) { this.logger.info({ port: this.config.port, host: this.config.host }, 'Gati server listening'); } resolve(); }); this.server.on('error', (error) => { reject(error); }); } catch (error) { reject(error); } }); } /** * Stop the HTTP server gracefully * Waits for active requests to complete before shutting down */ async close() { if (!this.server) { return; } this.isShuttingDown = true; // Wait for active requests to complete (with timeout) const maxWaitMs = 10000; // 10 seconds const checkIntervalMs = 100; // Check every 100ms const startTime = Date.now(); while (this.activeRequests > 0 && Date.now() - startTime < maxWaitMs) { await new Promise(resolve => setTimeout(resolve, checkIntervalMs)); } if (this.activeRequests > 0 && this.config.logging) { this.logger.warn({ activeRequests: this.activeRequests }, 'Server shutting down with active requests still pending'); } return new Promise((resolve, reject) => { this.server?.close((error) => { if (error) { reject(error); } else { this.server = null; this.isShuttingDown = false; if (this.config.logging) { this.logger.info('Gati server shut down successfully'); } resolve(); } }); }); } /** * Parse request body based on Content-Type */ async parseRequestBody(req) { return new Promise((resolve, reject) => { const chunks = []; req.on('data', (chunk) => { chunks.push(chunk); }); req.on('end', () => { try { const rawBody = Buffer.concat(chunks); // No body content if (rawBody.length === 0) { resolve({ body: undefined, rawBody: '' }); return; } const contentType = req.headers['content-type'] || ''; // Parse JSON if (contentType.includes('application/json')) { const bodyString = rawBody.toString('utf-8'); try { const parsed = JSON.parse(bodyString); resolve({ body: parsed, rawBody: bodyString }); } catch (error) { // Invalid JSON, return as text resolve({ body: undefined, rawBody: bodyString }); } return; } // Parse URL-encoded form data if (contentType.includes('application/x-www-form-urlencoded')) { const bodyString = rawBody.toString('utf-8'); const params = new URLSearchParams(bodyString); const parsed = {}; params.forEach((value, key) => { parsed[key] = value; }); resolve({ body: parsed, rawBody: bodyString }); return; } // Default: return as text or buffer if (contentType.includes('text/')) { const bodyString = rawBody.toString('utf-8'); resolve({ body: bodyString, rawBody: bodyString }); } else { resolve({ body: rawBody, rawBody }); } } catch (error) { reject(error); } }); req.on('error', (error) => { reject(error); }); }); } /** * Handle incoming HTTP requests */ async handleRequest(incomingMessage, serverResponse) { // Check if shutting down if (this.isShuttingDown) { serverResponse.statusCode = 503; serverResponse.end('Service Unavailable'); return; } // Track active requests this.activeRequests++; // Set request timeout const requestTimeout = setTimeout(() => { if (!serverResponse.headersSent) { serverResponse.statusCode = 408; serverResponse.setHeader('Content-Type', 'application/json'); serverResponse.end(JSON.stringify({ error: 'Request Timeout', message: 'Request exceeded configured timeout', })); } }, this.config.timeout); let lctx = null; try { // Parse request body const { body, rawBody } = await this.parseRequestBody(incomingMessage); // Create request and response objects const req = createRequest({ raw: incomingMessage, method: (incomingMessage.method || 'GET'), path: incomingMessage.url || '/', body, rawBody, }); const res = createResponse({ raw: serverResponse }); // Extract distributed tracing headers const traceId = incomingMessage.headers['x-trace-id'] || generateTraceId(); const parentSpanId = incomingMessage.headers['x-parent-span-id']; // Generate client ID and metadata const clientIp = incomingMessage.socket.remoteAddress || 'unknown'; const userAgent = incomingMessage.headers['user-agent'] || 'unknown'; const clientIdentifier = `${clientIp}:${userAgent}`; // Create a consistent client ID using a simple hash let hash = 0; for (let i = 0; i < clientIdentifier.length; i++) { const char = clientIdentifier.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } const clientId = `client_${Math.abs(hash).toString(36)}`; // Extract external references from headers/cookies const sessionId = incomingMessage.headers['x-session-id'] || this.extractSessionFromCookie(incomingMessage.headers.cookie); const userId = incomingMessage.headers['x-user-id']; const tenantId = incomingMessage.headers['x-tenant-id']; // Create lightweight local context for this request lctx = createLocalContext({ traceId, parentSpanId, clientId, refs: { sessionId, userId, tenantId, }, client: { ip: clientIp, userAgent, region: this.gctx.instance.region, }, meta: { timestamp: Date.now(), instanceId: this.gctx.instance.id, region: this.gctx.instance.region, method: req.method, path: req.path || '/', }, }, undefined, this.gctx.timescape.registry); try { // Execute middleware chain and route handler await this.middleware.execute(req, res, this.gctx, lctx, async () => { // Find matching route const match = this.router.match(req.method, req.path || '/'); if (!match) { // No route found res.status(404).json({ error: 'Not Found', message: `Cannot ${req.method} ${req.path}`, }); return; } // Attach path params to request req.params = match.params; // Execute the handler await executeHandler(match.route.handler, req, res, this.gctx, lctx); }); } catch (error) { // This should be caught by middleware error handling // But if it reaches here, send a generic error if (!res.headersSent) { res.status(500).json({ error: 'Internal Server Error', message: error instanceof Error ? error.message : 'Unknown error', }); } } } finally { // Execute request cleanup hooks if (lctx) { try { await lctx.lifecycle.executeCleanup(); } catch (cleanupError) { console.error('Request cleanup failed:', cleanupError); } } // Clear request timeout clearTimeout(requestTimeout); // Decrement active request counter this.activeRequests--; } } /** * Create default logging middleware */ createLoggingMiddleware() { return async (req, _res, _gctx, lctx, next) => { const start = Date.now(); const requestId = lctx.requestId || 'unknown'; this.logger.info({ requestId, method: req.method, path: req.path }, 'Incoming request'); await next(); const duration = Date.now() - start; this.logger.info({ requestId, method: req.method, path: req.path, duration }, 'Request completed'); }; } /** * Create default error handler */ createDefaultErrorHandler() { return (error, _req, res, _gctx, lctx) => { const requestId = lctx.requestId || 'unknown'; this.logger.error({ requestId, error: error.message, stack: error.stack }, 'Request error'); if (!res.headersSent) { res.status(500).json({ error: 'Internal Server Error', message: this.config.logging ? error.message : 'An error occurred', ...(this.config.logging && { requestId }), }); } }; } /** * Get the current configuration */ getConfig() { return { ...this.config }; } /** * Check if server is running */ isRunning() { return this.server !== null && !this.isShuttingDown; } /** * Get global context (for module initialization) */ getGlobalContext() { return this.gctx; } /** * Get route manager (for clearing routes) */ getRouteManager() { return this.router; } /** * Extract session ID from cookie header */ extractSessionFromCookie(cookieHeader) { if (!cookieHeader) return undefined; const cookies = cookieHeader.split(';'); for (const cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'sessionId' || name === 'session_id') { return value; } } return undefined; } } /** * Create a new Gati application */ export function createApp(config) { return new GatiApp(config); } //# sourceMappingURL=app-core.js.map