implemented awareness protocol and added cursor support
This commit is contained in:
parent
31d6ef6296
commit
aafe15757f
@ -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<Y.Text | Y.XmlElement, PModel.Node>} 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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<Object, Set<number>>}
|
||||
*/
|
||||
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)
|
||||
|
@ -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 {
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
/**
|
||||
|
@ -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'
|
||||
|
||||
/**
|
||||
|
@ -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'
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
2
src/Y.js
2
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'
|
||||
|
||||
/**
|
||||
|
@ -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'
|
||||
|
98
src/protocols/awarenessProtocol.js
Normal file
98
src/protocols/awarenessProtocol.js
Normal file
@ -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<UserStateUpdate>} 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)
|
||||
}
|
||||
}
|
@ -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:
|
Loading…
x
Reference in New Issue
Block a user