UNPKG

lsh-framework

Version:

A powerful, extensible shell with advanced job management, database persistence, and modern CLI features

291 lines (290 loc) 8.7 kB
/** * Base API Server * Abstract base class for all API servers to eliminate duplication in: * - Express middleware setup * - Server lifecycle management * - Signal handling * - Error handling */ import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import { EventEmitter } from 'events'; import { createLogger } from './logger.js'; /** * Abstract base class for API servers * * Provides: * - Express app setup with common middleware * - Server lifecycle (start/stop) * - Signal handling (SIGTERM, SIGINT, SIGHUP) * - Error handling (uncaughtException, unhandledRejection) * - Request logging * - Structured logging * * @example * ```typescript * class MyAPIServer extends BaseAPIServer { * constructor() { * super({ port: 3000 }, 'MyAPI'); * } * * protected setupRoutes(): void { * this.app.get('/api/hello', (req, res) => { * res.json({ message: 'Hello World' }); * }); * } * } * ``` */ export class BaseAPIServer extends EventEmitter { app; server; config; logger; isShuttingDown = false; /** * Create a new API server * * @param config - Server configuration * @param loggerName - Name for the logger context */ constructor(config, loggerName) { super(); this.config = { port: 3000, corsOrigins: '*', enableHelmet: true, jsonLimit: '10mb', enableRequestLogging: true, enableSignalHandlers: true, enableErrorHandlers: true, ...config }; this.logger = createLogger(loggerName); this.app = express(); this.setupMiddleware(); if (this.config.enableErrorHandlers) { this.setupErrorHandlers(); } if (this.config.enableSignalHandlers) { this.setupSignalHandlers(); } } /** * Setup Express middleware * Can be overridden for custom middleware setup */ setupMiddleware() { // Security middleware if (this.config.enableHelmet) { this.app.use(helmet({ crossOriginEmbedderPolicy: false, })); } // CORS this.app.use(this.configureCORS()); // Body parsing this.app.use(express.json({ limit: this.config.jsonLimit })); this.app.use(express.urlencoded({ extended: true })); // Request logging if (this.config.enableRequestLogging) { this.app.use(this.requestLogger.bind(this)); } } /** * Configure CORS middleware * Can be overridden for custom CORS configuration */ configureCORS() { const origins = this.config.corsOrigins; if (origins === '*') { return cors(); } if (Array.isArray(origins)) { return cors({ origin: (origin, callback) => { if (!origin || origins.some(pattern => { const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(origin); })) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } } }); } return cors({ origin: origins }); } /** * Request logging middleware */ requestLogger(req, res, next) { this.logger.info(`${req.method} ${req.path}`); next(); } /** * Setup error handlers for uncaught exceptions and unhandled rejections */ setupErrorHandlers() { process.on('uncaughtException', (error) => { this.logger.error('Uncaught Exception', error); this.handleFatalError(error); }); process.on('unhandledRejection', (reason, promise) => { this.logger.error('Unhandled Rejection', reason instanceof Error ? reason : new Error(String(reason)), { promise: String(promise) }); }); } /** * Setup signal handlers for graceful shutdown */ setupSignalHandlers() { const signals = ['SIGTERM', 'SIGINT']; signals.forEach(signal => { process.on(signal, () => { this.logger.info(`Received ${signal}, initiating graceful shutdown`); this.gracefulShutdown(signal); }); }); process.on('SIGHUP', () => { this.logger.info('Received SIGHUP'); this.handleSIGHUP(); }); } /** * Handle SIGHUP signal - can be overridden for custom behavior (e.g., reload config) */ handleSIGHUP() { this.logger.info('SIGHUP handler not implemented, ignoring'); } /** * Handle fatal errors * @param error - The fatal error */ handleFatalError(error) { this.logger.error('Fatal error, shutting down', error); process.exit(1); } /** * Perform graceful shutdown * @param signal - The signal that triggered the shutdown */ async gracefulShutdown(signal) { if (this.isShuttingDown) { this.logger.warn('Shutdown already in progress'); return; } this.isShuttingDown = true; this.logger.info(`Graceful shutdown initiated by ${signal}`); try { // Give ongoing requests time to complete await this.stop(); this.logger.info('Server stopped successfully'); process.exit(0); } catch (error) { this.logger.error('Error during shutdown', error); process.exit(1); } } /** * Start the API server * @returns Promise that resolves when server is listening */ async start() { return new Promise((resolve, reject) => { try { // Setup routes before starting server this.setupRoutes(); this.server = this.app.listen(this.config.port, () => { this.logger.info(`Server started on port ${this.config.port}`); this.emit('started'); resolve(); }); this.server.on('error', (error) => { this.logger.error('Server error', error); this.emit('error', error); reject(error); }); } catch (error) { this.logger.error('Failed to start server', error); reject(error); } }); } /** * Stop the API server * @param timeout - Maximum time to wait for connections to close (ms) * @returns Promise that resolves when server is stopped */ async stop(timeout = 5000) { return new Promise((resolve, reject) => { if (!this.server) { resolve(); return; } this.logger.info('Stopping server...'); // Set timeout for force shutdown const forceShutdownTimer = setTimeout(() => { this.logger.warn('Force closing server after timeout'); this.server?.close(); resolve(); }, timeout); this.server?.close((error) => { clearTimeout(forceShutdownTimer); if (error) { this.logger.error('Error stopping server', error); reject(error); } else { this.logger.info('Server stopped'); this.emit('stopped'); resolve(); } }); // Allow subclasses to cleanup this.onStop(); }); } /** * Hook called when server is stopping * Override this to cleanup resources, close connections, etc. */ onStop() { // Override in subclasses if needed } /** * Get the Express application * @returns The Express app instance */ getApp() { return this.app; } /** * Get the HTTP server * @returns The HTTP server instance */ getServer() { return this.server; } /** * Check if server is running * @returns True if server is running */ isRunning() { return !!this.server?.listening; } /** * Get server configuration * @returns The server configuration */ getConfig() { return { ...this.config }; } } export default BaseAPIServer;