@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
633 lines (632 loc) • 23.9 kB
JavaScript
/**
* Hono Server Adapter
* Primary server adapter implementation using Hono framework
* Hono is chosen for its performance, TypeScript-first design, and edge compatibility
*/
import { Hono } from "hono";
import { cors } from "hono/cors";
import { HTTPException } from "hono/http-exception";
import { logger as honoLogger } from "hono/logger";
import { secureHeaders } from "hono/secure-headers";
import { streamSSE } from "hono/streaming";
import { timeout } from "hono/timeout";
import { logger } from "../../utils/logger.js";
import { AlreadyRunningError, ServerStopError, wrapError } from "../errors.js";
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
import { isErrorResponse } from "../utils/validation.js";
/**
* Hono-specific server adapter
* Supports multiple runtimes: Bun, Deno, Node.js
*/
export class HonoServerAdapter extends BaseServerAdapter {
app;
server;
rateLimitStore = new Map();
rateLimitCleanupInterval;
// Store context by request ID for sharing between middleware and route handlers
requestContextStore = new Map();
constructor(neurolink, config = {}) {
super(neurolink, config);
}
/**
* Create rate limiter middleware for Hono
*/
createRateLimiter() {
return async (c, next) => {
// Skip rate limiting for excluded paths
if (this.config.rateLimit.skipPaths) {
for (const skipPath of this.config.rateLimit.skipPaths) {
if (c.req.path.startsWith(skipPath)) {
return next();
}
}
}
// Use custom keyGenerator if provided, otherwise default to IP-based key
let key;
if (this.config.rateLimit.keyGenerator) {
// Build minimal context for keyGenerator
const headers = {};
c.req.raw.headers.forEach((value, name) => {
headers[name] = value;
});
const minCtx = {
requestId: c.req.header("X-Request-ID") || this.generateRequestId(),
method: c.req.method,
path: c.req.path,
headers,
query: this.extractQuery(c),
params: c.req.param(),
neurolink: this.neurolink,
toolRegistry: this.toolRegistry,
externalServerManager: this.externalServerManager,
timestamp: Date.now(),
metadata: {},
};
key = this.config.rateLimit.keyGenerator(minCtx);
}
else {
key =
c.req.header("x-forwarded-for") ||
c.req.header("x-real-ip") ||
"unknown";
}
const now = Date.now();
const windowMs = this.config.rateLimit.windowMs;
const maxRequests = this.config.rateLimit.maxRequests;
let record = this.rateLimitStore.get(key);
if (!record || now > record.resetAt) {
record = { count: 0, resetAt: now + windowMs };
this.rateLimitStore.set(key, record);
}
record.count++;
// Set rate limit headers
c.header("X-RateLimit-Limit", String(maxRequests));
c.header("X-RateLimit-Remaining", String(Math.max(0, maxRequests - record.count)));
c.header("X-RateLimit-Reset", String(Math.ceil(record.resetAt / 1000)));
if (record.count > maxRequests) {
const retryAfter = Math.ceil((record.resetAt - now) / 1000);
c.header("Retry-After", String(retryAfter));
return c.json({
error: {
code: "RATE_LIMIT_EXCEEDED",
message: this.config.rateLimit.message,
},
metadata: {
timestamp: new Date().toISOString(),
retryAfter,
},
}, 429);
}
return next();
};
}
/**
* Periodically clean up expired rate limit entries
*/
cleanupRateLimitStore() {
const now = Date.now();
for (const [key, entry] of this.rateLimitStore.entries()) {
if (now > entry.resetAt) {
this.rateLimitStore.delete(key);
}
}
}
/**
* Initialize Hono framework
*/
initializeFramework() {
this.app = new Hono();
// Add secure headers
this.app.use("*", secureHeaders());
// Add CORS if enabled
if (this.config.cors.enabled) {
this.app.use("*", cors({
origin: this.config.cors.origins,
allowMethods: this.config.cors.methods,
allowHeaders: this.config.cors.headers,
credentials: this.config.cors.credentials,
maxAge: this.config.cors.maxAge,
}));
}
// Add timeout middleware
this.app.use("*", timeout(this.config.timeout));
// Add logging if enabled
if (this.config.logging.enabled) {
this.app.use("*", honoLogger());
}
// Add rate limiting if enabled
if (this.config.rateLimit.enabled) {
this.app.use("*", this.createRateLimiter());
// Schedule periodic cleanup of expired rate limit entries (every minute)
this.rateLimitCleanupInterval = setInterval(() => this.cleanupRateLimitStore(), 60000);
}
// Global error handler
this.app.onError((error, c) => {
const requestId = c.req.header("X-Request-ID") || this.generateRequestId();
logger.error("[HonoAdapter] Request error", {
requestId,
error: error.message,
stack: error.stack,
});
this.emit("error", {
requestId,
error,
timestamp: new Date(),
});
if (error instanceof HTTPException) {
return c.json({
error: {
code: `HTTP_${error.status}`,
message: error.message,
},
metadata: {
requestId,
timestamp: new Date().toISOString(),
},
}, error.status);
}
return c.json({
error: {
code: "INTERNAL_ERROR",
message: "An internal error occurred",
},
metadata: {
requestId,
timestamp: new Date().toISOString(),
},
}, 500);
});
// 404 handler
this.app.notFound((c) => {
return c.json({
error: {
code: "NOT_FOUND",
message: `Route ${c.req.method} ${c.req.path} not found`,
},
}, 404);
});
}
/**
* Register route with Hono
*/
registerFrameworkRoute(route) {
const method = route.method.toLowerCase();
this.app[method](route.path, async (c) => {
const requestId = c.req.header("X-Request-ID") || this.generateRequestId();
const connectionId = `conn-${requestId}`;
const startTime = Date.now();
// Track connection for graceful shutdown
this.trackConnection(connectionId, undefined, requestId);
try {
// Extract path parameters
const params = c.req.param();
// Reuse existing context from middleware if available, otherwise create new one
const existingCtx = this.requestContextStore.get(requestId);
const ctx = existingCtx ||
this.createContext({
requestId,
method: c.req.method,
path: c.req.path,
headers: this.extractHeaders(c),
query: this.extractQuery(c),
params,
body: await this.extractBody(c),
});
// 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 await this.handleStreamingResponse(c, 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
if (ctx.responseHeaders) {
for (const [key, value] of Object.entries(ctx.responseHeaders)) {
c.header(key, value);
}
}
// Return error response with proper status code
// Cast to ContentfulStatusCode since we know error codes are valid HTTP status codes
return c.json({
error: result.error,
metadata: {
...result.metadata,
requestId,
timestamp: new Date().toISOString(),
duration,
},
}, statusCode);
}
// Emit response event
this.emit("response", {
requestId,
statusCode: 200,
duration,
timestamp: new Date(),
});
// Apply response headers from middleware
if (ctx.responseHeaders) {
for (const [key, value] of Object.entries(ctx.responseHeaders)) {
c.header(key, value);
}
}
// Return formatted response
return c.json({
data: result,
metadata: {
requestId,
timestamp: new Date().toISOString(),
duration,
},
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error("[HonoAdapter] Handler error", {
requestId,
route: route.path,
error: errorMessage,
});
throw new HTTPException(500, { message: errorMessage });
}
finally {
// Untrack connection when request completes
this.untrackConnection(connectionId);
// Clean up context store to prevent memory leaks
this.requestContextStore.delete(requestId);
}
});
}
/**
* Handle streaming response using SSE
*/
async handleStreamingResponse(c, ctx, route) {
// Apply middleware response headers to streaming response before starting the stream
if (ctx.responseHeaders) {
for (const [key, value] of Object.entries(ctx.responseHeaders)) {
c.header(key, value);
}
}
return streamSSE(c, async (stream) => {
try {
// Get streaming result from handler
const result = await route.handler(ctx);
// If result is an async iterable, stream it
if (result &&
typeof result === "object" &&
Symbol.asyncIterator in result) {
for await (const chunk of result) {
// Transform raw provider chunks into StreamEvent format
// The client SDK expects { type, content, timestamp, ... } objects
const streamEvent = this.toStreamEvent(chunk);
await stream.writeSSE({
data: JSON.stringify(streamEvent),
});
}
}
else if (result && isErrorResponse(result)) {
// Error result, send as error event
const errorResult = result;
const statusCode = errorResult.httpStatus ?? 500;
await stream.writeSSE({
data: JSON.stringify({
type: "error",
error: {
code: errorResult.error?.code ?? "STREAM_ERROR",
message: errorResult.error?.message ?? "Stream error",
status: statusCode,
},
timestamp: Date.now(),
}),
});
}
else {
// Single result, normalize into a StreamEvent so the client can
// match on the `type` field via handleEvent()
const streamEvent = this.toStreamEvent(result);
await stream.writeSSE({
data: JSON.stringify(streamEvent),
});
}
// Send [DONE] signal that the client SDK expects
await stream.writeSSE({
data: "[DONE]",
});
}
catch (error) {
await stream.writeSSE({
data: JSON.stringify({
type: "error",
error: {
code: "STREAM_ERROR",
message: error instanceof Error ? error.message : "Stream error",
status: 500,
},
timestamp: Date.now(),
}),
});
// Still send [DONE] after error so the client terminates cleanly
await stream.writeSSE({
data: "[DONE]",
});
}
});
}
/**
* Transform a raw provider chunk into a StreamEvent
* Provider chunks are typically { content: string } objects;
* the client SDK expects { type: "text", content: string, timestamp: number }
*/
toStreamEvent(chunk) {
// Already a StreamEvent (has a type field)
if (chunk &&
typeof chunk === "object" &&
"type" in chunk &&
typeof chunk.type === "string") {
return {
...chunk,
timestamp: chunk.timestamp ?? Date.now(),
};
}
// Raw text chunk from provider: { content: string }
if (chunk && typeof chunk === "object" && "content" in chunk) {
return {
type: "text",
content: String(chunk.content),
timestamp: Date.now(),
};
}
// String chunk
if (typeof chunk === "string") {
return {
type: "text",
content: chunk,
timestamp: Date.now(),
};
}
// Unknown shape — wrap as metadata event (StreamEventType has no "data" variant)
return {
type: "metadata",
metadata: chunk,
timestamp: Date.now(),
};
}
/**
* Register middleware with Hono
*/
registerFrameworkMiddleware(middleware) {
const paths = middleware.paths || ["*"];
for (const path of paths) {
this.app.use(path, async (c, next) => {
// Skip excluded paths
if (middleware.excludePaths?.some((p) => c.req.path.startsWith(p))) {
return next();
}
// Extract path parameters
const params = c.req.param();
// Get or generate request ID for context lookup
const requestId = c.req.header("X-Request-ID") || this.generateRequestId();
// Reuse existing context from previous middleware if available
let ctx = this.requestContextStore.get(requestId);
if (!ctx) {
// Create new context for the first middleware
ctx = this.createContext({
requestId,
method: c.req.method,
path: c.req.path,
headers: this.extractHeaders(c),
query: this.extractQuery(c),
params,
body: await this.extractBody(c),
});
// Store context for sharing between middleware and route handlers
this.requestContextStore.set(requestId, ctx);
}
// Execute middleware
return middleware.handler(ctx, next);
});
}
}
/**
* Start the Hono 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;
try {
// Check if running in Bun environment
if (typeof Bun !== "undefined") {
this.server = Bun.serve({
port,
hostname: host,
fetch: this.app.fetch,
});
}
else if (typeof Deno !== "undefined") {
// Deno runtime
this.server = Deno.serve({
port,
hostname: host,
}, this.app.fetch);
}
else {
// Fallback to Node.js http module via @hono/node-server
const { serve } = await import("@hono/node-server");
this.server = serve({
fetch: this.app.fetch,
port,
hostname: host,
});
}
this.isRunning = true;
this.startTime = new Date();
this.lifecycleState = "running";
logger.info(`[HonoAdapter] Server started on ${host}:${port}`);
this.emit("started", {
port,
host,
timestamp: this.startTime,
});
}
catch (error) {
this.lifecycleState = "error";
throw error;
}
}
/**
* Stop the Hono 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();
// Clean up rate limit cleanup interval
if (this.rateLimitCleanupInterval) {
clearInterval(this.rateLimitCleanupInterval);
this.rateLimitCleanupInterval = undefined;
}
logger.info("[HonoAdapter] Server stopped", { uptime });
this.emit("stopped", {
uptime,
timestamp: new Date(),
});
// Reset state for restart capability
this.resetServerState();
this.server = undefined;
this.rateLimitStore.clear();
}
catch (error) {
const wrappedError = wrapError(error);
throw new ServerStopError(wrappedError.message, wrappedError);
}
}
// ============================================
// Lifecycle Methods (Framework-Specific)
// ============================================
/**
* Stop accepting new connections
*/
async stopAcceptingConnections() {
// For Hono, this is handled by the underlying server
logger.debug("[HonoAdapter] Stopping acceptance of new connections");
}
/**
* Close the underlying server
*/
async closeServer() {
if (!this.server) {
return;
}
// Handle different server types (Bun, Deno, Node.js)
const serverObj = this.server;
if (typeof serverObj.stop === "function") {
// Bun server
serverObj.stop();
}
else if (typeof serverObj.shutdown === "function") {
// Deno server
await serverObj.shutdown();
}
else if (typeof serverObj.close === "function") {
// Node.js http server
await new Promise((resolve, reject) => {
serverObj.close?.((err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
}
/**
* Force close all active connections
*/
async forceCloseConnections() {
logger.info("[HonoAdapter] Force closing connections", {
count: this.activeConnections.size,
});
if (this.server) {
const serverObj = this.server;
if (typeof serverObj.closeAllConnections === "function") {
serverObj.closeAllConnections();
}
}
this.activeConnections.clear();
}
/**
* Get the Hono app instance
*/
getFrameworkInstance() {
return this.app;
}
// ============================================
// Helper Methods
// ============================================
extractHeaders(c) {
const headers = {};
c.req.raw.headers.forEach((value, key) => {
headers[key] = value;
});
return headers;
}
extractQuery(c) {
const query = {};
const url = new URL(c.req.url);
url.searchParams.forEach((value, key) => {
query[key] = value;
});
return query;
}
async extractBody(c) {
if (!this.config.bodyParser.enabled) {
return undefined;
}
const contentType = c.req.header("Content-Type") || "";
if (contentType.includes("application/json")) {
try {
return await c.req.json();
}
catch {
return undefined;
}
}
if (contentType.includes("application/x-www-form-urlencoded")) {
try {
return await c.req.parseBody();
}
catch {
return undefined;
}
}
return undefined;
}
}