cmy / server.js
Rajhuggingface4253's picture
Update server.js
c1a0ed7 verified
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}`);
});