added utilities to make and recover snapshots
This commit is contained in:
		
							parent
							
								
									77e479c03b
								
							
						
					
					
						commit
						3a0694c35c
					
				@ -11,6 +11,8 @@ import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state
 | 
				
			|||||||
import * as math from '../lib/math.js'
 | 
					import * as math from '../lib/math.js'
 | 
				
			||||||
import * as object from '../lib/object.js'
 | 
					import * as object from '../lib/object.js'
 | 
				
			||||||
import * as YPos from '../utils/relativePosition.js'
 | 
					import * as YPos from '../utils/relativePosition.js'
 | 
				
			||||||
 | 
					import { isVisible } from '../utils/snapshot.js'
 | 
				
			||||||
 | 
					import { simpleDiff } from '../lib/diff.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
 | 
					 * @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
 | 
				
			||||||
@ -33,6 +35,9 @@ export const prosemirrorPluginKey = new PluginKey('yjs')
 | 
				
			|||||||
export const prosemirrorPlugin = yXmlFragment => {
 | 
					export const prosemirrorPlugin = yXmlFragment => {
 | 
				
			||||||
  let changedInitialContent = false
 | 
					  let changedInitialContent = false
 | 
				
			||||||
  const plugin = new Plugin({
 | 
					  const plugin = new Plugin({
 | 
				
			||||||
 | 
					    props: {
 | 
				
			||||||
 | 
					      editable: (state) => prosemirrorPluginKey.getState(state).snapshot == null
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    key: prosemirrorPluginKey,
 | 
					    key: prosemirrorPluginKey,
 | 
				
			||||||
    state: {
 | 
					    state: {
 | 
				
			||||||
      init: (initargs, state) => {
 | 
					      init: (initargs, state) => {
 | 
				
			||||||
@ -55,7 +60,16 @@ export const prosemirrorPlugin = yXmlFragment => {
 | 
				
			|||||||
          if (change !== undefined && change.snapshot !== undefined) {
 | 
					          if (change !== undefined && change.snapshot !== undefined) {
 | 
				
			||||||
            // snapshot changed, rerender next
 | 
					            // snapshot changed, rerender next
 | 
				
			||||||
            setTimeout(() => {
 | 
					            setTimeout(() => {
 | 
				
			||||||
              pluginState.binding._renderSnapshot(change.snapshot)
 | 
					              if (change.restore == null) {
 | 
				
			||||||
 | 
					                pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot)
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                pluginState.binding._renderSnapshot(change.snapshot, change.snapshot)
 | 
				
			||||||
 | 
					                // reset to current prosemirror state
 | 
				
			||||||
 | 
					                delete pluginState.restore
 | 
				
			||||||
 | 
					                delete pluginState.snapshot
 | 
				
			||||||
 | 
					                delete pluginState.prevSnapshot
 | 
				
			||||||
 | 
					                pluginState.binding._prosemirrorChanged(pluginState.binding.prosemirrorView.state.doc)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            }, 0)
 | 
					            }, 0)
 | 
				
			||||||
          } else if (pluginState.snapshot == null) {
 | 
					          } else if (pluginState.snapshot == null) {
 | 
				
			||||||
            // only apply if no snapshot active
 | 
					            // only apply if no snapshot active
 | 
				
			||||||
@ -112,7 +126,14 @@ export const cursorPlugin = new Plugin({
 | 
				
			|||||||
      const y = ystate.y
 | 
					      const y = ystate.y
 | 
				
			||||||
      const awareness = y.getAwarenessInfo()
 | 
					      const awareness = y.getAwarenessInfo()
 | 
				
			||||||
      const decorations = []
 | 
					      const decorations = []
 | 
				
			||||||
 | 
					      if (ystate.snapshot != null) {
 | 
				
			||||||
 | 
					        // do not render cursors while snapshot is active
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      awareness.forEach((aw, userID) => {
 | 
					      awareness.forEach((aw, userID) => {
 | 
				
			||||||
 | 
					        if (userID === y.userID) {
 | 
				
			||||||
 | 
					          return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (aw.cursor != null) {
 | 
					        if (aw.cursor != null) {
 | 
				
			||||||
          let user = aw.user || {}
 | 
					          let user = aw.user || {}
 | 
				
			||||||
          if (user.color == null) {
 | 
					          if (user.color == null) {
 | 
				
			||||||
@ -154,7 +175,7 @@ export const cursorPlugin = new Plugin({
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    const updateCursorInfo = () => {
 | 
					    const updateCursorInfo = () => {
 | 
				
			||||||
      const current = y.getLocalAwarenessInfo()
 | 
					      const current = y.getLocalAwarenessInfo()
 | 
				
			||||||
      if (view.hasFocus()) {
 | 
					      if (view.hasFocus() && ystate.binding !== null) {
 | 
				
			||||||
        const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
 | 
					        const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
 | 
				
			||||||
        const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
 | 
					        const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
 | 
				
			||||||
        if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
 | 
					        if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
 | 
				
			||||||
@ -321,11 +342,24 @@ export class ProsemirrorBinding {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
    yXmlFragment.observeDeep(this._observeFunction)
 | 
					    yXmlFragment.observeDeep(this._observeFunction)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  _renderSnapshot (snapshot) {
 | 
					  _forceRerender () {
 | 
				
			||||||
 | 
					    this.mapping = new Map()
 | 
				
			||||||
 | 
					    this.mux(() => {
 | 
				
			||||||
 | 
					      const fragmentContent = this.type.toArray().map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null)
 | 
				
			||||||
 | 
					      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.dispatch(tr)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {*} snapshot
 | 
				
			||||||
 | 
					   * @param {*} prevSnapshot
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  _renderSnapshot (snapshot, prevSnapshot) {
 | 
				
			||||||
    // clear mapping because we are going to rerender
 | 
					    // clear mapping because we are going to rerender
 | 
				
			||||||
    this.mapping = new Map()
 | 
					    this.mapping = new Map()
 | 
				
			||||||
    this.mux(() => {
 | 
					    this.mux(() => {
 | 
				
			||||||
      const fragmentContent = this.type.toArray(snapshot).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot)).filter(n => n !== null)
 | 
					      const fragmentContent = this.type.toArray({ sm: snapshot.sm, ds: prevSnapshot.ds}).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot, prevSnapshot)).filter(n => n !== null)
 | 
				
			||||||
      const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
 | 
					      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.dispatch(tr)
 | 
					      this.prosemirrorView.dispatch(tr)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -370,12 +404,13 @@ export class ProsemirrorBinding {
 | 
				
			|||||||
 * @param {PModel.Schema} schema
 | 
					 * @param {PModel.Schema} schema
 | 
				
			||||||
 * @param {ProsemirrorMapping} mapping
 | 
					 * @param {ProsemirrorMapping} mapping
 | 
				
			||||||
 * @param {HistorySnapshot} [snapshot]
 | 
					 * @param {HistorySnapshot} [snapshot]
 | 
				
			||||||
 | 
					 * @param {HistorySnapshot} [prevSnapshot]
 | 
				
			||||||
 * @return {PModel.Node}
 | 
					 * @return {PModel.Node}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const createNodeIfNotExists = (el, schema, mapping, snapshot) => {
 | 
					export const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot) => {
 | 
				
			||||||
  const node = mapping.get(el)
 | 
					  const node = mapping.get(el)
 | 
				
			||||||
  if (node === undefined) {
 | 
					  if (node === undefined) {
 | 
				
			||||||
    return createNodeFromYElement(el, schema, mapping, snapshot)
 | 
					    return createNodeFromYElement(el, schema, mapping, snapshot, prevSnapshot)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return node
 | 
					  return node
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -385,19 +420,31 @@ export const createNodeIfNotExists = (el, schema, mapping, snapshot) => {
 | 
				
			|||||||
 * @param {YXmlElement} el
 | 
					 * @param {YXmlElement} el
 | 
				
			||||||
 * @param {PModel.Schema} schema
 | 
					 * @param {PModel.Schema} schema
 | 
				
			||||||
 * @param {ProsemirrorMapping} mapping
 | 
					 * @param {ProsemirrorMapping} mapping
 | 
				
			||||||
 * @param {import('../protocols/history.js').HistorySnapshot} snapshot
 | 
					 * @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
 | 
				
			||||||
 | 
					 * @param {import('../protocols/history.js').HistorySnapshot} [prevSnapshot]
 | 
				
			||||||
 * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
 | 
					 * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const createNodeFromYElement = (el, schema, mapping, snapshot) => {
 | 
					export const createNodeFromYElement = (el, schema, mapping, snapshot, prevSnapshot) => {
 | 
				
			||||||
 | 
					  let _snapshot = snapshot
 | 
				
			||||||
 | 
					  let _prevSnapshot = prevSnapshot
 | 
				
			||||||
 | 
					  if (snapshot !== undefined && prevSnapshot !== undefined) {
 | 
				
			||||||
 | 
					    if (!isVisible(el, snapshot)) {
 | 
				
			||||||
 | 
					      // if this element is already rendered as deleted (ychange), then do not render children as deleted
 | 
				
			||||||
 | 
					      _snapshot = {sm: snapshot.sm, ds: prevSnapshot.ds}
 | 
				
			||||||
 | 
					      _prevSnapshot = _snapshot
 | 
				
			||||||
 | 
					    } else if (!isVisible(el, prevSnapshot)) {
 | 
				
			||||||
 | 
					      _prevSnapshot = _snapshot
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  const children = []
 | 
					  const children = []
 | 
				
			||||||
  el.toArray(snapshot).forEach(type => {
 | 
					  const createChildren = type => {
 | 
				
			||||||
    if (type.constructor === YXmlElement) {
 | 
					    if (type.constructor === YXmlElement) {
 | 
				
			||||||
      const n = createNodeIfNotExists(type, schema, mapping, snapshot)
 | 
					      const n = createNodeIfNotExists(type, schema, mapping, _snapshot, _prevSnapshot)
 | 
				
			||||||
      if (n !== null) {
 | 
					      if (n !== null) {
 | 
				
			||||||
        children.push(n)
 | 
					        children.push(n)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      const ns = createTextNodesFromYText(type, schema, mapping, snapshot)
 | 
					      const ns = createTextNodesFromYText(type, schema, mapping, _snapshot, _prevSnapshot)
 | 
				
			||||||
      if (ns !== null) {
 | 
					      if (ns !== null) {
 | 
				
			||||||
        ns.forEach(textchild => {
 | 
					        ns.forEach(textchild => {
 | 
				
			||||||
          if (textchild !== null) {
 | 
					          if (textchild !== null) {
 | 
				
			||||||
@ -406,16 +453,31 @@ export const createNodeFromYElement = (el, schema, mapping, snapshot) => {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  }
 | 
				
			||||||
 | 
					  if (snapshot === undefined || prevSnapshot === undefined) {
 | 
				
			||||||
 | 
					    el.toArray().forEach(createChildren)
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    el.toArray({sm: snapshot.sm, ds: prevSnapshot.ds}).forEach(createChildren)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  let node
 | 
					  let node
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(snapshot), children)
 | 
					    const attrs = el.getAttributes(_snapshot)
 | 
				
			||||||
 | 
					    if (snapshot !== undefined) {
 | 
				
			||||||
 | 
					      if (!isVisible(el, snapshot)) {
 | 
				
			||||||
 | 
					        attrs.ychange = { user: el._id.user, state: 'removed' }
 | 
				
			||||||
 | 
					      } else if (!isVisible(el, prevSnapshot)) {
 | 
				
			||||||
 | 
					        attrs.ychange = { user: el._id.user, state: 'added' }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    node = schema.node(el.nodeName.toLowerCase(), attrs, children)
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch (e) {
 | 
				
			||||||
    // an error occured while creating the node. This is probably a result because of a concurrent action.
 | 
					    // an error occured while creating the node. This is probably a result because of a concurrent action.
 | 
				
			||||||
    // delete the node and do not push to children
 | 
					    // ignore the node while rendering
 | 
				
			||||||
 | 
					    /* do not delete anymore
 | 
				
			||||||
    el._y.transact(() => {
 | 
					    el._y.transact(() => {
 | 
				
			||||||
      el._delete(el._y, true)
 | 
					      el._delete(el._y, true)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
    return null
 | 
					    return null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  mapping.set(el, node)
 | 
					  mapping.set(el, node)
 | 
				
			||||||
@ -428,11 +490,12 @@ export const createNodeFromYElement = (el, schema, mapping, snapshot) => {
 | 
				
			|||||||
 * @param {PModel.Schema} schema
 | 
					 * @param {PModel.Schema} schema
 | 
				
			||||||
 * @param {ProsemirrorMapping} mapping
 | 
					 * @param {ProsemirrorMapping} mapping
 | 
				
			||||||
 * @param {HistorySnapshot} [snapshot]
 | 
					 * @param {HistorySnapshot} [snapshot]
 | 
				
			||||||
 | 
					 * @param {HistorySnapshot} [prevSnapshot]
 | 
				
			||||||
 * @return {Array<PModel.Node>}
 | 
					 * @return {Array<PModel.Node>}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const createTextNodesFromYText = (text, schema, mapping, snapshot) => {
 | 
					export const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot) => {
 | 
				
			||||||
  const nodes = []
 | 
					  const nodes = []
 | 
				
			||||||
  const deltas = text.toDelta(snapshot)
 | 
					  const deltas = text.toDelta(snapshot, prevSnapshot)
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    for (let i = 0; i < deltas.length; i++) {
 | 
					    for (let i = 0; i < deltas.length; i++) {
 | 
				
			||||||
      const delta = deltas[i]
 | 
					      const delta = deltas[i]
 | 
				
			||||||
@ -446,9 +509,11 @@ export const createTextNodesFromYText = (text, schema, mapping, snapshot) => {
 | 
				
			|||||||
      mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
 | 
					      mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
    text._y.transact(() => {
 | 
					    text._y.transact(() => {
 | 
				
			||||||
      text._delete(text._y, true)
 | 
					      text._delete(text._y, true)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
    return null
 | 
					    return null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return nodes
 | 
					  return nodes
 | 
				
			||||||
@ -465,13 +530,17 @@ export const createTypeFromNode = (node, mapping) => {
 | 
				
			|||||||
  if (node.isText) {
 | 
					  if (node.isText) {
 | 
				
			||||||
    type = new YText()
 | 
					    type = new YText()
 | 
				
			||||||
    const attrs = {}
 | 
					    const attrs = {}
 | 
				
			||||||
    node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
 | 
					    node.marks.forEach(mark => {
 | 
				
			||||||
 | 
					      if (mark.type.name !== 'ychange') {
 | 
				
			||||||
 | 
					        attrs[mark.type.name] = mark.attrs
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
    type.insert(0, node.text, attrs)
 | 
					    type.insert(0, node.text, attrs)
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    type = new YXmlElement(node.type.name)
 | 
					    type = new YXmlElement(node.type.name)
 | 
				
			||||||
    for (let key in node.attrs) {
 | 
					    for (let key in node.attrs) {
 | 
				
			||||||
      const val = node.attrs[key]
 | 
					      const val = node.attrs[key]
 | 
				
			||||||
      if (val !== null) {
 | 
					      if (val !== null && key !== 'ychange') {
 | 
				
			||||||
        type.setAttribute(key, val)
 | 
					        type.setAttribute(key, val)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -490,7 +559,9 @@ const equalAttrs = (pattrs, yattrs) => {
 | 
				
			|||||||
  let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
 | 
					  let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
 | 
				
			||||||
  for (let i = 0; i < keys.length && eq; i++) {
 | 
					  for (let i = 0; i < keys.length && eq; i++) {
 | 
				
			||||||
    const key = keys[i]
 | 
					    const key = keys[i]
 | 
				
			||||||
    eq = pattrs[key] === yattrs[key]
 | 
					    const l = pattrs[key]
 | 
				
			||||||
 | 
					    const r = yattrs[key]
 | 
				
			||||||
 | 
					    eq = key === 'ychange' || l === r || (typeof l === 'object' && typeof r === 'object' && equalAttrs(l, r))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return eq
 | 
					  return eq
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -554,7 +625,7 @@ const updateYFragment = (yDomFragment, pContent, mapping) => {
 | 
				
			|||||||
    const pAttrs = pContent.attrs
 | 
					    const pAttrs = pContent.attrs
 | 
				
			||||||
    for (let key in pAttrs) {
 | 
					    for (let key in pAttrs) {
 | 
				
			||||||
      if (pAttrs[key] !== null) {
 | 
					      if (pAttrs[key] !== null) {
 | 
				
			||||||
        if (yDomAttrs[key] !== pAttrs[key]) {
 | 
					        if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
 | 
				
			||||||
          yDomFragment.setAttribute(key, pAttrs[key])
 | 
					          yDomFragment.setAttribute(key, pAttrs[key])
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
@ -610,8 +681,23 @@ const updateYFragment = (yDomFragment, pContent, mapping) => {
 | 
				
			|||||||
      const rightP = pContent.child(pChildCnt - right - 1)
 | 
					      const rightP = pContent.child(pChildCnt - right - 1)
 | 
				
			||||||
      if (leftY.constructor === YText && leftP.isText) {
 | 
					      if (leftY.constructor === YText && leftP.isText) {
 | 
				
			||||||
        if (!equalYTextPText(leftY, leftP)) {
 | 
					        if (!equalYTextPText(leftY, leftP)) {
 | 
				
			||||||
          yDomFragment.delete(left, 1)
 | 
					          // try to apply diff. Only if attrs don't match, delete insert
 | 
				
			||||||
          yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
 | 
					          // TODO: use a single ytext to hold all following Prosemirror Text nodes
 | 
				
			||||||
 | 
					          const pattrs = {}
 | 
				
			||||||
 | 
					          leftP.marks.forEach(mark => {
 | 
				
			||||||
 | 
					            if (mark.type.name !== 'ychange') {
 | 
				
			||||||
 | 
					              pattrs[mark.type.name] = mark.attrs
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          const delta = leftY.toDelta()
 | 
				
			||||||
 | 
					          if (delta.length === 1 && delta[0].insert && equalAttrs(pattrs, delta[0].attributes || {})) {
 | 
				
			||||||
 | 
					            const diff = simpleDiff(delta[0].insert, leftP.text)
 | 
				
			||||||
 | 
					            leftY.delete(diff.pos, diff.remove)
 | 
				
			||||||
 | 
					            leftY.insert(diff.pos, diff.insert)
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            yDomFragment.delete(left, 1)
 | 
				
			||||||
 | 
					            yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        left += 1
 | 
					        left += 1
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import {Plugin} from "prosemirror-state"
 | 
					import {Plugin} from 'prosemirror-state'
 | 
				
			||||||
import crel from 'crel'
 | 
					import crel from 'crel'
 | 
				
			||||||
import * as Y from '../index.js'
 | 
					import * as Y from '../index.js'
 | 
				
			||||||
import { prosemirrorPluginKey } from '../bindings/prosemirror.js'
 | 
					import { prosemirrorPluginKey } from '../bindings/prosemirror.js'
 | 
				
			||||||
@ -7,21 +7,53 @@ import * as encoding from '../lib/encoding.js'
 | 
				
			|||||||
import * as decoding from '../lib/decoding.js'
 | 
					import * as decoding from '../lib/decoding.js'
 | 
				
			||||||
import * as historyProtocol from '../protocols/history.js'
 | 
					import * as historyProtocol from '../protocols/history.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const niceColors = ['#3cb44b', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#008080', '#9a6324', '#800000', '#808000', '#000075', '#808080']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createUserCSS = (userid, username, color = 'rgb(250, 129, 0)', color2 = 'rgba(250, 129, 0, .41)') => `
 | 
				
			||||||
 | 
					  [ychange_state][ychange_user="${userid}"]:hover::before {
 | 
				
			||||||
 | 
					    content: "${username}" !important;
 | 
				
			||||||
 | 
					    background-color: ${color} !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  [ychange_state="added"][ychange_user="${userid}"] {
 | 
				
			||||||
 | 
					    background-color: ${color2} !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  [ychange_state="removed"][ychange_user="${userid}"] {
 | 
				
			||||||
 | 
					    color: ${color} !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const noteHistoryPlugin = new Plugin({
 | 
					export const noteHistoryPlugin = new Plugin({
 | 
				
			||||||
  view (editorView) { return new NoteHistoryPlugin(editorView) }
 | 
					  state: {
 | 
				
			||||||
 | 
					    init (initargs, state) {
 | 
				
			||||||
 | 
					      return new NoteHistoryPlugin()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    apply (tr, pluginState) {
 | 
				
			||||||
 | 
					      return pluginState
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  view (editorView) {
 | 
				
			||||||
 | 
					    const hstate = noteHistoryPlugin.getState(editorView.state)
 | 
				
			||||||
 | 
					    hstate.init(editorView)
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      destroy: hstate.destroy.bind(hstate)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createWrapper = () => {
 | 
					const createWrapper = () => {
 | 
				
			||||||
  const wrapper = crel('div', { style: 'display: flex' })
 | 
					  const wrapper = crel('div', { style: 'display: flex;' })
 | 
				
			||||||
  const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
 | 
					  const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
 | 
				
			||||||
  wrapper.insertBefore(historyContainer, null)
 | 
					  wrapper.insertBefore(historyContainer, null)
 | 
				
			||||||
  return { wrapper, historyContainer }
 | 
					  const userStyleContainer = crel('style')
 | 
				
			||||||
 | 
					  wrapper.insertBefore(userStyleContainer, null)
 | 
				
			||||||
 | 
					  return { wrapper, historyContainer, userStyleContainer }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NoteHistoryPlugin {
 | 
					class NoteHistoryPlugin {
 | 
				
			||||||
  constructor(editorView) {
 | 
					  init (editorView) {
 | 
				
			||||||
    this.editorView = editorView
 | 
					    this.editorView = editorView
 | 
				
			||||||
    const { historyContainer, wrapper } = createWrapper()
 | 
					    const { historyContainer, wrapper, userStyleContainer } = createWrapper()
 | 
				
			||||||
 | 
					    this.userStyleContainer = userStyleContainer
 | 
				
			||||||
    this.wrapper = wrapper
 | 
					    this.wrapper = wrapper
 | 
				
			||||||
    this.historyContainer = historyContainer
 | 
					    this.historyContainer = historyContainer
 | 
				
			||||||
    const n = editorView.dom.parentNode.parentNode
 | 
					    const n = editorView.dom.parentNode.parentNode
 | 
				
			||||||
@ -33,35 +65,94 @@ class NoteHistoryPlugin {
 | 
				
			|||||||
    const history = y.define('history', Y.Array)
 | 
					    const history = y.define('history', Y.Array)
 | 
				
			||||||
    history.observe(this.render.bind(this))
 | 
					    history.observe(this.render.bind(this))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  destroy () {
 | 
				
			||||||
 | 
					    this.wrapper.parentNode.replaceChild(this.wrapper.firstChild, this.wrapper)
 | 
				
			||||||
 | 
					    const y = prosemirrorPluginKey.getState(this.editorView.state).y
 | 
				
			||||||
 | 
					    const history = y.define('history', Y.Array)
 | 
				
			||||||
 | 
					    history.unobserve(this.render)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const y = prosemirrorPluginKey.getState(this.editorView.state).y
 | 
					    const y = prosemirrorPluginKey.getState(this.editorView.state).y
 | 
				
			||||||
    const history = y.define('history', Y.Array).toArray()
 | 
					    const history = y.define('history', Y.Array).toArray()
 | 
				
			||||||
    const fragment = document.createDocumentFragment()
 | 
					    const fragment = document.createDocumentFragment()
 | 
				
			||||||
    const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
 | 
					    const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
 | 
				
			||||||
    snapshotBtn.addEventListener('click', this.snapshot.bind(this))
 | 
					 | 
				
			||||||
    fragment.insertBefore(snapshotBtn, null)
 | 
					    fragment.insertBefore(snapshotBtn, null)
 | 
				
			||||||
 | 
					    let _prevSnap = null // empty
 | 
				
			||||||
 | 
					    snapshotBtn.addEventListener('click', () => {
 | 
				
			||||||
 | 
					      const awareness = y.getAwarenessInfo()
 | 
				
			||||||
 | 
					      const userMap = new Map()
 | 
				
			||||||
 | 
					      const aw = y.getLocalAwarenessInfo()
 | 
				
			||||||
 | 
					      userMap.set(y.userID, aw.name || 'unknown')
 | 
				
			||||||
 | 
					      awareness.forEach((a, userID) => {
 | 
				
			||||||
 | 
					        userMap.set(userID, a.name || 'Unknown')
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      this.snapshot(userMap)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
    history.forEach(buf => {
 | 
					    history.forEach(buf => {
 | 
				
			||||||
      const decoder = decoding.createDecoder(buf)
 | 
					      const decoder = decoding.createDecoder(buf)
 | 
				
			||||||
      const snapshot = historyProtocol.readHistorySnapshot(decoder)
 | 
					      const snapshot = historyProtocol.readHistorySnapshot(decoder)
 | 
				
			||||||
      const date = new Date(decoding.readUint32(decoder) * 1000)
 | 
					      const date = new Date(decoding.readUint32(decoder) * 1000)
 | 
				
			||||||
 | 
					      const restoreBtn = crel('button', { type: 'button' }, ['restore'])
 | 
				
			||||||
      const a = crel('a', [
 | 
					      const a = crel('a', [
 | 
				
			||||||
        '• '+ date.toUTCString()
 | 
					        '• ' + date.toUTCString(), restoreBtn
 | 
				
			||||||
      ])
 | 
					      ])
 | 
				
			||||||
      const el = crel('div', [ a ])
 | 
					      const el = crel('div', [ a ])
 | 
				
			||||||
 | 
					      let prevSnapshot = _prevSnap // rebind to new variable
 | 
				
			||||||
 | 
					      restoreBtn.addEventListener('click', event => {
 | 
				
			||||||
 | 
					        if (prevSnapshot === null) {
 | 
				
			||||||
 | 
					          prevSnapshot = { ds: snapshot.ds, sm: new Map() }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot, restore: true }))
 | 
				
			||||||
 | 
					        event.stopPropagation()
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
      a.addEventListener('click', () => {
 | 
					      a.addEventListener('click', () => {
 | 
				
			||||||
        console.log('setting snapshot')
 | 
					        console.log('setting snapshot')
 | 
				
			||||||
        this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot }))
 | 
					        if (prevSnapshot === null) {
 | 
				
			||||||
 | 
					          prevSnapshot = { ds: snapshot.ds, sm: new Map() }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.renderSnapshot(snapshot, prevSnapshot)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      fragment.insertBefore(el, null)
 | 
					      fragment.insertBefore(el, null)
 | 
				
			||||||
 | 
					      _prevSnap = snapshot
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    this.historyContainer.innerHTML = ''
 | 
					    this.historyContainer.innerHTML = ''
 | 
				
			||||||
    this.historyContainer.insertBefore(fragment, null)
 | 
					    this.historyContainer.insertBefore(fragment, null)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  snapshot () {
 | 
					  renderSnapshot (snapshot, prevSnapshot) {
 | 
				
			||||||
 | 
					    this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot }))
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @type {Array<string|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    let colors = niceColors.slice()
 | 
				
			||||||
 | 
					    let style = ''
 | 
				
			||||||
 | 
					    snapshot.userMap.forEach((name, userid) => {
 | 
				
			||||||
 | 
					      /**
 | 
				
			||||||
 | 
					       * @type {any}
 | 
				
			||||||
 | 
					       */
 | 
				
			||||||
 | 
					      const randInt = name.split('').map(s => s.charCodeAt(0)).reduce((a, b) => a + b)
 | 
				
			||||||
 | 
					      let color = null
 | 
				
			||||||
 | 
					      let i = 0
 | 
				
			||||||
 | 
					      for (; i < colors.length && color === null; i++) {
 | 
				
			||||||
 | 
					        color = colors[(randInt + i) % colors.length]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (color === null) {
 | 
				
			||||||
 | 
					        colors = niceColors.slice()
 | 
				
			||||||
 | 
					        i = 0
 | 
				
			||||||
 | 
					        color = colors[randInt % colors.length]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      colors[randInt % colors.length] = null
 | 
				
			||||||
 | 
					      style += createUserCSS(userid, name, color, color + '69')
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    this.userStyleContainer.innerHTML = style
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @param {Map<number, string>} [updatedUserMap] Maps from userid (yjs model) to account name (e.g. mail address)
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  snapshot (updatedUserMap = new Map()) {
 | 
				
			||||||
    const y = prosemirrorPluginKey.getState(this.editorView.state).y
 | 
					    const y = prosemirrorPluginKey.getState(this.editorView.state).y
 | 
				
			||||||
    const history = y.define('history', Y.Array)
 | 
					    const history = y.define('history', Y.Array)
 | 
				
			||||||
    const encoder = encoding.createEncoder()
 | 
					    const encoder = encoding.createEncoder()
 | 
				
			||||||
    historyProtocol.writeHistorySnapshot(encoder, y)
 | 
					    historyProtocol.writeHistorySnapshot(encoder, y, updatedUserMap)
 | 
				
			||||||
    encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
 | 
					    encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
 | 
				
			||||||
    history.push([encoding.toBuffer(encoder)])
 | 
					    history.push([encoding.toBuffer(encoder)])
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
<html>
 | 
					<html>
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
    <title>Yjs Prosemirror Example</title>
 | 
					    <title>Yjs Prosemirror Example</title>
 | 
				
			||||||
    <link rel=stylesheet href="https://prosemirror.net/css/editor.css">
 | 
					    <link rel=stylesheet href="./prosemirror.css">
 | 
				
			||||||
    <style>
 | 
					    <style>
 | 
				
			||||||
      placeholder {
 | 
					      placeholder {
 | 
				
			||||||
        display: inline;
 | 
					        display: inline;
 | 
				
			||||||
@ -30,6 +30,7 @@
 | 
				
			|||||||
        border-left-width: 2px;
 | 
					        border-left-width: 2px;
 | 
				
			||||||
        border-color: orange;
 | 
					        border-color: orange;
 | 
				
			||||||
        height: 1em;
 | 
					        height: 1em;
 | 
				
			||||||
 | 
					        word-break: normal;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .ProseMirror-yjs-cursor > div {
 | 
					      .ProseMirror-yjs-cursor > div {
 | 
				
			||||||
        position: relative;
 | 
					        position: relative;
 | 
				
			||||||
@ -45,6 +46,36 @@
 | 
				
			|||||||
        padding-left: 2px;
 | 
					        padding-left: 2px;
 | 
				
			||||||
        padding-right: 2px;
 | 
					        padding-right: 2px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      [ychange_state] {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      [ychange_state]:hover::before {
 | 
				
			||||||
 | 
					        content: attr(ychange_user);
 | 
				
			||||||
 | 
					        background-color: #fa8100;
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        top: -14px;
 | 
				
			||||||
 | 
					        right: 0;
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					        padding: 0 2px;
 | 
				
			||||||
 | 
					        border-radius: 3px 3px 0 0;
 | 
				
			||||||
 | 
					        color: #fdfdfe;
 | 
				
			||||||
 | 
					        user-select: none;
 | 
				
			||||||
 | 
					        word-break: normal;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      *[ychange_state='added'] {
 | 
				
			||||||
 | 
					        background-color: #fa810069;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      ychange[ychange_state='removed'] {
 | 
				
			||||||
 | 
					        color: rgb(250, 129, 0);
 | 
				
			||||||
 | 
					        text-decoration: line-through;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      *:not(ychange)[ychange_state='removed'] {
 | 
				
			||||||
 | 
					        background-color: #ff9494c9;
 | 
				
			||||||
 | 
					        text-decoration: line-through;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      img[ychange_state='removed'] {
 | 
				
			||||||
 | 
					        padding: 2px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
 | 
				
			|||||||
@ -6,19 +6,19 @@ import * as conf from './exampleConfig.js'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { EditorState } from 'prosemirror-state'
 | 
					import { EditorState } from 'prosemirror-state'
 | 
				
			||||||
import { EditorView } from 'prosemirror-view'
 | 
					import { EditorView } from 'prosemirror-view'
 | 
				
			||||||
import { DOMParser } from 'prosemirror-model'
 | 
					import { DOMParser, Schema } from 'prosemirror-model'
 | 
				
			||||||
import { schema } from 'prosemirror-schema-basic'
 | 
					import { schema } from './prosemirror-schema.js'
 | 
				
			||||||
import { exampleSetup } from 'prosemirror-example-setup'
 | 
					import { exampleSetup } from 'prosemirror-example-setup'
 | 
				
			||||||
import { noteHistoryPlugin } from './prosemirror-history.js'
 | 
					import { noteHistoryPlugin } from './prosemirror-history.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const provider = new WebsocketProvider(conf.serverAddress)
 | 
					const provider = new WebsocketProvider(conf.serverAddress)
 | 
				
			||||||
const ydocument = provider.get('prosemirror')
 | 
					const ydocument = provider.get('prosemirror', { gc: false })
 | 
				
			||||||
const type = ydocument.define('prosemirror', Y.XmlFragment)
 | 
					const type = ydocument.define('prosemirror', Y.XmlFragment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
 | 
					const prosemirrorView = new EditorView(document.querySelector('#editor'), {
 | 
				
			||||||
  state: EditorState.create({
 | 
					  state: EditorState.create({
 | 
				
			||||||
    doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
 | 
					    doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
 | 
				
			||||||
    plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin, noteHistoryPlugin])
 | 
					    plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin /* noteHistoryPlugin */])
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								index.js
									
									
									
									
									
								
							@ -55,4 +55,3 @@ registerStruct(10, YXmlText)
 | 
				
			|||||||
registerStruct(11, YXmlHook)
 | 
					registerStruct(11, YXmlHook)
 | 
				
			||||||
registerStruct(12, ItemEmbed)
 | 
					registerStruct(12, ItemEmbed)
 | 
				
			||||||
registerStruct(13, ItemBinary)
 | 
					registerStruct(13, ItemBinary)
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -2173,7 +2173,8 @@
 | 
				
			|||||||
    "crel": {
 | 
					    "crel": {
 | 
				
			||||||
      "version": "3.1.0",
 | 
					      "version": "3.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/crel/-/crel-3.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/crel/-/crel-3.1.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg=="
 | 
					      "integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "cross-spawn": {
 | 
					    "cross-spawn": {
 | 
				
			||||||
      "version": "5.1.0",
 | 
					      "version": "5.1.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -48,17 +48,17 @@ const readEntry = (arr, ydocument) => mux(() =>
 | 
				
			|||||||
 * @param {string} docName
 | 
					 * @param {string} docName
 | 
				
			||||||
 * @param {Y.Y} ydocument
 | 
					 * @param {Y.Y} ydocument
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const loadFromPersistence = (db, docName, ydocument) => new Promise((resolve, reject)=>
 | 
					const loadFromPersistence = (db, docName, ydocument) => new Promise((resolve, reject) =>
 | 
				
			||||||
  db.createReadStream({
 | 
					  db.createReadStream({
 | 
				
			||||||
    gte: `${docName}#`,
 | 
					    gte: `${docName}#`,
 | 
				
			||||||
    lte: `${docName}#Z`,
 | 
					    lte: `${docName}#Z`,
 | 
				
			||||||
    keys: false,
 | 
					    keys: false,
 | 
				
			||||||
    values: true
 | 
					    values: true
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  .on('data', data => readEntry(data, ydocument))
 | 
					    .on('data', data => readEntry(data, ydocument))
 | 
				
			||||||
  .on('error', reject)
 | 
					    .on('error', reject)
 | 
				
			||||||
  .on('end', resolve)
 | 
					    .on('end', resolve)
 | 
				
			||||||
  .on('close', resolve)
 | 
					    .on('close', resolve)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const persistState = (db, docName, ydocument) => {
 | 
					const persistState = (db, docName, ydocument) => {
 | 
				
			||||||
@ -68,9 +68,9 @@ const persistState = (db, docName, ydocument) => {
 | 
				
			|||||||
  const entryPromise = db.put(entryKey, Y.encoding.toBuffer(encoder))
 | 
					  const entryPromise = db.put(entryKey, Y.encoding.toBuffer(encoder))
 | 
				
			||||||
  const delOps = []
 | 
					  const delOps = []
 | 
				
			||||||
  return new Promise((resolve, reject) => db.createKeyStream({
 | 
					  return new Promise((resolve, reject) => db.createKeyStream({
 | 
				
			||||||
      gte: `${docName}#`,
 | 
					    gte: `${docName}#`,
 | 
				
			||||||
      lt: entryKey,
 | 
					    lt: entryKey
 | 
				
			||||||
    })
 | 
					  })
 | 
				
			||||||
    .on('data', key => delOps.push({ type: 'del', key }))
 | 
					    .on('data', key => delOps.push({ type: 'del', key }))
 | 
				
			||||||
    .on('error', reject)
 | 
					    .on('error', reject)
 | 
				
			||||||
    .on('end', resolve)
 | 
					    .on('end', resolve)
 | 
				
			||||||
 | 
				
			|||||||
@ -40,24 +40,22 @@ export const readUsersStateChange = (decoder, y) => {
 | 
				
			|||||||
    const userID = decoding.readVarUint(decoder)
 | 
					    const userID = decoding.readVarUint(decoder)
 | 
				
			||||||
    const clock = decoding.readVarUint(decoder)
 | 
					    const clock = decoding.readVarUint(decoder)
 | 
				
			||||||
    const state = JSON.parse(decoding.readVarString(decoder))
 | 
					    const state = JSON.parse(decoding.readVarString(decoder))
 | 
				
			||||||
    if (userID !== y.userID) {
 | 
					    const uClock = y.awarenessClock.get(userID) || 0
 | 
				
			||||||
      const uClock = y.awarenessClock.get(userID) || 0
 | 
					    y.awarenessClock.set(userID, clock)
 | 
				
			||||||
      y.awarenessClock.set(userID, clock)
 | 
					    if (state === null) {
 | 
				
			||||||
      if (state === null) {
 | 
					      // only write if clock increases. cannot overwrite
 | 
				
			||||||
        // only write if clock increases. cannot overwrite
 | 
					      if (y.awareness.has(userID) && uClock < clock) {
 | 
				
			||||||
        if (y.awareness.has(userID) && uClock < clock) {
 | 
					        y.awareness.delete(userID)
 | 
				
			||||||
          y.awareness.delete(userID)
 | 
					        removed.push(userID)
 | 
				
			||||||
          removed.push(userID)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else if (uClock <= clock) { // allow to overwrite (e.g. when client was on, then offline)
 | 
					 | 
				
			||||||
        if (y.awareness.has(userID)) {
 | 
					 | 
				
			||||||
          updated.push(userID)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          added.push(userID)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        y.awareness.set(userID, state)
 | 
					 | 
				
			||||||
        y.awarenessClock.set(userID, clock)
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (uClock <= clock) { // allow to overwrite (e.g. when client was on, then offline)
 | 
				
			||||||
 | 
					      if (y.awareness.has(userID)) {
 | 
				
			||||||
 | 
					        updated.push(userID)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        added.push(userID)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      y.awareness.set(userID, state)
 | 
				
			||||||
 | 
					      y.awarenessClock.set(userID, clock)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (added.length > 0 || updated.length > 0 || removed.length > 0) {
 | 
					  if (added.length > 0 || updated.length > 0 || removed.length > 0) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
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 { Y } from '../utils/Y.js' // eslint-disable-line
 | 
					import { Y } from '../utils/Y.js' // eslint-disable-line
 | 
				
			||||||
@ -10,15 +9,22 @@ import { writeStateMap, readStateMap } from '../utils/StateStore.js'
 | 
				
			|||||||
 * @typedef {Object} HistorySnapshot
 | 
					 * @typedef {Object} HistorySnapshot
 | 
				
			||||||
 * @property {DeleteStore} HistorySnapshot.ds
 | 
					 * @property {DeleteStore} HistorySnapshot.ds
 | 
				
			||||||
 * @property {Map<number,number>} HistorySnapshot.sm
 | 
					 * @property {Map<number,number>} HistorySnapshot.sm
 | 
				
			||||||
 | 
					 * @property {Map<number,string>} HistorySnapshot.userMap
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {encoding.Encoder} encoder
 | 
					 * @param {encoding.Encoder} encoder
 | 
				
			||||||
 * @param {Y} y
 | 
					 * @param {Y} y
 | 
				
			||||||
 | 
					 * @param {Map<number, string>} userMap
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const writeHistorySnapshot = (encoder, y) => {
 | 
					export const writeHistorySnapshot = (encoder, y, userMap) => {
 | 
				
			||||||
  writeDeleteStore(encoder, y.ds)
 | 
					  writeDeleteStore(encoder, y.ds)
 | 
				
			||||||
  writeStateMap(encoder, y.ss.state)
 | 
					  writeStateMap(encoder, y.ss.state)
 | 
				
			||||||
 | 
					  encoding.writeVarUint(encoder, userMap.size)
 | 
				
			||||||
 | 
					  userMap.forEach((accountname, userid) => {
 | 
				
			||||||
 | 
					    encoding.writeVarUint(encoder, userid)
 | 
				
			||||||
 | 
					    encoding.writeVarString(encoder, accountname)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -26,8 +32,15 @@ export const writeHistorySnapshot = (encoder, y) => {
 | 
				
			|||||||
 * @param {decoding.Decoder} decoder
 | 
					 * @param {decoding.Decoder} decoder
 | 
				
			||||||
 * @return {HistorySnapshot}
 | 
					 * @return {HistorySnapshot}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const readHistorySnapshot = (decoder) => {
 | 
					export const readHistorySnapshot = decoder => {
 | 
				
			||||||
  const ds = readFreshDeleteStore(decoder)
 | 
					  const ds = readFreshDeleteStore(decoder)
 | 
				
			||||||
  const sm = readStateMap(decoder)
 | 
					  const sm = readStateMap(decoder)
 | 
				
			||||||
  return { ds, sm }
 | 
					  const size = decoding.readVarUint(decoder)
 | 
				
			||||||
 | 
					  const userMap = new Map()
 | 
				
			||||||
 | 
					  for (let i = 0; i < size; i++) {
 | 
				
			||||||
 | 
					    const userid = decoding.readVarUint(decoder)
 | 
				
			||||||
 | 
					    const accountname = decoding.readVarString(decoder)
 | 
				
			||||||
 | 
					    userMap.set(userid, accountname)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return { ds, sm, userMap }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -97,8 +97,8 @@ const broadcastUpdate = (y, transaction) => {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WebsocketsSharedDocument extends Y.Y {
 | 
					class WebsocketsSharedDocument extends Y.Y {
 | 
				
			||||||
  constructor (url) {
 | 
					  constructor (url, opts) {
 | 
				
			||||||
    super()
 | 
					    super(opts)
 | 
				
			||||||
    this.url = url
 | 
					    this.url = url
 | 
				
			||||||
    this.wsconnected = false
 | 
					    this.wsconnected = false
 | 
				
			||||||
    this.mux = Y.createMutex()
 | 
					    this.mux = Y.createMutex()
 | 
				
			||||||
@ -112,7 +112,7 @@ class WebsocketsSharedDocument extends Y.Y {
 | 
				
			|||||||
      const encoder = readMessage(this, data) // already muxed
 | 
					      const encoder = readMessage(this, data) // already muxed
 | 
				
			||||||
      this.mux(() => {
 | 
					      this.mux(() => {
 | 
				
			||||||
        if (Y.encoding.length(encoder) > 1) {
 | 
					        if (Y.encoding.length(encoder) > 1) {
 | 
				
			||||||
            bc.publish(url, Y.encoding.toBuffer(encoder))
 | 
					          bc.publish(url, Y.encoding.toBuffer(encoder))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -173,10 +173,10 @@ export class WebsocketProvider {
 | 
				
			|||||||
   * @param {string} name
 | 
					   * @param {string} name
 | 
				
			||||||
   * @return {WebsocketsSharedDocument}
 | 
					   * @return {WebsocketsSharedDocument}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  get (name) {
 | 
					  get (name, opts) {
 | 
				
			||||||
    let doc = this.docs.get(name)
 | 
					    let doc = this.docs.get(name)
 | 
				
			||||||
    if (doc === undefined) {
 | 
					    if (doc === undefined) {
 | 
				
			||||||
      doc = new WebsocketsSharedDocument(this.url + name)
 | 
					      doc = new WebsocketsSharedDocument(this.url + name, opts)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return doc
 | 
					    return doc
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,8 @@ const http = require('http')
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const port = process.env.PORT || 1234
 | 
					const port = process.env.PORT || 1234
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// disable gc when using snapshots!
 | 
				
			||||||
 | 
					const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'
 | 
				
			||||||
const persistenceDir = process.env.YPERSISTENCE
 | 
					const persistenceDir = process.env.YPERSISTENCE
 | 
				
			||||||
let persistence = null
 | 
					let persistence = null
 | 
				
			||||||
if (typeof persistenceDir === 'string') {
 | 
					if (typeof persistenceDir === 'string') {
 | 
				
			||||||
@ -44,7 +46,7 @@ const afterTransaction = (doc, transaction) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class WSSharedDoc extends Y.Y {
 | 
					class WSSharedDoc extends Y.Y {
 | 
				
			||||||
  constructor () {
 | 
					  constructor () {
 | 
				
			||||||
    super({ gc: true })
 | 
					    super({ gc: gcEnabled })
 | 
				
			||||||
    this.mux = Y.createMutex()
 | 
					    this.mux = Y.createMutex()
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
 | 
					     * Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								tmp/000005.ldb
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tmp/000005.ldb
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								tmp/000006.log
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tmp/000006.log
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							@ -1 +0,0 @@
 | 
				
			|||||||
MANIFEST-000004
 | 
					 | 
				
			||||||
							
								
								
									
										5
									
								
								tmp/LOG
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								tmp/LOG
									
									
									
									
									
								
							@ -1,5 +0,0 @@
 | 
				
			|||||||
2018/12/22-13:33:44.376775 70000c740000 Recovering log #3
 | 
					 | 
				
			||||||
2018/12/22-13:33:44.377203 70000c740000 Level-0 table #5: started
 | 
					 | 
				
			||||||
2018/12/22-13:33:44.379580 70000c740000 Level-0 table #5: 4834 bytes OK
 | 
					 | 
				
			||||||
2018/12/22-13:33:44.380496 70000c740000 Delete type=0 #3
 | 
					 | 
				
			||||||
2018/12/22-13:33:44.380670 70000c740000 Delete type=3 #2
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
2018/12/22-13:23:54.104944 700007836000 Delete type=3 #1
 | 
					 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							@ -8,7 +8,7 @@ import { ItemJSON } from '../structs/ItemJSON.js'
 | 
				
			|||||||
import * as stringify from '../utils/structStringify.js'
 | 
					import * as stringify from '../utils/structStringify.js'
 | 
				
			||||||
import { YEvent } from '../utils/YEvent.js'
 | 
					import { YEvent } from '../utils/YEvent.js'
 | 
				
			||||||
import { ItemBinary } from '../structs/ItemBinary.js'
 | 
					import { ItemBinary } from '../structs/ItemBinary.js'
 | 
				
			||||||
import { isVisible } from '../utils/snapshot.js';
 | 
					import { isVisible } from '../utils/snapshot.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Event that describes the changes on a YMap.
 | 
					 * Event that describes the changes on a YMap.
 | 
				
			||||||
@ -82,11 +82,11 @@ export class YMap extends Type {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      for (let key in this._map) {
 | 
					      this._map.forEach((_, key) => {
 | 
				
			||||||
        if (this.has(key, snapshot)) {
 | 
					        if (YMap.prototype.has.call(this, key, snapshot)) {
 | 
				
			||||||
          keys.push(key)
 | 
					          keys.push(key)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return keys
 | 
					    return keys
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -572,11 +572,12 @@ export class YText extends YArray {
 | 
				
			|||||||
   * Returns the Delta representation of this YText type.
 | 
					   * Returns the Delta representation of this YText type.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
 | 
					   * @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
 | 
				
			||||||
 | 
					   * @param {import('../protocols/history.js').HistorySnapshot} [prevSnapshot]
 | 
				
			||||||
   * @return {Delta} The Delta representation of this type.
 | 
					   * @return {Delta} The Delta representation of this type.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @public
 | 
					   * @public
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  toDelta (snapshot) {
 | 
					  toDelta (snapshot, prevSnapshot) {
 | 
				
			||||||
    let ops = []
 | 
					    let ops = []
 | 
				
			||||||
    let currentAttributes = new Map()
 | 
					    let currentAttributes = new Map()
 | 
				
			||||||
    let str = ''
 | 
					    let str = ''
 | 
				
			||||||
@ -602,9 +603,24 @@ export class YText extends YArray {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    while (n !== null) {
 | 
					    while (n !== null) {
 | 
				
			||||||
      if (isVisible(n, snapshot)) {
 | 
					      if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
 | 
				
			||||||
        switch (n.constructor) {
 | 
					        switch (n.constructor) {
 | 
				
			||||||
          case ItemString:
 | 
					          case ItemString:
 | 
				
			||||||
 | 
					            const cur = currentAttributes.get('ychange')
 | 
				
			||||||
 | 
					            if (snapshot !== undefined && !isVisible(n, snapshot)) {
 | 
				
			||||||
 | 
					              if (cur === undefined || cur.user !== n._id.user || cur.state !== 'removed') {
 | 
				
			||||||
 | 
					                packStr()
 | 
				
			||||||
 | 
					                currentAttributes.set('ychange', { user: n._id.user, state: 'removed' })
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
 | 
				
			||||||
 | 
					              if (cur === undefined || cur.user !== n._id.user || cur.state !== 'added') {
 | 
				
			||||||
 | 
					                packStr()
 | 
				
			||||||
 | 
					                currentAttributes.set('ychange', { user: n._id.user, state: 'added' })
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } else if (cur !== undefined) {
 | 
				
			||||||
 | 
					              packStr()
 | 
				
			||||||
 | 
					              currentAttributes.delete('ychange')
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            str += n._content
 | 
					            str += n._content
 | 
				
			||||||
            break
 | 
					            break
 | 
				
			||||||
          case ItemFormat:
 | 
					          case ItemFormat:
 | 
				
			||||||
 | 
				
			|||||||
@ -326,9 +326,9 @@ export class YXmlElement extends YXmlFragment {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      for (let key in this._map) {
 | 
					      YMap.prototype.keys.call(this, snapshot).forEach(key => {
 | 
				
			||||||
        return YMap.prototype.get.call(this, key, snapshot)
 | 
					        obj[key] = YMap.prototype.get.call(this, key, snapshot)
 | 
				
			||||||
      }
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return obj
 | 
					    return obj
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -92,7 +92,6 @@ export class DeleteStore extends Tree {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Stringifies a message-encoded Delete Set.
 | 
					 * Stringifies a message-encoded Delete Set.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param {Item} item
 | 
					 * @param {Item} item
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user