diff --git a/src/contexts/NotificationContext.js b/src/contexts/NotificationContext.js index ac56a318..8254a29d 100644 --- a/src/contexts/NotificationContext.js +++ b/src/contexts/NotificationContext.js @@ -59,6 +59,11 @@ export const NotificationProvider = ({ children }) => { const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏 const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器 + // ⚡ 方案2: 使用 Ref 存储最新的回调函数引用(避免闭包陷阱) + const addNotificationRef = useRef(null); + const adaptEventToNotificationRef = useRef(null); + const isFirstConnect = useRef(true); // 标记是否首次连接 + // ⚡ 使用权限引导管理 Hook const { shouldShowGuide, markGuideAsShown } = usePermissionGuide(); @@ -595,26 +600,42 @@ export const NotificationProvider = ({ children }) => { return newNotification.id; }, [notifications, toast, sendBrowserNotification, addWebNotification, browserPermission, hasRequestedPermission, requestBrowserPermission]); - // 连接到 Socket 服务 + /** + * ✅ 方案2: 同步最新的回调函数到 Ref + * 确保 Socket 监听器始终使用最新的函数引用(避免闭包陷阱) + */ + useEffect(() => { + addNotificationRef.current = addNotification; + console.log('[NotificationContext] 📝 已更新 addNotificationRef'); + }, [addNotification]); + + useEffect(() => { + adaptEventToNotificationRef.current = adaptEventToNotification; + console.log('[NotificationContext] 📝 已更新 adaptEventToNotificationRef'); + }, [adaptEventToNotification]); + + + // ========== 连接到 Socket 服务(⚡ 方案2: 只执行一次) ========== useEffect(() => { logger.info('NotificationContext', 'Initializing socket connection...'); - console.log('%c[NotificationContext] Initializing socket connection', 'color: #673AB7; font-weight: bold;'); + console.log('%c[NotificationContext] 🚀 初始化 Socket 连接(方案2:只注册一次)', 'color: #673AB7; font-weight: bold;'); - // ✅ 第一步: 注册所有事件监听器 - console.log('%c[NotificationContext] Step 1: Registering event listeners...', 'color: #673AB7;'); - - // 监听连接状态 + // ========== 监听连接成功(首次连接 + 重连) ========== socket.on('connect', () => { - const wasDisconnected = connectionStatus !== CONNECTION_STATUS.CONNECTED; setIsConnected(true); setReconnectAttempt(0); - logger.info('NotificationContext', 'Socket connected', { wasDisconnected }); - console.log('%c[NotificationContext] ✅ Received connect event, updating state to connected', 'color: #4CAF50; font-weight: bold;'); - // 如果之前断开过,显示 RECONNECTED 状态2秒后自动消失 - if (wasDisconnected) { + // 判断是首次连接还是重连 + if (isFirstConnect.current) { + console.log('%c[NotificationContext] ✅ 首次连接成功', 'color: #4CAF50; font-weight: bold;'); + console.log('[NotificationContext] Socket ID:', socket.getSocketId?.()); + setConnectionStatus(CONNECTION_STATUS.CONNECTED); + isFirstConnect.current = false; + logger.info('NotificationContext', 'Socket connected (first time)'); + } else { + console.log('%c[NotificationContext] 🔄 重连成功!', 'color: #FF9800; font-weight: bold;'); setConnectionStatus(CONNECTION_STATUS.RECONNECTED); - logger.info('NotificationContext', 'Reconnected, will auto-dismiss in 2s'); + logger.info('NotificationContext', 'Socket reconnected'); // 清除之前的定时器 if (reconnectedTimerRef.current) { @@ -626,12 +647,10 @@ export const NotificationProvider = ({ children }) => { setConnectionStatus(CONNECTION_STATUS.CONNECTED); logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status'); }, 2000); - } else { - setConnectionStatus(CONNECTION_STATUS.CONNECTED); } - // 订阅事件推送 - console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;'); + // ⚡ 重连后只需重新订阅,不需要重新注册监听器 + console.log('%c[NotificationContext] 🔔 重新订阅事件推送...', 'color: #FF9800; font-weight: bold;'); if (socket.subscribeToEvents) { socket.subscribeToEvents({ @@ -642,45 +661,47 @@ export const NotificationProvider = ({ children }) => { console.log('[NotificationContext] 订阅确认:', data); logger.info('NotificationContext', 'Events subscribed', data); }, - // ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听 }); } else { console.error('[NotificationContext] ❌ socket.subscribeToEvents 方法不可用'); } }); + // ========== 监听断开连接 ========== socket.on('disconnect', (reason) => { setIsConnected(false); setConnectionStatus(CONNECTION_STATUS.DISCONNECTED); logger.warn('NotificationContext', 'Socket disconnected', { reason }); + console.log('%c[NotificationContext] ⚠️ Socket 已断开', 'color: #FF5722;', { reason }); }); - // 监听连接错误 + // ========== 监听连接错误 ========== socket.on('connect_error', (error) => { logger.error('NotificationContext', 'Socket connect_error', error); setConnectionStatus(CONNECTION_STATUS.RECONNECTING); - // 获取重连次数 const attempts = socket.getReconnectAttempts?.() || 0; setReconnectAttempt(attempts); logger.info('NotificationContext', 'Reconnection attempt', { attempts }); + console.log(`%c[NotificationContext] 🔄 重连中... (第 ${attempts} 次尝试)`, 'color: #FF9800;'); }); - // 监听重连失败 + // ========== 监听重连失败 ========== socket.on('reconnect_failed', () => { logger.error('NotificationContext', 'Socket reconnect_failed'); setConnectionStatus(CONNECTION_STATUS.FAILED); + console.error('[NotificationContext] ❌ 重连失败'); toast({ title: '连接失败', description: '无法连接到服务器,请检查网络连接', status: 'error', - duration: null, // 不自动关闭 + duration: null, isClosable: true, }); }); - // 监听新事件推送(统一事件名) + // ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ========== socket.on('new_event', (data) => { console.log('\n%c════════════════════════════════════════', 'color: #FF9800; font-weight: bold;'); console.log('%c[NotificationContext] 📨 收到 new_event 事件!', 'color: #FF9800; font-weight: bold;'); @@ -693,17 +714,24 @@ export const NotificationProvider = ({ children }) => { logger.info('NotificationContext', 'Received new event', data); - // ========== Socket层去重检查 ========== - // 生成更健壮的事件ID - const eventId = data.id || - `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + // ⚠️ 防御性检查:确保 ref 已初始化 + if (!addNotificationRef.current || !adaptEventToNotificationRef.current) { + console.error('%c[NotificationContext] ❌ Ref 未初始化,跳过处理', 'color: #F44336; font-weight: bold;'); + logger.error('NotificationContext', 'Refs not initialized', { + addNotificationRef: !!addNotificationRef.current, + adaptEventToNotificationRef: !!adaptEventToNotificationRef.current, + }); + return; + } + + // ========== Socket层去重检查 ========== + const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - // 如果缺少原始ID,记录警告 if (!data.id) { logger.warn('NotificationContext', 'Event missing ID, generated fallback', { eventId, eventType: data.type, - title: data.title + title: data.title, }); } @@ -711,55 +739,61 @@ export const NotificationProvider = ({ children }) => { logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId }); console.warn('[NotificationContext] ⚠️ 重复事件,已忽略:', eventId); console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;'); - return; // 重复事件,直接忽略 + return; } - // 记录已处理的事件ID processedEventIds.current.add(eventId); console.log('[NotificationContext] ✓ 事件已记录,防止重复处理'); - // 限制Set大小,避免内存泄漏 + // 限制 Set 大小,避免内存泄漏 if (processedEventIds.current.size > MAX_PROCESSED_IDS) { const idsArray = Array.from(processedEventIds.current); processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS)); logger.debug('NotificationContext', 'Cleaned up old processed event IDs', { - kept: MAX_PROCESSED_IDS + kept: MAX_PROCESSED_IDS, }); } // ========== Socket层去重检查结束 ========== - // 使用适配器转换事件格式 + // ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱) console.log('[NotificationContext] 正在转换事件格式...'); - const notification = adaptEventToNotification(data); + const notification = adaptEventToNotificationRef.current(data); console.log('[NotificationContext] 转换后的通知对象:', notification); + // ✅ 使用 ref.current 访问最新的 addNotification 函数 console.log('[NotificationContext] 准备添加通知到队列...'); - addNotification(notification); + addNotificationRef.current(notification); console.log('[NotificationContext] ✅ 通知已添加到队列'); console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;'); }); - // 保留系统通知监听(兼容性) + // ========== 监听系统通知(兼容性) ========== socket.on('system_notification', (data) => { logger.info('NotificationContext', 'Received system notification', data); - addNotification(data); + console.log('[NotificationContext] 📢 收到系统通知:', data); + + if (addNotificationRef.current) { + addNotificationRef.current(data); + } else { + console.error('[NotificationContext] ❌ addNotificationRef 未初始化'); + } }); - console.log('%c[NotificationContext] ✅ All event listeners registered', 'color: #4CAF50; font-weight: bold;'); + console.log('%c[NotificationContext] ✅ 所有监听器已注册(只注册一次)', 'color: #4CAF50; font-weight: bold;'); - // ✅ 第二步: 获取最大重连次数 + // ========== 获取最大重连次数 ========== const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity; setMaxReconnectAttempts(maxAttempts); logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts }); - // ✅ 第三步: 调用 socket.connect() - console.log('%c[NotificationContext] Step 2: Calling socket.connect()...', 'color: #673AB7; font-weight: bold;'); + // ========== 启动连接 ========== + console.log('%c[NotificationContext] 🔌 调用 socket.connect()...', 'color: #673AB7; font-weight: bold;'); socket.connect(); - console.log('%c[NotificationContext] socket.connect() completed', 'color: #673AB7;'); - // 清理函数 + // ========== 清理函数(组件卸载时) ========== return () => { logger.info('NotificationContext', 'Cleaning up socket connection'); + console.log('%c[NotificationContext] 🧹 清理 Socket 连接', 'color: #9E9E9E;'); // 清理 reconnected 状态定时器 if (reconnectedTimerRef.current) { @@ -774,15 +808,20 @@ export const NotificationProvider = ({ children }) => { }); notificationTimers.current.clear(); + // 移除所有事件监听器 socket.off('connect'); socket.off('disconnect'); socket.off('connect_error'); socket.off('reconnect_failed'); socket.off('new_event'); socket.off('system_notification'); + + // 断开连接 socket.disconnect(); + + console.log('%c[NotificationContext] ✅ 清理完成', 'color: #4CAF50;'); }; - }, []); // ✅ 空依赖数组,确保只执行一次,避免 React 严格模式重复执行 + }, []); // ⚠️ 空依赖数组,确保只执行一次 // ==================== 智能自动重试 ====================