File size: 9,359 Bytes
e8a6607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe2dc13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8a6607
 
 
9e90bda
 
e8a6607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import express, { Request, Response, NextFunction, Express } from 'express'; //use express module to create a server obj
import IORedis from 'ioredis';
// import { Queue } from 'bullmq';
import { rateLimit, ipKeyGenerator } from 'express-rate-limit';

//For flutter app .dev purpose
import http, { Server as HttpServer } from "http";

import https, { Server as HttpsServer } from 'https';
import cors from 'cors';
import helmet from 'helmet';
import path from 'path';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';

import logger from './logger';

import { appRouter } from './api/routes/index'; // import a centralized routes group
import { initializeSocket } from './api/socket/socket';
import { RedisClientInfra } from './redis';
import { QueueInfra } from './queues';
// import { GptChatWorker } from './workers';
import { GptChatQueueService } from './queues/gpt-chat-queue-services';
import { 
    initPubSubRedis,
    closePubSubRedis
} from './redis';

import * as fs from 'fs';

// Load env file based on NODE_ENV
const envFile: string = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';

dotenv.config({ path: envFile });

const certPath = process.env.SSL_CERT || './ssl/certificate.crt';
const keyPath = process.env.SSL_KEY || './ssl/certificate.key';

const allowedOrigins = [process.env.FRONTEND_URL as string];

const host: string | undefined = process.env.HOST; 
const port: number = Number(process.env.PORT || 7860);

const app: Express = express(); // create a server

// Global rate limiting
const globalLimiter = rateLimit({
    windowMs: 1 * 1000, // 1 second
    max: 10000, // 10,000 requests per second globally
    handler: (req: Request, res: Response) => {
        res.status(429).json({ error: 'System overloaded' });
    }
});

// Express rate limiter configuration
const ipLimiter = rateLimit({
    windowMs: 10 * 1000, // 10 Sec window
    max: 60, // Maximum 60 requests per 10 Secs per user
    keyGenerator: (req: Request) =>  ipKeyGenerator(req.ip ?? 'unknown-ip'),
    message: { message: 'Too many requests from this IP, please try again after some time' },
    headers: true,
});

const corsOptions = {
    origin: function (origin: string | undefined, callback: Function) {
        // Allow requests with no origin (like mobile apps or curl requests)
        console.log("CORS callback origin:", origin);
        console.log("Allowed:", allowedOrigins);

        if (!origin) {
            console.log("No origin");
            return callback(null, true);
        }

        if (allowedOrigins.includes(origin)) {
            console.log("Origin allowed");
            callback(null, origin);
        } else {
            console.log("Cors blocked");
            return callback(new Error("Not allowed by CORS"));
        }
      
    },
    credentials: true,
    exposedHeaders: ['Authorization'],
    allowedHeaders: [
        'Content-Type',
        'Authorization',
        'X-Requested-With',
        'x-socket-id',
        'x-anonuser-id',
        // 'Cookie'
    ],
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
};

if (process.env.NODE_ENV !== 'production') {
    app.use((req, res, next) => {
        console.log("Method:", req.method);
        console.log("Origin:", req.headers.origin);
        next();
    });
}

const publicPath = path.join(__dirname, "../public");
console.log("Public path:", publicPath);
console.log("Exists:", fs.existsSync(publicPath));
console.log("Index exists:", fs.existsSync(path.join(publicPath, "index.html")));

app.use(express.static(publicPath));


// Test route
app.get("/health", (req, res) => {
    res.status(200).send("Backend is alive");
});


app.get("/*splat", (req, res) => {
    res.sendFile(path.join(__dirname, "../public", "index.html"));
});

app.use(cors(corsOptions));
app.options(/.*/, cors(corsOptions));

app.set("trust proxy", 1);

app.use(globalLimiter);
app.use(ipLimiter);
app.use(cookieParser());

app.use(helmet());
app.use(express.json()); //middleware to parse JSON

let server: HttpsServer | HttpServer;
if (process.env.NODE_ENV === 'production') {
    // HTTP for production
    server = http.createServer(app); //This binding is required to handle some low-level server event handling
} else {
    // HTTPS for development
    const cert: Buffer = fs.readFileSync(certPath);
    const key: Buffer = fs.readFileSync(keyPath);
    server = https.createServer({ key, cert }, app); //This binding is required to handle some low-level server event handling
}

let aiQueueRedisInfra: RedisClientInfra;
let aiQueueInfra: QueueInfra;
// let gptChatWorker: GptChatWorker;
let gptChatQueueService: GptChatQueueService;

async function initQueueWorkers() {
    // Setup redis connection
    aiQueueRedisInfra = new RedisClientInfra('gpt-chat-redis');
    aiQueueRedisInfra.init();
    aiQueueRedisInfra.testRedisConnection();

    // Setup and start queue
    aiQueueInfra = new QueueInfra(aiQueueRedisInfra.redis, 'gpt-chat');
    aiQueueInfra.init();
    aiQueueInfra.testQueueRedisConnection();

    // Setup and start worker
    // gptChatWorker = new GptChatWorker(aiQueueRedisInfra.redis, 'gpt-chat');
    // gptChatWorker.init();

    gptChatQueueService = new GptChatQueueService(aiQueueRedisInfra.redis, aiQueueInfra.queue, 'gpt-chat');
};


async function bootStrap() {
    
    try {
        logger.info('πŸš€ Server starting...');

        // Redis socket notification
        await initPubSubRedis('api-pub-redis', 'api-sub-redis');

        await initializeSocket(server); //Initialize socket.io with the server

        // Start API Queue and workers
        await initQueueWorkers();

        if (gptChatQueueService) {
            app.use(appRouter(gptChatQueueService));
        } else {
            logger.error('Some services are still not ready, restart the server');
        }
        server.listen(port , () => {
            logger.info(`Server running on ${host}:${port}`);
        });
    } catch (err: any) {
        logger.error('Server startup failed', err);
        process.exit(1);
    }
};

bootStrap();

//Confirm that the server is listening after successfull start
server.on('listening', () => {
    logger.info('Server successfully started');
    logger.info(`Server is listening on localhost:${port}`);
});

//Handle error when server starts
server.on('error', (err: NodeJS.ErrnoException) => {
    logger.error({err}, `Server error: , ${err.message}`);
    if (err.code === 'EADDRINUSE') { //In case any error happened by the selected port number
        logger.warn(`Port ${port} is already in use`);
        //////////////////////////////////////
        //code to handle to manually select a port and manual restart of server using electron
        //////////////////////////////////////
    }
});

server.on('close', () => {
   logger.info('All API connections closed');
});


const closeServer = () => {
  return new Promise<void>((resolve) => {
    const timeout = setTimeout(() => {
      logger.warn('Force closing server (timeout)');
      resolve();
    }, 10000);

    server.close(() => {
      clearTimeout(timeout);   // ⭐ stop timeout
      logger.info('HTTP server closed');
      resolve();
    });
  });
};


//Server and database shut down function 
const serverShutdown = async (): Promise<void> => {
    
    try {
        logger.info('Server shutting down started...');
        await closeServer();
        // Stop all Workers, Queueu events, Queueu and Redis for queue
        if (aiQueueInfra) {
            // await gptChatWorker.closeGptChatWorker(); // First close all workers related to the queue
            await gptChatQueueService.closeQueueEvents(); // Then close all queue events related to queue
            await aiQueueInfra.closeQueue(); // Then close all queue 
            await aiQueueInfra.closeQueueRedis(); // At last close the redis connection which makes the queue possible            
        }
       
    } catch (err: any) {
        logger.error({err}, '❌ Error stopping schedulers');
    }
};

const withTimeout = (promise: Promise<any>, timeoutMs: number, operation: string): Promise<any> => {
    return Promise.race([
        promise,
        new Promise((_, reject) => 
            setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms for ${operation}`)), timeoutMs)
        )
    ]);
};

let isGracefullShuttingDown = false;

const gracefulShutdown = async (signal?: string): Promise<void> => {
    if (isGracefullShuttingDown) {
        logger.warn(`Shutdown already running. Ignoring ${signal}`);
        return;
    }

    isGracefullShuttingDown = true;
    logger.info(`Received ${signal}. Starting graceful shutdown...`);

    try {
        logger.info('🚨 Starting graceful shutdown process...');
        
        logger.info('πŸ›‘ Server shutdown called...');
        await withTimeout(serverShutdown(), 60000, 'serverShutdown');
        logger.info('βœ… Server shutdown completed');

        logger.info('πŸŽ‰ All cleanup completed successfully');
        process.exit(0);
        
    } catch (err: any) {
        logger.error({err}, '❌ Shutdown error');
        process.exit(1);
    }
};

process.removeAllListeners('SIGTERM');
process.removeAllListeners('SIGINT');

// Signal listeners
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);