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

487 lines (486 loc) 18.6 kB
/** * Express Server Adapter * Server adapter implementation using Express framework */ import { logger } from "../../utils/logger.js"; import { AlreadyRunningError, ServerAdapterError, ServerStartError, ServerStopError, wrapError, } from "../errors.js"; import { BaseServerAdapter } from "../abstract/baseServerAdapter.js"; import { isErrorResponse } from "../utils/validation.js"; /** * Express-specific server adapter */ export class ExpressServerAdapter extends BaseServerAdapter { app; server; frameworkInitialized = false; sockets = new Set(); constructor(neurolink, config = {}) { super(neurolink, config); } /** * Initialize Express framework asynchronously */ initializeFramework() { // Framework will be initialized asynchronously in initializeFrameworkAsync // This is called by the base class constructor, but we need async imports } /** * Initialize Express framework with async imports */ async initializeFrameworkAsync() { if (this.frameworkInitialized) { return; } // Dynamic import to avoid loading if not used (ESM-compatible) const expressModule = await import("express"); const express = expressModule.default; this.app = express(); // Body parsing if (this.config.bodyParser.enabled) { const bodyLimit = this.config.bodyParser.maxSize || this.config.bodyParser.jsonLimit || "10mb"; this.app.use(express.json({ limit: this.config.bodyParser.jsonLimit || bodyLimit })); this.app.use(express.urlencoded({ extended: true, limit: bodyLimit })); } // CORS if (this.config.cors.enabled) { const corsModule = await import("cors"); const cors = corsModule.default; this.app.use(cors({ origin: this.config.cors.origins, methods: this.config.cors.methods, allowedHeaders: this.config.cors.headers, credentials: this.config.cors.credentials, maxAge: this.config.cors.maxAge, })); } // Rate limiting if (this.config.rateLimit.enabled) { const rateLimitModule = await import("express-rate-limit"); const rateLimit = rateLimitModule.default; const windowMs = this.config.rateLimit.windowMs; const limiter = rateLimit({ windowMs, max: this.config.rateLimit.maxRequests, message: { error: { code: "RATE_LIMIT_EXCEEDED", message: this.config.rateLimit.message, }, }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { const retryAfter = Math.ceil(windowMs / 1000); res.setHeader("Retry-After", String(retryAfter)); res.status(429).json({ error: { code: "RATE_LIMIT_EXCEEDED", message: this.config.rateLimit.message, }, metadata: { timestamp: new Date().toISOString(), retryAfter, }, }); }, }); this.app.use(limiter); } // Request ID middleware this.app.use((req, res, next) => { const requestId = req.headers["x-request-id"] || this.generateRequestId(); req.headers["x-request-id"] = requestId; res.setHeader("X-Request-ID", requestId); next(); }); // Logging middleware if (this.config.logging.enabled) { this.app.use((req, res, next) => { const startTime = Date.now(); const requestId = req.headers["x-request-id"]; logger.info(`[ExpressAdapter] ${req.method} ${req.path}`, { requestId, }); res.on("finish", () => { logger.info(`[ExpressAdapter] ${req.method} ${req.path} ${res.statusCode}`, { requestId, duration: Date.now() - startTime, }); }); next(); }); } this.frameworkInitialized = true; } /** * Override initialize to ensure async framework setup */ async initialize() { // Initialize Express asynchronously first await this.initializeFrameworkAsync(); // Then call base class initialize await super.initialize(); } /** * Register route with Express */ registerFrameworkRoute(route) { const method = route.method.toLowerCase(); this.app[method](route.path, async (req, res, next) => { const requestId = req.headers["x-request-id"]; const startTime = Date.now(); // Create server context const ctx = this.createContext({ requestId, method: req.method, path: req.path, headers: req.headers, query: req.query, params: req.params, body: req.body, }); // Copy response headers from middleware (stored in res.locals by middleware) if (res.locals.responseHeaders) { ctx.responseHeaders = { ...res.locals.responseHeaders }; } // Emit request event this.emit("request", { requestId, method: ctx.method, path: ctx.path, timestamp: new Date(), }); try { // Handle streaming if configured if (route.streaming?.enabled) { return this.handleStreamingResponse(res, ctx, route); } // Execute handler const result = await route.handler(ctx); const duration = Date.now() - startTime; // Check if result is an error response if (isErrorResponse(result)) { const statusCode = result.httpStatus ?? 500; // Emit response event with error status this.emit("response", { requestId, statusCode, duration, timestamp: new Date(), }); // Apply response headers from middleware and handler if (ctx.responseHeaders) { for (const [key, value] of Object.entries(ctx.responseHeaders)) { res.setHeader(key, value); } } // Return error response with proper status code res.status(statusCode).json({ error: result.error, metadata: { ...result.metadata, requestId, timestamp: new Date().toISOString(), duration, }, }); return; } // Emit response event this.emit("response", { requestId, statusCode: 200, duration, timestamp: new Date(), }); // Apply response headers from middleware and handler if (ctx.responseHeaders) { for (const [key, value] of Object.entries(ctx.responseHeaders)) { res.setHeader(key, value); } } // Return formatted response res.json({ data: result, metadata: { requestId, timestamp: new Date().toISOString(), duration, }, }); } catch (error) { next(error); } }); // Error handling middleware (should be registered once after all routes) this.setupErrorHandler(); } /** * Setup error handling middleware */ errorHandlerRegistered = false; setupErrorHandler() { if (this.errorHandlerRegistered) { return; } this.errorHandlerRegistered = true; this.app.use((error, req, res, _next) => { const requestId = req.headers["x-request-id"]; logger.error("[ExpressAdapter] Request error", { requestId, error: error.message, stack: error.stack, }); this.emit("error", { requestId, error, timestamp: new Date(), }); // Use dynamic status code from ServerAdapterError if available const isServerAdapterError = error instanceof ServerAdapterError; const statusCode = isServerAdapterError ? error.getHttpStatus() : 500; const errorCode = isServerAdapterError ? error.code : "INTERNAL_ERROR"; const errorMessage = isServerAdapterError ? error.message : "An internal error occurred"; res.status(statusCode).json({ error: { code: errorCode, message: errorMessage, }, metadata: { requestId, timestamp: new Date().toISOString(), }, }); }); } /** * Handle streaming response using SSE */ async handleStreamingResponse(res, ctx, route) { res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); try { const result = await route.handler(ctx); if (result && typeof result === "object" && Symbol.asyncIterator in result) { for await (const chunk of result) { res.write(`event: message\n`); res.write(`data: ${JSON.stringify(chunk)}\n\n`); } } else { res.write(`event: complete\n`); res.write(`data: ${JSON.stringify(result)}\n\n`); } res.write(`event: done\n`); res.write(`data: \n\n`); res.end(); } catch (error) { res.write(`event: error\n`); res.write(`data: ${JSON.stringify({ error: error instanceof Error ? error.message : "Stream error", })}\n\n`); res.end(); } } /** * Register middleware with Express */ registerFrameworkMiddleware(middleware) { const paths = middleware.paths || ["/"]; for (const path of paths) { this.app.use(path, async (req, res, next) => { // Skip excluded paths if (middleware.excludePaths?.some((p) => req.path.startsWith(p))) { return next(); } // Initialize response headers storage in res.locals if not present if (!res.locals.responseHeaders) { res.locals.responseHeaders = {}; } // Create context with existing response headers from previous middleware const ctx = this.createContext({ requestId: req.headers["x-request-id"] || this.generateRequestId(), method: req.method, path: req.path, headers: req.headers, query: req.query, params: req.params, body: req.body, }); // Copy existing response headers to context ctx.responseHeaders = { ...res.locals.responseHeaders }; // Execute middleware try { await middleware.handler(ctx, async () => { // After middleware execution, merge response headers back to res.locals if (ctx.responseHeaders) { Object.assign(res.locals.responseHeaders, ctx.responseHeaders); } return new Promise((resolve) => { next(); resolve(); }); }); // Also merge headers after handler returns (for middleware that set headers after next()) if (ctx.responseHeaders) { Object.assign(res.locals.responseHeaders, ctx.responseHeaders); } } catch (error) { next(error); } }); } } /** * Start the Express server */ async start() { // Validate lifecycle state this.validateLifecycleState("start", ["initialized", "stopped"]); if (this.isRunning) { throw new AlreadyRunningError(this.config.port, this.config.host); } this.lifecycleState = "starting"; const { port, host } = this.config; const startupTimeout = this.config.timeout || 30000; const startPromise = new Promise((resolve, reject) => { try { this.server = this.app.listen(port, host, () => { this.isRunning = true; this.startTime = new Date(); this.lifecycleState = "running"; // Track connections for graceful shutdown this.server?.on("connection", (socket) => { const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; this.sockets.add(socket); this.trackConnection(connectionId, socket); socket.on("close", () => { this.sockets.delete(socket); this.untrackConnection(connectionId); }); }); logger.info(`[ExpressAdapter] Server started on ${host}:${port}`); this.emit("started", { port, host, timestamp: this.startTime, }); resolve(); }); this.server?.on("error", (error) => { this.lifecycleState = "error"; reject(error); }); } catch (error) { this.lifecycleState = "error"; reject(error); } }); let startupTimer; const timeoutPromise = new Promise((_, reject) => { startupTimer = setTimeout(() => { this.lifecycleState = "error"; reject(new ServerStartError(`Express server startup timed out after ${startupTimeout}ms`, undefined, port, host)); }, startupTimeout); }); try { await Promise.race([startPromise, timeoutPromise]); } finally { // Always clear the timeout to prevent memory leak if (startupTimer) { clearTimeout(startupTimer); } } } /** * Stop the Express server with graceful shutdown */ async stop() { if (!this.isRunning || !this.server) { return; // Already stopped, return gracefully (idempotent) } const uptime = this.startTime ? Date.now() - this.startTime.getTime() : 0; try { // Use graceful shutdown from base class await this.gracefulShutdown(); logger.info("[ExpressAdapter] Server stopped", { uptime }); this.emit("stopped", { uptime, timestamp: new Date(), }); // Reset state for restart capability this.resetServerState(); this.server = undefined; this.sockets.clear(); this.frameworkInitialized = false; this.errorHandlerRegistered = false; } catch (error) { const wrappedError = wrapError(error); throw new ServerStopError(wrappedError.message, wrappedError); } } // ============================================ // Lifecycle Methods (Framework-Specific) // ============================================ /** * Stop accepting new connections */ async stopAcceptingConnections() { // For Express/Node.js HTTP server, we don't explicitly stop accepting // The server.close() will stop accepting new connections // But we can prevent new requests by setting a flag checked in middleware logger.debug("[ExpressAdapter] Stopping acceptance of new connections"); } /** * Close the underlying server */ async closeServer() { return new Promise((resolve, reject) => { if (!this.server) { resolve(); return; } this.server.close((err) => { if (err) { reject(err); } else { resolve(); } }); }); } /** * Force close all active connections */ async forceCloseConnections() { logger.info("[ExpressAdapter] Force closing connections", { count: this.sockets.size, }); for (const socket of this.sockets) { socket.destroy(); } this.sockets.clear(); this.activeConnections.clear(); } /** * Get the Express app instance */ getFrameworkInstance() { return this.app; } }