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