generated from fahricansecer/boilerplate-be
206 lines
5.6 KiB
TypeScript
206 lines
5.6 KiB
TypeScript
import {
|
||
WebSocketGateway,
|
||
WebSocketServer,
|
||
SubscribeMessage,
|
||
OnGatewayConnection,
|
||
OnGatewayDisconnect,
|
||
ConnectedSocket,
|
||
MessageBody,
|
||
} from '@nestjs/websockets';
|
||
import { Logger } from '@nestjs/common';
|
||
import { Server, Socket } from 'socket.io';
|
||
|
||
/**
|
||
* ContentGen AI — Gerçek Zamanlı WebSocket Gateway
|
||
*
|
||
* Frontend'e render ilerleme bildirimlerini iletir.
|
||
* C# Worker → RenderCallbackController → EventsGateway → Frontend
|
||
*
|
||
* Room yapısı: "project:{projectId}" — her proje kendi room'una abone olur
|
||
*/
|
||
@WebSocketGateway({
|
||
cors: {
|
||
origin: ['http://localhost:3001', 'http://localhost:3000', process.env.FRONTEND_URL || '*'],
|
||
credentials: true,
|
||
},
|
||
namespace: '/ws',
|
||
transports: ['websocket', 'polling'],
|
||
})
|
||
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||
@WebSocketServer()
|
||
server: Server;
|
||
|
||
private readonly logger = new Logger(EventsGateway.name);
|
||
private connectedClients = 0;
|
||
|
||
handleConnection(client: Socket) {
|
||
this.connectedClients++;
|
||
this.logger.log(`Client bağlandı: ${client.id} (toplam: ${this.connectedClients})`);
|
||
}
|
||
|
||
handleDisconnect(client: Socket) {
|
||
this.connectedClients--;
|
||
this.logger.log(`Client ayrıldı: ${client.id} (toplam: ${this.connectedClients})`);
|
||
}
|
||
|
||
/**
|
||
* Frontend, proje detay sayfasına girdiğinde bu event'i gönderir.
|
||
* Client ilgili proje room'una katılır.
|
||
*/
|
||
@SubscribeMessage('join:project')
|
||
handleJoinProject(
|
||
@ConnectedSocket() client: Socket,
|
||
@MessageBody() data: { projectId: string },
|
||
) {
|
||
const room = `project:${data.projectId}`;
|
||
client.join(room);
|
||
this.logger.debug(`Client ${client.id} → room: ${room}`);
|
||
return { event: 'joined', data: { room, projectId: data.projectId } };
|
||
}
|
||
|
||
/**
|
||
* Frontend, proje sayfasından ayrıldığında bu event'i gönderir.
|
||
*/
|
||
@SubscribeMessage('leave:project')
|
||
handleLeaveProject(
|
||
@ConnectedSocket() client: Socket,
|
||
@MessageBody() data: { projectId: string },
|
||
) {
|
||
const room = `project:${data.projectId}`;
|
||
client.leave(room);
|
||
this.logger.debug(`Client ${client.id} ← room: ${room}`);
|
||
return { event: 'left', data: { room } };
|
||
}
|
||
|
||
/**
|
||
* Kullanıcı kendi bildirim room'una katılır.
|
||
* Frontend login sonrası bu event'i göndererek anlık bildirimleri alır.
|
||
*/
|
||
@SubscribeMessage('join:user')
|
||
handleJoinUser(
|
||
@ConnectedSocket() client: Socket,
|
||
@MessageBody() data: { userId: string },
|
||
) {
|
||
const room = `user:${data.userId}`;
|
||
client.join(room);
|
||
this.logger.debug(`Client ${client.id} → user room: ${room}`);
|
||
return { event: 'joined', data: { room, userId: data.userId } };
|
||
}
|
||
|
||
/**
|
||
* Kullanıcı bildirim room'undan ayrılır.
|
||
*/
|
||
@SubscribeMessage('leave:user')
|
||
handleLeaveUser(
|
||
@ConnectedSocket() client: Socket,
|
||
@MessageBody() data: { userId: string },
|
||
) {
|
||
const room = `user:${data.userId}`;
|
||
client.leave(room);
|
||
this.logger.debug(`Client ${client.id} ← user room: ${room}`);
|
||
return { event: 'left', data: { room } };
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Server → Client Events (RenderCallbackController tarafından tetiklenir)
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Render ilerleme bildirimi gönder.
|
||
* Stage: 'tts' | 'image_generation' | 'music_generation' | 'compositing' | 'encoding'
|
||
*/
|
||
emitRenderProgress(
|
||
projectId: string,
|
||
payload: {
|
||
progress: number;
|
||
stage: string;
|
||
stageLabel: string;
|
||
currentScene?: number;
|
||
totalScenes?: number;
|
||
eta?: number; // Tahmini kalan saniye
|
||
},
|
||
) {
|
||
this.server.to(`project:${projectId}`).emit('render:progress', {
|
||
projectId,
|
||
...payload,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Render tamamlandı bildirimi.
|
||
*/
|
||
emitRenderCompleted(
|
||
projectId: string,
|
||
payload: {
|
||
finalVideoUrl: string;
|
||
thumbnailUrl?: string;
|
||
processingTimeMs: number;
|
||
fileSize: number;
|
||
},
|
||
) {
|
||
this.server.to(`project:${projectId}`).emit('render:completed', {
|
||
projectId,
|
||
...payload,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Render hatası bildirimi.
|
||
*/
|
||
emitRenderFailed(
|
||
projectId: string,
|
||
payload: {
|
||
error: string;
|
||
stage: string;
|
||
attemptNumber: number;
|
||
canRetry: boolean;
|
||
},
|
||
) {
|
||
this.server.to(`project:${projectId}`).emit('render:failed', {
|
||
projectId,
|
||
...payload,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Proje durum değişikliği bildirimi (status change).
|
||
*/
|
||
emitProjectStatusChanged(projectId: string, status: string) {
|
||
this.server.to(`project:${projectId}`).emit('project:status', {
|
||
projectId,
|
||
status,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Kullanıcıya anlık bildirim gönder.
|
||
* NotificationsService.createNotification() tarafından çağrılır.
|
||
*/
|
||
emitNotification(
|
||
userId: string,
|
||
payload: {
|
||
id: string;
|
||
type: string;
|
||
title: string;
|
||
message?: string | null;
|
||
metadata?: unknown;
|
||
isRead: boolean;
|
||
createdAt: string;
|
||
},
|
||
) {
|
||
this.server.to(`user:${userId}`).emit('notification:new', {
|
||
...payload,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
/** Bağlı client sayısını döndür (health check için) */
|
||
getConnectedClientsCount(): number {
|
||
return this.connectedClients;
|
||
}
|
||
}
|