| 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' |
|
|
| |
| 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> |
| } |
|
|