CustomWebsocketProvider.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import type * as Y from 'yjs'
  2. import * as syncProtocol from 'y-protocols/sync'
  3. import * as awarenessProtocol from 'y-protocols/awareness'
  4. import * as encoding from 'lib0/encoding'
  5. import * as decoding from 'lib0/decoding'
  6. import { Observable } from 'lib0/observable'
  7. const messageSync = 0
  8. const messageAwareness = 1
  9. const messageAuth = 2
  10. const messageQueryAwareness = 3
  11. export interface YjsRedisProviderOptions {
  12. params?: Record<string, string>
  13. awareness?: awarenessProtocol.Awareness
  14. resyncInterval?: number
  15. maxBackoffTime?: number
  16. connect?: boolean
  17. }
  18. /**
  19. * 兼容 y-websocket WebsocketProvider 接口的自定义实现。
  20. * 关键改进:
  21. * 1. readMessage 循环解析整条 buffer,兼容多消息合并推送
  22. * 2. 额外 emit 'raw-message',把完整 Uint8Array 抛出,供上层解析
  23. * 3. 保留 ws / awareness / params / synced / connect / destroy 等原 API
  24. */
  25. export class YjsRedisProvider extends Observable<string> {
  26. public doc: Y.Doc
  27. public awareness: awarenessProtocol.Awareness
  28. public ws: WebSocket | null = null
  29. public wsconnected = false
  30. public wsconnecting = false
  31. public synced = false
  32. public shouldConnect = true
  33. public params: Record<string, string>
  34. private serverUrl: string
  35. private roomname: string
  36. private url: string
  37. private resyncInterval: number
  38. private maxBackoffTime: number
  39. private reconnectAttempts = 0
  40. private _resyncIntervalId: any = null
  41. private _updateHandler: (update: Uint8Array, origin: any) => void
  42. private _awarenessUpdateHandler: (...args: any[]) => void
  43. private messageHandlers: Array<(
  44. encoder: encoding.Encoder,
  45. decoder: decoding.Decoder,
  46. provider: YjsRedisProvider,
  47. emitSynced: boolean,
  48. messageType: number
  49. ) => void>
  50. constructor(serverUrl: string, roomname: string, doc: Y.Doc, opts: YjsRedisProviderOptions = {}) {
  51. super()
  52. this.doc = doc
  53. this.roomname = roomname
  54. this.serverUrl = serverUrl.replace(/\/+$/, '')
  55. this.params = opts.params || {}
  56. this.resyncInterval = opts.resyncInterval ?? -1
  57. this.maxBackoffTime = opts.maxBackoffTime ?? 2500
  58. this.awareness = opts.awareness || new awarenessProtocol.Awareness(doc)
  59. this.awareness.setLocalState({})
  60. this.url = this._buildUrl()
  61. this.messageHandlers = []
  62. // 0: Sync
  63. this.messageHandlers[messageSync] = (encoder, decoder, provider, emitSynced) => {
  64. encoding.writeVarUint(encoder, messageSync)
  65. const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, doc, provider)
  66. if (emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) {
  67. provider.synced = true
  68. provider.emit('sync', [true])
  69. }
  70. }
  71. // 1: Awareness
  72. this.messageHandlers[messageAwareness] = (encoder, decoder, provider) => {
  73. try {
  74. awarenessProtocol.applyAwarenessUpdate(
  75. provider.awareness,
  76. decoding.readVarUint8Array(decoder),
  77. provider
  78. )
  79. }
  80. catch (e) {
  81. // awareness 可能已被上层 destroy,忽略
  82. }
  83. }
  84. // 2: Auth(y-redis 返回权限信息)
  85. this.messageHandlers[messageAuth] = (encoder, decoder, provider) => {
  86. try {
  87. const authType = decoding.readVarUint(decoder)
  88. if (authType === 0) {
  89. provider.emit('auth-message', ['permission-denied'])
  90. }
  91. else if (authType === 1) {
  92. provider.emit('auth-message', ['authenticated'])
  93. }
  94. }
  95. catch (e) { /* ignore */ }
  96. }
  97. // 3: QueryAwareness
  98. this.messageHandlers[messageQueryAwareness] = (encoder, decoder, provider) => {
  99. encoding.writeVarUint(encoder, messageAwareness)
  100. encoding.writeVarUint8Array(
  101. encoder,
  102. awarenessProtocol.encodeAwarenessUpdate(
  103. provider.awareness,
  104. Array.from(provider.awareness.getStates().keys())
  105. )
  106. )
  107. }
  108. // 本地文档更新 → 广播
  109. this._updateHandler = (update, origin) => {
  110. if (origin !== this) {
  111. const encoder = encoding.createEncoder()
  112. encoding.writeVarUint(encoder, messageSync)
  113. syncProtocol.writeUpdate(encoder, update)
  114. this._broadcast(encoding.toUint8Array(encoder))
  115. }
  116. }
  117. this.doc.on('update', this._updateHandler)
  118. // 本地 awareness 更新 → 广播
  119. this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
  120. const changedClients = added.concat(updated, removed)
  121. const encoder = encoding.createEncoder()
  122. encoding.writeVarUint(encoder, messageAwareness)
  123. encoding.writeVarUint8Array(
  124. encoder,
  125. awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)
  126. )
  127. this._broadcast(encoding.toUint8Array(encoder))
  128. }
  129. this.awareness.on('update', this._awarenessUpdateHandler)
  130. if (opts.connect !== false) {
  131. this.connect()
  132. }
  133. }
  134. private _buildUrl(): string {
  135. const qs = new URLSearchParams(this.params).toString()
  136. return `${this.serverUrl}/${this.roomname}${qs ? '?' + qs : ''}`
  137. }
  138. /** 更新参数(例如 token 过期后刷新 yauth),下次 connect 生效 */
  139. public updateParams(params: Record<string, string>): void {
  140. this.params = params
  141. this.url = this._buildUrl()
  142. this.emit('url-updated', [this.url])
  143. }
  144. public connect(): void {
  145. if (this.wsconnected || this.wsconnecting || !this.shouldConnect) return
  146. this.wsconnecting = true
  147. this.emit('status', [{ status: 'connecting' }])
  148. const ws = new WebSocket(this.url)
  149. this.ws = ws
  150. ws.binaryType = 'arraybuffer'
  151. ws.onopen = () => {
  152. this.wsconnecting = false
  153. this.wsconnected = true
  154. this.reconnectAttempts = 0
  155. this.emit('status', [{ status: 'connected' }])
  156. // Sync step 1
  157. const encoder = encoding.createEncoder()
  158. encoding.writeVarUint(encoder, messageSync)
  159. syncProtocol.writeSyncStep1(encoder, this.doc)
  160. ws.send(encoding.toUint8Array(encoder))
  161. // Awareness
  162. const states = this.awareness.getStates()
  163. if (states.size > 0) {
  164. const enc = encoding.createEncoder()
  165. encoding.writeVarUint(enc, messageAwareness)
  166. encoding.writeVarUint8Array(
  167. enc,
  168. awarenessProtocol.encodeAwarenessUpdate(this.awareness, Array.from(states.keys()))
  169. )
  170. ws.send(encoding.toUint8Array(enc))
  171. }
  172. // 启动周期 resync(如果用户传了 resyncInterval)
  173. if (this.resyncInterval > 0) {
  174. this._resyncIntervalId = setInterval(() => {
  175. if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  176. const e = encoding.createEncoder()
  177. encoding.writeVarUint(e, messageSync)
  178. syncProtocol.writeSyncStep1(e, this.doc)
  179. this.ws.send(encoding.toUint8Array(e))
  180. }
  181. }, this.resyncInterval)
  182. }
  183. }
  184. ws.onmessage = (event: MessageEvent) => {
  185. if (event.data instanceof ArrayBuffer) {
  186. this._readMessage(new Uint8Array(event.data))
  187. }
  188. else if (event.data instanceof Blob) {
  189. event.data.arrayBuffer().then(buf => this._readMessage(new Uint8Array(buf)))
  190. }
  191. else if (typeof event.data === 'string') {
  192. this._readMessage(new TextEncoder().encode(event.data))
  193. }
  194. }
  195. ws.onclose = (event: CloseEvent) => {
  196. this.wsconnecting = false
  197. if (this._resyncIntervalId) {
  198. clearInterval(this._resyncIntervalId)
  199. this._resyncIntervalId = null
  200. }
  201. if (this.wsconnected) {
  202. this.wsconnected = false
  203. this.synced = false
  204. try {
  205. awarenessProtocol.removeAwarenessStates(
  206. this.awareness,
  207. Array.from(this.awareness.getStates().keys()).filter(
  208. client => client !== this.doc.clientID
  209. ),
  210. this
  211. )
  212. }
  213. catch (e) { /* ignore */ }
  214. this.emit('status', [{ status: 'disconnected' }])
  215. }
  216. this.emit('connection-close', [event, this])
  217. this.ws = null
  218. if (this.shouldConnect) {
  219. const delay = Math.min(this.maxBackoffTime, 100 * Math.pow(2, this.reconnectAttempts))
  220. this.reconnectAttempts++
  221. setTimeout(() => this.connect(), delay)
  222. }
  223. }
  224. ws.onerror = (event: Event) => {
  225. this.emit('connection-error', [event, this])
  226. }
  227. }
  228. /**
  229. * 核心:循环解析整条 buffer,兼容多消息合并
  230. * 额外:先抛出 raw-message,让上层拿到完整原始数据
  231. */
  232. private _readMessage(data: Uint8Array): void {
  233. // ★ 关键:把完整原始数据先广播出去
  234. this.emit('raw-message', [data])
  235. let offset = 0
  236. while (offset < data.length) {
  237. const decoder = decoding.createDecoder(data.subarray(offset))
  238. try {
  239. const messageType = decoding.readVarUint(decoder)
  240. const encoder = encoding.createEncoder()
  241. const handler = this.messageHandlers[messageType]
  242. if (handler) {
  243. handler(encoder, decoder, this, true, messageType)
  244. if (encoding.length(encoder) > 0 && this.ws?.readyState === WebSocket.OPEN) {
  245. this.ws.send(encoding.toUint8Array(encoder))
  246. }
  247. }
  248. const consumed = decoder.pos
  249. if (consumed === 0) break
  250. offset += consumed
  251. // 保留原有 message 事件,兼容旧监听(data 是这一条的片段)
  252. this.emit('message', [{
  253. data: data.slice(offset - consumed, offset),
  254. type: messageType,
  255. }])
  256. }
  257. catch (err) {
  258. // 解析失败立即停止,避免错位
  259. console.warn('[YjsRedisProvider] parse error at offset', offset, err)
  260. break
  261. }
  262. }
  263. }
  264. private _broadcast(data: Uint8Array): void {
  265. if (this.ws?.readyState === WebSocket.OPEN) {
  266. this.ws.send(data)
  267. }
  268. }
  269. public disconnect(): void {
  270. this.shouldConnect = false
  271. if (this._resyncIntervalId) {
  272. clearInterval(this._resyncIntervalId)
  273. this._resyncIntervalId = null
  274. }
  275. if (this.ws !== null) {
  276. this.ws.close()
  277. }
  278. }
  279. public destroy(): void {
  280. this.disconnect()
  281. this.doc.off('update', this._updateHandler)
  282. try {
  283. this.awareness.off('update', this._awarenessUpdateHandler)
  284. }
  285. catch (e) { /* ignore */ }
  286. try {
  287. this.awareness.destroy()
  288. }
  289. catch (e) { /* ignore */ }
  290. super.destroy()
  291. }
  292. }