|
|
@@ -1517,12 +1517,8 @@ const clearAllSyncStates = () => {
|
|
|
if (props.type == '1' && isCreator.value && docSocket.value) {
|
|
|
console.log('🧹 创建老师退出,清空所有同步状态')
|
|
|
docSocket.value.transact(() => {
|
|
|
- // 清空普通消息(Map 结构,messageV2 避免与旧 Array('message') 根类型冲突)
|
|
|
- const messageMap = docSocket.value?.getMap?.('messageV2')
|
|
|
- if (messageMap) {
|
|
|
- messageMap.delete('latest')
|
|
|
- }
|
|
|
- // 清空计时器消息数组
|
|
|
+ // ★ 不删除 latest!delete 会传播给学生导致 get('latest') 返回 undefined
|
|
|
+ // 旧 latest 会被 sendMessage 的 set('latest', message) 自然覆盖
|
|
|
const timerMessagesArray = docSocket.value?.getArray?.('timerMessages')
|
|
|
if (timerMessagesArray) {
|
|
|
timerMessagesArray.delete(0, timerMessagesArray.length)
|
|
|
@@ -3262,16 +3258,23 @@ const messageInit = () => {
|
|
|
yIsResultArrayMessages.value = null
|
|
|
|
|
|
// 普通消息(Map,只保留最后一条)
|
|
|
- // 必须使用 messageV2:服务器上旧的 message 是 Array 根类型,
|
|
|
- // 同名再 getMap 会导致根类型冲突、后注册的被改名,observe 间歇性失效
|
|
|
- yMessage.value = doc.getMap('messageV2')
|
|
|
+ // 必须使用 messageV3:服务器上 messageV2 累积了旧的 delete('latest') 标记,
|
|
|
+ // 导致 get('latest') 永远返回 undefined(Yjs 的 typeMapGet 在最后一个 item 被 deleted 时返回 undefined)
|
|
|
+ yMessage.value = doc.getMap('messageV3')
|
|
|
yMessage.value.observe((e: any) => {
|
|
|
// 同时兼容 keysChanged 和 changes.keys(不同 Yjs 版本/事件结构)
|
|
|
const changed = e.keysChanged || e.changes?.keys
|
|
|
+ // ★ 打印每个 key 的 action(add/update/delete)和 oldValue
|
|
|
+ const keysMap = e.keys || e.changes?.keys
|
|
|
+ if (keysMap && keysMap.get) {
|
|
|
+ for (const [key, info] of keysMap.entries()) {
|
|
|
+ console.log(`🔗 observe key="${key}" action=${info.action} oldValue=`, info.oldValue)
|
|
|
+ }
|
|
|
+ }
|
|
|
console.log('🔗 yMessage observe 触发, changed keys:', changed ? Array.from(changed) : 'null')
|
|
|
if (!changed || !changed.has('latest')) return
|
|
|
const message = yMessage.value!.get('latest')
|
|
|
- console.log('🔗 yMessage latest:', message?.type, 'mId:', message?.mId, '当前 mId:', mId.value)
|
|
|
+ console.log('🔗 yMessage latest:', message, '当前 mId:', mId.value)
|
|
|
if (message && message.mId !== mId.value) {
|
|
|
getMessages(message)
|
|
|
}
|
|
|
@@ -3375,11 +3378,8 @@ const messageInit = () => {
|
|
|
if (isFirstEnter.value && isCreator.value && docSocket.value) {
|
|
|
console.log('🧹 首次进入且为创建者,清空所有同步状态')
|
|
|
docSocket.value.transact(() => {
|
|
|
- // 清空普通消息(Map 结构,最新一条存在 latest key)
|
|
|
- const messageMapForClear = docSocket.value?.getMap?.('messageV2')
|
|
|
- if (messageMapForClear) {
|
|
|
- messageMapForClear.delete('latest')
|
|
|
- }
|
|
|
+ // ★ 不删除 latest!delete 会传播给学生导致 get('latest') 返回 undefined
|
|
|
+ // 旧 latest 会被 sendMessage 的 set('latest', message) 自然覆盖
|
|
|
// 清空计时器消息数组
|
|
|
const timerMessagesArray = docSocket.value?.getArray?.('timerMessages')
|
|
|
if (timerMessagesArray) {
|
|
|
@@ -3619,6 +3619,9 @@ const sendMessage = (obj: any) => {
|
|
|
// 用 Map.set 代替 Array.delete+push,只产生 1 条 update
|
|
|
yMessage.value.set('latest', message)
|
|
|
}
|
|
|
+ else {
|
|
|
+ console.warn('⚠️ sendMessage: yMessage.value 为 null,消息未写入! type:', messageType, 'docSocket:', !!docSocket.value)
|
|
|
+ }
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
@@ -3828,6 +3831,13 @@ const getMessages = (msgObj: any) => {
|
|
|
})
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
+ // ★ 老师收到学生 class_begin 后,回发当前幻灯片,确保学生有 latest 可跟随
|
|
|
+ if (msgObj.type === 'class_begin' && isCreator.value) {
|
|
|
+ const curIndex = slidesStore.slideIndex ?? 0
|
|
|
+ sendMessage({ slideIndex: curIndex, courseid: props.courseid, type: 'slideIndex' })
|
|
|
+ console.log('🔗 老师响应 class_begin,回发 slideIndex:', curIndex)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -4057,17 +4067,18 @@ onMounted(() => {
|
|
|
console.log('════════ Yjs 诊断 ════════\ndocSocket.value 为 null')
|
|
|
return null
|
|
|
}
|
|
|
- const ymap = doc.getMap('messageV2')
|
|
|
- const msgType = doc.share.get('messageV2')
|
|
|
+ const msgV3Type = doc.share.get('messageV3')
|
|
|
+ const oldMsgType = doc.share.get('message')
|
|
|
const report = {
|
|
|
'1. doc.guid': doc.guid,
|
|
|
- '2. yMessage.value === doc.getMap("messageV2")': yMessage.value === ymap,
|
|
|
+ '2. yMessage.value === doc.share.get("messageV3")': yMessage.value === msgV3Type,
|
|
|
'3. yMessage.value?.doc?.guid': yMessage.value?.doc?.guid,
|
|
|
'4. doc.store.clients.size': doc.store.clients.size,
|
|
|
- '5. doc.share.get("messageV2") 类型': msgType?.constructor?.name,
|
|
|
- '6. doc.getMap("messageV2").size': ymap.size,
|
|
|
- '7. doc.getMap("messageV2").get("latest")': ymap.get('latest'),
|
|
|
- '8. yMessage.value?.get("latest")': yMessage.value?.get('latest'),
|
|
|
+ '5a. share.get("messageV3") 类型': msgV3Type?.constructor?.name,
|
|
|
+ '5b. share.get("message") 类型': oldMsgType?.constructor?.name,
|
|
|
+ '6. messageV3.size': msgV3Type?.size,
|
|
|
+ '7. messageV3?.get("latest")': msgV3Type?.get?.('latest'),
|
|
|
+ '8. yMessage.value?.get("latest")': yMessage.value?.get?.('latest'),
|
|
|
'9. doc 里所有顶层 key': Array.from(doc.share.entries()).map(([k, v]: [string, any]) => k + ':' + v.constructor.name),
|
|
|
'10. provider synced': providerSocket.value?.synced,
|
|
|
'11. ws.readyState': providerSocket.value?.ws?.readyState,
|
|
|
@@ -4308,7 +4319,6 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
|
|
|
isConnecting.value = true
|
|
|
connectionStatus.value = 'connecting'
|
|
|
-
|
|
|
try {
|
|
|
// 清理之前的连接
|
|
|
// if (providerSocket.value && type == 1) {
|
|
|
@@ -4350,11 +4360,103 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
)
|
|
|
providerSocket.value.awareness.setLocalState(null)
|
|
|
providerSocket.value.awareness.destroy() // 完全禁用 awareness
|
|
|
+
|
|
|
+ // ★ 三层日志之一:raw websocket 消息(确认数据到达浏览器)
|
|
|
+ const _ws = (providerSocket.value as any).ws
|
|
|
+ if (_ws && !_ws.__logged) {
|
|
|
+ _ws.__logged = true // 防止重复绑定
|
|
|
+ // 包一层 onmessage,抓 provider 处理异常
|
|
|
+ const _origOnMsg = _ws.onmessage
|
|
|
+ if (typeof _origOnMsg === 'function') {
|
|
|
+ _ws.onmessage = function(ev: MessageEvent) {
|
|
|
+ try {
|
|
|
+ _origOnMsg.call(_ws, ev)
|
|
|
+ }
|
|
|
+ catch (e: any) {
|
|
|
+ console.error('❌ provider onmessage 异常:', e?.message || e, e?.stack || '')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ _ws.addEventListener('message', (ev: MessageEvent) => {
|
|
|
+ try {
|
|
|
+ const data = new Uint8Array(ev.data)
|
|
|
+ const msgType = data[0]
|
|
|
+ const typeNames: any = { 0: 'sync', 1: 'awareness', 2: 'queryAwareness' }
|
|
|
+ let detail = ''
|
|
|
+ if (msgType === 0) {
|
|
|
+ const sub = data[1]
|
|
|
+ detail = ({ 0: 'step1(请求缺失)', 1: 'step2(服务器推送)', 2: 'update' } as any)[sub] || 'unknown'
|
|
|
+ }
|
|
|
+ console.log(`📥 ws raw: ${typeNames[msgType] || msgType} ${detail} (${data.length} bytes)`)
|
|
|
+
|
|
|
+ // ★ 诊断:sync update 到达时手动解码 + applyUpdate,看是不是重复数据
|
|
|
+ if (msgType === 0 && data[1] === 2) {
|
|
|
+ const doc = docSocket.value
|
|
|
+ if (doc) {
|
|
|
+ // 解码 varint 提取 update bytes
|
|
|
+ let pos = 2 // 跳过 msgType + subType
|
|
|
+ let updateLen = 0
|
|
|
+ let shift = 0
|
|
|
+ while (pos < data.length) {
|
|
|
+ const b = data[pos++]
|
|
|
+ updateLen |= (b & 0x7f) << shift
|
|
|
+ if (b < 0x80) break
|
|
|
+ shift += 7
|
|
|
+ }
|
|
|
+ const updateBytes = data.slice(pos, pos + updateLen)
|
|
|
+
|
|
|
+ // ★ 用 Y.decodeUpdate 解码 update(不创建 temp doc,不触发 applyUpdate 的 DELETE)
|
|
|
+ try {
|
|
|
+ const { structs } = Y.decodeUpdate(updateBytes)
|
|
|
+ console.log(`📦 解码: updateBytes=${updateBytes.length}, structs=${structs.length}`)
|
|
|
+ for (const struct of structs) {
|
|
|
+ const arr = struct?.content?.arr
|
|
|
+ console.log(`📦 struct: type=${struct?.content?.constructor?.name}, parentSub=${struct?.parentSub}, arr=${arr ? JSON.stringify(arr)?.slice(0, 150) : '无'}`)
|
|
|
+ if (arr && Array.isArray(arr)) {
|
|
|
+ for (const val of arr) {
|
|
|
+ if (val && typeof val === 'object' && val.type && val.mId) {
|
|
|
+ console.log(`📦 ✅ 从 update 提取消息: type=${val.type}, mId=${val.mId}, 当前 mId=${mId.value}`)
|
|
|
+ if (val.mId !== mId.value) {
|
|
|
+ getMessages(val)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ console.log('📦 update 解码失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ console.log('📥 ws raw: parse error', e)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // ★ 三层日志之二:Y.Doc update 事件 — 列出所有根类型的 size,看更新到底改了哪个
|
|
|
+ docSocket.value.on('update', (update: Uint8Array, origin: any) => {
|
|
|
+ const originName = origin?.constructor?.name || (origin === providerSocket.value ? 'ws-provider' : typeof origin)
|
|
|
+ const share = (docSocket.value as any)?.share
|
|
|
+ const sizes: string[] = []
|
|
|
+ if (share) {
|
|
|
+ for (const [name, type] of share.entries()) {
|
|
|
+ const sz = type.size ?? type.length ?? '?'
|
|
|
+ const latest = type.get?.('latest')
|
|
|
+ sizes.push(`${name}=${sz}${latest ? '(' + latest.type + ')' : ''}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ console.log(`📥 Y.Doc update: ${update.length} bytes, origin: ${originName}, types: [${sizes.join(', ')}]`)
|
|
|
+ })
|
|
|
+
|
|
|
// 清理可能残留的旧心跳定时器
|
|
|
if (heartbeatTimer.value) {
|
|
|
clearInterval(heartbeatTimer.value)
|
|
|
heartbeatTimer.value = null
|
|
|
}
|
|
|
+
|
|
|
// 启动定期更新 token
|
|
|
// 30分钟后再次更新
|
|
|
if (authTokenUpdateTimer.value) {
|
|
|
@@ -4380,12 +4482,15 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
|
|
|
mId.value = Math.random().toString(36).substr(2, 9)
|
|
|
messageInit()
|
|
|
+ console.log('🔗 connected 后: yMessage.value =', yMessage.value ? '已绑定' : 'null', 'doc.guid=', docSocket.value?.guid)
|
|
|
// 连接成功后,读取当前计时器状态(Map)
|
|
|
if (docSocket.value) {
|
|
|
initializeYjsData()
|
|
|
}
|
|
|
- if(type == 1){
|
|
|
- createWebSocketConnection(2)
|
|
|
+ if (type == 1) {
|
|
|
+ setTimeout(() => {
|
|
|
+ createWebSocketConnection(2)
|
|
|
+ }, 1000)
|
|
|
}
|
|
|
}
|
|
|
else if (event.status === 'disconnected') {
|
|
|
@@ -4424,6 +4529,23 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
if (!isSynced) return
|
|
|
console.log('🔗 Yjs sync 完成,开始 catch-up')
|
|
|
|
|
|
+ // ★ 诊断:枚举 doc.share 里所有根类型,给每个都加 observe
|
|
|
+ const share = (docSocket.value as any).share
|
|
|
+ if (share) {
|
|
|
+ console.log('🔗 根类型列表:')
|
|
|
+ for (const [name, type] of share.entries()) {
|
|
|
+ console.log(` ${name}: ${type.constructor.name}, size: ${type.size ?? type.length ?? '?'}`)
|
|
|
+ // 给每个根类型加 observe,看哪个在动
|
|
|
+ if (type.observe && !type.__diagObserved) {
|
|
|
+ type.__diagObserved = true
|
|
|
+ type.observe((e: any) => {
|
|
|
+ const changed = e.keysChanged || e.changes?.keys
|
|
|
+ console.log(`🔗 [${name}] observe 触发! changed:`, changed ? Array.from(changed) : e)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// catch-up:读取服务器同步过来的 latest 消息
|
|
|
if (yMessage.value) {
|
|
|
const latest = yMessage.value.get('latest')
|
|
|
@@ -4448,11 +4570,23 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
|
|
|
// 学生端补发上线消息,让老师知道学生已就绪
|
|
|
if (props.type === '2') {
|
|
|
+ console.log('🔗 sync handler: yMessage.value =', yMessage.value ? '已绑定' : 'null', 'doc.guid=', docSocket.value?.guid)
|
|
|
sendMessage({ type: 'class_begin', courseid: props.courseid })
|
|
|
+ // ★ 确认 class_begin 是否真的写入了 messageV3
|
|
|
+ const msgV3 = (docSocket.value as any)?.share?.get?.('messageV3')
|
|
|
+ console.log('🔗 学生发送 class_begin 后, messageV3.size:', msgV3?.size, 'latest:', msgV3?.get?.('latest')?.type)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ★ 老师端:sync 完成后补发当前幻灯片,确保 messageV3 的 latest 有值
|
|
|
+ // 解决:服务器上 latest 被 clearAllSyncStates 删除后,新连入学生拿到空 Map 无法跟随
|
|
|
+ if (props.type === '1') {
|
|
|
+ const curIndex = slidesStore.slideIndex ?? 0
|
|
|
+ sendMessage({ slideIndex: curIndex, courseid: props.courseid, type: 'slideIndex' })
|
|
|
+ console.log('🔗 老师补发当前幻灯片 slideIndex:', curIndex)
|
|
|
}
|
|
|
|
|
|
// 自动运行 Yjs 诊断
|
|
|
- ;(window as any).PPTistStudent?.diagnoseYjs?.()
|
|
|
+ (window as any).PPTistStudent?.diagnoseYjs?.()
|
|
|
})
|
|
|
// 监听连接错误
|
|
|
// providerSocket.value.on('connection-error', (error: any) => {
|