implement new awareness protocol

This commit is contained in:
Kevin Jahns
2019-05-19 21:06:08 +02:00
parent b82399798c
commit fb18c6ce30
5 changed files with 203 additions and 172 deletions

View File

@@ -19,11 +19,148 @@ import * as mutex from 'lib0/mutex.js'
import { Observable } from 'lib0/observable.js'
const messageSync = 0
const messageQueryAwareness = 3
const messageAwareness = 1
const messageAuth = 2
const reconnectTimeout = 3000
/**
* Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
* The document name is attached to the provided url. I.e. the following example
* creates a websocket connection to http://localhost:1234/my-document-name
*
* @example
* import * as Y from 'yjs'
* import { WebsocketProvider } from 'y-websocket'
* const doc = new Y.Doc()
* const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)
*
* @extends {Observable<string>}
*/
export class WebsocketProvider extends Observable {
/**
* @param {string} url
* @param {string} roomname
* @param {Y.Doc} doc
*/
constructor (url, roomname, doc, awareness = new awarenessProtocol.Awareness(doc)) {
super()
window.addEventListener('beforeunload', () => {
awarenessProtocol.removeAwarenessStates(this.awareness, [this.doc.clientID], null)
})
// ensure that url is always ends with /
while (url[url.length - 1] === '/') {
url = url.slice(0, url.length - 1)
}
this.url = url + '/' + roomname
this.roomname = roomname
this.doc = doc
/**
* @type {Object<string,Object>}
*/
this._localAwarenessState = {}
this.awareness = awareness
this.wsconnected = false
this.mux = mutex.createMutex()
/**
* @type {WebSocket?}
*/
this.ws = null
this.shouldReconnect = true
/**
* @param {ArrayBuffer} data
*/
this._bcSubscriber = data => {
this.mux(() => {
const encoder = readMessage(this, new Uint8Array(data))
if (encoding.length(encoder) > 1) {
bc.publish(this.url, encoding.toUint8Array(encoder))
}
})
}
/**
* Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
* @param {Uint8Array} update
* @param {any} origin
*/
this._updateHandler = (update, origin) => {
if (origin !== this.ws || origin === null) {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeUpdate(encoder, update)
const buf = encoding.toUint8Array(encoder)
if (this.wsconnected) {
// @ts-ignore We know that wsconnected = true
this.ws.send(buf)
}
this.mux(() => {
bc.publish(this.url, buf)
})
}
}
/**
* @param {any} changed
* @param {any} origin
*/
this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
// only broadcast local awareness information and when ws connected
const predicate = /** @param {number} id */ id => id === doc.clientID
if (added.some(predicate) || updated.some(predicate) || removed.some(predicate)) {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, [doc.clientID]))
const buf = encoding.toUint8Array(encoder)
if (this.wsconnected && this.ws !== null) {
this.ws.send(buf)
}
this.mux(() => {
bc.publish(this.url, buf)
})
}
}
awareness.on('change', this._awarenessUpdateHandler)
this.connect()
}
destroy () {
this.disconnect()
this.awareness.off('change', this._awarenessUpdateHandler)
super.destroy()
}
disconnect () {
this.shouldReconnect = false
if (this.ws !== null) {
this.awareness.setLocalState(null)
this.ws.close()
bc.unsubscribe(this.url, this._bcSubscriber)
this.doc.off('update', this._updateHandler)
}
}
connect () {
this.shouldReconnect = true
if (!this.wsconnected && this.ws === null) {
if (this.awareness.getLocalState() === null) {
this.awareness.setLocalState({})
}
setupWS(this)
bc.subscribe(this.url, this._bcSubscriber)
// send sync step1 to bc
this.mux(() => {
// write sync step 1
const encoderSync = encoding.createEncoder()
encoding.writeVarUint(encoderSync, messageSync)
syncProtocol.writeSyncStep1(encoderSync, this.doc)
bc.publish(this.url, encoding.toUint8Array(encoderSync))
// write queryAwareness
const encoderAwareness = encoding.createEncoder()
encoding.writeVarUint(encoderAwareness, messageQueryAwareness)
bc.publish(this.url, encoding.toUint8Array(encoderAwareness))
})
this.doc.on('update', this._updateHandler)
}
}
}
/**
* @param {WebsocketProvider} provider
* @param {string} reason
@@ -44,13 +181,19 @@ const readMessage = (provider, buf) => {
encoding.writeVarUint(encoder, messageSync)
syncProtocol.readSyncMessage(decoder, encoder, provider.doc, provider.ws)
break
case messageQueryAwareness:
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())))
break
case messageAwareness:
provider.mux(() =>
awarenessProtocol.readAwarenessMessage(decoder, provider)
)
awarenessProtocol.applyAwarenessUpdate(provider.awareness, decoding.readVarUint8Array(decoder), provider)
break
case messageAuth:
authProtocol.readAuthMessage(decoder, provider, permissionDeniedHandler)
break
default:
console.error('Unable to compute message')
return encoder
}
return encoder
}
@@ -70,22 +213,14 @@ const setupWS = provider => {
}
websocket.onclose = () => {
provider.ws = null
provider.wsconnected = false
// update awareness (all users left)
/**
* @type {Array<number>}
*/
const removed = []
provider.getAwarenessInfo().forEach((_, clientID) => {
removed.push(clientID)
})
provider.awareness = new Map()
provider.emit('awareness', [{
added: [], updated: [], removed
}])
provider.emit('status', [{
status: 'disconnected'
}])
if (provider.wsconnected) {
provider.wsconnected = false
// update awareness (all users left)
awarenessProtocol.removeAwarenessStates(provider.awareness, Array.from(provider.awareness.getStates().keys()), provider)
provider.emit('status', [{
status: 'disconnected'
}])
}
if (provider.shouldReconnect) {
setTimeout(setupWS, reconnectTimeout, provider, provider.url)
}
@@ -100,103 +235,7 @@ const setupWS = provider => {
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeSyncStep1(encoder, provider.doc)
websocket.send(encoding.toUint8Array(encoder))
// force send stored awareness info
provider.setAwarenessField(null, null)
}
}
/**
* Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
* The document name is attached to the provided url. I.e. the following example
* creates a websocket connection to http://localhost:1234/my-document-name
*
* @example
* import * as Y from 'yjs'
* import { WebsocketProvider } from 'y-websocket'
* const doc = new Y.Doc()
* const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)
*
* @extends {Observable<string>}
*/
export class WebsocketProvider extends Observable {
/**
* @param {string} url
* @param {string} roomname
* @param {Y.Doc} doc
*/
constructor (url, roomname, doc) {
super()
// ensure that url is always ends with /
while (url[url.length - 1] === '/') {
url = url.slice(0, url.length - 1)
}
this.url = url + '/' + roomname
this.roomname = roomname
this.doc = doc
/**
* @type {Object<string,Object>}
*/
this._localAwarenessState = {}
this.awareness = new Map()
this.awarenessClock = new Map()
this.wsconnected = false
this.mux = mutex.createMutex()
/**
* @type {WebSocket?}
*/
this.ws = null
this.shouldReconnect = true
/**
* @param {ArrayBuffer} data
*/
this._bcSubscriber = data => {
const encoder = readMessage(this, new Uint8Array(data))
this.mux(() => {
if (encoding.length(encoder) > 1) {
bc.publish(url, encoding.toUint8Array(encoder))
}
})
}
/**
* @param {Uint8Array} update
* @param {any} origin
*/
this._updateHandler = (update, origin) => {
if (origin !== this.ws) {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeUpdate(encoder, update)
const buf = encoding.toUint8Array(encoder)
if (this.wsconnected) {
// @ts-ignore We know that wsconnected = true
this.ws.send(buf)
}
bc.publish(this.url, buf)
}
}
this.connect()
}
disconnect () {
this.shouldReconnect = false
if (this.ws !== null) {
this.ws.close()
bc.unsubscribe(this.url, this._bcSubscriber)
this.off('update', this._updateHandler)
}
}
connect () {
this.shouldReconnect = true
if (!this.wsconnected && this.ws === null) {
setupWS(this)
bc.subscribe(this.url, this._bcSubscriber)
// send sync step1 to bc
this.mux(() => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeSyncStep1(encoder, this.doc)
bc.publish(this.url, encoding.toUint8Array(encoder))
})
this.on('update', this._updateHandler)
}
// by updating the local awareness state we trigger the event handler that propagates this information to other clients.
provider.awareness.setLocalState(provider.awareness.getLocalState() || {})
}
}