|
@@ -1,37 +1,105 @@
|
|
|
-import { getToken } from '@/utils/auth'
|
|
|
|
|
|
|
+import store from '@/store'
|
|
|
import { Message } from 'element-ui'
|
|
import { Message } from 'element-ui'
|
|
|
|
|
+import { getToken } from '@/utils/auth'
|
|
|
import { getTsbWsBind } from '@/api/tsb/ws'
|
|
import { getTsbWsBind } from '@/api/tsb/ws'
|
|
|
import { initTsbWsRouter } from '@/utils/tsbWsRouter'
|
|
import { initTsbWsRouter } from '@/utils/tsbWsRouter'
|
|
|
import {
|
|
import {
|
|
|
|
|
+ buildDeviceSession,
|
|
|
|
|
+ clearTsbDeviceSession,
|
|
|
|
|
+ hasTsbDeviceSession,
|
|
|
|
|
+ loadTsbDeviceSession,
|
|
|
|
|
+ saveTsbDeviceSession
|
|
|
|
|
+} from '@/utils/tsbDeviceSession'
|
|
|
|
|
+import {
|
|
|
initTabCoordinator,
|
|
initTabCoordinator,
|
|
|
- isLeaderTab,
|
|
|
|
|
broadcastWsMessage,
|
|
broadcastWsMessage,
|
|
|
broadcastWsStatus,
|
|
broadcastWsStatus,
|
|
|
- requestSend,
|
|
|
|
|
- requestLeaderReconnect,
|
|
|
|
|
notifyLogout,
|
|
notifyLogout,
|
|
|
- destroyTabCoordinator,
|
|
|
|
|
- reclaimLeadershipIfStale
|
|
|
|
|
|
|
+ destroyTabCoordinator
|
|
|
} from '@/utils/tsbWebSocketTab'
|
|
} from '@/utils/tsbWebSocketTab'
|
|
|
|
|
+import {
|
|
|
|
|
+ initTabRegistryLifecycle,
|
|
|
|
|
+ registerDevice,
|
|
|
|
|
+ refreshRegistryHeartbeat,
|
|
|
|
|
+ stopHeartbeat,
|
|
|
|
|
+ syncOpenedDevices,
|
|
|
|
|
+ unregisterAllForCurrentTab,
|
|
|
|
|
+ unregisterDevice
|
|
|
|
|
+} from '@/utils/tsbDeviceTabRegistry'
|
|
|
|
|
+import { ensureWorkspaceTag } from '@/utils/tsbWorkspaceNav'
|
|
|
|
|
+
|
|
|
|
|
+/** deviceSn -> 连接池条目 */
|
|
|
|
|
+const devicePool = new Map()
|
|
|
|
|
|
|
|
-let socket = null
|
|
|
|
|
-let reconnectTimer = null
|
|
|
|
|
-let manualClose = false
|
|
|
|
|
|
|
+let manualCloseAll = false
|
|
|
let messageHandler = null
|
|
let messageHandler = null
|
|
|
-let connectId = 0
|
|
|
|
|
-let reconnectAttempts = 0
|
|
|
|
|
-let reconnectExhaustedNotified = false
|
|
|
|
|
-let followerConnectedState = false
|
|
|
|
|
let tabCoordinatorInited = false
|
|
let tabCoordinatorInited = false
|
|
|
let wsRouterInited = false
|
|
let wsRouterInited = false
|
|
|
-/** 当前选中的设备 SN(仅用户点击卡片后赋值) */
|
|
|
|
|
|
|
+let sessionInitialized = false
|
|
|
let currentDeviceSn = null
|
|
let currentDeviceSn = null
|
|
|
-/** 是否由用户主动发起连接(用于控制提示与重连) */
|
|
|
|
|
let userInitiatedConnect = false
|
|
let userInitiatedConnect = false
|
|
|
|
|
|
|
|
const RECONNECT_DELAY = 3000
|
|
const RECONNECT_DELAY = 3000
|
|
|
const MAX_RECONNECT_ATTEMPTS = 3
|
|
const MAX_RECONNECT_ATTEMPTS = 3
|
|
|
|
|
|
|
|
|
|
+/** 同一设备连接中的 Promise,避免重复预检 */
|
|
|
|
|
+const pendingConnects = new Map()
|
|
|
|
|
+
|
|
|
|
|
+function createPoolEntry() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ socket: null,
|
|
|
|
|
+ connectId: 0,
|
|
|
|
|
+ reconnectTimer: null,
|
|
|
|
|
+ reconnectAttempts: 0,
|
|
|
|
|
+ reconnectExhaustedNotified: false,
|
|
|
|
|
+ manualClose: false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function poolKey(deviceSn) {
|
|
|
|
|
+ return String(deviceSn)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getPoolEntry(deviceSn) {
|
|
|
|
|
+ const key = poolKey(deviceSn)
|
|
|
|
|
+ if (!devicePool.has(key)) {
|
|
|
|
|
+ devicePool.set(key, createPoolEntry())
|
|
|
|
|
+ }
|
|
|
|
|
+ return devicePool.get(key)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function isEntryConnected(entry) {
|
|
|
|
|
+ return !!(entry && entry.socket && entry.socket.readyState === WebSocket.OPEN)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function clearEntryReconnectTimer(entry) {
|
|
|
|
|
+ if (entry.reconnectTimer) {
|
|
|
|
|
+ clearTimeout(entry.reconnectTimer)
|
|
|
|
|
+ entry.reconnectTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resetEntryReconnectAttempts(entry) {
|
|
|
|
|
+ entry.reconnectAttempts = 0
|
|
|
|
|
+ entry.reconnectExhaustedNotified = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getCurrentEntry() {
|
|
|
|
|
+ return currentDeviceSn != null ? getPoolEntry(currentDeviceSn) : null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function broadcastWsStatusForDevice(deviceSn) {
|
|
|
|
|
+ if (!isValidDeviceSn(deviceSn)) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const entry = getPoolEntry(deviceSn)
|
|
|
|
|
+ broadcastWsStatus(isEntryConnected(entry), deviceSn)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function broadcastWsStatusForCurrent() {
|
|
|
|
|
+ broadcastWsStatusForDevice(currentDeviceSn)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function ensureWsRouter() {
|
|
function ensureWsRouter() {
|
|
|
if (wsRouterInited) {
|
|
if (wsRouterInited) {
|
|
|
return
|
|
return
|
|
@@ -46,49 +114,17 @@ function ensureTabCoordinator() {
|
|
|
}
|
|
}
|
|
|
tabCoordinatorInited = true
|
|
tabCoordinatorInited = true
|
|
|
initTabCoordinator({
|
|
initTabCoordinator({
|
|
|
- onBecomeLeader: (forceReconnect) => {
|
|
|
|
|
- if (!userInitiatedConnect || !currentDeviceSn) {
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- if (forceReconnect) {
|
|
|
|
|
- leaderDisconnectSilently()
|
|
|
|
|
- resetReconnectAttempts()
|
|
|
|
|
- runPrecheckAndConnect(false)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- runPrecheckAndConnect(true)
|
|
|
|
|
- },
|
|
|
|
|
- onBecomeFollower: () => leaderDisconnectSilently(),
|
|
|
|
|
- onBroadcastMessage: (message) => messageHandler?.(message),
|
|
|
|
|
- onFollowerSend: (payload) => leaderSend(payload.cmdType, payload.data),
|
|
|
|
|
- onLeaderStatus: (connected) => {
|
|
|
|
|
- followerConnectedState = connected
|
|
|
|
|
- },
|
|
|
|
|
- onStatusRequest: () => {
|
|
|
|
|
- const connected = socket && socket.readyState === WebSocket.OPEN
|
|
|
|
|
- broadcastWsStatus(connected)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ onTabLogout: () => disconnectAllDevices(false),
|
|
|
|
|
+ onBroadcastMessage: (message) => dispatchBroadcastMessage(message)
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function resetReconnectAttempts() {
|
|
|
|
|
- reconnectAttempts = 0
|
|
|
|
|
- reconnectExhaustedNotified = false
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function buildWsUrl(token, deviceSn) {
|
|
function buildWsUrl(token, deviceSn) {
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
|
const base = process.env.VUE_APP_BASE_API || ''
|
|
const base = process.env.VUE_APP_BASE_API || ''
|
|
|
return `${protocol}//${window.location.host}${base}/websocket/tsb?token=${encodeURIComponent(token)}&deviceSn=${encodeURIComponent(deviceSn)}`
|
|
return `${protocol}//${window.location.host}${base}/websocket/tsb?token=${encodeURIComponent(token)}&deviceSn=${encodeURIComponent(deviceSn)}`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function clearReconnectTimer() {
|
|
|
|
|
- if (reconnectTimer) {
|
|
|
|
|
- clearTimeout(reconnectTimer)
|
|
|
|
|
- reconnectTimer = null
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function extractErrorMessage(err) {
|
|
function extractErrorMessage(err) {
|
|
|
if (!err) {
|
|
if (!err) {
|
|
|
return '预检失败'
|
|
return '预检失败'
|
|
@@ -99,232 +135,434 @@ function extractErrorMessage(err) {
|
|
|
return err.msg || err.message || '预检失败'
|
|
return err.msg || err.message || '预检失败'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function runPrecheck(deviceSn, silent = true) {
|
|
|
|
|
- return getTsbWsBind(deviceSn, silent).then(res => res.data)
|
|
|
|
|
|
|
+function isValidDeviceSn(deviceSn) {
|
|
|
|
|
+ return deviceSn != null && deviceSn !== ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function runPrecheckAndConnect(resetAttempts = true) {
|
|
|
|
|
- if (!isLeaderTab() || !currentDeviceSn || manualClose) {
|
|
|
|
|
- return Promise.resolve(false)
|
|
|
|
|
- }
|
|
|
|
|
- const token = getToken()
|
|
|
|
|
- if (!token) {
|
|
|
|
|
- return Promise.resolve(false)
|
|
|
|
|
|
|
+function isDeviceStillOpened(deviceSn) {
|
|
|
|
|
+ return (store.getters.tsbOpenedDevices || []).some(
|
|
|
|
|
+ d => String(d.deviceSn) === String(deviceSn)
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 无本会话:新浏览器页签从首页点卡片,须本页预检建连,禁止借用 Leader 已有连接 */
|
|
|
|
|
+function isFreshTabConnect() {
|
|
|
|
|
+ return !hasTsbDeviceSession()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function notifyPrecheckFailure(deviceSn, err, silent) {
|
|
|
|
|
+ if (silent || poolKey(deviceSn) !== poolKey(currentDeviceSn)) {
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- return runPrecheck(currentDeviceSn).then(() => {
|
|
|
|
|
- leaderConnect(resetAttempts)
|
|
|
|
|
- return true
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ Message.error(extractErrorMessage(err))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function scheduleReconnect() {
|
|
|
|
|
- if (!userInitiatedConnect || !currentDeviceSn || !isLeaderTab() || manualClose || !getToken()) {
|
|
|
|
|
|
|
+function dispatchBroadcastMessage(message) {
|
|
|
|
|
+ if (!messageHandler || !message) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
|
|
|
- if (!reconnectExhaustedNotified) {
|
|
|
|
|
- reconnectExhaustedNotified = true
|
|
|
|
|
- Message.warning(`WebSocket 自动重连已达上限(${MAX_RECONNECT_ATTEMPTS} 次),请手动重连`)
|
|
|
|
|
- }
|
|
|
|
|
- broadcastWsStatus(false)
|
|
|
|
|
|
|
+ const msgSn = message._tsbDeviceSn
|
|
|
|
|
+ const tabSn = store.getters.tsbCurrentDeviceSn || currentDeviceSn
|
|
|
|
|
+ if (msgSn != null && tabSn != null && poolKey(msgSn) !== poolKey(tabSn)) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- reconnectAttempts++
|
|
|
|
|
- clearReconnectTimer()
|
|
|
|
|
- reconnectTimer = setTimeout(() => {
|
|
|
|
|
- runPrecheck(currentDeviceSn).then(() => {
|
|
|
|
|
- leaderConnect(false)
|
|
|
|
|
- }).catch(() => {
|
|
|
|
|
- scheduleReconnect()
|
|
|
|
|
- })
|
|
|
|
|
- }, RECONNECT_DELAY)
|
|
|
|
|
|
|
+ const payload = { ...message }
|
|
|
|
|
+ delete payload._tsbDeviceSn
|
|
|
|
|
+ messageHandler(payload)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function persistDeviceSession(device) {
|
|
|
|
|
+ if (!device || device.deviceSn == null) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const prev = loadTsbDeviceSession()
|
|
|
|
|
+ const openedDevices = (prev && prev.openedDevices) || store.getters.tsbOpenedDevices || []
|
|
|
|
|
+ const devicePanelMap = store.state.tsb.devicePanelMap || (prev && prev.devicePanelMap) || {}
|
|
|
|
|
+ const session = buildDeviceSession(device, openedDevices, devicePanelMap)
|
|
|
|
|
+ saveTsbDeviceSession(session)
|
|
|
|
|
+ store.commit('tsb/SET_CURRENT_DEVICE', device)
|
|
|
|
|
+ store.commit('tsb/ADD_OPENED_DEVICE', device)
|
|
|
|
|
+ store.commit('tsb/SET_SESSION_CHECKED', true)
|
|
|
|
|
+ registerDevice(device.deviceSn)
|
|
|
|
|
+ refreshRegistryHeartbeat((store.getters.tsbOpenedDevices || []).map(d => d.deviceSn))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function restoreDeviceSession() {
|
|
|
|
|
+ const session = loadTsbDeviceSession()
|
|
|
|
|
+ if (!hasTsbDeviceSession()) {
|
|
|
|
|
+ store.commit('tsb/SET_SESSION_CHECKED', false)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ store.commit('tsb/RESTORE_SESSION', session)
|
|
|
|
|
+ currentDeviceSn = session.currentDevice.deviceSn
|
|
|
|
|
+ userInitiatedConnect = true
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function runPrecheck(deviceSn, silent = true) {
|
|
|
|
|
+ if (!isValidDeviceSn(deviceSn)) {
|
|
|
|
|
+ return Promise.reject('请指定设备')
|
|
|
|
|
+ }
|
|
|
|
|
+ return getTsbWsBind(deviceSn, silent).then(res => res.data || {})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function bindSocketEvents(ws, id) {
|
|
|
|
|
|
|
+function bindSocketEvents(ws, entry, deviceSn) {
|
|
|
|
|
+ const id = entry.connectId
|
|
|
ws.onopen = () => {
|
|
ws.onopen = () => {
|
|
|
- if (id !== connectId) {
|
|
|
|
|
|
|
+ if (id !== entry.connectId) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- clearReconnectTimer()
|
|
|
|
|
- broadcastWsStatus(true)
|
|
|
|
|
|
|
+ clearEntryReconnectTimer(entry)
|
|
|
|
|
+ broadcastWsStatusForDevice(deviceSn)
|
|
|
}
|
|
}
|
|
|
ws.onmessage = (evt) => {
|
|
ws.onmessage = (evt) => {
|
|
|
- if (id !== connectId || !messageHandler) {
|
|
|
|
|
|
|
+ if (id !== entry.connectId || !messageHandler) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
const message = JSON.parse(evt.data)
|
|
const message = JSON.parse(evt.data)
|
|
|
- messageHandler(message)
|
|
|
|
|
- broadcastWsMessage(message)
|
|
|
|
|
|
|
+ broadcastWsMessage({ ...message, _tsbDeviceSn: deviceSn })
|
|
|
|
|
+ if (poolKey(deviceSn) === poolKey(currentDeviceSn)) {
|
|
|
|
|
+ messageHandler(message)
|
|
|
|
|
+ }
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.warn('TSB WebSocket 消息解析失败', e)
|
|
console.warn('TSB WebSocket 消息解析失败', e)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
ws.onclose = () => {
|
|
ws.onclose = () => {
|
|
|
- if (id !== connectId) {
|
|
|
|
|
|
|
+ if (id !== entry.connectId) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- socket = null
|
|
|
|
|
- broadcastWsStatus(false)
|
|
|
|
|
- if (!manualClose && userInitiatedConnect && isLeaderTab() && getToken() && currentDeviceSn) {
|
|
|
|
|
- scheduleReconnect()
|
|
|
|
|
|
|
+ entry.socket = null
|
|
|
|
|
+ broadcastWsStatusForDevice(deviceSn)
|
|
|
|
|
+ if (!entry.manualClose && !manualCloseAll && userInitiatedConnect && getToken()
|
|
|
|
|
+ && isDeviceStillOpened(deviceSn)) {
|
|
|
|
|
+ scheduleReconnectForDevice(deviceSn)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
ws.onerror = () => {
|
|
ws.onerror = () => {
|
|
|
- if (id !== connectId) {
|
|
|
|
|
|
|
+ if (id !== entry.connectId) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
ws.close()
|
|
ws.close()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function leaderConnect(resetAttempts = true) {
|
|
|
|
|
|
|
+function leaderConnectDevice(deviceSn, resetAttempts = true) {
|
|
|
ensureTabCoordinator()
|
|
ensureTabCoordinator()
|
|
|
- if (!isLeaderTab() || !currentDeviceSn) {
|
|
|
|
|
|
|
+ if (!userInitiatedConnect) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
const token = getToken()
|
|
const token = getToken()
|
|
|
- if (!token || manualClose) {
|
|
|
|
|
|
|
+ if (!token || manualCloseAll) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
|
|
|
|
|
|
+ const entry = getPoolEntry(deviceSn)
|
|
|
|
|
+ if (isEntryConnected(entry) || (entry.socket && entry.socket.readyState === WebSocket.CONNECTING)) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- clearReconnectTimer()
|
|
|
|
|
|
|
+ clearEntryReconnectTimer(entry)
|
|
|
if (resetAttempts !== false) {
|
|
if (resetAttempts !== false) {
|
|
|
- resetReconnectAttempts()
|
|
|
|
|
|
|
+ resetEntryReconnectAttempts(entry)
|
|
|
}
|
|
}
|
|
|
- manualClose = false
|
|
|
|
|
- const id = ++connectId
|
|
|
|
|
- socket = new WebSocket(buildWsUrl(token, currentDeviceSn))
|
|
|
|
|
- bindSocketEvents(socket, id)
|
|
|
|
|
|
|
+ entry.manualClose = false
|
|
|
|
|
+ entry.connectId++
|
|
|
|
|
+ entry.socket = new WebSocket(buildWsUrl(token, deviceSn))
|
|
|
|
|
+ bindSocketEvents(entry.socket, entry, deviceSn)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function leaderDisconnectSilently() {
|
|
|
|
|
- manualClose = true
|
|
|
|
|
- clearReconnectTimer()
|
|
|
|
|
- connectId++
|
|
|
|
|
- if (socket) {
|
|
|
|
|
- socket.close()
|
|
|
|
|
- socket = null
|
|
|
|
|
|
|
+function applyPrecheckResultForDevice(deviceSn, bind, resetAttempts = true) {
|
|
|
|
|
+ const entry = getPoolEntry(deviceSn)
|
|
|
|
|
+ if (isEntryConnected(entry)) {
|
|
|
|
|
+ clearEntryReconnectTimer(entry)
|
|
|
|
|
+ if (resetAttempts !== false) {
|
|
|
|
|
+ resetEntryReconnectAttempts(entry)
|
|
|
|
|
+ }
|
|
|
|
|
+ broadcastWsStatusForDevice(deviceSn)
|
|
|
|
|
+ return bind
|
|
|
|
|
+ }
|
|
|
|
|
+ leaderConnectDevice(deviceSn, resetAttempts)
|
|
|
|
|
+ return bind
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function scheduleReconnectForDevice(deviceSn) {
|
|
|
|
|
+ if (!isValidDeviceSn(deviceSn) || !isDeviceStillOpened(deviceSn)
|
|
|
|
|
+ || !userInitiatedConnect || manualCloseAll || !getToken()) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const entry = getPoolEntry(deviceSn)
|
|
|
|
|
+ if (entry.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
|
|
|
+ if (!entry.reconnectExhaustedNotified) {
|
|
|
|
|
+ entry.reconnectExhaustedNotified = true
|
|
|
|
|
+ }
|
|
|
|
|
+ broadcastWsStatusForDevice(deviceSn)
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- manualClose = false
|
|
|
|
|
- broadcastWsStatus(false)
|
|
|
|
|
|
|
+ entry.reconnectAttempts++
|
|
|
|
|
+ clearEntryReconnectTimer(entry)
|
|
|
|
|
+ entry.reconnectTimer = setTimeout(() => {
|
|
|
|
|
+ if (!isDeviceStillOpened(deviceSn)) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ runPrecheck(deviceSn, true).then((bind) => {
|
|
|
|
|
+ applyPrecheckResultForDevice(deviceSn, bind, false)
|
|
|
|
|
+ }).catch((err) => {
|
|
|
|
|
+ clearEntryReconnectTimer(entry)
|
|
|
|
|
+ entry.reconnectAttempts = MAX_RECONNECT_ATTEMPTS
|
|
|
|
|
+ notifyPrecheckFailure(deviceSn, err, false)
|
|
|
|
|
+ broadcastWsStatusForDevice(deviceSn)
|
|
|
|
|
+ })
|
|
|
|
|
+ }, RECONNECT_DELAY)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function ensureDeviceConnection(deviceSn, silent = true) {
|
|
|
|
|
+ if (!isValidDeviceSn(deviceSn) || !userInitiatedConnect || manualCloseAll) {
|
|
|
|
|
+ return Promise.resolve(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!isDeviceStillOpened(deviceSn)) {
|
|
|
|
|
+ return Promise.resolve(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ const entry = getPoolEntry(deviceSn)
|
|
|
|
|
+ if (isEntryConnected(entry)) {
|
|
|
|
|
+ broadcastWsStatusForDevice(deviceSn)
|
|
|
|
|
+ return Promise.resolve(true)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!getToken()) {
|
|
|
|
|
+ return Promise.resolve(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ return runPrecheck(deviceSn, true).then((bind) => {
|
|
|
|
|
+ applyPrecheckResultForDevice(deviceSn, bind, silent !== false)
|
|
|
|
|
+ return true
|
|
|
|
|
+ }).catch((err) => {
|
|
|
|
|
+ notifyPrecheckFailure(deviceSn, err, silent)
|
|
|
|
|
+ return false
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeDeviceConnection(deviceSn, removeFromPool = true) {
|
|
|
|
|
+ const key = poolKey(deviceSn)
|
|
|
|
|
+ const entry = devicePool.get(key)
|
|
|
|
|
+ if (!entry) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ entry.manualClose = true
|
|
|
|
|
+ clearEntryReconnectTimer(entry)
|
|
|
|
|
+ entry.connectId++
|
|
|
|
|
+ if (entry.socket) {
|
|
|
|
|
+ entry.socket.close()
|
|
|
|
|
+ entry.socket = null
|
|
|
|
|
+ }
|
|
|
|
|
+ entry.manualClose = false
|
|
|
|
|
+ if (removeFromPool) {
|
|
|
|
|
+ devicePool.delete(key)
|
|
|
|
|
+ }
|
|
|
|
|
+ broadcastWsStatusForDevice(deviceSn)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function forceReconnectDevice(deviceSn) {
|
|
|
|
|
+ if (!isValidDeviceSn(deviceSn)) {
|
|
|
|
|
+ return Promise.reject('请先在首页选择设备')
|
|
|
|
|
+ }
|
|
|
|
|
+ const inPool = devicePool.has(poolKey(deviceSn))
|
|
|
|
|
+ if (!isDeviceStillOpened(deviceSn) && !inPool) {
|
|
|
|
|
+ return Promise.reject('设备已关闭,请重新选择')
|
|
|
|
|
+ }
|
|
|
|
|
+ closeDeviceConnection(deviceSn, false)
|
|
|
|
|
+ const entry = getPoolEntry(deviceSn)
|
|
|
|
|
+ resetEntryReconnectAttempts(entry)
|
|
|
|
|
+ return runPrecheck(deviceSn, true).then((bind) => {
|
|
|
|
|
+ applyPrecheckResultForDevice(deviceSn, bind, true)
|
|
|
|
|
+ if (poolKey(deviceSn) === poolKey(currentDeviceSn)) {
|
|
|
|
|
+ broadcastWsStatusForCurrent()
|
|
|
|
|
+ }
|
|
|
|
|
+ return bind
|
|
|
|
|
+ }).catch((err) => Promise.reject(extractErrorMessage(err)))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function disconnectAllDevices(logout) {
|
|
|
|
|
+ manualCloseAll = true
|
|
|
|
|
+ devicePool.forEach((entry, sn) => {
|
|
|
|
|
+ entry.manualClose = true
|
|
|
|
|
+ clearEntryReconnectTimer(entry)
|
|
|
|
|
+ entry.connectId++
|
|
|
|
|
+ if (entry.socket) {
|
|
|
|
|
+ entry.socket.close()
|
|
|
|
|
+ entry.socket = null
|
|
|
|
|
+ }
|
|
|
|
|
+ entry.manualClose = false
|
|
|
|
|
+ })
|
|
|
|
|
+ const closedSns = [...devicePool.keys()]
|
|
|
|
|
+ devicePool.clear()
|
|
|
|
|
+ closedSns.forEach((sn) => broadcastWsStatus(false, sn))
|
|
|
|
|
+ if (logout) {
|
|
|
|
|
+ currentDeviceSn = null
|
|
|
|
|
+ userInitiatedConnect = false
|
|
|
|
|
+ sessionInitialized = false
|
|
|
|
|
+ clearTsbDeviceSession()
|
|
|
|
|
+ stopHeartbeat()
|
|
|
|
|
+ unregisterAllForCurrentTab()
|
|
|
|
|
+ notifyLogout()
|
|
|
|
|
+ destroyTabCoordinator()
|
|
|
|
|
+ tabCoordinatorInited = false
|
|
|
|
|
+ wsRouterInited = false
|
|
|
|
|
+ }
|
|
|
|
|
+ manualCloseAll = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function leaderSend(cmdType, data) {
|
|
function leaderSend(cmdType, data) {
|
|
|
- if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
|
|
|
|
|
+ const entry = getCurrentEntry()
|
|
|
|
|
+ if (!isEntryConnected(entry)) {
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
- socket.send(JSON.stringify({
|
|
|
|
|
|
|
+ entry.socket.send(JSON.stringify({
|
|
|
cmdType,
|
|
cmdType,
|
|
|
data: data || {}
|
|
data: data || {}
|
|
|
}))
|
|
}))
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 初始化协调器,不自动连接 */
|
|
|
|
|
function connect() {
|
|
function connect() {
|
|
|
ensureWsRouter()
|
|
ensureWsRouter()
|
|
|
- ensureTabCoordinator()
|
|
|
|
|
- reclaimLeadershipIfStale()
|
|
|
|
|
|
|
+ if (userInitiatedConnect && hasTsbDeviceSession()) {
|
|
|
|
|
+ ensureTabCoordinator()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 登录后不自动连接 */
|
|
|
|
|
function connectAfterLogin() {
|
|
function connectAfterLogin() {
|
|
|
|
|
+ if (sessionInitialized) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ sessionInitialized = true
|
|
|
ensureWsRouter()
|
|
ensureWsRouter()
|
|
|
|
|
+ if (!restoreDeviceSession()) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ initTabRegistryLifecycle()
|
|
|
|
|
+ syncOpenedDevices(store.getters.tsbOpenedDevices || [])
|
|
|
ensureTabCoordinator()
|
|
ensureTabCoordinator()
|
|
|
|
|
+ if (!getToken()) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const opened = store.getters.tsbOpenedDevices || []
|
|
|
|
|
+ opened.forEach((device) => {
|
|
|
|
|
+ if (device && isValidDeviceSn(device.deviceSn)) {
|
|
|
|
|
+ ensureDeviceConnection(device.deviceSn, true)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ ensureWorkspaceTag()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 用户点击设备卡片后:预检通过再建立 WebSocket
|
|
|
|
|
- */
|
|
|
|
|
-function connectWithDevice(deviceSn) {
|
|
|
|
|
- if (deviceSn == null || deviceSn === '') {
|
|
|
|
|
|
|
+function switchToDevice(device) {
|
|
|
|
|
+ if (!device || !isValidDeviceSn(device.deviceSn)) {
|
|
|
return Promise.reject('请指定设备')
|
|
return Promise.reject('请指定设备')
|
|
|
}
|
|
}
|
|
|
|
|
+ const opened = store.getters.tsbOpenedDevices || []
|
|
|
|
|
+ if (!opened.some(d => String(d.deviceSn) === String(device.deviceSn))) {
|
|
|
|
|
+ return connectWithDevice(device)
|
|
|
|
|
+ }
|
|
|
ensureWsRouter()
|
|
ensureWsRouter()
|
|
|
ensureTabCoordinator()
|
|
ensureTabCoordinator()
|
|
|
- reclaimLeadershipIfStale()
|
|
|
|
|
- if (currentDeviceSn != null && currentDeviceSn !== deviceSn) {
|
|
|
|
|
- leaderDisconnectSilently()
|
|
|
|
|
- }
|
|
|
|
|
- currentDeviceSn = deviceSn
|
|
|
|
|
|
|
+ currentDeviceSn = device.deviceSn
|
|
|
userInitiatedConnect = true
|
|
userInitiatedConnect = true
|
|
|
- if (!isLeaderTab()) {
|
|
|
|
|
- return Promise.resolve()
|
|
|
|
|
|
|
+ store.commit('tsb/SET_CURRENT_DEVICE', device)
|
|
|
|
|
+ const panelMap = store.state.tsb.devicePanelMap || {}
|
|
|
|
|
+ saveTsbDeviceSession(buildDeviceSession(device, opened, panelMap))
|
|
|
|
|
+ broadcastWsStatusForCurrent()
|
|
|
|
|
+ const entry = getPoolEntry(device.deviceSn)
|
|
|
|
|
+ if (isEntryConnected(entry)) {
|
|
|
|
|
+ return Promise.resolve(device)
|
|
|
}
|
|
}
|
|
|
- return runPrecheck(deviceSn, false).then((bind) => {
|
|
|
|
|
- leaderConnect(true)
|
|
|
|
|
- return bind
|
|
|
|
|
- }).catch((err) => {
|
|
|
|
|
- return Promise.reject(extractErrorMessage(err))
|
|
|
|
|
|
|
+ return ensureDeviceConnection(device.deviceSn, true).then(() => device)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function ensureCurrentDeviceConnection() {
|
|
|
|
|
+ if (!userInitiatedConnect || !isValidDeviceSn(currentDeviceSn)) {
|
|
|
|
|
+ return Promise.resolve(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ return ensureDeviceConnection(currentDeviceSn, true)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function connectWithDevice(deviceOrSn) {
|
|
|
|
|
+ const device = typeof deviceOrSn === 'object' ? deviceOrSn : { deviceSn: deviceOrSn }
|
|
|
|
|
+ if (!isValidDeviceSn(device.deviceSn)) {
|
|
|
|
|
+ return Promise.reject('请指定设备')
|
|
|
|
|
+ }
|
|
|
|
|
+ const key = poolKey(device.deviceSn)
|
|
|
|
|
+ if (pendingConnects.has(key)) {
|
|
|
|
|
+ return pendingConnects.get(key)
|
|
|
|
|
+ }
|
|
|
|
|
+ const task = doConnectWithDevice(device).finally(() => {
|
|
|
|
|
+ pendingConnects.delete(key)
|
|
|
})
|
|
})
|
|
|
|
|
+ pendingConnects.set(key, task)
|
|
|
|
|
+ return task
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function disconnect(logout) {
|
|
|
|
|
|
|
+function doConnectWithDevice(device) {
|
|
|
|
|
+ initTabRegistryLifecycle()
|
|
|
|
|
+
|
|
|
|
|
+ ensureWsRouter()
|
|
|
ensureTabCoordinator()
|
|
ensureTabCoordinator()
|
|
|
- manualClose = true
|
|
|
|
|
- clearReconnectTimer()
|
|
|
|
|
- resetReconnectAttempts()
|
|
|
|
|
- connectId++
|
|
|
|
|
- if (socket) {
|
|
|
|
|
- socket.close()
|
|
|
|
|
- socket = null
|
|
|
|
|
- }
|
|
|
|
|
- followerConnectedState = false
|
|
|
|
|
- broadcastWsStatus(false)
|
|
|
|
|
- if (logout) {
|
|
|
|
|
- currentDeviceSn = null
|
|
|
|
|
- userInitiatedConnect = false
|
|
|
|
|
- notifyLogout()
|
|
|
|
|
- destroyTabCoordinator()
|
|
|
|
|
- tabCoordinatorInited = false
|
|
|
|
|
- wsRouterInited = false
|
|
|
|
|
- } else {
|
|
|
|
|
- manualClose = false
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const entry = getPoolEntry(device.deviceSn)
|
|
|
|
|
+ if (isEntryConnected(entry) && !isFreshTabConnect()) {
|
|
|
|
|
+ persistDeviceSession(device)
|
|
|
|
|
+ currentDeviceSn = device.deviceSn
|
|
|
|
|
+ userInitiatedConnect = true
|
|
|
|
|
+ broadcastWsStatusForCurrent()
|
|
|
|
|
+ return Promise.resolve(device)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 静默预检,错误由调用方统一提示一次(避免与 axios 拦截器重复弹窗)
|
|
|
|
|
+ return runPrecheck(device.deviceSn, true).then((bind) => {
|
|
|
|
|
+ persistDeviceSession(device)
|
|
|
|
|
+ currentDeviceSn = device.deviceSn
|
|
|
|
|
+ userInitiatedConnect = true
|
|
|
|
|
+ applyPrecheckResultForDevice(device.deviceSn, bind, true)
|
|
|
|
|
+ broadcastWsStatusForCurrent()
|
|
|
|
|
+ return bind
|
|
|
|
|
+ }).catch((err) => Promise.reject(extractErrorMessage(err)))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function disconnect(logout) {
|
|
|
|
|
+ ensureTabCoordinator()
|
|
|
|
|
+ disconnectAllDevices(logout)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function reconnect() {
|
|
function reconnect() {
|
|
|
- if (!currentDeviceSn) {
|
|
|
|
|
|
|
+ if (!userInitiatedConnect || !isValidDeviceSn(currentDeviceSn)) {
|
|
|
return Promise.reject('请先在首页选择设备')
|
|
return Promise.reject('请先在首页选择设备')
|
|
|
}
|
|
}
|
|
|
- ensureTabCoordinator()
|
|
|
|
|
- clearReconnectTimer()
|
|
|
|
|
- resetReconnectAttempts()
|
|
|
|
|
- userInitiatedConnect = true
|
|
|
|
|
- if (isLeaderTab()) {
|
|
|
|
|
- return runPrecheck(currentDeviceSn, false).then((bind) => {
|
|
|
|
|
- leaderDisconnectSilently()
|
|
|
|
|
- leaderConnect(true)
|
|
|
|
|
- return bind
|
|
|
|
|
- }).catch((err) => Promise.reject(extractErrorMessage(err)))
|
|
|
|
|
|
|
+ if (!isDeviceStillOpened(currentDeviceSn)) {
|
|
|
|
|
+ return Promise.reject('设备已关闭,请重新选择')
|
|
|
}
|
|
}
|
|
|
- requestLeaderReconnect()
|
|
|
|
|
- return Promise.resolve()
|
|
|
|
|
|
|
+ ensureTabCoordinator()
|
|
|
|
|
+ return forceReconnectDevice(currentDeviceSn)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function isReconnectExhausted() {
|
|
function isReconnectExhausted() {
|
|
|
- return reconnectAttempts >= MAX_RECONNECT_ATTEMPTS
|
|
|
|
|
|
|
+ const entry = getCurrentEntry()
|
|
|
|
|
+ return entry ? entry.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS : false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function notifyConnectSuccess() {
|
|
function notifyConnectSuccess() {
|
|
|
- resetReconnectAttempts()
|
|
|
|
|
- broadcastWsStatus(true)
|
|
|
|
|
|
|
+ const entry = getCurrentEntry()
|
|
|
|
|
+ if (entry) {
|
|
|
|
|
+ resetEntryReconnectAttempts(entry)
|
|
|
|
|
+ }
|
|
|
|
|
+ broadcastWsStatusForCurrent()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function notifyConnectFailed() {
|
|
function notifyConnectFailed() {
|
|
|
- // 鉴权失败等场景由 onclose 触发 scheduleReconnect
|
|
|
|
|
|
|
+ // 鉴权失败等场景由 onclose 触发 scheduleReconnectForDevice
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function send(cmdType, data) {
|
|
function send(cmdType, data) {
|
|
|
- ensureTabCoordinator()
|
|
|
|
|
- if (isLeaderTab()) {
|
|
|
|
|
- return leaderSend(cmdType, data)
|
|
|
|
|
|
|
+ if (!hasSelectedDevice()) {
|
|
|
|
|
+ return false
|
|
|
}
|
|
}
|
|
|
- requestSend({ cmdType, data })
|
|
|
|
|
- return true
|
|
|
|
|
|
|
+ ensureTabCoordinator()
|
|
|
|
|
+ return leaderSend(cmdType, data)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function sendPageSync(cmdType, data) {
|
|
function sendPageSync(cmdType, data) {
|
|
@@ -336,25 +574,46 @@ function onMessage(handler) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function isConnected() {
|
|
function isConnected() {
|
|
|
- if (isLeaderTab()) {
|
|
|
|
|
- return socket && socket.readyState === WebSocket.OPEN
|
|
|
|
|
- }
|
|
|
|
|
- return followerConnectedState
|
|
|
|
|
|
|
+ return isEntryConnected(getCurrentEntry())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getReconnectAttempts() {
|
|
function getReconnectAttempts() {
|
|
|
- return reconnectAttempts
|
|
|
|
|
|
|
+ const entry = getCurrentEntry()
|
|
|
|
|
+ return entry ? entry.reconnectAttempts : 0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getCurrentDeviceSn() {
|
|
function getCurrentDeviceSn() {
|
|
|
return currentDeviceSn
|
|
return currentDeviceSn
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function hasSelectedDevice() {
|
|
|
|
|
+ return userInitiatedConnect && currentDeviceSn != null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function releaseWorkspaceSession() {
|
|
|
|
|
+ const sn = currentDeviceSn
|
|
|
|
|
+ currentDeviceSn = null
|
|
|
|
|
+ userInitiatedConnect = false
|
|
|
|
|
+ stopHeartbeat()
|
|
|
|
|
+ unregisterAllForCurrentTab()
|
|
|
|
|
+ if (isValidDeviceSn(sn)) {
|
|
|
|
|
+ broadcastWsStatus(false, sn)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onDeviceTabClosed(deviceSn) {
|
|
|
|
|
+ unregisterDevice(deviceSn)
|
|
|
|
|
+ refreshRegistryHeartbeat((store.getters.tsbOpenedDevices || []).map(d => d.deviceSn))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export default {
|
|
export default {
|
|
|
connect,
|
|
connect,
|
|
|
connectAfterLogin,
|
|
connectAfterLogin,
|
|
|
connectWithDevice,
|
|
connectWithDevice,
|
|
|
|
|
+ switchToDevice,
|
|
|
|
|
+ ensureCurrentDeviceConnection,
|
|
|
disconnect,
|
|
disconnect,
|
|
|
|
|
+ disconnectDevice: closeDeviceConnection,
|
|
|
reconnect,
|
|
reconnect,
|
|
|
send,
|
|
send,
|
|
|
sendPageSync,
|
|
sendPageSync,
|
|
@@ -363,6 +622,9 @@ export default {
|
|
|
isReconnectExhausted,
|
|
isReconnectExhausted,
|
|
|
getReconnectAttempts,
|
|
getReconnectAttempts,
|
|
|
getCurrentDeviceSn,
|
|
getCurrentDeviceSn,
|
|
|
|
|
+ hasSelectedDevice,
|
|
|
|
|
+ releaseWorkspaceSession,
|
|
|
|
|
+ onDeviceTabClosed,
|
|
|
notifyConnectSuccess,
|
|
notifyConnectSuccess,
|
|
|
notifyConnectFailed
|
|
notifyConnectFailed
|
|
|
}
|
|
}
|