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 PModel from 'prosemirror-model'
|
||||||
import * as Y from '../../src/index.js'
|
import * as Y from '../../src/index.js'
|
||||||
import { createMutex } from '../../lib/mutex.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
|
* @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
|
* @typedef {BindMapping<Y.Text | Y.XmlElement, PModel.Node>} ProsemirrorMapping
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class ProsemirrorBinding {
|
export const prosemirrorPluginKey = new PluginKey('yjs')
|
||||||
/**
|
|
||||||
* @param {Y.XmlFragment} yDomFragment The bind source
|
/**
|
||||||
* @param {EditorView} prosemirror The target binding
|
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
|
||||||
*/
|
*
|
||||||
constructor (yDomFragment, prosemirror) {
|
* This plugin also keeps references to the type and the shared document so other plugins can access it.
|
||||||
this.type = yDomFragment
|
* @param {Y.XmlFragment} yXmlFragment
|
||||||
this.prosemirror = prosemirror
|
*/
|
||||||
const mux = createMutex()
|
export const prosemirrorPlugin = yXmlFragment => {
|
||||||
this.mux = mux
|
const pluginState = {
|
||||||
/**
|
type: yXmlFragment,
|
||||||
* @type {ProsemirrorMapping}
|
y: yXmlFragment._y,
|
||||||
*/
|
binding: null
|
||||||
const mapping = new BindMapping()
|
}
|
||||||
this.mapping = mapping
|
const plugin = new Plugin({
|
||||||
const oldDispatch = prosemirror.props.dispatchTransaction || null
|
key: prosemirrorPluginKey,
|
||||||
/**
|
state: {
|
||||||
* @type {any}
|
init: (initargs, state) => {
|
||||||
*/
|
return pluginState
|
||||||
const updatedProps = {
|
},
|
||||||
dispatchTransaction: function (tr) {
|
apply: (tr, pluginState) => {
|
||||||
// TODO: remove
|
return pluginState
|
||||||
const newState = prosemirror.state.apply(tr)
|
}
|
||||||
mux(() => {
|
},
|
||||||
updateYFragment(yDomFragment, newState, mapping)
|
view: view => {
|
||||||
})
|
const binding = new ProsemirrorBinding(yXmlFragment, view)
|
||||||
if (oldDispatch !== null) {
|
pluginState.binding = binding
|
||||||
oldDispatch.call(this, tr)
|
return {
|
||||||
} else {
|
update: () => {
|
||||||
prosemirror.updateState(newState)
|
binding._prosemirrorChanged()
|
||||||
|
},
|
||||||
|
destroy: () => {
|
||||||
|
binding.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prosemirror.setProps(updatedProps)
|
})
|
||||||
yDomFragment.observeDeep(events => {
|
return plugin
|
||||||
if (events.length === 0) {
|
}
|
||||||
return
|
|
||||||
}
|
export const cursorPluginKey = new PluginKey('yjs-cursor')
|
||||||
mux(() => {
|
|
||||||
events.forEach(event => {
|
export const cursorPlugin = new Plugin({
|
||||||
// recompute node for each parent
|
key: cursorPluginKey,
|
||||||
// except main node, compute main node in the end
|
props: {
|
||||||
let target = event.target
|
decorations: state => {
|
||||||
if (target !== yDomFragment) {
|
const y = prosemirrorPluginKey.getState(state).y
|
||||||
do {
|
const awareness = y.getAwarenessInfo()
|
||||||
if (target.constructor === Y.XmlElement) {
|
const decorations = []
|
||||||
createNodeFromYElement(target, prosemirror.state.schema, mapping)
|
awareness.forEach((state, userID) => {
|
||||||
}
|
if (state.cursor != null) {
|
||||||
target = target._parent
|
const username = `User: ${userID}`
|
||||||
} while (target._parent !== yDomFragment)
|
decorations.push(Decoration.widget(state.cursor.from, () => {
|
||||||
}
|
const cursor = document.createElement('span')
|
||||||
})
|
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||||
const fragmentContent = yDomFragment.toArray().map(t => createNodeIfNotExists(t, prosemirror.state.schema, mapping))
|
const user = document.createElement('div')
|
||||||
const tr = prosemirror.state.tr.replace(0, prosemirror.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
|
user.insertBefore(document.createTextNode(username), null)
|
||||||
const newState = prosemirror.updateState(prosemirror.state.apply(tr))
|
cursor.insertBefore(user, null)
|
||||||
console.log('state updated', newState, tr)
|
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;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.ProseMirror img { max-width: 100px }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
import * as Y from '../../src/index.js'
|
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 WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
|
||||||
|
|
||||||
import {EditorState} from 'prosemirror-state'
|
import {EditorState} from 'prosemirror-state'
|
||||||
@ -10,21 +10,24 @@ import {schema} from 'prosemirror-schema-basic'
|
|||||||
import {exampleSetup} from 'prosemirror-example-setup'
|
import {exampleSetup} from 'prosemirror-example-setup'
|
||||||
import { PlaceholderPlugin, startImageUpload } from './PlaceholderPlugin.js'
|
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 provider = new WebsocketProvider('ws://localhost:1234/')
|
||||||
const ydocument = provider.get('prosemirror')
|
const ydocument = provider.get('prosemirror')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {any}
|
||||||
*/
|
*/
|
||||||
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
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.view = view
|
||||||
window.EditorState = EditorState
|
window.EditorState = EditorState
|
||||||
window.EditorView = EditorView
|
window.EditorView = EditorView
|
||||||
@ -33,7 +36,6 @@ window.Fragment = Fragment
|
|||||||
window.Node = Node
|
window.Node = Node
|
||||||
window.Schema = Schema
|
window.Schema = Schema
|
||||||
window.Slice = Slice
|
window.Slice = Slice
|
||||||
window.prosemirrorBinding = prosemirrorBinding
|
|
||||||
|
|
||||||
document.querySelector('#image-upload').addEventListener('change', e => {
|
document.querySelector('#image-upload').addEventListener('change', e => {
|
||||||
if (view.state.selection.$from.parent.inlineContent && e.target.files.length) {
|
if (view.state.selection.$from.parent.inlineContent && e.target.files.length) {
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
import * as Y from '../../src/index.js'
|
import * as Y from '../../src/index.js'
|
||||||
export * from '../../src/index.js'
|
export * from '../../src/index.js'
|
||||||
|
|
||||||
|
const messageSync = 0
|
||||||
|
const messageAwareness = 1
|
||||||
|
|
||||||
const reconnectTimeout = 100
|
const reconnectTimeout = 100
|
||||||
|
|
||||||
const setupWS = (doc, url) => {
|
const setupWS = (doc, url) => {
|
||||||
@ -12,10 +15,19 @@ const setupWS = (doc, url) => {
|
|||||||
websocket.onmessage = event => {
|
websocket.onmessage = event => {
|
||||||
const decoder = Y.createDecoder(event.data)
|
const decoder = Y.createDecoder(event.data)
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.createEncoder()
|
||||||
doc.mux(() =>
|
const messageType = Y.readVarUint(decoder)
|
||||||
Y.readMessage(decoder, encoder, doc)
|
switch (messageType) {
|
||||||
)
|
case messageSync:
|
||||||
if (Y.length(encoder) > 0) {
|
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))
|
websocket.send(Y.toBuffer(encoder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,8 +46,11 @@ const setupWS = (doc, url) => {
|
|||||||
})
|
})
|
||||||
// always send sync step 1 when connected
|
// always send sync step 1 when connected
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.createEncoder()
|
||||||
|
Y.writeVarUint(encoder, messageSync)
|
||||||
Y.writeSyncStep1(encoder, doc)
|
Y.writeSyncStep1(encoder, doc)
|
||||||
websocket.send(Y.toBuffer(encoder))
|
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) {
|
if (y.wsconnected && transaction.encodedStructsLen > 0) {
|
||||||
y.mux(() => {
|
y.mux(() => {
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.createEncoder()
|
||||||
|
Y.writeVarUint(encoder, messageSync)
|
||||||
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||||
y.ws.send(Y.toBuffer(encoder))
|
y.ws.send(Y.toBuffer(encoder))
|
||||||
})
|
})
|
||||||
@ -54,9 +70,29 @@ class WebsocketsSharedDocument extends Y.Y {
|
|||||||
super()
|
super()
|
||||||
this.wsconnected = false
|
this.wsconnected = false
|
||||||
this.mux = Y.createMutex()
|
this.mux = Y.createMutex()
|
||||||
|
this.ws = null
|
||||||
|
this._localAwarenessState = {}
|
||||||
|
this.awareness = new Map()
|
||||||
setupWS(this, url)
|
setupWS(this, url)
|
||||||
this.on('afterTransaction', broadcastUpdate)
|
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 {
|
export default class WebsocketProvider {
|
||||||
|
@ -3,12 +3,16 @@ const WebSocket = require('ws')
|
|||||||
const wss = new WebSocket.Server({ port: 1234 })
|
const wss = new WebSocket.Server({ port: 1234 })
|
||||||
const docs = new Map()
|
const docs = new Map()
|
||||||
|
|
||||||
|
const messageSync = 0
|
||||||
|
const messageAwareness = 1
|
||||||
|
|
||||||
const afterTransaction = (doc, transaction) => {
|
const afterTransaction = (doc, transaction) => {
|
||||||
if (transaction.encodedStructsLen > 0) {
|
if (transaction.encodedStructsLen > 0) {
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.createEncoder()
|
||||||
|
Y.writeVarUint(encoder, messageSync)
|
||||||
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||||
const message = Y.toBuffer(encoder)
|
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 () {
|
constructor () {
|
||||||
super()
|
super()
|
||||||
this.mux = Y.createMutex()
|
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)
|
this.on('afterTransaction', afterTransaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,9 +33,28 @@ class WSSharedDoc extends Y.Y {
|
|||||||
const messageListener = (conn, doc, message) => {
|
const messageListener = (conn, doc, message) => {
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.createEncoder()
|
||||||
const decoder = Y.createDecoder(message)
|
const decoder = Y.createDecoder(message)
|
||||||
Y.readMessage(decoder, encoder, doc)
|
const messageType = Y.readVarUint(decoder)
|
||||||
if (Y.length(encoder) > 0) {
|
switch (messageType) {
|
||||||
conn.send(Y.toBuffer(encoder))
|
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()
|
doc = new WSSharedDoc()
|
||||||
docs.set(req.url.slice(1), doc)
|
docs.set(req.url.slice(1), doc)
|
||||||
}
|
}
|
||||||
doc.conns.add(conn)
|
doc.conns.set(conn, new Set())
|
||||||
// listen and reply to events
|
// listen and reply to events
|
||||||
conn.on('message', message => messageListener(conn, doc, message))
|
conn.on('message', message => messageListener(conn, doc, message))
|
||||||
conn.on('close', () =>
|
conn.on('close', () => {
|
||||||
|
const controlledIds = doc.conns.get(conn)
|
||||||
doc.conns.delete(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
|
// send sync step 1
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.createEncoder()
|
||||||
|
Y.writeVarUint(encoder, messageSync)
|
||||||
Y.writeSyncStep1(encoder, doc)
|
Y.writeSyncStep1(encoder, doc)
|
||||||
conn.send(Y.toBuffer(encoder))
|
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)
|
wss.on('connection', setupConnection)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Tree from '../../lib/Tree.js'
|
import Tree from '../../lib/Tree.js'
|
||||||
import * as ID from '../Util/ID.js'
|
import * as ID from '../Util/ID.js'
|
||||||
import { getStruct } from '../Util/structReferences.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'
|
import GC from '../Struct/GC.js'
|
||||||
|
|
||||||
export default class OperationStore extends Tree {
|
export default class OperationStore extends Tree {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { getStructReference } from '../Util/structReferences.js'
|
import { getStructReference } from '../Util/structReferences.js'
|
||||||
import * as ID from '../Util/ID.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 { writeStructToTransaction } from '../Util/Transaction.js'
|
||||||
import * as decoding from '../../lib/decoding.js'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
import * as encoding from '../../lib/encoding.js'
|
import * as encoding from '../../lib/encoding.js'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Item from './Item.js'
|
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 encoding from '../../lib/encoding.js'
|
||||||
import * as decoding from '../../lib/decoding.js'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Item from './Item.js'
|
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 encoding from '../../lib/encoding.js'
|
||||||
import * as decoding from '../../lib/decoding.js'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Item, { splitHelper } from './Item.js'
|
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 encoding from '../../lib/encoding.js'
|
||||||
import * as decoding from '../../lib/decoding.js'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Item, { splitHelper } from './Item.js'
|
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 encoding from '../../lib/encoding.js'
|
||||||
import * as decoding from '../../lib/decoding.js'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Type from '../../Struct/Type.js'
|
import Type from '../../Struct/Type.js'
|
||||||
import ItemJSON from '../../Struct/ItemJSON.js'
|
import ItemJSON from '../../Struct/ItemJSON.js'
|
||||||
import ItemString from '../../Struct/ItemString.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'
|
import YEvent from '../../Util/YEvent.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Item from '../../Struct/Item.js'
|
import Item from '../../Struct/Item.js'
|
||||||
import Type from '../../Struct/Type.js'
|
import Type from '../../Struct/Type.js'
|
||||||
import ItemJSON from '../../Struct/ItemJSON.js'
|
import ItemJSON from '../../Struct/ItemJSON.js'
|
||||||
import { logItemHelper } from '../../message.js'
|
import { logItemHelper } from '../../protocols/syncProtocol.js'
|
||||||
import YEvent from '../../Util/YEvent.js'
|
import YEvent from '../../Util/YEvent.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import ItemEmbed from '../../Struct/ItemEmbed.js'
|
import ItemEmbed from '../../Struct/ItemEmbed.js'
|
||||||
import ItemString from '../../Struct/ItemString.js'
|
import ItemString from '../../Struct/ItemString.js'
|
||||||
import ItemFormat from '../../Struct/ItemFormat.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'
|
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 YArray from '../YArray/YArray.js'
|
||||||
import YXmlEvent from './YXmlEvent.js'
|
import YXmlEvent from './YXmlEvent.js'
|
||||||
import { logItemHelper } from '../../message.js'
|
import { logItemHelper } from '../../protocols/syncProtocol.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./YXmlElement.js').default} YXmlElement
|
* @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 NamedEventHandler from '../lib/NamedEventHandler.js'
|
||||||
import Transaction from './Util/Transaction.js'
|
import Transaction from './Util/Transaction.js'
|
||||||
import * as encoding from '../lib/encoding.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'
|
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 { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
|
||||||
export { registerStruct as registerType } from './Util/structReferences.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/encoding.js'
|
||||||
export * from '../lib/decoding.js'
|
export * from '../lib/decoding.js'
|
||||||
export * from '../lib/mutex.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 encoding from '../../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.js'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
import * as ID from './Util/ID.js'
|
import * as ID from '../Util/ID.js'
|
||||||
import { getStruct } from './Util/structReferences.js'
|
import { getStruct } from '../Util/structReferences.js'
|
||||||
import { deleteItemRange } from './Struct/Delete.js'
|
import { deleteItemRange } from '../Struct/Delete.js'
|
||||||
import { integrateRemoteStruct } from './Util/integrateRemoteStructs.js'
|
import { integrateRemoteStruct } from '../Util/integrateRemoteStructs.js'
|
||||||
import Item from './Struct/Item.js'
|
import Item from '../Struct/Item.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./Store/StateStore.js').default} StateStore
|
* @typedef {import('../Store/StateStore.js').default} StateStore
|
||||||
* @typedef {import('./Y.js').default} Y
|
* @typedef {import('../Y.js').default} Y
|
||||||
* @typedef {import('./Struct/Item.js').default} Item
|
* @typedef {import('../Struct/Item.js').default} Item
|
||||||
* @typedef {import('./Store/StateStore.js').StateSet} StateSet
|
* @typedef {import('../Store/StateStore.js').StateSet} StateSet
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -439,7 +439,7 @@ export const readUpdate = readStructs
|
|||||||
* @param {Y} y
|
* @param {Y} y
|
||||||
* @return {string} The message converted to string
|
* @return {string} The message converted to string
|
||||||
*/
|
*/
|
||||||
export const stringifyMessage = (decoder, y) => {
|
export const stringifySyncMessage = (decoder, y) => {
|
||||||
const messageType = decoding.readVarUint(decoder)
|
const messageType = decoding.readVarUint(decoder)
|
||||||
let stringifiedMessage
|
let stringifiedMessage
|
||||||
let stringifiedMessageType
|
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 {encoding.Encoder} encoder The reply message. Will not be sent if empty.
|
||||||
* @param {Y} y
|
* @param {Y} y
|
||||||
*/
|
*/
|
||||||
export const readMessage = (decoder, encoder, y) => {
|
export const readSyncMessage = (decoder, encoder, y) => {
|
||||||
const messageType = decoding.readVarUint(decoder)
|
const messageType = decoding.readVarUint(decoder)
|
||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
case messageYjsSyncStep1:
|
case messageYjsSyncStep1:
|
Loading…
x
Reference in New Issue
Block a user