|
|
@@ -1,83 +1,303 @@
|
|
|
/**
|
|
|
- * SSE工具函数
|
|
|
+ * SSE工具函数(支持自动重连)
|
|
|
*/
|
|
|
|
|
|
import { getToken, getTokenType } from './auth.js'
|
|
|
|
|
|
/**
|
|
|
- * 创建SSE连接
|
|
|
- * @param {string} url - SSE URL
|
|
|
- * @param {Object} handlers - 事件处理器
|
|
|
- * @param {Function} handlers.onMessage - 接收消息时的回调
|
|
|
- * @param {Function} handlers.onError - 错误回调
|
|
|
- * @param {Function} handlers.onComplete - 完成回调
|
|
|
- * @returns {EventSource} EventSource实例
|
|
|
+ * SSE连接管理器类(支持自动重连)
|
|
|
*/
|
|
|
-export const createSSEConnection = (url, handlers = {}) => {
|
|
|
- // EventSource 不支持自定义请求头,需要通过 URL 参数传递 Token
|
|
|
- const token = getToken()
|
|
|
- const tokenType = getTokenType()
|
|
|
-
|
|
|
- if (token && tokenType) {
|
|
|
- const urlObj = new URL(url, window.location.origin)
|
|
|
- // 将 Token 作为 URL 参数传递(后端需要支持从 URL 参数读取 Token)
|
|
|
- urlObj.searchParams.set('token', token)
|
|
|
- url = urlObj.toString()
|
|
|
-
|
|
|
- console.log('🔐 SSE 连接已添加认证 Token(通过 URL 参数)')
|
|
|
- } else {
|
|
|
- console.warn('⚠️ SSE 连接未找到 Token,可能会导致认证失败')
|
|
|
+class SSEConnectionManager {
|
|
|
+ constructor(url, handlers = {}, options = {}) {
|
|
|
+ this.originalUrl = url
|
|
|
+ this.handlers = handlers
|
|
|
+ this.options = {
|
|
|
+ maxRetries: options.maxRetries || 5, // 最大重试次数
|
|
|
+ retryDelay: options.retryDelay || 1000, // 初始重试延迟(毫秒)
|
|
|
+ maxRetryDelay: options.maxRetryDelay || 30000, // 最大重试延迟(毫秒)
|
|
|
+ enableAutoReconnect: options.enableAutoReconnect !== false, // 是否启用自动重连,默认true
|
|
|
+ ...options
|
|
|
+ }
|
|
|
+
|
|
|
+ this.eventSource = null
|
|
|
+ this.retryCount = 0
|
|
|
+ this.retryTimer = null
|
|
|
+ this.isManualClose = false // 是否手动关闭
|
|
|
+ this.isCompleted = false // 是否已完成(收到completed消息)
|
|
|
+ this.lastMessageTime = Date.now() // 最后一次收到消息的时间
|
|
|
+ this.heartbeatTimer = null // 心跳检测定时器
|
|
|
+
|
|
|
+ this.connect()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 建立SSE连接
|
|
|
+ */
|
|
|
+ connect() {
|
|
|
+ // 如果手动关闭或已完成,不再重连
|
|
|
+ if (this.isManualClose || this.isCompleted) {
|
|
|
+ console.log('🚫 SSE连接已手动关闭或已完成,不再重连')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加Token到URL
|
|
|
+ let url = this.originalUrl
|
|
|
+ const token = getToken()
|
|
|
+ const tokenType = getTokenType()
|
|
|
+
|
|
|
+ if (token && tokenType) {
|
|
|
+ const urlObj = new URL(url, window.location.origin)
|
|
|
+ urlObj.searchParams.set('token', token)
|
|
|
+ url = urlObj.toString()
|
|
|
+ console.log('🔐 SSE 连接已添加认证 Token(通过 URL 参数)')
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ SSE 连接未找到 Token,可能会导致认证失败')
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.eventSource = new EventSource(url)
|
|
|
+ console.log(`🔌 SSE连接已建立 (重试次数: ${this.retryCount}/${this.options.maxRetries})`)
|
|
|
+
|
|
|
+ // 绑定事件处理器
|
|
|
+ this.eventSource.onmessage = this.handleMessage.bind(this)
|
|
|
+ this.eventSource.onerror = this.handleError.bind(this)
|
|
|
+ this.eventSource.onopen = this.handleOpen.bind(this)
|
|
|
+
|
|
|
+ // 启动心跳检测(可选)
|
|
|
+ this.startHeartbeat()
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 创建SSE连接失败:', error)
|
|
|
+ this.scheduleReconnect()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理连接打开事件
|
|
|
+ */
|
|
|
+ handleOpen() {
|
|
|
+ console.log('✅ SSE连接已打开')
|
|
|
+ this.retryCount = 0 // 重置重试计数
|
|
|
+ this.lastMessageTime = Date.now()
|
|
|
+
|
|
|
+ // 调用用户的onOpen回调(如果有)
|
|
|
+ if (this.handlers.onOpen) {
|
|
|
+ this.handlers.onOpen()
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- const eventSource = new EventSource(url)
|
|
|
-
|
|
|
- eventSource.onmessage = (event) => {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理消息事件
|
|
|
+ */
|
|
|
+ handleMessage(event) {
|
|
|
+ this.lastMessageTime = Date.now()
|
|
|
+
|
|
|
try {
|
|
|
const data = JSON.parse(event.data)
|
|
|
-
|
|
|
+
|
|
|
// 根据消息类型分发
|
|
|
if (data.type === 'completed') {
|
|
|
- handlers.onComplete && handlers.onComplete(data)
|
|
|
- eventSource.close()
|
|
|
+ console.log('✅ SSE流程已完成')
|
|
|
+ this.isCompleted = true
|
|
|
+ this.handlers.onComplete && this.handlers.onComplete(data)
|
|
|
+ this.close()
|
|
|
} else if (data.type === 'interrupted') {
|
|
|
- handlers.onInterrupted && handlers.onInterrupted(data)
|
|
|
- eventSource.close()
|
|
|
+ console.log('⚠️ SSE流程被中断')
|
|
|
+ this.isCompleted = true
|
|
|
+ this.handlers.onInterrupted && this.handlers.onInterrupted(data)
|
|
|
+ this.close()
|
|
|
} else if (data.type === 'error') {
|
|
|
- handlers.onError && handlers.onError(new Error(data.message))
|
|
|
- eventSource.close()
|
|
|
+ console.error('❌ SSE返回错误:', data.message)
|
|
|
+ this.handlers.onError && this.handlers.onError(new Error(data.message))
|
|
|
+ this.close()
|
|
|
} else {
|
|
|
- handlers.onMessage && handlers.onMessage(data)
|
|
|
+ // 普通消息
|
|
|
+ this.handlers.onMessage && this.handlers.onMessage(data)
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('解析SSE消息失败:', error)
|
|
|
- handlers.onError && handlers.onError(error)
|
|
|
+ console.error('❌ 解析SSE消息失败:', error)
|
|
|
+ this.handlers.onError && this.handlers.onError(error)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- eventSource.onerror = (error) => {
|
|
|
- console.error('SSE连接错误:', error)
|
|
|
- console.error('EventSource readyState:', eventSource.readyState)
|
|
|
- console.error('EventSource url:', eventSource.url)
|
|
|
-
|
|
|
- // 创建更详细的错误信息
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理错误事件
|
|
|
+ */
|
|
|
+ handleError(error) {
|
|
|
+ console.error('❌ SSE连接错误:', error)
|
|
|
+
|
|
|
+ if (this.eventSource) {
|
|
|
+ console.error('EventSource readyState:', this.eventSource.readyState)
|
|
|
+ console.error('EventSource url:', this.eventSource.url)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是手动关闭或已完成,不处理错误
|
|
|
+ if (this.isManualClose || this.isCompleted) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建详细的错误信息
|
|
|
const detailedError = new Error(
|
|
|
- `SSE连接失败 (状态: ${eventSource.readyState === 0 ? '连接中' : eventSource.readyState === 1 ? '已连接' : '已关闭'})`
|
|
|
+ `SSE连接失败 (状态: ${this.eventSource?.readyState === 0 ? '连接中' : this.eventSource?.readyState === 1 ? '已连接' : '已关闭'})`
|
|
|
)
|
|
|
-
|
|
|
- handlers.onError && handlers.onError(detailedError)
|
|
|
- eventSource.close()
|
|
|
+
|
|
|
+ // 调用错误回调
|
|
|
+ this.handlers.onError && this.handlers.onError(detailedError)
|
|
|
+
|
|
|
+ // 关闭当前连接
|
|
|
+ if (this.eventSource) {
|
|
|
+ this.eventSource.close()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试重连
|
|
|
+ this.scheduleReconnect()
|
|
|
}
|
|
|
-
|
|
|
- return eventSource
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安排重连
|
|
|
+ */
|
|
|
+ scheduleReconnect() {
|
|
|
+ // 如果禁用自动重连,直接返回
|
|
|
+ if (!this.options.enableAutoReconnect) {
|
|
|
+ console.log('🚫 自动重连已禁用')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果手动关闭或已完成,不重连
|
|
|
+ if (this.isManualClose || this.isCompleted) {
|
|
|
+ console.log('🚫 连接已手动关闭或已完成,不再重连')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否超过最大重试次数
|
|
|
+ if (this.retryCount >= this.options.maxRetries) {
|
|
|
+ console.error(`❌ 已达到最大重试次数 (${this.options.maxRetries}),停止重连`)
|
|
|
+ this.handlers.onMaxRetriesReached && this.handlers.onMaxRetriesReached()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算重试延迟(指数退避)
|
|
|
+ const delay = Math.min(
|
|
|
+ this.options.retryDelay * Math.pow(2, this.retryCount),
|
|
|
+ this.options.maxRetryDelay
|
|
|
+ )
|
|
|
+
|
|
|
+ this.retryCount++
|
|
|
+ console.log(`🔄 将在 ${delay}ms 后进行第 ${this.retryCount} 次重连...`)
|
|
|
+
|
|
|
+ // 调用重连前回调
|
|
|
+ if (this.handlers.onReconnecting) {
|
|
|
+ this.handlers.onReconnecting(this.retryCount, delay)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除之前的重试定时器
|
|
|
+ if (this.retryTimer) {
|
|
|
+ clearTimeout(this.retryTimer)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置重连定时器
|
|
|
+ this.retryTimer = setTimeout(() => {
|
|
|
+ this.connect()
|
|
|
+ }, delay)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动心跳检测(可选)
|
|
|
+ */
|
|
|
+ startHeartbeat() {
|
|
|
+ // 清除之前的心跳定时器
|
|
|
+ if (this.heartbeatTimer) {
|
|
|
+ clearInterval(this.heartbeatTimer)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果配置了心跳间隔,启动心跳检测
|
|
|
+ if (this.options.heartbeatInterval) {
|
|
|
+ this.heartbeatTimer = setInterval(() => {
|
|
|
+ const timeSinceLastMessage = Date.now() - this.lastMessageTime
|
|
|
+
|
|
|
+ // 如果超过心跳间隔的2倍没有收到消息,认为连接可能断开
|
|
|
+ if (timeSinceLastMessage > this.options.heartbeatInterval * 2) {
|
|
|
+ console.warn('⚠️ 心跳检测:长时间未收到消息,可能连接已断开')
|
|
|
+ // 可以选择主动关闭并重连
|
|
|
+ if (this.eventSource && this.eventSource.readyState === EventSource.OPEN) {
|
|
|
+ this.eventSource.close()
|
|
|
+ this.scheduleReconnect()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, this.options.heartbeatInterval)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手动关闭连接
|
|
|
+ */
|
|
|
+ close() {
|
|
|
+ console.log('🔌 手动关闭SSE连接')
|
|
|
+ this.isManualClose = true
|
|
|
+
|
|
|
+ // 清除重试定时器
|
|
|
+ if (this.retryTimer) {
|
|
|
+ clearTimeout(this.retryTimer)
|
|
|
+ this.retryTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除心跳定时器
|
|
|
+ if (this.heartbeatTimer) {
|
|
|
+ clearInterval(this.heartbeatTimer)
|
|
|
+ this.heartbeatTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭EventSource
|
|
|
+ if (this.eventSource && this.eventSource.readyState !== EventSource.CLOSED) {
|
|
|
+ this.eventSource.close()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前连接状态
|
|
|
+ */
|
|
|
+ getState() {
|
|
|
+ return {
|
|
|
+ readyState: this.eventSource?.readyState,
|
|
|
+ retryCount: this.retryCount,
|
|
|
+ isManualClose: this.isManualClose,
|
|
|
+ isCompleted: this.isCompleted
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 创建SSE连接(带自动重连功能)
|
|
|
+ * @param {string} url - SSE URL
|
|
|
+ * @param {Object} handlers - 事件处理器
|
|
|
+ * @param {Function} handlers.onMessage - 接收消息时的回调
|
|
|
+ * @param {Function} handlers.onError - 错误回调
|
|
|
+ * @param {Function} handlers.onComplete - 完成回调
|
|
|
+ * @param {Function} handlers.onInterrupted - 中断回调
|
|
|
+ * @param {Function} handlers.onOpen - 连接打开回调
|
|
|
+ * @param {Function} handlers.onReconnecting - 重连中回调
|
|
|
+ * @param {Function} handlers.onMaxRetriesReached - 达到最大重试次数回调
|
|
|
+ * @param {Object} options - 配置选项
|
|
|
+ * @param {number} options.maxRetries - 最大重试次数,默认5
|
|
|
+ * @param {number} options.retryDelay - 初始重试延迟(毫秒),默认1000
|
|
|
+ * @param {number} options.maxRetryDelay - 最大重试延迟(毫秒),默认30000
|
|
|
+ * @param {boolean} options.enableAutoReconnect - 是否启用自动重连,默认true
|
|
|
+ * @param {number} options.heartbeatInterval - 心跳检测间隔(毫秒),可选
|
|
|
+ * @returns {SSEConnectionManager} SSE连接管理器实例
|
|
|
+ */
|
|
|
+export const createSSEConnection = (url, handlers = {}, options = {}) => {
|
|
|
+ return new SSEConnectionManager(url, handlers, options)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 关闭SSE连接
|
|
|
- * @param {EventSource} eventSource - EventSource实例
|
|
|
+ * @param {SSEConnectionManager|EventSource} connection - SSE连接管理器或EventSource实例
|
|
|
*/
|
|
|
-export const closeSSEConnection = (eventSource) => {
|
|
|
- if (eventSource && eventSource.readyState !== EventSource.CLOSED) {
|
|
|
- eventSource.close()
|
|
|
+export const closeSSEConnection = (connection) => {
|
|
|
+ if (connection instanceof SSEConnectionManager) {
|
|
|
+ connection.close()
|
|
|
+ } else if (connection && connection.readyState !== EventSource.CLOSED) {
|
|
|
+ connection.close()
|
|
|
}
|
|
|
}
|
|
|
|