admin/hooks/useAdminChatSocket.ts
alisaza c2bc256103 fix(chat): use Engine.IO path /chat for admin oversight socket
Match consumer/backend so oversight realtime uses wss://host/chat instead
of the default /socket.io path.
2026-09-07 21:49:25 +03:30

118 lines
3.4 KiB
TypeScript

'use client'
import { useEffect, useRef, useState } from 'react'
import { io } from 'socket.io-client'
import { CHAT_SOCKET_NAMESPACE, CHAT_SOCKET_PATH, CHAT_SOCKET_URL } from '@/config/chatSocket'
import useAuth from '@/hooks/useAuth'
import { reportBrowserEvent } from '@/lib/observability/client'
import type { AdminChatMessage } from '@/services/adminChat'
type OversightSocketStatus = 'connecting' | 'connected' | 'error'
interface OversightAck {
ok: boolean
error?: string
conversationId?: string
}
const useAdminChatSocket = (conversationId: string, onMessage: (message: AdminChatMessage) => void) => {
const { user } = useAuth()
const onMessageRef = useRef(onMessage)
const [status, setStatus] = useState<OversightSocketStatus>('connecting')
const [error, setError] = useState<string | null>(null)
useEffect(() => {
onMessageRef.current = onMessage
}, [onMessage])
useEffect(() => {
if (!conversationId || !user?.accessToken) return
let disposed = false
let joined = false
let disconnected = false
let disconnectTimer: ReturnType<typeof setTimeout> | undefined
let retryTimer: ReturnType<typeof setTimeout> | undefined
const socket = io(`${CHAT_SOCKET_URL}${CHAT_SOCKET_NAMESPACE}`, {
path: CHAT_SOCKET_PATH,
transports: ['websocket'],
autoConnect: true,
auth: { token: user.accessToken },
})
const disconnect = () => {
if (disconnected) return
disconnected = true
if (disconnectTimer) clearTimeout(disconnectTimer)
if (retryTimer) clearTimeout(retryTimer)
socket.removeAllListeners()
socket.disconnect()
}
const join = (attempt = 1) => {
if (disposed) return
setStatus('connecting')
socket.timeout(5000).emit('admin:conversation:join', { conversationId }, (timeoutError: Error | null, ack?: OversightAck) => {
if (disposed) return
if (timeoutError || !ack?.ok) {
const reason = timeoutError ? 'ACK_TIMEOUT' : (ack?.error ?? 'UNKNOWN')
if (attempt === 1 && (timeoutError || reason === 'UNAUTHORIZED')) {
retryTimer = setTimeout(() => {
join(2)
}, 400)
return
}
setStatus('error')
setError('اتصال زنده به گفتگو برقرار نشد.')
reportBrowserEvent({
name: 'admin_chat_socket_join_failure',
level: 'warning',
attributes: { error: reason },
})
disconnect()
return
}
joined = true
setError(null)
setStatus('connected')
})
}
socket.on('connect', join)
socket.on('connect_error', () => {
if (disposed) return
setStatus('error')
setError('ارتباط زنده با سرور برقرار نشد.')
})
socket.on('message:new', (payload: { message?: AdminChatMessage }) => {
if (disposed || payload.message?.conversationId !== conversationId) return
onMessageRef.current(payload.message)
})
return () => {
disposed = true
if (!joined || !socket.connected) {
disconnect()
return
}
const finishDisconnect = () => {
disconnect()
}
disconnectTimer = setTimeout(finishDisconnect, 800)
socket.timeout(750).emit('admin:conversation:leave', { conversationId }, finishDisconnect)
}
}, [conversationId, user?.accessToken])
return { status, error }
}
export default useAdminChatSocket