Compare commits
42 Commits
ydb-integr
...
v13.0.0-76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4063e28b5e | ||
|
|
b6f7cd7869 | ||
|
|
1a79e429ed | ||
|
|
04066a5678 | ||
|
|
e09ef15349 | ||
|
|
3d70eee959 | ||
|
|
582095e5a3 | ||
|
|
c9ea3a412e | ||
|
|
a2c51c36e9 | ||
|
|
ab3dba5b06 | ||
|
|
3ddff186c2 | ||
|
|
9bd199a6e7 | ||
|
|
01d0825ae6 | ||
|
|
e2f98525d2 | ||
|
|
70a0a03130 | ||
|
|
656d85c62e | ||
|
|
e168dd48fb | ||
|
|
12d43199d5 | ||
|
|
539fa8b21d | ||
|
|
f572f94586 | ||
|
|
c12d00b227 | ||
|
|
e4a5f2caec | ||
|
|
9f9f465238 | ||
|
|
8450ff86d7 | ||
|
|
70139262c5 | ||
|
|
9c0da271eb | ||
|
|
ade3e1949d | ||
|
|
eec63a008f | ||
|
|
52abcdd043 | ||
|
|
f94653424a | ||
|
|
d67a794e2c | ||
|
|
60318083a6 | ||
|
|
7607070452 | ||
|
|
28fb7b6e9c | ||
|
|
aafe15757f | ||
|
|
31d6ef6296 | ||
|
|
32b8fac37f | ||
|
|
e8060de914 | ||
|
|
22b036527c | ||
|
|
feb1e030d7 | ||
|
|
bd271e3952 | ||
|
|
df80938190 |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"source": "./src",
|
||||
"source": ".",
|
||||
"destination": "./docs",
|
||||
"excludes": ["build", "node_modules", "tests-lib", "test"],
|
||||
"plugins": [{
|
||||
"name": "esdoc-standard-plugin",
|
||||
"option": {
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,7 +2,7 @@ node_modules
|
||||
bower_components
|
||||
docs
|
||||
/y.*
|
||||
/examples/yjs-dist.js*
|
||||
/examples_all/*/index.dist.*
|
||||
.vscode
|
||||
.yjsPersisted
|
||||
build
|
||||
build
|
||||
|
||||
50
.jsdoc.json
Normal file
50
.jsdoc.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
# 
|
||||
# 
|
||||
|
||||
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
|
||||
@@ -66,7 +66,7 @@ missing modules.
|
||||
|
||||
### 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-websockets-client@8/dist/y-websockets-client.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
|
||||
|
||||
21
README.v13.md
Normal file
21
README.v13.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 
|
||||
> 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
1
bindings/dom.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dom/DomBinding.js'
|
||||
@@ -1,15 +1,23 @@
|
||||
/**
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
/* global MutationObserver, getSelection */
|
||||
|
||||
import { fromRelativePosition } from '../../Util/relativePosition.js'
|
||||
import Binding from '../Binding.js'
|
||||
import { fromRelativePosition } from '../../utils/relativePosition.js'
|
||||
import { createMutex } from '../../lib/mutex.js'
|
||||
import { createAssociation, removeAssociation } from './util.js'
|
||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
|
||||
import { defaultFilter, applyFilterOnType } from './filter.js'
|
||||
import typeObserver from './typeObserver.js'
|
||||
import domObserver from './domObserver.js'
|
||||
import { typeObserver } from './typeObserver.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 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
|
||||
* truth.
|
||||
@@ -31,10 +40,26 @@ export default class DomBinding extends Binding {
|
||||
* @param {Object} [opts] Optional configurations
|
||||
|
||||
* @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 = {}) {
|
||||
// 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
|
||||
opts.document = opts.document || document
|
||||
opts.hooks = opts.hooks || {}
|
||||
@@ -81,16 +106,16 @@ export default class DomBinding extends Binding {
|
||||
this.y = y
|
||||
// Force flush dom changes before Type changes are applied (they might
|
||||
// modify the dom)
|
||||
this._beforeTransactionHandler = (y, transaction, remote) => {
|
||||
this._beforeTransactionHandler = y => {
|
||||
this._domObserver(this._mutationObserver.takeRecords())
|
||||
this._mutualExclude(() => {
|
||||
beforeTransactionSelectionFixer(this, remote)
|
||||
beforeTransactionSelectionFixer(this)
|
||||
})
|
||||
}
|
||||
y.on('beforeTransaction', this._beforeTransactionHandler)
|
||||
this._afterTransactionHandler = (y, transaction, remote) => {
|
||||
this._afterTransactionHandler = (y, transaction) => {
|
||||
this._mutualExclude(() => {
|
||||
afterTransactionSelectionFixer(this, remote)
|
||||
afterTransactionSelectionFixer(this)
|
||||
})
|
||||
// remove associations
|
||||
// TODO: this could be done more efficiently
|
||||
@@ -121,8 +146,15 @@ export default class DomBinding extends Binding {
|
||||
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.
|
||||
*/
|
||||
setFilter (filter) {
|
||||
@@ -199,13 +231,18 @@ export default class DomBinding extends Binding {
|
||||
y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
|
||||
y.off('afterTransaction', this._afterTransactionHandler)
|
||||
document.removeEventListener('selectionchange', this._selectionchange)
|
||||
super.destroy()
|
||||
this.type = null
|
||||
this.target = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter defines which elements and attributes to share.
|
||||
* Return null if the node should be filtered. Otherwise return the Map of
|
||||
* accepted attributes.
|
||||
*
|
||||
* @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
|
||||
* @callback FilterFunction
|
||||
* @param {string} nodeName
|
||||
* @param {Map} attrs
|
||||
* @return {Map|null}
|
||||
*/
|
||||
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
||||
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||
import {
|
||||
iterateUntilUndeleted,
|
||||
removeAssociation,
|
||||
insertNodeHelper } from './util.js'
|
||||
import diff from '../../../lib/simpleDiff.js'
|
||||
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
|
||||
import { simpleDiff } from '../../lib/diff.js'
|
||||
import { YXmlFragment } from '../../types/YXmlElement.js'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* You can detect that a node was moved because expectedId
|
||||
* !== actualId in the list
|
||||
*
|
||||
* @function
|
||||
* @private
|
||||
*/
|
||||
function applyChangesFromDom (binding, dom, yxml, _document) {
|
||||
const applyChangesFromDom = (binding, dom, yxml, _document) => {
|
||||
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
|
||||
return
|
||||
}
|
||||
@@ -32,7 +37,7 @@ function applyChangesFromDom (binding, dom, yxml, _document) {
|
||||
}
|
||||
}
|
||||
// 1. Check if any of the nodes was deleted
|
||||
yxml.forEach(function (childType) {
|
||||
yxml.forEach(childType => {
|
||||
if (knownChildren.has(childType) === false) {
|
||||
childType._delete(y)
|
||||
removeAssociation(binding, binding.typeToDom.get(childType), childType)
|
||||
@@ -82,8 +87,9 @@ function applyChangesFromDom (binding, dom, yxml, _document) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export default function domObserver (mutations, _document) {
|
||||
export function domObserver (mutations, _document) {
|
||||
this._mutualExclude(() => {
|
||||
this.type._y.transact(() => {
|
||||
let diffChildren = new Set()
|
||||
@@ -107,7 +113,7 @@ export default function domObserver (mutations, _document) {
|
||||
}
|
||||
switch (mutation.type) {
|
||||
case 'characterData':
|
||||
var change = diff(yxml.toString(), dom.nodeValue)
|
||||
var change = simpleDiff(yxml.toString(), dom.nodeValue)
|
||||
yxml.delete(change.pos, change.remove)
|
||||
yxml.insert(change.pos, change.insert)
|
||||
break
|
||||
@@ -1,18 +1,26 @@
|
||||
/**
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
import YXmlText from '../../Types/YXml/YXmlText.js'
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
||||
import YXmlElement from '../../Types/YXml/YXmlElement.js'
|
||||
import { YXmlText } from '../../types/YXmlText.js'
|
||||
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||
import { YXmlElement } from '../../types/YXmlElement.js'
|
||||
import { createAssociation, domsToTypes } from './util.js'
|
||||
import { filterDomAttributes, defaultFilter } from './filter.js'
|
||||
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @typedef {import('./filter.js').DomFilter} DomFilter
|
||||
* @typedef {import('./DomBinding.js').default} DomBinding
|
||||
* @callback DomFilter
|
||||
* @param {string} nodeName
|
||||
* @param {Map<string, string>} attrs
|
||||
* @return {Map | null}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
||||
*
|
||||
* @function
|
||||
* @param {Element|Text} element The DOM Element
|
||||
* @param {?Document} _document Optional. Provide the global document object
|
||||
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
|
||||
@@ -20,7 +28,7 @@ import { filterDomAttributes, defaultFilter } from './filter.js'
|
||||
* @param {?DomBinding} binding Warning: This property is for internal use only!
|
||||
* @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}
|
||||
*/
|
||||
@@ -1,29 +1,33 @@
|
||||
import isParentOf from '../../Util/isParentOf.js'
|
||||
|
||||
/**
|
||||
* @callback DomFilter
|
||||
* @param {string} nodeName
|
||||
* @param {Map<string, string>} attrs
|
||||
* @return {Map | null}
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
import { Y } from '../../utils/Y.js' // eslint-disable-line
|
||||
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
|
||||
import { isParentOf } from '../../utils/isParentOf.js'
|
||||
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Default filter method (does nothing).
|
||||
*
|
||||
* @function
|
||||
* @param {String} nodeName The nodeName of the element
|
||||
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
|
||||
* @return {Map | null} The allowed attributes or null, if the element should be
|
||||
* filtered.
|
||||
*/
|
||||
export function defaultFilter (nodeName, attrs) {
|
||||
export const defaultFilter = (nodeName, attrs) => {
|
||||
// TODO: implement basic filter that filters out dangerous properties!
|
||||
return attrs
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {Element} dom
|
||||
* @param {Function} filter
|
||||
*/
|
||||
export function filterDomAttributes (dom, filter) {
|
||||
export const filterDomAttributes = (dom, filter) => {
|
||||
const attrs = new Map()
|
||||
for (let i = dom.attributes.length - 1; i >= 0; i--) {
|
||||
const attr = dom.attributes[i]
|
||||
@@ -35,14 +39,14 @@ export function filterDomAttributes (dom, filter) {
|
||||
/**
|
||||
* Applies a filter on a type.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {Y} y The Yjs instance.
|
||||
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
|
||||
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export function applyFilterOnType (y, binding, type) {
|
||||
if (isParentOf(binding.type, type)) {
|
||||
export const applyFilterOnType = (y, binding, type) => {
|
||||
if (isParentOf(binding.type, type) && type instanceof YXmlElement) {
|
||||
const nodeName = type.nodeName
|
||||
let attributes = new Map()
|
||||
if (type.getAttributes !== undefined) {
|
||||
@@ -53,7 +57,7 @@ export function applyFilterOnType (y, binding, type) {
|
||||
}
|
||||
const filteredAttributes = binding.filter(nodeName, new Map(attributes))
|
||||
if (filteredAttributes === null) {
|
||||
type._delete(y)
|
||||
type._delete(y, true)
|
||||
} else {
|
||||
// iterate original attributes
|
||||
attributes.forEach((value, key) => {
|
||||
@@ -1,10 +1,17 @@
|
||||
/**
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
/* globals getSelection */
|
||||
|
||||
import { getRelativePosition } from '../../Util/relativePosition.js'
|
||||
import { getRelativePosition } from '../../utils/relativePosition.js'
|
||||
|
||||
let relativeSelection = null
|
||||
|
||||
function _getCurrentRelativeSelection (domBinding) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
const _getCurrentRelativeSelection = domBinding => {
|
||||
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
|
||||
const baseNodeType = domBinding.domToType.get(baseNode)
|
||||
const extentNodeType = domBinding.domToType.get(extentNode)
|
||||
@@ -17,18 +24,25 @@ function _getCurrentRelativeSelection (domBinding) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
|
||||
|
||||
export function beforeTransactionSelectionFixer (domBinding) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const beforeTransactionSelectionFixer = domBinding => {
|
||||
relativeSelection = getCurrentRelativeSelection(domBinding)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the browser range after every transaction.
|
||||
* This prevents any collapsing issues with the local selection.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export function afterTransactionSelectionFixer (domBinding) {
|
||||
export const afterTransactionSelectionFixer = domBinding => {
|
||||
if (relativeSelection !== null) {
|
||||
domBinding.restoreSelection(relativeSelection)
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
/**
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
/* global getSelection */
|
||||
|
||||
import YXmlText from '../../Types/YXml/YXmlText.js'
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
||||
import { YXmlText } from '../../types/YXmlText.js'
|
||||
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||
import { removeDomChildrenUntilElementFound } from './util.js'
|
||||
|
||||
function findScrollReference (scrollingElement) {
|
||||
const findScrollReference = scrollingElement => {
|
||||
if (scrollingElement !== null) {
|
||||
let anchor = getSelection().anchorNode
|
||||
if (anchor == null) {
|
||||
@@ -34,7 +38,7 @@ function findScrollReference (scrollingElement) {
|
||||
return null
|
||||
}
|
||||
|
||||
function fixScroll (scrollingElement, ref) {
|
||||
const fixScroll = (scrollingElement, ref) => {
|
||||
if (ref !== null) {
|
||||
const { elem, top } = ref
|
||||
const currentTop = elem.getBoundingClientRect().top
|
||||
@@ -48,7 +52,7 @@ function fixScroll (scrollingElement, ref) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export default function typeObserver (events) {
|
||||
export const typeObserver = function (events) {
|
||||
this._mutualExclude(() => {
|
||||
const scrollRef = findScrollReference(this.scrollingElement)
|
||||
events.forEach(event => {
|
||||
@@ -1,19 +1,16 @@
|
||||
|
||||
import domToType from './domToType.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Types/YXml/YXmlText.js').default} YXmlText
|
||||
* @typedef {import('../../Types/YXml/YXmlElement.js').default} YXmlElement
|
||||
* @typedef {import('../../Types/YXml/YXmlHook.js').default} YXmlHook
|
||||
* @typedef {import('./DomBinding.js').default} DomBinding
|
||||
* @module bindings/dom
|
||||
*/
|
||||
|
||||
import { domToType } from './domToType.js'
|
||||
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Iterates items until an undeleted item is found.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export function iterateUntilUndeleted (item) {
|
||||
export const iterateUntilUndeleted = item => {
|
||||
while (item !== null && item._deleted) {
|
||||
item = item._right
|
||||
}
|
||||
@@ -24,12 +21,14 @@ export function iterateUntilUndeleted (item) {
|
||||
* Removes an association (the information that a DOM element belongs to a
|
||||
* type).
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {DomBinding} domBinding The binding object
|
||||
* @param {Element} dom The dom that is to be associated with type
|
||||
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
|
||||
*
|
||||
*/
|
||||
export function removeAssociation (domBinding, dom, type) {
|
||||
export const removeAssociation = (domBinding, dom, type) => {
|
||||
domBinding.domToType.delete(dom)
|
||||
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
|
||||
* type).
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {DomBinding} domBinding The binding object
|
||||
* @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) {
|
||||
domBinding.domToType.set(dom, type)
|
||||
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
|
||||
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {DomBinding} domBinding The binding object
|
||||
* @param {Element} oldDom The existing dom
|
||||
* @param {Element} newDom The new dom object
|
||||
*/
|
||||
export function switchAssociation (domBinding, oldDom, newDom) {
|
||||
export const switchAssociation = (domBinding, oldDom, newDom) => {
|
||||
if (domBinding !== undefined) {
|
||||
const type = domBinding.domToType.get(oldDom)
|
||||
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
|
||||
* specified position.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {YXmlElement} type The type in which to insert DOM elements.
|
||||
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
||||
* inserted after this node. Set null to insert at
|
||||
@@ -81,15 +86,13 @@ export function switchAssociation (domBinding, oldDom, newDom) {
|
||||
* @param {?Document} _document Optional. Provide the global document object.
|
||||
* @param {DomBinding} binding The dom binding
|
||||
* @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)
|
||||
return type.insertAfter(prev, types)
|
||||
}
|
||||
|
||||
export function domsToTypes (doms, _document, hooks, filter, binding) {
|
||||
export const domsToTypes = (doms, _document, hooks, filter, binding) => {
|
||||
const types = []
|
||||
for (let dom of doms) {
|
||||
const t = domToType(dom, _document, hooks, filter, binding)
|
||||
@@ -102,8 +105,9 @@ export function domsToTypes (doms, _document, hooks, filter, binding) {
|
||||
|
||||
/**
|
||||
* @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)
|
||||
if (insertedNodes.length > 0) {
|
||||
return insertedNodes[0]
|
||||
@@ -115,14 +119,14 @@ export function insertNodeHelper (yxml, prevExpectedNode, child, _document, bind
|
||||
/**
|
||||
* Remove children until `elem` is found.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @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.
|
||||
* @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) {
|
||||
const del = currentChild
|
||||
currentChild = currentChild.nextSibling
|
||||
633
bindings/prosemirror.js
Normal file
633
bindings/prosemirror.js
Normal file
@@ -0,0 +1,633 @@
|
||||
/**
|
||||
* @module bindings/prosemirror
|
||||
*/
|
||||
|
||||
import { YText } from '../types/YText.js' // eslint-disable-line
|
||||
import { YXmlElement, YXmlFragment } from '../types/YXmlElement.js' // eslint-disable-line
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import * as PModel from 'prosemirror-model'
|
||||
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
|
||||
import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state' // eslint-disable-line
|
||||
import * as math from '../lib/math.js'
|
||||
import * as object from '../lib/object.js'
|
||||
import * as YPos from '../utils/relativePosition.js'
|
||||
|
||||
/**
|
||||
* @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
|
||||
*/
|
||||
|
||||
/**
|
||||
* The unique prosemirror plugin key for prosemirrorPlugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const prosemirrorPluginKey = new PluginKey('yjs')
|
||||
|
||||
/**
|
||||
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
|
||||
*
|
||||
* This plugin also keeps references to the type and the shared document so other plugins can access it.
|
||||
* @param {YXmlFragment} yXmlFragment
|
||||
* @return {Plugin} Returns a prosemirror plugin that binds to this type
|
||||
*/
|
||||
export const prosemirrorPlugin = yXmlFragment => {
|
||||
const pluginState = {
|
||||
type: yXmlFragment,
|
||||
y: yXmlFragment._y,
|
||||
binding: null
|
||||
}
|
||||
let changedInitialContent = false
|
||||
const plugin = new Plugin({
|
||||
key: prosemirrorPluginKey,
|
||||
state: {
|
||||
init: (initargs, state) => {
|
||||
return pluginState
|
||||
},
|
||||
apply: (tr, pluginState) => {
|
||||
// update Yjs state when apply is called. We need to do this here to compute the correct cursor decorations with the cursor plugin
|
||||
if (pluginState.binding !== null && (changedInitialContent || tr.doc.content.size > 4)) {
|
||||
changedInitialContent = true
|
||||
pluginState.binding._prosemirrorChanged(tr.doc)
|
||||
}
|
||||
return pluginState
|
||||
}
|
||||
},
|
||||
view: view => {
|
||||
const binding = new ProsemirrorBinding(yXmlFragment, view)
|
||||
pluginState.binding = binding
|
||||
return {
|
||||
update: () => {
|
||||
if (changedInitialContent || view.state.doc.content.size > 4) {
|
||||
changedInitialContent = true
|
||||
binding._prosemirrorChanged(view.state.doc)
|
||||
}
|
||||
},
|
||||
destroy: () => {
|
||||
binding.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique prosemirror plugin key for cursorPlugin.type
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const cursorPluginKey = new PluginKey('yjs-cursor')
|
||||
|
||||
/**
|
||||
* A prosemirror plugin that listens to awareness information on Yjs.
|
||||
* This requires that a `prosemirrorPlugin` is also bound to the prosemirror.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const cursorPlugin = new Plugin({
|
||||
key: cursorPluginKey,
|
||||
props: {
|
||||
decorations: state => {
|
||||
const ystate = prosemirrorPluginKey.getState(state)
|
||||
const y = ystate.y
|
||||
const awareness = y.getAwarenessInfo()
|
||||
const decorations = []
|
||||
awareness.forEach((aw, userID) => {
|
||||
if (aw.cursor != null) {
|
||||
let user = aw.user || {}
|
||||
if (user.color == null) {
|
||||
user.color = '#ffa50070'
|
||||
}
|
||||
if (user.name == null) {
|
||||
user.name = `User: ${userID}`
|
||||
}
|
||||
let anchor = relativePositionToAbsolutePosition(ystate.type, aw.cursor.anchor || null, ystate.binding.mapping)
|
||||
let head = relativePositionToAbsolutePosition(ystate.type, aw.cursor.head || null, ystate.binding.mapping)
|
||||
if (anchor !== null && head !== null) {
|
||||
let maxsize = math.max(state.doc.content.size - 1, 0)
|
||||
anchor = math.min(anchor, maxsize)
|
||||
head = math.min(head, maxsize)
|
||||
decorations.push(Decoration.widget(head, () => {
|
||||
const cursor = document.createElement('span')
|
||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||
cursor.setAttribute('style', `border-color: ${user.color}`)
|
||||
const userDiv = document.createElement('div')
|
||||
userDiv.setAttribute('style', `background-color: ${user.color}`)
|
||||
userDiv.insertBefore(document.createTextNode(user.name), null)
|
||||
cursor.insertBefore(userDiv, null)
|
||||
return cursor
|
||||
}, { key: userID + '' }))
|
||||
const from = math.min(anchor, head)
|
||||
const to = math.max(anchor, head)
|
||||
decorations.push(Decoration.inline(from, to, { style: `background-color: ${user.color}` }))
|
||||
}
|
||||
}
|
||||
})
|
||||
return DecorationSet.create(state.doc, decorations)
|
||||
}
|
||||
},
|
||||
view: view => {
|
||||
const ystate = prosemirrorPluginKey.getState(view.state)
|
||||
const y = ystate.y
|
||||
const awarenessListener = () => {
|
||||
view.updateState(view.state)
|
||||
}
|
||||
const updateCursorInfo = () => {
|
||||
const current = y.getLocalAwarenessInfo()
|
||||
if (view.hasFocus()) {
|
||||
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
|
||||
const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
|
||||
if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
|
||||
y.setAwarenessField('cursor', {
|
||||
anchor, head
|
||||
})
|
||||
}
|
||||
} else if (current.cursor !== null) {
|
||||
y.setAwarenessField('cursor', null)
|
||||
}
|
||||
}
|
||||
y.on('awareness', awarenessListener)
|
||||
view.dom.addEventListener('focusin', updateCursorInfo)
|
||||
view.dom.addEventListener('focusout', updateCursorInfo)
|
||||
return {
|
||||
update: updateCursorInfo,
|
||||
destroy: () => {
|
||||
const y = prosemirrorPluginKey.getState(view.state).y
|
||||
y.setAwarenessField('cursor', null)
|
||||
y.off('awareness', awarenessListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Transforms a Prosemirror based absolute position to a Yjs based relative position.
|
||||
*
|
||||
* @param {number} pos
|
||||
* @param {YXmlFragment} type
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {any} relative position
|
||||
*/
|
||||
export const absolutePositionToRelativePosition = (pos, type, mapping) => {
|
||||
if (pos === 0) {
|
||||
return YPos.getRelativePosition(type, 0)
|
||||
}
|
||||
let n = type._first
|
||||
if (n !== null) {
|
||||
while (type !== n) {
|
||||
const pNodeSize = (mapping.get(n) || { nodeSize: 0 }).nodeSize
|
||||
if (n.constructor === YText) {
|
||||
if (n.length >= pos) {
|
||||
return YPos.getRelativePosition(n, pos)
|
||||
} else {
|
||||
pos -= n.length
|
||||
}
|
||||
if (n._next !== null) {
|
||||
n = n._next
|
||||
} else {
|
||||
do {
|
||||
n = n._parent
|
||||
pos--
|
||||
} while (n._next === null && n !== type)
|
||||
if (n !== type) {
|
||||
n = n._next
|
||||
}
|
||||
}
|
||||
} else if (n._first !== null && pos < pNodeSize) {
|
||||
n = n._first
|
||||
pos--
|
||||
} else {
|
||||
if (pos === 1 && n.length === 0 && pNodeSize > 1) {
|
||||
// edge case, should end in this paragraph
|
||||
return ['endof', n._id.user, n._id.clock, null, null]
|
||||
}
|
||||
pos -= pNodeSize
|
||||
if (n._next !== null) {
|
||||
n = n._next
|
||||
} else {
|
||||
if (pos === 0) {
|
||||
n = n._parent
|
||||
return ['endof', n._id.user, n._id.clock || null, n._id.name || null, n._id.type || null]
|
||||
}
|
||||
do {
|
||||
n = n._parent
|
||||
pos--
|
||||
} while (n._next === null && n !== type)
|
||||
if (n !== type) {
|
||||
n = n._next
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos === 0 && n.constructor !== YText && n !== type) { // TODO: set to <= 0
|
||||
return [n._id.user, n._id.clock]
|
||||
}
|
||||
}
|
||||
}
|
||||
return YPos.getRelativePosition(type, type.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {YXmlFragment} yDoc Top level type that is bound to pView
|
||||
* @param {any} relPos Encoded Yjs based relative position
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
*/
|
||||
export const relativePositionToAbsolutePosition = (yDoc, relPos, mapping) => {
|
||||
const decodedPos = YPos.fromRelativePosition(yDoc._y, relPos)
|
||||
if (decodedPos === null) {
|
||||
return null
|
||||
}
|
||||
let type = decodedPos.type
|
||||
let pos = 0
|
||||
if (type.constructor === YText) {
|
||||
pos = decodedPos.offset
|
||||
} else if (!type._deleted) {
|
||||
let n = type._first
|
||||
let i = 0
|
||||
while (i < type.length && i < decodedPos.offset && n !== null) {
|
||||
i++
|
||||
pos += mapping.get(n).nodeSize
|
||||
n = n._next
|
||||
}
|
||||
pos += 1 // increase because we go out of n
|
||||
}
|
||||
while (type !== yDoc) {
|
||||
const parent = type._parent
|
||||
if (!parent._deleted) {
|
||||
pos += 1 // the start tag
|
||||
let n = parent._first
|
||||
// now iterate until we found type
|
||||
while (n !== null) {
|
||||
if (n === type) {
|
||||
break
|
||||
}
|
||||
pos += mapping.get(n).nodeSize
|
||||
n = n._next
|
||||
}
|
||||
}
|
||||
type = parent
|
||||
}
|
||||
return pos - 1 // we don't count the most outer tag, because it is a fragment
|
||||
}
|
||||
|
||||
/**
|
||||
* Binding for prosemirror.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export class ProsemirrorBinding {
|
||||
/**
|
||||
* @param {YXmlFragment} yXmlFragment The bind source
|
||||
* @param {EditorView} prosemirrorView The target binding
|
||||
*/
|
||||
constructor (yXmlFragment, prosemirrorView) {
|
||||
this.type = yXmlFragment
|
||||
this.prosemirrorView = prosemirrorView
|
||||
this.mux = createMutex()
|
||||
/**
|
||||
* @type {ProsemirrorMapping}
|
||||
*/
|
||||
this.mapping = new Map()
|
||||
this._observeFunction = this._typeChanged.bind(this)
|
||||
this.y = yXmlFragment._y
|
||||
/**
|
||||
* current selection as relative positions in the Yjs model
|
||||
*/
|
||||
this._relSelection = null
|
||||
this.y.on('beforeTransaction', e => {
|
||||
this._relSelection = {
|
||||
anchor: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.anchor, yXmlFragment, this.mapping),
|
||||
head: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.head, yXmlFragment, this.mapping)
|
||||
}
|
||||
})
|
||||
yXmlFragment.observeDeep(this._observeFunction)
|
||||
}
|
||||
_typeChanged (events, transaction) {
|
||||
if (events.length === 0) {
|
||||
return
|
||||
}
|
||||
console.info('new types:', transaction.newTypes.size, 'deleted types:', transaction.deletedStructs.size, transaction.newTypes, transaction.deletedStructs)
|
||||
this.mux(() => {
|
||||
const delStruct = (_, struct) => this.mapping.delete(struct)
|
||||
transaction.deletedStructs.forEach(struct => this.mapping.delete(struct))
|
||||
transaction.changedTypes.forEach(delStruct)
|
||||
transaction.changedParentTypes.forEach(delStruct)
|
||||
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null)
|
||||
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
|
||||
const relSel = this._relSelection
|
||||
if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
|
||||
const anchor = relativePositionToAbsolutePosition(this.type, relSel.anchor, this.mapping)
|
||||
const head = relativePositionToAbsolutePosition(this.type, relSel.head, this.mapping)
|
||||
if (anchor !== null && head !== null) {
|
||||
tr.setSelection(TextSelection.create(tr.doc, anchor, head))
|
||||
}
|
||||
}
|
||||
this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr))
|
||||
})
|
||||
}
|
||||
_prosemirrorChanged (doc) {
|
||||
this.mux(() => {
|
||||
updateYFragment(this.type, doc.content, this.mapping)
|
||||
})
|
||||
}
|
||||
destroy () {
|
||||
this.type.unobserveDeep(this._observeFunction)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @privateMapping
|
||||
* @param {YXmlElement} el
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {PModel.Node}
|
||||
*/
|
||||
export const createNodeIfNotExists = (el, schema, mapping) => {
|
||||
const node = mapping.get(el)
|
||||
if (node === undefined) {
|
||||
return createNodeFromYElement(el, schema, mapping)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {YXmlElement} el
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
|
||||
*/
|
||||
export const createNodeFromYElement = (el, schema, mapping) => {
|
||||
const children = []
|
||||
el.toArray().forEach(type => {
|
||||
if (type.constructor === YXmlElement) {
|
||||
const n = createNodeIfNotExists(type, schema, mapping)
|
||||
if (n !== null) {
|
||||
children.push(n)
|
||||
}
|
||||
} else {
|
||||
const ns = createTextNodesFromYText(type, schema, mapping)
|
||||
if (ns !== null) {
|
||||
ns.forEach(textchild => {
|
||||
if (textchild !== null) {
|
||||
children.push(textchild)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
let node
|
||||
try {
|
||||
node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), children)
|
||||
} catch (e) {
|
||||
// an error occured while creating the node. This is probably a result because of a concurrent action.
|
||||
// delete the node and do not push to children
|
||||
el._y.transact(() => {
|
||||
el._delete(el._y, true)
|
||||
})
|
||||
return null
|
||||
}
|
||||
mapping.set(el, node)
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {YText} text
|
||||
* @param {PModel.Schema} schema
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {Array<PModel.Node>}
|
||||
*/
|
||||
export const createTextNodesFromYText = (text, schema, mapping) => {
|
||||
const nodes = []
|
||||
const deltas = text.toDelta()
|
||||
try {
|
||||
for (let i = 0; i < deltas.length; i++) {
|
||||
const delta = deltas[i]
|
||||
const marks = []
|
||||
for (let markName in delta.attributes) {
|
||||
marks.push(schema.mark(markName, delta.attributes[markName]))
|
||||
}
|
||||
nodes.push(schema.text(delta.insert, marks))
|
||||
}
|
||||
if (nodes.length > 0) {
|
||||
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
||||
}
|
||||
} catch (e) {
|
||||
text._y.transact(() => {
|
||||
text._delete(text._y, true)
|
||||
})
|
||||
return null
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {PModel.Node} node
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
* @return {YXmlElement | YText}
|
||||
*/
|
||||
export const createTypeFromNode = (node, mapping) => {
|
||||
let type
|
||||
if (node.isText) {
|
||||
type = new YText()
|
||||
const attrs = {}
|
||||
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
|
||||
type.insert(0, node.text, attrs)
|
||||
} else {
|
||||
type = new YXmlElement(node.type.name)
|
||||
for (let key in node.attrs) {
|
||||
const val = node.attrs[key]
|
||||
if (val !== null) {
|
||||
type.setAttribute(key, val)
|
||||
}
|
||||
}
|
||||
const ins = []
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
ins.push(createTypeFromNode(node.child(i), mapping))
|
||||
}
|
||||
type.insert(0, ins)
|
||||
}
|
||||
mapping.set(type, node)
|
||||
return type
|
||||
}
|
||||
|
||||
const equalAttrs = (pattrs, yattrs) => {
|
||||
const keys = Object.keys(pattrs).filter(key => pattrs[key] === null)
|
||||
let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
|
||||
for (let i = 0; i < keys.length && eq; i++) {
|
||||
const key = keys[i]
|
||||
eq = pattrs[key] === yattrs[key]
|
||||
}
|
||||
return eq
|
||||
}
|
||||
|
||||
const equalYTextPText = (ytext, ptext) => {
|
||||
const d = ytext.toDelta()[0]
|
||||
return d.insert === ptext.text && object.keys(d.attributes || {}).length === ptext.marks.length && ptext.marks.every(mark => equalAttrs(d.attributes[mark.type.name], mark.attrs))
|
||||
}
|
||||
|
||||
const equalYTypePNode = (ytype, pnode) =>
|
||||
ytype.constructor === YText
|
||||
? equalYTextPText(ytype, pnode)
|
||||
: (matchNodeName(ytype, pnode) && ytype.length === pnode.childCount && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, pnode.child(i))))
|
||||
|
||||
const computeChildEqualityFactor = (ytype, pnode, mapping) => {
|
||||
const yChildren = ytype.toArray()
|
||||
const pChildCnt = pnode.childCount
|
||||
const yChildCnt = yChildren.length
|
||||
const minCnt = math.min(yChildCnt, pChildCnt)
|
||||
let left = 0
|
||||
let right = 0
|
||||
let foundMappedChild = false
|
||||
for (; left < minCnt; left++) {
|
||||
const leftY = yChildren[left]
|
||||
const leftP = pnode.child(left)
|
||||
if (mapping.get(leftY) === leftP) {
|
||||
foundMappedChild = true// definite (good) match!
|
||||
} else if (!equalYTypePNode(leftY, leftP)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for (; left + right < minCnt; right++) {
|
||||
const rightY = yChildren[yChildCnt - right - 1]
|
||||
const rightP = pnode.child(pChildCnt - right - 1)
|
||||
if (mapping.get(rightY) !== rightP) {
|
||||
foundMappedChild = true
|
||||
} else if (!equalYTypePNode(rightP, rightP)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
equalityFactor: left + right,
|
||||
foundMappedChild
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {YXmlFragment} yDomFragment
|
||||
* @param {PModel.Node} pContent
|
||||
* @param {ProsemirrorMapping} mapping
|
||||
*/
|
||||
const updateYFragment = (yDomFragment, pContent, mapping) => {
|
||||
if (yDomFragment instanceof YXmlElement && yDomFragment.nodeName.toLowerCase() !== pContent.type.name) {
|
||||
throw new Error('node name mismatch!')
|
||||
}
|
||||
mapping.set(yDomFragment, pContent)
|
||||
// update attributes
|
||||
if (yDomFragment instanceof YXmlElement) {
|
||||
const yDomAttrs = yDomFragment.getAttributes()
|
||||
const pAttrs = pContent.attrs
|
||||
for (let key in pAttrs) {
|
||||
if (pAttrs[key] !== null) {
|
||||
if (yDomAttrs[key] !== pAttrs[key]) {
|
||||
yDomFragment.setAttribute(key, pAttrs[key])
|
||||
}
|
||||
} else {
|
||||
yDomFragment.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
// remove all keys that are no longer in pAttrs
|
||||
for (let key in yDomAttrs) {
|
||||
if (pAttrs[key] === undefined) {
|
||||
yDomFragment.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
// update children
|
||||
const pChildCnt = pContent.childCount
|
||||
const yChildren = yDomFragment.toArray()
|
||||
const yChildCnt = yChildren.length
|
||||
const minCnt = math.min(pChildCnt, yChildCnt)
|
||||
let left = 0
|
||||
let right = 0
|
||||
// find number of matching elements from left
|
||||
for (;left < minCnt; left++) {
|
||||
const leftY = yChildren[left]
|
||||
const leftP = pContent.child(left)
|
||||
if (mapping.get(leftY) !== leftP) {
|
||||
if (equalYTypePNode(leftY, leftP)) {
|
||||
// update mapping
|
||||
mapping.set(leftY, leftP)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// find number of matching elements from right
|
||||
for (;right + left < minCnt; right++) {
|
||||
const rightY = yChildren[yChildCnt - right - 1]
|
||||
const rightP = pContent.child(pChildCnt - right - 1)
|
||||
if (mapping.get(rightY) !== rightP) {
|
||||
if (equalYTypePNode(rightY, rightP)) {
|
||||
// update mapping
|
||||
mapping.set(rightY, rightP)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
yDomFragment._y.transact(() => {
|
||||
// try to compare and update
|
||||
while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
|
||||
const leftY = yChildren[left]
|
||||
const leftP = pContent.child(left)
|
||||
const rightY = yChildren[yChildCnt - right - 1]
|
||||
const rightP = pContent.child(pChildCnt - right - 1)
|
||||
if (leftY.constructor === YText && leftP.isText) {
|
||||
if (!equalYTextPText(leftY, leftP)) {
|
||||
yDomFragment.delete(left, 1)
|
||||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||
}
|
||||
left += 1
|
||||
} else {
|
||||
let updateLeft = matchNodeName(leftY, leftP)
|
||||
let updateRight = matchNodeName(rightY, rightP)
|
||||
if (updateLeft && updateRight) {
|
||||
// decide which which element to update
|
||||
const equalityLeft = computeChildEqualityFactor(leftY, leftP, mapping)
|
||||
const equalityRight = computeChildEqualityFactor(rightY, rightP, mapping)
|
||||
if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
|
||||
updateRight = false
|
||||
} else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
|
||||
updateLeft = false
|
||||
} else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
|
||||
updateLeft = false
|
||||
} else {
|
||||
updateRight = false
|
||||
}
|
||||
}
|
||||
if (updateLeft) {
|
||||
updateYFragment(leftY, leftP, mapping)
|
||||
left += 1
|
||||
} else if (updateRight) {
|
||||
updateYFragment(rightY, rightP, mapping)
|
||||
right += 1
|
||||
} else {
|
||||
yDomFragment.delete(left, 1)
|
||||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||
left += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
const yDelLen = yChildCnt - left - right
|
||||
if (yDelLen > 0) {
|
||||
yDomFragment.delete(left, yDelLen)
|
||||
}
|
||||
if (left + right < pChildCnt) {
|
||||
const ins = []
|
||||
for (let i = left; i < pChildCnt - right; i++) {
|
||||
ins.push(createTypeFromNode(pContent.child(i), mapping))
|
||||
}
|
||||
yDomFragment.insert(left, ins)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {YXmlElement} yElement
|
||||
* @param {any} pNode Prosemirror Node
|
||||
*/
|
||||
const matchNodeName = (yElement, pNode) => yElement.nodeName === pNode.type.name.toUpperCase()
|
||||
@@ -1,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
|
||||
// Force flush Quill changes.
|
||||
quill.update('yjs')
|
||||
this._mutualExclude(function () {
|
||||
this._mutualExclude(() => {
|
||||
// Apply computed delta.
|
||||
quill.updateContents(event.delta, 'yjs')
|
||||
// Force flush Quill changes. Ignore applied changes.
|
||||
@@ -12,7 +16,7 @@ function typeObserver (event) {
|
||||
})
|
||||
}
|
||||
|
||||
function quillObserver (delta) {
|
||||
const quillObserver = function (delta) {
|
||||
this._mutualExclude(() => {
|
||||
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
|
||||
* // way around!
|
||||
*/
|
||||
export default class QuillBinding extends Binding {
|
||||
export class QuillBinding {
|
||||
/**
|
||||
* @param {YText} textType
|
||||
* @param {Quill} quill
|
||||
*/
|
||||
constructor (textType, quill) {
|
||||
// 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.
|
||||
quill.setContents(textType.toDelta(), 'yjs')
|
||||
// Observers are handled by this class.
|
||||
@@ -48,6 +65,7 @@ export default class QuillBinding extends Binding {
|
||||
// Remove everything that is handled by this class.
|
||||
this.type.unobserve(this._typeObserver)
|
||||
this.target.off('text-change', this._quillObserver)
|
||||
super.destroy()
|
||||
this.type = null
|
||||
this.target = null
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* @module bindings/textarea
|
||||
*/
|
||||
|
||||
import Binding from '../Binding.js'
|
||||
import simpleDiff from '../../Util/simpleDiff.js'
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
||||
import { simpleDiff } from '../lib/diff.js'
|
||||
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
|
||||
function typeObserver () {
|
||||
this._mutualExclude(() => {
|
||||
@@ -35,10 +38,22 @@ function domObserver () {
|
||||
* const binding = new Y.QuillBinding(type, textarea)
|
||||
*
|
||||
*/
|
||||
export default class TextareaBinding extends Binding {
|
||||
export class TextareaBinding {
|
||||
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
|
||||
domTextarea.value = textType.toString()
|
||||
// Observers are handled by this class
|
||||
@@ -51,6 +66,7 @@ export default class TextareaBinding extends Binding {
|
||||
// Remove everything that is handled by this class
|
||||
this.type.unobserve(this._typeObserver)
|
||||
this.target.unobserve(this._domObserver)
|
||||
super.destroy()
|
||||
this.type = null
|
||||
this.target = null
|
||||
}
|
||||
}
|
||||
1
examples/.gitignore
vendored
Normal file
1
examples/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
@@ -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
36
examples/dom.html
Normal 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
14
examples/dom.js
Normal 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
|
||||
}
|
||||
9
examples/exampleConfig.js
Normal file
9
examples/exampleConfig.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
const isDeployed = location.hostname === 'yjs.website'
|
||||
|
||||
if (!isDeployed) {
|
||||
console.log('%cYjs: Start your local websocket server by running %c`npm run websocket-server`', 'color:blue', 'color: grey; font-weight: bold')
|
||||
}
|
||||
|
||||
export const serverAddress = isDeployed ? 'wss://api.yjs.website' : 'ws://localhost:1234'
|
||||
14
examples/examples.json
Normal file
14
examples/examples.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"prosemirror": {
|
||||
"title": "Prosemirror Binding"
|
||||
},
|
||||
"textarea": {
|
||||
"title": "Textarea Binding"
|
||||
},
|
||||
"quill": {
|
||||
"title": "Quill Binding"
|
||||
},
|
||||
"dom": {
|
||||
"title": "Dom Binding"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
@@ -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
84
examples/prosemirror.html
Normal 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
24
examples/prosemirror.js
Normal 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
49
examples/quill.html
Normal 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
30
examples/quill.js
Normal 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)
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
29
examples/style.css
Normal 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
30
examples/textarea.html
Normal 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
15
examples/textarea.js
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -14,7 +14,7 @@ let chatprotocol = y.define('chatprotocol', Y.Array)
|
||||
let chatcontainer = document.querySelector('#chat')
|
||||
|
||||
// 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 uname = document.createElement('span')
|
||||
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.
|
||||
// The rest is deleted
|
||||
function cleanupChat () {
|
||||
const cleanupChat = () => {
|
||||
if (chatprotocol.length > 7) {
|
||||
chatprotocol.delete(0, chatprotocol.length - 7)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ cleanupChat()
|
||||
chatprotocol.toArray().forEach(appendMessage)
|
||||
|
||||
// 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
|
||||
cleanupChat()
|
||||
chatcontainer.innerHTML = ''
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.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 YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
|
||||
import YXmlText from '../../src/Types/YXml/YXmlText.js'
|
||||
@@ -18,7 +18,6 @@
|
||||
</style>
|
||||
<script src="../../y.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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
import { createYdbClient } from '../../YdbClient/index.js'
|
||||
import Y from '../../src/Y.dist.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 r = Math.random() * 16 | 0
|
||||
@@ -36,13 +36,13 @@ let quill = new Quill('#quill-container', {
|
||||
|
||||
let cursors = quill.getModule('cursors')
|
||||
|
||||
function drawCursors () {
|
||||
const drawCursors = () => {
|
||||
cursors.clearCursors()
|
||||
users.map((user, userId) => {
|
||||
if (user !== myUserInfo) {
|
||||
let relativeRange = user.get('range')
|
||||
let lastUpdated = new Date(user.get('last updated'))
|
||||
if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) {
|
||||
let lastUpdated = new Date(user.get('last updated')).getTime()
|
||||
if (lastUpdated != null && new Date().getTime() - lastUpdated < 20000 && relativeRange != null) {
|
||||
let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
|
||||
let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
|
||||
let range = { index: start, length: end - start }
|
||||
55
index.js
Normal file
55
index.js
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
import './structs/Item.js'
|
||||
import { Delete } from './structs/Delete.js'
|
||||
import { ItemJSON } from './structs/ItemJSON.js'
|
||||
import { ItemString } from './structs/ItemString.js'
|
||||
import { ItemFormat } from './structs/ItemFormat.js'
|
||||
import { ItemEmbed } from './structs/ItemEmbed.js'
|
||||
import { GC } from './structs/GC.js'
|
||||
|
||||
import { YArray } from './types/YArray.js'
|
||||
import { YMap } from './types/YMap.js'
|
||||
import { YText } from './types/YText.js'
|
||||
import { YXmlText } from './types/YXmlText.js'
|
||||
import { YXmlHook } from './types/YXmlHook.js'
|
||||
import { YXmlElement, YXmlFragment } from './types/YXmlElement.js'
|
||||
|
||||
import { registerStruct } from './utils/structReferences.js'
|
||||
|
||||
import * as decoding from './lib/decoding.js'
|
||||
import * as encoding from './lib/encoding.js'
|
||||
import * as awarenessProtocol from './protocols/awareness.js'
|
||||
import * as syncProtocol from './protocols/sync.js'
|
||||
import * as authProtocol from './protocols/auth.js'
|
||||
|
||||
export { decoding, encoding, awarenessProtocol, syncProtocol, authProtocol }
|
||||
|
||||
export { Y } from './utils/Y.js'
|
||||
export { UndoManager } from './utils/UndoManager.js'
|
||||
export { Transaction } from './utils/Transaction.js'
|
||||
|
||||
export { YArray as Array } from './types/YArray.js'
|
||||
export { YMap as Map } from './types/YMap.js'
|
||||
export { YText as Text } from './types/YText.js'
|
||||
export { YXmlText as XmlText } from './types/YXmlText.js'
|
||||
export { YXmlHook as XmlHook } from './types/YXmlHook.js'
|
||||
export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.js'
|
||||
|
||||
export { getRelativePosition, fromRelativePosition } from './utils/relativePosition.js'
|
||||
export { registerStruct } from './utils/structReferences.js'
|
||||
export * from './lib/mutex.js'
|
||||
|
||||
registerStruct(0, GC)
|
||||
registerStruct(1, ItemJSON)
|
||||
registerStruct(2, ItemString)
|
||||
registerStruct(3, ItemFormat)
|
||||
registerStruct(4, Delete)
|
||||
|
||||
registerStruct(5, YArray)
|
||||
registerStruct(6, YMap)
|
||||
registerStruct(7, YText)
|
||||
registerStruct(8, YXmlFragment)
|
||||
registerStruct(9, YXmlElement)
|
||||
registerStruct(10, YXmlText)
|
||||
registerStruct(11, YXmlHook)
|
||||
registerStruct(12, ItemEmbed)
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Handles named events.
|
||||
*/
|
||||
export default class NamedEventHandler {
|
||||
export class NamedEventHandler {
|
||||
constructor () {
|
||||
this._eventListener = new Map()
|
||||
this._stateListener = new Map()
|
||||
@@ -57,7 +57,7 @@ export default class NamedEventHandler {
|
||||
let state = this._stateListener.get(name)
|
||||
if (state === undefined) {
|
||||
state = {}
|
||||
state.promise = new Promise(function (resolve) {
|
||||
state.promise = new Promise(resolve => {
|
||||
state.resolve = resolve
|
||||
})
|
||||
this._stateListener.set(name, state)
|
||||
|
||||
19
lib/Tree.js
19
lib/Tree.js
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* @module tree
|
||||
*/
|
||||
|
||||
function rotate (tree, parent, newParent, n) {
|
||||
const rotate = (tree, parent, newParent, n) => {
|
||||
if (parent === null) {
|
||||
tree.root = newParent
|
||||
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
|
||||
*/
|
||||
export default class Tree {
|
||||
export class Tree {
|
||||
constructor () {
|
||||
this.root = null
|
||||
this.length = 0
|
||||
@@ -310,12 +319,6 @@ export default class Tree {
|
||||
}
|
||||
}
|
||||
_fixDelete (n) {
|
||||
function isBlack (node) {
|
||||
return node !== null ? node.isBlack() : true
|
||||
}
|
||||
function isRed (node) {
|
||||
return node !== null ? node.isRed() : false
|
||||
}
|
||||
if (n.parent === null) {
|
||||
// this can only be called after the first iteration of fixDelete.
|
||||
return
|
||||
|
||||
@@ -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 BITS21 = (1 << 21) - 1
|
||||
@@ -5,3 +13,28 @@ export const BITS16 = (1 << 16) - 1
|
||||
|
||||
export const BIT26 = 1 << 26
|
||||
export const BIT32 = 1 << 32
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
* @return {string}
|
||||
*/
|
||||
export const toBase64 = bytes => {
|
||||
let s = ''
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
s += string.fromCharCode(bytes[i])
|
||||
}
|
||||
return btoa(s)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export const fromBase64 = s => {
|
||||
const a = atob(s)
|
||||
const bytes = globals.createUint8ArrayFromLen(a.length)
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
bytes[i] = a.charCodeAt(i)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
72
lib/broadcastchannel.js
Normal file
72
lib/broadcastchannel.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
import * as binary from './binary.js'
|
||||
import * as globals from './globals.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object} Channel
|
||||
* @property {Set<Function>} Channel.subs
|
||||
* @property {BC} Channel.bc
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Map<string, Channel>}
|
||||
*/
|
||||
const channels = new Map()
|
||||
|
||||
class LocalStoragePolyfill {
|
||||
constructor (room) {
|
||||
this.room = room
|
||||
this.onmessage = null
|
||||
addEventListener('storage', e => e.key === room && this.onmessage !== null && this.onmessage({ data: binary.fromBase64(e.newValue) }))
|
||||
}
|
||||
/**
|
||||
* @param {ArrayBuffer} data
|
||||
*/
|
||||
postMessage (buf) {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(this.room, binary.toBase64(globals.createUint8ArrayFromArrayBuffer(buf)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use BroadcastChannel or Polyfill
|
||||
const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel
|
||||
|
||||
/**
|
||||
* @param {string} room
|
||||
* @return {Channel}
|
||||
*/
|
||||
const getChannel = room => {
|
||||
let c = channels.get(room)
|
||||
if (c === undefined) {
|
||||
const subs = new Set()
|
||||
const bc = new BC(room)
|
||||
bc.onmessage = e => subs.forEach(sub => sub(e.data))
|
||||
c = {
|
||||
bc, subs
|
||||
}
|
||||
channels.set(room, c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {string} room
|
||||
* @param {Function} f
|
||||
*/
|
||||
export const subscribe = (room, f) => getChannel(room).subs.add(f)
|
||||
|
||||
/**
|
||||
* Publish data to all subscribers (including subscribers on this tab)
|
||||
*
|
||||
* @function
|
||||
* @param {string} room
|
||||
* @param {ArrayBuffer} data
|
||||
*/
|
||||
export const publish = (room, data) => {
|
||||
const c = getChannel(room)
|
||||
c.bc.postMessage(data)
|
||||
c.subs.forEach(sub => sub(data))
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @module decoding
|
||||
*/
|
||||
|
||||
/* global Buffer */
|
||||
|
||||
@@ -17,17 +20,26 @@ export class Decoder {
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @return {Decoder}
|
||||
*/
|
||||
export const createDecoder = buffer => new Decoder(buffer)
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const hasContent = decoder => decoder.pos !== decoder.arr.length
|
||||
|
||||
/**
|
||||
* Clone a decoder instance.
|
||||
* Optionally set a new position parameter.
|
||||
*
|
||||
* @function
|
||||
* @param {Decoder} decoder The decoder instance
|
||||
* @param {number} [newPos] Defaults to current position
|
||||
* @return {Decoder} A clone of `decoder`
|
||||
*/
|
||||
export const clone = (decoder, newPos = decoder.pos) => {
|
||||
@@ -38,6 +50,7 @@ export const clone = (decoder, newPos = decoder.pos) => {
|
||||
|
||||
/**
|
||||
* Read `len` bytes as an ArrayBuffer.
|
||||
* @function
|
||||
* @param {Decoder} decoder The decoder instance
|
||||
* @param {number} len The length of bytes to read
|
||||
* @return {ArrayBuffer}
|
||||
@@ -52,6 +65,7 @@ export const readArrayBuffer = (decoder, len) => {
|
||||
|
||||
/**
|
||||
* Read variable length payload as ArrayBuffer
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @return {ArrayBuffer}
|
||||
*/
|
||||
@@ -59,6 +73,7 @@ export const readPayload = decoder => readArrayBuffer(decoder, readVarUint(decod
|
||||
|
||||
/**
|
||||
* Read the rest of the content as an ArrayBuffer
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @return {ArrayBuffer}
|
||||
*/
|
||||
@@ -66,6 +81,7 @@ export const readTail = decoder => readArrayBuffer(decoder, decoder.arr.length -
|
||||
|
||||
/**
|
||||
* Skip one byte, jump to the next position.
|
||||
* @function
|
||||
* @param {Decoder} decoder The decoder instance
|
||||
* @return {number} The next position
|
||||
*/
|
||||
@@ -73,6 +89,7 @@ export const skip8 = decoder => decoder.pos++
|
||||
|
||||
/**
|
||||
* Read one byte as unsigned integer.
|
||||
* @function
|
||||
* @param {Decoder} decoder The decoder instance
|
||||
* @return {number} Unsigned 8-bit integer
|
||||
*/
|
||||
@@ -81,6 +98,7 @@ export const readUint8 = decoder => decoder.arr[decoder.pos++]
|
||||
/**
|
||||
* Read 4 bytes as unsigned integer.
|
||||
*
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @return {number} An unsigned integer.
|
||||
*/
|
||||
@@ -98,6 +116,7 @@ export const readUint32 = decoder => {
|
||||
* Look ahead without incrementing position.
|
||||
* to the next byte and read it as unsigned integer.
|
||||
*
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @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^14 is stored in two bylength
|
||||
*
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @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
|
||||
* * 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.
|
||||
* For effiency reasons we apply a maximum of 10000 characters at once.
|
||||
*
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @return {String} The read String.
|
||||
*/
|
||||
@@ -157,6 +193,8 @@ export const readVarString = decoder => {
|
||||
|
||||
/**
|
||||
* Look ahead and read varString without incrementing position
|
||||
*
|
||||
* @function
|
||||
* @param {Decoder} decoder
|
||||
* @return {string}
|
||||
*/
|
||||
@@ -166,3 +204,4 @@ export const peekVarString = decoder => {
|
||||
decoder.pos = pos
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @module diff
|
||||
*/
|
||||
|
||||
/**
|
||||
* A SimpleDiff describes a change on a String.
|
||||
@@ -27,7 +30,7 @@
|
||||
* @param {String} b The updated version of the string
|
||||
* @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 right = 0 // number of same characters counting from right
|
||||
while (left < a.length && left < b.length && a[left] === b[left]) {
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
/**
|
||||
* @module encoding
|
||||
*/
|
||||
import * as globals from './globals.js'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
@@ -15,10 +17,18 @@ export class Encoder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @return {Encoder}
|
||||
*/
|
||||
export const createEncoder = () => new Encoder()
|
||||
|
||||
/**
|
||||
* The current length of the encoded data.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @return {number}
|
||||
*/
|
||||
export const length = encoder => {
|
||||
let len = encoder.cpos
|
||||
@@ -30,6 +40,8 @@ export const length = encoder => {
|
||||
|
||||
/**
|
||||
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @return {ArrayBuffer} The created ArrayBuffer.
|
||||
*/
|
||||
@@ -48,6 +60,7 @@ export const toBuffer = encoder => {
|
||||
/**
|
||||
* Write one byte to the encoder.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @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.
|
||||
* Position must already be written (i.e. encoder.length > pos)
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} pos Position to which to write data
|
||||
* @param {number} num Unsigned 8-bit integer
|
||||
@@ -89,6 +103,7 @@ export const set = (encoder, pos, num) => {
|
||||
/**
|
||||
* Write one byte as an unsigned integer.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @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.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @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.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @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.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @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
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @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.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @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)
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
@@ -171,6 +192,7 @@ export const writeVarUint = (encoder, num) => {
|
||||
/**
|
||||
* Write a variable length string.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @param {String} str The string that is to be encoded.
|
||||
*/
|
||||
@@ -188,6 +210,7 @@ export const writeVarString = (encoder, str) => {
|
||||
*
|
||||
* TODO: can be improved!
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder The enUint8Arr
|
||||
* @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.
|
||||
*
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @param {ArrayBuffer} arrayBuffer
|
||||
*/
|
||||
@@ -209,6 +233,7 @@ export const writeArrayBuffer = (encoder, arrayBuffer) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {Encoder} encoder
|
||||
* @param {ArrayBuffer} arrayBuffer
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @module globals
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
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 max = (a, b) => a > b ? a : b
|
||||
|
||||
/**
|
||||
* @param {number} t Time to wait
|
||||
* @return {Promise} Promise that is resolved after t ms
|
||||
|
||||
11
lib/idb.js
11
lib/idb.js
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @module lib/idb
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
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>}
|
||||
*/
|
||||
export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => {
|
||||
@@ -115,7 +121,7 @@ export const getAllKeysValues = (store, range) =>
|
||||
* Iterate on keys and values
|
||||
* @param {IDBObjectStore} store
|
||||
* @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) => {
|
||||
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)
|
||||
*
|
||||
* @param {IDBObjectStore} store
|
||||
* @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) => {
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as test from './test.js'
|
||||
import * as test from './testing.js'
|
||||
import * as idb from './idb.js'
|
||||
import * as logging from './logging.js'
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @module logging
|
||||
*/
|
||||
|
||||
import * as globals from './globals.js'
|
||||
|
||||
|
||||
28
lib/math.js
28
lib/math.js
@@ -1,2 +1,28 @@
|
||||
|
||||
/**
|
||||
* @module math
|
||||
*/
|
||||
export const floor = Math.floor
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @return {number} The sum of a and b
|
||||
*/
|
||||
export const add = (a, b) => a + b
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @return {number} The smaller element of a and b
|
||||
*/
|
||||
export const min = (a, b) => a < b ? a : b
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
* @return {number} The bigger element of a and b
|
||||
*/
|
||||
export const max = (a, b) => a > b ? a : b
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
*
|
||||
* @example
|
||||
* const mutex = createMutex()
|
||||
* mutex(function () {
|
||||
* mutex(() => {
|
||||
* // This function is immediately executed
|
||||
* mutex(function () {
|
||||
* // This function is never executed, as it is called with the same
|
||||
* // mutex function
|
||||
* mutex(() => {
|
||||
* // This function is not executed, as the mutex is already active.
|
||||
* })
|
||||
* })
|
||||
*
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
/**
|
||||
* @module number
|
||||
*/
|
||||
|
||||
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
|
||||
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
|
||||
|
||||
14
lib/object.js
Normal file
14
lib/object.js
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
export const create = Object.create(null)
|
||||
|
||||
export const keys = Object.keys
|
||||
|
||||
export const equalFlat = (a, b) => {
|
||||
const keys = Object.keys(a)
|
||||
let eq = keys.length === Object.keys(b).length
|
||||
for (let i = 0; i < keys.length && eq; i++) {
|
||||
const key = keys[i]
|
||||
eq = a[key] === b[key]
|
||||
}
|
||||
return eq
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* @module prng
|
||||
*/
|
||||
const N = 624
|
||||
const M = 397
|
||||
|
||||
function twist (u, v) {
|
||||
return ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
|
||||
}
|
||||
const twist = (u, v) => ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
|
||||
|
||||
function nextState (state) {
|
||||
const nextState = (state) => {
|
||||
let p = 0
|
||||
let j
|
||||
for (j = N - M + 1; --j; p++) {
|
||||
@@ -29,7 +30,7 @@ function nextState (state) {
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
@@ -1,13 +1,16 @@
|
||||
/**
|
||||
* @module prng
|
||||
*/
|
||||
|
||||
import Mt19937 from './Mt19937.js'
|
||||
import Xoroshiro128plus from './Xoroshiro128plus.js'
|
||||
import Xorshift32 from './Xorshift32.js'
|
||||
import { Mt19937 } from './Mt19937.js'
|
||||
import { Xoroshiro128plus } from './Xoroshiro128plus.js'
|
||||
import { Xorshift32 } from './Xorshift32.js'
|
||||
import * as time from '../../time.js'
|
||||
|
||||
const DIAMETER = 300
|
||||
const NUMBERS = 10000
|
||||
|
||||
function runPRNG (name, Gen) {
|
||||
const runPRNG = (name, Gen) => {
|
||||
console.log('== ' + name + ' ==')
|
||||
const gen = new Gen(1234)
|
||||
let head = 0
|
||||
@@ -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.
|
||||
@@ -14,7 +17,7 @@ import Xorshift32 from './Xorshift32.js'
|
||||
*
|
||||
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
|
||||
*/
|
||||
export default class Xoroshiro128plus {
|
||||
export class Xoroshiro128plus {
|
||||
constructor (seed) {
|
||||
this.seed = seed
|
||||
// This is a variant of Xoroshiro128plus to fill the initial state
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* @module prng
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -1,10 +1,13 @@
|
||||
/**
|
||||
* @module prng
|
||||
*/
|
||||
|
||||
import * as binary from '../binary.js'
|
||||
import { fromCharCode, fromCodePoint } from '../string.js'
|
||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
|
||||
import * as math from '../math.js'
|
||||
|
||||
import DefaultPRNG from './PRNG/Xoroshiro128plus.js'
|
||||
import { Xoroshiro128plus as DefaultPRNG } from './PRNG/Xoroshiro128plus.js'
|
||||
|
||||
/**
|
||||
* Description of the function
|
||||
@@ -1,13 +1,17 @@
|
||||
/**
|
||||
* @module prng
|
||||
*/
|
||||
|
||||
/**
|
||||
*TODO: enable tests
|
||||
import * as rt from '../rich-text/formatters.mjs'
|
||||
import { test } from '../test/test.mjs'
|
||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs'
|
||||
import Xorshift32 from './PRNG/Xorshift32.mjs'
|
||||
import MT19937 from './PRNG/Mt19937.mjs'
|
||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs'
|
||||
import { MAX_SAFE_INTEGER } from '../number/constants.mjs'
|
||||
import { BIT32 } from '../binary/constants.mjs'
|
||||
import * as rt from '../rich-text/formatters.js''
|
||||
import { test } from '../test/test.js''
|
||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.js''
|
||||
import Xorshift32 from './PRNG/Xorshift32.js''
|
||||
import MT19937 from './PRNG/Mt19937.js''
|
||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.js''
|
||||
import { MAX_SAFE_INTEGER } from '../number/constants.js''
|
||||
import { BIT32 } from '../binary/constants.js''
|
||||
|
||||
function init (Gen) {
|
||||
return {
|
||||
3
lib/random.js
Normal file
3
lib/random.js
Normal file
@@ -0,0 +1,3 @@
|
||||
/**
|
||||
* @module random
|
||||
*/
|
||||
@@ -1,2 +1,6 @@
|
||||
/**
|
||||
* @module string
|
||||
*/
|
||||
|
||||
export const fromCharCode = String.fromCharCode
|
||||
export const fromCodePoint = String.fromCodePoint
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
/**
|
||||
* @module testing
|
||||
*/
|
||||
|
||||
import * as logging from './logging.js'
|
||||
import simpleDiff from './simpleDiff.js'
|
||||
import { simpleDiff } from './diff.js'
|
||||
|
||||
export const run = async (name, f) => {
|
||||
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
||||
579
package-lock.json
generated
579
package-lock.json
generated
@@ -1,21 +1,94 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-66",
|
||||
"version": "13.0.0-76",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"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": {
|
||||
"version": "0.0.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.38.tgz",
|
||||
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/events": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
|
||||
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "6.0.110",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
|
||||
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz",
|
||||
"integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/events": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"abab": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
||||
@@ -110,17 +183,6 @@
|
||||
"integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=",
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
|
||||
@@ -1577,6 +1639,12 @@
|
||||
"integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
@@ -1631,12 +1699,6 @@
|
||||
"integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
||||
@@ -1662,22 +1724,13 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"center-align": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
|
||||
"integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
|
||||
"catharsis": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz",
|
||||
"integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"align-text": "^0.1.3",
|
||||
"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
|
||||
}
|
||||
"underscore-contrib": "~0.3.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
@@ -1761,17 +1814,6 @@
|
||||
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
@@ -2012,6 +2054,12 @@
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
||||
"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": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
||||
@@ -3073,9 +3121,9 @@
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"dev": true
|
||||
},
|
||||
"extend-shallow": {
|
||||
@@ -3978,6 +4026,12 @@
|
||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
@@ -4082,7 +4136,7 @@
|
||||
},
|
||||
"http-errors": {
|
||||
"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=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -4579,6 +4633,15 @@
|
||||
"dev": 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": {
|
||||
"version": "3.0.2",
|
||||
"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": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||
"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": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz",
|
||||
@@ -5003,12 +5118,6 @@
|
||||
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=",
|
||||
"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": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
|
||||
@@ -5040,7 +5149,7 @@
|
||||
},
|
||||
"magic-string": {
|
||||
"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==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -5061,7 +5170,7 @@
|
||||
},
|
||||
"marked": {
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "2.3.11",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
@@ -5412,7 +5536,7 @@
|
||||
},
|
||||
"parchment": {
|
||||
"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==",
|
||||
"dev": true
|
||||
},
|
||||
@@ -5656,6 +5780,158 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
@@ -5684,7 +5960,7 @@
|
||||
},
|
||||
"quill": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "http://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.6.tgz",
|
||||
"integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -5715,14 +5991,6 @@
|
||||
"deep-equal": "^1.0.1",
|
||||
"extend": "^3.0.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": {
|
||||
@@ -6034,6 +6302,23 @@
|
||||
"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": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz",
|
||||
@@ -6069,15 +6354,6 @@
|
||||
"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": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||
@@ -6097,6 +6373,12 @@
|
||||
"@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": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz",
|
||||
@@ -6111,7 +6393,7 @@
|
||||
},
|
||||
"rollup-plugin-commonjs": {
|
||||
"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==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -6216,12 +6498,46 @@
|
||||
}
|
||||
},
|
||||
"rollup-plugin-uglify": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-1.0.2.tgz",
|
||||
"integrity": "sha1-1KpvXfE1Iurhuhd4DHxMcJYDg1k=",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.0.tgz",
|
||||
"integrity": "sha512-XtzZd159QuOaXNvcxyBcbUCSoBsv5YYWK+7ZwUyujSmISst8avRfjWlp7cGu8T2O52OJnpEBvl+D4WLV1k1iQQ==",
|
||||
"dev": true,
|
||||
"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": {
|
||||
@@ -6252,6 +6568,12 @@
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
|
||||
@@ -6313,6 +6635,12 @@
|
||||
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
|
||||
"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": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
|
||||
@@ -6804,6 +7132,15 @@
|
||||
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
|
||||
"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": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -6842,14 +7179,27 @@
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "2.8.29",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
|
||||
"integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
|
||||
"integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-map": "~0.5.1",
|
||||
"uglify-to-browserify": "~1.0.0",
|
||||
"yargs": "~3.10.0"
|
||||
"commander": "~2.17.1",
|
||||
"source-map": "~0.6.1"
|
||||
},
|
||||
"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": {
|
||||
@@ -6859,6 +7209,29 @@
|
||||
"dev": 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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
|
||||
@@ -6950,6 +7323,12 @@
|
||||
"integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==",
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz",
|
||||
@@ -6997,18 +7376,6 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -7039,6 +7406,12 @@
|
||||
"dev": 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": {
|
||||
"version": "4.0.1",
|
||||
"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",
|
||||
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
package.json
80
package.json
@@ -1,30 +1,55 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-66",
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./src/index.js",
|
||||
"version": "13.0.0-76",
|
||||
"description": "A ",
|
||||
"main": "./build/yjs.js",
|
||||
"module": "./index.js'",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"test": "npm run lint",
|
||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||
"lint": "standard src/**/*.js test/**/*.js tests-lib/**/*.js",
|
||||
"docs": "esdoc",
|
||||
"build": "rm -rf build examples/build && PRODUCTION=1 rollup -c",
|
||||
"watch": "rollup -wc",
|
||||
"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/",
|
||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||
"postversion": "npm run dist"
|
||||
"postversion": "npm run build",
|
||||
"websocket-server": "node ./provider/websocket/server.js",
|
||||
"now-start": "npm run websocket-server"
|
||||
},
|
||||
"files": [
|
||||
"y.*",
|
||||
"src/*",
|
||||
".esdoc.json",
|
||||
"docs/*"
|
||||
"build/*",
|
||||
"bindings/*",
|
||||
"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": {
|
||||
"ignore": [
|
||||
"/y.js",
|
||||
"/y.js.map"
|
||||
"/build",
|
||||
"/node_modules",
|
||||
"/rollup.test.js",
|
||||
"/rollup.test.js"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
@@ -32,13 +57,7 @@
|
||||
"url": "https://github.com/y-js/yjs.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Yjs",
|
||||
"OT",
|
||||
"Collaboration",
|
||||
"Synchronization",
|
||||
"ShareJS",
|
||||
"Coweb",
|
||||
"Concurrency"
|
||||
"crdt"
|
||||
],
|
||||
"author": "Kevin Jahns",
|
||||
"email": "kevin.jahns@rwth-aachen.de",
|
||||
@@ -48,6 +67,7 @@
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"devDependencies": {
|
||||
"@types/ws": "^6.0.1",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-regenerator": "^6.26.0",
|
||||
@@ -57,18 +77,26 @@
|
||||
"cutest": "^0.1.9",
|
||||
"esdoc": "^1.1.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-cursors": "^1.0.3",
|
||||
"rollup": "^0.58.2",
|
||||
"rollup-cli": "^1.0.9",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.4.1",
|
||||
"rollup-plugin-inject": "^2.2.0",
|
||||
"rollup-plugin-multi-entry": "^2.0.2",
|
||||
"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-watch": "^3.2.2",
|
||||
"standard": "^11.0.1"
|
||||
"standard": "^11.0.1",
|
||||
"tui-jsdoc-template": "^1.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^6.1.0"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/*
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
|
||||
|
||||
function createFilePath (persistence, roomName) {
|
||||
@@ -10,10 +11,10 @@ function createFilePath (persistence, roomName) {
|
||||
return path.join(persistence.dir, roomName)
|
||||
}
|
||||
|
||||
export default class FilePersistence {
|
||||
export class FilePersistence {
|
||||
constructor (dir) {
|
||||
this.dir = dir
|
||||
this._mutex = createMutualExclude()
|
||||
this._mutex = createMutex()
|
||||
}
|
||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||
// TODO: implement
|
||||
@@ -70,3 +71,4 @@ export default class FilePersistence {
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
||||
553
persistences/IndexedDBPersistence.js
Normal file
553
persistences/IndexedDBPersistence.js
Normal file
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
import { Y } from '../utils/Y.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
|
||||
|
||||
function rtop (request) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDB (room) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open(room)
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('updates')) {
|
||||
db.deleteObjectStore('updates')
|
||||
}
|
||||
db.createObjectStore('updates', {autoIncrement: true})
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function persist (room) {
|
||||
let t = room.db.transaction(['updates'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll())
|
||||
.then(updates => {
|
||||
// apply all previous updates before deleting them
|
||||
room.mutex(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
})
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
// delete all pending updates
|
||||
rtop(updatesStore.clear()).then(() => {
|
||||
// write current model
|
||||
updatesStore.put(encoder.createBuffer())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveUpdate (room, updateBuffer) {
|
||||
const db = room.db
|
||||
if (db !== null) {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||
rtop(updatesStore.count()).then(cnt => {
|
||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||
persist(room)
|
||||
}
|
||||
})
|
||||
return updatePut
|
||||
}
|
||||
}
|
||||
|
||||
function registerRoomInPersistence (documentsDB, roomName) {
|
||||
return documentsDB.then(
|
||||
db => Promise.all([
|
||||
db,
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
])
|
||||
).then(
|
||||
([db, doc]) => {
|
||||
if (doc === undefined) {
|
||||
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 400
|
||||
|
||||
export class IndexedDBPersistence {
|
||||
constructor () {
|
||||
this._rooms = new Map()
|
||||
this._documentsDB = new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open('_yjs_documents')
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('documents')) {
|
||||
db.deleteObjectStore('documents')
|
||||
}
|
||||
db.createObjectStore('documents', { keyPath: "roomName" })
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
addEventListener('unload', () => {
|
||||
// close everything when page unloads
|
||||
this._rooms.forEach(room => {
|
||||
if (room.db !== null) {
|
||||
room.db.close()
|
||||
} else {
|
||||
room.dbPromise.then(db => db.close())
|
||||
}
|
||||
})
|
||||
this._documentsDB.then(db => db.close())
|
||||
})
|
||||
}
|
||||
getAllDocuments () {
|
||||
return this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
||||
)
|
||||
}
|
||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||
this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
||||
)
|
||||
}
|
||||
|
||||
_createYInstance (roomName) {
|
||||
const room = this._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
return room.y
|
||||
}
|
||||
const y = new Y()
|
||||
return openDB(roomName).then(
|
||||
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
||||
).then(
|
||||
updates =>
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
).then(() => Promise.resolve(y))
|
||||
}
|
||||
|
||||
_persistStructsDS (roomName, structsDS) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||
encoder.writeArrayBuffer(structsDS)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
_persistStructs (roomName, structs) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_UPDATE)
|
||||
encoder.writeArrayBuffer(structs)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
connectY (roomName, y) {
|
||||
if (this._rooms.has(roomName)) {
|
||||
throw new Error('A Y instance is already bound to this room!')
|
||||
}
|
||||
let room = {
|
||||
db: null,
|
||||
dbPromise: null,
|
||||
channel: null,
|
||||
mutex: createMutex(),
|
||||
y
|
||||
}
|
||||
if (typeof BroadcastChannel !== 'undefined') {
|
||||
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
||||
room.channel.addEventListener('message', e => {
|
||||
room.mutex(function () {
|
||||
decodePersisted(y, new BinaryDecoder(e.data))
|
||||
})
|
||||
})
|
||||
}
|
||||
y.on('destroyed', () => {
|
||||
this.disconnectY(roomName, y)
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
room.mutex(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const update = new BinaryEncoder()
|
||||
encodeUpdate(y, transaction.encodedStructs, update)
|
||||
const updateBuffer = update.createBuffer()
|
||||
if (room.channel !== null) {
|
||||
room.channel.postMessage(updateBuffer)
|
||||
}
|
||||
if (transaction.encodedStructsLen > 0
|
||||
import { Y } from '../utils/Y.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
|
||||
|
||||
function rtop (request) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDB (room) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open(room)
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('updates')) {
|
||||
db.deleteObjectStore('updates')
|
||||
}
|
||||
db.createObjectStore('updates', {autoIncrement: true})
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function persist (room) {
|
||||
let t = room.db.transaction(['updates'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll())
|
||||
.then(updates => {
|
||||
// apply all previous updates before deleting them
|
||||
room.mutex(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
})
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
// delete all pending updates
|
||||
rtop(updatesStore.clear()).then(() => {
|
||||
// write current model
|
||||
updatesStore.put(encoder.createBuffer())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveUpdate (room, updateBuffer) {
|
||||
const db = room.db
|
||||
if (db !== null) {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||
rtop(updatesStore.count()).then(cnt => {
|
||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||
persist(room)
|
||||
}
|
||||
})
|
||||
return updatePut
|
||||
}
|
||||
}
|
||||
|
||||
function registerRoomInPersistence (documentsDB, roomName) {
|
||||
return documentsDB.then(
|
||||
db => Promise.all([
|
||||
db,
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
])
|
||||
).then(
|
||||
([db, doc]) => {
|
||||
if (doc === undefined) {
|
||||
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 400
|
||||
|
||||
export class IndexedDBPersistence {
|
||||
constructor () {
|
||||
this._rooms = new Map()
|
||||
this._documentsDB = new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open('_yjs_documents')
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('documents')) {
|
||||
db.deleteObjectStore('documents')
|
||||
}
|
||||
db.createObjectStore('documents', { keyPath: "roomName" })
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
addEventListener('unload', () => {
|
||||
// close everything when page unloads
|
||||
this._rooms.forEach(room => {
|
||||
if (room.db !== null) {
|
||||
room.db.close()
|
||||
} else {
|
||||
room.dbPromise.then(db => db.close())
|
||||
}
|
||||
})
|
||||
this._documentsDB.then(db => db.close())
|
||||
})
|
||||
}
|
||||
getAllDocuments () {
|
||||
return this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
||||
)
|
||||
}
|
||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||
this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
||||
)
|
||||
}
|
||||
|
||||
_createYInstance (roomName) {
|
||||
const room = this._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
return room.y
|
||||
}
|
||||
const y = new Y()
|
||||
return openDB(roomName).then(
|
||||
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
||||
).then(
|
||||
updates =>
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
).then(() => Promise.resolve(y))
|
||||
}
|
||||
|
||||
_persistStructsDS (roomName, structsDS) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||
encoder.writeArrayBuffer(structsDS)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
_persistStructs (roomName, structs) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_UPDATE)
|
||||
encoder.writeArrayBuffer(structs)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
connectY (roomName, y) {
|
||||
if (this._rooms.has(roomName)) {
|
||||
throw new Error('A Y instance is already bound to this room!')
|
||||
}
|
||||
let room = {
|
||||
db: null,
|
||||
dbPromise: null,
|
||||
channel: null,
|
||||
mutex: createMutex(),
|
||||
y
|
||||
}
|
||||
if (typeof BroadcastChannel !== 'undefined') {
|
||||
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
||||
room.channel.addEventListener('message', e => {
|
||||
room.mutex(function () {
|
||||
decodePersisted(y, new BinaryDecoder(e.data))
|
||||
})
|
||||
})
|
||||
}
|
||||
y.on('destroyed', () => {
|
||||
this.disconnectY(roomName, y)
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
room.mutex(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const update = new BinaryEncoder()
|
||||
encodeUpdate(y, transaction.encodedStructs, update)
|
||||
const updateBuffer = update.createBuffer()
|
||||
if (room.channel !== null) {
|
||||
room.channel.postMessage(updateBuffer)
|
||||
}
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
if (room.db !== null) {
|
||||
saveUpdate(room, updateBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// register document in documentsDB
|
||||
this._documentsDB.then(
|
||||
db =>
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
.then(
|
||||
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
||||
)
|
||||
)
|
||||
// open room db and read existing data
|
||||
return room.dbPromise = openDB(roomName)
|
||||
.then(db => {
|
||||
room.db = db
|
||||
const t = room.db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
// write current state as update
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
||||
// read persisted state
|
||||
return rtop(updatesStore.getAll()).then(updates => {
|
||||
room.mutex(() => {
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
disconnectY (roomName) {
|
||||
const {
|
||||
db, channel
|
||||
} = this._rooms.get(roomName)
|
||||
db.close()
|
||||
if (channel !== null) {
|
||||
channel.close()
|
||||
}
|
||||
this._rooms.delete(roomName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*
|
||||
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||
this.disconnectY(roomName)
|
||||
return rtop(indexedDB.deleteDatabase(roomName))
|
||||
}
|
||||
}
|
||||
{
|
||||
if (room.db !== null) {
|
||||
saveUpdate(room, updateBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// register document in documentsDB
|
||||
this._documentsDB.then(
|
||||
db =>
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
.then(
|
||||
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
||||
)
|
||||
)
|
||||
// open room db and read existing data
|
||||
return room.dbPromise = openDB(roomName)
|
||||
.then(db => {
|
||||
room.db = db
|
||||
const t = room.db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
// write current state as update
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
||||
// read persisted state
|
||||
return rtop(updatesStore.getAll()).then(updates => {
|
||||
room.mutex(() => {
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
disconnectY (roomName) {
|
||||
const {
|
||||
db, channel
|
||||
} = this._rooms.get(roomName)
|
||||
db.close()
|
||||
if (channel !== null) {
|
||||
channel.close()
|
||||
}
|
||||
this._rooms.delete(roomName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*
|
||||
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||
this.disconnectY(roomName)
|
||||
return rtop(indexedDB.deleteDatabase(roomName))
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1,3 +1,4 @@
|
||||
/*
|
||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
|
||||
import { writeStructs } from '../MessageHandler/syncStep1.js'
|
||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
|
||||
@@ -6,10 +7,10 @@ export const PERSIST_UPDATE = 0
|
||||
/**
|
||||
* Write an update to an encoder.
|
||||
*
|
||||
* @param {Yjs} y A Yjs instance
|
||||
* @param {BinaryEncoder} updateEncoder I.e. transaction.encodedStructs
|
||||
*/
|
||||
export function encodeUpdate (y, updateEncoder, encoder) {
|
||||
* @param {Y} y A Yjs instance
|
||||
* @param {Encoder} updateEncoder I.e. transaction.encodedStructs
|
||||
*
|
||||
export const encodeUpdate = (y, updateEncoder, encoder) => {
|
||||
encoder.writeVarUint(PERSIST_UPDATE)
|
||||
encoder.writeBinaryEncoder(updateEncoder)
|
||||
}
|
||||
@@ -19,10 +20,10 @@ export const PERSIST_STRUCTS_DS = 1
|
||||
/**
|
||||
* Write the current Yjs data model to an encoder.
|
||||
*
|
||||
* @param {Yjs} y A Yjs instance
|
||||
* @param {BinaryEncoder} encoder An encoder to write to
|
||||
*/
|
||||
export function encodeStructsDS (y, encoder) {
|
||||
* @param {Y} y A Yjs instance
|
||||
* @param {Encoder} encoder An encoder to write to
|
||||
*
|
||||
export const encodeStructsDS = (y, encoder) => {
|
||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||
writeStructs(y, encoder, new Map())
|
||||
writeDeleteSet(y, encoder)
|
||||
@@ -30,10 +31,10 @@ export function encodeStructsDS (y, encoder) {
|
||||
|
||||
/**
|
||||
* Feed the Yjs instance with the persisted state
|
||||
* @param {Yjs} y A Yjs instance.
|
||||
* @param {BinaryDecoder} decoder A Decoder instance that holds the file content.
|
||||
*/
|
||||
export function decodePersisted (y, decoder) {
|
||||
* @param {Y} y A Yjs instance.
|
||||
* @param {Decoder} decoder A Decoder instance that holds the file content.
|
||||
*
|
||||
export const decodePersisted = (y, decoder) => {
|
||||
y.transact(() => {
|
||||
while (decoder.hasContent()) {
|
||||
const contentType = decoder.readVarUint()
|
||||
@@ -49,3 +50,4 @@ export function decodePersisted (y, decoder) {
|
||||
}
|
||||
}, true)
|
||||
}
|
||||
*/
|
||||
5
persistences/indexeddb.js
Normal file
5
persistences/indexeddb.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as idb from '../lib/idb.js'
|
||||
|
||||
const bc = new BroadcastChannel('ydb-client')
|
||||
|
||||
idb.openDB()
|
||||
33
protocols/auth.js
Normal file
33
protocols/auth.js
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js';
|
||||
|
||||
export const messagePermissionDenied = 0
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {string} reason
|
||||
*/
|
||||
export const writePermissionDenied = (encoder, reason) => {
|
||||
encoding.writeVarUint(encoder, messagePermissionDenied)
|
||||
encoding.writeVarString(encoder, reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback PermissionDeniedHandler
|
||||
* @param {any} y
|
||||
* @param {string} reason
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @param {PermissionDeniedHandler} permissionDeniedHandler
|
||||
*/
|
||||
export const readAuthMessage = (decoder, y, permissionDeniedHandler) => {
|
||||
switch (decoding.readVarUint(decoder)) {
|
||||
case messagePermissionDenied: permissionDeniedHandler(y, decoding.readVarString(decoder))
|
||||
}
|
||||
}
|
||||
112
protocols/awareness.js
Normal file
112
protocols/awareness.js
Normal 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
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
/**
|
||||
* @module sync-protocol
|
||||
*/
|
||||
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as ID from './Util/ID.js'
|
||||
import { getStruct } from './Util/structReferences.js'
|
||||
import { deleteItemRange } from './Struct/Delete.js'
|
||||
import { integrateRemoteStruct } from './Util/integrateRemoteStructs.js'
|
||||
import Item from './Struct/Item.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { getStruct } from '../utils/structReferences.js'
|
||||
import { deleteItemRange } from '../utils/structManipulation.js'
|
||||
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
import { Item } from '../structs/Item.js'
|
||||
import * as stringify from '../utils/structStringify.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('./Store/StateStore.js').default} StateStore
|
||||
* @typedef {import('./Y.js').default} Y
|
||||
* @typedef {import('./Struct/Item.js').default} Item
|
||||
* @typedef {import('./Store/StateStore.js').StateSet} StateSet
|
||||
* @typedef {Map<number, number>} StateSet
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -39,9 +41,9 @@ import Item from './Struct/Item.js'
|
||||
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
||||
*/
|
||||
|
||||
const messageYjsSyncStep1 = 0
|
||||
const messageYjsSyncStep2 = 1
|
||||
const messageYjsUpdate = 2
|
||||
export const messageYjsSyncStep1 = 0
|
||||
export const messageYjsSyncStep2 = 1
|
||||
export const messageYjsUpdate = 2
|
||||
|
||||
/**
|
||||
* Stringifies a message-encoded Delete Set.
|
||||
@@ -54,7 +56,7 @@ export const stringifyDeleteSet = (decoder) => {
|
||||
const dsLength = decoding.readUint32(decoder)
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user
|
||||
const dvLength = decoding.readVarUint(decoder)
|
||||
const dvLength = decoding.readUint32(decoder)
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
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
|
||||
if (currentUser !== user) {
|
||||
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
|
||||
encoding.setUint32(encoder, lastLenPos, currentLength)
|
||||
}
|
||||
@@ -192,8 +195,8 @@ export const readDeleteSet = (decoder, y) => {
|
||||
*/
|
||||
export const stringifyStateSet = decoder => {
|
||||
let s = 'State Set: '
|
||||
readStateSet(decoder).forEach((user, userState) => {
|
||||
s += `(${user}: ${userState}), `
|
||||
readStateSet(decoder).forEach((clock, user) => {
|
||||
s += `(${user}: ${clock}), `
|
||||
})
|
||||
return s
|
||||
}
|
||||
@@ -201,15 +204,15 @@ export const stringifyStateSet = decoder => {
|
||||
/**
|
||||
* Write StateSet to Encoder
|
||||
*
|
||||
* @param {Y} y
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Y} y
|
||||
*/
|
||||
export const writeStateSet = (encoder, y) => {
|
||||
const state = y.ss.state
|
||||
// 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.
|
||||
encoding.writeUint32(encoder, state.size)
|
||||
state.forEach((user, clock) => {
|
||||
state.forEach((clock, user) => {
|
||||
encoding.writeVarUint(encoder, user)
|
||||
encoding.writeVarUint(encoder, clock)
|
||||
})
|
||||
@@ -232,50 +235,6 @@ export const readStateSet = decoder => {
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify an item id.
|
||||
*
|
||||
* @param {ID.ID | ID.RootID} id
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifyID = id => id instanceof ID.ID ? `(${id.user},${id.clock})` : `(${id.name},${id.type})`
|
||||
|
||||
/**
|
||||
* Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent).
|
||||
*
|
||||
* @param {Item | Y | null} item
|
||||
* @return {string}
|
||||
*/
|
||||
export const stringifyItemID = item => {
|
||||
let result
|
||||
if (item === null) {
|
||||
result = '()'
|
||||
} else if (item instanceof Item) {
|
||||
result = stringifyID(item._id)
|
||||
} else {
|
||||
// must be a Yjs instance
|
||||
// Don't include Y in this module, so we prevent circular dependencies.
|
||||
result = 'y'
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility to convert an item to a readable format.
|
||||
*
|
||||
* @param {String} name The name of the item class (YText, ItemString, ..).
|
||||
* @param {Item} item The item instance.
|
||||
* @param {String} [append] Additional information to append to the returned
|
||||
* string.
|
||||
* @return {String} A readable string that represents the item object.
|
||||
*
|
||||
*/
|
||||
export const logItemHelper = (name, item, append) => {
|
||||
const left = item._left !== null ? stringifyID(item._left._lastId) : '()'
|
||||
const origin = item._origin !== null ? stringifyID(item._origin._lastId) : '()'
|
||||
return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item._right)},parent:${stringifyItemID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
@@ -291,7 +250,7 @@ export const stringifyStructs = (decoder, y) => {
|
||||
let missing = struct._fromBinary(y, decoder)
|
||||
let logMessage = ' ' + struct._logString()
|
||||
if (missing.length > 0) {
|
||||
logMessage += ' .. missing: ' + missing.map(stringifyItemID).join(', ')
|
||||
logMessage += ' .. missing: ' + missing.map(stringify.stringifyItemID).join(', ')
|
||||
}
|
||||
str += logMessage + '\n'
|
||||
}
|
||||
@@ -317,7 +276,9 @@ export const writeStructs = (encoder, y, ss) => {
|
||||
const overlappingLeft = y.os.findPrev(minBound)
|
||||
const rightID = overlappingLeft === null ? null : overlappingLeft._id
|
||||
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)
|
||||
len++
|
||||
}
|
||||
@@ -406,10 +367,9 @@ export const stringifySyncStep2 = (decoder, y) => {
|
||||
* Read and apply Structs and then DeleteSet to a y instance.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Y} y
|
||||
*/
|
||||
export const readSyncStep2 = (decoder, encoder, y) => {
|
||||
export const readSyncStep2 = (decoder, y) => {
|
||||
readStructs(decoder, y)
|
||||
readDeleteSet(decoder, y)
|
||||
}
|
||||
@@ -424,6 +384,7 @@ export const stringifyUpdate = (decoder, y) =>
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} numOfStructs
|
||||
* @param {encoding.Encoder} updates
|
||||
*/
|
||||
export const writeUpdate = (encoder, numOfStructs, updates) => {
|
||||
@@ -439,7 +400,7 @@ export const readUpdate = readStructs
|
||||
* @param {Y} y
|
||||
* @return {string} The message converted to string
|
||||
*/
|
||||
export const stringifyMessage = (decoder, y) => {
|
||||
export const stringifySyncMessage = (decoder, y) => {
|
||||
const messageType = decoding.readVarUint(decoder)
|
||||
let stringifiedMessage
|
||||
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 {Y} y
|
||||
*/
|
||||
export const readMessage = (decoder, encoder, y) => {
|
||||
export const readSyncMessage = (decoder, encoder, y) => {
|
||||
const messageType = decoding.readVarUint(decoder)
|
||||
switch (messageType) {
|
||||
case messageYjsSyncStep1:
|
||||
readSyncStep1(decoder, encoder, y)
|
||||
break
|
||||
case messageYjsSyncStep2:
|
||||
y.transact(() => readSyncStep2(decoder, encoder, y), true)
|
||||
y.transact(() => readSyncStep2(decoder, y), true)
|
||||
break
|
||||
case messageYjsUpdate:
|
||||
y.transact(() => readUpdate(decoder, y), true)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user