mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
1 Commits
devin/1734
...
chore/live
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9772cf7bfa |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
21
live/src/core/controllers/collaboration.controller.ts
Normal file
21
live/src/core/controllers/collaboration.controller.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
16
live/src/core/controllers/health.controller.ts
Normal file
16
live/src/core/controllers/health.controller.ts
Normal 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" });
|
||||
}
|
||||
}
|
||||
28
live/src/core/lib/controller.ts
Normal file
28
live/src/core/lib/controller.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
89
live/src/core/lib/decorators.ts
Normal file
89
live/src/core/lib/decorators.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
2
live/src/core/lib/index.ts
Normal file
2
live/src/core/lib/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./controller";
|
||||
export * from "./decorators";
|
||||
@@ -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
6
live/src/start.ts
Normal 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();
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
Reference in New Issue
Block a user