websocket - WIP

This commit is contained in:
Philipinho
2026-01-17 03:15:58 +00:00
parent bcb004af21
commit 5c9eed53c0
4 changed files with 163 additions and 5 deletions
@@ -0,0 +1,91 @@
import { Injectable } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { ExcalidrawFollowPayload } from '../types/excalidraw.types';
@Injectable()
export class ExcalidrawCollabService {
async handleJoinRoom(
client: Socket,
server: Server,
roomId: string,
): Promise<void> {
await client.join(roomId);
const sockets = await server.in(roomId).fetchSockets();
if (sockets.length <= 1) {
server.to(client.id).emit('first-in-room');
} else {
client.broadcast.to(roomId).emit('new-user', client.id);
}
server.in(roomId).emit(
'room-user-change',
sockets.map((socket) => socket.id),
);
}
handleServerBroadcast(
client: Socket,
roomId: string,
encryptedData: ArrayBuffer,
iv: Uint8Array,
): void {
client.broadcast.to(roomId).emit('client-broadcast', encryptedData, iv);
}
handleServerVolatileBroadcast(
client: Socket,
roomId: string,
encryptedData: ArrayBuffer,
iv: Uint8Array,
): void {
client.volatile.broadcast
.to(roomId)
.emit('client-broadcast', encryptedData, iv);
}
async handleUserFollow(
client: Socket,
server: Server,
payload: ExcalidrawFollowPayload,
): Promise<void> {
const roomId = `follow@${payload.userToFollow.socketId}`;
if (payload.action === 'FOLLOW') {
await client.join(roomId);
} else {
await client.leave(roomId);
}
const sockets = await server.in(roomId).fetchSockets();
const followedBy = sockets.map((socket) => socket.id);
server.to(payload.userToFollow.socketId).emit(
'user-follow-room-change',
followedBy,
);
}
async handleDisconnecting(client: Socket, server: Server): Promise<void> {
for (const roomId of Array.from(client.rooms)) {
const otherClients = (await server.in(roomId).fetchSockets()).filter(
(socket) => socket.id !== client.id,
);
const isFollowRoom = roomId.startsWith('follow@');
if (!isFollowRoom && otherClients.length > 0) {
client.broadcast.to(roomId).emit(
'room-user-change',
otherClients.map((socket) => socket.id),
);
}
if (isFollowRoom && otherClients.length === 0) {
const socketId = roomId.replace('follow@', '');
server.to(socketId).emit('broadcast-unfollow');
}
}
}
}
@@ -0,0 +1,9 @@
export type ExcalidrawUserToFollow = {
socketId: string;
username: string;
};
export type ExcalidrawFollowPayload = {
userToFollow: ExcalidrawUserToFollow;
action: 'FOLLOW' | 'UNFOLLOW';
};
+61 -4
View File
@@ -1,6 +1,7 @@
import {
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
@@ -11,17 +12,23 @@ import { JwtPayload, JwtType } from '../core/auth/dto/jwt-payload';
import { OnModuleDestroy } from '@nestjs/common';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import * as cookie from 'cookie';
import { ExcalidrawCollabService } from './services/excalidraw-collab.service';
import { ExcalidrawFollowPayload } from './types/excalidraw.types';
@WebSocketGateway({
cors: { origin: '*' },
transports: ['websocket'],
})
export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
export class WsGateway
implements OnGatewayConnection, OnGatewayDisconnect, OnModuleDestroy
{
@WebSocketServer()
server: Server;
constructor(
private tokenService: TokenService,
private spaceMemberRepo: SpaceMemberRepo,
private excalidrawCollabService: ExcalidrawCollabService,
) {}
async handleConnection(client: Socket, ...args: any[]): Promise<void> {
@@ -41,6 +48,8 @@ export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
const spaceRooms = userSpaceIds.map((id) => this.getSpaceRoomName(id));
client.join([workspaceRoom, ...spaceRooms]);
this.server.to(client.id).emit('init-room');
} catch (err) {
client.emit('Unauthorized');
client.disconnect();
@@ -66,9 +75,15 @@ export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
}
@SubscribeMessage('join-room')
handleJoinRoom(client: Socket, @MessageBody() roomName: string): void {
// if room is a space, check if user has permissions
//client.join(roomName);
async handleJoinRoom(
client: Socket,
@MessageBody() roomId: string,
): Promise<void> {
await this.excalidrawCollabService.handleJoinRoom(
client,
this.server,
roomId,
);
}
@SubscribeMessage('leave-room')
@@ -76,6 +91,48 @@ export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
client.leave(roomName);
}
@SubscribeMessage('server-broadcast')
handleServerBroadcast(
client: Socket,
[roomId, encryptedData, iv]: [string, ArrayBuffer, Uint8Array],
): void {
this.excalidrawCollabService.handleServerBroadcast(
client,
roomId,
encryptedData,
iv,
);
}
@SubscribeMessage('server-volatile-broadcast')
handleServerVolatileBroadcast(
client: Socket,
[roomId, encryptedData, iv]: [string, ArrayBuffer, Uint8Array],
): void {
this.excalidrawCollabService.handleServerVolatileBroadcast(
client,
roomId,
encryptedData,
iv,
);
}
@SubscribeMessage('user-follow')
async handleUserFollow(
client: Socket,
@MessageBody() payload: ExcalidrawFollowPayload,
): Promise<void> {
await this.excalidrawCollabService.handleUserFollow(
client,
this.server,
payload,
);
}
async handleDisconnect(client: Socket): Promise<void> {
await this.excalidrawCollabService.handleDisconnecting(client, this.server);
}
onModuleDestroy() {
if (this.server) {
this.server.close();
+2 -1
View File
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { WsGateway } from './ws.gateway';
import { TokenModule } from '../core/auth/token.module';
import { ExcalidrawCollabService } from './services/excalidraw-collab.service';
@Module({
imports: [TokenModule],
providers: [WsGateway],
providers: [WsGateway, ExcalidrawCollabService],
})
export class WsModule {}