Compare commits

...

6 Commits

Author SHA1 Message Date
sriramveeraghanta
cdb488f864 fix: error handling 2025-07-30 17:10:08 +05:30
sriramveeraghanta
6a54a07cee chore: cleanup 2025-07-30 15:14:39 +05:30
sriramveeraghanta
78169fec66 fix: redis and hocuspocus as singletons 2025-07-30 14:56:21 +05:30
sriramveeraghanta
b2ed07a778 Merge branch 'preview' of github.com:makeplane/plane into fix-live-sync-changes 2025-07-30 13:32:28 +05:30
sriram veeraghanta
1f7ef1865c Merge branch 'preview' of github.com:makeplane/plane into fix-live-sync-changes 2025-07-26 15:29:53 +05:30
sriramveeraghanta
18b206952b chore: moving file strucutre 2025-07-24 16:07:03 +05:30
15 changed files with 535 additions and 268 deletions

View File

@@ -51,4 +51,4 @@ ENV TURBO_TELEMETRY_DISABLED=1
EXPOSE 3000
CMD ["node", "live/server.js"]
CMD ["node", "live/start.js"]

View File

@@ -0,0 +1,31 @@
import type { Request } from "express";
import type { WebSocket as WS } from "ws";
import type { Hocuspocus } from "@hocuspocus/server";
import { logger } from "@plane/logger";
import { Controller, WebSocket } from "@plane/decorators";
@Controller("/collaboration")
export class CollaborationController {
private metrics = {
errors: 0,
};
constructor(private readonly hocusPocusServer: Hocuspocus) {}
@WebSocket("/")
handleConnection(ws: WS, req: Request) {
try {
// Initialize the connection with Hocuspocus
this.hocusPocusServer.handleConnection(ws, req);
// Set up error handling for the connection
ws.on("error", (error) => {
logger.error("WebSocket connection error:", error);
ws.close();
});
} catch (error) {
logger.error("WebSocket connection error:", error);
ws.close();
}
}
}

View File

@@ -0,0 +1,14 @@
import type { Request, Response } from "express";
import { Controller, Get } from "@plane/decorators";
@Controller("/health")
export class HealthController {
@Get("/")
async healthCheck(_req: Request, res: Response) {
res.status(200).json({
status: "OK",
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || "1.0.0",
});
}
}

View File

View File

@@ -1,120 +0,0 @@
// Third-party libraries
import { Redis } from "ioredis";
// Hocuspocus extensions and core
import { Database } from "@hocuspocus/extension-database";
import { Extension } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";
import { Redis as HocusPocusRedis } from "@hocuspocus/extension-redis";
// core helpers and utilities
import { manualLogger } from "@/core/helpers/logger.js";
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
// core libraries
import { fetchPageDescriptionBinary, updatePageDescription } from "@/core/lib/page.js";
// plane live libraries
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
import { updateDocument } from "@/plane-live/lib/update-document.js";
// types
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
export const getExtensions: () => Promise<Extension[]> = async () => {
const extensions: Extension[] = [
new Logger({
onChange: false,
log: (message) => {
manualLogger.info(message);
},
}),
new Database({
fetch: async ({ context, documentName: pageId, requestParameters }) => {
const cookie = (context as HocusPocusServerContext).cookie;
// query params
const params = requestParameters;
const documentType = params.get("documentType")?.toString() as TDocumentTypes | undefined;
// TODO: Fix this lint error.
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
try {
let fetchedData = null;
if (documentType === "project_page") {
fetchedData = await fetchPageDescriptionBinary(params, pageId, cookie);
} else {
fetchedData = await fetchDocument({
cookie,
documentType,
pageId,
params,
});
}
resolve(fetchedData);
} catch (error) {
manualLogger.error("Error in fetching document", error);
}
});
},
store: async ({ context, state, documentName: pageId, requestParameters }) => {
const cookie = (context as HocusPocusServerContext).cookie;
// query params
const params = requestParameters;
const documentType = params.get("documentType")?.toString() as TDocumentTypes | undefined;
// TODO: Fix this lint error.
// eslint-disable-next-line no-async-promise-executor
return new Promise(async () => {
try {
if (documentType === "project_page") {
await updatePageDescription(params, pageId, state, cookie);
} else {
await updateDocument({
cookie,
documentType,
pageId,
params,
updatedDescription: state,
});
}
} catch (error) {
manualLogger.error("Error in updating document:", error);
}
});
},
}),
];
const redisUrl = getRedisUrl();
if (redisUrl) {
try {
const redisClient = new Redis(redisUrl);
await new Promise<void>((resolve, reject) => {
redisClient.on("error", (error: any) => {
if (error?.code === "ENOTFOUND" || error.message.includes("WRONGPASS") || error.message.includes("NOAUTH")) {
redisClient.disconnect();
}
manualLogger.warn(
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data between multiple plane live servers)`,
error
);
reject(error);
});
redisClient.on("ready", () => {
extensions.push(new HocusPocusRedis({ redis: redisClient }));
manualLogger.info("Redis Client connected ✅");
resolve();
});
});
} catch (error) {
manualLogger.warn(
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data between multiple plane live servers)`,
error
);
}
} else {
manualLogger.warn(
"Redis URL is not set, continuing without Redis (you won't be able to sync data between multiple plane live servers)"
);
}
return extensions;
};

View File

@@ -1,18 +0,0 @@
import { ErrorRequestHandler } from "express";
import { manualLogger } from "@/core/helpers/logger.js";
export const errorHandler: ErrorRequestHandler = (err, _req, res) => {
// Log the error
manualLogger.error(err);
// Set the response status
res.status(err.status || 500);
// Send the response
res.json({
error: {
message: process.env.NODE_ENV === "production" ? "An unexpected error occurred" : err.message,
...(process.env.NODE_ENV !== "production" && { stack: err.stack }),
},
});
};

View File

@@ -1,69 +0,0 @@
import { Server } from "@hocuspocus/server";
import { v4 as uuidv4 } from "uuid";
// lib
import { handleAuthentication } from "@/core/lib/authentication.js";
// extensions
import { getExtensions } from "@/core/extensions/index.js";
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
// editor types
import { TUserDetails } from "@plane/editor";
// types
import { type HocusPocusServerContext } from "@/core/types/common.js";
export const getHocusPocusServer = async () => {
const extensions = await getExtensions();
const serverName = process.env.HOSTNAME || uuidv4();
return Server.configure({
name: serverName,
onAuthenticate: async ({
requestHeaders,
context,
// user id used as token for authentication
token,
}) => {
let cookie: string | undefined = undefined;
let userId: string | undefined = undefined;
// Extract cookie (fallback to request headers) and userId from token (for scenarios where
// the cookies are not passed in the request headers)
try {
const parsedToken = JSON.parse(token) as TUserDetails;
userId = parsedToken.id;
cookie = parsedToken.cookie;
} catch (error) {
// If token parsing fails, fallback to request headers
console.error("Token parsing failed, using request headers:", error);
} finally {
// If cookie is still not found, fallback to request headers
if (!cookie) {
cookie = requestHeaders.cookie?.toString();
}
}
if (!cookie || !userId) {
throw new Error("Credentials not provided");
}
// set cookie in context, so it can be used throughout the ws connection
(context as HocusPocusServerContext).cookie = cookie;
try {
await handleAuthentication({
cookie,
userId,
});
} catch (_error) {
throw Error("Authentication unsuccessful!");
}
},
async onStateless({ payload, document }) {
// broadcast the client event (derived from the server event) to all the clients so that they can update their state
const response = DocumentCollaborativeEvents[payload as TDocumentEventsServer].client;
if (response) {
document.broadcastStateless(response);
}
},
extensions,
debounce: 10000,
});
};

View File

@@ -1,7 +1,7 @@
// services
import { UserService } from "@/core/services/user.service.js";
import { UserService } from "@/core/services/user.service";
// core helpers
import { manualLogger } from "@/core/helpers/logger.js";
import { logger } from "@plane/logger";
const userService = new UserService();
@@ -17,7 +17,7 @@ export const handleAuthentication = async (props: Props) => {
try {
response = await userService.currentUser(cookie);
} catch (error) {
manualLogger.error("Failed to fetch current user:", error);
logger.error("Failed to fetch current user:", error);
throw error;
}
if (response.id !== userId) {

View File

@@ -1,8 +1,9 @@
import { logger } from "@plane/logger";
// helpers
import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "@/core/helpers/page.js";
// services
import { PageService } from "@/core/services/page.service.js";
import { manualLogger } from "../helpers/logger.js";
const pageService = new PageService();
export const updatePageDescription = async (
@@ -29,7 +30,7 @@ export const updatePageDescription = async (
await pageService.updateDescription(workspaceSlug, projectId, pageId, payload, cookie);
} catch (error) {
manualLogger.error("Update error:", error);
logger.error("Update error:", error);
throw error;
}
};
@@ -47,7 +48,7 @@ const fetchDescriptionHTMLAndTransform = async (
const { contentBinary } = getBinaryDataFromHTMLString(pageDetails.description_html ?? "<p></p>");
return contentBinary;
} catch (error) {
manualLogger.error("Error while transforming from HTML to Uint8Array", error);
logger.error("Error while transforming from HTML to Uint8Array", error);
throw error;
}
};
@@ -74,7 +75,7 @@ export const fetchPageDescriptionBinary = async (
return binaryData;
} catch (error) {
manualLogger.error("Fetch error:", error);
logger.error("Fetch error:", error);
throw error;
}
};

View File

@@ -1,15 +0,0 @@
export function getRedisUrl() {
const redisUrl = process.env.REDIS_URL?.trim();
const redisHost = process.env.REDIS_HOST?.trim();
const redisPort = process.env.REDIS_PORT?.trim();
if (redisUrl) {
return redisUrl;
}
if (redisHost && redisPort && !Number.isNaN(Number(redisPort))) {
return `redis://${redisHost}:${redisPort}`;
}
return "";
}

200
apps/live/src/hocuspocus.ts Normal file
View File

@@ -0,0 +1,200 @@
import { Server, Hocuspocus } from "@hocuspocus/server";
import { v4 as uuidv4 } from "uuid";
import { Logger } from "@hocuspocus/extension-logger";
import { Database } from "@hocuspocus/extension-database";
import { Redis } from "@hocuspocus/extension-redis";
import { logger } from "@plane/logger";
// lib
import { handleAuthentication } from "@/core/lib/authentication";
// extensions
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
import { fetchPageDescriptionBinary, updatePageDescription } from "@/core/lib/page";
// plane live libraries
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
import { updateDocument } from "@/plane-live/lib/update-document.js";
// editor types
import { TUserDetails } from "@plane/editor";
// types
import type { HocusPocusServerContext, TDocumentTypes } from "@/core/types/common";
// redis
import { redisManager } from "@/redis";
export class HocusPocusServerManager {
private static instance: HocusPocusServerManager | null = null;
private server: Hocuspocus | null = null;
private isInitialized: boolean = false;
// server options
private serverName = process.env.HOSTNAME || uuidv4();
private constructor() {
// Private constructor to prevent direct instantiation
}
/**
* Get the singleton instance of HocusPocusServerManager
*/
public static getInstance(): HocusPocusServerManager {
if (!HocusPocusServerManager.instance) {
HocusPocusServerManager.instance = new HocusPocusServerManager();
}
return HocusPocusServerManager.instance;
}
/**
* Authenticate the user
* @param requestHeaders - The request headers
* @param context - The context
* @param token - The token
* @returns The authenticated user
*/
private onAuthenticate = async ({ requestHeaders, context, token }: any) => {
let cookie: string | undefined = undefined;
let userId: string | undefined = undefined;
// Extract cookie (fallback to request headers) and userId from token (for scenarios where
// the cookies are not passed in the request headers)
try {
const parsedToken = JSON.parse(token) as TUserDetails;
userId = parsedToken.id;
cookie = parsedToken.cookie;
} catch (error) {
// If token parsing fails, fallback to request headers
console.error("Token parsing failed, using request headers:", error);
} finally {
// If cookie is still not found, fallback to request headers
if (!cookie) {
cookie = requestHeaders.cookie?.toString();
}
}
if (!cookie || !userId) {
throw new Error("Credentials not provided");
}
// set cookie in context, so it can be used throughout the ws connection
(context as HocusPocusServerContext).cookie = cookie;
try {
await handleAuthentication({
cookie,
userId,
});
} catch (_error) {
throw Error("Authentication unsuccessful!");
}
};
private onStateless = async ({ payload, document }: any) => {
// broadcast the client event (derived from the server event) to all the clients so that they can update their state
const response = DocumentCollaborativeEvents[payload as TDocumentEventsServer].client;
if (response) {
document.broadcastStateless(response);
}
};
private onDatabaseFetch = async ({ context, documentName: pageId, requestParameters }: any) => {
const cookie = (context as HocusPocusServerContext).cookie;
// query params
const params = requestParameters;
const documentType = params.get("documentType")?.toString() as TDocumentTypes | undefined;
return new Promise(async (resolve) => {
try {
let fetchedData = null;
if (documentType === "project_page") {
fetchedData = await fetchPageDescriptionBinary(params, pageId, cookie);
} else {
fetchedData = await fetchDocument({
cookie,
documentType,
pageId,
params,
});
}
resolve(fetchedData);
} catch (error) {
logger.error("Error in fetching document", error);
}
});
};
private onDatabaseStore = async ({ context, state, documentName: pageId, requestParameters }: any) => {
const cookie = (context as HocusPocusServerContext).cookie;
// query params
const params = requestParameters;
const documentType = params.get("documentType")?.toString() as TDocumentTypes | undefined;
// TODO: Fix this lint error.
// eslint-disable-next-line no-async-promise-executor
return new Promise(async () => {
try {
if (documentType === "project_page") {
await updatePageDescription(params, pageId, state, cookie);
} else {
await updateDocument({
cookie,
documentType,
pageId,
params,
updatedDescription: state,
});
}
} catch (error) {
logger.error("Error in updating document:", error);
}
});
};
/**
* Initialize and configure the HocusPocus server
*/
public async initialize(): Promise<Hocuspocus> {
if (this.isInitialized && this.server) {
return this.server;
}
this.server = Server.configure({
name: this.serverName,
onAuthenticate: this.onAuthenticate,
onStateless: this.onStateless,
extensions: [
new Logger({
onChange: false,
log: (message) => {
logger.info(message);
},
}),
new Database({
fetch: this.onDatabaseFetch as any,
store: this.onDatabaseStore as any,
}),
new Redis({
redis: redisManager.getClient(),
}),
],
debounce: 10000,
});
this.isInitialized = true;
return this.server;
}
/**
* Get the configured server instance
*/
public getServer(): Hocuspocus | null {
return this.server;
}
/**
* Check if the server has been initialized
*/
public isServerInitialized(): boolean {
return this.isInitialized;
}
/**
* Reset the singleton instance (useful for testing)
*/
public static resetInstance(): void {
HocusPocusServerManager.instance = null;
}
}

View File

@@ -1,5 +1,4 @@
import { pinoHttp } from "pino-http";
import { Logger } from "pino";
const transport = {
target: "pino-pretty",
@@ -36,5 +35,3 @@ export const logger = pinoHttp({
},
},
});
export const manualLogger: Logger = logger.logger;

210
apps/live/src/redis.ts Normal file
View File

@@ -0,0 +1,210 @@
import Redis from "ioredis";
import { logger } from "@plane/logger";
export class RedisManager {
private static instance: RedisManager;
private redisClient: Redis | null = null;
private isConnected: boolean = false;
private connectionPromise: Promise<void> | null = null;
private constructor() {}
public static getInstance(): RedisManager {
if (!RedisManager.instance) {
RedisManager.instance = new RedisManager();
}
return RedisManager.instance;
}
public async initialize(): Promise<void> {
if (this.redisClient && this.isConnected) {
logger.info("Redis client already initialized and connected");
return;
}
if (this.connectionPromise) {
logger.info("Redis connection already in progress, waiting...");
await this.connectionPromise;
return;
}
this.connectionPromise = this.connect();
await this.connectionPromise;
}
private getRedisUrl(): string {
const redisUrl = process.env.REDIS_URL?.trim();
const redisHost = process.env.REDIS_HOST?.trim();
const redisPort = process.env.REDIS_PORT?.trim();
if (redisUrl) {
return redisUrl;
}
if (redisHost && redisPort && !Number.isNaN(Number(redisPort))) {
return `redis://${redisHost}:${redisPort}`;
}
return "";
}
private async connect(): Promise<void> {
try {
const redisUrl = this.getRedisUrl();
if (!redisUrl) {
logger.warn("No Redis URL provided, Redis functionality will be disabled");
this.isConnected = false;
return;
}
this.redisClient = new Redis(redisUrl, {
lazyConnect: true,
keepAlive: 30000,
connectTimeout: 10000,
commandTimeout: 5000,
enableOfflineQueue: false,
maxRetriesPerRequest: 3,
});
// Set up event listeners
this.redisClient.on("connect", () => {
logger.info("Redis client connected");
this.isConnected = true;
});
this.redisClient.on("ready", () => {
logger.info("Redis client ready");
this.isConnected = true;
});
this.redisClient.on("error", (error) => {
logger.error("Redis client error:", error);
this.isConnected = false;
});
this.redisClient.on("close", () => {
logger.warn("Redis client connection closed");
this.isConnected = false;
});
this.redisClient.on("reconnecting", () => {
logger.info("Redis client reconnecting...");
this.isConnected = false;
});
// Connect to Redis
await this.redisClient.connect();
// Test the connection
await this.redisClient.ping();
logger.info("Redis connection test successful");
} catch (error) {
logger.error("Failed to initialize Redis client:", error);
this.isConnected = false;
throw error;
} finally {
this.connectionPromise = null;
}
}
public getClient(): Redis | null {
if (!this.redisClient || !this.isConnected) {
logger.warn("Redis client not available or not connected");
return null;
}
return this.redisClient;
}
public isClientConnected(): boolean {
return this.isConnected && this.redisClient !== null;
}
public async disconnect(): Promise<void> {
if (this.redisClient) {
try {
await this.redisClient.quit();
logger.info("Redis client disconnected gracefully");
} catch (error) {
logger.error("Error disconnecting Redis client:", error);
// Force disconnect if quit fails
this.redisClient.disconnect();
} finally {
this.redisClient = null;
this.isConnected = false;
}
}
}
// Convenience methods for common Redis operations
public async set(key: string, value: string, ttl?: number): Promise<boolean> {
const client = this.getClient();
if (!client) return false;
try {
if (ttl) {
await client.setex(key, ttl, value);
} else {
await client.set(key, value);
}
return true;
} catch (error) {
logger.error(`Error setting Redis key ${key}:`, error);
return false;
}
}
public async get(key: string): Promise<string | null> {
const client = this.getClient();
if (!client) return null;
try {
return await client.get(key);
} catch (error) {
logger.error(`Error getting Redis key ${key}:`, error);
return null;
}
}
public async del(key: string): Promise<boolean> {
const client = this.getClient();
if (!client) return false;
try {
await client.del(key);
return true;
} catch (error) {
logger.error(`Error deleting Redis key ${key}:`, error);
return false;
}
}
public async exists(key: string): Promise<boolean> {
const client = this.getClient();
if (!client) return false;
try {
const result = await client.exists(key);
return result === 1;
} catch (error) {
logger.error(`Error checking Redis key ${key}:`, error);
return false;
}
}
public async expire(key: string, ttl: number): Promise<boolean> {
const client = this.getClient();
if (!client) return false;
try {
const result = await client.expire(key, ttl);
return result === 1;
} catch (error) {
logger.error(`Error setting expiry for Redis key ${key}:`, error);
return false;
}
}
}
// Export a default instance for convenience
export const redisManager = RedisManager.getInstance();

View File

@@ -3,13 +3,16 @@ import cors from "cors";
import expressWs from "express-ws";
import express, { Request, Response } from "express";
import helmet from "helmet";
import { logger } from "@plane/logger";
// hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
import { HocusPocusServerManager } from "@/hocuspocus";
// helpers
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
import { logger, manualLogger } from "@/core/helpers/logger.js";
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document";
import { logger as loggerMiddleware } from "@/middlewares/logger";
// types
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
import { TConvertDocumentRequestBody } from "@/core/types/common";
// redis
import { redisManager } from "@/redis";
export class Server {
private app: any;
@@ -23,17 +26,33 @@ export class Server {
expressWs(this.app);
this.app.set("port", process.env.PORT || 3000);
this.setupMiddleware();
this.setupHocusPocus();
this.setupRoutes();
}
public async initialize(): Promise<void> {
return redisManager
.initialize()
.then(() => {
logger.info("Redis setup completed");
const manager = HocusPocusServerManager.getInstance();
manager.initialize().catch((error) => {
logger.error("Failed to initialize HocusPocusServer:");
throw error;
});
})
.catch((error) => {
logger.error("Failed to setup Redis:", error);
throw error;
});
}
private setupMiddleware() {
// Security middleware
this.app.use(helmet());
// Middleware for response compression
this.app.use(compression({ level: 6, threshold: 5 * 1000 }));
// Logging middleware
this.app.use(logger);
this.app.use(loggerMiddleware);
// Body parsing middleware
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
@@ -42,13 +61,6 @@ export class Server {
this.app.use(process.env.LIVE_BASE_PATH || "/live", this.router);
}
private async setupHocusPocus() {
this.hocuspocusServer = await getHocusPocusServer().catch((err) => {
manualLogger.error("Failed to initialize HocusPocusServer:", err);
process.exit(1);
});
}
private setupRoutes() {
this.router.get("/health", (_req: Request, res: Response) => {
res.status(200).json({ status: "OK" });
@@ -58,7 +70,7 @@ export class Server {
try {
this.hocuspocusServer.handleConnection(ws, req);
} catch (err) {
manualLogger.error("WebSocket connection error:", err);
logger.error("WebSocket connection error:", err);
ws.close();
}
});
@@ -81,7 +93,7 @@ export class Server {
description_binary,
});
} catch (error) {
manualLogger.error("Error in /convert-document endpoint:", error);
logger.error("Error in /convert-document endpoint:", error);
res.status(500).json({
message: `Internal server error.`,
});
@@ -97,35 +109,23 @@ export class Server {
public listen() {
this.serverInstance = this.app.listen(this.app.get("port"), () => {
manualLogger.info(`Plane Live server has started at port ${this.app.get("port")}`);
logger.info(`Plane Live server has started at port ${this.app.get("port")}`);
});
}
public async destroy() {
// Close the HocusPocus server WebSocket connections
await this.hocuspocusServer.destroy();
manualLogger.info("HocusPocus server WebSocket connections closed gracefully.");
logger.info("HocusPocus server WebSocket connections closed gracefully.");
// Disconnect Redis
await redisManager.disconnect();
logger.info("Redis connection closed gracefully.");
// Close the Express server
this.serverInstance.close(() => {
manualLogger.info("Express server closed gracefully.");
logger.info("Express server closed gracefully.");
process.exit(1);
});
}
}
const server = new Server();
server.listen();
// Graceful shutdown on unhandled rejection
process.on("unhandledRejection", async (err: any) => {
manualLogger.info("Unhandled Rejection: ", err);
manualLogger.info(`UNHANDLED REJECTION! 💥 Shutting down...`);
await server.destroy();
});
// Graceful shutdown on uncaught exception
process.on("uncaughtException", async (err: any) => {
manualLogger.info("Uncaught Exception: ", err);
manualLogger.info(`UNCAUGHT EXCEPTION! 💥 Shutting down...`);
await server.destroy();
});

36
apps/live/src/start.ts Normal file
View File

@@ -0,0 +1,36 @@
import { Server } from "./server";
import { logger } from "@plane/logger";
let server: Server;
async function startServer() {
server = new Server();
try {
await server.initialize();
server.listen();
} catch (error) {
logger.error("Failed to start server:", error);
process.exit(1);
}
}
startServer();
// Graceful shutdown on unhandled rejection
process.on("unhandledRejection", async (err: any) => {
logger.info("Unhandled Rejection: ", err);
logger.info(`UNHANDLED REJECTION! 💥 Shutting down...`);
if (server) {
await server.destroy();
}
});
// Graceful shutdown on uncaught exception
process.on("uncaughtException", async (err: any) => {
logger.info("Uncaught Exception: ", err);
logger.info(`UNCAUGHT EXCEPTION! 💥 Shutting down...`);
if (server) {
await server.destroy();
}
});