Compare commits
12 Commits
v13.0.0-73
...
v13.0.0-76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4063e28b5e | ||
|
|
b6f7cd7869 | ||
|
|
1a79e429ed | ||
|
|
04066a5678 | ||
|
|
e09ef15349 | ||
|
|
3d70eee959 | ||
|
|
582095e5a3 | ||
|
|
c9ea3a412e | ||
|
|
a2c51c36e9 | ||
|
|
ab3dba5b06 | ||
|
|
3ddff186c2 | ||
|
|
9bd199a6e7 |
@@ -5,7 +5,7 @@
|
|||||||
"dictionaries": ["jsdoc"]
|
"dictionaries": ["jsdoc"]
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
"include": ["./types", "./utils/UndoManager.mjs", "./utils/Y.mjs", "./provider", "./bindings"],
|
"include": ["./types", "./utils/UndoManager.js", "./utils/Y.js", "./provider", "./bindings"],
|
||||||
"includePattern": ".js$"
|
"includePattern": ".js$"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"checkJs": true,
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
..
|
||||||
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./node_modules/yjs/"
|
"./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 */
|
/* global MutationObserver, getSelection */
|
||||||
|
|
||||||
import { fromRelativePosition } from '../../utils/relativePosition.mjs'
|
import { fromRelativePosition } from '../../utils/relativePosition.js'
|
||||||
import { createMutex } from '../../lib/mutex.mjs'
|
import { createMutex } from '../../lib/mutex.js'
|
||||||
import { createAssociation, removeAssociation } from './util.mjs'
|
import { createAssociation, removeAssociation } from './util.js'
|
||||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.mjs'
|
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
|
||||||
import { defaultFilter, applyFilterOnType } from './filter.mjs'
|
import { defaultFilter, applyFilterOnType } from './filter.js'
|
||||||
import { typeObserver } from './typeObserver.mjs'
|
import { typeObserver } from './typeObserver.js'
|
||||||
import { domObserver } from './domObserver.mjs'
|
import { domObserver } from './domObserver.js'
|
||||||
import { YXmlFragment } from '../../types/YXmlElement.mjs' // eslint-disable-line
|
import { YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @callback DomFilter
|
* @callback DomFilter
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
* @module bindings/dom
|
* @module bindings/dom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { YXmlHook } from '../../types/YXmlHook.mjs'
|
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||||
import {
|
import {
|
||||||
iterateUntilUndeleted,
|
iterateUntilUndeleted,
|
||||||
removeAssociation,
|
removeAssociation,
|
||||||
insertNodeHelper } from './util.mjs'
|
insertNodeHelper } from './util.js'
|
||||||
import { simpleDiff } from '../../lib/diff.mjs'
|
import { simpleDiff } from '../../lib/diff.js'
|
||||||
import { YXmlFragment } from '../../types/YXmlElement.mjs'
|
import { YXmlFragment } from '../../types/YXmlElement.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Check if any of the nodes was deleted
|
* 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.
|
* recreate a new yxml element that is bound to that node.
|
||||||
* You can detect that a node was moved because expectedId
|
* You can detect that a node was moved because expectedId
|
||||||
* !== actualId in the list
|
* !== actualId in the list
|
||||||
|
*
|
||||||
|
* @function
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const applyChangesFromDom = (binding, dom, yxml, _document) => {
|
const applyChangesFromDom = (binding, dom, yxml, _document) => {
|
||||||
@@ -85,6 +87,7 @@ const applyChangesFromDom = (binding, dom, yxml, _document) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
*/
|
*/
|
||||||
export function domObserver (mutations, _document) {
|
export function domObserver (mutations, _document) {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
import { YXmlText } from '../../types/YXmlText.mjs'
|
import { YXmlText } from '../../types/YXmlText.js'
|
||||||
import { YXmlHook } from '../../types/YXmlHook.mjs'
|
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||||
import { YXmlElement } from '../../types/YXmlElement.mjs'
|
import { YXmlElement } from '../../types/YXmlElement.js'
|
||||||
import { createAssociation, domsToTypes } from './util.mjs'
|
import { createAssociation, domsToTypes } from './util.js'
|
||||||
import { filterDomAttributes, defaultFilter } from './filter.mjs'
|
import { filterDomAttributes, defaultFilter } from './filter.js'
|
||||||
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
|
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @callback DomFilter
|
* @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.
|
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Element|Text} element The DOM Element
|
* @param {Element|Text} element The DOM Element
|
||||||
* @param {?Document} _document Optional. Provide the global document object
|
* @param {?Document} _document Optional. Provide the global document object
|
||||||
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
|
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
* @module bindings/dom
|
* @module bindings/dom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Y } from '../../utils/Y.mjs' // eslint-disable-line
|
import { Y } from '../../utils/Y.js' // eslint-disable-line
|
||||||
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.mjs' // eslint-disable-line
|
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
|
||||||
import { isParentOf } from '../../utils/isParentOf.mjs'
|
import { isParentOf } from '../../utils/isParentOf.js'
|
||||||
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
|
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default filter method (does nothing).
|
* Default filter method (does nothing).
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {String} nodeName The nodeName of the element
|
* @param {String} nodeName The nodeName of the element
|
||||||
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
|
* @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
|
* @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) => {
|
export const filterDomAttributes = (dom, filter) => {
|
||||||
const attrs = new Map()
|
const attrs = new Map()
|
||||||
@@ -35,11 +39,11 @@ export const filterDomAttributes = (dom, filter) => {
|
|||||||
/**
|
/**
|
||||||
* Applies a filter on a type.
|
* Applies a filter on a type.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
* @param {Y} y The Yjs instance.
|
* @param {Y} y The Yjs instance.
|
||||||
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
|
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
|
||||||
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
|
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
export const applyFilterOnType = (y, binding, type) => {
|
export const applyFilterOnType = (y, binding, type) => {
|
||||||
if (isParentOf(binding.type, type) && type instanceof YXmlElement) {
|
if (isParentOf(binding.type, type) && type instanceof YXmlElement) {
|
||||||
@@ -4,10 +4,13 @@
|
|||||||
|
|
||||||
/* globals getSelection */
|
/* globals getSelection */
|
||||||
|
|
||||||
import { getRelativePosition } from '../../utils/relativePosition.mjs'
|
import { getRelativePosition } from '../../utils/relativePosition.js'
|
||||||
|
|
||||||
let relativeSelection = null
|
let relativeSelection = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
const _getCurrentRelativeSelection = domBinding => {
|
const _getCurrentRelativeSelection = domBinding => {
|
||||||
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
|
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
|
||||||
const baseNodeType = domBinding.domToType.get(baseNode)
|
const baseNodeType = domBinding.domToType.get(baseNode)
|
||||||
@@ -21,8 +24,14 @@ const _getCurrentRelativeSelection = domBinding => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
|
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
export const beforeTransactionSelectionFixer = domBinding => {
|
export const beforeTransactionSelectionFixer = domBinding => {
|
||||||
relativeSelection = getCurrentRelativeSelection(domBinding)
|
relativeSelection = getCurrentRelativeSelection(domBinding)
|
||||||
}
|
}
|
||||||
@@ -30,6 +39,7 @@ export const beforeTransactionSelectionFixer = domBinding => {
|
|||||||
/**
|
/**
|
||||||
* Reset the browser range after every transaction.
|
* Reset the browser range after every transaction.
|
||||||
* This prevents any collapsing issues with the local selection.
|
* This prevents any collapsing issues with the local selection.
|
||||||
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export const afterTransactionSelectionFixer = domBinding => {
|
export const afterTransactionSelectionFixer = domBinding => {
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
/* global getSelection */
|
/* global getSelection */
|
||||||
|
|
||||||
import { YXmlText } from '../../types/YXmlText.mjs'
|
import { YXmlText } from '../../types/YXmlText.js'
|
||||||
import { YXmlHook } from '../../types/YXmlHook.mjs'
|
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||||
import { removeDomChildrenUntilElementFound } from './util.mjs'
|
import { removeDomChildrenUntilElementFound } from './util.js'
|
||||||
|
|
||||||
const findScrollReference = scrollingElement => {
|
const findScrollReference = scrollingElement => {
|
||||||
if (scrollingElement !== null) {
|
if (scrollingElement !== null) {
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
* @module bindings/dom
|
* @module bindings/dom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { domToType } from './domToType.mjs'
|
import { domToType } from './domToType.js'
|
||||||
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
|
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterates items until an undeleted item is found.
|
* Iterates items until an undeleted item is found.
|
||||||
@@ -22,6 +22,7 @@ export const iterateUntilUndeleted = item => {
|
|||||||
* type).
|
* type).
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
* @param {DomBinding} domBinding The binding object
|
* @param {DomBinding} domBinding The binding object
|
||||||
* @param {Element} dom The dom that is to be associated with type
|
* @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
|
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
|
||||||
@@ -37,6 +38,7 @@ export const removeAssociation = (domBinding, dom, type) => {
|
|||||||
* type).
|
* type).
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
* @param {DomBinding} domBinding The binding object
|
* @param {DomBinding} domBinding The binding object
|
||||||
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
|
* @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
|
* @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.
|
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
* @param {DomBinding} domBinding The binding object
|
* @param {DomBinding} domBinding The binding object
|
||||||
* @param {Element} oldDom The existing dom
|
* @param {Element} oldDom The existing dom
|
||||||
* @param {Element} newDom The new dom object
|
* @param {Element} newDom The new dom object
|
||||||
@@ -74,6 +77,7 @@ export const switchAssociation = (domBinding, oldDom, newDom) => {
|
|||||||
* specified position.
|
* specified position.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
* @param {YXmlElement} type The type in which to insert DOM elements.
|
* @param {YXmlElement} type The type in which to insert DOM elements.
|
||||||
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
||||||
* inserted after this node. Set null to insert at
|
* inserted after this node. Set null to insert at
|
||||||
@@ -101,6 +105,7 @@ export const domsToTypes = (doms, _document, hooks, filter, binding) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
*/
|
*/
|
||||||
export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, binding) => {
|
export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, binding) => {
|
||||||
let insertedNodes = insertDomElementsAfter(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.
|
* Remove children until `elem` is found.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
* @param {Element} parent The parent of `elem` and `currentChild`.
|
* @param {Element} parent The parent of `elem` and `currentChild`.
|
||||||
* @param {Node} currentChild Start removing elements with `currentChild`. If
|
* @param {Node} currentChild Start removing elements with `currentChild`. If
|
||||||
* `currentChild` is `elem` it won't be removed.
|
* `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
|
* @module bindings/quill
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createMutex } from '../lib/mutex.mjs'
|
import { createMutex } from '../lib/mutex.js'
|
||||||
|
|
||||||
const typeObserver = function (event) {
|
const typeObserver = function (event) {
|
||||||
const quill = this.target
|
const quill = this.target
|
||||||
@@ -53,7 +53,6 @@ export class QuillBinding {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this._mutualExclude = createMutex()
|
this._mutualExclude = createMutex()
|
||||||
|
|
||||||
// Set initial value.
|
// Set initial value.
|
||||||
quill.setContents(textType.toDelta(), 'yjs')
|
quill.setContents(textType.toDelta(), 'yjs')
|
||||||
// Observers are handled by this class.
|
// Observers are handled by this class.
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
* @module bindings/textarea
|
* @module bindings/textarea
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { simpleDiff } from '../lib/diff.mjs'
|
import { simpleDiff } from '../lib/diff.js'
|
||||||
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.mjs'
|
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.js'
|
||||||
import { createMutex } from '../lib/mutex.mjs'
|
import { createMutex } from '../lib/mutex.js'
|
||||||
|
|
||||||
function typeObserver () {
|
function typeObserver () {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
@@ -10,13 +10,18 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 class="code-html">
|
||||||
<div id="content" contenteditable=""></div>
|
|
||||||
|
<div id="content" contenteditable=""></div>
|
||||||
</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">
|
<script class="code-js" src="./build/dom.js">
|
||||||
import * as Y from 'yjs/index.mjs'
|
import * as Y from 'yjs/index.js'
|
||||||
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
|
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||||
import { DomBinding } from 'yjs/bindings/dom.mjs'
|
import { DomBinding } from 'yjs/bindings/dom.js'
|
||||||
|
|
||||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||||
const ydocument = provider.get('dom')
|
const ydocument = provider.get('dom')
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import * as Y from '../index.mjs'
|
import * as Y from '../index.js'
|
||||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
import { WebsocketProvider } from '../provider/websocket.js'
|
||||||
import { DomBinding } from '../bindings/dom.mjs'
|
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 ydocument = provider.get('dom')
|
||||||
const type = ydocument.define('xml', Y.XmlFragment)
|
const type = ydocument.define('xml', Y.XmlFragment)
|
||||||
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
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;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.ProseMirror img { max-width: 100px }
|
.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 {
|
.ProseMirror-yjs-cursor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-left: black;
|
border-left: black;
|
||||||
@@ -41,13 +48,17 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 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>
|
</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">
|
<script class="code-js" src="./build/prosemirror.js">
|
||||||
import * as Y from 'yjs'
|
import * as Y from 'yjs'
|
||||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
import { WebsocketProvider } from '../provider/websocket.js'
|
||||||
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
|
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
|
||||||
|
|
||||||
import { EditorState } from 'prosemirror-state'
|
import { EditorState } from 'prosemirror-state'
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as Y from '../index.mjs'
|
import * as Y from '../index.js'
|
||||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
import { WebsocketProvider } from '../provider/websocket.js'
|
||||||
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
|
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror.js'
|
||||||
|
|
||||||
|
import * as conf from './exampleConfig.js'
|
||||||
|
|
||||||
import { EditorState } from 'prosemirror-state'
|
import { EditorState } from 'prosemirror-state'
|
||||||
import { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
@@ -8,7 +10,7 @@ import { DOMParser } from 'prosemirror-model'
|
|||||||
import { schema } from 'prosemirror-schema-basic'
|
import { schema } from 'prosemirror-schema-basic'
|
||||||
import { exampleSetup } from 'prosemirror-example-setup'
|
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 ydocument = provider.get('prosemirror')
|
||||||
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
||||||
|
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Yjs Prosemirror Example</title>
|
<title>Yjs Quill Example</title>
|
||||||
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 class="code-html">
|
||||||
<div id="quill-container">
|
<div id="quill-container">
|
||||||
<div id="quill">
|
<div id="quill">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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">
|
<script class="code-js" src="./build/quill.js">
|
||||||
import * as Y from 'yjs'
|
import * as Y from 'yjs'
|
||||||
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
|
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||||
import { QuillBinding } from 'yjs/bindings/quill.mjs'
|
import { QuillBinding } from 'yjs/bindings/quill.js'
|
||||||
|
|
||||||
import Quill from 'quill'
|
import Quill from 'quill'
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import * as Y from '../index.mjs'
|
import * as Y from '../index.js'
|
||||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
import { WebsocketProvider } from '../provider/websocket.js'
|
||||||
import { QuillBinding } from '../bindings/quill.mjs'
|
import { QuillBinding } from '../bindings/quill.js'
|
||||||
|
|
||||||
|
import * as conf from './exampleConfig.js'
|
||||||
|
|
||||||
import Quill from 'quill'
|
import Quill from 'quill'
|
||||||
|
|
||||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
const provider = new WebsocketProvider(conf.serverAddress)
|
||||||
const ydocument = provider.get('quill')
|
const ydocument = provider.get('quill')
|
||||||
const ytext = ydocument.define('quill', Y.Text)
|
const ytext = ydocument.define('quill', Y.Text)
|
||||||
|
|
||||||
@@ -2,16 +2,19 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Yjs Textarea Example</title>
|
<title>Yjs Textarea Example</title>
|
||||||
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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">
|
<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>
|
</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">
|
<script class="code-js" src="./build/textarea.js">
|
||||||
import * as Y from 'yjs'
|
import * as Y from 'yjs'
|
||||||
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
|
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||||
import { TextareaBinding } from 'yjs/bindings/textarea.mjs'
|
import { TextareaBinding } from 'yjs/bindings/textarea.js'
|
||||||
|
|
||||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||||
const ydocument = provider.get('textarea')
|
const ydocument = provider.get('textarea')
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import * as Y from '../index.mjs'
|
import * as Y from '../index.js'
|
||||||
import { WebsocketProvider } from '../provider/websocket.mjs'
|
import { WebsocketProvider } from '../provider/websocket.js'
|
||||||
import { TextareaBinding } from '../bindings/textarea.mjs'
|
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 ydocument = provider.get('textarea')
|
||||||
const type = ydocument.define('textarea', Y.Text)
|
const type = ydocument.define('textarea', Y.Text)
|
||||||
const textarea = document.querySelector('textarea')
|
const textarea = document.querySelector('textarea')
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<div id="aceContainer"></div>
|
<div id="aceContainer"></div>
|
||||||
<script src="../../y.js"></script>
|
<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="../bower_components/ace-builds/src/ace.js"></script>
|
||||||
|
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<input type="submit" value="Send">
|
<input type="submit" value="Send">
|
||||||
</form>
|
</form>
|
||||||
<script src="../../y.js"></script>
|
<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>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div id="codeMirrorContainer"></div>
|
<div id="codeMirrorContainer"></div>
|
||||||
|
|
||||||
<script src="../../y.js"></script>
|
<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/lib/codemirror.js"></script>
|
||||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||||
<script src="../../y.js"></script>
|
<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="../bower_components/d3/d3.min.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
</head>
|
</head>
|
||||||
<script src="../../y.js"></script>
|
<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="../bower_components/d3/d3.min.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
|
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
|
||||||
import Y from '../../src/Y.mjs'
|
import Y from '../../src/Y.js'
|
||||||
import DomBinding from '../../bindings/DomBinding/DomBinding.mjs'
|
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
|
||||||
import UndoManager from '../../src/Util/UndoManager.mjs'
|
import UndoManager from '../../src/Util/UndoManager.js'
|
||||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
|
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
|
||||||
import YXmlText from '../../src/Types/YXml/YXmlText.mjs'
|
import YXmlText from '../../src/Types/YXml/YXmlText.js'
|
||||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.mjs'
|
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
|
||||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.mjs'
|
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.js'
|
||||||
|
|
||||||
const connector = new YWebsocketsConnector()
|
const connector = new YWebsocketsConnector()
|
||||||
const persistence = new YIndexdDBPersistence()
|
const persistence = new YIndexdDBPersistence()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="../../y.js"></script>
|
<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>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<script src="../../y.js"></script>
|
<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="../bower_components/d3/d3.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="../../y.js"></script>
|
<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="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
|
||||||
import { createYdbClient } from '../../YdbClient/index.mjs'
|
import { createYdbClient } from '../../YdbClient/index.js'
|
||||||
import Y from '../../src/Y.dist.mjs'
|
import Y from '../../src/Y.dist.js'
|
||||||
import * as ydb from '../../YdbClient/YdbClient.mjs'
|
import * as ydb from '../../YdbClient/YdbClient.js'
|
||||||
import DomBinding from '../../bindings/DomBinding/DomBinding.mjs'
|
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
|
||||||
|
|
||||||
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||||
const r = Math.random() * 16 | 0
|
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 */
|
/* global Buffer */
|
||||||
|
|
||||||
import * as globals from './globals.mjs'
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Decoder handles the decoding of an ArrayBuffer.
|
* 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
|
* Read string of variable length
|
||||||
* * varUint is used to store the length of the string
|
* * varUint is used to store the length of the string
|
||||||
@@ -189,3 +204,4 @@ export const peekVarString = decoder => {
|
|||||||
decoder.pos = pos
|
decoder.pos = pos
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @module encoding
|
* @module encoding
|
||||||
*/
|
*/
|
||||||
import * as globals from './globals.mjs'
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
const bits7 = 0b1111111
|
const bits7 = 0b1111111
|
||||||
const bits8 = 0b11111111
|
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.
|
* 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 error = description => new Error(description)
|
||||||
|
|
||||||
export const max = (a, b) => a > b ? a : b
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} t Time to wait
|
* @param {number} t Time to wait
|
||||||
* @return {Promise} Promise that is resolved after t ms
|
* @return {Promise} Promise that is resolved after t ms
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* @module idb
|
* @module lib/idb
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
|
||||||
import * as globals from './globals.mjs'
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* IDB Request to Promise transformer
|
* IDB Request to Promise transformer
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as test from './testing.mjs'
|
import * as test from './testing.js'
|
||||||
import * as idb from './idb.mjs'
|
import * as idb from './idb.js'
|
||||||
import * as logging from './logging.mjs'
|
import * as logging from './logging.js'
|
||||||
|
|
||||||
const initTestDB = db => idb.createStores(db, [['test']])
|
const initTestDB = db => idb.createStores(db, [['test']])
|
||||||
const testDBName = 'idb-test'
|
const testDBName = 'idb-test'
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @module logging
|
* @module logging
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as globals from './globals.mjs'
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
let date = new Date().getTime()
|
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
|
* @module prng
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Mt19937 } from './Mt19937.mjs'
|
import { Mt19937 } from './Mt19937.js'
|
||||||
import { Xoroshiro128plus } from './Xoroshiro128plus.mjs'
|
import { Xoroshiro128plus } from './Xoroshiro128plus.js'
|
||||||
import { Xorshift32 } from './Xorshift32.mjs'
|
import { Xorshift32 } from './Xorshift32.js'
|
||||||
import * as time from '../../time.mjs'
|
import * as time from '../../time.js'
|
||||||
|
|
||||||
const DIAMETER = 300
|
const DIAMETER = 300
|
||||||
const NUMBERS = 10000
|
const NUMBERS = 10000
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @module prng
|
* @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.
|
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
* @module prng
|
* @module prng
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as binary from '../binary.mjs'
|
import * as binary from '../binary.js'
|
||||||
import { fromCharCode, fromCodePoint } from '../string.mjs'
|
import { fromCharCode, fromCodePoint } from '../string.js'
|
||||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.mjs'
|
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
|
||||||
import * as math from '../math.mjs'
|
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
|
* 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
|
*TODO: enable tests
|
||||||
import * as rt from '../rich-text/formatters.mjs''
|
import * as rt from '../rich-text/formatters.js''
|
||||||
import { test } from '../test/test.mjs''
|
import { test } from '../test/test.js''
|
||||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs''
|
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.js''
|
||||||
import Xorshift32 from './PRNG/Xorshift32.mjs''
|
import Xorshift32 from './PRNG/Xorshift32.js''
|
||||||
import MT19937 from './PRNG/Mt19937.mjs''
|
import MT19937 from './PRNG/Mt19937.js''
|
||||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs''
|
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.js''
|
||||||
import { MAX_SAFE_INTEGER } from '../number/constants.mjs''
|
import { MAX_SAFE_INTEGER } from '../number/constants.js''
|
||||||
import { BIT32 } from '../binary/constants.mjs''
|
import { BIT32 } from '../binary/constants.js''
|
||||||
|
|
||||||
function init (Gen) {
|
function init (Gen) {
|
||||||
return {
|
return {
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
* @module testing
|
* @module testing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as logging from './logging.mjs'
|
import * as logging from './logging.js'
|
||||||
import { simpleDiff } from './diff.mjs'
|
import { simpleDiff } from './diff.js'
|
||||||
|
|
||||||
export const run = async (name, f) => {
|
export const run = async (name, f) => {
|
||||||
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-73",
|
"version": "13.0.0-76",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -67,12 +67,28 @@
|
|||||||
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
|
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
|
||||||
"dev": true
|
"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": {
|
"@types/node": {
|
||||||
"version": "6.0.110",
|
"version": "6.0.110",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
|
||||||
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
|
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
|
||||||
"dev": true
|
"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": {
|
"abab": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
||||||
@@ -5944,7 +5960,7 @@
|
|||||||
},
|
},
|
||||||
"quill": {
|
"quill": {
|
||||||
"version": "1.3.6",
|
"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==",
|
"integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -1,22 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-73",
|
"version": "13.0.0-76",
|
||||||
"description": "A ",
|
"description": "A ",
|
||||||
"module": "./index.mjs'",
|
"main": "./build/yjs.js",
|
||||||
|
"module": "./index.js'",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"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",
|
"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",
|
"lint": "standard **/*.js",
|
||||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
||||||
"serve-docs": "npm run docs && serve ./docs/",
|
"serve-docs": "npm run docs && serve ./docs/",
|
||||||
"postversion": "npm run build",
|
"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"
|
"now-start": "npm run websocket-server"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
"build/*",
|
||||||
"bindings/*",
|
"bindings/*",
|
||||||
"docs/*",
|
"docs/*",
|
||||||
"examples/*",
|
"examples/*",
|
||||||
@@ -65,6 +67,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "http://y-js.org",
|
"homepage": "http://y-js.org",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/ws": "^6.0.1",
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babel-plugin-transform-regenerator": "^6.26.0",
|
"babel-plugin-transform-regenerator": "^6.26.0",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { createMutex } from '../lib/mutex.mjs'
|
import { createMutex } from '../lib/mutex.js'
|
||||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.mjs'
|
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
|
||||||
|
|
||||||
function createFilePath (persistence, roomName) {
|
function createFilePath (persistence, roomName) {
|
||||||
// TODO: filename checking!
|
// 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 { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
|
||||||
import { writeStructs } from '../MessageHandler/syncStep1.mjs'
|
import { writeStructs } from '../MessageHandler/syncStep1.js'
|
||||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.mjs'
|
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
|
||||||
|
|
||||||
export const PERSIST_UPDATE = 0
|
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
|
* @module awareness-protocol
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
|
||||||
const messageUsersStateChanged = 0
|
const messageUsersStateChanged = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} UserStateUpdate
|
* @typedef {Object} UserStateUpdate
|
||||||
* @property {number} UserStateUpdate.userID
|
* @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 {decoding.Decoder} decoder
|
||||||
* @param {encoding.Encoder} encoder
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @return {Array<UserState>} Array of state updates
|
||||||
*/
|
*/
|
||||||
export const forwardAwarenessMessage = (decoder, encoder) => {
|
export const forwardAwarenessMessage = (decoder, encoder) => {
|
||||||
|
let s = []
|
||||||
switch (decoding.readVarUint(decoder)) {
|
switch (decoding.readVarUint(decoder)) {
|
||||||
case messageUsersStateChanged:
|
case messageUsersStateChanged:
|
||||||
return forwardUsersStateChange(decoder, encoder)
|
s = forwardUsersStateChange(decoder, encoder)
|
||||||
}
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
* @module sync-protocol
|
* @module sync-protocol
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import * as ID from '../utils/ID.mjs'
|
import * as ID from '../utils/ID.js'
|
||||||
import { getStruct } from '../utils/structReferences.mjs'
|
import { getStruct } from '../utils/structReferences.js'
|
||||||
import { deleteItemRange } from '../utils/structManipulation.mjs'
|
import { deleteItemRange } from '../utils/structManipulation.js'
|
||||||
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.mjs'
|
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.js'
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
import { Item } from '../structs/Item.mjs'
|
import { Item } from '../structs/Item.js'
|
||||||
|
import * as stringify from '../utils/structStringify.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Map<number, number>} StateSet
|
* @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)
|
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const messageYjsSyncStep1 = 0
|
export const messageYjsSyncStep1 = 0
|
||||||
const messageYjsSyncStep2 = 1
|
export const messageYjsSyncStep2 = 1
|
||||||
const messageYjsUpdate = 2
|
export const messageYjsUpdate = 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stringifies a message-encoded Delete Set.
|
* Stringifies a message-encoded Delete Set.
|
||||||
@@ -83,7 +84,7 @@ export const writeDeleteSet = (encoder, y) => {
|
|||||||
const gc = n.gc
|
const gc = n.gc
|
||||||
if (currentUser !== user) {
|
if (currentUser !== user) {
|
||||||
numberOfUsers++
|
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
|
if (currentUser !== null) { // happens on first iteration
|
||||||
encoding.setUint32(encoder, lastLenPos, currentLength)
|
encoding.setUint32(encoder, lastLenPos, currentLength)
|
||||||
@@ -234,50 +235,6 @@ export const readStateSet = decoder => {
|
|||||||
return ss
|
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 {decoding.Decoder} decoder
|
||||||
* @param {Y} y
|
* @param {Y} y
|
||||||
@@ -293,7 +250,7 @@ export const stringifyStructs = (decoder, y) => {
|
|||||||
let missing = struct._fromBinary(y, decoder)
|
let missing = struct._fromBinary(y, decoder)
|
||||||
let logMessage = ' ' + struct._logString()
|
let logMessage = ' ' + struct._logString()
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
logMessage += ' .. missing: ' + missing.map(stringifyItemID).join(', ')
|
logMessage += ' .. missing: ' + missing.map(stringify.stringifyItemID).join(', ')
|
||||||
}
|
}
|
||||||
str += logMessage + '\n'
|
str += logMessage + '\n'
|
||||||
}
|
}
|
||||||
@@ -410,10 +367,9 @@ export const stringifySyncStep2 = (decoder, y) => {
|
|||||||
* Read and apply Structs and then DeleteSet to a y instance.
|
* Read and apply Structs and then DeleteSet to a y instance.
|
||||||
*
|
*
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
* @param {encoding.Encoder} encoder
|
|
||||||
* @param {Y} y
|
* @param {Y} y
|
||||||
*/
|
*/
|
||||||
export const readSyncStep2 = (decoder, encoder, y) => {
|
export const readSyncStep2 = (decoder, y) => {
|
||||||
readStructs(decoder, y)
|
readStructs(decoder, y)
|
||||||
readDeleteSet(decoder, y)
|
readDeleteSet(decoder, y)
|
||||||
}
|
}
|
||||||
@@ -480,7 +436,7 @@ export const readSyncMessage = (decoder, encoder, y) => {
|
|||||||
readSyncStep1(decoder, encoder, y)
|
readSyncStep1(decoder, encoder, y)
|
||||||
break
|
break
|
||||||
case messageYjsSyncStep2:
|
case messageYjsSyncStep2:
|
||||||
y.transact(() => readSyncStep2(decoder, encoder, y), true)
|
y.transact(() => readSyncStep2(decoder, y), true)
|
||||||
break
|
break
|
||||||
case messageYjsUpdate:
|
case messageYjsUpdate:
|
||||||
y.transact(() => readUpdate(decoder, y), true)
|
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 => {
|
||||||
|
const encoder = readMessage(this, data) // already muxed
|
||||||
|
this.mux(() => {
|
||||||
|
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'
|
const Y = require('../../build/yjs.js')
|
||||||
import WebSocket from 'ws'
|
const WebSocket = require('ws')
|
||||||
import http from 'http'
|
const http = require('http')
|
||||||
|
|
||||||
const port = process.env.PORT || 1234
|
const port = process.env.PORT || 1234
|
||||||
|
|
||||||
@@ -19,20 +19,21 @@ const docs = new Map()
|
|||||||
|
|
||||||
const messageSync = 0
|
const messageSync = 0
|
||||||
const messageAwareness = 1
|
const messageAwareness = 1
|
||||||
|
const messageAuth = 2
|
||||||
|
|
||||||
const afterTransaction = (doc, transaction) => {
|
const afterTransaction = (doc, transaction) => {
|
||||||
if (transaction.encodedStructsLen > 0) {
|
if (transaction.encodedStructsLen > 0) {
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.encoding.createEncoder()
|
||||||
Y.writeVarUint(encoder, messageSync)
|
Y.encoding.writeVarUint(encoder, messageSync)
|
||||||
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
Y.syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||||
const message = Y.toBuffer(encoder)
|
const message = Y.encoding.toBuffer(encoder)
|
||||||
doc.conns.forEach((_, conn) => conn.send(message))
|
doc.conns.forEach((_, conn) => conn.send(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WSSharedDoc extends Y.Y {
|
class WSSharedDoc extends Y.Y {
|
||||||
constructor () {
|
constructor () {
|
||||||
super()
|
super({ gc: true })
|
||||||
this.mux = Y.createMutex()
|
this.mux = Y.createMutex()
|
||||||
/**
|
/**
|
||||||
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
||||||
@@ -45,25 +46,25 @@ class WSSharedDoc extends Y.Y {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageListener = (conn, doc, message) => {
|
const messageListener = (conn, doc, message) => {
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.encoding.createEncoder()
|
||||||
const decoder = Y.createDecoder(message)
|
const decoder = Y.decoding.createDecoder(message)
|
||||||
const messageType = Y.readVarUint(decoder)
|
const messageType = Y.decoding.readVarUint(decoder)
|
||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
case messageSync:
|
case messageSync:
|
||||||
Y.writeVarUint(encoder, messageSync)
|
Y.encoding.writeVarUint(encoder, messageSync)
|
||||||
Y.readSyncMessage(decoder, encoder, doc)
|
Y.syncProtocol.readSyncMessage(decoder, encoder, doc)
|
||||||
if (Y.length(encoder) > 1) {
|
if (Y.encoding.length(encoder) > 1) {
|
||||||
conn.send(Y.toBuffer(encoder))
|
conn.send(Y.encoding.toBuffer(encoder))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case messageAwareness: {
|
case messageAwareness: {
|
||||||
Y.writeVarUint(encoder, messageAwareness)
|
Y.encoding.writeVarUint(encoder, messageAwareness)
|
||||||
const updates = Y.forwardAwarenessMessage(decoder, encoder)
|
const updates = Y.awarenessProtocol.forwardAwarenessMessage(decoder, encoder)
|
||||||
updates.forEach(update => {
|
updates.forEach(update => {
|
||||||
doc.awareness.set(update.userID, update.state)
|
doc.awareness.set(update.userID, update.state)
|
||||||
doc.conns.get(conn).add(update.userID)
|
doc.conns.get(conn).add(update.userID)
|
||||||
})
|
})
|
||||||
const buff = Y.toBuffer(encoder)
|
const buff = Y.encoding.toBuffer(encoder)
|
||||||
doc.conns.forEach((_, c) => {
|
doc.conns.forEach((_, c) => {
|
||||||
c.send(buff)
|
c.send(buff)
|
||||||
})
|
})
|
||||||
@@ -86,29 +87,29 @@ const setupConnection = (conn, req) => {
|
|||||||
conn.on('close', () => {
|
conn.on('close', () => {
|
||||||
const controlledIds = doc.conns.get(conn)
|
const controlledIds = doc.conns.get(conn)
|
||||||
doc.conns.delete(conn)
|
doc.conns.delete(conn)
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.encoding.createEncoder()
|
||||||
Y.writeVarUint(encoder, messageAwareness)
|
Y.encoding.writeVarUint(encoder, messageAwareness)
|
||||||
Y.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => {
|
Y.awarenessProtocol.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => {
|
||||||
doc.awareness.delete(userID)
|
doc.awareness.delete(userID)
|
||||||
return { userID, state: null }
|
return { userID, state: null }
|
||||||
}))
|
}))
|
||||||
const buf = Y.toBuffer(encoder)
|
const buf = Y.encoding.toBuffer(encoder)
|
||||||
doc.conns.forEach((_, conn) => conn.send(buf))
|
doc.conns.forEach((_, conn) => conn.send(buf))
|
||||||
})
|
})
|
||||||
// send sync step 1
|
// send sync step 1
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.encoding.createEncoder()
|
||||||
Y.writeVarUint(encoder, messageSync)
|
Y.encoding.writeVarUint(encoder, messageSync)
|
||||||
Y.writeSyncStep1(encoder, doc)
|
Y.syncProtocol.writeSyncStep1(encoder, doc)
|
||||||
conn.send(Y.toBuffer(encoder))
|
conn.send(Y.encoding.toBuffer(encoder))
|
||||||
if (doc.awareness.size > 0) {
|
if (doc.awareness.size > 0) {
|
||||||
const encoder = Y.createEncoder()
|
const encoder = Y.encoding.createEncoder()
|
||||||
const userStates = []
|
const userStates = []
|
||||||
doc.awareness.forEach((state, userID) => {
|
doc.awareness.forEach((state, userID) => {
|
||||||
userStates.push({ state, userID })
|
userStates.push({ state, userID })
|
||||||
})
|
})
|
||||||
Y.writeVarUint(encoder, messageAwareness)
|
Y.encoding.writeVarUint(encoder, messageAwareness)
|
||||||
Y.writeUsersStateChange(encoder, userStates)
|
Y.awarenessProtocol.writeUsersStateChange(encoder, userStates)
|
||||||
conn.send(Y.toBuffer(encoder))
|
conn.send(Y.encoding.toBuffer(encoder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @module provider/ydb
|
* @module provider/ydb
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as globals from './globals.mjs'
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
export const Class = class NamedEventHandler {
|
export const Class = class NamedEventHandler {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -3,19 +3,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
import * as idbactions from './idbactions.mjs'
|
import * as idbactions from './idbactions.js'
|
||||||
import * as globals from '../../lib/globals.mjs'
|
import * as globals from '../../lib/globals.js'
|
||||||
import * as message from './message.mjs'
|
import * as message from './message.js'
|
||||||
import * as bc from './broadcastchannel.mjs'
|
import * as bc from './broadcastchannel.js'
|
||||||
import * as encoding from '../../lib/encoding.mjs'
|
import * as encoding from '../../lib/encoding.js'
|
||||||
import * as logging from '../../lib/logging.mjs'
|
import * as logging from '../../lib/logging.js'
|
||||||
import * as idb from '../../lib/idb.mjs'
|
import * as idb from '../../lib/idb.js'
|
||||||
import * as decoding from '../../lib/decoding.mjs'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
import { Y } from '../../utils/Y.mjs'
|
import { Y } from '../../utils/Y.js'
|
||||||
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.mjs'
|
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.js'
|
||||||
import { createMutualExclude } from '../../lib/mutualExclude.mjs'
|
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
||||||
|
|
||||||
import * as NamedEventHandler from './NamedEventHandler.mjs'
|
import * as NamedEventHandler from './NamedEventHandler.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef RoomState
|
* @typedef RoomState
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
|
||||||
import * as test from './test.mjs'
|
import * as test from './test.js'
|
||||||
import * as ydbClient from './YdbClient.mjs'
|
import * as ydbClient from './YdbClient.js'
|
||||||
import * as globals from './globals.mjs'
|
import * as globals from './globals.js'
|
||||||
import * as idbactions from './idbactions.mjs'
|
import * as idbactions from './idbactions.js'
|
||||||
import * as logging from './logging.mjs'
|
import * as logging from './logging.js'
|
||||||
|
|
||||||
const wsUrl = 'ws://127.0.0.1:8899/ws'
|
const wsUrl = 'ws://127.0.0.1:8899/ws'
|
||||||
const testRoom = 'testroom'
|
const testRoom = 'testroom'
|
||||||
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
|
||||||
import * as decoding from '../../lib/decoding.mjs'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
import * as encoding from '../../lib/encoding.mjs'
|
import * as encoding from '../../lib/encoding.js'
|
||||||
import * as globals from '../../lib/globals.mjs'
|
import * as globals from '../../lib/globals.js'
|
||||||
import * as NamedEventHandler from './NamedEventHandler.mjs'
|
import * as NamedEventHandler from './NamedEventHandler.js'
|
||||||
|
|
||||||
const bc = new BroadcastChannel('ydb-client')
|
const bc = new BroadcastChannel('ydb-client')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Map<string, Set<Function>>}
|
* @type {Map<string, Set<Function>>}
|
||||||
*/
|
*/
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
* - A client may update a room when the room is in either US or Co
|
* - A client may update a room when the room is in either US or Co
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as encoding from '../../lib/encoding.mjs'
|
import * as encoding from '../../lib/encoding.js'
|
||||||
import * as decoding from '../../lib/decoding.mjs'
|
import * as decoding from '../../lib/decoding.js'
|
||||||
import * as idb from '../../lib/idb.mjs'
|
import * as idb from '../../lib/idb.js'
|
||||||
import * as globals from '../../lib/globals.mjs'
|
import * as globals from '../../lib/globals.js'
|
||||||
import * as message from './message.mjs'
|
import * as message from './message.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get 'client-unconfirmed' store from transaction
|
* Get 'client-unconfirmed' store from transaction
|
||||||
@@ -262,7 +262,7 @@ export const getRoomMetas = t => {
|
|||||||
result.push({
|
result.push({
|
||||||
room: metakey.slice(5),
|
room: metakey.slice(5),
|
||||||
rsid,
|
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))
|
).then(() => globals.presolve(result))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as globals from '../../lib/globals.mjs'
|
import * as globals from '../../lib/globals.js'
|
||||||
import * as idbactions from './idbactions.mjs'
|
import * as idbactions from './idbactions.js'
|
||||||
import * as test from '../../lib/testing.mjs'
|
import * as test from '../../lib/testing.js'
|
||||||
|
|
||||||
idbactions.deleteDB().then(() => idbactions.openDB()).then(db => {
|
idbactions.deleteDB().then(() => idbactions.openDB()).then(db => {
|
||||||
test.run('update lifetime 1', async (testname) => {
|
test.run('update lifetime 1', async (testname) => {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @module provider/ydb
|
* @module provider/ydb
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ydbclient from './YdbClient.mjs'
|
import * as ydbclient from './YdbClient.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
* @module provider/ydb
|
* @module provider/ydb
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as encoding from './encoding.mjs'
|
import * as encoding from './encoding.js'
|
||||||
import * as decoding from './decoding.mjs'
|
import * as decoding from './decoding.js'
|
||||||
import * as idbactions from './idbactions.mjs'
|
import * as idbactions from './idbactions.js'
|
||||||
import * as logging from './logging.mjs'
|
import * as logging from './logging.js'
|
||||||
import * as bc from './broadcastchannel.mjs'
|
import * as bc from './broadcastchannel.js'
|
||||||
|
|
||||||
/* make sure to update message.go in ydb when updating these values.. */
|
/* make sure to update message.go in ydb when updating these values.. */
|
||||||
export const MESSAGE_UPDATE = 0 // TODO: rename host_unconfirmed?
|
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 babel from 'rollup-plugin-babel'
|
||||||
import uglify from 'rollup-plugin-uglify-es'
|
import uglify from 'rollup-plugin-uglify-es'
|
||||||
|
|
||||||
|
// set this to [] to disable obfuscation
|
||||||
|
const minificationPlugins = process.env.PRODUCTION ? [babel(), uglify()] : []
|
||||||
|
|
||||||
export default [{
|
export default [{
|
||||||
input: './index.mjs',
|
input: './index.js',
|
||||||
output: [{
|
output: [{
|
||||||
name: 'Y',
|
name: 'Y',
|
||||||
file: 'build/yjs.js',
|
file: 'build/yjs.js',
|
||||||
@@ -12,9 +15,9 @@ export default [{
|
|||||||
sourcemap: true
|
sourcemap: true
|
||||||
}]
|
}]
|
||||||
}, {
|
}, {
|
||||||
input: 'tests/index.mjs',
|
input: 'tests/index.js',
|
||||||
output: {
|
output: {
|
||||||
file: 'build/y.test.mjs',
|
file: 'build/y.test.js',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
name: 'ytests',
|
name: 'ytests',
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
@@ -27,7 +30,7 @@ export default [{
|
|||||||
commonjs()
|
commonjs()
|
||||||
]
|
]
|
||||||
}, {
|
}, {
|
||||||
input: './examples/prosemirror.mjs',
|
input: './examples/prosemirror.js',
|
||||||
output: {
|
output: {
|
||||||
name: 'prosemirror',
|
name: 'prosemirror',
|
||||||
file: 'examples/build/prosemirror.js',
|
file: 'examples/build/prosemirror.js',
|
||||||
@@ -39,36 +42,28 @@ export default [{
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
module: true
|
module: true
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs()
|
||||||
babel(),
|
].concat(minificationPlugins)
|
||||||
uglify()
|
|
||||||
]
|
|
||||||
}, {
|
}, {
|
||||||
input: './examples/dom.mjs',
|
input: './examples/dom.js',
|
||||||
output: {
|
output: {
|
||||||
name: 'dom',
|
name: 'dom',
|
||||||
file: 'examples/build/dom.js',
|
file: 'examples/build/dom.js',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: minificationPlugins
|
||||||
babel(),
|
|
||||||
uglify()
|
|
||||||
]
|
|
||||||
}, {
|
}, {
|
||||||
input: './examples/textarea.mjs',
|
input: './examples/textarea.js',
|
||||||
output: {
|
output: {
|
||||||
name: 'textarea',
|
name: 'textarea',
|
||||||
file: 'examples/build/textarea.js',
|
file: 'examples/build/textarea.js',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: minificationPlugins
|
||||||
babel(),
|
|
||||||
uglify()
|
|
||||||
]
|
|
||||||
}, {
|
}, {
|
||||||
input: './examples/quill.mjs',
|
input: './examples/quill.js',
|
||||||
output: {
|
output: {
|
||||||
name: 'textarea',
|
name: 'textarea',
|
||||||
file: 'examples/build/quill.js',
|
file: 'examples/build/quill.js',
|
||||||
@@ -80,8 +75,6 @@ export default [{
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
module: true
|
module: true
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs()
|
||||||
babel(),
|
].concat(minificationPlugins)
|
||||||
uglify()
|
|
||||||
]
|
|
||||||
}]
|
}]
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getStructReference } from '../utils/structReferences.mjs'
|
import { getStructReference } from '../utils/structReferences.js'
|
||||||
import * as ID from '../utils/ID.mjs'
|
import * as ID from '../utils/ID.js'
|
||||||
import { stringifyID } from '../protocols/syncProtocol.mjs'
|
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
||||||
import { writeStructToTransaction } from '../utils/Transaction.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
// import { Item } from './Item.js' // eslint-disable-line
|
||||||
import { Item } from './Item.mjs' // eslint-disable-line
|
// import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
import { deleteItemRange } from '../utils/structManipulation.js'
|
||||||
import { deleteItemRange } from '../utils/structManipulation.mjs'
|
import * as stringify from '../utils/structStringify.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
@@ -99,6 +99,6 @@ export class Delete {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
_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
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getStructReference } from '../utils/structReferences.mjs'
|
import { getStructReference } from '../utils/structReferences.js'
|
||||||
import * as ID from '../utils/ID.mjs'
|
import * as ID from '../utils/ID.js'
|
||||||
import { writeStructToTransaction } from '../utils/Transaction.mjs'
|
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
// import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
|
||||||
// TODO should have the same base class as Item
|
// TODO should have the same base class as Item
|
||||||
export class GC {
|
export class GC {
|
||||||
@@ -19,6 +19,10 @@ export class GC {
|
|||||||
this._length = 0
|
this._length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _redone () {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
get _deleted () {
|
get _deleted () {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,30 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getStructReference } from '../utils/structReferences.mjs'
|
import { getStructReference } from '../utils/structReferences.js'
|
||||||
import * as ID from '../utils/ID.mjs'
|
import * as ID from '../utils/ID.js'
|
||||||
import { Delete } from './Delete.mjs'
|
import { Delete } from './Delete.js'
|
||||||
import { transactionTypeChanged, writeStructToTransaction } from '../utils/Transaction.mjs'
|
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
||||||
import { GC } from './GC.mjs'
|
import { GC } from './GC.js'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { Y } from '../utils/Y.mjs'
|
// import { Type } from './Type.js' // eslint-disable-line
|
||||||
import { Type } from './Type.mjs' // 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
|
* @private
|
||||||
@@ -113,6 +128,30 @@ export class Item {
|
|||||||
this._redone = null
|
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)
|
* 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.
|
* Redoes the effect of this operation.
|
||||||
*
|
*
|
||||||
* @param {Y} y The Yjs instance.
|
* @param {Y} y The Yjs instance.
|
||||||
* @param {Array<Item>} redoitems
|
* @param {Set<Item>} redoitems
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@@ -135,7 +174,7 @@ export class Item {
|
|||||||
if (this._redone !== null) {
|
if (this._redone !== null) {
|
||||||
return this._redone
|
return this._redone
|
||||||
}
|
}
|
||||||
if (this._parent instanceof Y) {
|
if (!(this._parent instanceof Item)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let struct = this._copy()
|
let struct = this._copy()
|
||||||
@@ -251,16 +290,22 @@ export class Item {
|
|||||||
*/
|
*/
|
||||||
_delete (y, createDelete = true, gcChildren) {
|
_delete (y, createDelete = true, gcChildren) {
|
||||||
if (!this._deleted) {
|
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
|
this._deleted = true
|
||||||
y.ds.mark(this._id, this._length, false)
|
y.ds.mark(this._id, this._length, false)
|
||||||
let del = new Delete()
|
let del = new Delete()
|
||||||
del._targetID = this._id
|
del._targetID = this._id
|
||||||
del._length = this._length
|
del._length = len
|
||||||
if (createDelete) {
|
if (createDelete) {
|
||||||
// broadcast and persists Delete
|
// broadcast and persists Delete
|
||||||
del._integrate(y, true)
|
del._integrate(y, true)
|
||||||
}
|
}
|
||||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
transactionTypeChanged(y, parent, this._parentSub)
|
||||||
y._transaction.deletedStructs.add(this)
|
y._transaction.deletedStructs.add(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +455,10 @@ export class Item {
|
|||||||
right._left = this
|
right._left = this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// adjust the length of parent
|
||||||
|
if (parentSub === null && parent.length !== undefined && this._countable) {
|
||||||
|
parent.length += this._length
|
||||||
|
}
|
||||||
if (parent._deleted) {
|
if (parent._deleted) {
|
||||||
this._delete(y, false, true)
|
this._delete(y, false, true)
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item } from './Item.mjs'
|
import { Item } from './Item.js'
|
||||||
import { logItemHelper } from '../protocols/syncProtocol.mjs'
|
import * as stringify from '../utils/structStringify.js'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
|
||||||
export class ItemEmbed extends Item {
|
export class ItemEmbed extends Item {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -44,6 +44,6 @@ export class ItemEmbed extends Item {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
_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
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item } from './Item.mjs'
|
import { Item } from './Item.js'
|
||||||
import { logItemHelper } from '../protocols/syncProtocol.mjs'
|
import * as stringify from '../utils/structStringify.js'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
|
||||||
export class ItemFormat extends Item {
|
export class ItemFormat extends Item {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -51,6 +51,6 @@ export class ItemFormat extends Item {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
_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
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item, splitHelper } from './Item.mjs'
|
import { Item, splitHelper } from './Item.js'
|
||||||
import { logItemHelper } from '../protocols/syncProtocol.mjs'
|
import * as stringify from '../utils/structStringify.js'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
|
||||||
export class ItemJSON extends Item {
|
export class ItemJSON extends Item {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -19,7 +19,8 @@ export class ItemJSON extends Item {
|
|||||||
return struct
|
return struct
|
||||||
}
|
}
|
||||||
get _length () {
|
get _length () {
|
||||||
return this._content.length
|
const c = this._content
|
||||||
|
return c !== null ? c.length : 0
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param {Y} y
|
* @param {Y} y
|
||||||
@@ -46,11 +47,11 @@ export class ItemJSON extends Item {
|
|||||||
*/
|
*/
|
||||||
_toBinary (encoder) {
|
_toBinary (encoder) {
|
||||||
super._toBinary(encoder)
|
super._toBinary(encoder)
|
||||||
let len = this._content.length
|
const len = this._length
|
||||||
encoding.writeVarUint(encoder, len)
|
encoding.writeVarUint(encoder, len)
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
let encoded
|
let encoded
|
||||||
let content = this._content[i]
|
const content = this._content[i]
|
||||||
if (content === undefined) {
|
if (content === undefined) {
|
||||||
encoded = 'undefined'
|
encoded = 'undefined'
|
||||||
} else {
|
} else {
|
||||||
@@ -66,7 +67,7 @@ export class ItemJSON extends Item {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
_logString () {
|
||||||
return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
|
return stringify.logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
|
||||||
}
|
}
|
||||||
_splitAt (y, diff) {
|
_splitAt (y, diff) {
|
||||||
if (diff === 0) {
|
if (diff === 0) {
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item, splitHelper } from './Item.mjs'
|
import { Item, splitHelper } from './Item.js'
|
||||||
import { logItemHelper } from '../protocols/syncProtocol.mjs'
|
import * as stringify from '../utils/structStringify.js'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
|
||||||
export class ItemString extends Item {
|
export class ItemString extends Item {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -44,7 +44,7 @@ export class ItemString extends Item {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
_logString () {
|
||||||
return logItemHelper('ItemString', this, `content:"${this._content}"`)
|
return stringify.logItemHelper('ItemString', this, `content:"${this._content}"`)
|
||||||
}
|
}
|
||||||
_splitAt (y, diff) {
|
_splitAt (y, diff) {
|
||||||
if (diff === 0) {
|
if (diff === 0) {
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item } from './Item.mjs'
|
import { Item } from './Item.js'
|
||||||
import { EventHandler } from '../utils/EventHandler.mjs'
|
import { EventHandler } from '../utils/EventHandler.js'
|
||||||
import { createID } from '../utils/ID.mjs'
|
import { createID } from '../utils/ID.js'
|
||||||
import { YEvent } from '../utils/YEvent.mjs'
|
import { YEvent } from '../utils/YEvent.js'
|
||||||
import { Y } from '../utils/Y.mjs' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
|
||||||
// restructure children as if they were inserted one after another
|
// restructure children as if they were inserted one after another
|
||||||
const integrateChildren = (y, start) => {
|
const integrateChildren = (y, start) => {
|
||||||
@@ -57,6 +57,17 @@ export class Type extends Item {
|
|||||||
this._deepEventHandler = new EventHandler()
|
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.
|
* Compute the path from this type to the specified target.
|
||||||
*
|
*
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { test } from 'cutest'
|
import { test } from 'cutest'
|
||||||
import * as random from '../lib/prng/prng.mjs'
|
import * as random from '../lib/prng/prng.js'
|
||||||
import { DeleteStore } from '../utils/DeleteStore.mjs'
|
import { DeleteStore } from '../utils/DeleteStore.js'
|
||||||
import * as ID from '../utils/ID.mjs'
|
import * as ID from '../utils/ID.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a DS to an array of length 10.
|
* Converts a DS to an array of length 10.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { test } from 'cutest'
|
import { test } from 'cutest'
|
||||||
import { simpleDiff } from '../lib/diff.mjs'
|
import { simpleDiff } from '../lib/diff.js'
|
||||||
import * as random from '../lib/prng/prng.mjs'
|
import * as random from '../lib/prng/prng.js'
|
||||||
|
|
||||||
function runDiffTest (t, a, b, expected) {
|
function runDiffTest (t, a, b, expected) {
|
||||||
let result = simpleDiff(a, b)
|
let result = simpleDiff(a, b)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { test } from 'cutest'
|
import { test } from 'cutest'
|
||||||
import { generateRandomUint32 } from '../utils/generateRandomUint32.mjs'
|
import { generateRandomUint32 } from '../utils/generateRandomUint32.js'
|
||||||
import * as encoding from '../lib/encoding.mjs'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.mjs'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import * as random from '../lib/prng/prng.mjs'
|
import * as random from '../lib/prng/prng.js'
|
||||||
|
|
||||||
function testEncoding (t, write, read, val) {
|
function testEncoding (t, write, read, val) {
|
||||||
let encoder = encoding.createEncoder()
|
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