mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
feat(core): Initial support for two-way communication over websockets (#7570)
- Enable two-way communication with web sockets - Enable sending push messages to specific users - Add collaboration service for managing active users for workflow Missing things: - State is currently kept only in memory, making this not work in multi-master setups - Removing a user from active users in situations where they go inactive or we miss the "workflow closed" message - I think a timer based solution for this would cover most edge cases. I.e. have FE ping every X minutes, BE removes the user unless they have received a ping in Y minutes, where Y > X - FE changes to be added later by @MiloradFilipovic Github issue / Community forum post (link here to close automatically): --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -13,27 +13,52 @@ import { SSEPush } from './sse.push';
|
||||
import { WebSocketPush } from './websocket.push';
|
||||
import type { PushResponse, SSEPushRequest, WebSocketPushRequest } from './types';
|
||||
import type { IPushDataType } from '@/Interfaces';
|
||||
import type { User } from '@/databases/entities/User';
|
||||
|
||||
const useWebSockets = config.getEnv('push.backend') === 'websocket';
|
||||
|
||||
/**
|
||||
* Push service for uni- or bi-directional communication with frontend clients.
|
||||
* Uses either server-sent events (SSE, unidirectional from backend --> frontend)
|
||||
* or WebSocket (bidirectional backend <--> frontend) depending on the configuration.
|
||||
*
|
||||
* @emits message when a message is received from a client
|
||||
*/
|
||||
@Service()
|
||||
export class Push extends EventEmitter {
|
||||
public isBidirectional = useWebSockets;
|
||||
|
||||
private backend = useWebSockets ? Container.get(WebSocketPush) : Container.get(SSEPush);
|
||||
|
||||
handleRequest(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) {
|
||||
const {
|
||||
userId,
|
||||
query: { sessionId },
|
||||
} = req;
|
||||
if (req.ws) {
|
||||
(this.backend as WebSocketPush).add(req.query.sessionId, req.ws);
|
||||
(this.backend as WebSocketPush).add(sessionId, userId, req.ws);
|
||||
this.backend.on('message', (msg) => this.emit('message', msg));
|
||||
} else if (!useWebSockets) {
|
||||
(this.backend as SSEPush).add(req.query.sessionId, { req, res });
|
||||
(this.backend as SSEPush).add(sessionId, userId, { req, res });
|
||||
} else {
|
||||
res.status(401).send('Unauthorized');
|
||||
return;
|
||||
}
|
||||
this.emit('editorUiConnected', req.query.sessionId);
|
||||
|
||||
this.emit('editorUiConnected', sessionId);
|
||||
}
|
||||
|
||||
send<D>(type: IPushDataType, data: D, sessionId: string | undefined = undefined) {
|
||||
broadcast<D>(type: IPushDataType, data?: D) {
|
||||
this.backend.broadcast(type, data);
|
||||
}
|
||||
|
||||
send<D>(type: IPushDataType, data: D, sessionId: string) {
|
||||
this.backend.send(type, data, sessionId);
|
||||
}
|
||||
|
||||
sendToUsers<D>(type: IPushDataType, data: D, userIds: Array<User['id']>) {
|
||||
this.backend.sendToUsers(type, data, userIds);
|
||||
}
|
||||
}
|
||||
|
||||
export const setupPushServer = (restEndpoint: string, server: Server, app: Application) => {
|
||||
@@ -82,7 +107,8 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const authCookie: string = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
|
||||
await resolveJwt(authCookie);
|
||||
const user = await resolveJwt(authCookie);
|
||||
req.userId = user.id;
|
||||
} catch (error) {
|
||||
if (ws) {
|
||||
ws.send(`Unauthorized: ${(error as Error).message}`);
|
||||
|
||||
Reference in New Issue
Block a user