Compare commits
10 Commits
v13.0.0-73
...
v13.0.0-75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a79e429ed | ||
|
|
04066a5678 | ||
|
|
e09ef15349 | ||
|
|
3d70eee959 | ||
|
|
582095e5a3 | ||
|
|
c9ea3a412e | ||
|
|
a2c51c36e9 | ||
|
|
ab3dba5b06 | ||
|
|
3ddff186c2 | ||
|
|
9bd199a6e7 |
@@ -5,7 +5,7 @@
|
||||
"dictionaries": ["jsdoc"]
|
||||
},
|
||||
"source": {
|
||||
"include": ["./types", "./utils/UndoManager.mjs", "./utils/Y.mjs", "./provider", "./bindings"],
|
||||
"include": ["./types", "./utils/UndoManager.js", "./utils/Y.js", "./provider", "./bindings"],
|
||||
"includePattern": ".js$"
|
||||
},
|
||||
"plugins": [
|
||||
|
||||
@@ -9,10 +9,13 @@ Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the
|
||||
|
||||
```json
|
||||
{
|
||||
"checkJs": true,
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
..
|
||||
},
|
||||
"include": [
|
||||
"./node_modules/yjs/"
|
||||
]
|
||||
..
|
||||
}
|
||||
```
|
||||
|
||||
1
bindings/dom.js
Normal file
1
bindings/dom.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dom/DomBinding.js'
|
||||
@@ -1 +0,0 @@
|
||||
export * from './dom/DomBinding.mjs'
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
/* global MutationObserver, getSelection */
|
||||
|
||||
import { fromRelativePosition } from '../../utils/relativePosition.mjs'
|
||||
import { createMutex } from '../../lib/mutex.mjs'
|
||||
import { createAssociation, removeAssociation } from './util.mjs'
|
||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.mjs'
|
||||
import { defaultFilter, applyFilterOnType } from './filter.mjs'
|
||||
import { typeObserver } from './typeObserver.mjs'
|
||||
import { domObserver } from './domObserver.mjs'
|
||||
import { YXmlFragment } from '../../types/YXmlElement.mjs' // eslint-disable-line
|
||||
import { fromRelativePosition } from '../../utils/relativePosition.js'
|
||||
import { createMutex } from '../../lib/mutex.js'
|
||||
import { createAssociation, removeAssociation } from './util.js'
|
||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
|
||||
import { defaultFilter, applyFilterOnType } from './filter.js'
|
||||
import { typeObserver } from './typeObserver.js'
|
||||
import { domObserver } from './domObserver.js'
|
||||
import { YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @callback DomFilter
|
||||
@@ -2,13 +2,13 @@
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
import { YXmlHook } from '../../types/YXmlHook.mjs'
|
||||
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||
import {
|
||||
iterateUntilUndeleted,
|
||||
removeAssociation,
|
||||
insertNodeHelper } from './util.mjs'
|
||||
import { simpleDiff } from '../../lib/diff.mjs'
|
||||
import { YXmlFragment } from '../../types/YXmlElement.mjs'
|
||||
insertNodeHelper } from './util.js'
|
||||
import { simpleDiff } from '../../lib/diff.js'
|
||||
import { YXmlFragment } from '../../types/YXmlElement.js'
|
||||
|
||||
/**
|
||||
* 1. Check if any of the nodes was deleted
|
||||
@@ -20,6 +20,8 @@ import { YXmlFragment } from '../../types/YXmlElement.mjs'
|
||||
* recreate a new yxml element that is bound to that node.
|
||||
* You can detect that a node was moved because expectedId
|
||||
* !== actualId in the list
|
||||
*
|
||||
* @function
|
||||
* @private
|
||||
*/
|
||||
const applyChangesFromDom = (binding, dom, yxml, _document) => {
|
||||
@@ -85,6 +87,7 @@ const applyChangesFromDom = (binding, dom, yxml, _document) => {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export function domObserver (mutations, _document) {
|
||||
this._mutualExclude(() => {
|
||||
@@ -3,12 +3,12 @@
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
import { YXmlText } from '../../types/YXmlText.mjs'
|
||||
import { YXmlHook } from '../../types/YXmlHook.mjs'
|
||||
import { YXmlElement } from '../../types/YXmlElement.mjs'
|
||||
import { createAssociation, domsToTypes } from './util.mjs'
|
||||
import { filterDomAttributes, defaultFilter } from './filter.mjs'
|
||||
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
|
||||
import { YXmlText } from '../../types/YXmlText.js'
|
||||
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||
import { YXmlElement } from '../../types/YXmlElement.js'
|
||||
import { createAssociation, domsToTypes } from './util.js'
|
||||
import { filterDomAttributes, defaultFilter } from './filter.js'
|
||||
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @callback DomFilter
|
||||
@@ -20,6 +20,7 @@ import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
|
||||
/**
|
||||
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
||||
*
|
||||
* @function
|
||||
* @param {Element|Text} element The DOM Element
|
||||
* @param {?Document} _document Optional. Provide the global document object
|
||||
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
|
||||
@@ -2,14 +2,15 @@
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
import { Y } from '../../utils/Y.mjs' // eslint-disable-line
|
||||
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.mjs' // eslint-disable-line
|
||||
import { isParentOf } from '../../utils/isParentOf.mjs'
|
||||
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
|
||||
import { Y } from '../../utils/Y.js' // eslint-disable-line
|
||||
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
|
||||
import { isParentOf } from '../../utils/isParentOf.js'
|
||||
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Default filter method (does nothing).
|
||||
*
|
||||
* @function
|
||||
* @param {String} nodeName The nodeName of the element
|
||||
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
|
||||
* @return {Map | null} The allowed attributes or null, if the element should be
|
||||
@@ -21,7 +22,10 @@ export const defaultFilter = (nodeName, attrs) => {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {Element} dom
|
||||
* @param {Function} filter
|
||||
*/
|
||||
export const filterDomAttributes = (dom, filter) => {
|
||||
const attrs = new Map()
|
||||
@@ -35,11 +39,11 @@ export const filterDomAttributes = (dom, filter) => {
|
||||
/**
|
||||
* Applies a filter on a type.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {Y} y The Yjs instance.
|
||||
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
|
||||
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const applyFilterOnType = (y, binding, type) => {
|
||||
if (isParentOf(binding.type, type) && type instanceof YXmlElement) {
|
||||
@@ -4,10 +4,13 @@
|
||||
|
||||
/* globals getSelection */
|
||||
|
||||
import { getRelativePosition } from '../../utils/relativePosition.mjs'
|
||||
import { getRelativePosition } from '../../utils/relativePosition.js'
|
||||
|
||||
let relativeSelection = null
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
const _getCurrentRelativeSelection = domBinding => {
|
||||
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
|
||||
const baseNodeType = domBinding.domToType.get(baseNode)
|
||||
@@ -21,8 +24,14 @@ const _getCurrentRelativeSelection = domBinding => {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const beforeTransactionSelectionFixer = domBinding => {
|
||||
relativeSelection = getCurrentRelativeSelection(domBinding)
|
||||
}
|
||||
@@ -30,6 +39,7 @@ export const beforeTransactionSelectionFixer = domBinding => {
|
||||
/**
|
||||
* Reset the browser range after every transaction.
|
||||
* This prevents any collapsing issues with the local selection.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const afterTransactionSelectionFixer = domBinding => {
|
||||
@@ -5,9 +5,9 @@
|
||||
/* eslint-env browser */
|
||||
/* global getSelection */
|
||||
|
||||
import { YXmlText } from '../../types/YXmlText.mjs'
|
||||
import { YXmlHook } from '../../types/YXmlHook.mjs'
|
||||
import { removeDomChildrenUntilElementFound } from './util.mjs'
|
||||
import { YXmlText } from '../../types/YXmlText.js'
|
||||
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||
import { removeDomChildrenUntilElementFound } from './util.js'
|
||||
|
||||
const findScrollReference = scrollingElement => {
|
||||
if (scrollingElement !== null) {
|
||||
@@ -2,8 +2,8 @@
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
import { domToType } from './domToType.mjs'
|
||||
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
|
||||
import { domToType } from './domToType.js'
|
||||
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Iterates items until an undeleted item is found.
|
||||
@@ -22,6 +22,7 @@ export const iterateUntilUndeleted = item => {
|
||||
* type).
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {DomBinding} domBinding The binding object
|
||||
* @param {Element} dom The dom that is to be associated with type
|
||||
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
|
||||
@@ -37,6 +38,7 @@ export const removeAssociation = (domBinding, dom, type) => {
|
||||
* type).
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {DomBinding} domBinding The binding object
|
||||
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
|
||||
* @param {YXmlFragment|YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
|
||||
@@ -54,6 +56,7 @@ export const createAssociation = (domBinding, dom, type) => {
|
||||
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {DomBinding} domBinding The binding object
|
||||
* @param {Element} oldDom The existing dom
|
||||
* @param {Element} newDom The new dom object
|
||||
@@ -74,6 +77,7 @@ export const switchAssociation = (domBinding, oldDom, newDom) => {
|
||||
* specified position.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {YXmlElement} type The type in which to insert DOM elements.
|
||||
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
||||
* inserted after this node. Set null to insert at
|
||||
@@ -101,6 +105,7 @@ export const domsToTypes = (doms, _document, hooks, filter, binding) => {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, binding) => {
|
||||
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
|
||||
@@ -115,6 +120,7 @@ export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, bindi
|
||||
* Remove children until `elem` is found.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {Element} parent The parent of `elem` and `currentChild`.
|
||||
* @param {Node} currentChild Start removing elements with `currentChild`. If
|
||||
* `currentChild` is `elem` it won't be removed.
|
||||
633
bindings/prosemirror.js
Normal file
633
bindings/prosemirror.js
Normal file
@@ -0,0 +1,633 @@
|
||||
/**
|
||||
* @module bindings/prosemirror
|
||||
*/
|
||||
|
||||
import { YText } from '../types/YText.js' // eslint-disable-line
|
||||
import { YXmlElement, YXmlFragment } from '../types/YXmlElement.js' // eslint-disable-line
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import * as PModel from 'prosemirror-model'
|
||||
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
|
||||
import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state' // eslint-disable-line
|
||||
import * as math from '../lib/math.js'
|
||||
import * as object from '../lib/object.js'
|
||||
import * as YPos from '../utils/relativePosition.js'
|
||||
|
||||
/**
|
||||
* @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
|
||||
*/
|
||||
|
||||
/**
|
||||
* The unique prosemirror plugin key for prosemirrorPlugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const prosemirrorPluginKey = new PluginKey('yjs')
|
||||
|
||||
/**
|
||||
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
|
||||
*
|
||||
* This plugin also keeps references to the type and the shared document so other plugins can access it.
|
||||
* @param {YXmlFragment} yXmlFragment
|
||||
* @return {Plugin} Returns a prosemirror plugin that binds to this type
|
||||
*/
|
||||
export const prosemirrorPlugin = yXmlFragment => {
|
||||
const pluginState = {
|
||||
type: yXmlFragment,
|
||||
y: yXmlFragment._y,
|
||||
binding: null
|
||||
}
|
||||
let changedInitialContent = false
|
||||
const plugin = new Plugin({
|
||||
key: prosemirrorPluginKey,
|
||||
state: {
|
||||
init: (initargs, state) => {
|
||||
return pluginState
|
||||
},
|
||||
apply: (tr, pluginState) => {
|
||||
// update Yjs state when apply is called. We need to do this here to compute the correct cursor decorations with the cursor plugin
|
||||
if (pluginState.binding !== null && (changedInitialContent || tr.doc.content.size > 4)) {
|
||||
changedInitialContent = true
|
||||
pluginState.binding._prosemirrorChanged(tr.doc)
|
||||
}
|
||||
return pluginState
|
||||
}
|
||||
},
|
||||
view: view => {
|
||||
const binding = new ProsemirrorBinding(yXmlFragment, view)
|
||||
pluginState.binding = binding
|
||||
return {
|
||||
update: () => {
|
||||
if (changedInitialContent || view.state.doc.content.size > 4) {
|
||||
changedInitialContent = true
|
||||
binding._prosemirrorChanged(view.state.doc)
|
||||
}
|
||||
},
|
||||
destroy: () => {
|
||||
binding.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique prosemirror plugin key for cursorPlugin.type
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const cursorPluginKey = new PluginKey('yjs-cursor')
|
||||
|
||||
/**
|
||||
* A prosemirror plugin that listens to awareness information on Yjs.
|
||||
* This requires that a `prosemirrorPlugin` is also bound to the prosemirror.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const cursorPlugin = new Plugin({
|
||||
key: cursorPluginKey,
|
||||
props: {
|
||||
decorations: state => {
|
||||
const ystate = prosemirrorPluginKey.getState(state)
|
||||
const y = ystate.y
|
||||
const awareness = y.getAwarenessInfo()
|
||||
const decorations = []
|
||||
awareness.forEach((aw, userID) => {
|
||||
if (aw.cursor != null) {
|
||||
let user = aw.user || {}
|
||||
if (user.color == null) {
|
||||
user.color = '#ffa50070'
|
||||
}
|
||||
if (user.name == null) {
|
||||
user.name = `User: ${userID}`
|
||||
}
|
||||
let anchor = relativePositionToAbsolutePosition(ystate.type, aw.cursor.anchor || null, ystate.binding.mapping)
|
||||
let head = relativePositionToAbsolutePosition(ystate.type, aw.cursor.head || null, ystate.binding.mapping)
|
||||
if (anchor !== null && head !== null) {
|
||||
let maxsize = math.max(state.doc.content.size - 1, 0)
|
||||
anchor = math.min(anchor, maxsize)
|
||||
head = math.min(head, maxsize)
|
||||
decorations.push(Decoration.widget(head, () => {
|
||||
const cursor = document.createElement('span')
|
||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||
cursor.setAttribute('style', `border-color: ${user.color}`)
|
||||
const userDiv = document.createElement('div')
|
||||
userDiv.setAttribute('style', `background-color: ${user.color}`)
|
||||
userDiv.insertBefore(document.createTextNode(user.name), null)
|
||||
cursor.insertBefore(userDiv, null)
|
||||
return cursor
|
||||
}, { key: userID + '' }))
|
||||
const from = math.min(anchor, head)
|
||||
const to = math.max(anchor, head)
|
||||
decorations.push(Decoration.inline(from, to, { style: `background-color: ${user.color}` }))
|
||||
}
|
||||
}
|
||||
})
|
||||
return DecorationSet.create(state.doc, decorations)
|
||||
}
|
||||
},
|
||||
view: view => {
|
||||
const ystate = prosemirrorPluginKey.getState(view.state)
|
||||
const y = ystate.y
|
||||
const awarenessListener = () => {
|
||||
view.updateState(view.state)
|
||||
}
|
||||
const updateCursorInfo = () => {
|
||||
const current = y.getLocalAwarenessInfo()
|
||||
if (view.hasFocus()) {
|
||||
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, 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)) {
|
||||
y.setAwarenessField('cursor', {
|
||||
anchor, head
|
||||
})
|
||||
}
|
||||
} else if (current.cursor !== null) {
|
||||
y.setAwarenessField('cursor', null)
|
||||
}
|
||||
}
|
||||
y.on('awareness', awarenessListener)
|
||||
view.dom.addEventListener('focusin', updateCursorInfo)
|
||||
view.dom.addEventListener('focusout', updateCursorInfo)
|
||||
return {
|
||||
update: updateCursorInfo,
|
||||
destroy: () => {
|
||||
const y = prosemirrorPluginKey.getState(view.state).y
|
||||
y.setAwarenessField('cursor', null)
|
||||
y.off('awareness', awarenessListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Transforms a Prosemirror based absolute position to a Yjs based relative position.
|
||||
*
|
||||
* @param {number} pos
|
||||
* @param {YXmlFragment} type
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {any} relative position
|
||||
*/
|
||||
export const absolutePositionToRelativePosition = (pos, type, mapping) => {
|
||||
if (pos === 0) {
|
||||
return YPos.getRelativePosition(type, 0)
|
||||
}
|
||||
let n = type._first
|
||||
if (n !== null) {
|
||||
while (type !== n) {
|
||||
const pNodeSize = (mapping.get(n) || { nodeSize: 0 }).nodeSize
|
||||
if (n.constructor === YText) {
|
||||
if (n.length >= pos) {
|
||||
return YPos.getRelativePosition(n, pos)
|
||||
} else {
|
||||
pos -= n.length
|
||||
}
|
||||
if (n._next !== null) {
|
||||
n = n._next
|
||||
} else {
|
||||
do {
|
||||
n = n._parent
|
||||
pos--
|
||||
} while (n._next === null && n !== type)
|
||||
if (n !== type) {
|
||||
n = n._next
|
||||
}
|
||||
}
|
||||
} else if (n._first !== null && pos < pNodeSize) {
|
||||
n = n._first
|
||||
pos--
|
||||
} else {
|
||||
if (pos === 1 && n.length === 0 && pNodeSize > 1) {
|
||||
// edge case, should end in this paragraph
|
||||
return ['endof', n._id.user, n._id.clock, null, null]
|
||||
}
|
||||
pos -= pNodeSize
|
||||
if (n._next !== null) {
|
||||
n = n._next
|
||||
} else {
|
||||
if (pos === 0) {
|
||||
n = n._parent
|
||||
return ['endof', n._id.user, n._id.clock || null, n._id.name || null, n._id.type || null]
|
||||
}
|
||||
do {
|
||||
n = n._parent
|
||||
pos--
|
||||
} while (n._next === null && n !== type)
|
||||
if (n !== type) {
|
||||
n = n._next
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos === 0 && n.constructor !== YText && n !== type) { // TODO: set to <= 0
|
||||
return [n._id.user, n._id.clock]
|
||||
}
|
||||
}
|
||||
}
|
||||
return YPos.getRelativePosition(type, type.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {YXmlFragment} yDoc Top level type that is bound to pView
|
||||
* @param {any} relPos Encoded Yjs based relative position
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
*/
|
||||
export const relativePositionToAbsolutePosition = (yDoc, relPos, mapping) => {
|
||||
const decodedPos = YPos.fromRelativePosition(yDoc._y, relPos)
|
||||
if (decodedPos === null) {
|
||||
return null
|
||||
}
|
||||
let type = decodedPos.type
|
||||
let pos = 0
|
||||
if (type.constructor === YText) {
|
||||
pos = decodedPos.offset
|
||||
} else if (!type._deleted) {
|
||||
let n = type._first
|
||||
let i = 0
|
||||
while (i < type.length && i < decodedPos.offset && n !== null) {
|
||||
i++
|
||||
pos += mapping.get(n).nodeSize
|
||||
n = n._next
|
||||
}
|
||||
pos += 1 // increase because we go out of n
|
||||
}
|
||||
while (type !== yDoc) {
|
||||
const parent = type._parent
|
||||
if (!parent._deleted) {
|
||||
pos += 1 // the start tag
|
||||
let n = parent._first
|
||||
// now iterate until we found type
|
||||
while (n !== null) {
|
||||
if (n === type) {
|
||||
break
|
||||
}
|
||||
pos += mapping.get(n).nodeSize
|
||||
n = n._next
|
||||
}
|
||||
}
|
||||
type = parent
|
||||
}
|
||||
return pos - 1 // we don't count the most outer tag, because it is a fragment
|
||||
}
|
||||
|
||||
/**
|
||||
* Binding for prosemirror.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export class ProsemirrorBinding {
|
||||
/**
|
||||
* @param {YXmlFragment} 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 Map()
|
||||
this._observeFunction = this._typeChanged.bind(this)
|
||||
this.y = yXmlFragment._y
|
||||
/**
|
||||
* current selection as relative positions in the Yjs model
|
||||
*/
|
||||
this._relSelection = null
|
||||
this.y.on('beforeTransaction', e => {
|
||||
this._relSelection = {
|
||||
anchor: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.anchor, yXmlFragment, this.mapping),
|
||||
head: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.head, yXmlFragment, this.mapping)
|
||||
}
|
||||
})
|
||||
yXmlFragment.observeDeep(this._observeFunction)
|
||||
}
|
||||
_typeChanged (events, transaction) {
|
||||
if (events.length === 0) {
|
||||
return
|
||||
}
|
||||
console.info('new types:', transaction.newTypes.size, 'deleted types:', transaction.deletedStructs.size, transaction.newTypes, transaction.deletedStructs)
|
||||
this.mux(() => {
|
||||
const delStruct = (_, struct) => this.mapping.delete(struct)
|
||||
transaction.deletedStructs.forEach(struct => this.mapping.delete(struct))
|
||||
transaction.changedTypes.forEach(delStruct)
|
||||
transaction.changedParentTypes.forEach(delStruct)
|
||||
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(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))
|
||||
const relSel = this._relSelection
|
||||
if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
|
||||
const anchor = relativePositionToAbsolutePosition(this.type, relSel.anchor, this.mapping)
|
||||
const head = relativePositionToAbsolutePosition(this.type, relSel.head, this.mapping)
|
||||
if (anchor !== null && head !== null) {
|
||||
tr.setSelection(TextSelection.create(tr.doc, anchor, head))
|
||||
}
|
||||
}
|
||||
this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr))
|
||||
})
|
||||
}
|
||||
_prosemirrorChanged (doc) {
|
||||
this.mux(() => {
|
||||
updateYFragment(this.type, doc.content, this.mapping)
|
||||
})
|
||||
}
|
||||
destroy () {
|
||||
this.type.unobserveDeep(this._observeFunction)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @privateMapping
|
||||
* @param {YXmlElement} el
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {PModel.Node}
|
||||
*/
|
||||
export const createNodeIfNotExists = (el, schema, mapping) => {
|
||||
const node = mapping.get(el)
|
||||
if (node === undefined) {
|
||||
return createNodeFromYElement(el, schema, mapping)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {YXmlElement} el
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @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) => {
|
||||
const children = []
|
||||
el.toArray().forEach(type => {
|
||||
if (type.constructor === YXmlElement) {
|
||||
const n = createNodeIfNotExists(type, schema, mapping)
|
||||
if (n !== null) {
|
||||
children.push(n)
|
||||
}
|
||||
} else {
|
||||
const ns = createTextNodesFromYText(type, schema, mapping)
|
||||
if (ns !== null) {
|
||||
ns.forEach(textchild => {
|
||||
if (textchild !== null) {
|
||||
children.push(textchild)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
let node
|
||||
try {
|
||||
node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), children)
|
||||
} catch (e) {
|
||||
// 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
|
||||
el._y.transact(() => {
|
||||
el._delete(el._y, true)
|
||||
})
|
||||
return null
|
||||
}
|
||||
mapping.set(el, node)
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {YText} text
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {Array<PModel.Node>}
|
||||
*/
|
||||
export const createTextNodesFromYText = (text, schema, mapping) => {
|
||||
const nodes = []
|
||||
const deltas = text.toDelta()
|
||||
try {
|
||||
for (let i = 0; i < deltas.length; i++) {
|
||||
const delta = deltas[i]
|
||||
const marks = []
|
||||
for (let markName in delta.attributes) {
|
||||
marks.push(schema.mark(markName, delta.attributes[markName]))
|
||||
}
|
||||
nodes.push(schema.text(delta.insert, marks))
|
||||
}
|
||||
if (nodes.length > 0) {
|
||||
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
||||
}
|
||||
} catch (e) {
|
||||
text._y.transact(() => {
|
||||
text._delete(text._y, true)
|
||||
})
|
||||
return null
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {PModel.Node} node
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {YXmlElement | YText}
|
||||
*/
|
||||
export const createTypeFromNode = (node, mapping) => {
|
||||
let type
|
||||
if (node.isText) {
|
||||
type = new YText()
|
||||
const attrs = {}
|
||||
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
|
||||
type.insert(0, node.text, attrs)
|
||||
} else {
|
||||
type = new YXmlElement(node.type.name)
|
||||
for (let key in node.attrs) {
|
||||
const val = node.attrs[key]
|
||||
if (val !== null) {
|
||||
type.setAttribute(key, val)
|
||||
}
|
||||
}
|
||||
const ins = []
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
ins.push(createTypeFromNode(node.child(i), mapping))
|
||||
}
|
||||
type.insert(0, ins)
|
||||
}
|
||||
mapping.set(type, node)
|
||||
return type
|
||||
}
|
||||
|
||||
const equalAttrs = (pattrs, yattrs) => {
|
||||
const keys = Object.keys(pattrs).filter(key => pattrs[key] === null)
|
||||
let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
|
||||
for (let i = 0; i < keys.length && eq; i++) {
|
||||
const key = keys[i]
|
||||
eq = pattrs[key] === yattrs[key]
|
||||
}
|
||||
return eq
|
||||
}
|
||||
|
||||
const equalYTextPText = (ytext, ptext) => {
|
||||
const d = ytext.toDelta()[0]
|
||||
return d.insert === ptext.text && object.keys(d.attributes || {}).length === ptext.marks.length && ptext.marks.every(mark => equalAttrs(d.attributes[mark.type.name], mark.attrs))
|
||||
}
|
||||
|
||||
const equalYTypePNode = (ytype, pnode) =>
|
||||
ytype.constructor === YText
|
||||
? equalYTextPText(ytype, pnode)
|
||||
: (matchNodeName(ytype, pnode) && ytype.length === pnode.childCount && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, pnode.child(i))))
|
||||
|
||||
const computeChildEqualityFactor = (ytype, pnode, mapping) => {
|
||||
const yChildren = ytype.toArray()
|
||||
const pChildCnt = pnode.childCount
|
||||
const yChildCnt = yChildren.length
|
||||
const minCnt = math.min(yChildCnt, pChildCnt)
|
||||
let left = 0
|
||||
let right = 0
|
||||
let foundMappedChild = false
|
||||
for (; left < minCnt; left++) {
|
||||
const leftY = yChildren[left]
|
||||
const leftP = pnode.child(left)
|
||||
if (mapping.get(leftY) === leftP) {
|
||||
foundMappedChild = true// definite (good) match!
|
||||
} else if (!equalYTypePNode(leftY, leftP)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for (; left + right < minCnt; right++) {
|
||||
const rightY = yChildren[yChildCnt - right - 1]
|
||||
const rightP = pnode.child(pChildCnt - right - 1)
|
||||
if (mapping.get(rightY) !== rightP) {
|
||||
foundMappedChild = true
|
||||
} else if (!equalYTypePNode(rightP, rightP)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
equalityFactor: left + right,
|
||||
foundMappedChild
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {YXmlFragment} yDomFragment
|
||||
* @param {PModel.Node} pContent
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
*/
|
||||
const updateYFragment = (yDomFragment, pContent, mapping) => {
|
||||
if (yDomFragment instanceof YXmlElement && yDomFragment.nodeName.toLowerCase() !== pContent.type.name) {
|
||||
throw new Error('node name mismatch!')
|
||||
}
|
||||
mapping.set(yDomFragment, pContent)
|
||||
// update attributes
|
||||
if (yDomFragment instanceof YXmlElement) {
|
||||
const yDomAttrs = yDomFragment.getAttributes()
|
||||
const pAttrs = pContent.attrs
|
||||
for (let key in pAttrs) {
|
||||
if (pAttrs[key] !== null) {
|
||||
if (yDomAttrs[key] !== pAttrs[key]) {
|
||||
yDomFragment.setAttribute(key, pAttrs[key])
|
||||
}
|
||||
} else {
|
||||
yDomFragment.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
// remove all keys that are no longer in pAttrs
|
||||
for (let key in yDomAttrs) {
|
||||
if (pAttrs[key] === undefined) {
|
||||
yDomFragment.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
// update children
|
||||
const pChildCnt = pContent.childCount
|
||||
const yChildren = yDomFragment.toArray()
|
||||
const yChildCnt = yChildren.length
|
||||
const minCnt = math.min(pChildCnt, yChildCnt)
|
||||
let left = 0
|
||||
let right = 0
|
||||
// find number of matching elements from left
|
||||
for (;left < minCnt; left++) {
|
||||
const leftY = yChildren[left]
|
||||
const leftP = pContent.child(left)
|
||||
if (mapping.get(leftY) !== leftP) {
|
||||
if (equalYTypePNode(leftY, leftP)) {
|
||||
// update mapping
|
||||
mapping.set(leftY, leftP)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// find number of matching elements from right
|
||||
for (;right + left < minCnt; right++) {
|
||||
const rightY = yChildren[yChildCnt - right - 1]
|
||||
const rightP = pContent.child(pChildCnt - right - 1)
|
||||
if (mapping.get(rightY) !== rightP) {
|
||||
if (equalYTypePNode(rightY, rightP)) {
|
||||
// update mapping
|
||||
mapping.set(rightY, rightP)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
yDomFragment._y.transact(() => {
|
||||
// try to compare and update
|
||||
while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
|
||||
const leftY = yChildren[left]
|
||||
const leftP = pContent.child(left)
|
||||
const rightY = yChildren[yChildCnt - right - 1]
|
||||
const rightP = pContent.child(pChildCnt - right - 1)
|
||||
if (leftY.constructor === YText && leftP.isText) {
|
||||
if (!equalYTextPText(leftY, leftP)) {
|
||||
yDomFragment.delete(left, 1)
|
||||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||
}
|
||||
left += 1
|
||||
} else {
|
||||
let updateLeft = matchNodeName(leftY, leftP)
|
||||
let updateRight = matchNodeName(rightY, rightP)
|
||||
if (updateLeft && updateRight) {
|
||||
// decide which which element to update
|
||||
const equalityLeft = computeChildEqualityFactor(leftY, leftP, mapping)
|
||||
const equalityRight = computeChildEqualityFactor(rightY, rightP, mapping)
|
||||
if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
|
||||
updateRight = false
|
||||
} else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
|
||||
updateLeft = false
|
||||
} else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
|
||||
updateLeft = false
|
||||
} else {
|
||||
updateRight = false
|
||||
}
|
||||
}
|
||||
if (updateLeft) {
|
||||
updateYFragment(leftY, leftP, mapping)
|
||||
left += 1
|
||||
} else if (updateRight) {
|
||||
updateYFragment(rightY, rightP, mapping)
|
||||
right += 1
|
||||
} else {
|
||||
yDomFragment.delete(left, 1)
|
||||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||
left += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
const yDelLen = yChildCnt - left - right
|
||||
if (yDelLen > 0) {
|
||||
yDomFragment.delete(left, yDelLen)
|
||||
}
|
||||
if (left + right < pChildCnt) {
|
||||
const ins = []
|
||||
for (let i = left; i < pChildCnt - right; i++) {
|
||||
ins.push(createTypeFromNode(pContent.child(i), mapping))
|
||||
}
|
||||
yDomFragment.insert(left, ins)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {YXmlElement} yElement
|
||||
* @param {any} pNode Prosemirror Node
|
||||
*/
|
||||
const matchNodeName = (yElement, pNode) => yElement.nodeName === pNode.type.name.toUpperCase()
|
||||
@@ -1,299 +0,0 @@
|
||||
/**
|
||||
* @module bindings/prosemirror
|
||||
*/
|
||||
|
||||
import { BindMapping } from '../utils/BindMapping.mjs'
|
||||
import { YText } from '../types/YText.mjs' // eslint-disable-line
|
||||
import { YXmlElement, YXmlFragment } from '../types/YXmlElement.mjs' // eslint-disable-line
|
||||
import { createMutex } from '../lib/mutex.mjs'
|
||||
import * as PModel from 'prosemirror-model'
|
||||
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
|
||||
import { Plugin, PluginKey, EditorState } from 'prosemirror-state' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @typedef {BindMapping<YText | YXmlElement, PModel.Node>} ProsemirrorMapping
|
||||
*/
|
||||
|
||||
/**
|
||||
* The unique prosemirror plugin key for prosemirrorPlugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const prosemirrorPluginKey = new PluginKey('yjs')
|
||||
|
||||
/**
|
||||
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
|
||||
*
|
||||
* This plugin also keeps references to the type and the shared document so other plugins can access it.
|
||||
* @param {YXmlFragment} yXmlFragment
|
||||
* @return {Plugin} Returns a prosemirror plugin that binds to this type
|
||||
*/
|
||||
export const prosemirrorPlugin = yXmlFragment => {
|
||||
const pluginState = {
|
||||
type: yXmlFragment,
|
||||
y: yXmlFragment._y,
|
||||
binding: null
|
||||
}
|
||||
const plugin = new Plugin({
|
||||
key: prosemirrorPluginKey,
|
||||
state: {
|
||||
init: (initargs, state) => {
|
||||
return pluginState
|
||||
},
|
||||
apply: (tr, pluginState) => {
|
||||
return pluginState
|
||||
}
|
||||
},
|
||||
view: view => {
|
||||
const binding = new ProsemirrorBinding(yXmlFragment, view)
|
||||
pluginState.binding = binding
|
||||
return {
|
||||
update: () => {
|
||||
binding._prosemirrorChanged()
|
||||
},
|
||||
destroy: () => {
|
||||
binding.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique prosemirror plugin key for cursorPlugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const cursorPluginKey = new PluginKey('yjs-cursor')
|
||||
|
||||
/**
|
||||
* A prosemirror plugin that listens to awareness information on Yjs.
|
||||
* This requires that a `prosemirrorPlugin` is also bound to the prosemirror.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const cursorPlugin = new Plugin({
|
||||
key: cursorPluginKey,
|
||||
props: {
|
||||
decorations: state => {
|
||||
const y = prosemirrorPluginKey.getState(state).y
|
||||
const awareness = y.getAwarenessInfo()
|
||||
const decorations = []
|
||||
awareness.forEach((state, userID) => {
|
||||
if (state.cursor != null) {
|
||||
const username = `User: ${userID}`
|
||||
decorations.push(Decoration.widget(state.cursor.from, () => {
|
||||
const cursor = document.createElement('span')
|
||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||
const user = document.createElement('div')
|
||||
user.insertBefore(document.createTextNode(username), null)
|
||||
cursor.insertBefore(user, null)
|
||||
return cursor
|
||||
}, { key: username }))
|
||||
decorations.push(Decoration.inline(state.cursor.from, state.cursor.to, { style: 'background-color: #ffa50070' }))
|
||||
}
|
||||
})
|
||||
return DecorationSet.create(state.doc, decorations)
|
||||
}
|
||||
},
|
||||
view: view => {
|
||||
const y = prosemirrorPluginKey.getState(view.state).y
|
||||
const awarenessListener = () => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Binding for prosemirror.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export class ProsemirrorBinding {
|
||||
/**
|
||||
* @param {YXmlFragment} 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 === YXmlElement) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Y.XmlElement} el
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {PModel.Node}
|
||||
*/
|
||||
export const createNodeIfNotExists = (el, schema, mapping) => {
|
||||
const node = mapping.getY(el)
|
||||
if (node === undefined) {
|
||||
return createNodeFromYElement(el, schema, mapping)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Y.XmlElement} el
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {PModel.Node}
|
||||
*/
|
||||
export const createNodeFromYElement = (el, schema, mapping) => {
|
||||
const children = []
|
||||
el.toArray().forEach(type => {
|
||||
if (type.constructor === YXmlElement) {
|
||||
children.push(createNodeIfNotExists(type, schema, mapping))
|
||||
} else {
|
||||
children.concat(createTextNodesFromYText(type, schema, mapping)).forEach(textchild => children.push(textchild))
|
||||
}
|
||||
})
|
||||
const node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), el.toArray().map(t => createNodeIfNotExists(t, schema, mapping)))
|
||||
mapping.bind(el, node)
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Y.Text} text
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {Array<PModel.Node>}
|
||||
*/
|
||||
export const createTextNodesFromYText = (text, schema, mapping) => {
|
||||
const nodes = []
|
||||
const deltas = text.toDelta()
|
||||
for (let i = 0; i < deltas.length; i++) {
|
||||
const delta = deltas[i]
|
||||
const marks = []
|
||||
for (let markName in delta.attributes) {
|
||||
marks.push(schema.mark(markName, delta.attributes[markName]))
|
||||
}
|
||||
nodes.push(schema.text(delta.insert, marks))
|
||||
}
|
||||
if (nodes.length > 0) {
|
||||
mapping.bind(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {PModel.Node} node
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {YXmlElement | YText}
|
||||
*/
|
||||
export const createTypeFromNode = (node, mapping) => {
|
||||
let type
|
||||
if (node.isText) {
|
||||
type = new YText()
|
||||
const attrs = {}
|
||||
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
|
||||
type.insert(0, node.text, attrs)
|
||||
} else {
|
||||
type = new YXmlElement(node.type.name)
|
||||
for (let key in node.attrs) {
|
||||
type.setAttribute(key, node.attrs[key])
|
||||
}
|
||||
type.insert(0, node.content.content.map(node => createTypeFromNode(node, mapping)))
|
||||
}
|
||||
mapping.bind(type, node)
|
||||
return type
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {YXmlFragment} yDomFragment
|
||||
* @param {EditorState} state
|
||||
* @param {BindMapping} mapping
|
||||
*/
|
||||
const updateYFragment = (yDomFragment, state, mapping) => {
|
||||
const pChildCnt = state.doc.content.childCount
|
||||
const yChildren = yDomFragment.toArray()
|
||||
const yChildCnt = yChildren.length
|
||||
const minCnt = pChildCnt < yChildCnt ? pChildCnt : yChildCnt
|
||||
let left = 0
|
||||
let right = 0
|
||||
// find number of matching elements from left
|
||||
for (;left < minCnt; left++) {
|
||||
if (state.doc.content.child(left) !== mapping.getY(yChildren[left])) {
|
||||
break
|
||||
}
|
||||
}
|
||||
// find number of matching elements from right
|
||||
for (;right < minCnt; right++) {
|
||||
if (state.doc.content.child(pChildCnt - right - 1) !== mapping.getY(yChildren[yChildCnt - right - 1])) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (left + right > pChildCnt) {
|
||||
// nothing changed
|
||||
return
|
||||
}
|
||||
yDomFragment._y.transact(() => {
|
||||
// now update y to match editor state
|
||||
yDomFragment.delete(left, yChildCnt - left - right)
|
||||
yDomFragment.insert(left, state.doc.content.content.slice(left, pChildCnt - right).map(node => createTypeFromNode(node, mapping)))
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* @module bindings/quill
|
||||
*/
|
||||
|
||||
import { createMutex } from '../lib/mutex.mjs'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
|
||||
const typeObserver = function (event) {
|
||||
const quill = this.target
|
||||
@@ -53,7 +53,6 @@ export class QuillBinding {
|
||||
* @private
|
||||
*/
|
||||
this._mutualExclude = createMutex()
|
||||
|
||||
// Set initial value.
|
||||
quill.setContents(textType.toDelta(), 'yjs')
|
||||
// Observers are handled by this class.
|
||||
@@ -2,9 +2,9 @@
|
||||
* @module bindings/textarea
|
||||
*/
|
||||
|
||||
import { simpleDiff } from '../lib/diff.mjs'
|
||||
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.mjs'
|
||||
import { createMutex } from '../lib/mutex.mjs'
|
||||
import { simpleDiff } from '../lib/diff.js'
|
||||
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
|
||||
function typeObserver () {
|
||||
this._mutualExclude(() => {
|
||||
@@ -10,13 +10,18 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>This example shows how to bind a YXmlFragment type to an arbitrary DOM element. We set the DOM element to contenteditable so it basically behaves like a very powerful rich-text editor.</p>
|
||||
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||
<hr>
|
||||
<div class="code-html">
|
||||
<div id="content" contenteditable=""></div>
|
||||
|
||||
<div id="content" contenteditable=""></div>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./dom.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/dom.js">
|
||||
import * as Y from 'yjs/index.mjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
|
||||
import { DomBinding } from 'yjs/bindings/dom.mjs'
|
||||
import * as Y from 'yjs/index.js'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { DomBinding } from 'yjs/bindings/dom.js'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
const ydocument = provider.get('dom')
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as Y from '../index.mjs'
|
||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
||||
import { DomBinding } from '../bindings/dom.mjs'
|
||||
import * as Y from '../index.js'
|
||||
import { WebsocketProvider } from '../provider/websocket.js'
|
||||
import { DomBinding } from '../bindings/dom.js'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('dom')
|
||||
const type = ydocument.define('xml', Y.XmlFragment)
|
||||
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
||||
9
examples/exampleConfig.js
Normal file
9
examples/exampleConfig.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
const isDeployed = location.hostname === 'yjs.website'
|
||||
|
||||
if (!isDeployed) {
|
||||
console.log('%cYjs: Start your local websocket server by running %c`npm run websocket-server`', 'color:blue', 'color: grey; font-weight: bold')
|
||||
}
|
||||
|
||||
export const serverAddress = isDeployed ? 'wss://api.yjs.website' : 'ws://localhost:1234'
|
||||
@@ -16,6 +16,13 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
.ProseMirror img { max-width: 100px }
|
||||
/* this is a rough fix for the first cursor position when the first paragraph is empty */
|
||||
.ProseMirror > .ProseMirror-yjs-cursor:first-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child {
|
||||
margin-top: 16px
|
||||
}
|
||||
.ProseMirror-yjs-cursor {
|
||||
position: absolute;
|
||||
border-left: black;
|
||||
@@ -41,13 +48,17 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>This example shows how to bind a YXmlFragment type to a <a href="http://prosemirror.net">Prosemirror</a> editor.</p>
|
||||
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||
<div class="code-html">
|
||||
<div id="editor" style="margin-bottom: 23px"></div>
|
||||
<div style="display: none" id="content"></div>
|
||||
|
||||
<div id="editor" style="margin-bottom: 23px"></div>
|
||||
<div style="display: none" id="content"></div>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./prosemirror.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/prosemirror.js">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
||||
import { WebsocketProvider } from '../provider/websocket.js'
|
||||
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
|
||||
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as Y from '../index.mjs'
|
||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
||||
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
|
||||
import * as Y from '../index.js'
|
||||
import { WebsocketProvider } from '../provider/websocket.js'
|
||||
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
@@ -8,7 +10,7 @@ import { DOMParser } from 'prosemirror-model'
|
||||
import { schema } from 'prosemirror-schema-basic'
|
||||
import { exampleSetup } from 'prosemirror-example-setup'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('prosemirror')
|
||||
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yjs Prosemirror Example</title>
|
||||
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
||||
</head>
|
||||
<title>Yjs Quill Example</title>
|
||||
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css">
|
||||
</head>
|
||||
<body>
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
<p>This example shows how to bind a YText type to <a href="https://quilljs.com">Quill</a> editor.</p>
|
||||
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||
<div class="code-html">
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./quill.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/quill.js">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
|
||||
import { QuillBinding } from 'yjs/bindings/quill.mjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { QuillBinding } from 'yjs/bindings/quill.js'
|
||||
|
||||
import Quill from 'quill'
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import * as Y from '../index.mjs'
|
||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
||||
import { QuillBinding } from '../bindings/quill.mjs'
|
||||
import * as Y from '../index.js'
|
||||
import { WebsocketProvider } from '../provider/websocket.js'
|
||||
import { QuillBinding } from '../bindings/quill.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import Quill from 'quill'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('quill')
|
||||
const ytext = ydocument.define('quill', Y.Text)
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Yjs Textarea Example</title>
|
||||
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
||||
</head>
|
||||
<body>
|
||||
<p>This example shows how to bind a YText type to a DOM Textarea.</p>
|
||||
<p>The content of this textarea is shared with every client who visits this domain.</p>
|
||||
<div class="code-html">
|
||||
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
|
||||
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./textarea.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/textarea.js">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
|
||||
import { TextareaBinding } from 'yjs/bindings/textarea.mjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { TextareaBinding } from 'yjs/bindings/textarea.js'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
const ydocument = provider.get('textarea')
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as Y from '../index.mjs'
|
||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
||||
import { TextareaBinding } from '../bindings/textarea.mjs'
|
||||
import * as Y from '../index.js'
|
||||
import { WebsocketProvider } from '../provider/websocket.js'
|
||||
import { TextareaBinding } from '../bindings/textarea.js'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('textarea')
|
||||
const type = ydocument.define('textarea', Y.Text)
|
||||
const textarea = document.querySelector('textarea')
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<div id="aceContainer"></div>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div id="codeMirrorContainer"></div>
|
||||
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.min.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
</head>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.min.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
|
||||
import Y from '../../src/Y.mjs'
|
||||
import DomBinding from '../../bindings/DomBinding/DomBinding.mjs'
|
||||
import UndoManager from '../../src/Util/UndoManager.mjs'
|
||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
|
||||
import YXmlText from '../../src/Types/YXml/YXmlText.mjs'
|
||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.mjs'
|
||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.mjs'
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
|
||||
import Y from '../../src/Y.js'
|
||||
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
|
||||
import UndoManager from '../../src/Util/UndoManager.js'
|
||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
|
||||
import YXmlText from '../../src/Types/YXml/YXmlText.js'
|
||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
|
||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.js'
|
||||
|
||||
const connector = new YWebsocketsConnector()
|
||||
const persistence = new YIndexdDBPersistence()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
</style>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</g>
|
||||
</svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
}
|
||||
</style>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
import { createYdbClient } from '../../YdbClient/index.mjs'
|
||||
import Y from '../../src/Y.dist.mjs'
|
||||
import * as ydb from '../../YdbClient/YdbClient.mjs'
|
||||
import DomBinding from '../../bindings/DomBinding/DomBinding.mjs'
|
||||
import { createYdbClient } from '../../YdbClient/index.js'
|
||||
import Y from '../../src/Y.dist.js'
|
||||
import * as ydb from '../../YdbClient/YdbClient.js'
|
||||
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
|
||||
|
||||
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = Math.random() * 16 | 0
|
||||
|
||||
55
index.js
Normal file
55
index.js
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
import './structs/Item.js'
|
||||
import { Delete } from './structs/Delete.js'
|
||||
import { ItemJSON } from './structs/ItemJSON.js'
|
||||
import { ItemString } from './structs/ItemString.js'
|
||||
import { ItemFormat } from './structs/ItemFormat.js'
|
||||
import { ItemEmbed } from './structs/ItemEmbed.js'
|
||||
import { GC } from './structs/GC.js'
|
||||
|
||||
import { YArray } from './types/YArray.js'
|
||||
import { YMap } from './types/YMap.js'
|
||||
import { YText } from './types/YText.js'
|
||||
import { YXmlText } from './types/YXmlText.js'
|
||||
import { YXmlHook } from './types/YXmlHook.js'
|
||||
import { YXmlElement, YXmlFragment } from './types/YXmlElement.js'
|
||||
|
||||
import { registerStruct } from './utils/structReferences.js'
|
||||
|
||||
import * as decoding from './lib/decoding.js'
|
||||
import * as encoding from './lib/encoding.js'
|
||||
import * as awarenessProtocol from './protocols/awareness.js'
|
||||
import * as syncProtocol from './protocols/sync.js'
|
||||
import * as authProtocol from './protocols/auth.js'
|
||||
|
||||
export { decoding, encoding, awarenessProtocol, syncProtocol, authProtocol }
|
||||
|
||||
export { Y } from './utils/Y.js'
|
||||
export { UndoManager } from './utils/UndoManager.js'
|
||||
export { Transaction } from './utils/Transaction.js'
|
||||
|
||||
export { YArray as Array } from './types/YArray.js'
|
||||
export { YMap as Map } from './types/YMap.js'
|
||||
export { YText as Text } from './types/YText.js'
|
||||
export { YXmlText as XmlText } from './types/YXmlText.js'
|
||||
export { YXmlHook as XmlHook } from './types/YXmlHook.js'
|
||||
export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.js'
|
||||
|
||||
export { getRelativePosition, fromRelativePosition } from './utils/relativePosition.js'
|
||||
export { registerStruct } from './utils/structReferences.js'
|
||||
export * from './lib/mutex.js'
|
||||
|
||||
registerStruct(0, GC)
|
||||
registerStruct(1, ItemJSON)
|
||||
registerStruct(2, ItemString)
|
||||
registerStruct(3, ItemFormat)
|
||||
registerStruct(4, Delete)
|
||||
|
||||
registerStruct(5, YArray)
|
||||
registerStruct(6, YMap)
|
||||
registerStruct(7, YText)
|
||||
registerStruct(8, YXmlFragment)
|
||||
registerStruct(9, YXmlElement)
|
||||
registerStruct(10, YXmlText)
|
||||
registerStruct(11, YXmlHook)
|
||||
registerStruct(12, ItemEmbed)
|
||||
50
index.mjs
50
index.mjs
@@ -1,50 +0,0 @@
|
||||
|
||||
import { Delete } from './structs/Delete.mjs'
|
||||
import { ItemJSON } from './structs/ItemJSON.mjs'
|
||||
import { ItemString } from './structs/ItemString.mjs'
|
||||
import { ItemFormat } from './structs/ItemFormat.mjs'
|
||||
import { ItemEmbed } from './structs/ItemEmbed.mjs'
|
||||
import { GC } from './structs/GC.mjs'
|
||||
|
||||
import { YArray } from './types/YArray.mjs'
|
||||
import { YMap } from './types/YMap.mjs'
|
||||
import { YText } from './types/YText.mjs'
|
||||
import { YXmlText } from './types/YXmlText.mjs'
|
||||
import { YXmlHook } from './types/YXmlHook.mjs'
|
||||
import { YXmlElement, YXmlFragment } from './types/YXmlElement.mjs'
|
||||
|
||||
import { registerStruct } from './utils/structReferences.mjs'
|
||||
|
||||
export { Y } from './utils/Y.mjs'
|
||||
export { UndoManager } from './utils/UndoManager.mjs'
|
||||
export { Transaction } from './utils/Transaction.mjs'
|
||||
|
||||
export { YArray as Array } from './types/YArray.mjs'
|
||||
export { YMap as Map } from './types/YMap.mjs'
|
||||
export { YText as Text } from './types/YText.mjs'
|
||||
export { YXmlText as XmlText } from './types/YXmlText.mjs'
|
||||
export { YXmlHook as XmlHook } from './types/YXmlHook.mjs'
|
||||
export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.mjs'
|
||||
|
||||
export { getRelativePosition, fromRelativePosition } from './utils/relativePosition.mjs'
|
||||
export { registerStruct } from './utils/structReferences.mjs'
|
||||
export * from './protocols/syncProtocol.mjs'
|
||||
export * from './protocols/awarenessProtocol.mjs'
|
||||
export * from './lib/encoding.mjs'
|
||||
export * from './lib/decoding.mjs'
|
||||
export * from './lib/mutex.mjs'
|
||||
|
||||
registerStruct(0, GC)
|
||||
registerStruct(1, ItemJSON)
|
||||
registerStruct(2, ItemString)
|
||||
registerStruct(3, ItemFormat)
|
||||
registerStruct(4, Delete)
|
||||
|
||||
registerStruct(5, YArray)
|
||||
registerStruct(6, YMap)
|
||||
registerStruct(7, YText)
|
||||
registerStruct(8, YXmlFragment)
|
||||
registerStruct(9, YXmlElement)
|
||||
registerStruct(10, YXmlText)
|
||||
registerStruct(11, YXmlHook)
|
||||
registerStruct(12, ItemEmbed)
|
||||
40
lib/binary.js
Normal file
40
lib/binary.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
/**
|
||||
* @module binary
|
||||
*/
|
||||
|
||||
import * as string from './string.js'
|
||||
import * as globals from './globals.js'
|
||||
|
||||
export const BITS32 = 0xFFFFFFFF
|
||||
export const BITS21 = (1 << 21) - 1
|
||||
export const BITS16 = (1 << 16) - 1
|
||||
|
||||
export const BIT26 = 1 << 26
|
||||
export const BIT32 = 1 << 32
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
* @return {string}
|
||||
*/
|
||||
export const toBase64 = bytes => {
|
||||
let s = ''
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
s += string.fromCharCode(bytes[i])
|
||||
}
|
||||
return btoa(s)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export const fromBase64 = s => {
|
||||
const a = atob(s)
|
||||
const bytes = globals.createUint8ArrayFromLen(a.length)
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
bytes[i] = a.charCodeAt(i)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* @module binary
|
||||
*/
|
||||
|
||||
export const BITS32 = 0xFFFFFFFF
|
||||
export const BITS21 = (1 << 21) - 1
|
||||
export const BITS16 = (1 << 16) - 1
|
||||
|
||||
export const BIT26 = 1 << 26
|
||||
export const BIT32 = 1 << 32
|
||||
72
lib/broadcastchannel.js
Normal file
72
lib/broadcastchannel.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
import * as binary from './binary.js'
|
||||
import * as globals from './globals.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object} Channel
|
||||
* @property {Set<Function>} Channel.subs
|
||||
* @property {BC} Channel.bc
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Map<string, Channel>}
|
||||
*/
|
||||
const channels = new Map()
|
||||
|
||||
class LocalStoragePolyfill {
|
||||
constructor (room) {
|
||||
this.room = room
|
||||
this.onmessage = null
|
||||
addEventListener('storage', e => e.key === room && this.onmessage !== null && this.onmessage({ data: binary.fromBase64(e.newValue) }))
|
||||
}
|
||||
/**
|
||||
* @param {ArrayBuffer} data
|
||||
*/
|
||||
postMessage (buf) {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(this.room, binary.toBase64(globals.createUint8ArrayFromArrayBuffer(buf)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use BroadcastChannel or Polyfill
|
||||
const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel
|
||||
|
||||
/**
|
||||
* @param {string} room
|
||||
* @return {Channel}
|
||||
*/
|
||||
const getChannel = room => {
|
||||
let c = channels.get(room)
|
||||
if (c === undefined) {
|
||||
const subs = new Set()
|
||||
const bc = new BC(room)
|
||||
bc.onmessage = e => subs.forEach(sub => sub(e.data))
|
||||
c = {
|
||||
bc, subs
|
||||
}
|
||||
channels.set(room, c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {string} room
|
||||
* @param {Function} f
|
||||
*/
|
||||
export const subscribe = (room, f) => getChannel(room).subs.add(f)
|
||||
|
||||
/**
|
||||
* Publish data to all subscribers (including subscribers on this tab)
|
||||
*
|
||||
* @function
|
||||
* @param {string} room
|
||||
* @param {ArrayBuffer} data
|
||||
*/
|
||||
export const publish = (room, data) => {
|
||||
const c = getChannel(room)
|
||||
c.bc.postMessage(data)
|
||||
c.subs.forEach(sub => sub(data))
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/* global Buffer */
|
||||
|
||||
import * as globals from './globals.mjs'
|
||||
import * as globals from './globals.js'
|
||||
|
||||
/**
|
||||
* A Decoder handles the decoding of an ArrayBuffer.
|
||||
@@ -148,6 +148,21 @@ export const readVarUint = decoder => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look ahead and read varUint without incrementing position
|
||||
*
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @return {number}
|
||||
*/
|
||||
export const peekVarUint = decoder => {
|
||||
let pos = decoder.pos
|
||||
let s = readVarUint(decoder)
|
||||
decoder.pos = pos
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read string of variable length
|
||||
* * varUint is used to store the length of the string
|
||||
@@ -189,3 +204,4 @@ export const peekVarString = decoder => {
|
||||
decoder.pos = pos
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @module encoding
|
||||
*/
|
||||
import * as globals from './globals.mjs'
|
||||
import * as globals from './globals.js'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as encoding from './encoding.mjs'
|
||||
import * as encoding from './encoding.js'
|
||||
|
||||
/**
|
||||
* Check if binary encoding is compatible with golang binary encoding - binary.PutVarUint.
|
||||
@@ -58,8 +58,6 @@ export const until = (timeout, check) => createPromise((resolve, reject) => {
|
||||
|
||||
export const error = description => new Error(description)
|
||||
|
||||
export const max = (a, b) => a > b ? a : b
|
||||
|
||||
/**
|
||||
* @param {number} t Time to wait
|
||||
* @return {Promise} Promise that is resolved after t ms
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* @module idb
|
||||
* @module lib/idb
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
import * as globals from './globals.mjs'
|
||||
import * as globals from './globals.js'
|
||||
|
||||
/*
|
||||
* IDB Request to Promise transformer
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as test from './testing.mjs'
|
||||
import * as idb from './idb.mjs'
|
||||
import * as logging from './logging.mjs'
|
||||
import * as test from './testing.js'
|
||||
import * as idb from './idb.js'
|
||||
import * as logging from './logging.js'
|
||||
|
||||
const initTestDB = db => idb.createStores(db, [['test']])
|
||||
const testDBName = 'idb-test'
|
||||
@@ -2,7 +2,7 @@
|
||||
* @module logging
|
||||
*/
|
||||
|
||||
import * as globals from './globals.mjs'
|
||||
import * as globals from './globals.js'
|
||||
|
||||
let date = new Date().getTime()
|
||||
|
||||
28
lib/math.js
Normal file
28
lib/math.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @module math
|
||||
*/
|
||||
export const floor = Math.floor
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @return {number} The sum of a and b
|
||||
*/
|
||||
export const add = (a, b) => a + b
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @return {number} The smaller element of a and b
|
||||
*/
|
||||
export const min = (a, b) => a < b ? a : b
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @return {number} The bigger element of a and b
|
||||
*/
|
||||
export const max = (a, b) => a > b ? a : b
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* @module math
|
||||
*/
|
||||
export const floor = Math.floor
|
||||
14
lib/object.js
Normal file
14
lib/object.js
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
export const create = Object.create(null)
|
||||
|
||||
export const keys = Object.keys
|
||||
|
||||
export const equalFlat = (a, b) => {
|
||||
const keys = Object.keys(a)
|
||||
let eq = keys.length === Object.keys(b).length
|
||||
for (let i = 0; i < keys.length && eq; i++) {
|
||||
const key = keys[i]
|
||||
eq = a[key] === b[key]
|
||||
}
|
||||
return eq
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
* @module prng
|
||||
*/
|
||||
|
||||
import { Mt19937 } from './Mt19937.mjs'
|
||||
import { Xoroshiro128plus } from './Xoroshiro128plus.mjs'
|
||||
import { Xorshift32 } from './Xorshift32.mjs'
|
||||
import * as time from '../../time.mjs'
|
||||
import { Mt19937 } from './Mt19937.js'
|
||||
import { Xoroshiro128plus } from './Xoroshiro128plus.js'
|
||||
import { Xorshift32 } from './Xorshift32.js'
|
||||
import * as time from '../../time.js'
|
||||
|
||||
const DIAMETER = 300
|
||||
const NUMBERS = 10000
|
||||
@@ -2,7 +2,7 @@
|
||||
* @module prng
|
||||
*/
|
||||
|
||||
import { Xorshift32 } from './Xorshift32.mjs'
|
||||
import { Xorshift32 } from './Xorshift32.js'
|
||||
|
||||
/**
|
||||
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
|
||||
@@ -2,12 +2,12 @@
|
||||
* @module prng
|
||||
*/
|
||||
|
||||
import * as binary from '../binary.mjs'
|
||||
import { fromCharCode, fromCodePoint } from '../string.mjs'
|
||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.mjs'
|
||||
import * as math from '../math.mjs'
|
||||
import * as binary from '../binary.js'
|
||||
import { fromCharCode, fromCodePoint } from '../string.js'
|
||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
|
||||
import * as math from '../math.js'
|
||||
|
||||
import { Xoroshiro128plus as DefaultPRNG } from './PRNG/Xoroshiro128plus.mjs'
|
||||
import { Xoroshiro128plus as DefaultPRNG } from './PRNG/Xoroshiro128plus.js'
|
||||
|
||||
/**
|
||||
* Description of the function
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* @module prng
|
||||
*/
|
||||
|
||||
import * as binary from '../binary.mjs'
|
||||
import { fromCharCode, fromCodePoint } from '../string.mjs'
|
||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.mjs'
|
||||
import * as math from '../math.mjs'
|
||||
|
||||
import { Xoroshiro128plus as DefaultPRNG } from './PRNG/Xoroshiro128plus.mjs'
|
||||
|
||||
/**
|
||||
* Description of the function
|
||||
* @callback generatorNext
|
||||
* @return {number} A 32bit integer
|
||||
*/
|
||||
|
||||
/**
|
||||
* A random type generator.
|
||||
*
|
||||
* @typedef {Object} PRNG
|
||||
* @property {generatorNext} next Generate new number
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a Xoroshiro128plus Pseudo-Random-Number-Generator.
|
||||
* This is the fastest full-period generator passing BigCrush without systematic failures.
|
||||
* But there are more PRNGs available in ./PRNG/.
|
||||
*
|
||||
* @param {number} seed A positive 32bit integer. Do not use negative numbers.
|
||||
* @return {PRNG}
|
||||
*/
|
||||
export const createPRNG = seed => new DefaultPRNG(Math.floor(seed < 1 ? seed * binary.BITS32 : seed))
|
||||
|
||||
/**
|
||||
* Generates a single random bool.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Boolean} A random boolean
|
||||
*/
|
||||
export const bool = gen => (gen.next() & 2) === 2 // brackets are non-optional!
|
||||
|
||||
/**
|
||||
* Generates a random integer with 53 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
|
||||
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
|
||||
* @return {Number} A random integer on [min, max]
|
||||
*/
|
||||
export const int53 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => math.floor(real53(gen) * (max + 1 - min) + min)
|
||||
|
||||
/**
|
||||
* Generates a random integer with 32 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
|
||||
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
|
||||
* @return {Number} A random integer on [min, max]
|
||||
*/
|
||||
export const int32 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => min + ((gen.next() >>> 0) % (max + 1 - min))
|
||||
|
||||
/**
|
||||
* Generates a random real on [0, 1) with 32 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Number} A random real number on [0, 1).
|
||||
*/
|
||||
export const real32 = gen => (gen.next() >>> 0) / binary.BITS32
|
||||
|
||||
/**
|
||||
* Generates a random real on [0, 1) with 53 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Number} A random real number on [0, 1).
|
||||
*/
|
||||
export const real53 = gen => (((gen.next() >>> 5) * binary.BIT26) + (gen.next() >>> 6)) / MAX_SAFE_INTEGER
|
||||
|
||||
/**
|
||||
* Generates a random character from char code 32 - 126. I.e. Characters, Numbers, special characters, and Space:
|
||||
*
|
||||
* (Space)!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~
|
||||
*/
|
||||
export const char = gen => fromCharCode(int32(gen, 32, 126))
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @return {string} A single letter (a-z)
|
||||
*/
|
||||
export const letter = gen => fromCharCode(int32(gen, 97, 122))
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @return {string} A random word without spaces consisting of letters (a-z)
|
||||
*/
|
||||
export const word = gen => {
|
||||
const len = int32(gen, 0, 20)
|
||||
let str = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += letter(gen)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: this function produces invalid runes. Does not cover all of utf16!!
|
||||
*/
|
||||
export const utf16Rune = gen => {
|
||||
const codepoint = int32(gen, 0, 256)
|
||||
return fromCodePoint(codepoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @param {number} [maxlen = 20]
|
||||
*/
|
||||
export const utf16String = (gen, maxlen = 20) => {
|
||||
const len = int32(gen, 0, maxlen)
|
||||
let str = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += utf16Rune(gen)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one element of a given array.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Array<T>} array Non empty Array of possible values.
|
||||
* @return {T} One of the values of the supplied Array.
|
||||
* @template T
|
||||
*/
|
||||
export const oneOf = (gen, array) => array[int32(gen, 0, array.length - 1)]
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
/**
|
||||
*TODO: enable tests
|
||||
import * as rt from '../rich-text/formatters.mjs''
|
||||
import { test } from '../test/test.mjs''
|
||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs''
|
||||
import Xorshift32 from './PRNG/Xorshift32.mjs''
|
||||
import MT19937 from './PRNG/Mt19937.mjs''
|
||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs''
|
||||
import { MAX_SAFE_INTEGER } from '../number/constants.mjs''
|
||||
import { BIT32 } from '../binary/constants.mjs''
|
||||
import * as rt from '../rich-text/formatters.js''
|
||||
import { test } from '../test/test.js''
|
||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.js''
|
||||
import Xorshift32 from './PRNG/Xorshift32.js''
|
||||
import MT19937 from './PRNG/Mt19937.js''
|
||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.js''
|
||||
import { MAX_SAFE_INTEGER } from '../number/constants.js''
|
||||
import { BIT32 } from '../binary/constants.js''
|
||||
|
||||
function init (Gen) {
|
||||
return {
|
||||
@@ -2,8 +2,8 @@
|
||||
* @module testing
|
||||
*/
|
||||
|
||||
import * as logging from './logging.mjs'
|
||||
import { simpleDiff } from './diff.mjs'
|
||||
import * as logging from './logging.js'
|
||||
import { simpleDiff } from './diff.js'
|
||||
|
||||
export const run = async (name, f) => {
|
||||
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-73",
|
||||
"version": "13.0.0-75",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -67,12 +67,28 @@
|
||||
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/events": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
|
||||
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "6.0.110",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
|
||||
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz",
|
||||
"integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/events": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"abab": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
||||
@@ -5944,7 +5960,7 @@
|
||||
},
|
||||
"quill": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "http://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
|
||||
"integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
||||
12
package.json
12
package.json
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-73",
|
||||
"version": "13.0.0-75",
|
||||
"description": "A ",
|
||||
"module": "./index.mjs'",
|
||||
"main": "./build/yjs.js",
|
||||
"module": "./index.js'",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"test": "npm run lint",
|
||||
"build": "rm -rf build examples/build && rollup -c",
|
||||
"build": "rm -rf build examples/build && PRODUCTION=1 rollup -c",
|
||||
"watch": "rollup -wc",
|
||||
"debug": "concurrently 'rollup -wc' 'cutest-serve build/y.test.js -o'",
|
||||
"debug": "concurrently 'npm run watch' 'cutest-serve build/y.test.js -o'",
|
||||
"lint": "standard **/*.js",
|
||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
||||
"serve-docs": "npm run docs && serve ./docs/",
|
||||
"postversion": "npm run build",
|
||||
"websocket-server": "node --experimental-modules ./provider/websocket/server.mjs",
|
||||
"websocket-server": "node ./provider/websocket/server.js",
|
||||
"now-start": "npm run websocket-server"
|
||||
},
|
||||
"files": [
|
||||
@@ -65,6 +66,7 @@
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"devDependencies": {
|
||||
"@types/ws": "^6.0.1",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-regenerator": "^6.26.0",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import { createMutex } from '../lib/mutex.mjs'
|
||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.mjs'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
|
||||
|
||||
function createFilePath (persistence, roomName) {
|
||||
// TODO: filename checking!
|
||||
553
persistences/IndexedDBPersistence.js
Normal file
553
persistences/IndexedDBPersistence.js
Normal file
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
import { Y } from '../utils/Y.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
|
||||
|
||||
function rtop (request) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDB (room) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open(room)
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('updates')) {
|
||||
db.deleteObjectStore('updates')
|
||||
}
|
||||
db.createObjectStore('updates', {autoIncrement: true})
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function persist (room) {
|
||||
let t = room.db.transaction(['updates'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll())
|
||||
.then(updates => {
|
||||
// apply all previous updates before deleting them
|
||||
room.mutex(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
})
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
// delete all pending updates
|
||||
rtop(updatesStore.clear()).then(() => {
|
||||
// write current model
|
||||
updatesStore.put(encoder.createBuffer())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveUpdate (room, updateBuffer) {
|
||||
const db = room.db
|
||||
if (db !== null) {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||
rtop(updatesStore.count()).then(cnt => {
|
||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||
persist(room)
|
||||
}
|
||||
})
|
||||
return updatePut
|
||||
}
|
||||
}
|
||||
|
||||
function registerRoomInPersistence (documentsDB, roomName) {
|
||||
return documentsDB.then(
|
||||
db => Promise.all([
|
||||
db,
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
])
|
||||
).then(
|
||||
([db, doc]) => {
|
||||
if (doc === undefined) {
|
||||
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 400
|
||||
|
||||
export class IndexedDBPersistence {
|
||||
constructor () {
|
||||
this._rooms = new Map()
|
||||
this._documentsDB = new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open('_yjs_documents')
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('documents')) {
|
||||
db.deleteObjectStore('documents')
|
||||
}
|
||||
db.createObjectStore('documents', { keyPath: "roomName" })
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
addEventListener('unload', () => {
|
||||
// close everything when page unloads
|
||||
this._rooms.forEach(room => {
|
||||
if (room.db !== null) {
|
||||
room.db.close()
|
||||
} else {
|
||||
room.dbPromise.then(db => db.close())
|
||||
}
|
||||
})
|
||||
this._documentsDB.then(db => db.close())
|
||||
})
|
||||
}
|
||||
getAllDocuments () {
|
||||
return this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
||||
)
|
||||
}
|
||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||
this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
||||
)
|
||||
}
|
||||
|
||||
_createYInstance (roomName) {
|
||||
const room = this._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
return room.y
|
||||
}
|
||||
const y = new Y()
|
||||
return openDB(roomName).then(
|
||||
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
||||
).then(
|
||||
updates =>
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
).then(() => Promise.resolve(y))
|
||||
}
|
||||
|
||||
_persistStructsDS (roomName, structsDS) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||
encoder.writeArrayBuffer(structsDS)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
_persistStructs (roomName, structs) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_UPDATE)
|
||||
encoder.writeArrayBuffer(structs)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
connectY (roomName, y) {
|
||||
if (this._rooms.has(roomName)) {
|
||||
throw new Error('A Y instance is already bound to this room!')
|
||||
}
|
||||
let room = {
|
||||
db: null,
|
||||
dbPromise: null,
|
||||
channel: null,
|
||||
mutex: createMutex(),
|
||||
y
|
||||
}
|
||||
if (typeof BroadcastChannel !== 'undefined') {
|
||||
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
||||
room.channel.addEventListener('message', e => {
|
||||
room.mutex(function () {
|
||||
decodePersisted(y, new BinaryDecoder(e.data))
|
||||
})
|
||||
})
|
||||
}
|
||||
y.on('destroyed', () => {
|
||||
this.disconnectY(roomName, y)
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
room.mutex(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const update = new BinaryEncoder()
|
||||
encodeUpdate(y, transaction.encodedStructs, update)
|
||||
const updateBuffer = update.createBuffer()
|
||||
if (room.channel !== null) {
|
||||
room.channel.postMessage(updateBuffer)
|
||||
}
|
||||
if (transaction.encodedStructsLen > 0
|
||||
import { Y } from '../utils/Y.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
|
||||
|
||||
function rtop (request) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDB (room) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open(room)
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('updates')) {
|
||||
db.deleteObjectStore('updates')
|
||||
}
|
||||
db.createObjectStore('updates', {autoIncrement: true})
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function persist (room) {
|
||||
let t = room.db.transaction(['updates'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll())
|
||||
.then(updates => {
|
||||
// apply all previous updates before deleting them
|
||||
room.mutex(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
})
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
// delete all pending updates
|
||||
rtop(updatesStore.clear()).then(() => {
|
||||
// write current model
|
||||
updatesStore.put(encoder.createBuffer())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveUpdate (room, updateBuffer) {
|
||||
const db = room.db
|
||||
if (db !== null) {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||
rtop(updatesStore.count()).then(cnt => {
|
||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||
persist(room)
|
||||
}
|
||||
})
|
||||
return updatePut
|
||||
}
|
||||
}
|
||||
|
||||
function registerRoomInPersistence (documentsDB, roomName) {
|
||||
return documentsDB.then(
|
||||
db => Promise.all([
|
||||
db,
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
])
|
||||
).then(
|
||||
([db, doc]) => {
|
||||
if (doc === undefined) {
|
||||
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 400
|
||||
|
||||
export class IndexedDBPersistence {
|
||||
constructor () {
|
||||
this._rooms = new Map()
|
||||
this._documentsDB = new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open('_yjs_documents')
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('documents')) {
|
||||
db.deleteObjectStore('documents')
|
||||
}
|
||||
db.createObjectStore('documents', { keyPath: "roomName" })
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
addEventListener('unload', () => {
|
||||
// close everything when page unloads
|
||||
this._rooms.forEach(room => {
|
||||
if (room.db !== null) {
|
||||
room.db.close()
|
||||
} else {
|
||||
room.dbPromise.then(db => db.close())
|
||||
}
|
||||
})
|
||||
this._documentsDB.then(db => db.close())
|
||||
})
|
||||
}
|
||||
getAllDocuments () {
|
||||
return this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
||||
)
|
||||
}
|
||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||
this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
||||
)
|
||||
}
|
||||
|
||||
_createYInstance (roomName) {
|
||||
const room = this._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
return room.y
|
||||
}
|
||||
const y = new Y()
|
||||
return openDB(roomName).then(
|
||||
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
||||
).then(
|
||||
updates =>
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
).then(() => Promise.resolve(y))
|
||||
}
|
||||
|
||||
_persistStructsDS (roomName, structsDS) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||
encoder.writeArrayBuffer(structsDS)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
_persistStructs (roomName, structs) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_UPDATE)
|
||||
encoder.writeArrayBuffer(structs)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
connectY (roomName, y) {
|
||||
if (this._rooms.has(roomName)) {
|
||||
throw new Error('A Y instance is already bound to this room!')
|
||||
}
|
||||
let room = {
|
||||
db: null,
|
||||
dbPromise: null,
|
||||
channel: null,
|
||||
mutex: createMutex(),
|
||||
y
|
||||
}
|
||||
if (typeof BroadcastChannel !== 'undefined') {
|
||||
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
||||
room.channel.addEventListener('message', e => {
|
||||
room.mutex(function () {
|
||||
decodePersisted(y, new BinaryDecoder(e.data))
|
||||
})
|
||||
})
|
||||
}
|
||||
y.on('destroyed', () => {
|
||||
this.disconnectY(roomName, y)
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
room.mutex(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const update = new BinaryEncoder()
|
||||
encodeUpdate(y, transaction.encodedStructs, update)
|
||||
const updateBuffer = update.createBuffer()
|
||||
if (room.channel !== null) {
|
||||
room.channel.postMessage(updateBuffer)
|
||||
}
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
if (room.db !== null) {
|
||||
saveUpdate(room, updateBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// register document in documentsDB
|
||||
this._documentsDB.then(
|
||||
db =>
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
.then(
|
||||
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
||||
)
|
||||
)
|
||||
// open room db and read existing data
|
||||
return room.dbPromise = openDB(roomName)
|
||||
.then(db => {
|
||||
room.db = db
|
||||
const t = room.db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
// write current state as update
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
||||
// read persisted state
|
||||
return rtop(updatesStore.getAll()).then(updates => {
|
||||
room.mutex(() => {
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
disconnectY (roomName) {
|
||||
const {
|
||||
db, channel
|
||||
} = this._rooms.get(roomName)
|
||||
db.close()
|
||||
if (channel !== null) {
|
||||
channel.close()
|
||||
}
|
||||
this._rooms.delete(roomName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*
|
||||
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||
this.disconnectY(roomName)
|
||||
return rtop(indexedDB.deleteDatabase(roomName))
|
||||
}
|
||||
}
|
||||
{
|
||||
if (room.db !== null) {
|
||||
saveUpdate(room, updateBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// register document in documentsDB
|
||||
this._documentsDB.then(
|
||||
db =>
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
.then(
|
||||
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
||||
)
|
||||
)
|
||||
// open room db and read existing data
|
||||
return room.dbPromise = openDB(roomName)
|
||||
.then(db => {
|
||||
room.db = db
|
||||
const t = room.db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
// write current state as update
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
||||
// read persisted state
|
||||
return rtop(updatesStore.getAll()).then(updates => {
|
||||
room.mutex(() => {
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
disconnectY (roomName) {
|
||||
const {
|
||||
db, channel
|
||||
} = this._rooms.get(roomName)
|
||||
db.close()
|
||||
if (channel !== null) {
|
||||
channel.close()
|
||||
}
|
||||
this._rooms.delete(roomName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*
|
||||
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||
this.disconnectY(roomName)
|
||||
return rtop(indexedDB.deleteDatabase(roomName))
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1,288 +0,0 @@
|
||||
|
||||
/*
|
||||
import { Y } from '../utils/Y.mjs'
|
||||
import { createMutex } from '../lib/mutex.mjs'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.mjs'
|
||||
|
||||
function rtop (request) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDB (room) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open(room)
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('updates')) {
|
||||
db.deleteObjectStore('updates')
|
||||
}
|
||||
db.createObjectStore('updates', {autoIncrement: true})
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function persist (room) {
|
||||
let t = room.db.transaction(['updates'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll())
|
||||
.then(updates => {
|
||||
// apply all previous updates before deleting them
|
||||
room.mutex(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
})
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
// delete all pending updates
|
||||
rtop(updatesStore.clear()).then(() => {
|
||||
// write current model
|
||||
updatesStore.put(encoder.createBuffer())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveUpdate (room, updateBuffer) {
|
||||
const db = room.db
|
||||
if (db !== null) {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||
rtop(updatesStore.count()).then(cnt => {
|
||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||
persist(room)
|
||||
}
|
||||
})
|
||||
return updatePut
|
||||
}
|
||||
}
|
||||
|
||||
function registerRoomInPersistence (documentsDB, roomName) {
|
||||
return documentsDB.then(
|
||||
db => Promise.all([
|
||||
db,
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
])
|
||||
).then(
|
||||
([db, doc]) => {
|
||||
if (doc === undefined) {
|
||||
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 400
|
||||
|
||||
export class IndexedDBPersistence {
|
||||
constructor () {
|
||||
this._rooms = new Map()
|
||||
this._documentsDB = new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open('_yjs_documents')
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('documents')) {
|
||||
db.deleteObjectStore('documents')
|
||||
}
|
||||
db.createObjectStore('documents', { keyPath: "roomName" })
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()EventListener' is not defined.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:160:36: 'BinaryDecoder' is not defined.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:167:25: 'BinaryEncoder' is not defined.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:178:25: 'BinaryEncoder' is not defined.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:203:34: 'BinaryDecoder' is not defined.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:213:17: 'encoder' is assigned a value but never used.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:213:31: 'BinaryEncoder' is not defined.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:214:30: 'BinaryEncoder' is not defined.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:230:12: Trailing spaces not allowed.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:237:5: Return statement should not contain assignment.
|
||||
/home/dmonad/go/src/github.com/y-js/yjs/persistences/IndexedDBPersistence.js:243:29: 'BinaryEncoder' i
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
addEventListener('unload', () => {
|
||||
// close everything when page unloads
|
||||
this._rooms.forEach(room => {
|
||||
if (room.db !== null) {
|
||||
room.db.close()
|
||||
} else {
|
||||
room.dbPromise.then(db => db.close())
|
||||
}
|
||||
})
|
||||
this._documentsDB.then(db => db.close())
|
||||
})
|
||||
}
|
||||
getAllDocuments () {
|
||||
return this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
||||
)
|
||||
}
|
||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||
this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
||||
)
|
||||
}
|
||||
|
||||
_createYInstance (roomName) {
|
||||
const room = this._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
return room.y
|
||||
}
|
||||
const y = new Y()
|
||||
return openDB(roomName).then(
|
||||
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
||||
).then(
|
||||
updates =>
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
).then(() => Promise.resolve(y))
|
||||
}
|
||||
|
||||
_persistStructsDS (roomName, structsDS) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||
encoder.writeArrayBuffer(structsDS)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
_persistStructs (roomName, structs) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_UPDATE)
|
||||
encoder.writeArrayBuffer(structs)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
connectY (roomName, y) {
|
||||
if (this._rooms.has(roomName)) {
|
||||
throw new Error('A Y instance is already bound to this room!')
|
||||
}
|
||||
let room = {
|
||||
db: null,
|
||||
dbPromise: null,
|
||||
channel: null,
|
||||
mutex: createMutex(),
|
||||
y
|
||||
}
|
||||
if (typeof BroadcastChannel !== 'undefined') {
|
||||
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
||||
room.channel.addEventListener('message', e => {
|
||||
room.mutex(function () {
|
||||
decodePersisted(y, new BinaryDecoder(e.data))
|
||||
})
|
||||
})
|
||||
}
|
||||
y.on('destroyed', () => {
|
||||
this.disconnectY(roomName, y)
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
room.mutex(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const update = new BinaryEncoder()
|
||||
encodeUpdate(y, transaction.encodedStructs, update)
|
||||
const updateBuffer = update.createBuffer()
|
||||
if (room.channel !== null) {
|
||||
room.channel.postMessage(updateBuffer)
|
||||
}
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
if (room.db !== null) {
|
||||
saveUpdate(room, updateBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// register document in documentsDB
|
||||
this._documentsDB.then(
|
||||
db =>
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
.then(
|
||||
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
||||
)
|
||||
)
|
||||
// open room db and read existing data
|
||||
return room.dbPromise = openDB(roomName)
|
||||
.then(db => {
|
||||
room.db = db
|
||||
const t = room.db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
// write current state as update
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
||||
// read persisted state
|
||||
return rtop(updatesStore.getAll()).then(updates => {
|
||||
room.mutex(() => {
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
disconnectY (roomName) {
|
||||
const {
|
||||
db, channel
|
||||
} = this._rooms.get(roomName)
|
||||
db.close()
|
||||
if (channel !== null) {
|
||||
channel.close()
|
||||
}
|
||||
this._rooms.delete(roomName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*
|
||||
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||
this.disconnectY(roomName)
|
||||
return rtop(indexedDB.deleteDatabase(roomName))
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.mjs'
|
||||
import { writeStructs } from '../MessageHandler/syncStep1.mjs'
|
||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.mjs'
|
||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
|
||||
import { writeStructs } from '../MessageHandler/syncStep1.js'
|
||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
|
||||
|
||||
export const PERSIST_UPDATE = 0
|
||||
/**
|
||||
5
persistences/indexeddb.js
Normal file
5
persistences/indexeddb.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as idb from '../lib/idb.js'
|
||||
|
||||
const bc = new BroadcastChannel('ydb-client')
|
||||
|
||||
idb.openDB()
|
||||
33
protocols/auth.js
Normal file
33
protocols/auth.js
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js';
|
||||
|
||||
export const messagePermissionDenied = 0
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {string} reason
|
||||
*/
|
||||
export const writePermissionDenied = (encoder, reason) => {
|
||||
encoding.writeVarUint(encoder, messagePermissionDenied)
|
||||
encoding.writeVarString(encoder, reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback PermissionDeniedHandler
|
||||
* @param {any} y
|
||||
* @param {string} reason
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @param {PermissionDeniedHandler} permissionDeniedHandler
|
||||
*/
|
||||
export const readAuthMessage = (decoder, y, permissionDeniedHandler) => {
|
||||
switch (decoding.readVarUint(decoder)) {
|
||||
case messagePermissionDenied: permissionDeniedHandler(y, decoding.readVarString(decoder))
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@
|
||||
* @module awareness-protocol
|
||||
*/
|
||||
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
const messageUsersStateChanged = 0
|
||||
|
||||
/**
|
||||
* @typedef {Object} UserStateUpdate
|
||||
* @property {number} UserStateUpdate.userID
|
||||
* @property {Object} state
|
||||
* @property {Object} UserStateUpdate.state
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -91,13 +91,22 @@ export const readAwarenessMessage = (decoder, y) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} UserState
|
||||
* @property {number} UserState.userID
|
||||
* @property {any} UserState.state
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @return {Array<UserState>} Array of state updates
|
||||
*/
|
||||
export const forwardAwarenessMessage = (decoder, encoder) => {
|
||||
let s = []
|
||||
switch (decoding.readVarUint(decoder)) {
|
||||
case messageUsersStateChanged:
|
||||
return forwardUsersStateChange(decoder, encoder)
|
||||
s = forwardUsersStateChange(decoder, encoder)
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
* @module sync-protocol
|
||||
*/
|
||||
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import * as ID from '../utils/ID.mjs'
|
||||
import { getStruct } from '../utils/structReferences.mjs'
|
||||
import { deleteItemRange } from '../utils/structManipulation.mjs'
|
||||
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.mjs'
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import { Item } from '../structs/Item.mjs'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { getStruct } from '../utils/structReferences.js'
|
||||
import { deleteItemRange } from '../utils/structManipulation.js'
|
||||
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
import { Item } from '../structs/Item.js'
|
||||
import * as stringify from '../utils/structStringify.js'
|
||||
|
||||
/**
|
||||
* @typedef {Map<number, number>} StateSet
|
||||
@@ -40,9 +41,9 @@ import { Item } from '../structs/Item.mjs'
|
||||
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
||||
*/
|
||||
|
||||
const messageYjsSyncStep1 = 0
|
||||
const messageYjsSyncStep2 = 1
|
||||
const messageYjsUpdate = 2
|
||||
export const messageYjsSyncStep1 = 0
|
||||
export const messageYjsSyncStep2 = 1
|
||||
export const messageYjsUpdate = 2
|
||||
|
||||
/**
|
||||
* Stringifies a message-encoded Delete Set.
|
||||
@@ -83,7 +84,7 @@ export const writeDeleteSet = (encoder, y) => {
|
||||
const gc = n.gc
|
||||
if (currentUser !== user) {
|
||||
numberOfUsers++
|
||||
// a new user was foundimport { StateSet } from '../Store/StateStore.mjs' // eslint-disable-line
|
||||
// a new user was foundimport { StateSet } from '../Store/StateStore.js' // eslint-disable-line
|
||||
|
||||
if (currentUser !== null) { // happens on first iteration
|
||||
encoding.setUint32(encoder, lastLenPos, currentLength)
|
||||
@@ -234,50 +235,6 @@ export const readStateSet = decoder => {
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify an item id.
|
||||
*
|
||||
* @param {ID.ID | ID.RootID} id
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifyID = id => id instanceof ID.ID ? `(${id.user},${id.clock})` : `(${id.name},${id.type})`
|
||||
|
||||
/**
|
||||
* Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent).
|
||||
*
|
||||
* @param {Item | Y | null} item
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifyItemID = item => {
|
||||
let result
|
||||
if (item === null) {
|
||||
result = '()'
|
||||
} else if (item instanceof Item) {
|
||||
result = stringifyID(item._id)
|
||||
} else {
|
||||
// must be a Yjs instance
|
||||
// Don't include Y in this module, so we prevent circular dependencies.
|
||||
result = 'y'
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility to convert an item to a readable format.
|
||||
*
|
||||
* @param {String} name The name of the item class (YText, ItemString, ..).
|
||||
* @param {Item} item The item instance.
|
||||
* @param {String} [append] Additional information to append to the returned
|
||||
* string.
|
||||
* @return {String} A readable string that represents the item object.
|
||||
*
|
||||
*/
|
||||
export const logItemHelper = (name, item, append) => {
|
||||
const left = item._left !== null ? stringifyID(item._left._lastId) : '()'
|
||||
const origin = item._origin !== null ? stringifyID(item._origin._lastId) : '()'
|
||||
return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item._right)},parent:${stringifyItemID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
@@ -293,7 +250,7 @@ export const stringifyStructs = (decoder, y) => {
|
||||
let missing = struct._fromBinary(y, decoder)
|
||||
let logMessage = ' ' + struct._logString()
|
||||
if (missing.length > 0) {
|
||||
logMessage += ' .. missing: ' + missing.map(stringifyItemID).join(', ')
|
||||
logMessage += ' .. missing: ' + missing.map(stringify.stringifyItemID).join(', ')
|
||||
}
|
||||
str += logMessage + '\n'
|
||||
}
|
||||
@@ -410,10 +367,9 @@ export const stringifySyncStep2 = (decoder, y) => {
|
||||
* Read and apply Structs and then DeleteSet to a y instance.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Y} y
|
||||
*/
|
||||
export const readSyncStep2 = (decoder, encoder, y) => {
|
||||
export const readSyncStep2 = (decoder, y) => {
|
||||
readStructs(decoder, y)
|
||||
readDeleteSet(decoder, y)
|
||||
}
|
||||
@@ -480,7 +436,7 @@ export const readSyncMessage = (decoder, encoder, y) => {
|
||||
readSyncStep1(decoder, encoder, y)
|
||||
break
|
||||
case messageYjsSyncStep2:
|
||||
y.transact(() => readSyncStep2(decoder, encoder, y), true)
|
||||
y.transact(() => readSyncStep2(decoder, y), true)
|
||||
break
|
||||
case messageYjsUpdate:
|
||||
y.transact(() => readUpdate(decoder, y), true)
|
||||
5
provider/websocket.js
Normal file
5
provider/websocket.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @module provider/websocket
|
||||
*/
|
||||
|
||||
export * from './websocket/WebSocketProvider.js'
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* @module provider/websocket
|
||||
*/
|
||||
|
||||
export * from './websocket/WebSocketProvider.mjs'
|
||||
166
provider/websocket/WebSocketProvider.js
Normal file
166
provider/websocket/WebSocketProvider.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @module provider/websocket
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
import * as Y from '../../index.js'
|
||||
import * as bc from '../../lib/broadcastchannel.js'
|
||||
|
||||
const messageSync = 0
|
||||
const messageAwareness = 1
|
||||
const messageAuth = 2
|
||||
|
||||
const reconnectTimeout = 3000
|
||||
|
||||
/**
|
||||
* @param {WebsocketsSharedDocument} doc
|
||||
* @param {string} reason
|
||||
*/
|
||||
const permissionDeniedHandler = (doc, reason) => console.warn(`Permission denied to access ${doc.url}.\n${reason}`)
|
||||
|
||||
/**
|
||||
* @param {WebsocketsSharedDocument} doc
|
||||
* @param {ArrayBuffer} buf
|
||||
* @return {Y.encoding.Encoder}
|
||||
*/
|
||||
const readMessage = (doc, buf) => {
|
||||
const decoder = Y.decoding.createDecoder(buf)
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
const messageType = Y.decoding.readVarUint(decoder)
|
||||
switch (messageType) {
|
||||
case messageSync:
|
||||
Y.encoding.writeVarUint(encoder, messageSync)
|
||||
doc.mux(() =>
|
||||
Y.syncProtocol.readSyncMessage(decoder, encoder, doc)
|
||||
)
|
||||
break
|
||||
case messageAwareness:
|
||||
Y.awarenessProtocol.readAwarenessMessage(decoder, doc)
|
||||
break
|
||||
case messageAuth:
|
||||
Y.authProtocol.readAuthMessage(decoder, doc, permissionDeniedHandler)
|
||||
}
|
||||
return encoder
|
||||
}
|
||||
|
||||
const setupWS = (doc, url) => {
|
||||
const websocket = new WebSocket(url)
|
||||
websocket.binaryType = 'arraybuffer'
|
||||
doc.ws = websocket
|
||||
websocket.onmessage = event => {
|
||||
const encoder = readMessage(doc, event.data)
|
||||
if (Y.encoding.length(encoder) > 1) {
|
||||
websocket.send(Y.encoding.toBuffer(encoder))
|
||||
}
|
||||
}
|
||||
websocket.onclose = () => {
|
||||
doc.ws = null
|
||||
doc.wsconnected = false
|
||||
doc.emit('status', {
|
||||
status: 'connected'
|
||||
})
|
||||
setTimeout(setupWS, reconnectTimeout, doc, url)
|
||||
}
|
||||
websocket.onopen = () => {
|
||||
doc.wsconnected = true
|
||||
doc.emit('status', {
|
||||
status: 'disconnected'
|
||||
})
|
||||
// always send sync step 1 when connected
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
Y.encoding.writeVarUint(encoder, messageSync)
|
||||
Y.syncProtocol.writeSyncStep1(encoder, doc)
|
||||
websocket.send(Y.encoding.toBuffer(encoder))
|
||||
// force send stored awareness info
|
||||
doc.setAwarenessField(null, null)
|
||||
}
|
||||
}
|
||||
|
||||
const broadcastUpdate = (y, transaction) => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
y.mux(() => {
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
Y.encoding.writeVarUint(encoder, messageSync)
|
||||
Y.syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||
const buf = Y.encoding.toBuffer(encoder)
|
||||
if (y.wsconnected) {
|
||||
y.ws.send(buf)
|
||||
}
|
||||
bc.publish(y.url, buf)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class WebsocketsSharedDocument extends Y.Y {
|
||||
constructor (url) {
|
||||
super()
|
||||
this.url = url
|
||||
this.wsconnected = false
|
||||
this.mux = Y.createMutex()
|
||||
this.ws = null
|
||||
this._localAwarenessState = {}
|
||||
this.awareness = new Map()
|
||||
setupWS(this, url)
|
||||
this.on('afterTransaction', broadcastUpdate)
|
||||
this._bcSubscriber = data => {
|
||||
this.mux(() => {
|
||||
const encoder = readMessage(this, data)
|
||||
if (Y.encoding.length(encoder) > 1) {
|
||||
bc.publish(url, Y.encoding.toBuffer(encoder))
|
||||
}
|
||||
})
|
||||
}
|
||||
bc.subscribe(url, this._bcSubscriber)
|
||||
// send sync step1 to bc
|
||||
this.mux(() => {
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
Y.encoding.writeVarUint(encoder, messageSync)
|
||||
Y.syncProtocol.writeSyncStep1(encoder, this)
|
||||
bc.publish(url, Y.encoding.toBuffer(encoder))
|
||||
})
|
||||
}
|
||||
getLocalAwarenessInfo () {
|
||||
return this._localAwarenessState
|
||||
}
|
||||
getAwarenessInfo () {
|
||||
return this.awareness
|
||||
}
|
||||
setAwarenessField (field, value) {
|
||||
if (field !== null) {
|
||||
this._localAwarenessState[field] = value
|
||||
}
|
||||
if (this.wsconnected) {
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
Y.encoding.writeVarUint(encoder, messageAwareness)
|
||||
Y.awarenessProtocol.writeUsersStateChange(encoder, [{ userID: this.userID, state: this._localAwarenessState }])
|
||||
const buf = Y.encoding.toBuffer(encoder)
|
||||
this.ws.send(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WebsocketProvider {
|
||||
constructor (url) {
|
||||
// ensure that url is always ends with /
|
||||
while (url[url.length - 1] === '/') {
|
||||
url = url.slice(0, url.length - 1)
|
||||
}
|
||||
this.url = url + '/'
|
||||
/**
|
||||
* @type {Map<string, WebsocketsSharedDocument>}
|
||||
*/
|
||||
this.docs = new Map()
|
||||
}
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {WebsocketsSharedDocument}
|
||||
*/
|
||||
get (name) {
|
||||
let doc = this.docs.get(name)
|
||||
if (doc === undefined) {
|
||||
doc = new WebsocketsSharedDocument(this.url + name)
|
||||
}
|
||||
return doc
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/**
|
||||
* @module provider/websocket
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
import * as Y from '../../index.mjs'
|
||||
export * from '../../index.mjs'
|
||||
|
||||
const messageSync = 0
|
||||
const messageAwareness = 1
|
||||
|
||||
const reconnectTimeout = 100
|
||||
|
||||
const setupWS = (doc, url) => {
|
||||
const websocket = new WebSocket(url)
|
||||
websocket.binaryType = 'arraybuffer'
|
||||
doc.ws = websocket
|
||||
websocket.onmessage = event => {
|
||||
const decoder = Y.createDecoder(event.data)
|
||||
const encoder = Y.createEncoder()
|
||||
const messageType = Y.readVarUint(decoder)
|
||||
switch (messageType) {
|
||||
case messageSync:
|
||||
Y.writeVarUint(encoder, messageSync)
|
||||
doc.mux(() =>
|
||||
Y.readSyncMessage(decoder, encoder, doc)
|
||||
)
|
||||
break
|
||||
case messageAwareness:
|
||||
Y.readAwarenessMessage(decoder, doc)
|
||||
break
|
||||
}
|
||||
if (Y.length(encoder) > 1) {
|
||||
websocket.send(Y.toBuffer(encoder))
|
||||
}
|
||||
}
|
||||
websocket.onclose = () => {
|
||||
doc.ws = null
|
||||
doc.wsconnected = false
|
||||
doc.emit('status', {
|
||||
status: 'connected'
|
||||
})
|
||||
setTimeout(setupWS, reconnectTimeout, doc, url)
|
||||
}
|
||||
websocket.onopen = () => {
|
||||
doc.wsconnected = true
|
||||
doc.emit('status', {
|
||||
status: 'disconnected'
|
||||
})
|
||||
// always send sync step 1 when connected
|
||||
const encoder = Y.createEncoder()
|
||||
Y.writeVarUint(encoder, messageSync)
|
||||
Y.writeSyncStep1(encoder, doc)
|
||||
websocket.send(Y.toBuffer(encoder))
|
||||
// force send stored awareness info
|
||||
doc.setAwarenessField(null, null)
|
||||
}
|
||||
}
|
||||
|
||||
const broadcastUpdate = (y, transaction) => {
|
||||
if (y.wsconnected && transaction.encodedStructsLen > 0) {
|
||||
y.mux(() => {
|
||||
const encoder = Y.createEncoder()
|
||||
Y.writeVarUint(encoder, messageSync)
|
||||
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||
y.ws.send(Y.toBuffer(encoder))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class WebsocketsSharedDocument extends Y.Y {
|
||||
constructor (url) {
|
||||
super()
|
||||
this.wsconnected = false
|
||||
this.mux = Y.createMutex()
|
||||
this.ws = null
|
||||
this._localAwarenessState = {}
|
||||
this.awareness = new Map()
|
||||
setupWS(this, url)
|
||||
this.on('afterTransaction', broadcastUpdate)
|
||||
}
|
||||
getLocalAwarenessInfo () {
|
||||
return this._localAwarenessState
|
||||
}
|
||||
getAwarenessInfo () {
|
||||
return this.awareness
|
||||
}
|
||||
setAwarenessField (field, value) {
|
||||
if (field !== null) {
|
||||
this._localAwarenessState[field] = value
|
||||
}
|
||||
if (this.wsconnected) {
|
||||
const encoder = Y.createEncoder()
|
||||
Y.writeVarUint(encoder, messageAwareness)
|
||||
Y.writeUsersStateChange(encoder, [{ userID: this.userID, state: this._localAwarenessState }])
|
||||
this.ws.send(Y.toBuffer(encoder))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WebsocketProvider {
|
||||
constructor (url) {
|
||||
// ensure that url is always ends with /
|
||||
while (url[url.length - 1] === '/') {
|
||||
url = url.slice(0, url.length - 1)
|
||||
}
|
||||
this.url = url + '/'
|
||||
/**
|
||||
* @type {Map<string, WebsocketsSharedDocument>}
|
||||
*/
|
||||
this.docs = new Map()
|
||||
}
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {WebsocketsSharedDocument}
|
||||
*/
|
||||
get (name) {
|
||||
let doc = this.docs.get(name)
|
||||
if (doc === undefined) {
|
||||
doc = new WebsocketsSharedDocument(this.url + name)
|
||||
}
|
||||
return doc
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* @module provider/websocket
|
||||
* @module provider/websocket/server
|
||||
*/
|
||||
|
||||
import * as Y from '../../index.mjs'
|
||||
import WebSocket from 'ws'
|
||||
import http from 'http'
|
||||
const Y = require('../../build/yjs.js')
|
||||
const WebSocket = require('ws')
|
||||
const http = require('http')
|
||||
|
||||
const port = process.env.PORT || 1234
|
||||
|
||||
@@ -19,20 +19,21 @@ const docs = new Map()
|
||||
|
||||
const messageSync = 0
|
||||
const messageAwareness = 1
|
||||
const messageAuth = 2
|
||||
|
||||
const afterTransaction = (doc, transaction) => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = Y.createEncoder()
|
||||
Y.writeVarUint(encoder, messageSync)
|
||||
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||
const message = Y.toBuffer(encoder)
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
Y.encoding.writeVarUint(encoder, messageSync)
|
||||
Y.syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||
const message = Y.encoding.toBuffer(encoder)
|
||||
doc.conns.forEach((_, conn) => conn.send(message))
|
||||
}
|
||||
}
|
||||
|
||||
class WSSharedDoc extends Y.Y {
|
||||
constructor () {
|
||||
super()
|
||||
super({ gc: true })
|
||||
this.mux = Y.createMutex()
|
||||
/**
|
||||
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
||||
@@ -45,25 +46,25 @@ class WSSharedDoc extends Y.Y {
|
||||
}
|
||||
|
||||
const messageListener = (conn, doc, message) => {
|
||||
const encoder = Y.createEncoder()
|
||||
const decoder = Y.createDecoder(message)
|
||||
const messageType = Y.readVarUint(decoder)
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
const decoder = Y.decoding.createDecoder(message)
|
||||
const messageType = Y.decoding.readVarUint(decoder)
|
||||
switch (messageType) {
|
||||
case messageSync:
|
||||
Y.writeVarUint(encoder, messageSync)
|
||||
Y.readSyncMessage(decoder, encoder, doc)
|
||||
if (Y.length(encoder) > 1) {
|
||||
conn.send(Y.toBuffer(encoder))
|
||||
Y.encoding.writeVarUint(encoder, messageSync)
|
||||
Y.syncProtocol.readSyncMessage(decoder, encoder, doc)
|
||||
if (Y.encoding.length(encoder) > 1) {
|
||||
conn.send(Y.encoding.toBuffer(encoder))
|
||||
}
|
||||
break
|
||||
case messageAwareness: {
|
||||
Y.writeVarUint(encoder, messageAwareness)
|
||||
const updates = Y.forwardAwarenessMessage(decoder, encoder)
|
||||
Y.encoding.writeVarUint(encoder, messageAwareness)
|
||||
const updates = Y.awarenessProtocol.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)
|
||||
const buff = Y.encoding.toBuffer(encoder)
|
||||
doc.conns.forEach((_, c) => {
|
||||
c.send(buff)
|
||||
})
|
||||
@@ -86,29 +87,29 @@ const setupConnection = (conn, req) => {
|
||||
conn.on('close', () => {
|
||||
const controlledIds = doc.conns.get(conn)
|
||||
doc.conns.delete(conn)
|
||||
const encoder = Y.createEncoder()
|
||||
Y.writeVarUint(encoder, messageAwareness)
|
||||
Y.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => {
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
Y.encoding.writeVarUint(encoder, messageAwareness)
|
||||
Y.awarenessProtocol.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => {
|
||||
doc.awareness.delete(userID)
|
||||
return { userID, state: null }
|
||||
}))
|
||||
const buf = Y.toBuffer(encoder)
|
||||
const buf = Y.encoding.toBuffer(encoder)
|
||||
doc.conns.forEach((_, conn) => conn.send(buf))
|
||||
})
|
||||
// send sync step 1
|
||||
const encoder = Y.createEncoder()
|
||||
Y.writeVarUint(encoder, messageSync)
|
||||
Y.writeSyncStep1(encoder, doc)
|
||||
conn.send(Y.toBuffer(encoder))
|
||||
const encoder = Y.encoding.createEncoder()
|
||||
Y.encoding.writeVarUint(encoder, messageSync)
|
||||
Y.syncProtocol.writeSyncStep1(encoder, doc)
|
||||
conn.send(Y.encoding.toBuffer(encoder))
|
||||
if (doc.awareness.size > 0) {
|
||||
const encoder = Y.createEncoder()
|
||||
const encoder = Y.encoding.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))
|
||||
Y.encoding.writeVarUint(encoder, messageAwareness)
|
||||
Y.awarenessProtocol.writeUsersStateChange(encoder, userStates)
|
||||
conn.send(Y.encoding.toBuffer(encoder))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @module provider/ydb
|
||||
*/
|
||||
|
||||
import * as globals from './globals.mjs'
|
||||
import * as globals from './globals.js'
|
||||
|
||||
export const Class = class NamedEventHandler {
|
||||
constructor () {
|
||||
@@ -3,19 +3,19 @@
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
import * as idbactions from './idbactions.mjs'
|
||||
import * as globals from '../../lib/globals.mjs'
|
||||
import * as message from './message.mjs'
|
||||
import * as bc from './broadcastchannel.mjs'
|
||||
import * as encoding from '../../lib/encoding.mjs'
|
||||
import * as logging from '../../lib/logging.mjs'
|
||||
import * as idb from '../../lib/idb.mjs'
|
||||
import * as decoding from '../../lib/decoding.mjs'
|
||||
import { Y } from '../../utils/Y.mjs'
|
||||
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.mjs'
|
||||
import { createMutualExclude } from '../../lib/mutualExclude.mjs'
|
||||
import * as idbactions from './idbactions.js'
|
||||
import * as globals from '../../lib/globals.js'
|
||||
import * as message from './message.js'
|
||||
import * as bc from './broadcastchannel.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as logging from '../../lib/logging.js'
|
||||
import * as idb from '../../lib/idb.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import { Y } from '../../utils/Y.js'
|
||||
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.js'
|
||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
||||
|
||||
import * as NamedEventHandler from './NamedEventHandler.mjs'
|
||||
import * as NamedEventHandler from './NamedEventHandler.js'
|
||||
|
||||
/**
|
||||
* @typedef RoomState
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
import * as test from './test.mjs'
|
||||
import * as ydbClient from './YdbClient.mjs'
|
||||
import * as globals from './globals.mjs'
|
||||
import * as idbactions from './idbactions.mjs'
|
||||
import * as logging from './logging.mjs'
|
||||
import * as test from './test.js'
|
||||
import * as ydbClient from './YdbClient.js'
|
||||
import * as globals from './globals.js'
|
||||
import * as idbactions from './idbactions.js'
|
||||
import * as logging from './logging.js'
|
||||
|
||||
const wsUrl = 'ws://127.0.0.1:8899/ws'
|
||||
const testRoom = 'testroom'
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
import * as decoding from '../../lib/decoding.mjs'
|
||||
import * as encoding from '../../lib/encoding.mjs'
|
||||
import * as globals from '../../lib/globals.mjs'
|
||||
import * as NamedEventHandler from './NamedEventHandler.mjs'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as globals from '../../lib/globals.js'
|
||||
import * as NamedEventHandler from './NamedEventHandler.js'
|
||||
|
||||
const bc = new BroadcastChannel('ydb-client')
|
||||
|
||||
/**
|
||||
* @type {Map<string, Set<Function>>}
|
||||
*/
|
||||
@@ -33,11 +33,11 @@
|
||||
* - A client may update a room when the room is in either US or Co
|
||||
*/
|
||||
|
||||
import * as encoding from '../../lib/encoding.mjs'
|
||||
import * as decoding from '../../lib/decoding.mjs'
|
||||
import * as idb from '../../lib/idb.mjs'
|
||||
import * as globals from '../../lib/globals.mjs'
|
||||
import * as message from './message.mjs'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as idb from '../../lib/idb.js'
|
||||
import * as globals from '../../lib/globals.js'
|
||||
import * as message from './message.js'
|
||||
|
||||
/**
|
||||
* Get 'client-unconfirmed' store from transaction
|
||||
@@ -262,7 +262,7 @@ export const getRoomMetas = t => {
|
||||
result.push({
|
||||
room: metakey.slice(5),
|
||||
rsid,
|
||||
offset: keys.reduce((cur, key) => globals.max(decodeHUKey(key).offset, cur), offset)
|
||||
offset: keys.reduce((cur, key) => math.max(decodeHUKey(key).offset, cur), offset)
|
||||
})
|
||||
})
|
||||
).then(() => globals.presolve(result))
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as globals from '../../lib/globals.mjs'
|
||||
import * as idbactions from './idbactions.mjs'
|
||||
import * as test from '../../lib/testing.mjs'
|
||||
import * as globals from '../../lib/globals.js'
|
||||
import * as idbactions from './idbactions.js'
|
||||
import * as test from '../../lib/testing.js'
|
||||
|
||||
idbactions.deleteDB().then(() => idbactions.openDB()).then(db => {
|
||||
test.run('update lifetime 1', async (testname) => {
|
||||
@@ -2,7 +2,7 @@
|
||||
* @module provider/ydb
|
||||
*/
|
||||
|
||||
import * as ydbclient from './YdbClient.mjs'
|
||||
import * as ydbclient from './YdbClient.js'
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
@@ -2,11 +2,11 @@
|
||||
* @module provider/ydb
|
||||
*/
|
||||
|
||||
import * as encoding from './encoding.mjs'
|
||||
import * as decoding from './decoding.mjs'
|
||||
import * as idbactions from './idbactions.mjs'
|
||||
import * as logging from './logging.mjs'
|
||||
import * as bc from './broadcastchannel.mjs'
|
||||
import * as encoding from './encoding.js'
|
||||
import * as decoding from './decoding.js'
|
||||
import * as idbactions from './idbactions.js'
|
||||
import * as logging from './logging.js'
|
||||
import * as bc from './broadcastchannel.js'
|
||||
|
||||
/* make sure to update message.go in ydb when updating these values.. */
|
||||
export const MESSAGE_UPDATE = 0 // TODO: rename host_unconfirmed?
|
||||
@@ -3,8 +3,11 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
import babel from 'rollup-plugin-babel'
|
||||
import uglify from 'rollup-plugin-uglify-es'
|
||||
|
||||
// set this to [] to disable obfuscation
|
||||
const minificationPlugins = process.env.PRODUCTION ? [babel(), uglify()] : []
|
||||
|
||||
export default [{
|
||||
input: './index.mjs',
|
||||
input: './index.js',
|
||||
output: [{
|
||||
name: 'Y',
|
||||
file: 'build/yjs.js',
|
||||
@@ -12,9 +15,9 @@ export default [{
|
||||
sourcemap: true
|
||||
}]
|
||||
}, {
|
||||
input: 'tests/index.mjs',
|
||||
input: 'tests/index.js',
|
||||
output: {
|
||||
file: 'build/y.test.mjs',
|
||||
file: 'build/y.test.js',
|
||||
format: 'iife',
|
||||
name: 'ytests',
|
||||
sourcemap: true
|
||||
@@ -27,7 +30,7 @@ export default [{
|
||||
commonjs()
|
||||
]
|
||||
}, {
|
||||
input: './examples/prosemirror.mjs',
|
||||
input: './examples/prosemirror.js',
|
||||
output: {
|
||||
name: 'prosemirror',
|
||||
file: 'examples/build/prosemirror.js',
|
||||
@@ -39,36 +42,28 @@ export default [{
|
||||
sourcemap: true,
|
||||
module: true
|
||||
}),
|
||||
commonjs(),
|
||||
babel(),
|
||||
uglify()
|
||||
]
|
||||
commonjs()
|
||||
].concat(minificationPlugins)
|
||||
}, {
|
||||
input: './examples/dom.mjs',
|
||||
input: './examples/dom.js',
|
||||
output: {
|
||||
name: 'dom',
|
||||
file: 'examples/build/dom.js',
|
||||
format: 'iife',
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
babel(),
|
||||
uglify()
|
||||
]
|
||||
plugins: minificationPlugins
|
||||
}, {
|
||||
input: './examples/textarea.mjs',
|
||||
input: './examples/textarea.js',
|
||||
output: {
|
||||
name: 'textarea',
|
||||
file: 'examples/build/textarea.js',
|
||||
format: 'iife',
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
babel(),
|
||||
uglify()
|
||||
]
|
||||
plugins: minificationPlugins
|
||||
}, {
|
||||
input: './examples/quill.mjs',
|
||||
input: './examples/quill.js',
|
||||
output: {
|
||||
name: 'textarea',
|
||||
file: 'examples/build/quill.js',
|
||||
@@ -80,8 +75,6 @@ export default [{
|
||||
sourcemap: true,
|
||||
module: true
|
||||
}),
|
||||
commonjs(),
|
||||
babel(),
|
||||
uglify()
|
||||
]
|
||||
commonjs()
|
||||
].concat(minificationPlugins)
|
||||
}]
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { getStructReference } from '../utils/structReferences.mjs'
|
||||
import * as ID from '../utils/ID.mjs'
|
||||
import { stringifyID } from '../protocols/syncProtocol.mjs'
|
||||
import { writeStructToTransaction } from '../utils/Transaction.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import { Item } from './Item.mjs' // eslint-disable-line
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import { deleteItemRange } from '../utils/structManipulation.mjs'
|
||||
import { getStructReference } from '../utils/structReferences.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
// import { Item } from './Item.js' // eslint-disable-line
|
||||
// import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
import { deleteItemRange } from '../utils/structManipulation.js'
|
||||
import * as stringify from '../utils/structStringify.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -99,6 +99,6 @@ export class Delete {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return `Delete - target: ${stringifyID(this._targetID)}, len: ${this._length}`
|
||||
return `Delete - target: ${stringify.stringifyID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { getStructReference } from '../utils/structReferences.mjs'
|
||||
import * as ID from '../utils/ID.mjs'
|
||||
import { writeStructToTransaction } from '../utils/Transaction.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import { getStructReference } from '../utils/structReferences.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
// import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
// TODO should have the same base class as Item
|
||||
export class GC {
|
||||
@@ -19,6 +19,10 @@ export class GC {
|
||||
this._length = 0
|
||||
}
|
||||
|
||||
get _redone () {
|
||||
return null
|
||||
}
|
||||
|
||||
get _deleted () {
|
||||
return true
|
||||
}
|
||||
@@ -2,15 +2,30 @@
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { getStructReference } from '../utils/structReferences.mjs'
|
||||
import * as ID from '../utils/ID.mjs'
|
||||
import { Delete } from './Delete.mjs'
|
||||
import { transactionTypeChanged, writeStructToTransaction } from '../utils/Transaction.mjs'
|
||||
import { GC } from './GC.mjs'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import { Y } from '../utils/Y.mjs'
|
||||
import { Type } from './Type.mjs' // eslint-disable-line
|
||||
import { getStructReference } from '../utils/structReferences.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { Delete } from './Delete.js'
|
||||
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
||||
import { GC } from './GC.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
// import { Type } from './Type.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const transactionTypeChanged = (y, type, sub) => {
|
||||
if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) {
|
||||
const changedTypes = y._transaction.changedTypes
|
||||
let subs = changedTypes.get(type)
|
||||
if (subs === undefined) {
|
||||
// create if it doesn't exist yet
|
||||
subs = new Set()
|
||||
changedTypes.set(type, subs)
|
||||
}
|
||||
subs.add(sub)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -113,6 +128,30 @@ export class Item {
|
||||
this._redone = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next non-deleted item
|
||||
* @private
|
||||
*/
|
||||
get _next () {
|
||||
let n = this._right
|
||||
while (n !== null && n._deleted) {
|
||||
n = n._right
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous non-deleted item
|
||||
* @private
|
||||
*/
|
||||
get _prev () {
|
||||
let n = this._left
|
||||
while (n !== null && n._deleted) {
|
||||
n = n._left
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Item with the same effect as this Item (without position effect)
|
||||
*
|
||||
@@ -127,7 +166,7 @@ export class Item {
|
||||
* Redoes the effect of this operation.
|
||||
*
|
||||
* @param {Y} y The Yjs instance.
|
||||
* @param {Array<Item>} redoitems
|
||||
* @param {Set<Item>} redoitems
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
@@ -135,7 +174,7 @@ export class Item {
|
||||
if (this._redone !== null) {
|
||||
return this._redone
|
||||
}
|
||||
if (this._parent instanceof Y) {
|
||||
if (!(this._parent instanceof Item)) {
|
||||
return
|
||||
}
|
||||
let struct = this._copy()
|
||||
@@ -251,16 +290,22 @@ export class Item {
|
||||
*/
|
||||
_delete (y, createDelete = true, gcChildren) {
|
||||
if (!this._deleted) {
|
||||
const parent = this._parent
|
||||
const len = this._length
|
||||
// adjust the length of parent
|
||||
if (parent.length !== undefined && this._countable) {
|
||||
parent.length -= len
|
||||
}
|
||||
this._deleted = true
|
||||
y.ds.mark(this._id, this._length, false)
|
||||
let del = new Delete()
|
||||
del._targetID = this._id
|
||||
del._length = this._length
|
||||
del._length = len
|
||||
if (createDelete) {
|
||||
// broadcast and persists Delete
|
||||
del._integrate(y, true)
|
||||
}
|
||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
||||
transactionTypeChanged(y, parent, this._parentSub)
|
||||
y._transaction.deletedStructs.add(this)
|
||||
}
|
||||
}
|
||||
@@ -410,6 +455,10 @@ export class Item {
|
||||
right._left = this
|
||||
}
|
||||
}
|
||||
// adjust the length of parent
|
||||
if (parentSub === null && parent.length !== undefined && this._countable) {
|
||||
parent.length += this._length
|
||||
}
|
||||
if (parent._deleted) {
|
||||
this._delete(y, false, true)
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item } from './Item.mjs'
|
||||
import { logItemHelper } from '../protocols/syncProtocol.mjs'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import { Item } from './Item.js'
|
||||
import * as stringify from '../utils/structStringify.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
export class ItemEmbed extends Item {
|
||||
constructor () {
|
||||
@@ -44,6 +44,6 @@ export class ItemEmbed extends Item {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`)
|
||||
return stringify.logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`)
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item } from './Item.mjs'
|
||||
import { logItemHelper } from '../protocols/syncProtocol.mjs'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import { Item } from './Item.js'
|
||||
import * as stringify from '../utils/structStringify.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
export class ItemFormat extends Item {
|
||||
constructor () {
|
||||
@@ -51,6 +51,6 @@ export class ItemFormat extends Item {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`)
|
||||
return stringify.logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`)
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item, splitHelper } from './Item.mjs'
|
||||
import { logItemHelper } from '../protocols/syncProtocol.mjs'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import { Item, splitHelper } from './Item.js'
|
||||
import * as stringify from '../utils/structStringify.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
export class ItemJSON extends Item {
|
||||
constructor () {
|
||||
@@ -19,7 +19,8 @@ export class ItemJSON extends Item {
|
||||
return struct
|
||||
}
|
||||
get _length () {
|
||||
return this._content.length
|
||||
const c = this._content
|
||||
return c !== null ? c.length : 0
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
@@ -46,11 +47,11 @@ export class ItemJSON extends Item {
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
let len = this._content.length
|
||||
const len = this._length
|
||||
encoding.writeVarUint(encoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
let encoded
|
||||
let content = this._content[i]
|
||||
const content = this._content[i]
|
||||
if (content === undefined) {
|
||||
encoded = 'undefined'
|
||||
} else {
|
||||
@@ -66,7 +67,7 @@ export class ItemJSON extends Item {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
|
||||
return stringify.logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
@@ -2,11 +2,11 @@
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item, splitHelper } from './Item.mjs'
|
||||
import { logItemHelper } from '../protocols/syncProtocol.mjs'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import { Item, splitHelper } from './Item.js'
|
||||
import * as stringify from '../utils/structStringify.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
export class ItemString extends Item {
|
||||
constructor () {
|
||||
@@ -44,7 +44,7 @@ export class ItemString extends Item {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return logItemHelper('ItemString', this, `content:"${this._content}"`)
|
||||
return stringify.logItemHelper('ItemString', this, `content:"${this._content}"`)
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
@@ -2,11 +2,11 @@
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item } from './Item.mjs'
|
||||
import { EventHandler } from '../utils/EventHandler.mjs'
|
||||
import { createID } from '../utils/ID.mjs'
|
||||
import { YEvent } from '../utils/YEvent.mjs'
|
||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
||||
import { Item } from './Item.js'
|
||||
import { EventHandler } from '../utils/EventHandler.js'
|
||||
import { createID } from '../utils/ID.js'
|
||||
import { YEvent } from '../utils/YEvent.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
// restructure children as if they were inserted one after another
|
||||
const integrateChildren = (y, start) => {
|
||||
@@ -57,6 +57,17 @@ export class Type extends Item {
|
||||
this._deepEventHandler = new EventHandler()
|
||||
}
|
||||
|
||||
/**
|
||||
* The first non-deleted item
|
||||
*/
|
||||
get _first () {
|
||||
let n = this._start
|
||||
while (n !== null && n._deleted) {
|
||||
n = n._right
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the path from this type to the specified target.
|
||||
*
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test } from 'cutest'
|
||||
import * as random from '../lib/prng/prng.mjs'
|
||||
import { DeleteStore } from '../utils/DeleteStore.mjs'
|
||||
import * as ID from '../utils/ID.mjs'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
import { DeleteStore } from '../utils/DeleteStore.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
|
||||
/**
|
||||
* Converts a DS to an array of length 10.
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test } from 'cutest'
|
||||
import { simpleDiff } from '../lib/diff.mjs'
|
||||
import * as random from '../lib/prng/prng.mjs'
|
||||
import { simpleDiff } from '../lib/diff.js'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
|
||||
function runDiffTest (t, a, b, expected) {
|
||||
let result = simpleDiff(a, b)
|
||||
@@ -1,8 +1,8 @@
|
||||
import { test } from 'cutest'
|
||||
import { generateRandomUint32 } from '../utils/generateRandomUint32.mjs'
|
||||
import * as encoding from '../lib/encoding.mjs'
|
||||
import * as decoding from '../lib/decoding.mjs'
|
||||
import * as random from '../lib/prng/prng.mjs'
|
||||
import { generateRandomUint32 } from '../utils/generateRandomUint32.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
|
||||
function testEncoding (t, write, read, val) {
|
||||
let encoder = encoding.createEncoder()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user