/** * Main SmartServe server class * Cross-platform HTTP server with decorator-based routing */ import * as plugins from '../plugins.js'; import type { ISmartServeOptions, ISmartServeInstance, IRequestContext, IConnectionInfo, THttpMethod, IInterceptOptions, TRequestInterceptor, TResponseInterceptor, } from './smartserve.interfaces.js'; import { HttpError, RouteNotFoundError, ServerAlreadyRunningError } from './smartserve.errors.js'; import { AdapterFactory, type BaseAdapter, type TRequestHandler } from '../adapters/index.js'; import { ControllerRegistry, type ICompiledRoute } from '../decorators/index.js'; import { FileServer } from '../files/index.js'; import { WebDAVHandler } from '../protocols/index.js'; /** * SmartServe - Cross-platform HTTP server * * @example * ```typescript * const server = new SmartServe({ port: 3000 }); * * // Register decorated controllers * server.register(UserController); * server.register(ProductController); * * await server.start(); * ``` */ export class SmartServe { private options: ISmartServeOptions; private adapter: BaseAdapter | null = null; private instance: ISmartServeInstance | null = null; private customHandler: TRequestHandler | null = null; private fileServer: FileServer | null = null; private webdavHandler: WebDAVHandler | null = null; constructor(options: ISmartServeOptions) { this.options = { hostname: '0.0.0.0', ...options, }; // Initialize file server if static options provided if (this.options.static) { this.fileServer = new FileServer(this.options.static); } // Initialize WebDAV handler if configured if (this.options.webdav) { this.webdavHandler = new WebDAVHandler(this.options.webdav); } } /** * Register a controller class or instance */ register(controllerOrInstance: Function | object): this { if (typeof controllerOrInstance === 'function') { // It's a class constructor const instance = new (controllerOrInstance as new () => any)(); ControllerRegistry.registerInstance(instance); } else { // It's an instance ControllerRegistry.registerInstance(controllerOrInstance); } return this; } /** * Set a custom request handler (bypasses decorator routing) */ setHandler(handler: TRequestHandler): this { this.customHandler = handler; return this; } /** * Start the server */ async start(): Promise { if (this.instance) { throw new ServerAlreadyRunningError(); } // Create adapter for current runtime this.adapter = await AdapterFactory.createAdapter(this.options); // Create request handler const handler = this.createRequestHandler(); // Start server this.instance = await this.adapter.start(handler); return this.instance; } /** * Stop the server */ async stop(): Promise { if (this.adapter) { await this.adapter.stop(); this.adapter = null; this.instance = null; } } /** * Get server instance (if running) */ getInstance(): ISmartServeInstance | null { return this.instance; } /** * Check if server is running */ isRunning(): boolean { return this.instance !== null; } /** * Create the main request handler */ private createRequestHandler(): TRequestHandler { return async (request: Request, connectionInfo: IConnectionInfo): Promise => { // Use custom handler if set if (this.customHandler) { return this.customHandler(request, connectionInfo); } // Parse URL and method const url = new URL(request.url); const method = request.method.toUpperCase() as THttpMethod; // Handle WebDAV requests first if handler is configured if (this.webdavHandler && this.webdavHandler.isWebDAVRequest(request)) { try { return await this.webdavHandler.handle(request); } catch (error) { return this.handleError(error as Error, request); } } // Match route first const match = ControllerRegistry.matchRoute(url.pathname, method); if (!match) { // No route found, try WebDAV for GET/PUT/DELETE/HEAD (standard HTTP methods WebDAV also handles) if (this.webdavHandler) { try { return await this.webdavHandler.handle(request); } catch (error) { return this.handleError(error as Error, request); } } // Try static files if (this.fileServer && (method === 'GET' || method === 'HEAD')) { try { const staticResponse = await this.fileServer.serve(request); if (staticResponse) { return staticResponse; } } catch (error) { return this.handleError(error as Error, request); } } // Still no match, return 404 const error = new RouteNotFoundError(url.pathname, method); if (this.options.onError) { return this.options.onError(error, request); } return error.toResponse(); } const { route, params } = match; try { // Create request context const context = await this.createContext(request, url, params, connectionInfo); // Run interceptors and handler return await this.executeRoute(route, context); } catch (error) { return this.handleError(error as Error, request); } }; } /** * Create request context from Request object */ private async createContext( request: Request, url: URL, params: Record, connectionInfo: IConnectionInfo ): Promise { // Parse query params const query: Record = {}; url.searchParams.forEach((value, key) => { query[key] = value; }); // Parse body (lazy) let body: any = undefined; const contentType = request.headers.get('content-type'); if (request.method !== 'GET' && request.method !== 'HEAD') { if (contentType?.includes('application/json')) { try { body = await request.json(); } catch { body = null; } } else if (contentType?.includes('application/x-www-form-urlencoded')) { try { const text = await request.text(); body = Object.fromEntries(new URLSearchParams(text)); } catch { body = null; } } else if (contentType?.includes('text/')) { try { body = await request.text(); } catch { body = null; } } } return { request, body, params, query, headers: request.headers, path: url.pathname, method: request.method.toUpperCase() as THttpMethod, url, runtime: this.adapter?.name ?? 'node', state: {}, }; } /** * Execute route with interceptor chain */ private async executeRoute( route: ICompiledRoute, context: IRequestContext ): Promise { // Collect all request interceptors const requestInterceptors: TRequestInterceptor[] = []; const responseInterceptors: TResponseInterceptor[] = []; for (const interceptor of route.interceptors) { if (interceptor.request) { const reqs = Array.isArray(interceptor.request) ? interceptor.request : [interceptor.request]; requestInterceptors.push(...reqs); } if (interceptor.response) { const ress = Array.isArray(interceptor.response) ? interceptor.response : [interceptor.response]; responseInterceptors.push(...ress); } } // Run request interceptors let currentContext = context; for (const interceptor of requestInterceptors) { const result = await interceptor(currentContext); if (result instanceof Response) { // Short-circuit with response return result; } if (result && typeof result === 'object' && 'request' in result) { // Updated context currentContext = result as IRequestContext; } // undefined means continue with current context } // Execute handler let handlerResult = await route.handler(currentContext); // Run response interceptors (in reverse order for onion model) for (let i = responseInterceptors.length - 1; i >= 0; i--) { const interceptor = responseInterceptors[i]; const result = await interceptor(handlerResult, currentContext); if (result instanceof Response) { return result; } handlerResult = result; } // Convert result to Response return this.resultToResponse(handlerResult); } /** * Convert handler result to Response */ private resultToResponse(result: any): Response { // Already a Response if (result instanceof Response) { return result; } // Null/undefined if (result === null || result === undefined) { return new Response(null, { status: 204 }); } // String if (typeof result === 'string') { return new Response(result, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } // Object/Array - serialize as JSON return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' }, }); } /** * Handle errors */ private handleError(error: Error, request: Request): Response { // Custom error handler if (this.options.onError) { try { const result = this.options.onError(error, request); if (result instanceof Promise) { // Can't await here, return 500 console.error('Error in error handler:', error); return new Response('Internal Server Error', { status: 500 }); } return result; } catch { // Error in error handler } } // HttpError if (error instanceof HttpError) { return error.toResponse(); } // Unknown error console.error('Unhandled error:', error); return new Response( JSON.stringify({ error: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, } ); } }