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