Compare commits

...

10 Commits

Author SHA1 Message Date
Kevin Jahns
1a79e429ed 13.0.0-75 2018-12-11 19:49:50 +01:00
Kevin Jahns
04066a5678 permission protocol + reduce circular dependencies 2018-12-11 19:49:21 +01:00
Kevin Jahns
e09ef15349 13.0.0-74 2018-12-04 18:07:04 +01:00
Kevin Jahns
3d70eee959 item: increase parent length only if parentSub=null 2018-12-03 23:09:59 +01:00
Kevin Jahns
582095e5a3 improved granularity of prosemirror binding 2018-12-03 17:09:00 +01:00
Kevin Jahns
c9ea3a412e more efficient length computing 2018-11-28 13:20:14 +01:00
Kevin Jahns
a2c51c36e9 implement generic broadcastchannel and apply it to websocket provider 2018-11-27 18:29:25 +01:00
Kevin Jahns
ab3dba5b06 add source file info to examples 2018-11-27 15:24:58 +01:00
Kevin Jahns
3ddff186c2 back to .js extension 2018-11-27 14:59:24 +01:00
Kevin Jahns
9bd199a6e7 add description to each example 2018-11-27 00:57:15 +01:00
137 changed files with 2372 additions and 1471 deletions

View File

@@ -5,7 +5,7 @@
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["./types", "./utils/UndoManager.mjs", "./utils/Y.mjs", "./provider", "./bindings"],
"include": ["./types", "./utils/UndoManager.js", "./utils/Y.js", "./provider", "./bindings"],
"includePattern": ".js$"
},
"plugins": [

View File

@@ -9,10 +9,13 @@ Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the
```json
{
"checkJs": true,
"compilerOptions": {
"allowJs": true,
"checkJs": true,
..
},
"include": [
"./node_modules/yjs/"
]
..
}
```

1
bindings/dom.js Normal file
View File

@@ -0,0 +1 @@
export * from './dom/DomBinding.js'

View File

@@ -1 +0,0 @@
export * from './dom/DomBinding.mjs'

View File

@@ -4,14 +4,14 @@
/* global MutationObserver, getSelection */
import { fromRelativePosition } from '../../utils/relativePosition.mjs'
import { createMutex } from '../../lib/mutex.mjs'
import { createAssociation, removeAssociation } from './util.mjs'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.mjs'
import { defaultFilter, applyFilterOnType } from './filter.mjs'
import { typeObserver } from './typeObserver.mjs'
import { domObserver } from './domObserver.mjs'
import { YXmlFragment } from '../../types/YXmlElement.mjs' // eslint-disable-line
import { fromRelativePosition } from '../../utils/relativePosition.js'
import { createMutex } from '../../lib/mutex.js'
import { createAssociation, removeAssociation } from './util.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
import { defaultFilter, applyFilterOnType } from './filter.js'
import { typeObserver } from './typeObserver.js'
import { domObserver } from './domObserver.js'
import { YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
/**
* @callback DomFilter

View File

@@ -2,13 +2,13 @@
* @module bindings/dom
*/
import { YXmlHook } from '../../types/YXmlHook.mjs'
import { YXmlHook } from '../../types/YXmlHook.js'
import {
iterateUntilUndeleted,
removeAssociation,
insertNodeHelper } from './util.mjs'
import { simpleDiff } from '../../lib/diff.mjs'
import { YXmlFragment } from '../../types/YXmlElement.mjs'
insertNodeHelper } from './util.js'
import { simpleDiff } from '../../lib/diff.js'
import { YXmlFragment } from '../../types/YXmlElement.js'
/**
* 1. Check if any of the nodes was deleted
@@ -20,6 +20,8 @@ import { YXmlFragment } from '../../types/YXmlElement.mjs'
* recreate a new yxml element that is bound to that node.
* You can detect that a node was moved because expectedId
* !== actualId in the list
*
* @function
* @private
*/
const applyChangesFromDom = (binding, dom, yxml, _document) => {
@@ -85,6 +87,7 @@ const applyChangesFromDom = (binding, dom, yxml, _document) => {
/**
* @private
* @function
*/
export function domObserver (mutations, _document) {
this._mutualExclude(() => {

View File

@@ -3,12 +3,12 @@
*/
/* eslint-env browser */
import { YXmlText } from '../../types/YXmlText.mjs'
import { YXmlHook } from '../../types/YXmlHook.mjs'
import { YXmlElement } from '../../types/YXmlElement.mjs'
import { createAssociation, domsToTypes } from './util.mjs'
import { filterDomAttributes, defaultFilter } from './filter.mjs'
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
import { YXmlText } from '../../types/YXmlText.js'
import { YXmlHook } from '../../types/YXmlHook.js'
import { YXmlElement } from '../../types/YXmlElement.js'
import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes, defaultFilter } from './filter.js'
import { DomBinding } from './DomBinding.js' // eslint-disable-line
/**
* @callback DomFilter
@@ -20,6 +20,7 @@ import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
/**
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
*
* @function
* @param {Element|Text} element The DOM Element
* @param {?Document} _document Optional. Provide the global document object
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks

View File

@@ -2,14 +2,15 @@
* @module bindings/dom
*/
import { Y } from '../../utils/Y.mjs' // eslint-disable-line
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.mjs' // eslint-disable-line
import { isParentOf } from '../../utils/isParentOf.mjs'
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
import { Y } from '../../utils/Y.js' // eslint-disable-line
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
import { isParentOf } from '../../utils/isParentOf.js'
import { DomBinding } from './DomBinding.js' // eslint-disable-line
/**
* Default filter method (does nothing).
*
* @function
* @param {String} nodeName The nodeName of the element
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
* @return {Map | null} The allowed attributes or null, if the element should be
@@ -21,7 +22,10 @@ export const defaultFilter = (nodeName, attrs) => {
}
/**
*
* @private
* @function
* @param {Element} dom
* @param {Function} filter
*/
export const filterDomAttributes = (dom, filter) => {
const attrs = new Map()
@@ -35,11 +39,11 @@ export const filterDomAttributes = (dom, filter) => {
/**
* Applies a filter on a type.
*
* @private
* @function
* @param {Y} y The Yjs instance.
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
*
* @private
*/
export const applyFilterOnType = (y, binding, type) => {
if (isParentOf(binding.type, type) && type instanceof YXmlElement) {

View File

@@ -4,10 +4,13 @@
/* globals getSelection */
import { getRelativePosition } from '../../utils/relativePosition.mjs'
import { getRelativePosition } from '../../utils/relativePosition.js'
let relativeSelection = null
/**
* @private
*/
const _getCurrentRelativeSelection = domBinding => {
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
const baseNodeType = domBinding.domToType.get(baseNode)
@@ -21,8 +24,14 @@ const _getCurrentRelativeSelection = domBinding => {
return null
}
/**
* @private
*/
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
/**
* @private
*/
export const beforeTransactionSelectionFixer = domBinding => {
relativeSelection = getCurrentRelativeSelection(domBinding)
}
@@ -30,6 +39,7 @@ export const beforeTransactionSelectionFixer = domBinding => {
/**
* Reset the browser range after every transaction.
* This prevents any collapsing issues with the local selection.
*
* @private
*/
export const afterTransactionSelectionFixer = domBinding => {

View File

@@ -5,9 +5,9 @@
/* eslint-env browser */
/* global getSelection */
import { YXmlText } from '../../types/YXmlText.mjs'
import { YXmlHook } from '../../types/YXmlHook.mjs'
import { removeDomChildrenUntilElementFound } from './util.mjs'
import { YXmlText } from '../../types/YXmlText.js'
import { YXmlHook } from '../../types/YXmlHook.js'
import { removeDomChildrenUntilElementFound } from './util.js'
const findScrollReference = scrollingElement => {
if (scrollingElement !== null) {

View File

@@ -2,8 +2,8 @@
* @module bindings/dom
*/
import { domToType } from './domToType.mjs'
import { DomBinding } from './DomBinding.mjs' // eslint-disable-line
import { domToType } from './domToType.js'
import { DomBinding } from './DomBinding.js' // eslint-disable-line
/**
* Iterates items until an undeleted item is found.
@@ -22,6 +22,7 @@ export const iterateUntilUndeleted = item => {
* type).
*
* @private
* @function
* @param {DomBinding} domBinding The binding object
* @param {Element} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
@@ -37,6 +38,7 @@ export const removeAssociation = (domBinding, dom, type) => {
* type).
*
* @private
* @function
* @param {DomBinding} domBinding The binding object
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
* @param {YXmlFragment|YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
@@ -54,6 +56,7 @@ export const createAssociation = (domBinding, dom, type) => {
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
*
* @private
* @function
* @param {DomBinding} domBinding The binding object
* @param {Element} oldDom The existing dom
* @param {Element} newDom The new dom object
@@ -74,6 +77,7 @@ export const switchAssociation = (domBinding, oldDom, newDom) => {
* specified position.
*
* @private
* @function
* @param {YXmlElement} type The type in which to insert DOM elements.
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
* inserted after this node. Set null to insert at
@@ -101,6 +105,7 @@ export const domsToTypes = (doms, _document, hooks, filter, binding) => {
/**
* @private
* @function
*/
export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, binding) => {
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
@@ -115,6 +120,7 @@ export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, bindi
* Remove children until `elem` is found.
*
* @private
* @function
* @param {Element} parent The parent of `elem` and `currentChild`.
* @param {Node} currentChild Start removing elements with `currentChild`. If
* `currentChild` is `elem` it won't be removed.

633
bindings/prosemirror.js Normal file
View 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()

View File

@@ -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)))
})
}

View File

@@ -2,7 +2,7 @@
* @module bindings/quill
*/
import { createMutex } from '../lib/mutex.mjs'
import { createMutex } from '../lib/mutex.js'
const typeObserver = function (event) {
const quill = this.target
@@ -53,7 +53,6 @@ export class QuillBinding {
* @private
*/
this._mutualExclude = createMutex()
// Set initial value.
quill.setContents(textType.toDelta(), 'yjs')
// Observers are handled by this class.

View File

@@ -2,9 +2,9 @@
* @module bindings/textarea
*/
import { simpleDiff } from '../lib/diff.mjs'
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.mjs'
import { createMutex } from '../lib/mutex.mjs'
import { simpleDiff } from '../lib/diff.js'
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.js'
import { createMutex } from '../lib/mutex.js'
function typeObserver () {
this._mutualExclude(() => {

View File

@@ -10,13 +10,18 @@
</style>
</head>
<body>
<p>This example shows how to bind a YXmlFragment type to an arbitrary DOM element. We set the DOM element to contenteditable so it basically behaves like a very powerful rich-text editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<hr>
<div class="code-html">
<div id="content" contenteditable=""></div>
<div id="content" contenteditable=""></div>
</div>
<!-- The actual source file for the following code is found in ./dom.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/dom.js">
import * as Y from 'yjs/index.mjs'
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
import { DomBinding } from 'yjs/bindings/dom.mjs'
import * as Y from 'yjs/index.js'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { DomBinding } from 'yjs/bindings/dom.js'
const provider = new WebsocketProvider('wss://api.yjs.website')
const ydocument = provider.get('dom')

View File

@@ -1,8 +1,10 @@
import * as Y from '../index.mjs'
import { WebsocketProvider } from '../provider/websocket.mjs'
import { DomBinding } from '../bindings/dom.mjs'
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { DomBinding } from '../bindings/dom.js'
const provider = new WebsocketProvider('wss://api.yjs.website')
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('dom')
const type = ydocument.define('xml', Y.XmlFragment)
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })

View 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'

View File

@@ -16,6 +16,13 @@
font-weight: bold;
}
.ProseMirror img { max-width: 100px }
/* this is a rough fix for the first cursor position when the first paragraph is empty */
.ProseMirror > .ProseMirror-yjs-cursor:first-child {
margin-top: 16px;
}
.ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child {
margin-top: 16px
}
.ProseMirror-yjs-cursor {
position: absolute;
border-left: black;
@@ -41,13 +48,17 @@
</style>
</head>
<body>
<p>This example shows how to bind a YXmlFragment type to a <a href="http://prosemirror.net">Prosemirror</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<div class="code-html">
<div id="editor" style="margin-bottom: 23px"></div>
<div style="display: none" id="content"></div>
<div id="editor" style="margin-bottom: 23px"></div>
<div style="display: none" id="content"></div>
</div>
<!-- The actual source file for the following code is found in ./prosemirror.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/prosemirror.js">
import * as Y from 'yjs'
import { WebsocketProvider } from '../provider/websocket.mjs'
import { WebsocketProvider } from '../provider/websocket.js'
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
import { EditorState } from 'prosemirror-state'

View File

@@ -1,6 +1,8 @@
import * as Y from '../index.mjs'
import { WebsocketProvider } from '../provider/websocket.mjs'
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror.js'
import * as conf from './exampleConfig.js'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
@@ -8,7 +10,7 @@ import { DOMParser } from 'prosemirror-model'
import { schema } from 'prosemirror-schema-basic'
import { exampleSetup } from 'prosemirror-example-setup'
const provider = new WebsocketProvider('wss://api.yjs.website')
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror')
const type = ydocument.define('prosemirror', Y.XmlFragment)

View File

@@ -1,21 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Prosemirror Example</title>
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
</head>
<title>Yjs Quill Example</title>
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css">
</head>
<body>
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<p>This example shows how to bind a YText type to <a href="https://quilljs.com">Quill</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<div class="code-html">
<div id="quill-container">
<div id="quill">
</div>
</div>
<div id="quill-container">
<div id="quill">
</div>
</div>
</div>
<!-- The actual source file for the following code is found in ./quill.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/quill.js">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
import { QuillBinding } from 'yjs/bindings/quill.mjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { QuillBinding } from 'yjs/bindings/quill.js'
import Quill from 'quill'

View File

@@ -1,10 +1,12 @@
import * as Y from '../index.mjs'
import { WebsocketProvider } from '../provider/websocket.mjs'
import { QuillBinding } from '../bindings/quill.mjs'
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { QuillBinding } from '../bindings/quill.js'
import * as conf from './exampleConfig.js'
import Quill from 'quill'
const provider = new WebsocketProvider('wss://api.yjs.website')
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('quill')
const ytext = ydocument.define('quill', Y.Text)

View File

@@ -2,16 +2,19 @@
<html>
<head>
<title>Yjs Textarea Example</title>
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
</head>
<body>
<p>This example shows how to bind a YText type to a DOM Textarea.</p>
<p>The content of this textarea is shared with every client who visits this domain.</p>
<div class="code-html">
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<!-- The actual source file for the following code is found in ./textarea.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/textarea.js">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.mjs'
import { TextareaBinding } from 'yjs/bindings/textarea.mjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { TextareaBinding } from 'yjs/bindings/textarea.js'
const provider = new WebsocketProvider('wss://api.yjs.website')
const ydocument = provider.get('textarea')

View File

@@ -1,8 +1,10 @@
import * as Y from '../index.mjs'
import { WebsocketProvider } from '../provider/websocket.mjs'
import { TextareaBinding } from '../bindings/textarea.mjs'
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { TextareaBinding } from '../bindings/textarea.js'
const provider = new WebsocketProvider('wss://api.yjs.website')
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('textarea')
const type = ydocument.define('textarea', Y.Text)
const textarea = document.querySelector('textarea')

View File

@@ -25,7 +25,7 @@
<div id="aceContainer"></div>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/ace-builds/src/ace.js"></script>
<script src="./index.js"></script>

View File

@@ -13,7 +13,7 @@
<input type="submit" value="Send">
</form>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -6,7 +6,7 @@
<div id="codeMirrorContainer"></div>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">

View File

@@ -13,7 +13,7 @@
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="./index.js"></script>
</body>

View File

@@ -2,7 +2,7 @@
<html>
</head>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="./index.js"></script>
<style>

View File

@@ -1,12 +1,12 @@
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
import Y from '../../src/Y.mjs'
import DomBinding from '../../bindings/DomBinding/DomBinding.mjs'
import UndoManager from '../../src/Util/UndoManager.mjs'
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
import YXmlText from '../../src/Types/YXml/YXmlText.mjs'
import YXmlElement from '../../src/Types/YXml/YXmlElement.mjs'
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.mjs'
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
import Y from '../../src/Y.js'
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
import UndoManager from '../../src/Util/UndoManager.js'
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
import YXmlText from '../../src/Types/YXml/YXmlText.js'
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.js'
const connector = new YWebsocketsConnector()
const persistence = new YIndexdDBPersistence()

View File

@@ -17,7 +17,7 @@
}
</style>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -17,7 +17,7 @@
</g>
</svg>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.js"></script>
<script src="./index.js"></script>
</body>

View File

@@ -14,7 +14,7 @@
}
</style>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.mjs'></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="./index.js"></script>
</body>

View File

@@ -1,9 +1,9 @@
/* eslint-env browser */
import { createYdbClient } from '../../YdbClient/index.mjs'
import Y from '../../src/Y.dist.mjs'
import * as ydb from '../../YdbClient/YdbClient.mjs'
import DomBinding from '../../bindings/DomBinding/DomBinding.mjs'
import { createYdbClient } from '../../YdbClient/index.js'
import Y from '../../src/Y.dist.js'
import * as ydb from '../../YdbClient/YdbClient.js'
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0

55
index.js Normal file
View 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)

View File

@@ -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
View 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
}

View File

@@ -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
View 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))
}

View File

@@ -4,7 +4,7 @@
/* global Buffer */
import * as globals from './globals.mjs'
import * as globals from './globals.js'
/**
* A Decoder handles the decoding of an ArrayBuffer.
@@ -148,6 +148,21 @@ export const readVarUint = decoder => {
}
}
/**
* Look ahead and read varUint without incrementing position
*
* @function
* @param {Decoder} decoder
* @return {number}
*/
export const peekVarUint = decoder => {
let pos = decoder.pos
let s = readVarUint(decoder)
decoder.pos = pos
return s
}
/**
* Read string of variable length
* * varUint is used to store the length of the string
@@ -189,3 +204,4 @@ export const peekVarString = decoder => {
decoder.pos = pos
return s
}

View File

@@ -1,7 +1,7 @@
/**
* @module encoding
*/
import * as globals from './globals.mjs'
import * as globals from './globals.js'
const bits7 = 0b1111111
const bits8 = 0b11111111

View File

@@ -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.

View File

@@ -58,8 +58,6 @@ export const until = (timeout, check) => createPromise((resolve, reject) => {
export const error = description => new Error(description)
export const max = (a, b) => a > b ? a : b
/**
* @param {number} t Time to wait
* @return {Promise} Promise that is resolved after t ms

View File

@@ -1,10 +1,10 @@
/**
* @module idb
* @module lib/idb
*/
/* eslint-env browser */
import * as globals from './globals.mjs'
import * as globals from './globals.js'
/*
* IDB Request to Promise transformer

View File

@@ -1,6 +1,6 @@
import * as test from './testing.mjs'
import * as idb from './idb.mjs'
import * as logging from './logging.mjs'
import * as test from './testing.js'
import * as idb from './idb.js'
import * as logging from './logging.js'
const initTestDB = db => idb.createStores(db, [['test']])
const testDBName = 'idb-test'

View File

@@ -2,7 +2,7 @@
* @module logging
*/
import * as globals from './globals.mjs'
import * as globals from './globals.js'
let date = new Date().getTime()

28
lib/math.js Normal file
View 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

View File

@@ -1,4 +0,0 @@
/**
* @module math
*/
export const floor = Math.floor

14
lib/object.js Normal file
View 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
}

View File

@@ -2,10 +2,10 @@
* @module prng
*/
import { Mt19937 } from './Mt19937.mjs'
import { Xoroshiro128plus } from './Xoroshiro128plus.mjs'
import { Xorshift32 } from './Xorshift32.mjs'
import * as time from '../../time.mjs'
import { Mt19937 } from './Mt19937.js'
import { Xoroshiro128plus } from './Xoroshiro128plus.js'
import { Xorshift32 } from './Xorshift32.js'
import * as time from '../../time.js'
const DIAMETER = 300
const NUMBERS = 10000

View File

@@ -2,7 +2,7 @@
* @module prng
*/
import { Xorshift32 } from './Xorshift32.mjs'
import { Xorshift32 } from './Xorshift32.js'
/**
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.

View File

@@ -2,12 +2,12 @@
* @module prng
*/
import * as binary from '../binary.mjs'
import { fromCharCode, fromCodePoint } from '../string.mjs'
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.mjs'
import * as math from '../math.mjs'
import * as binary from '../binary.js'
import { fromCharCode, fromCodePoint } from '../string.js'
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
import * as math from '../math.js'
import { Xoroshiro128plus as DefaultPRNG } from './PRNG/Xoroshiro128plus.mjs'
import { Xoroshiro128plus as DefaultPRNG } from './PRNG/Xoroshiro128plus.js'
/**
* Description of the function

View File

@@ -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)]

View File

@@ -4,14 +4,14 @@
/**
*TODO: enable tests
import * as rt from '../rich-text/formatters.mjs''
import { test } from '../test/test.mjs''
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs''
import Xorshift32 from './PRNG/Xorshift32.mjs''
import MT19937 from './PRNG/Mt19937.mjs''
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs''
import { MAX_SAFE_INTEGER } from '../number/constants.mjs''
import { BIT32 } from '../binary/constants.mjs''
import * as rt from '../rich-text/formatters.js''
import { test } from '../test/test.js''
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.js''
import Xorshift32 from './PRNG/Xorshift32.js''
import MT19937 from './PRNG/Mt19937.js''
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.js''
import { MAX_SAFE_INTEGER } from '../number/constants.js''
import { BIT32 } from '../binary/constants.js''
function init (Gen) {
return {

View File

@@ -2,8 +2,8 @@
* @module testing
*/
import * as logging from './logging.mjs'
import { simpleDiff } from './diff.mjs'
import * as logging from './logging.js'
import { simpleDiff } from './diff.js'
export const run = async (name, f) => {
console.log(`%cStart:%c ${name}`, 'color:blue;', '')

20
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-73",
"version": "13.0.0-75",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -67,12 +67,28 @@
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
"dev": true
},
"@types/events": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
"dev": true
},
"@types/node": {
"version": "6.0.110",
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
"dev": true
},
"@types/ws": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz",
"integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/node": "*"
}
},
"abab": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@@ -5944,7 +5960,7 @@
},
"quill": {
"version": "1.3.6",
"resolved": "http://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
"integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==",
"dev": true,
"requires": {

View File

@@ -1,19 +1,20 @@
{
"name": "yjs",
"version": "13.0.0-73",
"version": "13.0.0-75",
"description": "A ",
"module": "./index.mjs'",
"main": "./build/yjs.js",
"module": "./index.js'",
"sideEffects": false,
"scripts": {
"test": "npm run lint",
"build": "rm -rf build examples/build && rollup -c",
"build": "rm -rf build examples/build && PRODUCTION=1 rollup -c",
"watch": "rollup -wc",
"debug": "concurrently 'rollup -wc' 'cutest-serve build/y.test.js -o'",
"debug": "concurrently 'npm run watch' 'cutest-serve build/y.test.js -o'",
"lint": "standard **/*.js",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
"serve-docs": "npm run docs && serve ./docs/",
"postversion": "npm run build",
"websocket-server": "node --experimental-modules ./provider/websocket/server.mjs",
"websocket-server": "node ./provider/websocket/server.js",
"now-start": "npm run websocket-server"
},
"files": [
@@ -65,6 +66,7 @@
},
"homepage": "http://y-js.org",
"devDependencies": {
"@types/ws": "^6.0.1",
"babel-cli": "^6.26.0",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-transform-regenerator": "^6.26.0",

View File

@@ -1,10 +1,10 @@
/*
import fs from 'fs'
import path from 'path'
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import { createMutex } from '../lib/mutex.mjs'
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.mjs'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { createMutex } from '../lib/mutex.js'
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
function createFilePath (persistence, roomName) {
// TODO: filename checking!

View 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))
}
}
*/

View File

@@ -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))
}
}
*/

View File

@@ -1,7 +1,7 @@
/*
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.mjs'
import { writeStructs } from '../MessageHandler/syncStep1.mjs'
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.mjs'
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
import { writeStructs } from '../MessageHandler/syncStep1.js'
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
export const PERSIST_UPDATE = 0
/**

View 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
View 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))
}
}

View File

@@ -2,16 +2,16 @@
* @module awareness-protocol
*/
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
const messageUsersStateChanged = 0
/**
* @typedef {Object} UserStateUpdate
* @property {number} UserStateUpdate.userID
* @property {Object} state
* @property {Object} UserStateUpdate.state
*/
/**
@@ -91,13 +91,22 @@ export const readAwarenessMessage = (decoder, y) => {
}
}
/**
* @typedef {Object} UserState
* @property {number} UserState.userID
* @property {any} UserState.state
*/
/**
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @return {Array<UserState>} Array of state updates
*/
export const forwardAwarenessMessage = (decoder, encoder) => {
let s = []
switch (decoding.readVarUint(decoder)) {
case messageUsersStateChanged:
return forwardUsersStateChange(decoder, encoder)
s = forwardUsersStateChange(decoder, encoder)
}
return s
}

View File

@@ -2,14 +2,15 @@
* @module sync-protocol
*/
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import * as ID from '../utils/ID.mjs'
import { getStruct } from '../utils/structReferences.mjs'
import { deleteItemRange } from '../utils/structManipulation.mjs'
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.mjs'
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import { Item } from '../structs/Item.mjs'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import * as ID from '../utils/ID.js'
import { getStruct } from '../utils/structReferences.js'
import { deleteItemRange } from '../utils/structManipulation.js'
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
import { Item } from '../structs/Item.js'
import * as stringify from '../utils/structStringify.js'
/**
* @typedef {Map<number, number>} StateSet
@@ -40,9 +41,9 @@ import { Item } from '../structs/Item.mjs'
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
*/
const messageYjsSyncStep1 = 0
const messageYjsSyncStep2 = 1
const messageYjsUpdate = 2
export const messageYjsSyncStep1 = 0
export const messageYjsSyncStep2 = 1
export const messageYjsUpdate = 2
/**
* Stringifies a message-encoded Delete Set.
@@ -83,7 +84,7 @@ export const writeDeleteSet = (encoder, y) => {
const gc = n.gc
if (currentUser !== user) {
numberOfUsers++
// a new user was foundimport { StateSet } from '../Store/StateStore.mjs' // eslint-disable-line
// a new user was foundimport { StateSet } from '../Store/StateStore.js' // eslint-disable-line
if (currentUser !== null) { // happens on first iteration
encoding.setUint32(encoder, lastLenPos, currentLength)
@@ -234,50 +235,6 @@ export const readStateSet = decoder => {
return ss
}
/**
* Stringify an item id.
*
* @param {ID.ID | ID.RootID} id
* @return {string}
*/
export const stringifyID = id => id instanceof ID.ID ? `(${id.user},${id.clock})` : `(${id.name},${id.type})`
/**
* Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent).
*
* @param {Item | Y | null} item
* @return {string}
*/
export const stringifyItemID = item => {
let result
if (item === null) {
result = '()'
} else if (item instanceof Item) {
result = stringifyID(item._id)
} else {
// must be a Yjs instance
// Don't include Y in this module, so we prevent circular dependencies.
result = 'y'
}
return result
}
/**
* Helper utility to convert an item to a readable format.
*
* @param {String} name The name of the item class (YText, ItemString, ..).
* @param {Item} item The item instance.
* @param {String} [append] Additional information to append to the returned
* string.
* @return {String} A readable string that represents the item object.
*
*/
export const logItemHelper = (name, item, append) => {
const left = item._left !== null ? stringifyID(item._left._lastId) : '()'
const origin = item._origin !== null ? stringifyID(item._origin._lastId) : '()'
return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item._right)},parent:${stringifyItemID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
@@ -293,7 +250,7 @@ export const stringifyStructs = (decoder, y) => {
let missing = struct._fromBinary(y, decoder)
let logMessage = ' ' + struct._logString()
if (missing.length > 0) {
logMessage += ' .. missing: ' + missing.map(stringifyItemID).join(', ')
logMessage += ' .. missing: ' + missing.map(stringify.stringifyItemID).join(', ')
}
str += logMessage + '\n'
}
@@ -410,10 +367,9 @@ export const stringifySyncStep2 = (decoder, y) => {
* Read and apply Structs and then DeleteSet to a y instance.
*
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @param {Y} y
*/
export const readSyncStep2 = (decoder, encoder, y) => {
export const readSyncStep2 = (decoder, y) => {
readStructs(decoder, y)
readDeleteSet(decoder, y)
}
@@ -480,7 +436,7 @@ export const readSyncMessage = (decoder, encoder, y) => {
readSyncStep1(decoder, encoder, y)
break
case messageYjsSyncStep2:
y.transact(() => readSyncStep2(decoder, encoder, y), true)
y.transact(() => readSyncStep2(decoder, y), true)
break
case messageYjsUpdate:
y.transact(() => readUpdate(decoder, y), true)

5
provider/websocket.js Normal file
View File

@@ -0,0 +1,5 @@
/**
* @module provider/websocket
*/
export * from './websocket/WebSocketProvider.js'

View File

@@ -1,5 +0,0 @@
/**
* @module provider/websocket
*/
export * from './websocket/WebSocketProvider.mjs'

View File

@@ -0,0 +1,166 @@
/**
* @module provider/websocket
*/
/* eslint-env browser */
import * as Y from '../../index.js'
import * as bc from '../../lib/broadcastchannel.js'
const messageSync = 0
const messageAwareness = 1
const messageAuth = 2
const reconnectTimeout = 3000
/**
* @param {WebsocketsSharedDocument} doc
* @param {string} reason
*/
const permissionDeniedHandler = (doc, reason) => console.warn(`Permission denied to access ${doc.url}.\n${reason}`)
/**
* @param {WebsocketsSharedDocument} doc
* @param {ArrayBuffer} buf
* @return {Y.encoding.Encoder}
*/
const readMessage = (doc, buf) => {
const decoder = Y.decoding.createDecoder(buf)
const encoder = Y.encoding.createEncoder()
const messageType = Y.decoding.readVarUint(decoder)
switch (messageType) {
case messageSync:
Y.encoding.writeVarUint(encoder, messageSync)
doc.mux(() =>
Y.syncProtocol.readSyncMessage(decoder, encoder, doc)
)
break
case messageAwareness:
Y.awarenessProtocol.readAwarenessMessage(decoder, doc)
break
case messageAuth:
Y.authProtocol.readAuthMessage(decoder, doc, permissionDeniedHandler)
}
return encoder
}
const setupWS = (doc, url) => {
const websocket = new WebSocket(url)
websocket.binaryType = 'arraybuffer'
doc.ws = websocket
websocket.onmessage = event => {
const encoder = readMessage(doc, event.data)
if (Y.encoding.length(encoder) > 1) {
websocket.send(Y.encoding.toBuffer(encoder))
}
}
websocket.onclose = () => {
doc.ws = null
doc.wsconnected = false
doc.emit('status', {
status: 'connected'
})
setTimeout(setupWS, reconnectTimeout, doc, url)
}
websocket.onopen = () => {
doc.wsconnected = true
doc.emit('status', {
status: 'disconnected'
})
// always send sync step 1 when connected
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeSyncStep1(encoder, doc)
websocket.send(Y.encoding.toBuffer(encoder))
// force send stored awareness info
doc.setAwarenessField(null, null)
}
}
const broadcastUpdate = (y, transaction) => {
if (transaction.encodedStructsLen > 0) {
y.mux(() => {
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
const buf = Y.encoding.toBuffer(encoder)
if (y.wsconnected) {
y.ws.send(buf)
}
bc.publish(y.url, buf)
})
}
}
class WebsocketsSharedDocument extends Y.Y {
constructor (url) {
super()
this.url = url
this.wsconnected = false
this.mux = Y.createMutex()
this.ws = null
this._localAwarenessState = {}
this.awareness = new Map()
setupWS(this, url)
this.on('afterTransaction', broadcastUpdate)
this._bcSubscriber = data => {
this.mux(() => {
const encoder = readMessage(this, data)
if (Y.encoding.length(encoder) > 1) {
bc.publish(url, Y.encoding.toBuffer(encoder))
}
})
}
bc.subscribe(url, this._bcSubscriber)
// send sync step1 to bc
this.mux(() => {
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeSyncStep1(encoder, this)
bc.publish(url, Y.encoding.toBuffer(encoder))
})
}
getLocalAwarenessInfo () {
return this._localAwarenessState
}
getAwarenessInfo () {
return this.awareness
}
setAwarenessField (field, value) {
if (field !== null) {
this._localAwarenessState[field] = value
}
if (this.wsconnected) {
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageAwareness)
Y.awarenessProtocol.writeUsersStateChange(encoder, [{ userID: this.userID, state: this._localAwarenessState }])
const buf = Y.encoding.toBuffer(encoder)
this.ws.send(buf)
}
}
}
export class WebsocketProvider {
constructor (url) {
// ensure that url is always ends with /
while (url[url.length - 1] === '/') {
url = url.slice(0, url.length - 1)
}
this.url = url + '/'
/**
* @type {Map<string, WebsocketsSharedDocument>}
*/
this.docs = new Map()
}
/**
* @param {string} name
* @return {WebsocketsSharedDocument}
*/
get (name) {
let doc = this.docs.get(name)
if (doc === undefined) {
doc = new WebsocketsSharedDocument(this.url + name)
}
return doc
}
}

View File

@@ -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
}
}

View File

@@ -1,10 +1,10 @@
/**
* @module provider/websocket
* @module provider/websocket/server
*/
import * as Y from '../../index.mjs'
import WebSocket from 'ws'
import http from 'http'
const Y = require('../../build/yjs.js')
const WebSocket = require('ws')
const http = require('http')
const port = process.env.PORT || 1234
@@ -19,20 +19,21 @@ const docs = new Map()
const messageSync = 0
const messageAwareness = 1
const messageAuth = 2
const afterTransaction = (doc, transaction) => {
if (transaction.encodedStructsLen > 0) {
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageSync)
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
const message = Y.toBuffer(encoder)
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
const message = Y.encoding.toBuffer(encoder)
doc.conns.forEach((_, conn) => conn.send(message))
}
}
class WSSharedDoc extends Y.Y {
constructor () {
super()
super({ gc: true })
this.mux = Y.createMutex()
/**
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
@@ -45,25 +46,25 @@ class WSSharedDoc extends Y.Y {
}
const messageListener = (conn, doc, message) => {
const encoder = Y.createEncoder()
const decoder = Y.createDecoder(message)
const messageType = Y.readVarUint(decoder)
const encoder = Y.encoding.createEncoder()
const decoder = Y.decoding.createDecoder(message)
const messageType = Y.decoding.readVarUint(decoder)
switch (messageType) {
case messageSync:
Y.writeVarUint(encoder, messageSync)
Y.readSyncMessage(decoder, encoder, doc)
if (Y.length(encoder) > 1) {
conn.send(Y.toBuffer(encoder))
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.readSyncMessage(decoder, encoder, doc)
if (Y.encoding.length(encoder) > 1) {
conn.send(Y.encoding.toBuffer(encoder))
}
break
case messageAwareness: {
Y.writeVarUint(encoder, messageAwareness)
const updates = Y.forwardAwarenessMessage(decoder, encoder)
Y.encoding.writeVarUint(encoder, messageAwareness)
const updates = Y.awarenessProtocol.forwardAwarenessMessage(decoder, encoder)
updates.forEach(update => {
doc.awareness.set(update.userID, update.state)
doc.conns.get(conn).add(update.userID)
})
const buff = Y.toBuffer(encoder)
const buff = Y.encoding.toBuffer(encoder)
doc.conns.forEach((_, c) => {
c.send(buff)
})
@@ -86,29 +87,29 @@ const setupConnection = (conn, req) => {
conn.on('close', () => {
const controlledIds = doc.conns.get(conn)
doc.conns.delete(conn)
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageAwareness)
Y.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => {
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageAwareness)
Y.awarenessProtocol.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => {
doc.awareness.delete(userID)
return { userID, state: null }
}))
const buf = Y.toBuffer(encoder)
const buf = Y.encoding.toBuffer(encoder)
doc.conns.forEach((_, conn) => conn.send(buf))
})
// send sync step 1
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageSync)
Y.writeSyncStep1(encoder, doc)
conn.send(Y.toBuffer(encoder))
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeSyncStep1(encoder, doc)
conn.send(Y.encoding.toBuffer(encoder))
if (doc.awareness.size > 0) {
const encoder = Y.createEncoder()
const encoder = Y.encoding.createEncoder()
const userStates = []
doc.awareness.forEach((state, userID) => {
userStates.push({ state, userID })
})
Y.writeVarUint(encoder, messageAwareness)
Y.writeUsersStateChange(encoder, userStates)
conn.send(Y.toBuffer(encoder))
Y.encoding.writeVarUint(encoder, messageAwareness)
Y.awarenessProtocol.writeUsersStateChange(encoder, userStates)
conn.send(Y.encoding.toBuffer(encoder))
}
}

View File

@@ -2,7 +2,7 @@
* @module provider/ydb
*/
import * as globals from './globals.mjs'
import * as globals from './globals.js'
export const Class = class NamedEventHandler {
constructor () {

View File

@@ -3,19 +3,19 @@
*/
/* eslint-env browser */
import * as idbactions from './idbactions.mjs'
import * as globals from '../../lib/globals.mjs'
import * as message from './message.mjs'
import * as bc from './broadcastchannel.mjs'
import * as encoding from '../../lib/encoding.mjs'
import * as logging from '../../lib/logging.mjs'
import * as idb from '../../lib/idb.mjs'
import * as decoding from '../../lib/decoding.mjs'
import { Y } from '../../utils/Y.mjs'
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.mjs'
import { createMutualExclude } from '../../lib/mutualExclude.mjs'
import * as idbactions from './idbactions.js'
import * as globals from '../../lib/globals.js'
import * as message from './message.js'
import * as bc from './broadcastchannel.js'
import * as encoding from '../../lib/encoding.js'
import * as logging from '../../lib/logging.js'
import * as idb from '../../lib/idb.js'
import * as decoding from '../../lib/decoding.js'
import { Y } from '../../utils/Y.js'
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.js'
import { createMutualExclude } from '../../lib/mutualExclude.js'
import * as NamedEventHandler from './NamedEventHandler.mjs'
import * as NamedEventHandler from './NamedEventHandler.js'
/**
* @typedef RoomState

View File

@@ -4,11 +4,11 @@
/* eslint-env browser */
import * as test from './test.mjs'
import * as ydbClient from './YdbClient.mjs'
import * as globals from './globals.mjs'
import * as idbactions from './idbactions.mjs'
import * as logging from './logging.mjs'
import * as test from './test.js'
import * as ydbClient from './YdbClient.js'
import * as globals from './globals.js'
import * as idbactions from './idbactions.js'
import * as logging from './logging.js'
const wsUrl = 'ws://127.0.0.1:8899/ws'
const testRoom = 'testroom'

View File

@@ -4,12 +4,13 @@
/* eslint-env browser */
import * as decoding from '../../lib/decoding.mjs'
import * as encoding from '../../lib/encoding.mjs'
import * as globals from '../../lib/globals.mjs'
import * as NamedEventHandler from './NamedEventHandler.mjs'
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
import * as globals from '../../lib/globals.js'
import * as NamedEventHandler from './NamedEventHandler.js'
const bc = new BroadcastChannel('ydb-client')
/**
* @type {Map<string, Set<Function>>}
*/

View File

@@ -33,11 +33,11 @@
* - A client may update a room when the room is in either US or Co
*/
import * as encoding from '../../lib/encoding.mjs'
import * as decoding from '../../lib/decoding.mjs'
import * as idb from '../../lib/idb.mjs'
import * as globals from '../../lib/globals.mjs'
import * as message from './message.mjs'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
import * as idb from '../../lib/idb.js'
import * as globals from '../../lib/globals.js'
import * as message from './message.js'
/**
* Get 'client-unconfirmed' store from transaction
@@ -262,7 +262,7 @@ export const getRoomMetas = t => {
result.push({
room: metakey.slice(5),
rsid,
offset: keys.reduce((cur, key) => globals.max(decodeHUKey(key).offset, cur), offset)
offset: keys.reduce((cur, key) => math.max(decodeHUKey(key).offset, cur), offset)
})
})
).then(() => globals.presolve(result))

View File

@@ -1,6 +1,6 @@
import * as globals from '../../lib/globals.mjs'
import * as idbactions from './idbactions.mjs'
import * as test from '../../lib/testing.mjs'
import * as globals from '../../lib/globals.js'
import * as idbactions from './idbactions.js'
import * as test from '../../lib/testing.js'
idbactions.deleteDB().then(() => idbactions.openDB()).then(db => {
test.run('update lifetime 1', async (testname) => {

View File

@@ -2,7 +2,7 @@
* @module provider/ydb
*/
import * as ydbclient from './YdbClient.mjs'
import * as ydbclient from './YdbClient.js'
/**
* @param {string} url

View File

@@ -2,11 +2,11 @@
* @module provider/ydb
*/
import * as encoding from './encoding.mjs'
import * as decoding from './decoding.mjs'
import * as idbactions from './idbactions.mjs'
import * as logging from './logging.mjs'
import * as bc from './broadcastchannel.mjs'
import * as encoding from './encoding.js'
import * as decoding from './decoding.js'
import * as idbactions from './idbactions.js'
import * as logging from './logging.js'
import * as bc from './broadcastchannel.js'
/* make sure to update message.go in ydb when updating these values.. */
export const MESSAGE_UPDATE = 0 // TODO: rename host_unconfirmed?

View File

@@ -3,8 +3,11 @@ import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'
import uglify from 'rollup-plugin-uglify-es'
// set this to [] to disable obfuscation
const minificationPlugins = process.env.PRODUCTION ? [babel(), uglify()] : []
export default [{
input: './index.mjs',
input: './index.js',
output: [{
name: 'Y',
file: 'build/yjs.js',
@@ -12,9 +15,9 @@ export default [{
sourcemap: true
}]
}, {
input: 'tests/index.mjs',
input: 'tests/index.js',
output: {
file: 'build/y.test.mjs',
file: 'build/y.test.js',
format: 'iife',
name: 'ytests',
sourcemap: true
@@ -27,7 +30,7 @@ export default [{
commonjs()
]
}, {
input: './examples/prosemirror.mjs',
input: './examples/prosemirror.js',
output: {
name: 'prosemirror',
file: 'examples/build/prosemirror.js',
@@ -39,36 +42,28 @@ export default [{
sourcemap: true,
module: true
}),
commonjs(),
babel(),
uglify()
]
commonjs()
].concat(minificationPlugins)
}, {
input: './examples/dom.mjs',
input: './examples/dom.js',
output: {
name: 'dom',
file: 'examples/build/dom.js',
format: 'iife',
sourcemap: true
},
plugins: [
babel(),
uglify()
]
plugins: minificationPlugins
}, {
input: './examples/textarea.mjs',
input: './examples/textarea.js',
output: {
name: 'textarea',
file: 'examples/build/textarea.js',
format: 'iife',
sourcemap: true
},
plugins: [
babel(),
uglify()
]
plugins: minificationPlugins
}, {
input: './examples/quill.mjs',
input: './examples/quill.js',
output: {
name: 'textarea',
file: 'examples/build/quill.js',
@@ -80,8 +75,6 @@ export default [{
sourcemap: true,
module: true
}),
commonjs(),
babel(),
uglify()
]
commonjs()
].concat(minificationPlugins)
}]

View File

@@ -2,15 +2,15 @@
* @module structs
*/
import { getStructReference } from '../utils/structReferences.mjs'
import * as ID from '../utils/ID.mjs'
import { stringifyID } from '../protocols/syncProtocol.mjs'
import { writeStructToTransaction } from '../utils/Transaction.mjs'
import * as decoding from '../lib/decoding.mjs'
import * as encoding from '../lib/encoding.mjs'
import { Item } from './Item.mjs' // eslint-disable-line
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import { deleteItemRange } from '../utils/structManipulation.mjs'
import { getStructReference } from '../utils/structReferences.js'
import * as ID from '../utils/ID.js'
import { writeStructToTransaction } from '../utils/structEncoding.js'
import * as decoding from '../lib/decoding.js'
import * as encoding from '../lib/encoding.js'
// import { Item } from './Item.js' // eslint-disable-line
// import { Y } from '../utils/Y.js' // eslint-disable-line
import { deleteItemRange } from '../utils/structManipulation.js'
import * as stringify from '../utils/structStringify.js'
/**
* @private
@@ -99,6 +99,6 @@ export class Delete {
* @private
*/
_logString () {
return `Delete - target: ${stringifyID(this._targetID)}, len: ${this._length}`
return `Delete - target: ${stringify.stringifyID(this._targetID)}, len: ${this._length}`
}
}

View File

@@ -2,12 +2,12 @@
* @module structs
*/
import { getStructReference } from '../utils/structReferences.mjs'
import * as ID from '../utils/ID.mjs'
import { writeStructToTransaction } from '../utils/Transaction.mjs'
import * as decoding from '../lib/decoding.mjs'
import * as encoding from '../lib/encoding.mjs'
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import { getStructReference } from '../utils/structReferences.js'
import * as ID from '../utils/ID.js'
import { writeStructToTransaction } from '../utils/structEncoding.js'
import * as decoding from '../lib/decoding.js'
import * as encoding from '../lib/encoding.js'
// import { Y } from '../utils/Y.js' // eslint-disable-line
// TODO should have the same base class as Item
export class GC {
@@ -19,6 +19,10 @@ export class GC {
this._length = 0
}
get _redone () {
return null
}
get _deleted () {
return true
}

View File

@@ -2,15 +2,30 @@
* @module structs
*/
import { getStructReference } from '../utils/structReferences.mjs'
import * as ID from '../utils/ID.mjs'
import { Delete } from './Delete.mjs'
import { transactionTypeChanged, writeStructToTransaction } from '../utils/Transaction.mjs'
import { GC } from './GC.mjs'
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import { Y } from '../utils/Y.mjs'
import { Type } from './Type.mjs' // eslint-disable-line
import { getStructReference } from '../utils/structReferences.js'
import * as ID from '../utils/ID.js'
import { Delete } from './Delete.js'
import { writeStructToTransaction } from '../utils/structEncoding.js'
import { GC } from './GC.js'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
// import { Type } from './Type.js' // eslint-disable-line
/**
* @private
*/
export const transactionTypeChanged = (y, type, sub) => {
if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) {
const changedTypes = y._transaction.changedTypes
let subs = changedTypes.get(type)
if (subs === undefined) {
// create if it doesn't exist yet
subs = new Set()
changedTypes.set(type, subs)
}
subs.add(sub)
}
}
/**
* @private
@@ -113,6 +128,30 @@ export class Item {
this._redone = null
}
/**
* Returns the next non-deleted item
* @private
*/
get _next () {
let n = this._right
while (n !== null && n._deleted) {
n = n._right
}
return n
}
/**
* Returns the previous non-deleted item
* @private
*/
get _prev () {
let n = this._left
while (n !== null && n._deleted) {
n = n._left
}
return n
}
/**
* Creates an Item with the same effect as this Item (without position effect)
*
@@ -127,7 +166,7 @@ export class Item {
* Redoes the effect of this operation.
*
* @param {Y} y The Yjs instance.
* @param {Array<Item>} redoitems
* @param {Set<Item>} redoitems
*
* @private
*/
@@ -135,7 +174,7 @@ export class Item {
if (this._redone !== null) {
return this._redone
}
if (this._parent instanceof Y) {
if (!(this._parent instanceof Item)) {
return
}
let struct = this._copy()
@@ -251,16 +290,22 @@ export class Item {
*/
_delete (y, createDelete = true, gcChildren) {
if (!this._deleted) {
const parent = this._parent
const len = this._length
// adjust the length of parent
if (parent.length !== undefined && this._countable) {
parent.length -= len
}
this._deleted = true
y.ds.mark(this._id, this._length, false)
let del = new Delete()
del._targetID = this._id
del._length = this._length
del._length = len
if (createDelete) {
// broadcast and persists Delete
del._integrate(y, true)
}
transactionTypeChanged(y, this._parent, this._parentSub)
transactionTypeChanged(y, parent, this._parentSub)
y._transaction.deletedStructs.add(this)
}
}
@@ -410,6 +455,10 @@ export class Item {
right._left = this
}
}
// adjust the length of parent
if (parentSub === null && parent.length !== undefined && this._countable) {
parent.length += this._length
}
if (parent._deleted) {
this._delete(y, false, true)
}

View File

@@ -2,11 +2,11 @@
* @module structs
*/
import { Item } from './Item.mjs'
import { logItemHelper } from '../protocols/syncProtocol.mjs'
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import { Item } from './Item.js'
import * as stringify from '../utils/structStringify.js'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
export class ItemEmbed extends Item {
constructor () {
@@ -44,6 +44,6 @@ export class ItemEmbed extends Item {
* @private
*/
_logString () {
return logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`)
return stringify.logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`)
}
}

View File

@@ -2,11 +2,11 @@
* @module structs
*/
import { Item } from './Item.mjs'
import { logItemHelper } from '../protocols/syncProtocol.mjs'
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import { Item } from './Item.js'
import * as stringify from '../utils/structStringify.js'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
export class ItemFormat extends Item {
constructor () {
@@ -51,6 +51,6 @@ export class ItemFormat extends Item {
* @private
*/
_logString () {
return logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`)
return stringify.logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`)
}
}

View File

@@ -2,11 +2,11 @@
* @module structs
*/
import { Item, splitHelper } from './Item.mjs'
import { logItemHelper } from '../protocols/syncProtocol.mjs'
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import { Item, splitHelper } from './Item.js'
import * as stringify from '../utils/structStringify.js'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
export class ItemJSON extends Item {
constructor () {
@@ -19,7 +19,8 @@ export class ItemJSON extends Item {
return struct
}
get _length () {
return this._content.length
const c = this._content
return c !== null ? c.length : 0
}
/**
* @param {Y} y
@@ -46,11 +47,11 @@ export class ItemJSON extends Item {
*/
_toBinary (encoder) {
super._toBinary(encoder)
let len = this._content.length
const len = this._length
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
let encoded
let content = this._content[i]
const content = this._content[i]
if (content === undefined) {
encoded = 'undefined'
} else {
@@ -66,7 +67,7 @@ export class ItemJSON extends Item {
* @private
*/
_logString () {
return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
return stringify.logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
}
_splitAt (y, diff) {
if (diff === 0) {

View File

@@ -2,11 +2,11 @@
* @module structs
*/
import { Item, splitHelper } from './Item.mjs'
import { logItemHelper } from '../protocols/syncProtocol.mjs'
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import { Item, splitHelper } from './Item.js'
import * as stringify from '../utils/structStringify.js'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
export class ItemString extends Item {
constructor () {
@@ -44,7 +44,7 @@ export class ItemString extends Item {
* @private
*/
_logString () {
return logItemHelper('ItemString', this, `content:"${this._content}"`)
return stringify.logItemHelper('ItemString', this, `content:"${this._content}"`)
}
_splitAt (y, diff) {
if (diff === 0) {

View File

@@ -2,11 +2,11 @@
* @module structs
*/
import { Item } from './Item.mjs'
import { EventHandler } from '../utils/EventHandler.mjs'
import { createID } from '../utils/ID.mjs'
import { YEvent } from '../utils/YEvent.mjs'
import { Y } from '../utils/Y.mjs' // eslint-disable-line
import { Item } from './Item.js'
import { EventHandler } from '../utils/EventHandler.js'
import { createID } from '../utils/ID.js'
import { YEvent } from '../utils/YEvent.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
// restructure children as if they were inserted one after another
const integrateChildren = (y, start) => {
@@ -57,6 +57,17 @@ export class Type extends Item {
this._deepEventHandler = new EventHandler()
}
/**
* The first non-deleted item
*/
get _first () {
let n = this._start
while (n !== null && n._deleted) {
n = n._right
}
return n
}
/**
* Compute the path from this type to the specified target.
*

View File

@@ -1,7 +1,7 @@
import { test } from 'cutest'
import * as random from '../lib/prng/prng.mjs'
import { DeleteStore } from '../utils/DeleteStore.mjs'
import * as ID from '../utils/ID.mjs'
import * as random from '../lib/prng/prng.js'
import { DeleteStore } from '../utils/DeleteStore.js'
import * as ID from '../utils/ID.js'
/**
* Converts a DS to an array of length 10.

View File

@@ -1,6 +1,6 @@
import { test } from 'cutest'
import { simpleDiff } from '../lib/diff.mjs'
import * as random from '../lib/prng/prng.mjs'
import { simpleDiff } from '../lib/diff.js'
import * as random from '../lib/prng/prng.js'
function runDiffTest (t, a, b, expected) {
let result = simpleDiff(a, b)

View File

@@ -1,8 +1,8 @@
import { test } from 'cutest'
import { generateRandomUint32 } from '../utils/generateRandomUint32.mjs'
import * as encoding from '../lib/encoding.mjs'
import * as decoding from '../lib/decoding.mjs'
import * as random from '../lib/prng/prng.mjs'
import { generateRandomUint32 } from '../utils/generateRandomUint32.js'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import * as random from '../lib/prng/prng.js'
function testEncoding (t, write, read, val) {
let encoder = encoding.createEncoder()

Some files were not shown because too many files have changed in this diff Show More