Compare commits

...

1 Commits

Author SHA1 Message Date
Palanikannan M
9772cf7bfa fix: change live server initialization 2024-12-10 21:03:32 +05:30
11 changed files with 1335 additions and 146 deletions

View File

@@ -6,9 +6,9 @@
"private": true,
"type": "module",
"scripts": {
"dev": "concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
"dev": "concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/start.js\"",
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
"start": "node dist/server.js",
"start": "node dist/start.js",
"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
@@ -52,13 +52,13 @@
"@types/cors": "^2.8.17",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.21",
"@types/express-ws": "^3.0.4",
"@types/express-ws": "^3.0.5",
"@types/node": "^20.14.9",
"babel-plugin-module-resolver": "^5.0.2",
"concurrently": "^9.0.1",
"nodemon": "^3.1.7",
"reflect-metadata": "^0.2.2",
"ts-node": "^10.9.2",
"tsup": "^7.2.0",
"typescript": "5.3.3"
}
}

View File

@@ -0,0 +1,21 @@
import { Router } from "express";
import { WebSocket } from "ws";
import { manualLogger } from "../helpers/logger.js";
import type { Hocuspocus as HocusPocusServer } from "@hocuspocus/server";
export class CollaborationController {
constructor(private hocusPocusServer: HocusPocusServer) {
this.hocusPocusServer = hocusPocusServer;
}
registerRoutes(router: Router) {
router.ws("/collaboration", (ws: WebSocket, req) => {
try {
this.hocusPocusServer.handleConnection(ws, req);
} catch (err) {
manualLogger.error("WebSocket connection error:", err);
ws.close();
}
});
}
}

View File

@@ -0,0 +1,16 @@
import { Router, Request, Response } from "express";
import type { Hocuspocus as HocusPocusServer } from "@hocuspocus/server";
export class HealthController {
constructor(private hocusPocusServer: HocusPocusServer) {
this.hocusPocusServer = hocusPocusServer;
}
registerRoutes(router: Router) {
router.get("/health", this.healthCheck);
}
private healthCheck(_req: Request, res: Response) {
res.status(200).json({ status: "OK" });
}
}

View File

@@ -0,0 +1,28 @@
import { RequestHandler, Router } from "express";
type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
export function registerControllers(router: Router, Controller: any) {
const instance = new Controller();
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;
Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
if (methodName === "constructor") return; // Skip the constructor
const method = Reflect.getMetadata("method", instance, methodName) as HttpMethod;
const route = Reflect.getMetadata("route", instance, methodName) as string;
const middlewares = (Reflect.getMetadata("middlewares", instance, methodName) as RequestHandler[]) || [];
if (method && route) {
const handler = instance[methodName as keyof typeof instance] as unknown;
if (typeof handler === "function") {
(router[method] as (path: string, ...handlers: RequestHandler[]) => void)(
`${baseRoute}${route}`,
...middlewares,
handler.bind(instance)
);
}
}
});
}

View File

@@ -0,0 +1,89 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import "reflect-metadata";
import { RequestHandler } from "express";
/**
* Controller decorator
* @param baseRoute
* @returns
*/
export function Controller(baseRoute: string = ""): ClassDecorator {
return function (target: Function) {
Reflect.defineMetadata("baseRoute", baseRoute, target);
};
}
/**
* Controller GET method decorator
* @param baseRoute
* @returns
*/
export function Get(route: string): any {
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
Reflect.defineMetadata("method", "get", target, propertyKey);
Reflect.defineMetadata("route", route, target, propertyKey);
};
}
/**
* Controller POST method decorator
* @param baseRoute
* @returns
*/
export function Post(route: string): any {
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
Reflect.defineMetadata("method", "post", target, propertyKey);
Reflect.defineMetadata("route", route, target, propertyKey);
};
}
/**
* Controller PATCH method decorator
* @param baseRoute
* @returns
*/
export function Patch(route: string): any {
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
Reflect.defineMetadata("method", "patch", target, propertyKey);
Reflect.defineMetadata("route", route, target, propertyKey);
};
}
/**
* Controller PUT method decorator
* @param baseRoute
* @returns
*/
export function Put(route: string): any {
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
Reflect.defineMetadata("method", "put", target, propertyKey);
Reflect.defineMetadata("route", route, target, propertyKey);
};
}
/**
* Controller DELETE method decorator
* @param baseRoute
* @returns
*/
export function Delete(route: string): any {
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
Reflect.defineMetadata("method", "delete", target, propertyKey);
Reflect.defineMetadata("route", route, target, propertyKey);
};
}
/**
* Middle method decorator
* @param baseRoute
* @returns
*/
export function Middleware(middleware: RequestHandler): any {
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const middlewares = Reflect.getMetadata("middlewares", target, propertyKey) || [];
middlewares.push(middleware);
Reflect.defineMetadata("middlewares", middlewares, target, propertyKey);
};
}

View File

@@ -0,0 +1,2 @@
export * from "./controller";
export * from "./decorators";

View File

@@ -1,118 +1,166 @@
import "@/core/config/sentry-config.js";
import express from "express";
import expressWs from "express-ws";
import express, { Express } from "express";
import expressWs, { Application as WsApplication } from "express-ws";
import * as Sentry from "@sentry/node";
import compression from "compression";
import helmet from "helmet";
// cors
import cors from "cors";
import { Server as HTTPServer } from "http";
// core hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
import { logger, manualLogger } from "@/core/helpers/logger.js";
import { errorHandler } from "@/core/helpers/error-handler.js";
import { HealthController } from "@/core/controllers/health.controller.js";
import { CollaborationController } from "@/core/controllers/collaboration.controller.js";
import type { Hocuspocus as HocusPocusServer } from "@hocuspocus/server";
const app = express();
expressWs(app);
// Types
type WebSocketServerType = Express & WsApplication;
app.set("port", process.env.PORT || 3000);
const Controllers = [HealthController, CollaborationController];
export class Server {
private app: WebSocketServerType;
private port: number;
private hocusPocusServer: HocusPocusServer | null;
private httpServer: HTTPServer | null;
// Security middleware
app.use(helmet());
constructor() {
const expressApp = express();
const wsInstance = expressWs(expressApp);
this.app = wsInstance.app as WebSocketServerType;
this.port = Number(process.env.PORT || 3000);
this.hocusPocusServer = null;
this.httpServer = null;
// Middleware for response compression
app.use(
compression({
level: 6,
threshold: 5 * 1000,
}),
);
// Logging middleware
app.use(logger);
// Body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// cors middleware
app.use(cors());
const router = express.Router();
const HocusPocusServer = await getHocusPocusServer().catch((err) => {
manualLogger.error("Failed to initialize HocusPocusServer:", err);
process.exit(1);
});
router.get("/health", (_req, res) => {
res.status(200).json({ status: "OK" });
});
router.ws("/collaboration", (ws, req) => {
try {
HocusPocusServer.handleConnection(ws, req);
} catch (err) {
manualLogger.error("WebSocket connection error:", err);
ws.close();
this.setupMiddleware();
}
});
app.use(process.env.LIVE_BASE_PATH || "/live", router);
private setupMiddleware(): void {
// Security middleware
this.app.use(helmet());
app.use((_req, res) => {
res.status(404).send("Not Found");
});
Sentry.setupExpressErrorHandler(app);
app.use(errorHandler);
const liveServer = app.listen(app.get("port"), () => {
manualLogger.info(`Plane Live server has started at port ${app.get("port")}`);
});
const gracefulShutdown = async () => {
manualLogger.info("Starting graceful shutdown...");
try {
// Close the HocusPocus server WebSocket connections
await HocusPocusServer.destroy();
manualLogger.info(
"HocusPocus server WebSocket connections closed gracefully.",
// Compression middleware
this.app.use(
compression({
level: 6,
threshold: 5 * 1000,
}),
);
// Close the Express server
liveServer.close(() => {
manualLogger.info("Express server closed gracefully.");
process.exit(1);
});
} catch (err) {
manualLogger.error("Error during shutdown:", err);
process.exit(1);
// Logging middleware
this.app.use(logger);
// Body parsing middleware
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
// CORS middleware
this.app.use(cors());
}
// Forcefully shut down after 10 seconds if not closed
setTimeout(() => {
manualLogger.error("Forcing shutdown...");
process.exit(1);
}, 10000);
};
private async setupWebSocketServer(): Promise<void> {
try {
this.hocusPocusServer = await getHocusPocusServer();
} catch (err) {
manualLogger.error("Failed to initialize HocusPocusServer:", err);
process.exit(1);
}
}
// Graceful shutdown on unhandled rejection
process.on("unhandledRejection", (err: any) => {
manualLogger.info("Unhandled Rejection: ", err);
manualLogger.info(`UNHANDLED REJECTION! 💥 Shutting down...`);
gracefulShutdown();
});
private setupControllers(): void {
if (!this.hocusPocusServer) {
throw new Error("HocusPocus server not initialized");
}
// Graceful shutdown on uncaught exception
process.on("uncaughtException", (err: any) => {
manualLogger.info("Uncaught Exception: ", err);
manualLogger.info(`UNCAUGHT EXCEPTION! 💥 Shutting down...`);
gracefulShutdown();
});
const router = express.Router();
Controllers.forEach((Controller) => {
const instance = new Controller(this.hocusPocusServer);
instance.registerRoutes(router);
});
this.app.use(process.env.LIVE_BASE_PATH || "/live", router);
// 404 handler
this.app.use((_req: express.Request, res: express.Response) => {
res.status(404).send("Not Found");
});
}
private setupErrorHandling(): void {
Sentry.setupExpressErrorHandler(this.app);
this.app.use(errorHandler);
}
private setupShutdownHandlers(): void {
const handleShutdown = async (signal: string) => {
manualLogger.info(`${signal} received. Starting graceful shutdown...`);
await this.gracefulShutdown();
};
process.on("SIGTERM", () => handleShutdown("SIGTERM"));
process.on("SIGINT", () => handleShutdown("SIGINT"));
process.on("unhandledRejection", (err: Error | null) => {
manualLogger.info("Unhandled Rejection: ", err);
manualLogger.info(`UNHANDLED REJECTION! 💥 Shutting down...`);
this.gracefulShutdown();
});
process.on("uncaughtException", (err: Error) => {
manualLogger.info("Uncaught Exception: ", err);
manualLogger.info(`UNCAUGHT EXCEPTION! 💥 Shutting down...`);
this.gracefulShutdown();
});
}
public async gracefulShutdown(): Promise<void> {
manualLogger.info("Starting graceful shutdown...");
try {
if (this.hocusPocusServer) {
await this.hocusPocusServer.destroy();
manualLogger.info(
"HocusPocus server WebSocket connections closed gracefully.",
);
}
if (this.httpServer) {
await new Promise<void>((resolve) => {
this.httpServer?.close(() => {
manualLogger.info("Express server closed gracefully.");
resolve();
});
});
}
} catch (err) {
manualLogger.error("Error during shutdown:", err);
} finally {
process.exit(1);
}
}
public async start(): Promise<void> {
try {
// Initialize WebSocket server first
await this.setupWebSocketServer();
// Then setup controllers with the initialized hocusPocusServer
this.setupControllers();
// Setup error handling
this.setupErrorHandling();
// Start the server
this.httpServer = this.app.listen(this.port, () => {
manualLogger.info(`Plane Live server has started at port ${this.port}`);
});
// Setup graceful shutdown handlers
this.setupShutdownHandlers();
} catch (error) {
manualLogger.error("Failed to start server:", error);
process.exit(1);
}
}
}

6
live/src/start.ts Normal file
View File

@@ -0,0 +1,6 @@
import { Server } from "./server.js";
// Start the worker for taking over the migration jobs
const server = new Server();
server.start();

View File

@@ -3,16 +3,23 @@
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2015"],
"lib": [
"ES2015"
],
"outDir": "./dist",
"rootDir": ".",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/plane-live/*": ["./src/ce/*"]
"@/*": [
"./src/*"
],
"@/plane-live/*": [
"./src/ce/*"
]
},
"removeComments": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"sourceMap": true,
"inlineSources": true,
@@ -21,6 +28,13 @@
// This improves issue grouping in Sentry.
"sourceRoot": "/"
},
"include": ["src/**/*.ts", "tsup.config.ts"],
"exclude": ["./dist", "./build", "./node_modules"]
"include": [
"src/**/*.ts",
"tsup.config.ts"
],
"exclude": [
"./dist",
"./build",
"./node_modules"
]
}

View File

@@ -1,11 +0,0 @@
import { defineConfig, Options } from "tsup";
export default defineConfig((options: Options) => ({
entry: ["src/server.ts"],
format: ["cjs", "esm"],
dts: true,
clean: false,
external: ["react"],
injectStyle: true,
...options,
}));

1038
yarn.lock

File diff suppressed because it is too large Load Diff