|
|
@@ -0,0 +1,255 @@
|
|
|
+import type * as Y from 'yjs'
|
|
|
+import * as syncProtocol from 'y-protocols/sync'
|
|
|
+import * as awarenessProtocol from 'y-protocols/awareness'
|
|
|
+import * as encoding from 'lib0/encoding'
|
|
|
+import * as decoding from 'lib0/decoding'
|
|
|
+import { Observable } from 'lib0/observable'
|
|
|
+
|
|
|
+// y-redis 使用的消息类型常量(与 y-websocket 一致)
|
|
|
+const messageSync = 0
|
|
|
+const messageAwareness = 1
|
|
|
+const messageAuth = 2
|
|
|
+const messageQueryAwareness = 3
|
|
|
+
|
|
|
+export class YRedisWebsocketProvider extends Observable<string> {
|
|
|
+ public doc: Y.Doc
|
|
|
+ public awareness: awarenessProtocol.Awareness
|
|
|
+ public ws: WebSocket | null = null
|
|
|
+ public wsconnected = false
|
|
|
+ public synced = false
|
|
|
+ public shouldConnect = true
|
|
|
+
|
|
|
+ private url: string
|
|
|
+ private roomname: string
|
|
|
+ private params: Record<string, string>
|
|
|
+ private _updateHandler: (update: Uint8Array, origin: any) => void
|
|
|
+ private _awarenessUpdateHandler: (...args: any[]) => void
|
|
|
+ private messageHandlers: Array<(
|
|
|
+ encoder: encoding.Encoder,
|
|
|
+ decoder: decoding.Decoder,
|
|
|
+ provider: YRedisWebsocketProvider,
|
|
|
+ emitSynced: boolean,
|
|
|
+ messageType: number
|
|
|
+ ) => void>
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ serverUrl: string,
|
|
|
+ roomname: string,
|
|
|
+ doc: Y.Doc,
|
|
|
+ opts: { params?: Record<string, string>; awareness?: awarenessProtocol.Awareness } = {}
|
|
|
+ ) {
|
|
|
+ super()
|
|
|
+ this.doc = doc
|
|
|
+ this.roomname = roomname
|
|
|
+ this.params = opts.params || {}
|
|
|
+ this.awareness = opts.awareness || new awarenessProtocol.Awareness(doc)
|
|
|
+
|
|
|
+ // 构建带认证参数的 URL
|
|
|
+ const queryParams = new URLSearchParams(this.params)
|
|
|
+ this.url = `${serverUrl}/${roomname}${queryParams.toString() ? '?' + queryParams.toString() : ''}`
|
|
|
+
|
|
|
+ this.messageHandlers = []
|
|
|
+
|
|
|
+ // messageSync (0): 处理文档同步
|
|
|
+ this.messageHandlers[messageSync] = (encoder, decoder, provider, emitSynced) => {
|
|
|
+ encoding.writeVarUint(encoder, messageSync)
|
|
|
+ const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, doc, provider)
|
|
|
+ if (emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) {
|
|
|
+ provider.synced = true
|
|
|
+ provider.emit('sync', [true])
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // messageAwareness (1): 处理光标/在线状态
|
|
|
+ this.messageHandlers[messageAwareness] = (encoder, decoder, provider) => {
|
|
|
+ awarenessProtocol.applyAwarenessUpdate(
|
|
|
+ provider.awareness,
|
|
|
+ decoding.readVarUint8Array(decoder),
|
|
|
+ provider
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // ⭐ y-redis 关键:messageAuth (2) — 处理认证结果
|
|
|
+ this.messageHandlers[messageAuth] = (encoder, decoder, provider) => {
|
|
|
+ const authType = decoding.readVarUint(decoder)
|
|
|
+ if (authType === 0) {
|
|
|
+ // 认证失败:权限被拒绝
|
|
|
+ provider.emit('auth-message', ['permission-denied'])
|
|
|
+ console.error('[YRedisProvider] 认证失败,权限被拒绝')
|
|
|
+ provider.disconnect()
|
|
|
+ }
|
|
|
+ else if (authType === 1) {
|
|
|
+ // 认证成功(y-redis 某些版本会返回)
|
|
|
+ provider.emit('auth-message', ['authenticated'])
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // messageQueryAwareness (3): 请求 awareness 信息
|
|
|
+ this.messageHandlers[messageQueryAwareness] = (encoder, decoder, provider) => {
|
|
|
+ encoding.writeVarUint(encoder, messageAwareness)
|
|
|
+ encoding.writeVarUint8Array(
|
|
|
+ encoder,
|
|
|
+ awarenessProtocol.encodeAwarenessUpdate(
|
|
|
+ provider.awareness,
|
|
|
+ Array.from(provider.awareness.getStates().keys())
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // 本地文档更新 → 广播
|
|
|
+ this._updateHandler = (update, origin) => {
|
|
|
+ if (origin !== this) {
|
|
|
+ const encoder = encoding.createEncoder()
|
|
|
+ encoding.writeVarUint(encoder, messageSync)
|
|
|
+ syncProtocol.writeUpdate(encoder, update)
|
|
|
+ this.broadcastMessage(encoding.toUint8Array(encoder))
|
|
|
+ console.log(encoding.toUint8Array(encoder))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.doc.on('update', this._updateHandler)
|
|
|
+
|
|
|
+ // 本地 awareness 更新 → 广播
|
|
|
+ this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
|
|
|
+ const changedClients = added.concat(updated, removed)
|
|
|
+ if (origin !== this) {
|
|
|
+ const encoder = encoding.createEncoder()
|
|
|
+ encoding.writeVarUint(encoder, messageAwareness)
|
|
|
+ encoding.writeVarUint8Array(
|
|
|
+ encoder,
|
|
|
+ awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)
|
|
|
+ )
|
|
|
+ this.broadcastMessage(encoding.toUint8Array(encoder))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.awareness.on('update', this._awarenessUpdateHandler)
|
|
|
+
|
|
|
+ this.connect()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ⭐ 核心修复:循环解析整个消息缓冲区
|
|
|
+ * y-redis 可能将多条更新合并后一次性推送,必须全部处理
|
|
|
+ */
|
|
|
+ private readMessage(data: Uint8Array): void {
|
|
|
+ let offset = 0
|
|
|
+ while (offset < data.length) {
|
|
|
+ // 基于剩余数据创建解码器,decoder.pos 是相对于当前消息起点的
|
|
|
+ const decoder = decoding.createDecoder(data.subarray(offset))
|
|
|
+ try {
|
|
|
+ const messageType = decoding.readVarUint(decoder)
|
|
|
+ const encoder = encoding.createEncoder()
|
|
|
+ const handler = this.messageHandlers[messageType]
|
|
|
+
|
|
|
+ if (handler) {
|
|
|
+ handler(encoder, decoder, this, true, messageType)
|
|
|
+
|
|
|
+ // 如果 handler 生成了回复,立即发送
|
|
|
+ if (encoding.length(encoder) > 0) {
|
|
|
+ if (this.ws?.readyState === WebSocket.OPEN) {
|
|
|
+ this.ws.send(encoding.toUint8Array(encoder))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ⭐ 关键修正:使用处理完消息后的 decoder.pos 来推进 offset
|
|
|
+ const consumed = decoder.pos
|
|
|
+ if (consumed === 0) {
|
|
|
+ // 防止死循环:理论上不会发生,因为至少读取了消息类型
|
|
|
+ console.warn('[YRedisProvider] decoder 未消耗任何字节,终止解析')
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ offset += consumed
|
|
|
+
|
|
|
+ // 每条消息处理完后触发自定义事件
|
|
|
+ this.emit('message', [{ data: data.slice(offset - consumed, offset), type: messageType }])
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ console.error('[YRedisProvider] 解析消息失败,offset:', offset, err)
|
|
|
+ // 解析失败时终止本次处理,避免因错位导致更多误报
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ connect(): void {
|
|
|
+ if (this.wsconnected || !this.shouldConnect) return
|
|
|
+ this.emit('status', [{ status: 'connecting' }])
|
|
|
+
|
|
|
+ const ws = new WebSocket(this.url)
|
|
|
+ this.ws = ws
|
|
|
+ ws.binaryType = 'arraybuffer'
|
|
|
+
|
|
|
+ ws.onopen = () => {
|
|
|
+ this.wsconnected = true
|
|
|
+ this.emit('status', [{ status: 'connected' }])
|
|
|
+
|
|
|
+ // 发送 SyncStep1 发起初始同步
|
|
|
+ const encoder = encoding.createEncoder()
|
|
|
+ encoding.writeVarUint(encoder, messageSync)
|
|
|
+ syncProtocol.writeSyncStep1(encoder, this.doc)
|
|
|
+ ws.send(encoding.toUint8Array(encoder))
|
|
|
+
|
|
|
+ // 发送本地 awareness 状态
|
|
|
+ const states = this.awareness.getStates()
|
|
|
+ if (states.size > 0) {
|
|
|
+ const enc = encoding.createEncoder()
|
|
|
+ encoding.writeVarUint(enc, messageAwareness)
|
|
|
+ encoding.writeVarUint8Array(
|
|
|
+ enc,
|
|
|
+ awarenessProtocol.encodeAwarenessUpdate(this.awareness, Array.from(states.keys()))
|
|
|
+ )
|
|
|
+ ws.send(encoding.toUint8Array(enc))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ws.onmessage = (event: MessageEvent) => {
|
|
|
+ if (event.data instanceof ArrayBuffer) {
|
|
|
+ console.log('123', new Uint8Array(event.data))
|
|
|
+ this.readMessage(new Uint8Array(event.data))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ws.onclose = (event: CloseEvent) => {
|
|
|
+ this.wsconnected = false
|
|
|
+ this.synced = false
|
|
|
+ // 清除其他客户端的 awareness
|
|
|
+ awarenessProtocol.removeAwarenessStates(
|
|
|
+ this.awareness,
|
|
|
+ Array.from(this.awareness.getStates().keys()).filter(
|
|
|
+ client => client !== this.doc.clientID
|
|
|
+ ),
|
|
|
+ this
|
|
|
+ )
|
|
|
+ this.emit('status', [{ status: 'disconnected' }])
|
|
|
+ this.emit('connection-close', [event, this])
|
|
|
+ this.ws = null
|
|
|
+ if (this.shouldConnect) {
|
|
|
+ setTimeout(() => this.connect(), 2500)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ws.onerror = (event: Event) => {
|
|
|
+ this.emit('connection-error', [event, this])
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ disconnect(): void {
|
|
|
+ this.shouldConnect = false
|
|
|
+ if (this.ws) this.ws.close()
|
|
|
+ }
|
|
|
+
|
|
|
+ destroy(): void {
|
|
|
+ this.disconnect()
|
|
|
+ this.doc.off('update', this._updateHandler)
|
|
|
+ this.awareness.off('update', this._awarenessUpdateHandler)
|
|
|
+ this.awareness.destroy()
|
|
|
+ super.destroy()
|
|
|
+ }
|
|
|
+
|
|
|
+ private broadcastMessage(data: Uint8Array): void {
|
|
|
+ if (this.ws?.readyState === WebSocket.OPEN) {
|
|
|
+ this.ws.send(data)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|