import * as http from "http"; import * as fs from "fs"; import * as path from "path"; import * as net from "net"; import { promisify } from "util"; import { parentPort, isMainThread, workerData } from "worker_threads"; // Promisify file system operations const statAsync = promisify(fs.stat); const readFileAsync = promisify(fs.readFile); // Configuration interface with types // export interface ServerConfig { // port: number; // rootDir: string; // cacheMaxAge: number; // maxPortRetries?: number; // } // Default configuration const DEFAULT_CONFIG = { port: 31_111, rootDir: ".", cacheMaxAge: 86400, // 1 day in seconds maxPortRetries: 5, }; // MIME types mapping const MIME_TYPES = { ".html": "text/html", ".css": "text/css", ".js": "application/javascript", ".ts": "application/typescript", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".txt": "text/plain", ".pdf": "application/pdf", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".eot": "application/vnd.ms-fontobject", ".otf": "font/otf", ".mp4": "video/mp4", ".webm": "video/webm", ".mp3": "audio/mpeg", ".wav": "audio/wav", ".webp": "image/webp", }; /** * Checks if a port is available * @param port Port number to check * @returns Promise that resolves to true if port is available, false otherwise */ const isPortAvailable = (port) => { return new Promise((resolve) => { const tester = net .createServer() .once("error", () => { // Port is in use resolve(false); }) .once("listening", () => { // Port is available tester.close(() => resolve(true)); }) .listen(port); }); }; /** * Finds the first available port starting from the specified port * @param startPort Starting port number * @param maxRetries Maximum number of ports to try * @returns Promise resolving to the first available port, or undefined if none found */ const findAvailablePort = async (startPort, maxRetries) => { for (let i = 0; i < maxRetries; i++) { const port = startPort + i; // eslint-disable-next-line no-await-in-loop if (await isPortAvailable(port)) { return port; } } return undefined; }; /** * Handles HTTP requests, serving static files with caching */ const handleRequest = (config) => async (req, res) => { try { if (!req.url) { res.statusCode = 400; res.end("Bad Request"); return; } // Only allow GET requests if (req.method !== "GET") { res.statusCode = 405; res.setHeader("Allow", "GET"); res.end("Method Not Allowed"); return; } // Parse URL and sanitize path const parsedUrl = new URL(req.url, `http://${req.headers.host}`); let filePath = path.normalize( path.join(config.rootDir, parsedUrl.pathname) ); console.log(`filePath: ${filePath}`, "request", req.url); // Handle root path or directory paths, serve index.html if (filePath === path.normalize(config.rootDir) || filePath.endsWith("/")) { filePath = path.join(filePath, "index.html"); } // Check if file exists and get its stats let stats; try { console.log(`filePath: ${filePath}`, "stats"); stats = await statAsync(filePath); } catch (error) { // File not found res.statusCode = 404; res.end("Not Found"); return; } // Handle directory requests if (stats.isDirectory()) { try { // Redirect to directory with trailing slash if needed if (!req.url.endsWith("/")) { res.statusCode = 301; res.setHeader("Location", `${req.url}/`); res.end(); return; } // Try to serve index.html from directory filePath = path.join(filePath, "index.html"); stats = await statAsync(filePath); } catch (error) { res.statusCode = 404; res.end("Not Found"); return; } } // Get file extension and MIME type const ext = path.extname(filePath).toLowerCase(); const contentType = MIME_TYPES[ext] || "application/octet-stream"; // Handle caching - check if file has been modified const ifModifiedSince = req.headers["if-modified-since"]; if (ifModifiedSince) { const modifiedSinceDate = new Date(ifModifiedSince); // Check if the file hasn't been modified since the client's last request if (modifiedSinceDate && stats.mtime <= modifiedSinceDate) { res.statusCode = 304; // Not Modified res.end(); return; } } // Set cache headers const lastModified = stats.mtime.toUTCString(); res.setHeader("Last-Modified", lastModified); res.setHeader("Cache-Control", `public, max-age=${config.cacheMaxAge}`); res.setHeader("Content-Type", contentType); res.setHeader("Content-Length", stats.size); // Read and send file const fileContent = await readFileAsync(filePath); res.end(fileContent); } catch (error) { console.error(`[Worker ${process.pid}] Server error:`, error); // Only attempt to send error response if headers haven't been sent if (!res.headersSent) { res.statusCode = 500; res.end("Internal Server Error"); } } }; // Create and start the server // Modified to return the port and not handle process exit export const startServer = async (userConfig = {}) => { // Merge default config with user provided config const config = { ...DEFAULT_CONFIG, ...userConfig }; // Try to find an available port const maxRetries = config.maxPortRetries || 5; const availablePort = await findAvailablePort(config.port, maxRetries); if (!availablePort) { throw new Error( `Could not find an available port after trying ${maxRetries} ports starting from ${config.port}` ); } config.port = availablePort; // Update config with the actual port // Create server with the handler const server = http.createServer(handleRequest(config)); // Start the server await new Promise((resolve, reject) => { server.on("error", (err) => { console.error(`[Worker ${process.pid}] Server error:`, err); reject(err); // Reject promise on server error during startup }); server.listen(config.port, () => { console.log( `[Worker ${process.pid}] 🚀 Static file server running at http://localhost:${config.port}/` ); console.log( `[Worker ${process.pid}] 📁 Serving files from: ${path.resolve( config.rootDir )}` ); console.log( `[Worker ${process.pid}] 🔄 Cache max age: ${config.cacheMaxAge} seconds` ); resolve(); }); }); // Don't handle SIGINT here, let the main thread manage the worker lifecycle return { server, port: config.port }; }; // --- Worker Logic --- if (!isMainThread && parentPort) { const run = async () => { try { if (!workerData || !workerData.rootDir) { throw new Error("rootDir must be provided in workerData"); } const config = { ...DEFAULT_CONFIG, ...workerData }; const { port } = await startServer(config); parentPort?.postMessage({ status: "ready", port }); } catch (error) { console.error(`[Worker ${process.pid}] Failed to start server:`, error); parentPort?.postMessage({ status: "error", message: error.message }); } }; run().catch((err) => { // Catch unhandled promise rejections during startup console.error( `[Worker ${process.pid}] Unhandled error during startup:`, err ); parentPort?.postMessage({ status: "error", message: err.message || "Unknown startup error", }); }); // Keep the worker alive // The server itself will keep the event loop running } else if (!isMainThread) { // Should not happen if used correctly, but good to handle console.error("Running as worker but parentPort is not available."); process.exit(1); } // If it IS the main thread, exporting startServer allows for potential direct use or testing