|
@@ -3240,6 +3240,7 @@ const messageInit = () => {
|
|
|
// y-websocket 内部重连复用同一个 doc → observe 仍有效,直接返回(避免重复 observe)
|
|
// y-websocket 内部重连复用同一个 doc → observe 仍有效,直接返回(避免重复 observe)
|
|
|
// 只有 doc 被销毁重建(full reconnect)时才全部重新绑定
|
|
// 只有 doc 被销毁重建(full reconnect)时才全部重新绑定
|
|
|
const boundDoc = (yMessage.value as any)?.doc
|
|
const boundDoc = (yMessage.value as any)?.doc
|
|
|
|
|
+ console.log('🔗 messageInit: doc.guid=', doc.guid, 'boundDoc=', boundDoc?.guid, 'same=', boundDoc === doc)
|
|
|
if (boundDoc === doc) {
|
|
if (boundDoc === doc) {
|
|
|
// 同 doc 重连:补发上线消息即可
|
|
// 同 doc 重连:补发上线消息即可
|
|
|
if (props.type === '2') {
|
|
if (props.type === '2') {
|
|
@@ -3267,9 +3268,10 @@ const messageInit = () => {
|
|
|
yMessage.value.observe((e: any) => {
|
|
yMessage.value.observe((e: any) => {
|
|
|
// 同时兼容 keysChanged 和 changes.keys(不同 Yjs 版本/事件结构)
|
|
// 同时兼容 keysChanged 和 changes.keys(不同 Yjs 版本/事件结构)
|
|
|
const changed = e.keysChanged || e.changes?.keys
|
|
const changed = e.keysChanged || e.changes?.keys
|
|
|
|
|
+ console.log('🔗 yMessage observe 触发, changed keys:', changed ? Array.from(changed) : 'null')
|
|
|
if (!changed || !changed.has('latest')) return
|
|
if (!changed || !changed.has('latest')) return
|
|
|
const message = yMessage.value!.get('latest')
|
|
const message = yMessage.value!.get('latest')
|
|
|
- console.log('yMessage', message)
|
|
|
|
|
|
|
+ console.log('🔗 yMessage latest:', message?.type, 'mId:', message?.mId, '当前 mId:', mId.value)
|
|
|
if (message && message.mId !== mId.value) {
|
|
if (message && message.mId !== mId.value) {
|
|
|
getMessages(message)
|
|
getMessages(message)
|
|
|
}
|
|
}
|
|
@@ -4077,7 +4079,10 @@ onMounted(() => {
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
|
|
|
|
|
|
// pagehide 事件(作为补充,某些浏览器中比 beforeunload 更可靠)
|
|
// pagehide 事件(作为补充,某些浏览器中比 beforeunload 更可靠)
|
|
|
- window.addEventListener('pagehide', handlePageUnload)
|
|
|
|
|
|
|
+ window.addEventListener('pagehide', () => {
|
|
|
|
|
+ console.log('pagehide')
|
|
|
|
|
+ handlePageUnload()
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
// 存储清理函数,方便在 onUnmounted 中移除
|
|
// 存储清理函数,方便在 onUnmounted 中移除
|
|
|
;(window as any).__pptistStudentUnloadHandlers = {
|
|
;(window as any).__pptistStudentUnloadHandlers = {
|
|
@@ -4252,6 +4257,7 @@ const cleanupWebSocketConnection = () => {
|
|
|
if (docSocket.value) {
|
|
if (docSocket.value) {
|
|
|
// 注意:doc 会被 provider.destroy() 自动销毁,不需要手动 destroy
|
|
// 注意:doc 会被 provider.destroy() 自动销毁,不需要手动 destroy
|
|
|
// 但如果 provider 已经被销毁,这里置空即可
|
|
// 但如果 provider 已经被销毁,这里置空即可
|
|
|
|
|
+ docSocket.value.destroy()
|
|
|
docSocket.value = null
|
|
docSocket.value = null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -4310,23 +4316,17 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
docSocket.value,
|
|
docSocket.value,
|
|
|
{
|
|
{
|
|
|
params: { yauth: authToken.value },
|
|
params: { yauth: authToken.value },
|
|
|
- // maxBackoffTime: 5000, // 重连最长等 5 秒
|
|
|
|
|
|
|
+ resyncInterval: 5000, // 每 5 秒用标准协议请求服务器状态(代替畸形心跳)
|
|
|
|
|
+ maxBackoffTime: 5000, // 重连最长退避 5 秒
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
providerSocket.value.awareness.setLocalState(null)
|
|
providerSocket.value.awareness.setLocalState(null)
|
|
|
providerSocket.value.awareness.destroy() // 完全禁用 awareness
|
|
providerSocket.value.awareness.destroy() // 完全禁用 awareness
|
|
|
- // ★ 心跳(用 providerSocket.value,断开时清除避免泄漏)
|
|
|
|
|
- if (heartbeatTimer.value) clearInterval(heartbeatTimer.value)
|
|
|
|
|
- heartbeatTimer.value = setInterval(() => {
|
|
|
|
|
- if (providerSocket.value?.ws?.readyState === WebSocket.OPEN) {
|
|
|
|
|
- try {
|
|
|
|
|
- providerSocket.value.ws.send(new Uint8Array([0, 0]))
|
|
|
|
|
- }
|
|
|
|
|
- catch (e) {
|
|
|
|
|
- console.error('heartbeatTimer', e)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }, 30000)
|
|
|
|
|
|
|
+ // 清理可能残留的旧心跳定时器
|
|
|
|
|
+ if (heartbeatTimer.value) {
|
|
|
|
|
+ clearInterval(heartbeatTimer.value)
|
|
|
|
|
+ heartbeatTimer.value = null
|
|
|
|
|
+ }
|
|
|
// 启动定期更新 token
|
|
// 启动定期更新 token
|
|
|
// 30分钟后再次更新
|
|
// 30分钟后再次更新
|
|
|
if (authTokenUpdateTimer.value) {
|
|
if (authTokenUpdateTimer.value) {
|
|
@@ -4381,10 +4381,45 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
providerSocket.value.on('connection-error', (error: any) => {
|
|
providerSocket.value.on('connection-error', (error: any) => {
|
|
|
console.error('👉 WebSocket 连接错误(将自动重连):', error)
|
|
console.error('👉 WebSocket 连接错误(将自动重连):', error)
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
providerSocket.value.on('connection-close', (event: any) => {
|
|
providerSocket.value.on('connection-close', (event: any) => {
|
|
|
console.log('👉 WebSocket 连接关闭(将自动重连):', event)
|
|
console.log('👉 WebSocket 连接关闭(将自动重连):', event)
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+ // ★ synced 事件:在服务器状态同步完成(sync step 2 已应用到 Y.Doc)后触发
|
|
|
|
|
+ // 'connected' 在 onopen 时就触发,此时 sync 尚未完成,messageInit 的 catch-up 读不到数据
|
|
|
|
|
+ // 'synced' 才是做 catch-up 的正确时机——确保服务器上的 latest 已写入本地 Y.Doc
|
|
|
|
|
+ providerSocket.value.on('sync', (isSynced: boolean) => {
|
|
|
|
|
+ if (!isSynced) return
|
|
|
|
|
+ console.log('🔗 Yjs sync 完成,开始 catch-up')
|
|
|
|
|
+
|
|
|
|
|
+ // catch-up:读取服务器同步过来的 latest 消息
|
|
|
|
|
+ if (yMessage.value) {
|
|
|
|
|
+ const latest = yMessage.value.get('latest')
|
|
|
|
|
+ console.log('🔗 synced catch-up latest:', latest)
|
|
|
|
|
+ if (latest && latest.mId !== mId.value) {
|
|
|
|
|
+ console.log('🔗 synced catch-up: 处理 latest 消息', latest.type)
|
|
|
|
|
+ getMessages(latest)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // catch-up:读取计时器、激光笔、画图状态
|
|
|
|
|
+ if (yTimerState.value) {
|
|
|
|
|
+ const snapshot = yTimerState.value.toJSON()
|
|
|
|
|
+ applyTimerStateSnapshot(snapshot)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (yLaserState.value) {
|
|
|
|
|
+ const laserSnap = yLaserState.value.toJSON()
|
|
|
|
|
+ if (laserSnap.visible !== undefined) {
|
|
|
|
|
+ laserPenOverlay.value.visible = !!laserSnap.visible
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 学生端补发上线消息,让老师知道学生已就绪
|
|
|
|
|
+ if (props.type === '2') {
|
|
|
|
|
+ sendMessage({ type: 'class_begin', courseid: props.courseid })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
// 监听连接错误
|
|
// 监听连接错误
|
|
|
// providerSocket.value.on('connection-error', (error: any) => {
|
|
// providerSocket.value.on('connection-error', (error: any) => {
|
|
|
// console.error('👉 WebSocket连接错误:', error)
|
|
// console.error('👉 WebSocket连接错误:', error)
|