CustomWebsocketProvider.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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. // 远端 update:origin 是 provider 实例 → 不回发
  111. if (origin instanceof YjsRedisProvider) return
  112. // 本地编辑:广播给服务器
  113. const encoder = encoding.createEncoder()
  114. encoding.writeVarUint(encoder, messageSync)
  115. syncProtocol.writeUpdate(encoder, update)
  116. this._broadcast(encoding.toUint8Array(encoder))
  117. }
  118. this.doc.on('update', this._updateHandler)
  119. // 本地 awareness 更新 → 广播
  120. this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
  121. const changedClients = added.concat(updated, removed)
  122. const encoder = encoding.createEncoder()
  123. encoding.writeVarUint(encoder, messageAwareness)
  124. encoding.writeVarUint8Array(
  125. encoder,
  126. awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)
  127. )
  128. this._broadcast(encoding.toUint8Array(encoder))
  129. }
  130. this.awareness.on('update', this._awarenessUpdateHandler)
  131. if (opts.connect !== false) {
  132. this.connect()
  133. }
  134. }
  135. private _buildUrl(): string {
  136. const qs = new URLSearchParams(this.params).toString()
  137. return `${this.serverUrl}/${this.roomname}${qs ? '?' + qs : ''}`
  138. }
  139. /** 更新参数(例如 token 过期后刷新 yauth),下次 connect 生效 */
  140. public updateParams(params: Record<string, string>): void {
  141. this.params = params
  142. this.url = this._buildUrl()
  143. this.emit('url-updated', [this.url])
  144. }
  145. public connect(): void {
  146. if (this.wsconnected || this.wsconnecting || !this.shouldConnect) return
  147. this.wsconnecting = true
  148. this.emit('status', [{ status: 'connecting' }])
  149. const ws = new WebSocket(this.url)
  150. this.ws = ws
  151. ws.binaryType = 'arraybuffer'
  152. ws.onopen = () => {
  153. this.wsconnecting = false
  154. this.wsconnected = true
  155. this.reconnectAttempts = 0
  156. this.emit('status', [{ status: 'connected' }])
  157. // Sync step 1
  158. const encoder = encoding.createEncoder()
  159. encoding.writeVarUint(encoder, messageSync)
  160. syncProtocol.writeSyncStep1(encoder, this.doc)
  161. ws.send(encoding.toUint8Array(encoder))
  162. // Awareness
  163. const states = this.awareness.getStates()
  164. if (states.size > 0) {
  165. const enc = encoding.createEncoder()
  166. encoding.writeVarUint(enc, messageAwareness)
  167. encoding.writeVarUint8Array(
  168. enc,
  169. awarenessProtocol.encodeAwarenessUpdate(this.awareness, Array.from(states.keys()))
  170. )
  171. ws.send(encoding.toUint8Array(enc))
  172. }
  173. // 启动周期 resync(如果用户传了 resyncInterval)
  174. if (this.resyncInterval > 0) {
  175. this._resyncIntervalId = setInterval(() => {
  176. if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  177. const e = encoding.createEncoder()
  178. encoding.writeVarUint(e, messageSync)
  179. syncProtocol.writeSyncStep1(e, this.doc)
  180. this.ws.send(encoding.toUint8Array(e))
  181. }
  182. }, this.resyncInterval)
  183. }
  184. }
  185. ws.onmessage = (event: MessageEvent) => {
  186. if (event.data instanceof ArrayBuffer) {
  187. this._readMessage(new Uint8Array(event.data))
  188. }
  189. else if (event.data instanceof Blob) {
  190. event.data.arrayBuffer().then(buf => this._readMessage(new Uint8Array(buf)))
  191. }
  192. else if (typeof event.data === 'string') {
  193. this._readMessage(new TextEncoder().encode(event.data))
  194. }
  195. }
  196. ws.onclose = (event: CloseEvent) => {
  197. this.wsconnecting = false
  198. if (this._resyncIntervalId) {
  199. clearInterval(this._resyncIntervalId)
  200. this._resyncIntervalId = null
  201. }
  202. if (this.wsconnected) {
  203. this.wsconnected = false
  204. this.synced = false
  205. try {
  206. awarenessProtocol.removeAwarenessStates(
  207. this.awareness,
  208. Array.from(this.awareness.getStates().keys()).filter(
  209. client => client !== this.doc.clientID
  210. ),
  211. this
  212. )
  213. }
  214. catch (e) { /* ignore */ }
  215. this.emit('status', [{ status: 'disconnected' }])
  216. }
  217. this.emit('connection-close', [event, this])
  218. this.ws = null
  219. if (this.shouldConnect) {
  220. const delay = Math.min(this.maxBackoffTime, 100 * Math.pow(2, this.reconnectAttempts))
  221. this.reconnectAttempts++
  222. setTimeout(() => this.connect(), delay)
  223. }
  224. }
  225. ws.onerror = (event: Event) => {
  226. this.emit('connection-error', [event, this])
  227. }
  228. }
  229. /**
  230. * 核心:循环解析整条 buffer,兼容多消息合并
  231. * 额外:先抛出 raw-message,让上层拿到完整原始数据
  232. */
  233. private _readMessage(data: Uint8Array): void {
  234. // ★ 关键:把完整原始数据先广播出去
  235. this.emit('raw-message', [data])
  236. let offset = 0
  237. while (offset < data.length) {
  238. const decoder = decoding.createDecoder(data.subarray(offset))
  239. try {
  240. const messageType = decoding.readVarUint(decoder)
  241. const encoder = encoding.createEncoder()
  242. const handler = this.messageHandlers[messageType]
  243. if (handler) {
  244. handler(encoder, decoder, this, true, messageType)
  245. if (encoding.length(encoder) > 0 && this.ws?.readyState === WebSocket.OPEN) {
  246. this.ws.send(encoding.toUint8Array(encoder))
  247. }
  248. }
  249. const consumed = decoder.pos
  250. if (consumed === 0) break
  251. offset += consumed
  252. // 保留原有 message 事件,兼容旧监听(data 是这一条的片段)
  253. this.emit('message', [{
  254. data: data.slice(offset - consumed, offset),
  255. type: messageType,
  256. }])
  257. }
  258. catch (err) {
  259. // 解析失败立即停止,避免错位
  260. console.warn('[YjsRedisProvider] parse error at offset', offset, err)
  261. break
  262. }
  263. }
  264. }
  265. private _broadcast(data: Uint8Array): void {
  266. if (this.ws?.readyState === WebSocket.OPEN) {
  267. this.ws.send(data)
  268. }
  269. }
  270. this.shouldConnect = true
  271. this.connect()
  272. }
  273. public disconnect(): void {
  274. this.shouldConnect = false
  275. if (this._resyncIntervalId) {
  276. clearInterval(this._resyncIntervalId)
  277. this._resyncIntervalId = null
  278. }
  279. if (this.ws !== null) {
  280. this.ws.close()
  281. }
  282. }
  283. public destroy(): void {
  284. this.disconnect()
  285. this.doc.off('update', this._updateHandler)
  286. try {
  287. this.awareness.off('update', this._awarenessUpdateHandler)
  288. }
  289. catch (e) { /* ignore */ }
  290. try {
  291. this.awareness.destroy()
  292. }
  293. catch (e) { /* ignore */ }
  294. super.destroy()
  295. }
  296. }