@@ -2,13 +2,14 @@ import express from 'express';
22import webhookRouter from './webhooks/webhook.routes.js' ;
33import { calloraEvents } from './events/event.emitter.js' ;
44import helmet from 'helmet' ;
5- import { db , initializeDb , schema } from './db/index.js' ;
5+ import { db , initializeDb , schema , closeDb } from './db/index.js' ;
66import { eq , desc , and , type SQL } from 'drizzle-orm' ;
77import { requireAuth , type AuthenticatedLocals } from './middleware/requireAuth.js' ;
88import { errorHandler } from './middleware/errorHandler.js' ;
99import { BadRequestError , NotFoundError , UnauthorizedError , ForbiddenError } from './errors/index.js' ;
1010import * as developerRepository from './repositories/developerRepository.js' ;
1111import type { Response } from 'express' ;
12+ import type { Socket } from 'net' ;
1213
1314import { createDeveloperRouter } from './routes/developerRoutes.js' ;
1415import { createGatewayRouter } from './routes/gatewayRoutes.js' ;
@@ -100,9 +101,66 @@ if (isDirectExecution) {
100101 async function startServer ( ) {
101102 try {
102103 await initializeDb ( ) ;
103- app . listen ( PORT , ( ) => {
104+
105+ const server = app . listen ( PORT , ( ) => {
104106 console . log ( `Callora backend listening on http://localhost:${ PORT } ` ) ;
105107 } ) ;
108+
109+ // Track active connections so we can wait for them to finish
110+ const activeConnections = new Set < Socket > ( ) ;
111+
112+ server . on ( 'connection' , ( socket : Socket ) => {
113+ activeConnections . add ( socket ) ;
114+ socket . once ( 'close' , ( ) => activeConnections . delete ( socket ) ) ;
115+ } ) ;
116+
117+ async function gracefulShutdown ( signal : string ) {
118+ console . log ( `\n[shutdown] Received ${ signal } . Starting graceful shutdown...` ) ;
119+
120+ // 1. Stop accepting new requests
121+ server . close ( ( ) => {
122+ console . log ( '[shutdown] HTTP server closed. No new requests accepted.' ) ;
123+ } ) ;
124+
125+ // 2. Wait for in-flight requests to finish (max 30s)
126+ const TIMEOUT_MS = 30_000 ;
127+ const deadline = setTimeout ( ( ) => {
128+ console . warn ( '[shutdown] Timeout reached. Forcing exit.' ) ;
129+ process . exit ( 1 ) ;
130+ } , TIMEOUT_MS ) ;
131+ deadline . unref ( ) ;
132+
133+ // 3. Wait until all active connections are gone
134+ await new Promise < void > ( ( resolve ) => {
135+ if ( activeConnections . size === 0 ) return resolve ( ) ;
136+ console . log ( `[shutdown] Waiting for ${ activeConnections . size } in-flight connection(s)...` ) ;
137+ const interval = setInterval ( ( ) => {
138+ if ( activeConnections . size === 0 ) {
139+ clearInterval ( interval ) ;
140+ resolve ( ) ;
141+ }
142+ } , 200 ) ;
143+ } ) ;
144+
145+ // 4. Close the database
146+ console . log ( '[shutdown] Closing database...' ) ;
147+ try {
148+ closeDb ( ) ;
149+ console . log ( '[shutdown] Database closed.' ) ;
150+ } catch ( err ) {
151+ console . error ( '[shutdown] Error closing database:' , err ) ;
152+ }
153+
154+ // 5. Exit cleanly
155+ console . log ( '[shutdown] Shutdown complete. Exiting.' ) ;
156+ clearTimeout ( deadline ) ;
157+ process . exit ( 0 ) ;
158+ }
159+
160+ // Register shutdown signals
161+ process . on ( 'SIGTERM' , ( ) => gracefulShutdown ( 'SIGTERM' ) ) ;
162+ process . on ( 'SIGINT' , ( ) => gracefulShutdown ( 'SIGINT' ) ) ;
163+
106164 } catch ( error ) {
107165 console . error ( 'Failed to start server:' , error ) ;
108166 process . exit ( 1 ) ;
@@ -112,4 +170,4 @@ if (isDirectExecution) {
112170 startServer ( ) ;
113171}
114172
115- export default app ;
173+ export default app ;
0 commit comments