ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: единый realtime слой Tasker

Добавлен NODE.DC realtime event stream для workspace/project/member/invite/profile событий. Обновлены frontend stores и live controller. Доработаны settings modal, member dropdown и confirm remove под NODE.DC UX.
This commit is contained in:
DCCONSTRUCTIONS
2026-05-12 17:28:40 +03:00
parent fc59481703
commit 480f85cce8
23 changed files with 993 additions and 72 deletions
@@ -8,6 +8,7 @@ import { CollaborationController } from "./collaboration.controller";
import { DocumentController } from "./document.controller";
import { HealthController } from "./health.controller";
import { IssueStreamController } from "./issue-stream.controller";
import { NodeDCStreamController } from "./nodedc-stream.controller";
import { PdfExportController } from "./pdf-export.controller";
export const CONTROLLERS = [
@@ -15,5 +16,6 @@ export const CONTROLLERS = [
DocumentController,
HealthController,
IssueStreamController,
NodeDCStreamController,
PdfExportController,
];
@@ -0,0 +1,126 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Request } from "express";
import type Redis from "ioredis";
import type { WebSocket as WSSocket } from "ws";
// plane imports
import { Controller, WebSocket as WSDecorator } from "@plane/decorators";
import { logger } from "@plane/logger";
// redis
import { redisManager } from "@/redis";
// services
import { UserService } from "@/services/user.service";
const NODEDC_EVENT_CHANNEL_PREFIX = "plane:nodedc-events:user";
const HEARTBEAT_INTERVAL_MS = 25_000;
const sendJson = (ws: WSSocket, payload: Record<string, unknown>) => {
if (ws.readyState !== 1) return;
ws.send(JSON.stringify(payload));
};
@Controller("/nodedc")
export class NodeDCStreamController {
[key: string]: unknown;
@WSDecorator("/stream")
handleConnection(ws: WSSocket, req: Request) {
void this.handleNodeDCStream(ws, req);
}
private async handleNodeDCStream(ws: WSSocket, req: Request) {
const cookie = req.headers.cookie?.toString();
if (!cookie) {
ws.close(1008, "Missing NODE.DC stream credentials");
return;
}
let subscriber: Redis | undefined;
let heartbeat: NodeJS.Timeout | undefined;
let isCleanedUp = false;
const cleanup = async () => {
if (isCleanedUp) return;
isCleanedUp = true;
if (heartbeat) clearInterval(heartbeat);
if (!subscriber) return;
try {
await subscriber.unsubscribe();
subscriber.disconnect();
} catch (error) {
logger.error("NODEDC_STREAM_CONTROLLER: Redis cleanup failed:", error);
}
};
try {
const userService = new UserService();
const user = await userService.currentUser(cookie);
const redisClient = redisManager.getClient();
if (!redisClient) {
ws.close(1011, "NODE.DC stream unavailable");
return;
}
const channel = `${NODEDC_EVENT_CHANNEL_PREFIX}:${user.id}`;
subscriber = redisClient.duplicate({ lazyConnect: true });
await subscriber.connect();
await subscriber.subscribe(channel);
subscriber.on("message", (_channel, message) => {
try {
const event = JSON.parse(message) as Record<string, unknown>;
sendJson(ws, event);
} catch (error) {
logger.error("NODEDC_STREAM_CONTROLLER: Failed to forward event:", error);
}
});
subscriber.on("error", (error) => {
logger.error("NODEDC_STREAM_CONTROLLER: Redis subscriber error:", error);
ws.close(1011, "NODE.DC stream subscriber failed");
});
heartbeat = setInterval(() => {
sendJson(ws, { type: "nodedc.stream.ping", server_ts: new Date().toISOString() });
}, HEARTBEAT_INTERVAL_MS);
sendJson(ws, {
type: "nodedc.stream.ready",
user_id: user.id,
server_ts: new Date().toISOString(),
});
} catch (error) {
logger.error("NODEDC_STREAM_CONTROLLER: WebSocket authentication failed:", error);
ws.close(1008, "NODE.DC stream authentication failed");
await cleanup();
return;
}
ws.on("message", (message) => {
try {
const payload = JSON.parse(message.toString()) as { type?: string };
if (payload.type === "nodedc.stream.pong") return;
} catch {
// Client messages are optional for this stream.
}
});
ws.on("close", () => {
void cleanup();
});
ws.on("error", (error: Error) => {
logger.error("NODEDC_STREAM_CONTROLLER: WebSocket connection error:", error);
ws.close(1011, "NODE.DC stream connection failed");
void cleanup();
});
}
}