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

473 lines (472 loc) 18.3 kB
/** * Fastify Server Adapter * Server adapter implementation using Fastify framework * Fastify is known for its high performance and low overhead */ import { logger } from "../../utils/logger.js"; import { AlreadyRunningError, ServerStartError, ServerStopError, wrapError, } from "../errors.js"; import { withTimeout } from "../../utils/errorHandling.js"; import { BaseServerAdapter } from "../abstract/baseServerAdapter.js"; import { isErrorResponse } from "../utils/validation.js"; /** * Fastify-specific server adapter * Provides high-performance HTTP server with schema validation */ export class FastifyServerAdapter extends BaseServerAdapter { app = null; frameworkInitialized = false; constructor(neurolink, config = {}) { super(neurolink, config); } /** * Initialize Fastify framework * Called by base class but actual initialization happens in initializeFrameworkAsync */ initializeFramework() { // Framework will be initialized asynchronously in initializeFrameworkAsync // This is called by the base class constructor, but we need async imports } /** * Initialize Fastify framework with async imports */ async initializeFrameworkAsync() { if (this.frameworkInitialized) { return; } // Dynamic import to avoid loading if not used const fastifyModule = await import("fastify"); const Fastify = fastifyModule.default; this.app = Fastify({ logger: this.config.logging.enabled ? { level: this.config.logging.level, } : false, requestIdHeader: "x-request-id", genReqId: () => this.generateRequestId(), bodyLimit: this.parseBodyLimit(this.config.bodyParser.maxSize), }); // Register CORS plugin if enabled if (this.config.cors.enabled) { const corsModule = await import("@fastify/cors"); await this.app.register(corsModule.default, { 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, }); } // Register rate limiting plugin if enabled if (this.config.rateLimit.enabled) { const rateLimitModule = await import("@fastify/rate-limit"); const windowMs = this.config.rateLimit.windowMs; await this.app.register(rateLimitModule.default, { max: this.config.rateLimit.maxRequests, timeWindow: windowMs, errorResponseBuilder: (_request, context) => ({ error: { code: "RATE_LIMIT_EXCEEDED", message: this.config.rateLimit.message, }, metadata: { retryAfter: Math.ceil(context.ttl / 1000), timestamp: new Date().toISOString(), }, }), }); // Add hook to set Retry-After header for 429 responses this.app.addHook("onSend", async (_request, reply, payload) => { if (reply.statusCode === 429) { const retryAfter = Math.ceil(windowMs / 1000); reply.header("Retry-After", String(retryAfter)); } return payload; }); } // Add request ID to response headers this.app.addHook("onRequest", async (request, reply) => { reply.header("X-Request-ID", request.id); }); // Global error handler this.app.setErrorHandler((error, request, reply) => { const requestId = request.id; logger.error("[FastifyAdapter] Request error", { requestId, error: error.message, stack: error.stack, }); this.emit("error", { requestId, error, timestamp: new Date(), }); // Handle different error types if (error.statusCode) { reply.status(error.statusCode).send({ error: { code: `HTTP_${error.statusCode}`, message: error.message, }, metadata: { requestId, timestamp: new Date().toISOString(), }, }); return; } reply.status(500).send({ error: { code: "INTERNAL_ERROR", message: "An internal error occurred", }, metadata: { requestId, timestamp: new Date().toISOString(), }, }); }); // 404 handler this.app.setNotFoundHandler((request, reply) => { reply.status(404).send({ error: { code: "NOT_FOUND", message: `Route ${request.method} ${request.url} not found`, }, metadata: { requestId: request.id, timestamp: new Date().toISOString(), }, }); }); this.frameworkInitialized = true; } /** * Parse body limit string to number (bytes) */ parseBodyLimit(limit) { const match = limit.match(/^(\d+)(mb|kb|gb)?$/i); if (!match) { return 10 * 1024 * 1024; // Default 10MB } const value = parseInt(match[1], 10); const unit = (match[2] || "b").toLowerCase(); switch (unit) { case "gb": return value * 1024 * 1024 * 1024; case "mb": return value * 1024 * 1024; case "kb": return value * 1024; default: return value; } } /** * Override initialize to ensure async framework setup */ async initialize() { // Initialize Fastify asynchronously first await this.initializeFrameworkAsync(); // Then call base class initialize await super.initialize(); } /** * Register route with Fastify */ registerFrameworkRoute(route) { if (!this.app) { throw new Error("Fastify app not initialized. Call initialize() before registering routes."); } const method = route.method.toUpperCase(); // Fastify does not allow duplicate method+path registrations. // Skip if route already exists (e.g., built-in health routes). if (this.app.hasRoute({ method, url: route.path })) { return; } this.app.route({ method, url: route.path, handler: async (request, reply) => { const requestId = request.id; const startTime = Date.now(); // Create server context const ctx = this.createContext({ requestId, method: request.method, path: request.url.split("?")[0], // Remove query string headers: request.headers, query: request.query, params: request.params, body: request.body, }); // Copy response headers from middleware (stored in request by middleware) const reqWithHeaders = request; if (reqWithHeaders.responseHeaders) { ctx.responseHeaders = { ...reqWithHeaders.responseHeaders }; } // Emit request event this.emit("request", { requestId, method: ctx.method, path: ctx.path, timestamp: new Date(), }); // Handle streaming if configured if (route.streaming?.enabled) { return this.handleStreamingResponse(reply, 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)) { reply.header(key, value); } } // Return error response with proper status code reply.status(statusCode); return { error: result.error, metadata: { ...result.metadata, requestId, timestamp: new Date().toISOString(), duration, }, }; } // 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)) { reply.header(key, value); } } // Return formatted response return { data: result, metadata: { requestId, timestamp: new Date().toISOString(), duration, }, }; }, }); } /** * Handle streaming response using Server-Sent Events */ async handleStreamingResponse(reply, ctx, route) { // Set SSE headers reply.raw.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", "X-Accel-Buffering": "no", // Disable nginx buffering }); try { const result = await route.handler(ctx); // Check if result is an async iterable if (result && typeof result === "object" && Symbol.asyncIterator in result) { for await (const chunk of result) { reply.raw.write(`event: message\n`); reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`); } } else { // Single result, send as complete event reply.raw.write(`event: complete\n`); reply.raw.write(`data: ${JSON.stringify(result)}\n\n`); } // Send done event reply.raw.write(`event: done\n`); reply.raw.write(`data: \n\n`); reply.raw.end(); } catch (error) { reply.raw.write(`event: error\n`); reply.raw.write(`data: ${JSON.stringify({ error: error instanceof Error ? error.message : "Stream error", })}\n\n`); reply.raw.end(); } } /** * Register middleware with Fastify */ registerFrameworkMiddleware(middleware) { if (!this.app) { throw new Error("Fastify app not initialized. Call initialize() before registering middleware."); } this.app.addHook("preHandler", async (request, _reply) => { // Skip excluded paths if (middleware.excludePaths?.some((p) => request.url.startsWith(p))) { return; } // Check if path matches const paths = middleware.paths || ["/"]; const matches = paths.some((p) => request.url.startsWith(p) || p === "*"); if (!matches) { return; } // Initialize response headers storage if not present const reqWithHeaders = request; if (!reqWithHeaders.responseHeaders) { reqWithHeaders.responseHeaders = {}; } // Create context with existing response headers from previous middleware const ctx = this.createContext({ requestId: request.id, method: request.method, path: request.url.split("?")[0], headers: request.headers, query: request.query, params: request.params, body: request.body, }); // Copy existing response headers to context ctx.responseHeaders = { ...reqWithHeaders.responseHeaders }; // Execute middleware await middleware.handler(ctx, async () => { // After middleware execution, merge response headers back to request if (ctx.responseHeaders) { Object.assign(reqWithHeaders.responseHeaders, ctx.responseHeaders); } }); // Also merge headers after handler returns (for middleware that set headers after next()) if (ctx.responseHeaders) { Object.assign(reqWithHeaders.responseHeaders, ctx.responseHeaders); } }); } /** * Start the Fastify server */ async start() { // Validate lifecycle state this.validateLifecycleState("start", ["initialized", "stopped"]); if (this.isRunning) { throw new AlreadyRunningError(this.config.port, this.config.host); } if (!this.app) { throw new Error("Fastify app not initialized. Call initialize() before starting."); } // Capture non-null reference for use in closures below const app = this.app; this.lifecycleState = "starting"; const { port, host } = this.config; const startupTimeout = this.config.timeout || 30000; // Track connections via Fastify hooks (must be registered before listen) app.addHook("onRequest", async (request) => { const connectionId = `conn-${request.id}`; this.trackConnection(connectionId, request.raw.socket, request.id); }); app.addHook("onResponse", async (request) => { const connectionId = `conn-${request.id}`; this.untrackConnection(connectionId); }); try { await withTimeout(app.listen({ port, host }), startupTimeout, new ServerStartError(`Fastify server startup timed out after ${startupTimeout}ms`, undefined, port, host)); this.isRunning = true; this.startTime = new Date(); this.lifecycleState = "running"; logger.info(`[FastifyAdapter] Server started on ${host}:${port}`); this.emit("started", { port, host, timestamp: this.startTime, }); } catch (error) { this.lifecycleState = "error"; throw error; } } /** * Stop the Fastify server with graceful shutdown */ async stop() { if (!this.isRunning) { 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("[FastifyAdapter] Server stopped", { uptime }); this.emit("stopped", { uptime, timestamp: new Date(), }); // Reset state for restart capability this.resetServerState(); this.frameworkInitialized = false; this.app = null; } catch (error) { const wrappedError = wrapError(error); throw new ServerStopError(wrappedError.message, wrappedError); } } // ============================================ // Lifecycle Methods (Framework-Specific) // ============================================ /** * Stop accepting new connections */ async stopAcceptingConnections() { // Fastify's close() handles this internally logger.debug("[FastifyAdapter] Stopping acceptance of new connections"); } /** * Close the underlying server */ async closeServer() { if (this.app) { const closeTimeout = this.shutdownConfig.gracefulShutdownTimeoutMs; await withTimeout(this.app.close(), closeTimeout, new Error(`Fastify server close timed out after ${closeTimeout}ms`)); } } /** * Force close all active connections */ async forceCloseConnections() { logger.info("[FastifyAdapter] Force closing connections", { count: this.activeConnections.size, }); // Get the underlying server and destroy all sockets const server = this.app?.server; if (server) { // Force close by destroying the server server.closeAllConnections?.(); } this.activeConnections.clear(); } /** * Get the Fastify instance */ getFrameworkInstance() { return this.app; } }