@gati-framework/runtime
Version:
Gati runtime execution engine for running handler-based applications
470 lines • 16.7 kB
JavaScript
/**
* @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