Match consumer/backend so oversight realtime uses wss://host/chat instead of the default /socket.io path.
118 lines
3.4 KiB
TypeScript
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
|