UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

575 lines 22.8 kB
/** * Abstract Server Adapter * Base class for all framework-specific server adapters * Follows NeuroLink's composition and factory patterns */ import { EventEmitter } from "events"; import { getMetricsAggregator, SpanSerializer, SpanStatus, SpanType, } from "../../observability/index.js"; import { withTimeout } from "../../utils/errorHandling.js"; import { logger } from "../../utils/logger.js"; import { DrainTimeoutError, InvalidLifecycleStateError, ShutdownTimeoutError, } from "../errors.js"; /** * Abstract base class for server adapters * Provides common functionality and defines the interface for framework-specific implementations */ export class BaseServerAdapter extends EventEmitter { config; redactionConfig; neurolink; toolRegistry; externalServerManager; routes = new Map(); middlewares = []; isRunning = false; startTime; // Lifecycle management properties lifecycleState = "uninitialized"; activeConnections = new Map(); shutdownConfig; constructor(neurolink, config = {}) { super(); this.neurolink = neurolink; this.toolRegistry = neurolink.getToolRegistry(); this.externalServerManager = neurolink.getExternalServerManager(); // Store redaction config (optional, disabled by default) this.redactionConfig = config.redaction; // Apply shutdown defaults this.shutdownConfig = { gracefulShutdownTimeoutMs: config.shutdown?.gracefulShutdownTimeoutMs ?? 30000, drainTimeoutMs: config.shutdown?.drainTimeoutMs ?? 15000, forceClose: config.shutdown?.forceClose ?? true, }; // Apply defaults this.config = { port: config.port ?? 3000, host: config.host ?? "0.0.0.0", basePath: config.basePath ?? "/api", cors: { enabled: config.cors?.enabled ?? true, origins: config.cors?.origins ?? ["*"], methods: config.cors?.methods ?? [ "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", ], headers: config.cors?.headers ?? [ "Content-Type", "Authorization", "X-Request-ID", ], credentials: config.cors?.credentials ?? false, maxAge: config.cors?.maxAge ?? 86400, }, rateLimit: { enabled: config.rateLimit?.enabled ?? true, windowMs: config.rateLimit?.windowMs ?? 15 * 60 * 1000, // 15 minutes maxRequests: config.rateLimit?.maxRequests ?? 100, message: config.rateLimit?.message ?? "Too many requests, please try again later", skipPaths: config.rateLimit?.skipPaths, keyGenerator: config.rateLimit?.keyGenerator, }, bodyParser: { enabled: config.bodyParser?.enabled ?? true, maxSize: config.bodyParser?.maxSize ?? "10mb", jsonLimit: config.bodyParser?.jsonLimit ?? "10mb", urlEncoded: config.bodyParser?.urlEncoded ?? true, }, logging: { enabled: config.logging?.enabled ?? true, level: config.logging?.level ?? "info", includeBody: config.logging?.includeBody ?? false, includeResponse: config.logging?.includeResponse ?? false, }, timeout: config.timeout ?? 30000, enableMetrics: config.enableMetrics ?? true, enableSwagger: config.enableSwagger ?? false, disableBuiltInHealth: config.disableBuiltInHealth ?? false, shutdown: this.shutdownConfig, }; } // ============================================ // Common Methods (Shared Implementation) // ============================================ /** * Initialize the server adapter * Sets up routes, middleware, and framework */ async initialize() { // Validate lifecycle state for initialization if (this.lifecycleState !== "uninitialized" && this.lifecycleState !== "stopped") { throw new InvalidLifecycleStateError("initialize", this.lifecycleState, [ "uninitialized", "stopped", ]); } this.lifecycleState = "initializing"; logger.info("[ServerAdapter] Initializing server adapter", { port: this.config.port, host: this.config.host, basePath: this.config.basePath, }); const span = SpanSerializer.createSpan(SpanType.SERVER_REQUEST, "server.initialize", { "server.operation": "initialize", "server.port": this.config.port, "server.host": this.config.host, }); const startTime = Date.now(); try { // Initialize framework-specific setup this.initializeFramework(); // Register built-in middleware this.registerBuiltInMiddleware(); // Register built-in routes await this.registerBuiltInRoutes(); this.lifecycleState = "initialized"; this.emit("initialized", { config: this.config, routeCount: this.routes.size, middlewareCount: this.middlewares.length, }); logger.info("[ServerAdapter] Server adapter initialized", { routes: this.routes.size, middlewares: this.middlewares.length, }); span.durationMs = Date.now() - startTime; const endedSpan = SpanSerializer.endSpan(span, SpanStatus.OK); getMetricsAggregator().recordSpan(endedSpan); } catch (error) { this.lifecycleState = "error"; span.durationMs = Date.now() - startTime; const endedSpan = SpanSerializer.endSpan(span, SpanStatus.ERROR); endedSpan.statusMessage = error instanceof Error ? error.message : String(error); getMetricsAggregator().recordSpan(endedSpan); throw error; } } /** * Register a custom route */ registerRoute(route) { const routeKey = `${route.method.toUpperCase()}:${route.path}`; if (this.routes.has(routeKey)) { logger.warn(`[ServerAdapter] Route ${routeKey} already exists, replacing`); } this.routes.set(routeKey, route); this.registerFrameworkRoute(route); logger.debug(`[ServerAdapter] Registered route: ${routeKey}`, { description: route.description, streaming: route.streaming?.enabled, auth: route.auth, }); } /** * Register multiple routes from a route group */ registerRouteGroup(group) { // Register group-specific middleware first if (group.middleware) { for (const middleware of group.middleware) { this.registerMiddleware({ ...middleware, paths: middleware.paths ?? [group.prefix], }); } } // Register all routes in the group with prefix applied for (const route of group.routes) { // Only prepend prefix if route path doesn't already start with it // (route definitions include full paths like /api/agent/execute) const needsPrefix = !route.path.startsWith(group.prefix); const prefixedPath = this.normalizePath(needsPrefix ? `${group.prefix}${route.path}` : route.path); const prefixedRoute = { ...route, path: prefixedPath, }; this.registerRoute(prefixedRoute); } logger.debug(`[ServerAdapter] Registered route group: ${group.prefix}`, { routes: group.routes.length, middleware: group.middleware?.length ?? 0, }); } /** * Normalize a path by removing duplicate slashes and ensuring leading slash */ normalizePath(path) { return "/" + path.split("/").filter(Boolean).join("/"); } /** * Register custom middleware */ registerMiddleware(middleware) { this.middlewares.push(middleware); this.registerFrameworkMiddleware(middleware); logger.debug(`[ServerAdapter] Registered middleware: ${middleware.name}`, { order: middleware.order, paths: middleware.paths, }); } /** * Create request context from incoming request */ createContext(options) { return { requestId: options.requestId, method: options.method, path: options.path, headers: options.headers, query: options.query ?? {}, params: options.params ?? {}, body: options.body, neurolink: this.neurolink, toolRegistry: this.toolRegistry, externalServerManager: this.externalServerManager, timestamp: Date.now(), metadata: {}, redaction: this.redactionConfig, }; } /** * Register built-in middleware */ registerBuiltInMiddleware() { // Request ID middleware this.registerMiddleware({ name: "requestId", order: 0, handler: async (ctx, next) => { ctx.requestId = ctx.requestId || this.generateRequestId(); return next(); }, }); // Logging middleware if (this.config.logging.enabled) { this.registerMiddleware({ name: "logging", order: 1, handler: async (ctx, next) => { const start = Date.now(); logger.info(`[ServerAdapter] ${ctx.method} ${ctx.path}`, { requestId: ctx.requestId, }); const result = await next(); logger.info(`[ServerAdapter] ${ctx.method} ${ctx.path} completed`, { requestId: ctx.requestId, duration: Date.now() - start, }); return result; }, }); } } /** * Register built-in routes * Only registers health routes if disableBuiltInHealth is false (default) */ async registerBuiltInRoutes() { // Skip built-in health routes if disabled (to avoid duplication with healthRoutes) if (!this.config.disableBuiltInHealth) { // Health check this.registerRoute({ method: "GET", path: `${this.config.basePath}/health`, handler: async () => ({ status: "ok", timestamp: new Date().toISOString(), uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0, version: process.env.npm_package_version || "unknown", }), description: "Health check endpoint", tags: ["system"], }); // Ready check this.registerRoute({ method: "GET", path: `${this.config.basePath}/ready`, handler: async (ctx) => { const toolRegistry = ctx.toolRegistry; const readinessTimeout = this.config.timeout || 5000; let tools; let toolsAvailable; try { tools = await withTimeout(toolRegistry.listTools(), readinessTimeout, new Error(`toolRegistry.listTools timed out after ${readinessTimeout}ms`)); toolsAvailable = tools.length > 0; } catch (error) { logger.warn("[ServerAdapter] Tool registry check timed out", { timeout: readinessTimeout, error: error instanceof Error ? error.message : String(error), }); // Return degraded status but don't fail the readiness check toolsAvailable = false; } return { ready: true, timestamp: new Date().toISOString(), services: { neurolink: true, tools: toolsAvailable, externalServers: !!ctx.externalServerManager, }, }; }, description: "Readiness check endpoint", tags: ["system"], }); } // Metrics endpoint (if enabled) if (this.config.enableMetrics) { this.registerRoute({ method: "GET", path: `${this.config.basePath}/metrics`, handler: async () => { const status = this.getStatus(); return { server: { running: status.running, uptime: status.uptime, routes: status.routes, middlewares: status.middlewares, }, process: { memoryUsage: process.memoryUsage(), cpuUsage: process.cpuUsage(), pid: process.pid, }, timestamp: new Date().toISOString(), }; }, description: "Server metrics endpoint", tags: ["system"], }); } } /** * Generate unique request ID */ generateRequestId() { return `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } // ============================================ // Lifecycle Management Methods // ============================================ /** * Get the current lifecycle state */ getLifecycleState() { return this.lifecycleState; } /** * Track a new connection * @param id Unique connection identifier * @param socket Optional underlying socket object * @param requestId Optional associated request ID */ trackConnection(id, socket, requestId) { this.activeConnections.set(id, { id, createdAt: Date.now(), socket, requestId, isActive: true, }); logger.debug("[ServerAdapter] Connection tracked", { connectionId: id, activeConnections: this.activeConnections.size, }); } /** * Untrack a connection (when it's completed) * @param id Connection identifier to remove */ untrackConnection(id) { const removed = this.activeConnections.delete(id); if (removed) { logger.debug("[ServerAdapter] Connection untracked", { connectionId: id, activeConnections: this.activeConnections.size, }); } } /** * Get the number of active connections */ getActiveConnectionCount() { return this.activeConnections.size; } /** * Perform graceful shutdown with connection draining * This method handles the complete shutdown lifecycle */ async gracefulShutdown() { const { gracefulShutdownTimeoutMs, drainTimeoutMs, forceClose } = this.shutdownConfig; logger.info("[ServerAdapter] Starting graceful shutdown", { activeConnections: this.activeConnections.size, gracefulShutdownTimeoutMs, drainTimeoutMs, }); const shutdownSpan = SpanSerializer.createSpan(SpanType.SERVER_REQUEST, "server.shutdown", { "server.operation": "gracefulShutdown", "server.activeConnections": this.activeConnections.size, }); const shutdownStartTime = Date.now(); // Timer references for cleanup let shutdownTimer; let drainTimer; try { // Set draining state this.lifecycleState = "draining"; // Stop accepting new connections (covered by shutdown span) await this.stopAcceptingConnections(); logger.info("[ServerAdapter] Stopped accepting new connections"); // Create drain promise that resolves when all connections are closed const drainPromise = this.drainConnections(); // Create timeout promise for overall shutdown const shutdownTimeoutPromise = new Promise((_, reject) => { shutdownTimer = setTimeout(() => { reject(new ShutdownTimeoutError(gracefulShutdownTimeoutMs, this.activeConnections.size)); }, gracefulShutdownTimeoutMs); }); // Create timeout promise for drain phase const drainTimeoutPromise = new Promise((resolve) => { drainTimer = setTimeout(() => { resolve("drain_timeout"); }, drainTimeoutMs); }); // Race drain against drain timeout const drainResult = await Promise.race([ drainPromise, drainTimeoutPromise, ]); if (drainResult === "drain_timeout" && this.activeConnections.size > 0) { logger.warn("[ServerAdapter] Drain timeout reached", { remainingConnections: this.activeConnections.size, }); if (forceClose) { logger.info("[ServerAdapter] Force closing remaining connections"); await this.forceCloseConnections(); } else { throw new DrainTimeoutError(drainTimeoutMs, this.activeConnections.size); } } // Set stopping state this.lifecycleState = "stopping"; // Close the server with shutdown timeout await Promise.race([this.closeServer(), shutdownTimeoutPromise]); logger.info("[ServerAdapter] Server closed successfully"); shutdownSpan.durationMs = Date.now() - shutdownStartTime; const endedShutdownSpan = SpanSerializer.endSpan(shutdownSpan, SpanStatus.OK); getMetricsAggregator().recordSpan(endedShutdownSpan); } catch (error) { // If force close is enabled and we hit a timeout, try to force close if (forceClose && (error instanceof ShutdownTimeoutError || error instanceof DrainTimeoutError)) { logger.warn("[ServerAdapter] Timeout during shutdown, forcing close", { error: error.message, }); await this.forceCloseConnections(); await this.closeServer(); shutdownSpan.durationMs = Date.now() - shutdownStartTime; const endedShutdownSpan = SpanSerializer.endSpan(shutdownSpan, SpanStatus.OK); getMetricsAggregator().recordSpan(endedShutdownSpan); } else { this.lifecycleState = "error"; shutdownSpan.durationMs = Date.now() - shutdownStartTime; const endedShutdownSpan = SpanSerializer.endSpan(shutdownSpan, SpanStatus.ERROR); endedShutdownSpan.statusMessage = error instanceof Error ? error.message : String(error); getMetricsAggregator().recordSpan(endedShutdownSpan); throw error; } } finally { // Clean up timers to prevent memory leaks and unhandled rejections if (shutdownTimer) { clearTimeout(shutdownTimer); } if (drainTimer) { clearTimeout(drainTimer); } } } /** * Wait for all active connections to drain * Resolves when activeConnections is empty */ async drainConnections() { if (this.activeConnections.size === 0) { logger.debug("[ServerAdapter] No active connections to drain"); return; } logger.info("[ServerAdapter] Draining connections", { count: this.activeConnections.size, }); return new Promise((resolve) => { // Check periodically if all connections are drained const checkInterval = setInterval(() => { if (this.activeConnections.size === 0) { clearInterval(checkInterval); logger.info("[ServerAdapter] All connections drained"); resolve(); } }, 100); }); } /** * Reset server state for restart capability * Call this after stop() completes to allow restart */ resetServerState() { this.isRunning = false; this.startTime = undefined; this.activeConnections.clear(); this.lifecycleState = "stopped"; logger.debug("[ServerAdapter] Server state reset for restart capability"); } /** * Validate lifecycle state transition * @param operation The operation being performed * @param allowedStates States that allow the operation */ validateLifecycleState(operation, allowedStates) { if (!allowedStates.includes(this.lifecycleState)) { throw new InvalidLifecycleStateError(operation, this.lifecycleState, allowedStates); } } /** * Get server status */ getStatus() { return { running: this.isRunning, port: this.config.port, host: this.config.host, uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0, routes: this.routes.size, middlewares: this.middlewares.length, lifecycleState: this.lifecycleState, activeConnections: this.activeConnections.size, }; } /** * List all registered routes */ listRoutes() { return Array.from(this.routes.values()); } /** * Get configuration */ getConfig() { return { ...this.config }; } } //# sourceMappingURL=baseServerAdapter.js.map