Spaces:
Running
Running
| 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}`); | |
| }); |