ghp / packages /client /src /providers /SocketProvider.tsx
QSLY's picture
deploy: build Hugging Face Space from source
00a912e
Raw
History Blame Contribute Delete
3.35 kB
import { getSocket, type TypedSocket } from '@/lib/socket'
import { SERVER_URL } from '@/lib/config'
import { storage } from '@/lib/storage'
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { toast } from 'sonner'
import { fetchCurrentProfile } from '@/lib/profileApi'
import { SocketContext, type SocketContextValue } from '@/providers/socket-context'
/** Persistent toast id so we can dismiss it on reconnect */
const DISCONNECT_TOAST_ID = 'socket-disconnect'
export function SocketProvider({ children }: { children: ReactNode }) {
const socketRef = useRef<TypedSocket>(getSocket())
const hasDisconnectedRef = useRef(false)
const [isConnected, setIsConnected] = useState(socketRef.current.connected)
useEffect(() => {
const socket = socketRef.current
let cancelled = false
let reauthenticating = false
const onConnect = () => {
setIsConnected(true)
toast.dismiss(DISCONNECT_TOAST_ID)
if (hasDisconnectedRef.current) {
toast.success('已重新连接', { id: 'socket-reconnect' })
}
}
const onDisconnect = () => {
setIsConnected(false)
hasDisconnectedRef.current = true
toast.warning('连接已断开,正在重连…', {
id: DISCONNECT_TOAST_ID,
duration: Infinity,
})
}
const bootstrapIdentity = async (showError = true): Promise<boolean> => {
try {
const res = await fetch(`${SERVER_URL}/api/auth/identity/bootstrap`, {
method: 'POST',
credentials: 'include',
})
if (!res.ok) {
storage.clearUserId()
if (showError) toast.error('身份初始化失败,请刷新重试')
return false
}
const userId = res.headers.get('X-Identity-UserId') ?? res.headers.get('x-identity-userid')
if (userId && userId.trim().length > 0) {
await fetchCurrentProfile().catch(() => null)
} else {
storage.clearUserId()
}
return true
} catch {
storage.clearUserId()
if (showError) toast.error('连接服务器失败,请稍后重试')
return false
}
}
const ensureIdentityAndConnect = async (showError = true): Promise<void> => {
const ok = await bootstrapIdentity(showError)
if (ok && !cancelled && !socket.connected) {
socket.connect()
}
}
const onConnectError = async (err: Error) => {
if (err.message !== 'UNAUTHENTICATED') return
if (cancelled || reauthenticating) return
reauthenticating = true
try {
const ok = await bootstrapIdentity(false)
if (ok && !cancelled && !socket.connected) {
socket.connect()
}
} finally {
reauthenticating = false
}
}
socket.on('connect', onConnect)
socket.on('disconnect', onDisconnect)
socket.on('connect_error', onConnectError)
ensureIdentityAndConnect()
return () => {
cancelled = true
socket.off('connect', onConnect)
socket.off('disconnect', onDisconnect)
socket.off('connect_error', onConnectError)
}
}, [])
const value = useMemo<SocketContextValue>(() => ({ socket: socketRef.current, isConnected }), [isConnected])
return <SocketContext.Provider value={value}>{children}</SocketContext.Provider>
}