From aafe15757f6870c5d41d31d60e51386048072216 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Fri, 9 Nov 2018 00:13:30 +0100 Subject: [PATCH] implemented awareness protocol and added cursor support --- .../ProsemirrorBinding/ProsemirrorBinding.js | 190 +++++++++++++----- examples/prosemirror/index.html | 22 ++ examples/prosemirror/index.js | 22 +- provider/websocket/WebSocketProvider.js | 44 +++- provider/websocket/server.js | 64 +++++- src/Store/OperationStore.js | 2 +- src/Struct/Delete.js | 2 +- src/Struct/ItemEmbed.js | 2 +- src/Struct/ItemFormat.js | 2 +- src/Struct/ItemJSON.js | 2 +- src/Struct/ItemString.js | 2 +- src/Types/YArray/YArray.js | 2 +- src/Types/YMap/YMap.js | 2 +- src/Types/YText/YText.js | 2 +- src/Types/YXml/YXmlFragment.js | 2 +- src/Y.js | 2 +- src/index.js | 3 +- src/protocols/awarenessProtocol.js | 98 +++++++++ src/{message.js => protocols/syncProtocol.js} | 26 +-- 19 files changed, 391 insertions(+), 100 deletions(-) create mode 100644 src/protocols/awarenessProtocol.js rename src/{message.js => protocols/syncProtocol.js} (95%) diff --git a/bindings/ProsemirrorBinding/ProsemirrorBinding.js b/bindings/ProsemirrorBinding/ProsemirrorBinding.js index 32473c8f..92f859e2 100644 --- a/bindings/ProsemirrorBinding/ProsemirrorBinding.js +++ b/bindings/ProsemirrorBinding/ProsemirrorBinding.js @@ -2,6 +2,8 @@ import BindMapping from '../BindMapping.js' import * as PModel from 'prosemirror-model' import * as Y from '../../src/index.js' import { createMutex } from '../../lib/mutex.js' +import { Plugin, PluginKey } from 'prosemirror-state' +import { Decoration, DecorationSet } from 'prosemirror-view' /** * @typedef {import('prosemirror-view').EditorView} EditorView @@ -9,65 +11,147 @@ import { createMutex } from '../../lib/mutex.js' * @typedef {BindMapping} ProsemirrorMapping */ -export default class ProsemirrorBinding { - /** - * @param {Y.XmlFragment} yDomFragment The bind source - * @param {EditorView} prosemirror The target binding - */ - constructor (yDomFragment, prosemirror) { - this.type = yDomFragment - this.prosemirror = prosemirror - const mux = createMutex() - this.mux = mux - /** - * @type {ProsemirrorMapping} - */ - const mapping = new BindMapping() - this.mapping = mapping - const oldDispatch = prosemirror.props.dispatchTransaction || null - /** - * @type {any} - */ - const updatedProps = { - dispatchTransaction: function (tr) { - // TODO: remove - const newState = prosemirror.state.apply(tr) - mux(() => { - updateYFragment(yDomFragment, newState, mapping) - }) - if (oldDispatch !== null) { - oldDispatch.call(this, tr) - } else { - prosemirror.updateState(newState) +export const prosemirrorPluginKey = new PluginKey('yjs') + +/** + * This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync. + * + * This plugin also keeps references to the type and the shared document so other plugins can access it. + * @param {Y.XmlFragment} yXmlFragment + */ +export const prosemirrorPlugin = yXmlFragment => { + const pluginState = { + type: yXmlFragment, + y: yXmlFragment._y, + binding: null + } + const plugin = new Plugin({ + key: prosemirrorPluginKey, + state: { + init: (initargs, state) => { + return pluginState + }, + apply: (tr, pluginState) => { + return pluginState + } + }, + view: view => { + const binding = new ProsemirrorBinding(yXmlFragment, view) + pluginState.binding = binding + return { + update: () => { + binding._prosemirrorChanged() + }, + destroy: () => { + binding.destroy() } } } - prosemirror.setProps(updatedProps) - yDomFragment.observeDeep(events => { - if (events.length === 0) { - return - } - mux(() => { - events.forEach(event => { - // recompute node for each parent - // except main node, compute main node in the end - let target = event.target - if (target !== yDomFragment) { - do { - if (target.constructor === Y.XmlElement) { - createNodeFromYElement(target, prosemirror.state.schema, mapping) - } - target = target._parent - } while (target._parent !== yDomFragment) - } - }) - const fragmentContent = yDomFragment.toArray().map(t => createNodeIfNotExists(t, prosemirror.state.schema, mapping)) - const tr = prosemirror.state.tr.replace(0, prosemirror.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0)) - const newState = prosemirror.updateState(prosemirror.state.apply(tr)) - console.log('state updated', newState, tr) + }) + return plugin +} + +export const cursorPluginKey = new PluginKey('yjs-cursor') + +export const cursorPlugin = new Plugin({ + key: cursorPluginKey, + props: { + decorations: state => { + const y = prosemirrorPluginKey.getState(state).y + const awareness = y.getAwarenessInfo() + const decorations = [] + awareness.forEach((state, userID) => { + if (state.cursor != null) { + const username = `User: ${userID}` + decorations.push(Decoration.widget(state.cursor.from, () => { + const cursor = document.createElement('span') + cursor.classList.add('ProseMirror-yjs-cursor') + const user = document.createElement('div') + user.insertBefore(document.createTextNode(username), null) + cursor.insertBefore(user, null) + return cursor + }, { key: username })) + decorations.push(Decoration.inline(state.cursor.from, state.cursor.to, { style: 'background-color: #ffa50070' })) + } }) + return DecorationSet.create(state.doc, decorations) + } + }, + view: view => { + const y = prosemirrorPluginKey.getState(view.state).y + const awarenessListener = () => { + console.log(y.getAwarenessInfo()) + view.updateState(view.state) + } + y.on('awareness', awarenessListener) + return { + update: () => { + const y = prosemirrorPluginKey.getState(view.state).y + const from = view.state.selection.from + const to = view.state.selection.to + const current = y.getLocalAwarenessInfo() + if (current.cursor == null || current.cursor.to !== to || current.cursor.from !== from) { + y.setAwarenessField('cursor', { + from, to + }) + } + }, + destroy: () => { + const y = prosemirrorPluginKey.getState(view.state).y + y.setAwarenessField('cursor', null) + y.off('awareness', awarenessListener) + } + } + } +}) + +export default class ProsemirrorBinding { + /** + * @param {Y.XmlFragment} yXmlFragment The bind source + * @param {EditorView} prosemirrorView The target binding + */ + constructor (yXmlFragment, prosemirrorView) { + this.type = yXmlFragment + this.prosemirrorView = prosemirrorView + this.mux = createMutex() + /** + * @type {ProsemirrorMapping} + */ + this.mapping = new BindMapping() + this._observeFunction = this._typeChanged.bind(this) + yXmlFragment.observeDeep(this._observeFunction) + } + _typeChanged (events) { + if (events.length === 0) { + return + } + this.mux(() => { + events.forEach(event => { + // recompute node for each parent + // except main node, compute main node in the end + let target = event.target + if (target !== this.type) { + do { + if (target.constructor === Y.XmlElement) { + createNodeFromYElement(target, this.prosemirrorView.state.schema, this.mapping) + } + target = target._parent + } while (target._parent !== this.type) + } + }) + const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping)) + const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0)) + this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr)) }) } + _prosemirrorChanged () { + this.mux(() => { + updateYFragment(this.type, this.prosemirrorView.state, this.mapping) + }) + } + destroy () { + this.type.unobserveDeep(this._observeFunction) + } } /** diff --git a/examples/prosemirror/index.html b/examples/prosemirror/index.html index b9772123..2ae69ddc 100644 --- a/examples/prosemirror/index.html +++ b/examples/prosemirror/index.html @@ -16,6 +16,28 @@ font-weight: bold; } .ProseMirror img { max-width: 100px } + .ProseMirror-yjs-cursor { + position: absolute; + border-left: black; + border-left-style: solid; + border-left-width: 2px; + border-color: orange; + height: 1em; + } + .ProseMirror-yjs-cursor > div { + position: relative; + top: -1.05em; + font-size: 13px; + background-color: rgb(250, 129, 0); + font-family: serif; + font-style: normal; + font-weight: normal; + line-height: normal; + user-select: none; + color: white; + padding-left: 2px; + padding-right: 2px; + } diff --git a/examples/prosemirror/index.js b/examples/prosemirror/index.js index cfbd513b..b0b53976 100644 --- a/examples/prosemirror/index.js +++ b/examples/prosemirror/index.js @@ -1,6 +1,6 @@ /* eslint-env browser */ import * as Y from '../../src/index.js' -import ProsemirrorBinding from '../../bindings/ProsemirrorBinding/ProsemirrorBinding.js' +import { prosemirrorPlugin, cursorPlugin } from '../../bindings/ProsemirrorBinding/ProsemirrorBinding.js' import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js' import {EditorState} from 'prosemirror-state' @@ -10,21 +10,24 @@ import {schema} from 'prosemirror-schema-basic' import {exampleSetup} from 'prosemirror-example-setup' import { PlaceholderPlugin, startImageUpload } from './PlaceholderPlugin.js' -const view = new EditorView(document.querySelector('#editor'), { - state: EditorState.create({ - doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')), - plugins: exampleSetup({schema}).concat(PlaceholderPlugin) - }) -}) - const provider = new WebsocketProvider('ws://localhost:1234/') const ydocument = provider.get('prosemirror') + /** * @type {any} */ const type = ydocument.define('prosemirror', Y.XmlFragment) -const prosemirrorBinding = new ProsemirrorBinding(type, view) +const view = new EditorView(document.querySelector('#editor'), { + state: EditorState.create({ + doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')), + plugins: exampleSetup({schema}).concat([PlaceholderPlugin, prosemirrorPlugin(type), cursorPlugin]) + }) +}) + +window.provider = provider +window.ydocument = ydocument +window.type = type window.view = view window.EditorState = EditorState window.EditorView = EditorView @@ -33,7 +36,6 @@ window.Fragment = Fragment window.Node = Node window.Schema = Schema window.Slice = Slice -window.prosemirrorBinding = prosemirrorBinding document.querySelector('#image-upload').addEventListener('change', e => { if (view.state.selection.$from.parent.inlineContent && e.target.files.length) { diff --git a/provider/websocket/WebSocketProvider.js b/provider/websocket/WebSocketProvider.js index ee1c36c8..14c31944 100644 --- a/provider/websocket/WebSocketProvider.js +++ b/provider/websocket/WebSocketProvider.js @@ -3,6 +3,9 @@ import * as Y from '../../src/index.js' export * from '../../src/index.js' +const messageSync = 0 +const messageAwareness = 1 + const reconnectTimeout = 100 const setupWS = (doc, url) => { @@ -12,10 +15,19 @@ const setupWS = (doc, url) => { websocket.onmessage = event => { const decoder = Y.createDecoder(event.data) const encoder = Y.createEncoder() - doc.mux(() => - Y.readMessage(decoder, encoder, doc) - ) - if (Y.length(encoder) > 0) { + const messageType = Y.readVarUint(decoder) + switch (messageType) { + case messageSync: + Y.writeVarUint(encoder, messageSync) + doc.mux(() => + Y.readSyncMessage(decoder, encoder, doc) + ) + break + case messageAwareness: + Y.readAwarenessMessage(decoder, doc) + break + } + if (Y.length(encoder) > 1) { websocket.send(Y.toBuffer(encoder)) } } @@ -34,8 +46,11 @@ const setupWS = (doc, url) => { }) // always send sync step 1 when connected const encoder = Y.createEncoder() + Y.writeVarUint(encoder, messageSync) Y.writeSyncStep1(encoder, doc) websocket.send(Y.toBuffer(encoder)) + // force send stored awareness info + doc.setAwarenessField(null, null) } } @@ -43,6 +58,7 @@ const broadcastUpdate = (y, transaction) => { if (y.wsconnected && transaction.encodedStructsLen > 0) { y.mux(() => { const encoder = Y.createEncoder() + Y.writeVarUint(encoder, messageSync) Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs) y.ws.send(Y.toBuffer(encoder)) }) @@ -54,9 +70,29 @@ class WebsocketsSharedDocument extends Y.Y { super() this.wsconnected = false this.mux = Y.createMutex() + this.ws = null + this._localAwarenessState = {} + this.awareness = new Map() setupWS(this, url) this.on('afterTransaction', broadcastUpdate) } + getLocalAwarenessInfo () { + return this._localAwarenessState + } + getAwarenessInfo () { + return this.awareness + } + setAwarenessField (field, value) { + if (field !== null) { + this._localAwarenessState[field] = value + } + if (this.ws !== null) { + const encoder = Y.createEncoder() + Y.writeVarUint(encoder, messageAwareness) + Y.writeUsersStateChange(encoder, [{ userID: this.userID, state: this._localAwarenessState }]) + this.ws.send(Y.toBuffer(encoder)) + } + } } export default class WebsocketProvider { diff --git a/provider/websocket/server.js b/provider/websocket/server.js index b23ed34d..5de9ed7f 100644 --- a/provider/websocket/server.js +++ b/provider/websocket/server.js @@ -3,12 +3,16 @@ const WebSocket = require('ws') const wss = new WebSocket.Server({ port: 1234 }) const docs = new Map() +const messageSync = 0 +const messageAwareness = 1 + const afterTransaction = (doc, transaction) => { if (transaction.encodedStructsLen > 0) { const encoder = Y.createEncoder() + Y.writeVarUint(encoder, messageSync) Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs) const message = Y.toBuffer(encoder) - doc.conns.forEach(conn => conn.send(message)) + doc.conns.forEach((_, conn) => conn.send(message)) } } @@ -16,7 +20,12 @@ class WSSharedDoc extends Y.Y { constructor () { super() this.mux = Y.createMutex() - this.conns = new Set() + /** + * Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed + * @type {Map>} + */ + this.conns = new Map() + this.awareness = new Map() this.on('afterTransaction', afterTransaction) } } @@ -24,9 +33,28 @@ class WSSharedDoc extends Y.Y { const messageListener = (conn, doc, message) => { const encoder = Y.createEncoder() const decoder = Y.createDecoder(message) - Y.readMessage(decoder, encoder, doc) - if (Y.length(encoder) > 0) { - conn.send(Y.toBuffer(encoder)) + const messageType = Y.readVarUint(decoder) + switch (messageType) { + case messageSync: + Y.writeVarUint(encoder, messageSync) + Y.readSyncMessage(decoder, encoder, doc) + if (Y.length(encoder) > 1) { + conn.send(Y.toBuffer(encoder)) + } + break + case messageAwareness: { + Y.writeVarUint(encoder, messageAwareness) + const updates = Y.forwardAwarenessMessage(decoder, encoder) + updates.forEach(update => { + doc.awareness.set(update.userID, update.state) + doc.conns.get(conn).add(update.userID) + }) + const buff = Y.toBuffer(encoder) + doc.conns.forEach((_, c) => { + c.send(buff) + }) + break + } } } @@ -38,16 +66,36 @@ const setupConnection = (conn, req) => { doc = new WSSharedDoc() docs.set(req.url.slice(1), doc) } - doc.conns.add(conn) + doc.conns.set(conn, new Set()) // listen and reply to events conn.on('message', message => messageListener(conn, doc, message)) - conn.on('close', () => + conn.on('close', () => { + const controlledIds = doc.conns.get(conn) doc.conns.delete(conn) - ) + const encoder = Y.createEncoder() + Y.writeVarUint(encoder, messageAwareness) + Y.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => { + doc.awareness.delete(userID) + return { userID, state: null } + })) + const buf = Y.toBuffer(encoder) + doc.conns.forEach((_, conn) => conn.send(buf)) + }) // send sync step 1 const encoder = Y.createEncoder() + Y.writeVarUint(encoder, messageSync) Y.writeSyncStep1(encoder, doc) conn.send(Y.toBuffer(encoder)) + if (doc.awareness.size > 0) { + const encoder = Y.createEncoder() + const userStates = [] + doc.awareness.forEach((state, userID) => { + userStates.push({ state, userID }) + }) + Y.writeVarUint(encoder, messageAwareness) + Y.writeUsersStateChange(encoder, userStates) + conn.send(Y.toBuffer(encoder)) + } } wss.on('connection', setupConnection) diff --git a/src/Store/OperationStore.js b/src/Store/OperationStore.js index 89cf096d..b5cea63b 100644 --- a/src/Store/OperationStore.js +++ b/src/Store/OperationStore.js @@ -1,7 +1,7 @@ import Tree from '../../lib/Tree.js' import * as ID from '../Util/ID.js' import { getStruct } from '../Util/structReferences.js' -import { stringifyID, stringifyItemID } from '../message.js' +import { stringifyID, stringifyItemID } from '../protocols/syncProtocol.js' import GC from '../Struct/GC.js' export default class OperationStore extends Tree { diff --git a/src/Struct/Delete.js b/src/Struct/Delete.js index e952d847..1a3c3aa0 100644 --- a/src/Struct/Delete.js +++ b/src/Struct/Delete.js @@ -1,6 +1,6 @@ import { getStructReference } from '../Util/structReferences.js' import * as ID from '../Util/ID.js' -import { stringifyID } from '../message.js' +import { stringifyID } from '../protocols/syncProtocol.js' import { writeStructToTransaction } from '../Util/Transaction.js' import * as decoding from '../../lib/decoding.js' import * as encoding from '../../lib/encoding.js' diff --git a/src/Struct/ItemEmbed.js b/src/Struct/ItemEmbed.js index df7c3ee9..71a75508 100644 --- a/src/Struct/ItemEmbed.js +++ b/src/Struct/ItemEmbed.js @@ -1,5 +1,5 @@ import Item from './Item.js' -import { logItemHelper } from '../message.js' +import { logItemHelper } from '../protocols/syncProtocol.js' import * as encoding from '../../lib/encoding.js' import * as decoding from '../../lib/decoding.js' diff --git a/src/Struct/ItemFormat.js b/src/Struct/ItemFormat.js index 9b019364..ed700cb4 100644 --- a/src/Struct/ItemFormat.js +++ b/src/Struct/ItemFormat.js @@ -1,5 +1,5 @@ import Item from './Item.js' -import { logItemHelper } from '../message.js' +import { logItemHelper } from '../protocols/syncProtocol.js' import * as encoding from '../../lib/encoding.js' import * as decoding from '../../lib/decoding.js' diff --git a/src/Struct/ItemJSON.js b/src/Struct/ItemJSON.js index b0dffb20..49e3294c 100644 --- a/src/Struct/ItemJSON.js +++ b/src/Struct/ItemJSON.js @@ -1,5 +1,5 @@ import Item, { splitHelper } from './Item.js' -import { logItemHelper } from '../message.js' +import { logItemHelper } from '../protocols/syncProtocol.js' import * as encoding from '../../lib/encoding.js' import * as decoding from '../../lib/decoding.js' diff --git a/src/Struct/ItemString.js b/src/Struct/ItemString.js index ffc38622..16a51d79 100644 --- a/src/Struct/ItemString.js +++ b/src/Struct/ItemString.js @@ -1,5 +1,5 @@ import Item, { splitHelper } from './Item.js' -import { logItemHelper } from '../message.js' +import { logItemHelper } from '../protocols/syncProtocol.js' import * as encoding from '../../lib/encoding.js' import * as decoding from '../../lib/decoding.js' diff --git a/src/Types/YArray/YArray.js b/src/Types/YArray/YArray.js index ffe96017..5625b81d 100644 --- a/src/Types/YArray/YArray.js +++ b/src/Types/YArray/YArray.js @@ -1,7 +1,7 @@ import Type from '../../Struct/Type.js' import ItemJSON from '../../Struct/ItemJSON.js' import ItemString from '../../Struct/ItemString.js' -import { stringifyItemID, logItemHelper } from '../../message.js' +import { stringifyItemID, logItemHelper } from '../../protocols/syncProtocol.js' import YEvent from '../../Util/YEvent.js' /** diff --git a/src/Types/YMap/YMap.js b/src/Types/YMap/YMap.js index d0d226d3..0130caa6 100644 --- a/src/Types/YMap/YMap.js +++ b/src/Types/YMap/YMap.js @@ -1,7 +1,7 @@ import Item from '../../Struct/Item.js' import Type from '../../Struct/Type.js' import ItemJSON from '../../Struct/ItemJSON.js' -import { logItemHelper } from '../../message.js' +import { logItemHelper } from '../../protocols/syncProtocol.js' import YEvent from '../../Util/YEvent.js' /** diff --git a/src/Types/YText/YText.js b/src/Types/YText/YText.js index 0ce1e030..69804f44 100644 --- a/src/Types/YText/YText.js +++ b/src/Types/YText/YText.js @@ -1,7 +1,7 @@ import ItemEmbed from '../../Struct/ItemEmbed.js' import ItemString from '../../Struct/ItemString.js' import ItemFormat from '../../Struct/ItemFormat.js' -import { logItemHelper } from '../../message.js' +import { logItemHelper } from '../../protocols/syncProtocol.js' import { YArrayEvent, default as YArray } from '../YArray/YArray.js' /** diff --git a/src/Types/YXml/YXmlFragment.js b/src/Types/YXml/YXmlFragment.js index cd581677..0329f2ea 100644 --- a/src/Types/YXml/YXmlFragment.js +++ b/src/Types/YXml/YXmlFragment.js @@ -3,7 +3,7 @@ import YXmlTreeWalker from './YXmlTreeWalker.js' import YArray from '../YArray/YArray.js' import YXmlEvent from './YXmlEvent.js' -import { logItemHelper } from '../../message.js' +import { logItemHelper } from '../../protocols/syncProtocol.js' /** * @typedef {import('./YXmlElement.js').default} YXmlElement diff --git a/src/Y.js b/src/Y.js index 2287f531..7debe579 100644 --- a/src/Y.js +++ b/src/Y.js @@ -6,7 +6,7 @@ import { createRootID } from './Util/ID.js' import NamedEventHandler from '../lib/NamedEventHandler.js' import Transaction from './Util/Transaction.js' import * as encoding from '../lib/encoding.js' -import * as message from './message.js' +import * as message from './protocols/syncProtocol.js' import { integrateRemoteStructs } from './Util/integrateRemoteStructs.js' /** diff --git a/src/index.js b/src/index.js index 87acf628..b6e72cd4 100644 --- a/src/index.js +++ b/src/index.js @@ -30,7 +30,8 @@ export { default as XmlElement } from './Types/YXml/YXmlElement.js' export { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js' export { registerStruct as registerType } from './Util/structReferences.js' -export * from './message.js' +export * from './protocols/syncProtocol.js' +export * from './protocols/awarenessProtocol.js' export * from '../lib/encoding.js' export * from '../lib/decoding.js' export * from '../lib/mutex.js' diff --git a/src/protocols/awarenessProtocol.js b/src/protocols/awarenessProtocol.js new file mode 100644 index 00000000..4e590b87 --- /dev/null +++ b/src/protocols/awarenessProtocol.js @@ -0,0 +1,98 @@ + +import * as encoding from '../../lib/encoding.js' +import * as decoding from '../../lib/decoding.js' + +const messageUsersStateChanged = 0 + +/** + * @typedef {Object} UserStateUpdate + * @property {number} UserStateUpdate.userID + * @property {Object} state + */ + +/** + * @param {encoding.Encoder} encoder + * @param {Array} stateUpdates + */ +export const writeUsersStateChange = (encoder, stateUpdates) => { + const len = stateUpdates.length + encoding.writeVarUint(encoder, messageUsersStateChanged) + encoding.writeVarUint(encoder, len) + for (let i = 0; i < len; i++) { + const {userID, state} = stateUpdates[i] + encoding.writeVarUint(encoder, userID) + encoding.writeVarString(encoder, JSON.stringify(state)) + } +} + +export const readUsersStateChange = (decoder, y) => { + const added = [] + const updated = [] + const removed = [] + const len = decoding.readVarUint(decoder) + for (let i = 0; i < len; i++) { + const userID = decoding.readVarUint(decoder) + const state = JSON.parse(decoding.readVarString(decoder)) + if (userID !== y.userID) { + if (state === null) { + if (y.awareness.has(userID)) { + y.awareness.delete(userID) + removed.push(userID) + } + } else { + if (y.awareness.has(userID)) { + updated.push(userID) + } else { + added.push(userID) + } + y.awareness.set(userID, state) + } + } + } + if (added.length > 0 || updated.length > 0 || removed.length > 0) { + y.emit('awareness', { + added, updated, removed + }) + } +} + +/** + * @param {decoding.Decoder} decoder + * @param {encoding.Encoder} encoder + */ +export const forwardUsersStateChange = (decoder, encoder) => { + const len = decoding.readVarUint(decoder) + const updates = [] + encoding.writeVarUint(encoder, messageUsersStateChanged) + encoding.writeVarUint(encoder, len) + for (let i = 0; i < len; i++) { + const userID = decoding.readVarUint(decoder) + const state = decoding.readVarString(decoder) + encoding.writeVarUint(encoder, userID) + encoding.writeVarString(encoder, state) + updates.push({userID, state: JSON.parse(state)}) + } + return updates +} + +/** + * @param {decoding.Decoder} decoder + */ +export const readAwarenessMessage = (decoder, y) => { + switch (decoding.readVarUint(decoder)) { + case messageUsersStateChanged: + readUsersStateChange(decoder, y) + break + } +} + +/** + * @param {decoding.Decoder} decoder + * @param {encoding.Encoder} encoder + */ +export const forwardAwarenessMessage = (decoder, encoder) => { + switch (decoding.readVarUint(decoder)) { + case messageUsersStateChanged: + return forwardUsersStateChange(decoder, encoder) + } +} diff --git a/src/message.js b/src/protocols/syncProtocol.js similarity index 95% rename from src/message.js rename to src/protocols/syncProtocol.js index 0ce1ae3d..c779bcc4 100644 --- a/src/message.js +++ b/src/protocols/syncProtocol.js @@ -1,17 +1,17 @@ -import * as encoding from '../lib/encoding.js' -import * as decoding from '../lib/decoding.js' -import * as ID from './Util/ID.js' -import { getStruct } from './Util/structReferences.js' -import { deleteItemRange } from './Struct/Delete.js' -import { integrateRemoteStruct } from './Util/integrateRemoteStructs.js' -import Item from './Struct/Item.js' +import * as encoding from '../../lib/encoding.js' +import * as decoding from '../../lib/decoding.js' +import * as ID from '../Util/ID.js' +import { getStruct } from '../Util/structReferences.js' +import { deleteItemRange } from '../Struct/Delete.js' +import { integrateRemoteStruct } from '../Util/integrateRemoteStructs.js' +import Item from '../Struct/Item.js' /** - * @typedef {import('./Store/StateStore.js').default} StateStore - * @typedef {import('./Y.js').default} Y - * @typedef {import('./Struct/Item.js').default} Item - * @typedef {import('./Store/StateStore.js').StateSet} StateSet + * @typedef {import('../Store/StateStore.js').default} StateStore + * @typedef {import('../Y.js').default} Y + * @typedef {import('../Struct/Item.js').default} Item + * @typedef {import('../Store/StateStore.js').StateSet} StateSet */ /** @@ -439,7 +439,7 @@ export const readUpdate = readStructs * @param {Y} y * @return {string} The message converted to string */ -export const stringifyMessage = (decoder, y) => { +export const stringifySyncMessage = (decoder, y) => { const messageType = decoding.readVarUint(decoder) let stringifiedMessage let stringifiedMessageType @@ -468,7 +468,7 @@ export const stringifyMessage = (decoder, y) => { * @param {encoding.Encoder} encoder The reply message. Will not be sent if empty. * @param {Y} y */ -export const readMessage = (decoder, encoder, y) => { +export const readSyncMessage = (decoder, encoder, y) => { const messageType = decoding.readVarUint(decoder) switch (messageType) { case messageYjsSyncStep1: