Compare commits

..

42 Commits

Author SHA1 Message Date
Kevin Jahns
4063e28b5e 13.0.0-76 2018-12-11 20:19:07 +01:00
Kevin Jahns
b6f7cd7869 fix broadcast channel communication 2018-12-11 20:18:11 +01:00
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
Kevin Jahns
01d0825ae6 13.0.0-73 2018-11-26 17:14:48 +01:00
Kevin Jahns
e2f98525d2 clean examples build 2018-11-26 17:14:45 +01:00
Kevin Jahns
70a0a03130 no start content in prosemirror example 2018-11-26 16:59:01 +01:00
Kevin Jahns
656d85c62e add dom example 2018-11-26 16:06:17 +01:00
Kevin Jahns
e168dd48fb proper api endpoints for examples 2018-11-26 14:54:46 +01:00
Kevin Jahns
12d43199d5 add http listener to websocket-server 2018-11-26 13:08:23 +01:00
Kevin Jahns
539fa8b21d examples use hosted server 2018-11-26 02:13:06 +01:00
Kevin Jahns
f572f94586 port support 2018-11-25 23:41:17 +01:00
Kevin Jahns
c12d00b227 mjs nodejs support 2018-11-25 22:39:50 +01:00
Kevin Jahns
e4a5f2caec jsdoc fixes 2018-11-25 05:43:18 +01:00
Kevin Jahns
9f9f465238 update logo link 2018-11-25 04:50:23 +01:00
Kevin Jahns
8450ff86d7 make npm build ready for netlify 2018-11-25 04:41:52 +01:00
Kevin Jahns
70139262c5 add rollup-cli as dependency 2018-11-25 03:36:06 +01:00
Kevin Jahns
9c0da271eb large scale refactoring 2018-11-25 03:17:00 +01:00
Kevin Jahns
ade3e1949d update cdn destination. closes #128 2018-11-20 15:03:28 +01:00
Kevin Jahns
eec63a008f 13.0.0-72 2018-11-20 03:53:55 +01:00
Kevin Jahns
52abcdd043 fix all tests 2018-11-16 12:33:41 +01:00
Kevin Jahns
f94653424a add prosemirror tests 2018-11-14 07:20:06 +01:00
Kevin Jahns
d67a794e2c 13.0.0-71 2018-11-09 01:49:59 +01:00
Kevin Jahns
60318083a6 make websocket-server a binary and add bindings and provider to npm package 2018-11-09 01:49:43 +01:00
Kevin Jahns
7607070452 13.0.0-70 2018-11-09 01:24:06 +01:00
Kevin Jahns
28fb7b6e9c remove logging in prosemirror binding 2018-11-09 01:23:16 +01:00
Kevin Jahns
aafe15757f implemented awareness protocol and added cursor support 2018-11-09 00:13:30 +01:00
Kevin Jahns
31d6ef6296 cleanup prosemirror example 2018-11-06 15:15:27 +01:00
Kevin Jahns
32b8fac37f added prosemirror binding 2018-11-06 13:44:35 +01:00
Kevin Jahns
e8060de914 13.0.0-69 2018-11-02 01:54:53 +01:00
Kevin Jahns
22b036527c further refine build process to also include lib 2018-11-02 01:54:40 +01:00
Kevin Jahns
feb1e030d7 13.0.0-68 2018-11-02 01:52:24 +01:00
Kevin Jahns
bd271e3952 update publish process 2018-11-02 01:52:20 +01:00
Kevin Jahns
df80938190 13.0.0-67 2018-11-02 00:47:09 +01:00
172 changed files with 4441 additions and 2065 deletions

View File

@@ -1,6 +1,7 @@
{ {
"source": "./src", "source": ".",
"destination": "./docs", "destination": "./docs",
"excludes": ["build", "node_modules", "tests-lib", "test"],
"plugins": [{ "plugins": [{
"name": "esdoc-standard-plugin", "name": "esdoc-standard-plugin",
"option": { "option": {

4
.gitignore vendored
View File

@@ -2,7 +2,7 @@ node_modules
bower_components bower_components
docs docs
/y.* /y.*
/examples/yjs-dist.js* /examples_all/*/index.dist.*
.vscode .vscode
.yjsPersisted .yjsPersisted
build build

50
.jsdoc.json Normal file
View File

@@ -0,0 +1,50 @@
{
"sourceType": "module",
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["./types", "./utils/UndoManager.js", "./utils/Y.js", "./provider", "./bindings"],
"includePattern": ".js$"
},
"plugins": [
"plugins/markdown"
],
"templates": {
"referenceTitle": "Yjs",
"disableSort": false,
"useCollapsibles": true,
"collapse": true,
"resources": {
"y-js.org": "yjs.website"
},
"logo": {
"url": "https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png",
"width": "162px",
"height": "162px",
"link": "/"
},
"tabNames": {
"api": "API",
"tutorials": "Examples"
},
"footerText": "Shared Editing",
"css": [
"./style.css"
],
"default": {
"staticFiles": {
"include": ["examples/"]
}
}
},
"opts": {
"destination": "./docs/",
"encoding": "utf8",
"private": false,
"recurse": true,
"template": "./node_modules/tui-jsdoc-template",
"tutorials": "./examples"
}
}

View File

@@ -1,5 +1,5 @@
# ![Yjs](http://y-js.org/images/yjs.png) # ![Yjs](https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png)
Yjs is a framework for offline-first p2p shared editing on structured data like Yjs is a framework for offline-first p2p shared editing on structured data like
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
@@ -66,7 +66,7 @@ missing modules.
### CDN ### CDN
``` ```
<script src="https://cdn.jsdelivr.net/npm/yjs@12/src/y.js"></script> <script src="https://cdn.jsdelivr.net/npm/yjs@12/dist/y.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>

21
README.v13.md Normal file
View File

@@ -0,0 +1,21 @@
# ![Yjs](https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png)
> A CRDT library with a powerful abstraction of shared data
Yjs v13 is a work in progress.
### Typescript Declarations
Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the only way to get type declarations is by adding Yjs to the list of checked files:
```json
{
"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,15 +1,23 @@
/**
* @module bindings/dom
*/
/* global MutationObserver, getSelection */ /* global MutationObserver, getSelection */
import { fromRelativePosition } from '../../Util/relativePosition.js' import { fromRelativePosition } from '../../utils/relativePosition.js'
import Binding from '../Binding.js' import { createMutex } from '../../lib/mutex.js'
import { createAssociation, removeAssociation } from './util.js' import { createAssociation, removeAssociation } from './util.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js' import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
import { defaultFilter, applyFilterOnType } from './filter.js' import { defaultFilter, applyFilterOnType } from './filter.js'
import typeObserver from './typeObserver.js' import { typeObserver } from './typeObserver.js'
import domObserver from './domObserver.js' import { domObserver } from './domObserver.js'
import { YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
/** /**
* @typedef {import('./filter.js').DomFilter} DomFilter * @callback DomFilter
* @param {string} nodeName
* @param {Map<string, string>} attrs
* @return {Map | null}
*/ */
/** /**
@@ -22,8 +30,9 @@ import domObserver from './domObserver.js'
* const type = y.define('xml', Y.XmlFragment) * const type = y.define('xml', Y.XmlFragment)
* const binding = new Y.QuillBinding(type, div) * const binding = new Y.QuillBinding(type, div)
* *
* @class
*/ */
export default class DomBinding extends Binding { export class DomBinding {
/** /**
* @param {YXmlFragment} type The bind source. This is the ultimate source of * @param {YXmlFragment} type The bind source. This is the ultimate source of
* truth. * truth.
@@ -31,10 +40,26 @@ export default class DomBinding extends Binding {
* @param {Object} [opts] Optional configurations * @param {Object} [opts] Optional configurations
* @param {DomFilter} [opts.filter=defaultFilter] The filter function to use. * @param {DomFilter} [opts.filter=defaultFilter] The filter function to use.
* @param {Document} [opts.document=document] The filter function to use.
* @param {Object} [opts.hooks] The filter function to use.
* @param {Element} [opts.scrollingElement=null] The filter function to use.
*/ */
constructor (type, target, opts = {}) { constructor (type, target, opts = {}) {
// Binding handles textType as this.type and domTextarea as this.target // Binding handles textType as this.type and domTextarea as this.target
super(type, target) /**
* The Yjs type that is bound to `target`
* @type {YXmlFragment}
*/
this.type = type
/**
* The target that `type` is bound to.
* @type {Element}
*/
this.target = target
/**
* @private
*/
this._mutualExclude = createMutex()
this.opts = opts this.opts = opts
opts.document = opts.document || document opts.document = opts.document || document
opts.hooks = opts.hooks || {} opts.hooks = opts.hooks || {}
@@ -81,16 +106,16 @@ export default class DomBinding extends Binding {
this.y = y this.y = y
// Force flush dom changes before Type changes are applied (they might // Force flush dom changes before Type changes are applied (they might
// modify the dom) // modify the dom)
this._beforeTransactionHandler = (y, transaction, remote) => { this._beforeTransactionHandler = y => {
this._domObserver(this._mutationObserver.takeRecords()) this._domObserver(this._mutationObserver.takeRecords())
this._mutualExclude(() => { this._mutualExclude(() => {
beforeTransactionSelectionFixer(this, remote) beforeTransactionSelectionFixer(this)
}) })
} }
y.on('beforeTransaction', this._beforeTransactionHandler) y.on('beforeTransaction', this._beforeTransactionHandler)
this._afterTransactionHandler = (y, transaction, remote) => { this._afterTransactionHandler = (y, transaction) => {
this._mutualExclude(() => { this._mutualExclude(() => {
afterTransactionSelectionFixer(this, remote) afterTransactionSelectionFixer(this)
}) })
// remove associations // remove associations
// TODO: this could be done more efficiently // TODO: this could be done more efficiently
@@ -121,8 +146,15 @@ export default class DomBinding extends Binding {
createAssociation(this, target, type) createAssociation(this, target, type)
} }
flushDomChanges () {
this._domObserver(this._mutationObserver.takeRecords())
}
/** /**
* NOTE: currently does not apply filter to existing elements! * NOTE:
* * does not apply filter to existing elements!
* * only guarantees that changes are filtered locally. Remote sites may see different content.
*
* @param {DomFilter} filter The filter function to use from now on. * @param {DomFilter} filter The filter function to use from now on.
*/ */
setFilter (filter) { setFilter (filter) {
@@ -199,13 +231,18 @@ export default class DomBinding extends Binding {
y.off('beforeObserverCalls', this._beforeObserverCallsHandler) y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
y.off('afterTransaction', this._afterTransactionHandler) y.off('afterTransaction', this._afterTransactionHandler)
document.removeEventListener('selectionchange', this._selectionchange) document.removeEventListener('selectionchange', this._selectionchange)
super.destroy() this.type = null
this.target = null
} }
} }
/** /**
* A filter defines which elements and attributes to share. * A filter defines which elements and attributes to share.
* Return null if the node should be filtered. Otherwise return the Map of * Return null if the node should be filtered. Otherwise return the Map of
* accepted attributes. * accepted attributes.
* *
* @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction * @callback FilterFunction
* @param {string} nodeName
* @param {Map} attrs
* @return {Map|null}
*/ */

View File

@@ -1,11 +1,14 @@
/**
* @module bindings/dom
*/
import YXmlHook from '../../Types/YXml/YXmlHook.js' import { YXmlHook } from '../../types/YXmlHook.js'
import { import {
iterateUntilUndeleted, iterateUntilUndeleted,
removeAssociation, removeAssociation,
insertNodeHelper } from './util.js' insertNodeHelper } from './util.js'
import diff from '../../../lib/simpleDiff.js' import { simpleDiff } from '../../lib/diff.js'
import YXmlFragment from '../../Types/YXml/YXmlFragment.js' import { YXmlFragment } from '../../types/YXmlElement.js'
/** /**
* 1. Check if any of the nodes was deleted * 1. Check if any of the nodes was deleted
@@ -17,9 +20,11 @@ import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
* recreate a new yxml element that is bound to that node. * recreate a new yxml element that is bound to that node.
* You can detect that a node was moved because expectedId * You can detect that a node was moved because expectedId
* !== actualId in the list * !== actualId in the list
*
* @function
* @private * @private
*/ */
function applyChangesFromDom (binding, dom, yxml, _document) { const applyChangesFromDom = (binding, dom, yxml, _document) => {
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) { if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
return return
} }
@@ -32,7 +37,7 @@ function applyChangesFromDom (binding, dom, yxml, _document) {
} }
} }
// 1. Check if any of the nodes was deleted // 1. Check if any of the nodes was deleted
yxml.forEach(function (childType) { yxml.forEach(childType => {
if (knownChildren.has(childType) === false) { if (knownChildren.has(childType) === false) {
childType._delete(y) childType._delete(y)
removeAssociation(binding, binding.typeToDom.get(childType), childType) removeAssociation(binding, binding.typeToDom.get(childType), childType)
@@ -82,8 +87,9 @@ function applyChangesFromDom (binding, dom, yxml, _document) {
/** /**
* @private * @private
* @function
*/ */
export default function domObserver (mutations, _document) { export function domObserver (mutations, _document) {
this._mutualExclude(() => { this._mutualExclude(() => {
this.type._y.transact(() => { this.type._y.transact(() => {
let diffChildren = new Set() let diffChildren = new Set()
@@ -107,7 +113,7 @@ export default function domObserver (mutations, _document) {
} }
switch (mutation.type) { switch (mutation.type) {
case 'characterData': case 'characterData':
var change = diff(yxml.toString(), dom.nodeValue) var change = simpleDiff(yxml.toString(), dom.nodeValue)
yxml.delete(change.pos, change.remove) yxml.delete(change.pos, change.remove)
yxml.insert(change.pos, change.insert) yxml.insert(change.pos, change.insert)
break break

View File

@@ -1,18 +1,26 @@
/**
* @module bindings/dom
*/
/* eslint-env browser */ /* eslint-env browser */
import YXmlText from '../../Types/YXml/YXmlText.js' import { YXmlText } from '../../types/YXmlText.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js' import { YXmlHook } from '../../types/YXmlHook.js'
import YXmlElement from '../../Types/YXml/YXmlElement.js' import { YXmlElement } from '../../types/YXmlElement.js'
import { createAssociation, domsToTypes } from './util.js' import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes, defaultFilter } from './filter.js' import { filterDomAttributes, defaultFilter } from './filter.js'
import { DomBinding } from './DomBinding.js' // eslint-disable-line
/** /**
* @typedef {import('./filter.js').DomFilter} DomFilter * @callback DomFilter
* @typedef {import('./DomBinding.js').default} DomBinding * @param {string} nodeName
* @param {Map<string, string>} attrs
* @return {Map | null}
*/ */
/** /**
* Creates a Yjs type (YXml) based on the contents of a DOM Element. * Creates a Yjs type (YXml) based on the contents of a DOM Element.
* *
* @function
* @param {Element|Text} element The DOM Element * @param {Element|Text} element The DOM Element
* @param {?Document} _document Optional. Provide the global document object * @param {?Document} _document Optional. Provide the global document object
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks * @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
@@ -20,7 +28,7 @@ import { filterDomAttributes, defaultFilter } from './filter.js'
* @param {?DomBinding} binding Warning: This property is for internal use only! * @param {?DomBinding} binding Warning: This property is for internal use only!
* @return {YXmlElement | YXmlText | false} * @return {YXmlElement | YXmlText | false}
*/ */
export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) { export const domToType = (element, _document = document, hooks = {}, filter = defaultFilter, binding) => {
/** /**
* @type {any} * @type {any}
*/ */

View File

@@ -1,29 +1,33 @@
import isParentOf from '../../Util/isParentOf.js'
/** /**
* @callback DomFilter * @module bindings/dom
* @param {string} nodeName
* @param {Map<string, string>} attrs
* @return {Map | null}
*/ */
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). * Default filter method (does nothing).
* *
* @function
* @param {String} nodeName The nodeName of the element * @param {String} nodeName The nodeName of the element
* @param {Map} attrs Map of key-value pairs that are attributes of the node. * @param {Map} attrs Map of key-value pairs that are attributes of the node.
* @return {Map | null} The allowed attributes or null, if the element should be * @return {Map | null} The allowed attributes or null, if the element should be
* filtered. * filtered.
*/ */
export function defaultFilter (nodeName, attrs) { export const defaultFilter = (nodeName, attrs) => {
// TODO: implement basic filter that filters out dangerous properties! // TODO: implement basic filter that filters out dangerous properties!
return attrs return attrs
} }
/** /**
* * @private
* @function
* @param {Element} dom
* @param {Function} filter
*/ */
export function filterDomAttributes (dom, filter) { export const filterDomAttributes = (dom, filter) => {
const attrs = new Map() const attrs = new Map()
for (let i = dom.attributes.length - 1; i >= 0; i--) { for (let i = dom.attributes.length - 1; i >= 0; i--) {
const attr = dom.attributes[i] const attr = dom.attributes[i]
@@ -35,14 +39,14 @@ export function filterDomAttributes (dom, filter) {
/** /**
* Applies a filter on a type. * Applies a filter on a type.
* *
* @private
* @function
* @param {Y} y The Yjs instance. * @param {Y} y The Yjs instance.
* @param {DomBinding} binding The DOM binding instance that has the dom filter. * @param {DomBinding} binding The DOM binding instance that has the dom filter.
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to. * @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
*
* @private
*/ */
export function applyFilterOnType (y, binding, type) { export const applyFilterOnType = (y, binding, type) => {
if (isParentOf(binding.type, type)) { if (isParentOf(binding.type, type) && type instanceof YXmlElement) {
const nodeName = type.nodeName const nodeName = type.nodeName
let attributes = new Map() let attributes = new Map()
if (type.getAttributes !== undefined) { if (type.getAttributes !== undefined) {
@@ -53,7 +57,7 @@ export function applyFilterOnType (y, binding, type) {
} }
const filteredAttributes = binding.filter(nodeName, new Map(attributes)) const filteredAttributes = binding.filter(nodeName, new Map(attributes))
if (filteredAttributes === null) { if (filteredAttributes === null) {
type._delete(y) type._delete(y, true)
} else { } else {
// iterate original attributes // iterate original attributes
attributes.forEach((value, key) => { attributes.forEach((value, key) => {

View File

@@ -1,10 +1,17 @@
/**
* @module bindings/dom
*/
/* globals getSelection */ /* globals getSelection */
import { getRelativePosition } from '../../Util/relativePosition.js' import { getRelativePosition } from '../../utils/relativePosition.js'
let relativeSelection = null let relativeSelection = null
function _getCurrentRelativeSelection (domBinding) { /**
* @private
*/
const _getCurrentRelativeSelection = domBinding => {
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection() const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
const baseNodeType = domBinding.domToType.get(baseNode) const baseNodeType = domBinding.domToType.get(baseNode)
const extentNodeType = domBinding.domToType.get(extentNode) const extentNodeType = domBinding.domToType.get(extentNode)
@@ -17,18 +24,25 @@ function _getCurrentRelativeSelection (domBinding) {
return null return null
} }
/**
* @private
*/
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
export function beforeTransactionSelectionFixer (domBinding) { /**
* @private
*/
export const beforeTransactionSelectionFixer = domBinding => {
relativeSelection = getCurrentRelativeSelection(domBinding) relativeSelection = getCurrentRelativeSelection(domBinding)
} }
/** /**
* Reset the browser range after every transaction. * Reset the browser range after every transaction.
* This prevents any collapsing issues with the local selection. * This prevents any collapsing issues with the local selection.
*
* @private * @private
*/ */
export function afterTransactionSelectionFixer (domBinding) { export const afterTransactionSelectionFixer = domBinding => {
if (relativeSelection !== null) { if (relativeSelection !== null) {
domBinding.restoreSelection(relativeSelection) domBinding.restoreSelection(relativeSelection)
} }

View File

@@ -1,11 +1,15 @@
/**
* @module bindings/dom
*/
/* eslint-env browser */ /* eslint-env browser */
/* global getSelection */ /* global getSelection */
import YXmlText from '../../Types/YXml/YXmlText.js' import { YXmlText } from '../../types/YXmlText.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js' import { YXmlHook } from '../../types/YXmlHook.js'
import { removeDomChildrenUntilElementFound } from './util.js' import { removeDomChildrenUntilElementFound } from './util.js'
function findScrollReference (scrollingElement) { const findScrollReference = scrollingElement => {
if (scrollingElement !== null) { if (scrollingElement !== null) {
let anchor = getSelection().anchorNode let anchor = getSelection().anchorNode
if (anchor == null) { if (anchor == null) {
@@ -34,7 +38,7 @@ function findScrollReference (scrollingElement) {
return null return null
} }
function fixScroll (scrollingElement, ref) { const fixScroll = (scrollingElement, ref) => {
if (ref !== null) { if (ref !== null) {
const { elem, top } = ref const { elem, top } = ref
const currentTop = elem.getBoundingClientRect().top const currentTop = elem.getBoundingClientRect().top
@@ -48,7 +52,7 @@ function fixScroll (scrollingElement, ref) {
/** /**
* @private * @private
*/ */
export default function typeObserver (events) { export const typeObserver = function (events) {
this._mutualExclude(() => { this._mutualExclude(() => {
const scrollRef = findScrollReference(this.scrollingElement) const scrollRef = findScrollReference(this.scrollingElement)
events.forEach(event => { events.forEach(event => {

View File

@@ -1,19 +1,16 @@
import domToType from './domToType.js'
/** /**
* @typedef {import('../../Types/YXml/YXmlText.js').default} YXmlText * @module bindings/dom
* @typedef {import('../../Types/YXml/YXmlElement.js').default} YXmlElement
* @typedef {import('../../Types/YXml/YXmlHook.js').default} YXmlHook
* @typedef {import('./DomBinding.js').default} DomBinding
*/ */
import { domToType } from './domToType.js'
import { DomBinding } from './DomBinding.js' // eslint-disable-line
/** /**
* Iterates items until an undeleted item is found. * Iterates items until an undeleted item is found.
* *
* @private * @private
*/ */
export function iterateUntilUndeleted (item) { export const iterateUntilUndeleted = item => {
while (item !== null && item._deleted) { while (item !== null && item._deleted) {
item = item._right item = item._right
} }
@@ -24,12 +21,14 @@ export function iterateUntilUndeleted (item) {
* Removes an association (the information that a DOM element belongs to a * Removes an association (the information that a DOM element belongs to a
* type). * type).
* *
* @private
* @function
* @param {DomBinding} domBinding The binding object * @param {DomBinding} domBinding The binding object
* @param {Element} dom The dom that is to be associated with type * @param {Element} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom * @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
* *
*/ */
export function removeAssociation (domBinding, dom, type) { export const removeAssociation = (domBinding, dom, type) => {
domBinding.domToType.delete(dom) domBinding.domToType.delete(dom)
domBinding.typeToDom.delete(type) domBinding.typeToDom.delete(type)
} }
@@ -38,12 +37,14 @@ export function removeAssociation (domBinding, dom, type) {
* Creates an association (the information that a DOM element belongs to a * Creates an association (the information that a DOM element belongs to a
* type). * type).
* *
* @private
* @function
* @param {DomBinding} domBinding The binding object * @param {DomBinding} domBinding The binding object
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type * @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom * @param {YXmlFragment|YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
* *
*/ */
export function createAssociation (domBinding, dom, type) { export const createAssociation = (domBinding, dom, type) => {
if (domBinding !== undefined) { if (domBinding !== undefined) {
domBinding.domToType.set(dom, type) domBinding.domToType.set(dom, type)
domBinding.typeToDom.set(type, dom) domBinding.typeToDom.set(type, dom)
@@ -54,11 +55,13 @@ export function createAssociation (domBinding, dom, type) {
* If oldDom is associated with a type, associate newDom with the type and * If oldDom is associated with a type, associate newDom with the type and
* forget about oldDom. If oldDom is not associated with any type, nothing happens. * forget about oldDom. If oldDom is not associated with any type, nothing happens.
* *
* @private
* @function
* @param {DomBinding} domBinding The binding object * @param {DomBinding} domBinding The binding object
* @param {Element} oldDom The existing dom * @param {Element} oldDom The existing dom
* @param {Element} newDom The new dom object * @param {Element} newDom The new dom object
*/ */
export function switchAssociation (domBinding, oldDom, newDom) { export const switchAssociation = (domBinding, oldDom, newDom) => {
if (domBinding !== undefined) { if (domBinding !== undefined) {
const type = domBinding.domToType.get(oldDom) const type = domBinding.domToType.get(oldDom)
if (type !== undefined) { if (type !== undefined) {
@@ -73,6 +76,8 @@ export function switchAssociation (domBinding, oldDom, newDom) {
* The Dom elements will be bound to a new YXmlElement and inserted at the * The Dom elements will be bound to a new YXmlElement and inserted at the
* specified position. * specified position.
* *
* @private
* @function
* @param {YXmlElement} type The type in which to insert DOM elements. * @param {YXmlElement} type The type in which to insert DOM elements.
* @param {YXmlElement|null} prev The reference node. New YxmlElements are * @param {YXmlElement|null} prev The reference node. New YxmlElements are
* inserted after this node. Set null to insert at * inserted after this node. Set null to insert at
@@ -81,15 +86,13 @@ export function switchAssociation (domBinding, oldDom, newDom) {
* @param {?Document} _document Optional. Provide the global document object. * @param {?Document} _document Optional. Provide the global document object.
* @param {DomBinding} binding The dom binding * @param {DomBinding} binding The dom binding
* @return {Array<YXmlElement>} The YxmlElements that are inserted. * @return {Array<YXmlElement>} The YxmlElements that are inserted.
*
* @private
*/ */
export function insertDomElementsAfter (type, prev, doms, _document, binding) { export const insertDomElementsAfter = (type, prev, doms, _document, binding) => {
const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding) const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding)
return type.insertAfter(prev, types) return type.insertAfter(prev, types)
} }
export function domsToTypes (doms, _document, hooks, filter, binding) { export const domsToTypes = (doms, _document, hooks, filter, binding) => {
const types = [] const types = []
for (let dom of doms) { for (let dom of doms) {
const t = domToType(dom, _document, hooks, filter, binding) const t = domToType(dom, _document, hooks, filter, binding)
@@ -102,8 +105,9 @@ export function domsToTypes (doms, _document, hooks, filter, binding) {
/** /**
* @private * @private
* @function
*/ */
export function insertNodeHelper (yxml, prevExpectedNode, child, _document, binding) { export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, binding) => {
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding) let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
if (insertedNodes.length > 0) { if (insertedNodes.length > 0) {
return insertedNodes[0] return insertedNodes[0]
@@ -115,14 +119,14 @@ export function insertNodeHelper (yxml, prevExpectedNode, child, _document, bind
/** /**
* Remove children until `elem` is found. * Remove children until `elem` is found.
* *
* @private
* @function
* @param {Element} parent The parent of `elem` and `currentChild`. * @param {Element} parent The parent of `elem` and `currentChild`.
* @param {Element} currentChild Start removing elements with `currentChild`. If * @param {Node} currentChild Start removing elements with `currentChild`. If
* `currentChild` is `elem` it won't be removed. * `currentChild` is `elem` it won't be removed.
* @param {Element|null} elem The elemnt to look for. * @param {Element|null} elem The elemnt to look for.
*
* @private
*/ */
export function removeDomChildrenUntilElementFound (parent, currentChild, elem) { export const removeDomChildrenUntilElementFound = (parent, currentChild, elem) => {
while (currentChild !== elem) { while (currentChild !== elem) {
const del = currentChild const del = currentChild
currentChild = currentChild.nextSibling currentChild = currentChild.nextSibling

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,10 +1,14 @@
import Binding from '../Binding.js' /**
* @module bindings/quill
*/
function typeObserver (event) { import { createMutex } from '../lib/mutex.js'
const typeObserver = function (event) {
const quill = this.target const quill = this.target
// Force flush Quill changes. // Force flush Quill changes.
quill.update('yjs') quill.update('yjs')
this._mutualExclude(function () { this._mutualExclude(() => {
// Apply computed delta. // Apply computed delta.
quill.updateContents(event.delta, 'yjs') quill.updateContents(event.delta, 'yjs')
// Force flush Quill changes. Ignore applied changes. // Force flush Quill changes. Ignore applied changes.
@@ -12,7 +16,7 @@ function typeObserver (event) {
}) })
} }
function quillObserver (delta) { const quillObserver = function (delta) {
this._mutualExclude(() => { this._mutualExclude(() => {
this.type.applyDelta(delta.ops) this.type.applyDelta(delta.ops)
}) })
@@ -28,14 +32,27 @@ function quillObserver (delta) {
* // Now modifications on the DOM will be reflected in the Type, and the other * // Now modifications on the DOM will be reflected in the Type, and the other
* // way around! * // way around!
*/ */
export default class QuillBinding extends Binding { export class QuillBinding {
/** /**
* @param {YText} textType * @param {YText} textType
* @param {Quill} quill * @param {Quill} quill
*/ */
constructor (textType, quill) { constructor (textType, quill) {
// Binding handles textType as this.type and quill as this.target. // Binding handles textType as this.type and quill as this.target.
super(textType, quill) /**
* The Yjs type that is bound to `target`
* @type {YText}
*/
this.type = textType
/**
* The target that `type` is bound to.
* @type {Quill}
*/
this.target = quill
/**
* @private
*/
this._mutualExclude = createMutex()
// Set initial value. // Set initial value.
quill.setContents(textType.toDelta(), 'yjs') quill.setContents(textType.toDelta(), 'yjs')
// Observers are handled by this class. // Observers are handled by this class.
@@ -48,6 +65,7 @@ export default class QuillBinding extends Binding {
// Remove everything that is handled by this class. // Remove everything that is handled by this class.
this.type.unobserve(this._typeObserver) this.type.unobserve(this._typeObserver)
this.target.off('text-change', this._quillObserver) this.target.off('text-change', this._quillObserver)
super.destroy() this.type = null
this.target = null
} }
} }

View File

@@ -1,7 +1,10 @@
/**
* @module bindings/textarea
*/
import Binding from '../Binding.js' import { simpleDiff } from '../lib/diff.js'
import simpleDiff from '../../Util/simpleDiff.js' import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.js'
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js' import { createMutex } from '../lib/mutex.js'
function typeObserver () { function typeObserver () {
this._mutualExclude(() => { this._mutualExclude(() => {
@@ -35,10 +38,22 @@ function domObserver () {
* const binding = new Y.QuillBinding(type, textarea) * const binding = new Y.QuillBinding(type, textarea)
* *
*/ */
export default class TextareaBinding extends Binding { export class TextareaBinding {
constructor (textType, domTextarea) { constructor (textType, domTextarea) {
// Binding handles textType as this.type and domTextarea as this.target /**
super(textType, domTextarea) * The Yjs type that is bound to `target`
* @type {Type}
*/
this.type = textType
/**
* The target that `type` is bound to.
* @type {*}
*/
this.target = domTextarea
/**
* @private
*/
this._mutualExclude = createMutex()
// set initial value // set initial value
domTextarea.value = textType.toString() domTextarea.value = textType.toString()
// Observers are handled by this class // Observers are handled by this class
@@ -51,6 +66,7 @@ export default class TextareaBinding extends Binding {
// Remove everything that is handled by this class // Remove everything that is handled by this class
this.type.unobserve(this._typeObserver) this.type.unobserve(this._typeObserver)
this.target.unobserve(this._domObserver) this.target.unobserve(this._domObserver)
super.destroy() this.type = null
this.target = null
} }
} }

1
examples/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

View File

@@ -1,19 +0,0 @@
{
"name": "yjs-examples",
"version": "0.0.0",
"homepage": "y-js.org",
"authors": [
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
],
"description": "Examples for Yjs",
"license": "MIT",
"ignore": [],
"dependencies": {
"quill": "^1.0.0-rc.2",
"ace": "~1.2.3",
"ace-builds": "~1.2.3",
"jquery": "~2.2.2",
"d3": "^3.5.16",
"codemirror": "^5.25.0"
}
}

36
examples/dom.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Prosemirror Example</title>
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
<style>
#content {
min-height: 500px;
}
</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>
<!-- 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.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')
const type = ydocument.define('xml', Y.XmlFragment)
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
window.example = {
provider, ydocument, type, binding
}
</script>
</body>
</html>

14
examples/dom.js Normal file
View File

@@ -0,0 +1,14 @@
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { DomBinding } from '../bindings/dom.js'
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 })
window.example = {
provider, ydocument, type, binding
}

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'

14
examples/examples.json Normal file
View File

@@ -0,0 +1,14 @@
{
"prosemirror": {
"title": "Prosemirror Binding"
},
"textarea": {
"title": "Textarea Binding"
},
"quill": {
"title": "Quill Binding"
},
"dom": {
"title": "Dom Binding"
}
}

View File

@@ -1,55 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<style>
.wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 7px;
}
.one {
grid-column: 1 ;
}
.two {
grid-column: 2;
}
.three {
grid-column: 3;
}
textarea {
width: calc(100% - 10px)
}
.editor-container {
background-color: #4caf50;
padding: 4px 5px 10px 5px;
border-radius: 11px;
}
.editor-container[disconnected] {
background-color: red;
}
.disconnected-info {
display: none;
}
.editor-container[disconnected] .disconnected-info {
display: inline;
}
</style>
<div class="wrapper">
<div id="container1" class="one editor-container">
<h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<div id="container2" class="two editor-container">
<h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<div id="container3" class="three editor-container">
<h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
</div>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,38 +0,0 @@
/* global Y */
function bindYjsInstance (y, suffix) {
y.define('textarea', Y.Text).bind(document.getElementById('textarea' + suffix))
y.connector.socket.on('connection', function () {
document.getElementById('container' + suffix).removeAttribute('disconnected')
})
y.connector.socket.on('disconnect', function () {
document.getElementById('container' + suffix).setAttribute('disconnected', true)
})
}
let y1 = new Y('infinite-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.y1 = y1
bindYjsInstance(y1, '1')
let y2 = new Y('infinite-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.y2 = y2
bindYjsInstance(y2, '2')
let y3 = new Y('infinite-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.y3 = y3
bindYjsInstance(y1, '3')

View File

@@ -1,23 +0,0 @@
{
"name": "examples",
"version": "0.0.0",
"description": "",
"scripts": {
"dist": "rollup -c",
"watch": "rollup -cw"
},
"author": "Kevin Jahns",
"license": "MIT",
"dependencies": {
"monaco-editor": "^0.8.3",
"rollup": "^0.52.3"
},
"devDependencies": {
"standard": "^10.0.2"
},
"standard": {
"ignore": [
"bower_components"
]
}
}

84
examples/prosemirror.html Normal file
View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Prosemirror Example</title>
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
<style>
placeholder {
display: inline;
border: 1px solid #ccc;
color: #ccc;
}
placeholder:after {
content: "☁";
font-size: 200%;
line-height: 0.1;
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;
border-left-style: solid;
border-left-width: 2px;
border-color: orange;
height: 1em;
}
.ProseMirror-yjs-cursor > div {
position: relative;
top: -1.05em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-family: serif;
font-style: normal;
font-weight: normal;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
}
</style>
</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>
<!-- 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.js'
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
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 ydocument = provider.get('prosemirror')
const type = ydocument.define('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin])
})
})
window.example = { provider, ydocument, type, prosemirrorView }
</script>
</body>
</html>

24
examples/prosemirror.js Normal file
View File

@@ -0,0 +1,24 @@
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'
import { DOMParser } from 'prosemirror-model'
import { schema } from 'prosemirror-schema-basic'
import { exampleSetup } from 'prosemirror-example-setup'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror')
const type = ydocument.define('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin])
})
})
window.example = { provider, ydocument, type, prosemirrorView }

49
examples/quill.html Normal file
View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Quill Example</title>
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css">
</head>
<body>
<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>
<!-- 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.js'
import { QuillBinding } from 'yjs/bindings/quill.js'
import Quill from 'quill'
const provider = new WebsocketProvider('wss://api.yjs.website')
const ydocument = provider.get('quill')
const ytext = ydocument.define('quill', Y.Text)
const quill = new Quill('#quill-container', {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
]
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
window.quillBinding = new QuillBinding(ytext, quill)
</script>
</body>
</html>

30
examples/quill.js Normal file
View File

@@ -0,0 +1,30 @@
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(conf.serverAddress)
const ydocument = provider.get('quill')
const ytext = ydocument.define('quill', Y.Text)
const quill = new Quill('#quill-container', {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
]
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
window.quillBinding = new QuillBinding(ytext, quill)

View File

@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- Main Quill library -->
<script src="../../node_modules/quill/dist/quill.min.js"></script>
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
<!-- Yjs Library and connector -->
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
</head>
<body>
<div id="quill-container">
<div id="quill">
</div>
</div>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,33 +0,0 @@
/* global Y, Quill */
let y = new Y('quill-cursors-0', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
let quill = new Quill('#quill-container', {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
]
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
let yText = y.define('quill', Y.Text)
let quillBinding = new Y.QuillBinding(yText, quill)
window.quillBinding = quillBinding
window.yText = yText
window.y = y
window.quill = quill

View File

@@ -1,29 +0,0 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
input: 'yjs-dist.js',
name: 'Y',
output: {
file: 'yjs-dist.js',
format: 'umd'
},
plugins: [
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs()
],
sourcemap: true,
banner: `
/**
* ${pkg.name} - ${pkg.description}
* @version v${pkg.version}
* @license ${pkg.license}
*/
`
}

29
examples/style.css Normal file
View File

@@ -0,0 +1,29 @@
footer img {
display: none;
}
nav .title h1 a {
display: none;
}
footer {
background-color: #b93c1d;
}
#resizer {
background-color: #b93c1d;
}
.main section article.readme h1:first-child img {
display: none;
}
.main section article.readme h1:first-child {
margin-bottom: 16px;
margin-top: 30px;
}
.main section article.readme h1:first-child::before {
content: "Yjs";
font-size: 2em;
}

30
examples/textarea.html Normal file
View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Textarea Example</title>
</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>
</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.js'
import { TextareaBinding } from 'yjs/bindings/textarea.js'
const provider = new WebsocketProvider('wss://api.yjs.website')
const ydocument = provider.get('textarea')
const type = ydocument.define('textarea', Y.Text)
const textarea = document.querySelector('textarea')
const binding = new TextareaBinding(type, textarea)
window.textareaExample = {
provider, ydocument, type, textarea, binding
}
</script>
</body>
</html>

15
examples/textarea.js Normal file
View File

@@ -0,0 +1,15 @@
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { TextareaBinding } from '../bindings/textarea.js'
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')
const binding = new TextareaBinding(type, textarea)
window.textareaExample = {
provider, ydocument, type, textarea, binding
}

View File

@@ -1,7 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
<script type="module" src="./index.js"></script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
/* eslint-env browser */
import * as Y from '../../src/index.js'
import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
const provider = new WebsocketProvider('ws://localhost:1234/')
const ydocument = provider.get('textarea')
const type = ydocument.define('textarea', Y.Text)
const textarea = document.querySelector('textarea')
const binding = new Y.TextareaBinding(type, textarea)
window.textareaExample = {
provider, ydocument, type, textarea, binding
}

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<!-- jquery is not required for YXml. It is just here for convenience, and to test batch operations. -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</head>
<body>
<h1> Shared DOM Example </h1>
<p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
</div>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
</div>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
</div>
<script>
/* global $ */
var commands = document.querySelectorAll('.command')
Array.prototype.forEach.call(commands, function (command) {
var execute = function () {
// eslint-disable-next-line no-eval
eval(command.querySelector('input').value)
}
command.querySelector('button').onclick = execute
$(command.querySelector('input')).keyup(function (e) {
if (e.keyCode === 13) {
execute()
}
})
})
</script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
/* global Y */
let y = new Y('xml-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yXml = y
// bind xml type to a dom, and put it in body
window.sharedDom = y.define('xml', Y.XmlElement).toDom()
document.body.appendChild(window.sharedDom)

View File

@@ -14,7 +14,7 @@ let chatprotocol = y.define('chatprotocol', Y.Array)
let chatcontainer = document.querySelector('#chat') let chatcontainer = document.querySelector('#chat')
// This functions inserts a message at the specified position in the DOM // This functions inserts a message at the specified position in the DOM
function appendMessage (message, position) { const appendMessage = (message, position) => {
var p = document.createElement('p') var p = document.createElement('p')
var uname = document.createElement('span') var uname = document.createElement('span')
uname.appendChild(document.createTextNode(message.username + ': ')) uname.appendChild(document.createTextNode(message.username + ': '))
@@ -25,7 +25,7 @@ function appendMessage (message, position) {
// This function makes sure that only 7 messages exist in the chat history. // This function makes sure that only 7 messages exist in the chat history.
// The rest is deleted // The rest is deleted
function cleanupChat () { const cleanupChat = () => {
if (chatprotocol.length > 7) { if (chatprotocol.length > 7) {
chatprotocol.delete(0, chatprotocol.length - 7) chatprotocol.delete(0, chatprotocol.length - 7)
} }
@@ -36,7 +36,7 @@ cleanupChat()
chatprotocol.toArray().forEach(appendMessage) chatprotocol.toArray().forEach(appendMessage)
// whenever content changes, make sure to reflect the changes in the DOM // whenever content changes, make sure to reflect the changes in the DOM
chatprotocol.observe(function (event) { chatprotocol.observe(event => {
// concurrent insertions may result in a history > 7, so cleanup here // concurrent insertions may result in a history > 7, so cleanup here
cleanupChat() cleanupChat()
chatcontainer.innerHTML = '' chatcontainer.innerHTML = ''

View File

@@ -1,7 +1,7 @@
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js' import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
import Y from '../../src/Y.js' import Y from '../../src/Y.js'
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.js' import DomBinding from '../../bindings/DomBinding/DomBinding.js'
import UndoManager from '../../src/Util/UndoManager.js' import UndoManager from '../../src/Util/UndoManager.js'
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js' import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
import YXmlText from '../../src/Types/YXml/YXmlText.js' import YXmlText from '../../src/Types/YXml/YXmlText.js'

View File

@@ -18,7 +18,6 @@
</style> </style>
<script src="../../y.js"></script> <script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script> <script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src='../../../y-indexeddb/y-indexeddb.js'></script>
<script src="./index.js"></script> <script src="./index.js"></script>
</body> </body>
</html> </html>

View File

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

View File

@@ -36,13 +36,13 @@ let quill = new Quill('#quill-container', {
let cursors = quill.getModule('cursors') let cursors = quill.getModule('cursors')
function drawCursors () { const drawCursors = () => {
cursors.clearCursors() cursors.clearCursors()
users.map((user, userId) => { users.map((user, userId) => {
if (user !== myUserInfo) { if (user !== myUserInfo) {
let relativeRange = user.get('range') let relativeRange = user.get('range')
let lastUpdated = new Date(user.get('last updated')) let lastUpdated = new Date(user.get('last updated')).getTime()
if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) { if (lastUpdated != null && new Date().getTime() - lastUpdated < 20000 && relativeRange != null) {
let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
let range = { index: start, length: end - start } let range = { index: start, length: end - start }

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

@@ -2,7 +2,7 @@
/** /**
* Handles named events. * Handles named events.
*/ */
export default class NamedEventHandler { export class NamedEventHandler {
constructor () { constructor () {
this._eventListener = new Map() this._eventListener = new Map()
this._stateListener = new Map() this._stateListener = new Map()
@@ -57,7 +57,7 @@ export default class NamedEventHandler {
let state = this._stateListener.get(name) let state = this._stateListener.get(name)
if (state === undefined) { if (state === undefined) {
state = {} state = {}
state.promise = new Promise(function (resolve) { state.promise = new Promise(resolve => {
state.resolve = resolve state.resolve = resolve
}) })
this._stateListener.set(name, state) this._stateListener.set(name, state)

View File

@@ -1,5 +1,8 @@
/**
* @module tree
*/
function rotate (tree, parent, newParent, n) { const rotate = (tree, parent, newParent, n) => {
if (parent === null) { if (parent === null) {
tree.root = newParent tree.root = newParent
newParent._parent = null newParent._parent = null
@@ -111,10 +114,16 @@ class N {
} }
} }
const isBlack = node =>
node !== null ? node.isBlack() : true
const isRed = (node) =>
node !== null ? node.isRed() : false
/* /*
* This is a Red Black Tree implementation * This is a Red Black Tree implementation
*/ */
export default class Tree { export class Tree {
constructor () { constructor () {
this.root = null this.root = null
this.length = 0 this.length = 0
@@ -310,12 +319,6 @@ export default class Tree {
} }
} }
_fixDelete (n) { _fixDelete (n) {
function isBlack (node) {
return node !== null ? node.isBlack() : true
}
function isRed (node) {
return node !== null ? node.isRed() : false
}
if (n.parent === null) { if (n.parent === null) {
// this can only be called after the first iteration of fixDelete. // this can only be called after the first iteration of fixDelete.
return return

View File

@@ -1,3 +1,11 @@
/* eslint-env browser */
/**
* @module binary
*/
import * as string from './string.js'
import * as globals from './globals.js'
export const BITS32 = 0xFFFFFFFF export const BITS32 = 0xFFFFFFFF
export const BITS21 = (1 << 21) - 1 export const BITS21 = (1 << 21) - 1
@@ -5,3 +13,28 @@ export const BITS16 = (1 << 16) - 1
export const BIT26 = 1 << 26 export const BIT26 = 1 << 26
export const BIT32 = 1 << 32 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
}

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

@@ -1,3 +1,6 @@
/**
* @module decoding
*/
/* global Buffer */ /* global Buffer */
@@ -17,17 +20,26 @@ export class Decoder {
} }
/** /**
* @function
* @param {ArrayBuffer} buffer * @param {ArrayBuffer} buffer
* @return {Decoder} * @return {Decoder}
*/ */
export const createDecoder = buffer => new Decoder(buffer) export const createDecoder = buffer => new Decoder(buffer)
/**
* @function
* @param {Decoder} decoder
* @return {boolean}
*/
export const hasContent = decoder => decoder.pos !== decoder.arr.length export const hasContent = decoder => decoder.pos !== decoder.arr.length
/** /**
* Clone a decoder instance. * Clone a decoder instance.
* Optionally set a new position parameter. * Optionally set a new position parameter.
*
* @function
* @param {Decoder} decoder The decoder instance * @param {Decoder} decoder The decoder instance
* @param {number} [newPos] Defaults to current position
* @return {Decoder} A clone of `decoder` * @return {Decoder} A clone of `decoder`
*/ */
export const clone = (decoder, newPos = decoder.pos) => { export const clone = (decoder, newPos = decoder.pos) => {
@@ -38,6 +50,7 @@ export const clone = (decoder, newPos = decoder.pos) => {
/** /**
* Read `len` bytes as an ArrayBuffer. * Read `len` bytes as an ArrayBuffer.
* @function
* @param {Decoder} decoder The decoder instance * @param {Decoder} decoder The decoder instance
* @param {number} len The length of bytes to read * @param {number} len The length of bytes to read
* @return {ArrayBuffer} * @return {ArrayBuffer}
@@ -52,6 +65,7 @@ export const readArrayBuffer = (decoder, len) => {
/** /**
* Read variable length payload as ArrayBuffer * Read variable length payload as ArrayBuffer
* @function
* @param {Decoder} decoder * @param {Decoder} decoder
* @return {ArrayBuffer} * @return {ArrayBuffer}
*/ */
@@ -59,6 +73,7 @@ export const readPayload = decoder => readArrayBuffer(decoder, readVarUint(decod
/** /**
* Read the rest of the content as an ArrayBuffer * Read the rest of the content as an ArrayBuffer
* @function
* @param {Decoder} decoder * @param {Decoder} decoder
* @return {ArrayBuffer} * @return {ArrayBuffer}
*/ */
@@ -66,6 +81,7 @@ export const readTail = decoder => readArrayBuffer(decoder, decoder.arr.length -
/** /**
* Skip one byte, jump to the next position. * Skip one byte, jump to the next position.
* @function
* @param {Decoder} decoder The decoder instance * @param {Decoder} decoder The decoder instance
* @return {number} The next position * @return {number} The next position
*/ */
@@ -73,6 +89,7 @@ export const skip8 = decoder => decoder.pos++
/** /**
* Read one byte as unsigned integer. * Read one byte as unsigned integer.
* @function
* @param {Decoder} decoder The decoder instance * @param {Decoder} decoder The decoder instance
* @return {number} Unsigned 8-bit integer * @return {number} Unsigned 8-bit integer
*/ */
@@ -81,6 +98,7 @@ export const readUint8 = decoder => decoder.arr[decoder.pos++]
/** /**
* Read 4 bytes as unsigned integer. * Read 4 bytes as unsigned integer.
* *
* @function
* @param {Decoder} decoder * @param {Decoder} decoder
* @return {number} An unsigned integer. * @return {number} An unsigned integer.
*/ */
@@ -98,6 +116,7 @@ export const readUint32 = decoder => {
* Look ahead without incrementing position. * Look ahead without incrementing position.
* to the next byte and read it as unsigned integer. * to the next byte and read it as unsigned integer.
* *
* @function
* @param {Decoder} decoder * @param {Decoder} decoder
* @return {number} An unsigned integer. * @return {number} An unsigned integer.
*/ */
@@ -109,6 +128,7 @@ export const peekUint8 = decoder => decoder.arr[decoder.pos]
* * numbers < 2^7 is stored in one bytlength * * numbers < 2^7 is stored in one bytlength
* * numbers < 2^14 is stored in two bylength * * numbers < 2^14 is stored in two bylength
* *
* @function
* @param {Decoder} decoder * @param {Decoder} decoder
* @return {number} An unsigned integer.length * @return {number} An unsigned integer.length
*/ */
@@ -128,6 +148,21 @@ export const readVarUint = decoder => {
} }
} }
/**
* Look ahead and read varUint without incrementing position
*
* @function
* @param {Decoder} decoder
* @return {number}
*/
export const peekVarUint = decoder => {
let pos = decoder.pos
let s = readVarUint(decoder)
decoder.pos = pos
return s
}
/** /**
* Read string of variable length * Read string of variable length
* * varUint is used to store the length of the string * * varUint is used to store the length of the string
@@ -137,6 +172,7 @@ export const readVarUint = decoder => {
* But most environments have a maximum number of arguments per functions. * But most environments have a maximum number of arguments per functions.
* For effiency reasons we apply a maximum of 10000 characters at once. * For effiency reasons we apply a maximum of 10000 characters at once.
* *
* @function
* @param {Decoder} decoder * @param {Decoder} decoder
* @return {String} The read String. * @return {String} The read String.
*/ */
@@ -157,6 +193,8 @@ export const readVarString = decoder => {
/** /**
* Look ahead and read varString without incrementing position * Look ahead and read varString without incrementing position
*
* @function
* @param {Decoder} decoder * @param {Decoder} decoder
* @return {string} * @return {string}
*/ */
@@ -166,3 +204,4 @@ export const peekVarString = decoder => {
decoder.pos = pos decoder.pos = pos
return s return s
} }

View File

@@ -1,3 +1,6 @@
/**
* @module diff
*/
/** /**
* A SimpleDiff describes a change on a String. * A SimpleDiff describes a change on a String.
@@ -27,7 +30,7 @@
* @param {String} b The updated version of the string * @param {String} b The updated version of the string
* @return {SimpleDiff} The diff description. * @return {SimpleDiff} The diff description.
*/ */
export default function simpleDiff (a, b) { export const simpleDiff = (a, b) => {
let left = 0 // number of same characters counting from left let left = 0 // number of same characters counting from left
let right = 0 // number of same characters counting from right let right = 0 // number of same characters counting from right
while (left < a.length && left < b.length && a[left] === b[left]) { while (left < a.length && left < b.length && a[left] === b[left]) {

View File

@@ -1,4 +1,6 @@
/**
* @module encoding
*/
import * as globals from './globals.js' import * as globals from './globals.js'
const bits7 = 0b1111111 const bits7 = 0b1111111
@@ -15,10 +17,18 @@ export class Encoder {
} }
} }
/**
* @function
* @return {Encoder}
*/
export const createEncoder = () => new Encoder() export const createEncoder = () => new Encoder()
/** /**
* The current length of the encoded data. * The current length of the encoded data.
*
* @function
* @param {Encoder} encoder
* @return {number}
*/ */
export const length = encoder => { export const length = encoder => {
let len = encoder.cpos let len = encoder.cpos
@@ -30,6 +40,8 @@ export const length = encoder => {
/** /**
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer * Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
*
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @return {ArrayBuffer} The created ArrayBuffer. * @return {ArrayBuffer} The created ArrayBuffer.
*/ */
@@ -48,6 +60,7 @@ export const toBuffer = encoder => {
/** /**
* Write one byte to the encoder. * Write one byte to the encoder.
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} num The byte that is to be encoded. * @param {number} num The byte that is to be encoded.
*/ */
@@ -64,6 +77,7 @@ export const write = (encoder, num) => {
* Write one byte at a specific position. * Write one byte at a specific position.
* Position must already be written (i.e. encoder.length > pos) * Position must already be written (i.e. encoder.length > pos)
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} pos Position to which to write data * @param {number} pos Position to which to write data
* @param {number} num Unsigned 8-bit integer * @param {number} num Unsigned 8-bit integer
@@ -89,6 +103,7 @@ export const set = (encoder, pos, num) => {
/** /**
* Write one byte as an unsigned integer. * Write one byte as an unsigned integer.
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} num The number that is to be encoded. * @param {number} num The number that is to be encoded.
*/ */
@@ -97,6 +112,7 @@ export const writeUint8 = (encoder, num) => write(encoder, num & bits8)
/** /**
* Write one byte as an unsigned Integer at a specific location. * Write one byte as an unsigned Integer at a specific location.
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} pos The location where the data will be written. * @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded. * @param {number} num The number that is to be encoded.
@@ -106,6 +122,7 @@ export const setUint8 = (encoder, pos, num) => set(encoder, pos, num & bits8)
/** /**
* Write two bytes as an unsigned integer. * Write two bytes as an unsigned integer.
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} num The number that is to be encoded. * @param {number} num The number that is to be encoded.
*/ */
@@ -116,6 +133,7 @@ export const writeUint16 = (encoder, num) => {
/** /**
* Write two bytes as an unsigned integer at a specific location. * Write two bytes as an unsigned integer at a specific location.
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} pos The location where the data will be written. * @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded. * @param {number} num The number that is to be encoded.
@@ -128,6 +146,7 @@ export const setUint16 = (encoder, pos, num) => {
/** /**
* Write two bytes as an unsigned integer * Write two bytes as an unsigned integer
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} num The number that is to be encoded. * @param {number} num The number that is to be encoded.
*/ */
@@ -141,6 +160,7 @@ export const writeUint32 = (encoder, num) => {
/** /**
* Write two bytes as an unsigned integer at a specific location. * Write two bytes as an unsigned integer at a specific location.
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} pos The location where the data will be written. * @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded. * @param {number} num The number that is to be encoded.
@@ -157,6 +177,7 @@ export const setUint32 = (encoder, pos, num) => {
* *
* Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer) * Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {number} num The number that is to be encoded. * @param {number} num The number that is to be encoded.
*/ */
@@ -171,6 +192,7 @@ export const writeVarUint = (encoder, num) => {
/** /**
* Write a variable length string. * Write a variable length string.
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {String} str The string that is to be encoded. * @param {String} str The string that is to be encoded.
*/ */
@@ -188,6 +210,7 @@ export const writeVarString = (encoder, str) => {
* *
* TODO: can be improved! * TODO: can be improved!
* *
* @function
* @param {Encoder} encoder The enUint8Arr * @param {Encoder} encoder The enUint8Arr
* @param {Encoder} append The BinaryEncoder to be written. * @param {Encoder} append The BinaryEncoder to be written.
*/ */
@@ -196,6 +219,7 @@ export const writeBinaryEncoder = (encoder, append) => writeArrayBuffer(encoder,
/** /**
* Append an arrayBuffer to the encoder. * Append an arrayBuffer to the encoder.
* *
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {ArrayBuffer} arrayBuffer * @param {ArrayBuffer} arrayBuffer
*/ */
@@ -209,6 +233,7 @@ export const writeArrayBuffer = (encoder, arrayBuffer) => {
} }
/** /**
* @function
* @param {Encoder} encoder * @param {Encoder} encoder
* @param {ArrayBuffer} arrayBuffer * @param {ArrayBuffer} arrayBuffer
*/ */

View File

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

View File

@@ -1,3 +1,7 @@
/**
* @module lib/idb
*/
/* eslint-env browser */ /* eslint-env browser */
import * as globals from './globals.js' import * as globals from './globals.js'
@@ -12,6 +16,8 @@ export const rtop = request => globals.createPromise((resolve, reject) => {
}) })
/** /**
* @param {string} name
* @param {Function} initDB Called when the database is first created
* @return {Promise<IDBDatabase>} * @return {Promise<IDBDatabase>}
*/ */
export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => { export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => {
@@ -115,7 +121,7 @@ export const getAllKeysValues = (store, range) =>
* Iterate on keys and values * Iterate on keys and values
* @param {IDBObjectStore} store * @param {IDBObjectStore} store
* @param {IDBKeyRange?} keyrange * @param {IDBKeyRange?} keyrange
* @param {function(any, any)} f Return true in order to continue the cursor * @param {Function} f Return true in order to continue the cursor
*/ */
export const iterate = (store, keyrange, f) => globals.createPromise((resolve, reject) => { export const iterate = (store, keyrange, f) => globals.createPromise((resolve, reject) => {
const request = store.openCursor(keyrange) const request = store.openCursor(keyrange)
@@ -135,9 +141,10 @@ export const iterate = (store, keyrange, f) => globals.createPromise((resolve, r
/** /**
* Iterate on the keys (no values) * Iterate on the keys (no values)
*
* @param {IDBObjectStore} store * @param {IDBObjectStore} store
* @param {IDBKeyRange} keyrange * @param {IDBKeyRange} keyrange
* @param {function(IDBCursor)} f Call `idbcursor.continue()` to iterate further * @param {function} f Call `idbcursor.continue()` to iterate further
*/ */
export const iterateKeys = (store, keyrange, f) => { export const iterateKeys = (store, keyrange, f) => {
/** /**

View File

@@ -1,4 +1,4 @@
import * as test from './test.js' import * as test from './testing.js'
import * as idb from './idb.js' import * as idb from './idb.js'
import * as logging from './logging.js' import * as logging from './logging.js'

View File

@@ -1,3 +1,6 @@
/**
* @module logging
*/
import * as globals from './globals.js' import * as globals from './globals.js'

View File

@@ -1,2 +1,28 @@
/**
* @module math
*/
export const floor = Math.floor 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

@@ -4,11 +4,10 @@
* *
* @example * @example
* const mutex = createMutex() * const mutex = createMutex()
* mutex(function () { * mutex(() => {
* // This function is immediately executed * // This function is immediately executed
* mutex(function () { * mutex(() => {
* // This function is never executed, as it is called with the same * // This function is not executed, as the mutex is already active.
* // mutex function
* }) * })
* }) * })
* *

View File

@@ -1,2 +1,6 @@
/**
* @module number
*/
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER

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

@@ -1,11 +1,12 @@
/**
* @module prng
*/
const N = 624 const N = 624
const M = 397 const M = 397
function twist (u, v) { const twist = (u, v) => ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
return ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
}
function nextState (state) { const nextState = (state) => {
let p = 0 let p = 0
let j let j
for (j = N - M + 1; --j; p++) { for (j = N - M + 1; --j; p++) {
@@ -29,7 +30,7 @@ function nextState (state) {
* *
* @public * @public
*/ */
export default class Mt19937 { export class Mt19937 {
/** /**
* @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers. * @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
*/ */

View File

@@ -1,13 +1,16 @@
/**
* @module prng
*/
import Mt19937 from './Mt19937.js' import { Mt19937 } from './Mt19937.js'
import Xoroshiro128plus from './Xoroshiro128plus.js' import { Xoroshiro128plus } from './Xoroshiro128plus.js'
import Xorshift32 from './Xorshift32.js' import { Xorshift32 } from './Xorshift32.js'
import * as time from '../../time.js' import * as time from '../../time.js'
const DIAMETER = 300 const DIAMETER = 300
const NUMBERS = 10000 const NUMBERS = 10000
function runPRNG (name, Gen) { const runPRNG = (name, Gen) => {
console.log('== ' + name + ' ==') console.log('== ' + name + ' ==')
const gen = new Gen(1234) const gen = new Gen(1234)
let head = 0 let head = 0

View File

@@ -1,5 +1,8 @@
/**
* @module prng
*/
import Xorshift32 from './Xorshift32.js' import { Xorshift32 } from './Xorshift32.js'
/** /**
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures. * This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
@@ -14,7 +17,7 @@ import Xorshift32 from './Xorshift32.js'
* *
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c) * [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
*/ */
export default class Xoroshiro128plus { export class Xoroshiro128plus {
constructor (seed) { constructor (seed) {
this.seed = seed this.seed = seed
// This is a variant of Xoroshiro128plus to fill the initial state // This is a variant of Xoroshiro128plus to fill the initial state

View File

@@ -1,8 +1,11 @@
/**
* @module prng
*/
/** /**
* Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`. * Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`.
*/ */
export default class Xorshift32 { export class Xorshift32 {
/** /**
* @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers. * @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
*/ */

View File

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

View File

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

3
lib/random.js Normal file
View File

@@ -0,0 +1,3 @@
/**
* @module random
*/

View File

@@ -1,2 +1,6 @@
/**
* @module string
*/
export const fromCharCode = String.fromCharCode export const fromCharCode = String.fromCharCode
export const fromCodePoint = String.fromCodePoint export const fromCodePoint = String.fromCodePoint

View File

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

579
package-lock.json generated
View File

@@ -1,21 +1,94 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-66", "version": "13.0.0-76",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@babel/code-frame": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
"integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
"dev": true,
"requires": {
"@babel/highlight": "^7.0.0"
}
},
"@babel/highlight": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
"integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
"dev": true,
"requires": {
"chalk": "^2.0.0",
"esutils": "^2.0.2",
"js-tokens": "^4.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"@types/estree": { "@types/estree": {
"version": "0.0.38", "version": "0.0.38",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.38.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.38.tgz",
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==", "integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
"dev": true "dev": true
}, },
"@types/events": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "6.0.110", "version": "6.0.110",
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==", "integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
"dev": true "dev": true
}, },
"@types/ws": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz",
"integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/node": "*"
}
},
"abab": { "abab": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@@ -110,17 +183,6 @@
"integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=",
"dev": true "dev": true
}, },
"align-text": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
"dev": true,
"requires": {
"kind-of": "^3.0.2",
"longest": "^1.0.1",
"repeat-string": "^1.5.2"
}
},
"ansi-escapes": { "ansi-escapes": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
@@ -1577,6 +1639,12 @@
"integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=", "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=",
"dev": true "dev": true
}, },
"bluebird": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
"integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==",
"dev": true
},
"boolbase": { "boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -1631,12 +1699,6 @@
"integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
"dev": true "dev": true
}, },
"camelcase": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
"integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
"dev": true
},
"camelcase-keys": { "camelcase-keys": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
@@ -1662,22 +1724,13 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"center-align": { "catharsis": {
"version": "0.1.3", "version": "0.8.9",
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz",
"integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", "integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=",
"dev": true, "dev": true,
"requires": { "requires": {
"align-text": "^0.1.3", "underscore-contrib": "~0.3.0"
"lazy-cache": "^1.0.3"
},
"dependencies": {
"lazy-cache": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
"integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=",
"dev": true
}
} }
}, },
"chalk": { "chalk": {
@@ -1761,17 +1814,6 @@
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
"dev": true "dev": true
}, },
"cliui": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
"integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
"dev": true,
"requires": {
"center-align": "^0.1.1",
"right-align": "^0.1.1",
"wordwrap": "0.0.2"
}
},
"clone": { "clone": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -2012,6 +2054,12 @@
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true "dev": true
}, },
"crel": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/crel/-/crel-3.1.0.tgz",
"integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg==",
"dev": true
},
"cross-spawn": { "cross-spawn": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@@ -3073,9 +3121,9 @@
} }
}, },
"extend": { "extend": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true "dev": true
}, },
"extend-shallow": { "extend-shallow": {
@@ -3978,6 +4026,12 @@
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true "dev": true
}, },
"graceful-readlink": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
"dev": true
},
"har-schema": { "har-schema": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -4082,7 +4136,7 @@
}, },
"http-errors": { "http-errors": {
"version": "1.6.3", "version": "1.6.3",
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"dev": true, "dev": true,
"requires": { "requires": {
@@ -4579,6 +4633,15 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"jest-worker": {
"version": "23.2.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz",
"integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=",
"dev": true,
"requires": {
"merge-stream": "^1.0.1"
}
},
"js-tokens": { "js-tokens": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@@ -4603,12 +4666,64 @@
} }
} }
}, },
"js2xmlparser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz",
"integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=",
"dev": true,
"requires": {
"xmlcreate": "^1.0.1"
}
},
"jsbn": { "jsbn": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true "dev": true
}, },
"jsdoc": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz",
"integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==",
"dev": true,
"requires": {
"babylon": "7.0.0-beta.19",
"bluebird": "~3.5.0",
"catharsis": "~0.8.9",
"escape-string-regexp": "~1.0.5",
"js2xmlparser": "~3.0.0",
"klaw": "~2.0.0",
"marked": "~0.3.6",
"mkdirp": "~0.5.1",
"requizzle": "~0.2.1",
"strip-json-comments": "~2.0.1",
"taffydb": "2.6.2",
"underscore": "~1.8.3"
},
"dependencies": {
"babylon": {
"version": "7.0.0-beta.19",
"resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz",
"integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==",
"dev": true
},
"klaw": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz",
"integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.9"
}
},
"taffydb": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz",
"integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=",
"dev": true
}
}
},
"jsdom": { "jsdom": {
"version": "7.2.2", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz",
@@ -5003,12 +5118,6 @@
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=",
"dev": true "dev": true
}, },
"longest": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
"dev": true
},
"loose-envify": { "loose-envify": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
@@ -5040,7 +5149,7 @@
}, },
"magic-string": { "magic-string": {
"version": "0.22.5", "version": "0.22.5",
"resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
"integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==",
"dev": true, "dev": true,
"requires": { "requires": {
@@ -5061,7 +5170,7 @@
}, },
"marked": { "marked": {
"version": "0.3.19", "version": "0.3.19",
"resolved": "http://registry.npmjs.org/marked/-/marked-0.3.19.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz",
"integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==",
"dev": true "dev": true
}, },
@@ -5114,6 +5223,15 @@
} }
} }
}, },
"merge-stream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz",
"integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=",
"dev": true,
"requires": {
"readable-stream": "^2.0.1"
}
},
"micromatch": { "micromatch": {
"version": "2.3.11", "version": "2.3.11",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
@@ -5363,6 +5481,12 @@
} }
} }
}, },
"orderedmap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.0.0.tgz",
"integrity": "sha1-2Q/Cuh7QhRkJB9YB3sbmpT+NQbo=",
"dev": true
},
"os-homedir": { "os-homedir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
@@ -5412,7 +5536,7 @@
}, },
"parchment": { "parchment": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "http://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"dev": true "dev": true
}, },
@@ -5656,6 +5780,158 @@
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
} }
}, },
"prosemirror-commands": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.0.7.tgz",
"integrity": "sha512-IR8yMSdw7XlKuF68tydAak1J9P/lLD5ohsrL7pzoLsJAJAQU7mVPDXtGbQrrm0mesddFjcc1zNo/cJQN3lRYnA==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-dropcursor": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.1.tgz",
"integrity": "sha512-GeUyMO/tOEf8MXrP7Xb7UIMrfK86OGh0fnyBrHfhav4VjY9cw65mNoqHy87CklE5711AhCP5Qzfp8RL/hVKusg==",
"dev": true,
"requires": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"prosemirror-example-setup": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.0.1.tgz",
"integrity": "sha512-4NKWpdmm75Zzgq/dIrypRnkBNPx+ONKyoGF42a9g3VIVv0TWglf1CBNxt5kzCgli9xdfut/xE5B42F9DR6BLHw==",
"dev": true,
"requires": {
"prosemirror-commands": "^1.0.0",
"prosemirror-dropcursor": "^1.0.0",
"prosemirror-gapcursor": "^1.0.0",
"prosemirror-history": "^1.0.0",
"prosemirror-inputrules": "^1.0.0",
"prosemirror-keymap": "^1.0.0",
"prosemirror-menu": "^1.0.0",
"prosemirror-schema-list": "^1.0.0",
"prosemirror-state": "^1.0.0"
}
},
"prosemirror-gapcursor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.3.tgz",
"integrity": "sha512-X+hJhr42PcHWiSWL+lI5f/UeOhXCxlBFb8M6O8aG1hssmaRrW7sS2/Fjg5jFV+pTdS1REFkmm1occh01FMdDIQ==",
"dev": true,
"requires": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
}
},
"prosemirror-history": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.0.3.tgz",
"integrity": "sha512-IfFGbhafSx+R3aq7nLJGkXeu2iaUiP8mkU3aRu2uQcIIjU8Fq7RJfuvhIOJ2RNUoSyqF/ANkdTjnZ74F5eHs1Q==",
"dev": true,
"requires": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"rope-sequence": "^1.2.0"
}
},
"prosemirror-inputrules": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.0.1.tgz",
"integrity": "sha512-UHy22NmwxS5WIMQYkzraDttQAF8mpP82FfbJsmKFfx6jwkR/SZa+ZhbkLY0zKQ5fBdJN7euj36JG/B5iAlrpxA==",
"dev": true,
"requires": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-keymap": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz",
"integrity": "sha512-e79ApE7PXXZMFtPz7WbjycjAFd1NPjgY1MkecVz98tqwlBSggXWXYQnWFk6x7UkmnBYRHHbXHkR/RXmu2wyBJg==",
"dev": true,
"requires": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^1.1.8"
}
},
"prosemirror-menu": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.0.5.tgz",
"integrity": "sha512-9Vrn7CC191v7FA4QrAkL8W1SrR73V3CRIYCDuk94R8oFVk4VxSFdoKVLHuvGzxZ8b5LCu3DMJfh86YW9uL4RkQ==",
"dev": true,
"requires": {
"crel": "^3.0.0",
"prosemirror-commands": "^1.0.0",
"prosemirror-history": "^1.0.0",
"prosemirror-state": "^1.0.0"
}
},
"prosemirror-model": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.6.3.tgz",
"integrity": "sha512-iqIml664X9MUVGLz2nzK4xfAofX8+o7gs2mi2/k+pVD0qZ7th1Jm5eG3AsqWoEUIZuWeaOWCKpBl/dPnhIIWew==",
"dev": true,
"requires": {
"orderedmap": "^1.0.0"
}
},
"prosemirror-schema-basic": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.0.0.tgz",
"integrity": "sha512-xTFjtuLZgcRS4MoDbUyI9NSk/k/ACLGKZQcDXH18ctM9BOmP4z5rGZcA014fCF2FnMFOU+lKwusL0JjVrEectQ==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0"
}
},
"prosemirror-schema-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.0.1.tgz",
"integrity": "sha512-AiLIX6qm6PEeDtMCKZLcSLi55WXo1ls7DnRK+4hSkoi0IIzNdxGsRlecCd3MzEu//DVz3nAEh+zEmslyW+uk8g==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.2.2.tgz",
"integrity": "sha512-j8aC/kf9BJSCQau485I/9pj39XQoce+TqH5xzekT7WWFARTsRYFLJtiXBcCKakv1VSeev+sC3bJP0pLfz7Ft8g==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-transform": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.1.3.tgz",
"integrity": "sha512-1O6Di5lOL1mp4nuCnQNkHY7l2roIW5y8RH4ZG3hMYmkmDEWzTaFFnxxAAHsE5ipGLBSRcTlP7SsDhYBIdSuLpQ==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0"
}
},
"prosemirror-view": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.6.5.tgz",
"integrity": "sha512-brg8fExNrmklbLs8VJ7uvmo/Lh93EHErH47alI55hkJ12EF73K+t2+IyrlkJF84tt5wFBJ20LeSxF8HlJHXiYg==",
"dev": true,
"requires": {
"prosemirror-model": "^1.1.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"pseudomap": { "pseudomap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -5684,7 +5960,7 @@
}, },
"quill": { "quill": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "http://registry.npmjs.org/quill/-/quill-1.3.6.tgz", "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
"integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==", "integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==",
"dev": true, "dev": true,
"requires": { "requires": {
@@ -5715,14 +5991,6 @@
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"extend": "^3.0.2", "extend": "^3.0.2",
"fast-diff": "1.1.2" "fast-diff": "1.1.2"
},
"dependencies": {
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
}
} }
}, },
"randomatic": { "randomatic": {
@@ -6034,6 +6302,23 @@
"resolve-from": "^1.0.0" "resolve-from": "^1.0.0"
} }
}, },
"requizzle": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz",
"integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=",
"dev": true,
"requires": {
"underscore": "~1.6.0"
},
"dependencies": {
"underscore": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
"integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=",
"dev": true
}
}
},
"resolve": { "resolve": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz",
@@ -6069,15 +6354,6 @@
"signal-exit": "^3.0.2" "signal-exit": "^3.0.2"
} }
}, },
"right-align": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
"integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
"dev": true,
"requires": {
"align-text": "^0.1.1"
}
},
"rimraf": { "rimraf": {
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
@@ -6097,6 +6373,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"rollup-cli": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/rollup-cli/-/rollup-cli-1.0.9.tgz",
"integrity": "sha1-N/ShwgYxHikuMpfql3eduKIduZQ=",
"dev": true
},
"rollup-plugin-babel": { "rollup-plugin-babel": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz",
@@ -6111,7 +6393,7 @@
}, },
"rollup-plugin-commonjs": { "rollup-plugin-commonjs": {
"version": "8.4.1", "version": "8.4.1",
"resolved": "http://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.4.1.tgz", "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.4.1.tgz",
"integrity": "sha512-mg+WuD+jlwoo8bJtW3Mvx7Tz6TsIdMsdhuvCnDMoyjh0oxsVgsjB/N0X984RJCWwc5IIiqNVJhXeeITcc73++A==", "integrity": "sha512-mg+WuD+jlwoo8bJtW3Mvx7Tz6TsIdMsdhuvCnDMoyjh0oxsVgsjB/N0X984RJCWwc5IIiqNVJhXeeITcc73++A==",
"dev": true, "dev": true,
"requires": { "requires": {
@@ -6216,12 +6498,46 @@
} }
}, },
"rollup-plugin-uglify": { "rollup-plugin-uglify": {
"version": "1.0.2", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-1.0.2.tgz", "resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.0.tgz",
"integrity": "sha1-1KpvXfE1Iurhuhd4DHxMcJYDg1k=", "integrity": "sha512-XtzZd159QuOaXNvcxyBcbUCSoBsv5YYWK+7ZwUyujSmISst8avRfjWlp7cGu8T2O52OJnpEBvl+D4WLV1k1iQQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"uglify-js": "^2.6.1" "@babel/code-frame": "^7.0.0",
"jest-worker": "^23.2.0",
"serialize-javascript": "^1.5.0",
"uglify-js": "^3.4.9"
}
},
"rollup-plugin-uglify-es": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-uglify-es/-/rollup-plugin-uglify-es-0.0.1.tgz",
"integrity": "sha1-5FZE8raFpZq9uTY0ByB6A6e1qbc=",
"dev": true,
"requires": {
"uglify-es": "3.0.3"
},
"dependencies": {
"commander": {
"version": "2.9.0",
"resolved": "http://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
"integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
"dev": true,
"requires": {
"graceful-readlink": ">= 1.0.0"
}
},
"uglify-es": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.0.3.tgz",
"integrity": "sha1-Y8yEqpRos0lzpIh3h8ZMAaiodXY=",
"dev": true,
"requires": {
"commander": "~2.9.0",
"source-map": "~0.5.1",
"uglify-to-browserify": "~1.0.0"
}
}
} }
}, },
"rollup-pluginutils": { "rollup-pluginutils": {
@@ -6252,6 +6568,12 @@
"require-relative": "0.8.7" "require-relative": "0.8.7"
} }
}, },
"rope-sequence": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.2.2.tgz",
"integrity": "sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4=",
"dev": true
},
"run-async": { "run-async": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
@@ -6313,6 +6635,12 @@
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true "dev": true
}, },
"serialize-javascript": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz",
"integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==",
"dev": true
},
"serve-index": { "serve-index": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
@@ -6804,6 +7132,15 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true "dev": true
}, },
"tui-jsdoc-template": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tui-jsdoc-template/-/tui-jsdoc-template-1.2.2.tgz",
"integrity": "sha512-oqw0IYaot86VJ2owKBozJnilgta0Z55x8r9PeHj7vb+jDoSvJGRUQUcgs56SZh9HE20fx54Pe75p84X85/ygLA==",
"dev": true,
"requires": {
"cheerio": "^0.22.0"
}
},
"tunnel-agent": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -6842,14 +7179,27 @@
"dev": true "dev": true
}, },
"uglify-js": { "uglify-js": {
"version": "2.8.29", "version": "3.4.9",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
"integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"source-map": "~0.5.1", "commander": "~2.17.1",
"uglify-to-browserify": "~1.0.0", "source-map": "~0.6.1"
"yargs": "~3.10.0" },
"dependencies": {
"commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
} }
}, },
"uglify-to-browserify": { "uglify-to-browserify": {
@@ -6859,6 +7209,29 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"underscore": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=",
"dev": true
},
"underscore-contrib": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz",
"integrity": "sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=",
"dev": true,
"requires": {
"underscore": "1.6.0"
},
"dependencies": {
"underscore": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
"integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=",
"dev": true
}
}
},
"uniq": { "uniq": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
@@ -6950,6 +7323,12 @@
"integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==", "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==",
"dev": true "dev": true
}, },
"w3c-keyname": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-1.1.8.tgz",
"integrity": "sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA==",
"dev": true
},
"webidl-conversions": { "webidl-conversions": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz",
@@ -6997,18 +7376,6 @@
"isexe": "^2.0.0" "isexe": "^2.0.0"
} }
}, },
"window-size": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
"integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
"dev": true
},
"wordwrap": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
"integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
"dev": true
},
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -7039,6 +7406,12 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"xmlcreate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz",
"integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=",
"dev": true
},
"xtend": { "xtend": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
@@ -7050,18 +7423,6 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
"dev": true "dev": true
},
"yargs": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
"integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
"dev": true,
"requires": {
"camelcase": "^1.0.2",
"cliui": "^2.1.0",
"decamelize": "^1.0.0",
"window-size": "0.1.0"
}
} }
} }
} }

View File

@@ -1,30 +1,55 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-66", "version": "13.0.0-76",
"description": "A framework for real-time p2p shared editing on any data", "description": "A ",
"main": "./y.node.js", "main": "./build/yjs.js",
"browser": "./y.js", "module": "./index.js'",
"module": "./src/index.js", "sideEffects": false,
"scripts": { "scripts": {
"test": "npm run lint", "test": "npm run lint",
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'", "build": "rm -rf build examples/build && PRODUCTION=1 rollup -c",
"lint": "standard src/**/*.js test/**/*.js tests-lib/**/*.js", "watch": "rollup -wc",
"docs": "esdoc", "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/", "serve-docs": "npm run docs && serve ./docs/",
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js", "postversion": "npm run build",
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'", "websocket-server": "node ./provider/websocket/server.js",
"postversion": "npm run dist" "now-start": "npm run websocket-server"
}, },
"files": [ "files": [
"y.*", "build/*",
"src/*", "bindings/*",
".esdoc.json", "docs/*",
"docs/*" "examples/*",
"lib/*",
"persistences/*",
"protocols/*",
"provider/*",
"bindings/*",
"structs/*",
"tests/*",
"types/*",
"utils/*",
"index.js",
"README.md",
"LICENSE"
], ],
"dictionaries": {
"doc": "docs",
"example": "examples",
"test": "tests",
"lib": "./"
},
"bin": {
"y-websockets": "provider/websocket/server.js"
},
"standard": { "standard": {
"ignore": [ "ignore": [
"/y.js", "/build",
"/y.js.map" "/node_modules",
"/rollup.test.js",
"/rollup.test.js"
] ]
}, },
"repository": { "repository": {
@@ -32,13 +57,7 @@
"url": "https://github.com/y-js/yjs.git" "url": "https://github.com/y-js/yjs.git"
}, },
"keywords": [ "keywords": [
"Yjs", "crdt"
"OT",
"Collaboration",
"Synchronization",
"ShareJS",
"Coweb",
"Concurrency"
], ],
"author": "Kevin Jahns", "author": "Kevin Jahns",
"email": "kevin.jahns@rwth-aachen.de", "email": "kevin.jahns@rwth-aachen.de",
@@ -48,6 +67,7 @@
}, },
"homepage": "http://y-js.org", "homepage": "http://y-js.org",
"devDependencies": { "devDependencies": {
"@types/ws": "^6.0.1",
"babel-cli": "^6.26.0", "babel-cli": "^6.26.0",
"babel-plugin-external-helpers": "^6.22.0", "babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-transform-regenerator": "^6.26.0", "babel-plugin-transform-regenerator": "^6.26.0",
@@ -57,18 +77,26 @@
"cutest": "^0.1.9", "cutest": "^0.1.9",
"esdoc": "^1.1.0", "esdoc": "^1.1.0",
"esdoc-standard-plugin": "^1.0.0", "esdoc-standard-plugin": "^1.0.0",
"jsdoc": "^3.5.5",
"prosemirror-example-setup": "^1.0.1",
"prosemirror-schema-basic": "^1.0.0",
"prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.6.5",
"quill": "^1.3.6", "quill": "^1.3.6",
"quill-cursors": "^1.0.3", "quill-cursors": "^1.0.3",
"rollup": "^0.58.2", "rollup": "^0.58.2",
"rollup-cli": "^1.0.9",
"rollup-plugin-babel": "^2.7.1", "rollup-plugin-babel": "^2.7.1",
"rollup-plugin-commonjs": "^8.4.1", "rollup-plugin-commonjs": "^8.4.1",
"rollup-plugin-inject": "^2.2.0", "rollup-plugin-inject": "^2.2.0",
"rollup-plugin-multi-entry": "^2.0.2", "rollup-plugin-multi-entry": "^2.0.2",
"rollup-plugin-node-resolve": "^3.4.0", "rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-uglify": "^1.0.2", "rollup-plugin-uglify": "^6.0.0",
"rollup-plugin-uglify-es": "0.0.1",
"rollup-regenerator-runtime": "^6.23.1", "rollup-regenerator-runtime": "^6.23.1",
"rollup-watch": "^3.2.2", "rollup-watch": "^3.2.2",
"standard": "^11.0.1" "standard": "^11.0.1",
"tui-jsdoc-template": "^1.2.2"
}, },
"dependencies": { "dependencies": {
"ws": "^6.1.0" "ws": "^6.1.0"

View File

@@ -1,8 +1,9 @@
/*
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import * as encoding from '../../lib/encoding.js' import * as encoding from '../lib/encoding.js'
import * as decoding from '../../lib/decoding.js' import * as decoding from '../lib/decoding.js'
import { createMutualExclude } from '../../lib/mutualExclude.js' import { createMutex } from '../lib/mutex.js'
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js' import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
function createFilePath (persistence, roomName) { function createFilePath (persistence, roomName) {
@@ -10,10 +11,10 @@ function createFilePath (persistence, roomName) {
return path.join(persistence.dir, roomName) return path.join(persistence.dir, roomName)
} }
export default class FilePersistence { export class FilePersistence {
constructor (dir) { constructor (dir) {
this.dir = dir this.dir = dir
this._mutex = createMutualExclude() this._mutex = createMutex()
} }
setRemoteUpdateCounter (roomName, remoteUpdateCounter) { setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
// TODO: implement // TODO: implement
@@ -70,3 +71,4 @@ export default class FilePersistence {
}) })
} }
} }
*/

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,3 +1,4 @@
/*
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js' import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
import { writeStructs } from '../MessageHandler/syncStep1.js' import { writeStructs } from '../MessageHandler/syncStep1.js'
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js' import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
@@ -6,10 +7,10 @@ export const PERSIST_UPDATE = 0
/** /**
* Write an update to an encoder. * Write an update to an encoder.
* *
* @param {Yjs} y A Yjs instance * @param {Y} y A Yjs instance
* @param {BinaryEncoder} updateEncoder I.e. transaction.encodedStructs * @param {Encoder} updateEncoder I.e. transaction.encodedStructs
*/ *
export function encodeUpdate (y, updateEncoder, encoder) { export const encodeUpdate = (y, updateEncoder, encoder) => {
encoder.writeVarUint(PERSIST_UPDATE) encoder.writeVarUint(PERSIST_UPDATE)
encoder.writeBinaryEncoder(updateEncoder) encoder.writeBinaryEncoder(updateEncoder)
} }
@@ -19,10 +20,10 @@ export const PERSIST_STRUCTS_DS = 1
/** /**
* Write the current Yjs data model to an encoder. * Write the current Yjs data model to an encoder.
* *
* @param {Yjs} y A Yjs instance * @param {Y} y A Yjs instance
* @param {BinaryEncoder} encoder An encoder to write to * @param {Encoder} encoder An encoder to write to
*/ *
export function encodeStructsDS (y, encoder) { export const encodeStructsDS = (y, encoder) => {
encoder.writeVarUint(PERSIST_STRUCTS_DS) encoder.writeVarUint(PERSIST_STRUCTS_DS)
writeStructs(y, encoder, new Map()) writeStructs(y, encoder, new Map())
writeDeleteSet(y, encoder) writeDeleteSet(y, encoder)
@@ -30,10 +31,10 @@ export function encodeStructsDS (y, encoder) {
/** /**
* Feed the Yjs instance with the persisted state * Feed the Yjs instance with the persisted state
* @param {Yjs} y A Yjs instance. * @param {Y} y A Yjs instance.
* @param {BinaryDecoder} decoder A Decoder instance that holds the file content. * @param {Decoder} decoder A Decoder instance that holds the file content.
*/ *
export function decodePersisted (y, decoder) { export const decodePersisted = (y, decoder) => {
y.transact(() => { y.transact(() => {
while (decoder.hasContent()) { while (decoder.hasContent()) {
const contentType = decoder.readVarUint() const contentType = decoder.readVarUint()
@@ -49,3 +50,4 @@ export function decodePersisted (y, decoder) {
} }
}, true) }, true)
} }
*/

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

112
protocols/awareness.js Normal file
View File

@@ -0,0 +1,112 @@
/**
* @module awareness-protocol
*/
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} UserStateUpdate.state
*/
/**
* @param {encoding.Encoder} encoder
* @param {Array<UserStateUpdate>} stateUpdates
*/
export const writeUsersStateChange = (encoder, stateUpdates) => {
const len = stateUpdates.length
encoding.writeVarUint(encoder, messageUsersStateChanged)
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
const {userID, state} = stateUpdates[i]
encoding.writeVarUint(encoder, userID)
encoding.writeVarString(encoder, JSON.stringify(state))
}
}
export const readUsersStateChange = (decoder, y) => {
const added = []
const updated = []
const removed = []
const len = decoding.readVarUint(decoder)
for (let i = 0; i < len; i++) {
const userID = decoding.readVarUint(decoder)
const state = JSON.parse(decoding.readVarString(decoder))
if (userID !== y.userID) {
if (state === null) {
if (y.awareness.has(userID)) {
y.awareness.delete(userID)
removed.push(userID)
}
} else {
if (y.awareness.has(userID)) {
updated.push(userID)
} else {
added.push(userID)
}
y.awareness.set(userID, state)
}
}
}
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
y.emit('awareness', {
added, updated, removed
})
}
}
/**
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
*/
export const forwardUsersStateChange = (decoder, encoder) => {
const len = decoding.readVarUint(decoder)
const updates = []
encoding.writeVarUint(encoder, messageUsersStateChanged)
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
const userID = decoding.readVarUint(decoder)
const state = decoding.readVarString(decoder)
encoding.writeVarUint(encoder, userID)
encoding.writeVarString(encoder, state)
updates.push({userID, state: JSON.parse(state)})
}
return updates
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const readAwarenessMessage = (decoder, y) => {
switch (decoding.readVarUint(decoder)) {
case messageUsersStateChanged:
readUsersStateChange(decoder, y)
break
}
}
/**
* @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:
s = forwardUsersStateChange(decoder, encoder)
}
return s
}

View File

@@ -1,17 +1,19 @@
/**
* @module sync-protocol
*/
import * as encoding from '../lib/encoding.js' import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js' import * as decoding from '../lib/decoding.js'
import * as ID from './Util/ID.js' import * as ID from '../utils/ID.js'
import { getStruct } from './Util/structReferences.js' import { getStruct } from '../utils/structReferences.js'
import { deleteItemRange } from './Struct/Delete.js' import { deleteItemRange } from '../utils/structManipulation.js'
import { integrateRemoteStruct } from './Util/integrateRemoteStructs.js' import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.js'
import Item from './Struct/Item.js' import { Y } from '../utils/Y.js' // eslint-disable-line
import { Item } from '../structs/Item.js'
import * as stringify from '../utils/structStringify.js'
/** /**
* @typedef {import('./Store/StateStore.js').default} StateStore * @typedef {Map<number, number>} StateSet
* @typedef {import('./Y.js').default} Y
* @typedef {import('./Struct/Item.js').default} Item
* @typedef {import('./Store/StateStore.js').StateSet} StateSet
*/ */
/** /**
@@ -39,9 +41,9 @@ import Item from './Struct/Item.js'
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer) * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
*/ */
const messageYjsSyncStep1 = 0 export const messageYjsSyncStep1 = 0
const messageYjsSyncStep2 = 1 export const messageYjsSyncStep2 = 1
const messageYjsUpdate = 2 export const messageYjsUpdate = 2
/** /**
* Stringifies a message-encoded Delete Set. * Stringifies a message-encoded Delete Set.
@@ -54,7 +56,7 @@ export const stringifyDeleteSet = (decoder) => {
const dsLength = decoding.readUint32(decoder) const dsLength = decoding.readUint32(decoder)
for (let i = 0; i < dsLength; i++) { for (let i = 0; i < dsLength; i++) {
str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user
const dvLength = decoding.readVarUint(decoder) const dvLength = decoding.readUint32(decoder)
for (let j = 0; j < dvLength; j++) { for (let j = 0; j < dvLength; j++) {
str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n` str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n`
} }
@@ -82,7 +84,8 @@ export const writeDeleteSet = (encoder, y) => {
const gc = n.gc const gc = n.gc
if (currentUser !== user) { if (currentUser !== user) {
numberOfUsers++ numberOfUsers++
// a new user was found // a new user was foundimport { StateSet } from '../Store/StateStore.js' // eslint-disable-line
if (currentUser !== null) { // happens on first iteration if (currentUser !== null) { // happens on first iteration
encoding.setUint32(encoder, lastLenPos, currentLength) encoding.setUint32(encoder, lastLenPos, currentLength)
} }
@@ -192,8 +195,8 @@ export const readDeleteSet = (decoder, y) => {
*/ */
export const stringifyStateSet = decoder => { export const stringifyStateSet = decoder => {
let s = 'State Set: ' let s = 'State Set: '
readStateSet(decoder).forEach((user, userState) => { readStateSet(decoder).forEach((clock, user) => {
s += `(${user}: ${userState}), ` s += `(${user}: ${clock}), `
}) })
return s return s
} }
@@ -201,15 +204,15 @@ export const stringifyStateSet = decoder => {
/** /**
* Write StateSet to Encoder * Write StateSet to Encoder
* *
* @param {Y} y
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {Y} y
*/ */
export const writeStateSet = (encoder, y) => { export const writeStateSet = (encoder, y) => {
const state = y.ss.state const state = y.ss.state
// write as fixed-size number to stay consistent with the other encode functions. // write as fixed-size number to stay consistent with the other encode functions.
// => anytime we write the number of objects that follow, encode as fixed-size number. // => anytime we write the number of objects that follow, encode as fixed-size number.
encoding.writeUint32(encoder, state.size) encoding.writeUint32(encoder, state.size)
state.forEach((user, clock) => { state.forEach((clock, user) => {
encoding.writeVarUint(encoder, user) encoding.writeVarUint(encoder, user)
encoding.writeVarUint(encoder, clock) encoding.writeVarUint(encoder, clock)
}) })
@@ -232,50 +235,6 @@ export const readStateSet = decoder => {
return ss return ss
} }
/**
* Stringify an item id.
*
* @param {ID.ID | ID.RootID} id
* @return {string}
*/
export const stringifyID = id => id instanceof ID.ID ? `(${id.user},${id.clock})` : `(${id.name},${id.type})`
/**
* Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent).
*
* @param {Item | Y | null} item
* @return {string}
*/
export const stringifyItemID = item => {
let result
if (item === null) {
result = '()'
} else if (item instanceof Item) {
result = stringifyID(item._id)
} else {
// must be a Yjs instance
// Don't include Y in this module, so we prevent circular dependencies.
result = 'y'
}
return result
}
/**
* Helper utility to convert an item to a readable format.
*
* @param {String} name The name of the item class (YText, ItemString, ..).
* @param {Item} item The item instance.
* @param {String} [append] Additional information to append to the returned
* string.
* @return {String} A readable string that represents the item object.
*
*/
export const logItemHelper = (name, item, append) => {
const left = item._left !== null ? stringifyID(item._left._lastId) : '()'
const origin = item._origin !== null ? stringifyID(item._origin._lastId) : '()'
return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item._right)},parent:${stringifyItemID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
}
/** /**
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {Y} y * @param {Y} y
@@ -291,7 +250,7 @@ export const stringifyStructs = (decoder, y) => {
let missing = struct._fromBinary(y, decoder) let missing = struct._fromBinary(y, decoder)
let logMessage = ' ' + struct._logString() let logMessage = ' ' + struct._logString()
if (missing.length > 0) { if (missing.length > 0) {
logMessage += ' .. missing: ' + missing.map(stringifyItemID).join(', ') logMessage += ' .. missing: ' + missing.map(stringify.stringifyItemID).join(', ')
} }
str += logMessage + '\n' str += logMessage + '\n'
} }
@@ -317,7 +276,9 @@ export const writeStructs = (encoder, y, ss) => {
const overlappingLeft = y.os.findPrev(minBound) const overlappingLeft = y.os.findPrev(minBound)
const rightID = overlappingLeft === null ? null : overlappingLeft._id const rightID = overlappingLeft === null ? null : overlappingLeft._id
if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) { if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) {
const struct = overlappingLeft._clonePartial(clock - rightID.clock) // TODO: only write partial content (only missing content)
// const struct = overlappingLeft._clonePartial(clock - rightID.clock)
const struct = overlappingLeft
struct._toBinary(encoder) struct._toBinary(encoder)
len++ len++
} }
@@ -406,10 +367,9 @@ export const stringifySyncStep2 = (decoder, y) => {
* Read and apply Structs and then DeleteSet to a y instance. * Read and apply Structs and then DeleteSet to a y instance.
* *
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @param {Y} y * @param {Y} y
*/ */
export const readSyncStep2 = (decoder, encoder, y) => { export const readSyncStep2 = (decoder, y) => {
readStructs(decoder, y) readStructs(decoder, y)
readDeleteSet(decoder, y) readDeleteSet(decoder, y)
} }
@@ -424,6 +384,7 @@ export const stringifyUpdate = (decoder, y) =>
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {number} numOfStructs
* @param {encoding.Encoder} updates * @param {encoding.Encoder} updates
*/ */
export const writeUpdate = (encoder, numOfStructs, updates) => { export const writeUpdate = (encoder, numOfStructs, updates) => {
@@ -439,7 +400,7 @@ export const readUpdate = readStructs
* @param {Y} y * @param {Y} y
* @return {string} The message converted to string * @return {string} The message converted to string
*/ */
export const stringifyMessage = (decoder, y) => { export const stringifySyncMessage = (decoder, y) => {
const messageType = decoding.readVarUint(decoder) const messageType = decoding.readVarUint(decoder)
let stringifiedMessage let stringifiedMessage
let stringifiedMessageType let stringifiedMessageType
@@ -468,14 +429,14 @@ export const stringifyMessage = (decoder, y) => {
* @param {encoding.Encoder} encoder The reply message. Will not be sent if empty. * @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
* @param {Y} y * @param {Y} y
*/ */
export const readMessage = (decoder, encoder, y) => { export const readSyncMessage = (decoder, encoder, y) => {
const messageType = decoding.readVarUint(decoder) const messageType = decoding.readVarUint(decoder)
switch (messageType) { switch (messageType) {
case messageYjsSyncStep1: case messageYjsSyncStep1:
readSyncStep1(decoder, encoder, y) readSyncStep1(decoder, encoder, y)
break break
case messageYjsSyncStep2: case messageYjsSyncStep2:
y.transact(() => readSyncStep2(decoder, encoder, y), true) y.transact(() => readSyncStep2(decoder, y), true)
break break
case messageYjsUpdate: case messageYjsUpdate:
y.transact(() => readUpdate(decoder, y), true) y.transact(() => readUpdate(decoder, y), true)

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