@styris-ame/y-engineio 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/y-engineio.cjs +591 -0
- package/dist/y-engineio.cjs.map +1 -0
- package/dist/y-engineio.d.ts +135 -0
- package/dist/y-engineio.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/y-engineio.js +554 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module provider/engineio
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* eslint-env browser */
|
|
6
|
+
import * as Y from 'yjs' // eslint-disable-line
|
|
7
|
+
import * as bc from 'lib0/broadcastchannel'
|
|
8
|
+
import * as time from 'lib0/time'
|
|
9
|
+
import * as encoding from 'lib0/encoding'
|
|
10
|
+
import * as decoding from 'lib0/decoding'
|
|
11
|
+
import * as syncProtocol from 'y-protocols/sync'
|
|
12
|
+
import * as authProtocol from 'y-protocols/auth'
|
|
13
|
+
import * as awarenessProtocol from 'y-protocols/awareness'
|
|
14
|
+
import { ObservableV2 } from 'lib0/observable'
|
|
15
|
+
import * as math from 'lib0/math'
|
|
16
|
+
import * as url from 'lib0/url'
|
|
17
|
+
import * as env from 'lib0/environment'
|
|
18
|
+
import { Socket as Engine } from 'engine.io-client'
|
|
19
|
+
|
|
20
|
+
export const messageSync = 0
|
|
21
|
+
export const messageQueryAwareness = 3
|
|
22
|
+
export const messageAwareness = 1
|
|
23
|
+
export const messageAuth = 2
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* encoder, decoder, provider, emitSynced, messageType
|
|
27
|
+
* @type {Array<function(encoding.Encoder, decoding.Decoder, EngineIOProvider, boolean, number):void>}
|
|
28
|
+
*/
|
|
29
|
+
const messageHandlers = []
|
|
30
|
+
|
|
31
|
+
messageHandlers[messageSync] = (
|
|
32
|
+
encoder,
|
|
33
|
+
decoder,
|
|
34
|
+
provider,
|
|
35
|
+
emitSynced,
|
|
36
|
+
_messageType
|
|
37
|
+
) => {
|
|
38
|
+
encoding.writeVarUint(encoder, messageSync)
|
|
39
|
+
const syncMessageType = syncProtocol.readSyncMessage(
|
|
40
|
+
decoder,
|
|
41
|
+
encoder,
|
|
42
|
+
provider.doc,
|
|
43
|
+
provider
|
|
44
|
+
)
|
|
45
|
+
if (
|
|
46
|
+
emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 &&
|
|
47
|
+
!provider.synced
|
|
48
|
+
) {
|
|
49
|
+
provider.synced = true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
messageHandlers[messageQueryAwareness] = (
|
|
54
|
+
encoder,
|
|
55
|
+
_decoder,
|
|
56
|
+
provider,
|
|
57
|
+
_emitSynced,
|
|
58
|
+
_messageType
|
|
59
|
+
) => {
|
|
60
|
+
encoding.writeVarUint(encoder, messageAwareness)
|
|
61
|
+
encoding.writeVarUint8Array(
|
|
62
|
+
encoder,
|
|
63
|
+
awarenessProtocol.encodeAwarenessUpdate(
|
|
64
|
+
provider.awareness,
|
|
65
|
+
Array.from(provider.awareness.getStates().keys())
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
messageHandlers[messageAwareness] = (
|
|
71
|
+
_encoder,
|
|
72
|
+
decoder,
|
|
73
|
+
provider,
|
|
74
|
+
_emitSynced,
|
|
75
|
+
_messageType
|
|
76
|
+
) => {
|
|
77
|
+
awarenessProtocol.applyAwarenessUpdate(
|
|
78
|
+
provider.awareness,
|
|
79
|
+
decoding.readVarUint8Array(decoder),
|
|
80
|
+
provider
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
messageHandlers[messageAuth] = (
|
|
85
|
+
_encoder,
|
|
86
|
+
decoder,
|
|
87
|
+
provider,
|
|
88
|
+
_emitSynced,
|
|
89
|
+
_messageType
|
|
90
|
+
) => {
|
|
91
|
+
authProtocol.readAuthMessage(
|
|
92
|
+
decoder,
|
|
93
|
+
provider.doc,
|
|
94
|
+
(_ydoc, reason) => permissionDeniedHandler(provider, reason)
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const messageReconnectTimeout = 30000
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {EngineIOProvider} provider
|
|
102
|
+
* @param {string} reason
|
|
103
|
+
*/
|
|
104
|
+
const permissionDeniedHandler = (provider, reason) =>
|
|
105
|
+
console.warn(`Permission denied to access ${provider.url}.\n${reason}`)
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {ArrayBuffer | ArrayBufferView} data
|
|
109
|
+
* @return {Uint8Array}
|
|
110
|
+
*/
|
|
111
|
+
const toUint8Array = (data) =>
|
|
112
|
+
data instanceof ArrayBuffer
|
|
113
|
+
? new Uint8Array(data)
|
|
114
|
+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {EngineIOProvider} provider
|
|
118
|
+
* @param {Uint8Array} buf
|
|
119
|
+
* @param {boolean} emitSynced
|
|
120
|
+
* @return {encoding.Encoder}
|
|
121
|
+
*/
|
|
122
|
+
const readMessage = (provider, buf, emitSynced) => {
|
|
123
|
+
const decoder = decoding.createDecoder(buf)
|
|
124
|
+
const encoder = encoding.createEncoder()
|
|
125
|
+
const messageType = decoding.readVarUint(decoder)
|
|
126
|
+
const messageHandler = provider.messageHandlers[messageType]
|
|
127
|
+
if (/** @type {any} */ (messageHandler)) {
|
|
128
|
+
messageHandler(encoder, decoder, provider, emitSynced, messageType)
|
|
129
|
+
}
|
|
130
|
+
return encoder
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Outsource this function so that a new engine.io connection is created immediately.
|
|
135
|
+
* I suspect that the `close` event is not always fired if there are network issues.
|
|
136
|
+
*
|
|
137
|
+
* @param {EngineIOProvider} provider
|
|
138
|
+
* @param {Engine} engine
|
|
139
|
+
* @param {any} event
|
|
140
|
+
*/
|
|
141
|
+
const closeEngineConnection = (provider, engine, event) => {
|
|
142
|
+
if (engine === provider.engine) {
|
|
143
|
+
provider.emit('connection-close', [event, provider])
|
|
144
|
+
provider.engine = null
|
|
145
|
+
const anyEngine = /** @type {any} */ (engine)
|
|
146
|
+
if (anyEngine._yUnsubscribe) {
|
|
147
|
+
anyEngine._yUnsubscribe()
|
|
148
|
+
}
|
|
149
|
+
engine.close()
|
|
150
|
+
provider.connecting = false
|
|
151
|
+
if (provider.connected) {
|
|
152
|
+
provider.connected = false
|
|
153
|
+
provider.synced = false
|
|
154
|
+
// update awareness (all users except local left)
|
|
155
|
+
awarenessProtocol.removeAwarenessStates(
|
|
156
|
+
provider.awareness,
|
|
157
|
+
Array.from(provider.awareness.getStates().keys()).filter((client) =>
|
|
158
|
+
client !== provider.doc.clientID
|
|
159
|
+
),
|
|
160
|
+
provider
|
|
161
|
+
)
|
|
162
|
+
provider.emit('status', [{
|
|
163
|
+
status: 'disconnected'
|
|
164
|
+
}])
|
|
165
|
+
} else {
|
|
166
|
+
provider.unsuccessfulReconnects++
|
|
167
|
+
}
|
|
168
|
+
// Start with no reconnect timeout and increase timeout by
|
|
169
|
+
// using exponential backoff starting with 100ms
|
|
170
|
+
setTimeout(
|
|
171
|
+
setupEngine,
|
|
172
|
+
math.min(
|
|
173
|
+
math.pow(2, provider.unsuccessfulReconnects) * 100,
|
|
174
|
+
provider.maxBackoffTime
|
|
175
|
+
),
|
|
176
|
+
provider
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {EngineIOProvider} provider
|
|
183
|
+
*/
|
|
184
|
+
const setupEngine = (provider) => {
|
|
185
|
+
if (provider.shouldConnect && provider.engine === null) {
|
|
186
|
+
const engine = new provider._Engine(provider.serverUrl, {
|
|
187
|
+
...provider.engineOptions,
|
|
188
|
+
query: { ...provider.params, room: provider.roomname }
|
|
189
|
+
})
|
|
190
|
+
engine.binaryType = 'arraybuffer'
|
|
191
|
+
provider.engine = engine
|
|
192
|
+
provider.connecting = true
|
|
193
|
+
provider.connected = false
|
|
194
|
+
provider.synced = false
|
|
195
|
+
|
|
196
|
+
const onMessage = (/** @type {any} */ data) => {
|
|
197
|
+
if (provider.engine !== engine) return
|
|
198
|
+
if (typeof data === 'string') return
|
|
199
|
+
provider.lastMessageReceived = time.getUnixTime()
|
|
200
|
+
const encoder = readMessage(provider, toUint8Array(data), true)
|
|
201
|
+
if (encoding.length(encoder) > 1) {
|
|
202
|
+
engine.send(encoding.toUint8Array(encoder))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const onError = (/** @type {any} */ event) => {
|
|
206
|
+
if (provider.engine !== engine) return
|
|
207
|
+
provider.emit('connection-error', [event, provider])
|
|
208
|
+
}
|
|
209
|
+
const onPing = () => {
|
|
210
|
+
if (provider.engine !== engine) return
|
|
211
|
+
provider.lastMessageReceived = time.getUnixTime()
|
|
212
|
+
}
|
|
213
|
+
const onClose = (/** @type {any} */ reason, /** @type {any} */ description) => {
|
|
214
|
+
closeEngineConnection(provider, engine, { reason, description })
|
|
215
|
+
}
|
|
216
|
+
const onOpen = () => {
|
|
217
|
+
if (provider.engine !== engine) return
|
|
218
|
+
provider.lastMessageReceived = time.getUnixTime()
|
|
219
|
+
provider.connecting = false
|
|
220
|
+
provider.connected = true
|
|
221
|
+
provider.unsuccessfulReconnects = 0
|
|
222
|
+
provider.emit('status', [{
|
|
223
|
+
status: 'connected'
|
|
224
|
+
}])
|
|
225
|
+
// always send sync step 1 when connected
|
|
226
|
+
const encoder = encoding.createEncoder()
|
|
227
|
+
encoding.writeVarUint(encoder, messageSync)
|
|
228
|
+
syncProtocol.writeSyncStep1(encoder, provider.doc)
|
|
229
|
+
engine.send(encoding.toUint8Array(encoder))
|
|
230
|
+
// broadcast local awareness state
|
|
231
|
+
if (provider.awareness.getLocalState() !== null) {
|
|
232
|
+
const encoderAwarenessState = encoding.createEncoder()
|
|
233
|
+
encoding.writeVarUint(encoderAwarenessState, messageAwareness)
|
|
234
|
+
encoding.writeVarUint8Array(
|
|
235
|
+
encoderAwarenessState,
|
|
236
|
+
awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [
|
|
237
|
+
provider.doc.clientID
|
|
238
|
+
])
|
|
239
|
+
)
|
|
240
|
+
engine.send(encoding.toUint8Array(encoderAwarenessState))
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
engine.on('message', onMessage)
|
|
244
|
+
engine.on('error', onError)
|
|
245
|
+
engine.on('ping', onPing)
|
|
246
|
+
engine.on('close', onClose)
|
|
247
|
+
engine.on('open', onOpen)
|
|
248
|
+
const anyEngine = /** @type {any} */ (engine)
|
|
249
|
+
anyEngine._yUnsubscribe = () => {
|
|
250
|
+
engine.off('message', onMessage)
|
|
251
|
+
engine.off('error', onError)
|
|
252
|
+
engine.off('ping', onPing)
|
|
253
|
+
engine.off('close', onClose)
|
|
254
|
+
engine.off('open', onOpen)
|
|
255
|
+
}
|
|
256
|
+
provider.emit('status', [{
|
|
257
|
+
status: 'connecting'
|
|
258
|
+
}])
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {EngineIOProvider} provider
|
|
264
|
+
* @param {Uint8Array} buf
|
|
265
|
+
*/
|
|
266
|
+
const broadcastMessage = (provider, buf) => {
|
|
267
|
+
const engine = provider.engine
|
|
268
|
+
if (provider.connected && engine && engine.readyState === 'open') {
|
|
269
|
+
engine.send(buf)
|
|
270
|
+
}
|
|
271
|
+
if (provider.bcconnected) {
|
|
272
|
+
bc.publish(provider.bcChannel, buf, provider)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Engine.IO Provider for Yjs. Creates an engine.io connection to sync the shared
|
|
278
|
+
* document. Unlike a raw WebSocket, engine.io connects to a single endpoint and
|
|
279
|
+
* has no document name in the URL, so the room name is sent as the `room`
|
|
280
|
+
* handshake query parameter, letting the server accept or reject the connection
|
|
281
|
+
* before any sync frames are exchanged.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* import * as Y from 'yjs'
|
|
285
|
+
* import { EngineIOProvider } from '@styris-ame/y-engineio'
|
|
286
|
+
* const doc = new Y.Doc()
|
|
287
|
+
* const provider = new EngineIOProvider('http://localhost:1234', 'my-document-name', doc)
|
|
288
|
+
*
|
|
289
|
+
* @extends {ObservableV2<{ 'connection-close': (event: any, provider: EngineIOProvider) => any, 'status': (event: { status: 'connected' | 'disconnected' | 'connecting' }) => any, 'connection-error': (event: any, provider: EngineIOProvider) => any, 'sync': (state: boolean) => any, 'synced': (state: boolean) => any }>}
|
|
290
|
+
*/
|
|
291
|
+
export class EngineIOProvider extends ObservableV2 {
|
|
292
|
+
/**
|
|
293
|
+
* @param {string} serverUrl engine.io server origin, e.g. 'http://localhost:1234'
|
|
294
|
+
* @param {string} roomname
|
|
295
|
+
* @param {Y.Doc} doc
|
|
296
|
+
* @param {object} opts
|
|
297
|
+
* @param {boolean} [opts.connect]
|
|
298
|
+
* @param {awarenessProtocol.Awareness} [opts.awareness]
|
|
299
|
+
* @param {Object<string,string>} [opts.params] url query params, passed to engine.io as `query`
|
|
300
|
+
* @param {object} [opts.engineOptions] options forwarded to the engine.io-client `Socket` (e.g. `path`, `transports`, `withCredentials`, `extraHeaders`)
|
|
301
|
+
* @param {typeof Engine} [opts.EngineClass] override the engine.io `Socket` constructor (e.g. for testing)
|
|
302
|
+
* @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds
|
|
303
|
+
* @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)
|
|
304
|
+
* @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication
|
|
305
|
+
*/
|
|
306
|
+
constructor (serverUrl, roomname, doc, {
|
|
307
|
+
connect = true,
|
|
308
|
+
awareness = new awarenessProtocol.Awareness(doc),
|
|
309
|
+
params = {},
|
|
310
|
+
engineOptions = {},
|
|
311
|
+
EngineClass = Engine,
|
|
312
|
+
resyncInterval = -1,
|
|
313
|
+
maxBackoffTime = 2500,
|
|
314
|
+
disableBc = false
|
|
315
|
+
} = {}) {
|
|
316
|
+
super()
|
|
317
|
+
// ensure that serverUrl does not end with /
|
|
318
|
+
while (serverUrl[serverUrl.length - 1] === '/') {
|
|
319
|
+
serverUrl = serverUrl.slice(0, serverUrl.length - 1)
|
|
320
|
+
}
|
|
321
|
+
this.serverUrl = serverUrl
|
|
322
|
+
this.bcChannel = serverUrl + '/' + roomname
|
|
323
|
+
this.maxBackoffTime = maxBackoffTime
|
|
324
|
+
/**
|
|
325
|
+
* The specified url parameters. This can be safely updated. The changed parameters will be used
|
|
326
|
+
* when a new connection is established.
|
|
327
|
+
* @type {Object<string,string>}
|
|
328
|
+
*/
|
|
329
|
+
this.params = params
|
|
330
|
+
/**
|
|
331
|
+
* Options forwarded verbatim to the engine.io-client `Socket`.
|
|
332
|
+
* @type {object}
|
|
333
|
+
*/
|
|
334
|
+
this.engineOptions = engineOptions
|
|
335
|
+
this.roomname = roomname
|
|
336
|
+
this.doc = doc
|
|
337
|
+
this._Engine = EngineClass
|
|
338
|
+
this.awareness = awareness
|
|
339
|
+
this.connected = false
|
|
340
|
+
this.connecting = false
|
|
341
|
+
this.bcconnected = false
|
|
342
|
+
this.disableBc = disableBc
|
|
343
|
+
this.unsuccessfulReconnects = 0
|
|
344
|
+
this.messageHandlers = messageHandlers.slice()
|
|
345
|
+
/**
|
|
346
|
+
* @type {boolean}
|
|
347
|
+
*/
|
|
348
|
+
this._synced = false
|
|
349
|
+
/**
|
|
350
|
+
* The engine.io-client `Socket`, or null when disconnected.
|
|
351
|
+
* @type {Engine?}
|
|
352
|
+
*/
|
|
353
|
+
this.engine = null
|
|
354
|
+
this.lastMessageReceived = 0
|
|
355
|
+
/**
|
|
356
|
+
* Whether to connect to other peers or not
|
|
357
|
+
* @type {boolean}
|
|
358
|
+
*/
|
|
359
|
+
this.shouldConnect = connect
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @type {number}
|
|
363
|
+
*/
|
|
364
|
+
this._resyncInterval = 0
|
|
365
|
+
if (resyncInterval > 0) {
|
|
366
|
+
this._resyncInterval = /** @type {any} */ (setInterval(() => {
|
|
367
|
+
if (this.engine && this.engine.readyState === 'open') {
|
|
368
|
+
// resend sync step 1
|
|
369
|
+
const encoder = encoding.createEncoder()
|
|
370
|
+
encoding.writeVarUint(encoder, messageSync)
|
|
371
|
+
syncProtocol.writeSyncStep1(encoder, doc)
|
|
372
|
+
this.engine.send(encoding.toUint8Array(encoder))
|
|
373
|
+
}
|
|
374
|
+
}, resyncInterval))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* @param {ArrayBuffer} data
|
|
379
|
+
* @param {any} origin
|
|
380
|
+
*/
|
|
381
|
+
this._bcSubscriber = (data, origin) => {
|
|
382
|
+
if (origin !== this) {
|
|
383
|
+
const encoder = readMessage(this, new Uint8Array(data), false)
|
|
384
|
+
if (encoding.length(encoder) > 1) {
|
|
385
|
+
bc.publish(this.bcChannel, encoding.toUint8Array(encoder), this)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Listens to Yjs updates and sends them to remote peers (engine.io and broadcastchannel)
|
|
391
|
+
* @param {Uint8Array} update
|
|
392
|
+
* @param {any} origin
|
|
393
|
+
*/
|
|
394
|
+
this._updateHandler = (update, origin) => {
|
|
395
|
+
if (origin !== this) {
|
|
396
|
+
const encoder = encoding.createEncoder()
|
|
397
|
+
encoding.writeVarUint(encoder, messageSync)
|
|
398
|
+
syncProtocol.writeUpdate(encoder, update)
|
|
399
|
+
broadcastMessage(this, encoding.toUint8Array(encoder))
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
this.doc.on('update', this._updateHandler)
|
|
403
|
+
/**
|
|
404
|
+
* @param {any} changed
|
|
405
|
+
* @param {any} _origin
|
|
406
|
+
*/
|
|
407
|
+
this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
|
|
408
|
+
const changedClients = added.concat(updated).concat(removed)
|
|
409
|
+
const encoder = encoding.createEncoder()
|
|
410
|
+
encoding.writeVarUint(encoder, messageAwareness)
|
|
411
|
+
encoding.writeVarUint8Array(
|
|
412
|
+
encoder,
|
|
413
|
+
awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)
|
|
414
|
+
)
|
|
415
|
+
broadcastMessage(this, encoding.toUint8Array(encoder))
|
|
416
|
+
}
|
|
417
|
+
this._exitHandler = () => {
|
|
418
|
+
awarenessProtocol.removeAwarenessStates(
|
|
419
|
+
this.awareness,
|
|
420
|
+
[doc.clientID],
|
|
421
|
+
'app closed'
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
if (env.isNode && typeof process !== 'undefined') {
|
|
425
|
+
process.on('exit', this._exitHandler)
|
|
426
|
+
}
|
|
427
|
+
awareness.on('update', this._awarenessUpdateHandler)
|
|
428
|
+
this._checkInterval = /** @type {any} */ (setInterval(() => {
|
|
429
|
+
if (
|
|
430
|
+
this.connected &&
|
|
431
|
+
messageReconnectTimeout <
|
|
432
|
+
time.getUnixTime() - this.lastMessageReceived
|
|
433
|
+
) {
|
|
434
|
+
// no message received in a long time - not even your own awareness
|
|
435
|
+
// updates (which are updated every 15 seconds)
|
|
436
|
+
closeEngineConnection(this, /** @type {Engine} */ (this.engine), null)
|
|
437
|
+
}
|
|
438
|
+
}, messageReconnectTimeout / 10))
|
|
439
|
+
if (connect) {
|
|
440
|
+
this.connect()
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
get url () {
|
|
445
|
+
const encodedParams = url.encodeQueryParams({ ...this.params, room: this.roomname })
|
|
446
|
+
return this.serverUrl + (encodedParams.length === 0 ? '' : '?' + encodedParams)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* @type {boolean}
|
|
451
|
+
*/
|
|
452
|
+
get synced () {
|
|
453
|
+
return this._synced
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
set synced (state) {
|
|
457
|
+
if (this._synced !== state) {
|
|
458
|
+
this._synced = state
|
|
459
|
+
// @ts-ignore
|
|
460
|
+
this.emit('synced', [state])
|
|
461
|
+
this.emit('sync', [state])
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
destroy () {
|
|
466
|
+
if (this._resyncInterval !== 0) {
|
|
467
|
+
clearInterval(this._resyncInterval)
|
|
468
|
+
}
|
|
469
|
+
clearInterval(this._checkInterval)
|
|
470
|
+
this.disconnect()
|
|
471
|
+
if (env.isNode && typeof process !== 'undefined') {
|
|
472
|
+
process.off('exit', this._exitHandler)
|
|
473
|
+
}
|
|
474
|
+
this.awareness.off('update', this._awarenessUpdateHandler)
|
|
475
|
+
this.doc.off('update', this._updateHandler)
|
|
476
|
+
super.destroy()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
connectBc () {
|
|
480
|
+
if (this.disableBc) {
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
if (!this.bcconnected) {
|
|
484
|
+
bc.subscribe(this.bcChannel, this._bcSubscriber)
|
|
485
|
+
this.bcconnected = true
|
|
486
|
+
}
|
|
487
|
+
// send sync step1 to bc
|
|
488
|
+
// write sync step 1
|
|
489
|
+
const encoderSync = encoding.createEncoder()
|
|
490
|
+
encoding.writeVarUint(encoderSync, messageSync)
|
|
491
|
+
syncProtocol.writeSyncStep1(encoderSync, this.doc)
|
|
492
|
+
bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync), this)
|
|
493
|
+
// broadcast local state
|
|
494
|
+
const encoderState = encoding.createEncoder()
|
|
495
|
+
encoding.writeVarUint(encoderState, messageSync)
|
|
496
|
+
syncProtocol.writeSyncStep2(encoderState, this.doc)
|
|
497
|
+
bc.publish(this.bcChannel, encoding.toUint8Array(encoderState), this)
|
|
498
|
+
// write queryAwareness
|
|
499
|
+
const encoderAwarenessQuery = encoding.createEncoder()
|
|
500
|
+
encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness)
|
|
501
|
+
bc.publish(
|
|
502
|
+
this.bcChannel,
|
|
503
|
+
encoding.toUint8Array(encoderAwarenessQuery),
|
|
504
|
+
this
|
|
505
|
+
)
|
|
506
|
+
// broadcast local awareness state
|
|
507
|
+
const encoderAwarenessState = encoding.createEncoder()
|
|
508
|
+
encoding.writeVarUint(encoderAwarenessState, messageAwareness)
|
|
509
|
+
encoding.writeVarUint8Array(
|
|
510
|
+
encoderAwarenessState,
|
|
511
|
+
awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
|
512
|
+
this.doc.clientID
|
|
513
|
+
])
|
|
514
|
+
)
|
|
515
|
+
bc.publish(
|
|
516
|
+
this.bcChannel,
|
|
517
|
+
encoding.toUint8Array(encoderAwarenessState),
|
|
518
|
+
this
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
disconnectBc () {
|
|
523
|
+
// broadcast message with local awareness state set to null (indicating disconnect)
|
|
524
|
+
const encoder = encoding.createEncoder()
|
|
525
|
+
encoding.writeVarUint(encoder, messageAwareness)
|
|
526
|
+
encoding.writeVarUint8Array(
|
|
527
|
+
encoder,
|
|
528
|
+
awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
|
529
|
+
this.doc.clientID
|
|
530
|
+
], new Map())
|
|
531
|
+
)
|
|
532
|
+
broadcastMessage(this, encoding.toUint8Array(encoder))
|
|
533
|
+
if (this.bcconnected) {
|
|
534
|
+
bc.unsubscribe(this.bcChannel, this._bcSubscriber)
|
|
535
|
+
this.bcconnected = false
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
disconnect () {
|
|
540
|
+
this.shouldConnect = false
|
|
541
|
+
this.disconnectBc()
|
|
542
|
+
if (this.engine !== null) {
|
|
543
|
+
closeEngineConnection(this, this.engine, null)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
connect () {
|
|
548
|
+
this.shouldConnect = true
|
|
549
|
+
if (!this.connected && this.engine === null) {
|
|
550
|
+
setupEngine(this)
|
|
551
|
+
this.connectBc()
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|