import { ROUTES_ENUM } from '@/routes/route.constant' import { store } from '@/store/store' import * as signalR from '@microsoft/signalr' import { toast } from '@/components/ui' import Notification from '@/components/ui/Notification' import { VideoroomAttendanceDto, VideoroomChatDto } from '@/proxy/videoroom/models' export class SignalRService { private connection!: signalR.HubConnection private isConnected: boolean = false private currentSessionId?: string private isKicked: boolean = false private onAttendanceUpdate?: (record: VideoroomAttendanceDto) => void private onParticipantJoined?: ( userId: string, name: string, isTeacher: boolean, isActive: boolean, ) => void private onParticipantLeft?: (payload: { userId: string sessionId: string userName: string }) => void private onChatMessage?: (message: VideoroomChatDto) => void private onParticipantMuted?: (userId: string, isMuted: boolean) => void private onHandRaiseReceived?: (studentId: string) => void private onHandRaiseDismissed?: (studentId: string) => void private onOfferReceived?: (fromUserId: string, offer: RTCSessionDescriptionInit) => void private onAnswerReceived?: (fromUserId: string, answer: RTCSessionDescriptionInit) => void private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void private onForceCleanup?: () => void constructor() { const { auth } = store.getState() this.connection = new signalR.HubConnectionBuilder() .withUrl(`${import.meta.env.VITE_API_URL}/videoroomhub`, { accessTokenFactory: () => auth.session.token || '', }) .configureLogging(signalR.LogLevel.Information) .build() this.setupEventHandlers() } private setupEventHandlers() { if (!this.connection) return this.connection.on('AttendanceUpdated', (record: VideoroomAttendanceDto) => { this.onAttendanceUpdate?.(record) }) this.connection.on( 'ParticipantJoined', (userId: string, name: string, isTeacher: boolean, isActive: boolean) => { this.onParticipantJoined?.(userId, name, isTeacher, isActive) }, ) this.connection.on( 'ParticipantLeft', (payload: { userId: string; sessionId: string; userName: string }) => { this.onParticipantLeft?.(payload) }, ) this.connection.on('ChatMessage', (message: any) => { this.onChatMessage?.(message) }) this.connection.on('ParticipantMuted', (userId: string, isMuted: boolean) => { this.onParticipantMuted?.(userId, isMuted) }) this.connection.on('HandRaiseReceived', (payload: any) => { this.onHandRaiseReceived?.(payload.studentId) }) this.connection.on('HandRaiseDismissed', (payload: any) => { this.onHandRaiseDismissed?.(payload.studentId) }) this.connection.on('ReceiveOffer', (fromUserId: string, offer: RTCSessionDescriptionInit) => { this.onOfferReceived?.(fromUserId, offer) }) this.connection.on('ReceiveAnswer', (fromUserId: string, answer: RTCSessionDescriptionInit) => { this.onAnswerReceived?.(fromUserId, answer) }) this.connection.on( 'ReceiveIceCandidate', (fromUserId: string, candidate: RTCIceCandidateInit) => { this.onIceCandidateReceived?.(fromUserId, candidate) }, ) this.connection.onreconnected(async () => { this.isConnected = true toast.push(, { placement: 'top-end', }) if (this.currentSessionId && store.getState().auth.user) { const u = store.getState().auth.user await this.joinClass(this.currentSessionId, u.id, u.name, u.role === 'teacher', true) } }) this.connection.onclose(async () => { if (this.isKicked) { toast.push( , { placement: 'top-end' }, ) this.isConnected = false this.currentSessionId = undefined return } this.isConnected = false try { if (this.currentSessionId) { await this.connection.invoke('LeaveClass', this.currentSessionId) } } finally { this.currentSessionId = undefined } }) this.connection.on('Error', (message: string) => { toast.push(, { placement: 'top-end', }) }) this.connection.on('Warning', (message: string) => { toast.push(, { placement: 'top-end', }) }) this.connection.on('Info', (message: string) => { toast.push(, { placement: 'top-end', }) }) this.connection.onreconnecting(() => { if (this.isKicked) { toast.push( , ) this.connection.stop() throw new Error('Reconnect blocked after kick') } }) this.connection.on('ForceDisconnect', async (message: string) => { this.isKicked = true toast.push(, { placement: 'top-end', }) if (this.onForceCleanup) { this.onForceCleanup() } try { await this.connection.stop() } catch {} this.isConnected = false if (this.currentSessionId && store.getState().auth.user) { this.onParticipantLeft?.({ userId: store.getState().auth.user.id, sessionId: this.currentSessionId, userName: store.getState().auth.user.name, }) } this.currentSessionId = undefined window.location.href = ROUTES_ENUM.protected.admin.videoroom.roomList }) } async start(): Promise { try { const startPromise = this.connection.start() const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000), ) await Promise.race([startPromise, timeout]) this.isConnected = true toast.push(, { placement: 'top-end', }) } catch { toast.push( , { placement: 'top-end' }, ) this.isConnected = false } } async joinClass( sessionId: string, userId: string, userName: string, isTeacher: boolean, isActive: boolean, ): Promise { if (!this.isConnected) { toast.push( , ) return } this.currentSessionId = sessionId try { await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive) } catch { toast.push(, { placement: 'top-end', }) } } async leaveClass(sessionId: string): Promise { const { auth } = store.getState() if (!this.isConnected) { this.onParticipantLeft?.({ userId: auth.user.id, sessionId, userName: auth.user.name }) return } try { await this.connection.invoke('LeaveClass', sessionId) this.currentSessionId = undefined } catch { toast.push(, { placement: 'top-end', }) } } async sendChatMessage( sessionId: string, senderId: string, senderName: string, message: string, isTeacher: boolean, ): Promise { if (!this.isConnected) { const chatMessage: VideoroomChatDto = { id: crypto.randomUUID(), sessionId, senderId, senderName, message, timestamp: new Date().toISOString(), isTeacher, messageType: 'public', } setTimeout(() => { this.onChatMessage?.(chatMessage) }, 100) return } try { await this.connection.invoke( 'SendChatMessage', sessionId, senderId, senderName, message, isTeacher, 'public', ) } catch { toast.push(, { placement: 'top-end', }) } } async sendPrivateMessage( sessionId: string, senderId: string, senderName: string, message: string, recipientId: string, recipientName: string, isTeacher: boolean, ): Promise { if (!this.isConnected) { const chatMessage: VideoroomChatDto = { id: crypto.randomUUID(), sessionId, senderId, senderName, message, timestamp: new Date().toISOString(), isTeacher, recipientId, recipientName, messageType: 'private', } setTimeout(() => { this.onChatMessage?.(chatMessage) }, 100) return } try { await this.connection.invoke( 'SendPrivateMessage', sessionId, senderId, senderName, message, recipientId, recipientName, isTeacher, 'private', ) } catch { toast.push(, { placement: 'top-end', }) } } async sendAnnouncement( sessionId: string, senderId: string, senderName: string, message: string, isTeacher: boolean, ): Promise { if (!this.isConnected) { const chatMessage: VideoroomChatDto = { id: crypto.randomUUID(), sessionId, senderId, senderName, message, timestamp: new Date().toISOString(), isTeacher, messageType: 'announcement', } setTimeout(() => { this.onChatMessage?.(chatMessage) }, 100) return } try { await this.connection.invoke( 'SendAnnouncement', sessionId, senderId, senderName, message, isTeacher, ) } catch { toast.push(, { placement: 'top-end', }) } } async muteParticipant( sessionId: string, userId: string, isMuted: boolean, isTeacher: boolean, ): Promise { if (!this.isConnected) { setTimeout(() => { this.onParticipantMuted?.(userId, isMuted) }, 100) return } try { await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher) } catch { toast.push(, { placement: 'top-end', }) } } async raiseHand(sessionId: string, studentId: string, studentName: string): Promise { if (!this.isConnected) { setTimeout(() => { this.onHandRaiseReceived?.(studentId) }, 100) return } try { await this.connection.invoke('RaiseHand', sessionId, studentId, studentName) } catch { toast.push(, { placement: 'top-end', }) } } async kickParticipant(sessionId: string, participantId: string, userName: string): Promise { if (!this.isConnected) { setTimeout(() => { this.onParticipantLeft?.({ userId: participantId, sessionId, userName }) }, 100) return } try { await this.connection.invoke('KickParticipant', sessionId, participantId) } catch { toast.push(, { placement: 'top-end', }) } } async approveHandRaise(sessionId: string, studentId: string): Promise { if (!this.isConnected) { setTimeout(() => { this.onHandRaiseDismissed?.(studentId) }, 100) return } try { await this.connection.invoke('ApproveHandRaise', sessionId, studentId) } catch { toast.push(, { placement: 'top-end', }) } } async dismissHandRaise(sessionId: string, studentId: string): Promise { if (!this.isConnected) { setTimeout(() => { this.onHandRaiseDismissed?.(studentId) }, 100) return } try { await this.connection.invoke('DismissHandRaise', sessionId, studentId) } catch { toast.push(, { placement: 'top-end', }) } } async sendOffer(sessionId: string, targetUserId: string, offer: RTCSessionDescriptionInit) { if (!this.isConnected) return await this.connection.invoke('SendOffer', sessionId, targetUserId, offer) } async sendAnswer(sessionId: string, targetUserId: string, answer: RTCSessionDescriptionInit) { if (!this.isConnected) return await this.connection.invoke('SendAnswer', sessionId, targetUserId, answer) } async sendIceCandidate(sessionId: string, targetUserId: string, candidate: RTCIceCandidateInit) { if (!this.isConnected) return await this.connection.invoke('SendIceCandidate', sessionId, targetUserId, candidate) } setExistingParticipantsHandler(callback: (participants: any[]) => void) { this.connection.on('ExistingParticipants', callback) } setAttendanceUpdatedHandler(callback: (record: VideoroomAttendanceDto) => void) { this.onAttendanceUpdate = callback } setParticipantJoinHandler( callback: (userId: string, name: string, isTeacher: boolean, isActive: boolean) => void, ) { this.onParticipantJoined = callback } setParticipantLeaveHandler( callback: (payload: { userId: string; sessionId: string; userName: string }) => void, ) { this.onParticipantLeft = callback } setChatMessageReceivedHandler(callback: (message: VideoroomChatDto) => void) { this.onChatMessage = callback } setParticipantMutedHandler(callback: (userId: string, isMuted: boolean) => void) { this.onParticipantMuted = callback } setHandRaiseReceivedHandler(callback: (studentId: string) => void) { this.onHandRaiseReceived = callback } setHandRaiseDismissedHandler(callback: (studentId: string) => void) { this.onHandRaiseDismissed = callback } setOfferReceivedHandler( callback: (fromUserId: string, offer: RTCSessionDescriptionInit) => void, ) { this.onOfferReceived = callback } setAnswerReceivedHandler( callback: (fromUserId: string, answer: RTCSessionDescriptionInit) => void, ) { this.onAnswerReceived = callback } setIceCandidateReceivedHandler( callback: (fromUserId: string, candidate: RTCIceCandidateInit) => void, ) { this.onIceCandidateReceived = callback } async disconnect(): Promise { if (this.isConnected && this.currentSessionId) { try { await this.connection.invoke('LeaveClass', this.currentSessionId) } catch { toast.push(, { placement: 'top-end', }) } } if (this.connection) { await this.connection.stop() } this.isConnected = false this.currentSessionId = undefined } getConnectionState(): boolean { return this.isConnected } setForceCleanupHandler(callback: () => void) { this.onForceCleanup = callback } }