const express = require('express'); const cors = require('cors'); const os = require('os'); const imageProcessor = require('./image-processing-logic'); /** * PRODUCTION OPTIMIZATION: UV_THREADPOOL_SIZE * Node.js libuv defaults to 4 threads. For a CPU-intensive image processing server, * we scale this to match all available CPU cores so every core can process a request. * This MUST be set before any async I/O operations occur. * * With PM2 cluster mode (-i 4), each worker gets its own thread pool. * This ensures maximum throughput across all workers. */ const cpuCount = os.cpus().length; process.env.UV_THREADPOOL_SIZE = String(Math.max(4, cpuCount)); const app = express(); const PORT = process.env.PORT || 7860; // ─── Concurrency Queue (Request Throttling) ───────────────────────────────── // Instead of rejecting users when the server is busy, we queue excess requests. // This ensures no user gets an error — they just wait a bit longer. const MAX_CONCURRENT = Math.max(2, Math.floor(cpuCount / 2)); // e.g., 2 on 4-core, 4 on 8-core let activeRequests = 0; const waitingQueue = []; function acquireSlot() { return new Promise((resolve) => { if (activeRequests < MAX_CONCURRENT) { activeRequests++; resolve(); } else { waitingQueue.push(resolve); } }); } function releaseSlot() { if (waitingQueue.length > 0) { const next = waitingQueue.shift(); next(); // Don't decrement — the next request takes the slot } else { activeRequests--; } } // ───────────────────────────────────────────────────────────────────────────── // Use standard middleware. app.use(cors()); app.use(express.json({ limit: '50mb' })); // Health check endpoint app.get('/ping', (req, res) => { res.status(200).json({ status: 'ok', activeRequests, queuedRequests: waitingQueue.length, maxConcurrent: MAX_CONCURRENT, cpuCores: cpuCount, threadPoolSize: process.env.UV_THREADPOOL_SIZE }); }); /** * CMYK TIFF processing endpoint (Stream-based with Concurrency Queue). * Converts any supported image (PNG, JPEG, WebP, AVIF, TIFF) to professional CMYK TIFF format. * * Requests beyond MAX_CONCURRENT are queued, not rejected. */ app.post('/api/process-cmyk', async (req, res) => { // Acquire a concurrency slot (will wait if server is busy) await acquireSlot(); try { // Check if request has body if (!req.readable) { releaseSlot(); return res.status(400).json({ error: 'No image data provided', solution: 'Send image as raw binary in request body with Content-Type header' }); } // Validate Content-Type try { imageProcessor.validateContentType(req.get('Content-Type')); } catch (contentTypeError) { releaseSlot(); return res.status(400).json({ error: 'Invalid Content-Type', details: contentTypeError.message, supportedTypes: ['image/jpeg', 'image/png', 'image/tiff', 'image/webp', 'image/avif'] }); } // Extract and validate quality parameter const { quality = 95 } = req.query; let validatedOptions; try { validatedOptions = imageProcessor.validateOptions({ quality }); } catch (validationError) { releaseSlot(); return res.status(400).json({ error: 'Invalid parameters', details: validationError.message }); } // Set response headers res.setHeader('Content-Type', 'image/tiff'); res.setHeader('Content-Disposition', 'attachment; filename=professional_print.tiff'); res.setHeader('X-Image-Processor', 'CMYK-TIFF Professional'); res.setHeader('X-Processing-Quality', validatedOptions.quality); // Process the image stream to CMYK TIFF const processedStream = await imageProcessor.processImage(req, validatedOptions); // Handle stream errors during processing processedStream.on('error', (error) => { console.error('Stream processing error:', error); releaseSlot(); if (!res.headersSent) { res.status(500).json({ error: 'Image processing failed', details: 'Stream processing error occurred' }); } }); // Release the concurrency slot when the stream finishes or errors processedStream.on('end', () => { releaseSlot(); }); // Handle client disconnect (abort) to free the slot req.on('close', () => { if (!res.writableFinished) { releaseSlot(); } }); // Pipe to response processedStream.pipe(res); } catch (error) { releaseSlot(); console.error('CMYK processing endpoint error:', error); if (!res.headersSent) { const statusCode = error.message.includes('Invalid') || error.message.includes('Unsupported') ? 400 : 500; res.status(statusCode).json({ error: 'CMYK conversion failed', details: error.message, timestamp: new Date().toISOString() }); } } }); // 404 handler app.use('*', (req, res) => { res.status(404).send('Not Found'); }); // Global error handler app.use((error, req, res, next) => { console.error('Global error handler:', error); if (res.headersSent) { return next(error); } res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'production' ? 'Something went wrong' : error.message, timestamp: new Date().toISOString() }); }); // Process termination handlers process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); }); app.listen(PORT, () => { console.log(`CMYK Server | Port: ${PORT} | CPUs: ${cpuCount} | ThreadPool: ${process.env.UV_THREADPOOL_SIZE} | MaxConcurrent: ${MAX_CONCURRENT}`); });