Compare commits

...

29 Commits

Author SHA1 Message Date
Kevin Jahns
772bb87d5c 13.0.8 2020-05-13 19:29:51 +02:00
Kevin Jahns
dab172fa1d Rework UndoManager to support changes from other / multiple users 2020-05-13 19:28:30 +02:00
Kevin Jahns
a70c5112cd fix wrong type declaration in documentation. fixes #195 2020-05-11 11:10:38 +02:00
Kevin Jahns
7cb423c046 13.0.7 2020-05-11 01:46:51 +02:00
Kevin Jahns
4547b35641 cleanup formatting attributes 2020-05-11 01:45:27 +02:00
Kevin Jahns
4c87f9a021 13.0.6 2020-05-08 14:50:53 +02:00
Kevin Jahns
4b08c67e06 bump lib0 to fix critical encoding issue in safari 2020-05-08 14:49:50 +02:00
Kevin Jahns
9f5bc9ddfe change client id when duplicate content is detected 2020-05-03 16:10:58 +02:00
Kevin Jahns
b399ffa765 add gc information to API docs 2020-04-26 13:24:18 +02:00
Kevin Jahns
180f4667c1 Readme correction: UndoManager accepts options 2020-04-17 02:02:09 +02:00
Kevin Jahns
9455373611 Merge branch 'master' of github.com:yjs/yjs 2020-04-15 20:50:29 +02:00
Kevin Jahns
aa804d89c0 update now.sh links 2020-04-15 19:52:34 +02:00
Kevin Jahns
3ef51a5d1a run test-exhaustive 2020-04-03 12:11:25 +02:00
Kevin Jahns
e61089c659 npm ci before workflow start 2020-04-03 12:09:13 +02:00
Kevin Jahns
97625cf29b fix workflow 2020-04-03 12:05:43 +02:00
Kevin Jahns
a5dc6c27aa Setup github workflow 2020-04-03 12:02:37 +02:00
Kevin Jahns
26a51bafc9 13.0.5 2020-04-02 01:05:04 +02:00
Kevin Jahns
f40e09d156 type fixes for breaking typescript@3.8.* release 2020-04-02 01:03:30 +02:00
Kevin Jahns
81650bc8f6 Merge branch 'gived-ISNIT0/187' 2020-04-01 23:44:40 +02:00
Kevin Jahns
c87caafeb6 lint & refactor PR #187 2020-04-01 23:39:27 +02:00
Kevin Jahns
195b26d90f Merge branch 'ISNIT0/187' of https://github.com/gived/yjs into gived-ISNIT0/187 2020-04-01 14:05:18 +02:00
Kevin Jahns
7e0189ca84 Merge branch 'master' of github.com:yjs/yjs 2020-04-01 14:04:45 +02:00
Kevin Jahns
192706f2a8 update readme 2020-04-01 14:04:41 +02:00
Joe Reeve
a4ce8ae07d 🐛 fix for #187 2020-03-31 16:06:28 +01:00
Kevin Jahns
e04a980af1 Merge pull request #184 from yjs/readme-cleanup
remove deadlinks
2020-03-21 21:50:43 +01:00
Nik Graf
47d40eb6b0 remove deadlinks 2020-03-21 15:51:39 +01:00
Kevin Jahns
fc4a39cc7d Merge pull request #182 from LucasGenoud/patch-1
Update lib0 to latest version
2020-02-27 18:13:22 +01:00
LucasGenoud
44e1fd9f14 Update lib0 to latest version 2020-02-27 10:51:21 +01:00
Kevin Jahns
02cc5a215f bump lib0 2020-02-19 09:49:54 -06:00
30 changed files with 575 additions and 194 deletions

31
.github/workflows/nodejs.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 13.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run lint
- run: npm run test-extensive
env:
CI: true

View File

@@ -15,13 +15,15 @@ suited for even large documents.
* Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos)
* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev)
* Benchmarks:
* Benchmark Yjs vs. Automerge:
[https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
* Podcast [**"Yjs Deep Dive into real time collaborative editing solutions":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0)
* Podcast [**"Google Docs-style editing in Gutenberg with the YJS framework":**](https://publishpress.com/blog/yjs/)
:construction_worker_woman: If you are looking for professional support to build
collaborative or distributed applications ping us at <yjs@tag1consulting.com>.
:construction_worker_woman: If you are looking for professional (paid) support to
build collaborative or distributed applications ping us at
<yjs@tag1consulting.com>. Otherwise you can find help on our
[discussion board](https://discuss.yjs.dev).
## Table of Contents
@@ -38,12 +40,6 @@ collaborative or distributed applications ping us at <yjs@tag1consulting.com>.
* [Miscellaneous](#Miscellaneous)
* [Typescript Declarations](#Typescript-Declarations)
* [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm)
* [Evaluation](#Evaluation)
* [Existing shared editing libraries](#Exisisting-Javascript-Libraries)
* [CRDT Algorithms](#CRDT-Algorithms)
* [Comparison of CRDT with OT](#Comparing-CRDT-with-OT)
* [Comparison of CRDT Algorithms](#Comparing-CRDT-Algorithms)
* [Comparison of Yjs with other Implementations](#Comparing-Yjs-with-other-Implementations)
* [License and Author](#License-and-Author)
## Overview
@@ -56,13 +52,10 @@ are implemented in separate modules.
| Name | Cursors | Binding | Demo |
|---|:-:|---|---|
| [ProseMirror](https://prosemirror.net/) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) |
| [Quill](https://quilljs.com/) | ✔ | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) |
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) |
| [Textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) | | [y-textarea](http://github.com/yjs/y-textarea) | [demo](https://yjs-demos.now.sh/textarea/) |
| [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) | | [y-dom](http://github.com/yjs/y-dom) | [demo](https://yjs-demos.now.sh/dom/) |
| [ProseMirror](https://prosemirror.net/) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://demos.yjs.dev/prosemirror/prosemirror.html) |
| [Quill](https://quilljs.com/) | ✔ | [y-quill](http://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
### Providers
@@ -470,6 +463,12 @@ const doc = new Y.Doc()
<dl>
<b><code>clientID</code></b>
<dd>A unique id that identifies this client. (readonly)</dd>
<b><code>gc</code></b>
<dd>
Whether garbage collection is enabled on this doc instance. Set `doc.gc = false`
in order to disable gc and be able to restore old content. See https://github.com/yjs/yjs#yjs-crdt-algorithm
for more information about gc in Yjs.
</dd>
<b><code>transact(function(Transaction):void [, origin:any])</code></b>
<dd>
Every change on the shared document happens in a transaction. Observer calls and
@@ -646,7 +645,7 @@ Yjs ships with an Undo/Redo manager for selective undo/redo of of changes on a
Yjs type. The changes can be optionally scoped to transaction origins.
```js
const ytext = doc.getArray('array')
const ytext = doc.getText('text')
const undoManager = new Y.UndoManager(ytext)
ytext.insert(0, 'abc')
@@ -657,8 +656,8 @@ ytext.toString() // => 'abc'
```
<dl>
<b><code>constructor(scope:Y.AbstractType|Array&lt;Y.AbstractType&gt;,
[[{captureTimeout:number,trackedOrigins:Set&lt;any&gt;,deleteFilter:function(item):boolean}]])</code></b>
<b><code>constructor(scope:Y.AbstractType|Array&lt;Y.AbstractType&gt;
[, {captureTimeout:number,trackedOrigins:Set&lt;any&gt;,deleteFilter:function(item):boolean}])</code></b>
<dd>Accepts either single type as scope or an array of types.</dd>
<b><code>undo()</code></b>
<dd></dd>
@@ -698,28 +697,30 @@ StackItem won't be merged.
// without stopCapturing
ytext.insert(0, 'a')
ytext.insert(1, 'b')
um.undo()
undoManager.undo()
ytext.toString() // => '' (note that 'ab' was removed)
// with stopCapturing
ytext.insert(0, 'a')
um.stopCapturing()
undoManager.stopCapturing()
ytext.insert(0, 'b')
um.undo()
undoManager.undo()
ytext.toString() // => 'a' (note that only 'b' was removed)
```
#### Example: Specify tracked origins
Every change on the shared document has an origin. If no origin was specified,
it defaults to `null`. By specifying `trackedTransactionOrigins` you can
it defaults to `null`. By specifying `trackedOrigins` you can
selectively specify which changes should be tracked by `UndoManager`. The
UndoManager instance is always added to `trackedTransactionOrigins`.
UndoManager instance is always added to `trackedOrigins`.
```js
class CustomBinding {}
const ytext = doc.getArray('array')
const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding]))
const ytext = doc.getText('text')
const undoManager = new Y.UndoManager(ytext, {
trackedOrigins: new Set([42, CustomBinding])
})
ytext.insert(0, 'abc')
undoManager.undo()
@@ -756,8 +757,10 @@ additional meta information like the cursor location or the view on the
document. You can assign meta-information to Undo-/Redo-StackItems.
```js
const ytext = doc.getArray('array')
const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding]))
const ytext = doc.getText('text')
const undoManager = new Y.UndoManager(ytext, {
trackedOrigins: new Set([42, CustomBinding])
})
undoManager.on('stack-item-added', event => {
// save the current cursor location on the stack-item

42
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.4",
"version": "13.0.8",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -98,9 +98,9 @@
}
},
"acorn": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
"integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
"integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
"dev": true
},
"acorn-jsx": {
@@ -513,9 +513,9 @@
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
}
@@ -1439,11 +1439,11 @@
}
},
"lib0": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.12.tgz",
"integrity": "sha512-ileLyIMsWqcpyFLP6WMIgD6C7rkwrdYjH6CCFvAnqOhnGcyAl8LmLkiryVNmKW0sLRw/gLcB47LGXJDw0tDlkg==",
"version": "0.2.26",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.26.tgz",
"integrity": "sha512-DTf0VmFNi/eT+3Q+6rNHYdIAx69ROpvQkpnplpDoErW8NeRwjPwoIKjCF3rKebsMrQoxH4tFD1bvMQb4CUzcFg==",
"requires": {
"isomorphic.js": "^0.1.1"
"isomorphic.js": "^0.1.3"
}
},
"linkify-it": {
@@ -2135,9 +2135,9 @@
"dev": true
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"strip-json-comments": {
@@ -2473,9 +2473,9 @@
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
}
@@ -2783,12 +2783,12 @@
"dev": true
},
"y-protocols": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-0.2.0.tgz",
"integrity": "sha512-B9MCxMqLZCziLmQFlrXVN7MbIhXzF9bdwePcHzlVFIEHxnEvYIqAQYj4FHS376LBgfcjTUs7T614D+w0bpcJLA==",
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-0.2.3.tgz",
"integrity": "sha512-mJ838iW7XgMQqlv+9DtH7QyLqflZoy/VvaUWRIpwawee4mQiFJcEXazCmSYUHEbXIUuVNNc70FnuNSMWDC5vKQ==",
"dev": true,
"requires": {
"lib0": "^0.2.3"
"lib0": "^0.2.20"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.4",
"version": "13.0.8",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
@@ -56,7 +56,7 @@
},
"homepage": "https://yjs.dev",
"dependencies": {
"lib0": "^0.2.12"
"lib0": "^0.2.26"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^11.0.1",
@@ -70,6 +70,6 @@
"standard": "^14.0.0",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^3.7.5",
"y-protocols": "^0.2.0"
"y-protocols": "^0.2.3"
}
}

View File

@@ -71,7 +71,6 @@ export default [{
plugins: [
debugResolve,
nodeResolve({
sourcemap: true,
mainFields: ['module', 'browser', 'main']
}),
commonjs()
@@ -87,7 +86,6 @@ export default [{
plugins: [
debugResolve,
nodeResolve({
sourcemap: true,
mainFields: ['module', 'main']
}),
commonjs()

View File

@@ -26,6 +26,7 @@ export {
ContentType,
AbstractType,
RelativePosition,
getTypeChildren,
createRelativePositionFromTypeIndex,
createRelativePositionFromJSON,
createAbsolutePositionFromRelativePosition,
@@ -47,6 +48,7 @@ export {
typeMapGetSnapshot,
iterateDeletedStructs,
applyUpdate,
readUpdate,
encodeStateAsUpdate,
encodeStateVector,
UndoManager,

View File

@@ -6,9 +6,6 @@ import {
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
import * as error from 'lib0/error.js'
/**
* @private
*/
export class AbstractStruct {
/**
* @param {ID} id
@@ -40,7 +37,6 @@ export class AbstractStruct {
* @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset
* @param {number} encodingRef
* @private
*/
write (encoder, offset, encodingRef) {
throw error.methodUnimplemented()
@@ -54,9 +50,6 @@ export class AbstractStruct {
}
}
/**
* @private
*/
export class AbstractStructRef {
/**
* @param {ID} id

View File

@@ -5,9 +5,6 @@ import {
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export class ContentAny {
/**
* @param {Array<any>} arr
@@ -101,8 +98,6 @@ export class ContentAny {
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentAny}
*/

View File

@@ -7,9 +7,6 @@ import * as decoding from 'lib0/decoding.js'
import * as buffer from 'lib0/buffer.js'
import * as error from 'lib0/error.js'
/**
* @private
*/
export class ContentBinary {
/**
* @param {Uint8Array} content
@@ -92,8 +89,6 @@ export class ContentBinary {
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentBinary}
*/

View File

@@ -7,9 +7,6 @@ import {
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export class ContentDeleted {
/**
* @param {number} len

View File

@@ -95,8 +95,6 @@ export class ContentFormat {
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentFormat}
*/

View File

@@ -304,7 +304,6 @@ export class Item extends AbstractStruct {
/**
* @param {Transaction} transaction
* @private
*/
integrate (transaction) {
const store = transaction.doc.store
@@ -403,7 +402,6 @@ export class Item extends AbstractStruct {
/**
* Returns the next non-deleted item
* @private
*/
get next () {
let n = this.right
@@ -415,7 +413,6 @@ export class Item extends AbstractStruct {
/**
* Returns the previous non-deleted item
* @private
*/
get prev () {
let n = this.left
@@ -486,8 +483,6 @@ export class Item extends AbstractStruct {
/**
* @param {StructStore} store
* @param {boolean} parentGCd
*
* @private
*/
gc (store, parentGCd) {
if (!this.deleted) {
@@ -509,8 +504,6 @@ export class Item extends AbstractStruct {
*
* @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset
*
* @private
*/
write (encoder, offset) {
const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin

View File

@@ -19,10 +19,25 @@ import * as iterator from 'lib0/iterator.js'
import * as error from 'lib0/error.js'
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
/**
* Accumulate all (list) children of a type and return them as an Array.
*
* @param {AbstractType<any>} t
* @return {Array<Item>}
*/
export const getTypeChildren = t => {
let s = t._start
const arr = []
while (s) {
arr.push(s)
s = s.right
}
return arr
}
/**
* Call event listeners with an event. This will also add an event to all
* parents (for `.observeDeep` handlers).
* @private
*
* @template EventType
* @param {AbstractType<EventType>} type
@@ -54,17 +69,14 @@ export class AbstractType {
*/
this._item = null
/**
* @private
* @type {Map<string,Item>}
*/
this._map = new Map()
/**
* @private
* @type {Item|null}
*/
this._start = null
/**
* @private
* @type {Doc|null}
*/
this.doc = null
@@ -90,7 +102,6 @@ export class AbstractType {
*
* @param {Doc} y The Yjs instance
* @param {Item|null} item
* @private
*/
_integrate (y, item) {
this.doc = y
@@ -99,7 +110,6 @@ export class AbstractType {
/**
* @return {AbstractType<EventType>}
* @private
*/
_copy () {
throw error.methodUnimplemented()
@@ -107,7 +117,6 @@ export class AbstractType {
/**
* @param {encoding.Encoder} encoder
* @private
*/
_write (encoder) { }
@@ -128,8 +137,6 @@ export class AbstractType {
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*
* @private
*/
_callObserver (transaction, parentSubs) { /* skip if no type is specified */ }

View File

@@ -61,8 +61,6 @@ export class YArray extends AbstractType {
*
* @param {Doc} y The Yjs instance
* @param {Item} item
*
* @private
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -83,8 +81,6 @@ export class YArray extends AbstractType {
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*
* @private
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YArrayEvent(this, transaction))
@@ -200,7 +196,6 @@ export class YArray extends AbstractType {
/**
* @param {encoding.Encoder} encoder
* @private
*/
_write (encoder) {
encoding.writeVarUint(encoder, YArrayRefID)

View File

@@ -63,8 +63,6 @@ export class YMap extends AbstractType {
*
* @param {Doc} y The Yjs instance
* @param {Item} item
*
* @private
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -83,8 +81,6 @@ export class YMap extends AbstractType {
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*
* @private
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs))
@@ -215,8 +211,6 @@ export class YMap extends AbstractType {
/**
* @param {encoding.Encoder} encoder
*
* @private
*/
_write (encoder) {
encoding.writeVarUint(encoder, YMapRefID)

View File

@@ -14,15 +14,19 @@ import {
callTypeObservers,
transact,
ContentEmbed,
GC,
ContentFormat,
ContentString,
splitSnapshotAffectedStructs,
iterateDeletedStructs,
iterateStructs,
ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
import * as encoding from 'lib0/encoding.js'
import * as object from 'lib0/object.js'
import * as map from 'lib0/map.js'
/**
* @param {any} a
@@ -250,7 +254,7 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
left = insertPos.left
right = insertPos.right
// insert content
const content = text.constructor === String ? new ContentString(text) : new ContentEmbed(text)
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, content)
left.integrate(transaction)
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
@@ -320,6 +324,110 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)
}
/**
* Call this function after string content has been deleted in order to
* clean up formatting Items.
*
* @param {Transaction} transaction
* @param {Item} start
* @param {Item|null} end exclusive end, automatically iterates to the next Content Item
* @param {Map<string,any>} startAttributes
* @param {Map<string,any>} endAttributes This attribute is modified!
* @return {number} The amount of formatting Items deleted.
*
* @function
*/
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => {
while (end && end.content.constructor !== ContentString && end.content.constructor !== ContentEmbed) {
if (!end.deleted && end.content.constructor === ContentFormat) {
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
}
end = end.right
}
let cleanups = 0
while (start !== end) {
if (!start.deleted) {
const content = start.content
switch (content.constructor) {
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (content)
if ((endAttributes.get(key) || null) !== value || (startAttributes.get(key) || null) === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction)
cleanups++
}
break
}
}
}
start = /** @type {Item} */ (start.right)
}
return cleanups
}
/**
* @param {Transaction} transaction
* @param {Item | null} item
*/
const cleanupContextlessFormattingGap = (transaction, item) => {
// iterate until item.right is null or content
while (item && item.right && (item.right.deleted || (item.right.content.constructor !== ContentString && item.right.content.constructor !== ContentEmbed))) {
item = item.right
}
const attrs = new Set()
// iterate back until a content item is found
while (item && (item.deleted || (item.content.constructor !== ContentString && item.content.constructor !== ContentEmbed))) {
if (!item.deleted && item.content.constructor === ContentFormat) {
const key = /** @type {ContentFormat} */ (item.content).key
if (attrs.has(key)) {
item.delete(transaction)
} else {
attrs.add(key)
}
}
item = item.left
}
}
/**
* This function is experimental and subject to change / be removed.
*
* Ideally, we don't need this function at all. Formatting attributes should be cleaned up
* automatically after each change. This function iterates twice over the complete YText type
* and removes unnecessary formatting attributes. This is also helpful for testing.
*
* This function won't be exported anymore as soon as there is confidence that the YText type works as intended.
*
* @param {YText} type
* @return {number} How many formatting attributes have been cleaned up.
*/
export const cleanupYTextFormatting = type => {
let res = 0
transact(/** @type {Doc} */ (type.doc), transaction => {
let start = /** @type {Item} */ (type._start)
let end = type._start
let startAttributes = map.create()
const currentAttributes = map.copy(startAttributes)
while (end) {
if (end.deleted === false) {
switch (end.content.constructor) {
case ContentFormat:
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
break
case ContentEmbed:
case ContentString:
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
startAttributes = map.copy(currentAttributes)
start = end
break
}
}
end = end.right
}
})
return res
}
/**
* @param {Transaction} transaction
* @param {Item|null} left
@@ -332,6 +440,8 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
* @function
*/
const deleteText = (transaction, left, right, currentAttributes, length) => {
const startAttrs = map.copy(currentAttributes)
const start = right
while (length > 0 && right !== null) {
if (right.deleted === false) {
switch (right.content.constructor) {
@@ -351,6 +461,9 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
left = right
right = right.right
}
if (start) {
cleanupFormattingGap(transaction, start, right, startAttrs, map.copy(currentAttributes))
}
return { left, right }
}
@@ -400,7 +513,6 @@ export class YTextEvent extends YEvent {
constructor (ytext, transaction) {
super(ytext, transaction)
/**
* @private
* @type {Array<DeltaItem>|null}
*/
this._delta = null
@@ -612,7 +724,6 @@ export class YText extends AbstractType {
/**
* Array of pending operations on this type
* @type {Array<function():void>?}
* @private
*/
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
}
@@ -629,8 +740,6 @@ export class YText extends AbstractType {
/**
* @param {Doc} y
* @param {Item} item
*
* @private
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -651,11 +760,50 @@ export class YText extends AbstractType {
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*
* @private
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YTextEvent(this, transaction))
const event = new YTextEvent(this, transaction)
const doc = transaction.doc
// If a remote change happened, we try to cleanup potential formatting duplicates.
if (!transaction.local) {
// check if another formatting item was inserted
let foundFormattingItem = false
for (const [client, afterClock] of transaction.afterState) {
const clock = transaction.beforeState.get(client) || 0
if (afterClock === clock) {
continue
}
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
// @ts-ignore
if (!item.deleted && item.content.constructor === ContentFormat) {
foundFormattingItem = true
}
})
if (foundFormattingItem) {
break
}
}
transact(doc, t => {
if (foundFormattingItem) {
// If a formatting item was inserted, we simply clean the whole type.
// We need to compute currentAttributes for the current position anyway.
cleanupYTextFormatting(this)
} else {
// If no formatting attribute was inserted, we can make due with contextless
// formatting cleanups.
// Contextless: it is not necessary to compute currentAttributes for the affected position.
iterateDeletedStructs(t, transaction.deleteSet, item => {
if (item instanceof GC) {
return
}
if (item.parent === this) {
cleanupContextlessFormattingGap(t, item)
}
})
}
})
}
callTypeObservers(this, transaction, event)
}
/**
@@ -800,12 +948,24 @@ export class YText extends AbstractType {
str += /** @type {ContentString} */ (n.content).str
break
}
case ContentEmbed:
case ContentEmbed: {
packStr()
ops.push({
/**
* @type {Object<string,any>}
*/
const op = {
insert: /** @type {ContentEmbed} */ (n.content).embed
})
}
if (currentAttributes.size > 0) {
const attrs = /** @type {Object<string,any>} */ ({})
op.attributes = attrs
for (const [key, value] of currentAttributes) {
attrs[key] = value
}
}
ops.push(op)
break
}
case ContentFormat:
if (isVisible(n, snapshot)) {
packStr()
@@ -910,6 +1070,9 @@ export class YText extends AbstractType {
* @public
*/
format (index, length, attributes) {
if (length === 0) {
return
}
const y = this.doc
if (y !== null) {
transact(y, transaction => {
@@ -926,8 +1089,6 @@ export class YText extends AbstractType {
/**
* @param {encoding.Encoder} encoder
*
* @private
*/
_write (encoder) {
encoding.writeVarUint(encoder, YTextRefID)

View File

@@ -27,7 +27,6 @@ export class YXmlElement extends YXmlFragment {
this.nodeName = nodeName
/**
* @type {Map<string, any>|null}
* @private
*/
this._prelimAttrs = new Map()
}
@@ -41,7 +40,6 @@ export class YXmlElement extends YXmlFragment {
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @private
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -55,7 +53,6 @@ export class YXmlElement extends YXmlFragment {
* Creates an Item with the same effect as this Item (without position effect)
*
* @return {YXmlElement}
* @private
*/
_copy () {
return new YXmlElement(this.nodeName)
@@ -184,7 +181,6 @@ export class YXmlElement extends YXmlFragment {
*
* This is called when this Item is sent to a remote peer.
*
* @private
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_write (encoder) {
@@ -197,7 +193,6 @@ export class YXmlElement extends YXmlFragment {
* @param {decoding.Decoder} decoder
* @return {YXmlElement}
*
* @private
* @function
*/
export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder))

View File

@@ -127,7 +127,6 @@ export class YXmlFragment extends AbstractType {
super()
/**
* @type {Array<any>|null}
* @private
*/
this._prelimContent = []
}
@@ -141,7 +140,6 @@ export class YXmlFragment extends AbstractType {
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @private
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -224,7 +222,6 @@ export class YXmlFragment extends AbstractType {
/**
* Creates YXmlEvent and calls observers.
* @private
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
@@ -328,7 +325,6 @@ export class YXmlFragment extends AbstractType {
*
* This is called when this Item is sent to a remote peer.
*
* @private
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_write (encoder) {

View File

@@ -25,8 +25,6 @@ export class YXmlHook extends YMap {
/**
* Creates an Item with the same effect as this Item (without position effect)
*
* @private
*/
_copy () {
return new YXmlHook(this.hookName)
@@ -69,8 +67,6 @@ export class YXmlHook extends YMap {
* This is called when this Item is sent to a remote peer.
*
* @param {encoding.Encoder} encoder The encoder to write data to.
*
* @private
*/
_write (encoder) {
super._write(encoder)

View File

@@ -79,8 +79,6 @@ export class YXmlText extends YText {
/**
* @param {encoding.Encoder} encoder
*
* @private
*/
_write (encoder) {
encoding.writeVarUint(encoder, YXmlTextRefID)

View File

@@ -42,7 +42,6 @@ export class DeleteSet {
constructor () {
/**
* @type {Map<number,Array<DeleteItem>>}
* @private
*/
this.clients = new Map()
}

View File

@@ -17,6 +17,8 @@ import { Observable } from 'lib0/observable.js'
import * as random from 'lib0/random.js'
import * as map from 'lib0/map.js'
export const generateNewClientId = random.uint32
/**
* A Yjs instance handles the state of shared data.
* @extends Observable<string>
@@ -31,7 +33,7 @@ export class Doc extends Observable {
super()
this.gc = gc
this.gcFilter = gcFilter
this.clientID = random.uint32()
this.clientID = generateNewClientId()
/**
* @type {Map<string, AbstractType<YEvent>>}
*/
@@ -39,12 +41,10 @@ export class Doc extends Observable {
this.store = new StructStore()
/**
* @type {Transaction | null}
* @private
*/
this._transaction = null
/**
* @type {Array<Transaction>}
* @private
*/
this._transactionCleanups = []
}
@@ -105,6 +105,7 @@ export class Doc extends Observable {
t._map = type._map
type._map.forEach(/** @param {Item?} n */ n => {
for (; n !== null; n = n.left) {
// @ts-ignore
n.parent = t
}
})
@@ -170,8 +171,6 @@ export class Doc extends Observable {
/**
* Emit `destroy` event and unregister all event handlers.
*
* @protected
*/
destroy () {
this.emit('destroyed', [true])

View File

@@ -28,13 +28,11 @@ export class Snapshot {
constructor (ds, sv) {
/**
* @type {DeleteSet}
* @private
*/
this.ds = ds
/**
* State Map
* @type {Map<number,number>}
* @private
*/
this.sv = sv
}

View File

@@ -13,7 +13,6 @@ export class StructStore {
constructor () {
/**
* @type {Map<number,Array<GC|Item>>}
* @private
*/
this.clients = new Map()
/**
@@ -23,19 +22,16 @@ export class StructStore {
* slow in Chrome for arrays with more than 100k elements
* @see tryResumePendingStructRefs
* @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
* @private
*/
this.pendingClientsStructRefs = new Map()
/**
* Stack of pending structs waiting for struct dependencies
* Maximum length of stack is structReaders.size
* @type {Array<GCRef|ItemRef>}
* @private
*/
this.pendingStack = []
/**
* @type {Array<decoding.Decoder>}
* @private
*/
this.pendingDeleteReaders = []
}

View File

@@ -10,6 +10,7 @@ import {
findIndexSS,
callEventHandlerListeners,
Item,
generateNewClientId,
StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js'
@@ -17,6 +18,7 @@ import * as encoding from 'lib0/encoding.js'
import * as map from 'lib0/map.js'
import * as math from 'lib0/math.js'
import * as set from 'lib0/set.js'
import * as logging from 'lib0/logging.js'
import { callAll } from 'lib0/function.js'
/**
@@ -85,7 +87,6 @@ export class Transaction {
this.changedParentTypes = new Map()
/**
* @type {Set<ID>}
* @private
*/
this._mergeStructs = new Set()
/**
@@ -314,6 +315,10 @@ const cleanupTransactions = (transactionCleanups, i) => {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
doc.clientID = generateNewClientId()
logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {

View File

@@ -19,13 +19,13 @@ import { Observable } from 'lib0/observable.js'
class StackItem {
/**
* @param {DeleteSet} ds
* @param {number} start clock start of the local client
* @param {number} len
* @param {Map<number,number>} beforeState
* @param {Map<number,number>} afterState
*/
constructor (ds, start, len) {
constructor (ds, beforeState, afterState) {
this.ds = ds
this.start = start
this.len = len
this.beforeState = beforeState
this.afterState = afterState
/**
* Use this to save and restore metadata like selection range
*/
@@ -50,27 +50,58 @@ const popStackItem = (undoManager, stack, eventType) => {
transact(doc, transaction => {
while (stack.length > 0 && result === null) {
const store = doc.store
const clientID = doc.clientID
const stackItem = /** @type {StackItem} */ (stack.pop())
const stackStartClock = stackItem.start
const stackEndClock = stackItem.start + stackItem.len
/**
* @type {Set<Item>}
*/
const itemsToRedo = new Set()
// @todo iterateStructs should not need the structs parameter
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientID))
/**
* @type {Array<Item>}
*/
const itemsToDelete = []
let performedChange = false
if (stackStartClock !== stackEndClock) {
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
getItemCleanStart(transaction, createID(clientID, stackStartClock))
if (stackEndClock < getState(doc.store, clientID)) {
getItemCleanStart(transaction, createID(clientID, stackEndClock))
stackItem.afterState.forEach((endClock, client) => {
const startClock = stackItem.beforeState.get(client) || 0
const len = endClock - startClock
// @todo iterateStructs should not need the structs parameter
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
if (startClock !== endClock) {
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
// this must be executed before deleted structs are iterated.
getItemCleanStart(transaction, createID(client, startClock))
if (endClock < getState(doc.store, client)) {
getItemCleanStart(transaction, createID(client, endClock))
}
iterateStructs(transaction, structs, startClock, len, struct => {
if (struct instanceof Item) {
if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id)
if (diff > 0) {
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
}
if (item.length > len) {
getItemCleanStart(transaction, createID(item.id.client, endClock))
}
struct = item
}
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
}
})
}
}
})
iterateDeletedStructs(transaction, stackItem.ds, struct => {
const id = struct.id
const clock = id.clock
const client = id.client
const startClock = stackItem.beforeState.get(client) || 0
const endClock = stackItem.afterState.get(client) || 0
if (
struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) &&
// Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval.
!(struct.id.client === clientID && struct.id.clock >= stackStartClock && struct.id.clock < stackEndClock)
!(clock >= startClock && clock < endClock)
) {
itemsToRedo.add(struct)
}
@@ -78,27 +109,6 @@ const popStackItem = (undoManager, stack, eventType) => {
itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
})
/**
* @type {Array<Item>}
*/
const itemsToDelete = []
iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => {
if (struct instanceof Item) {
if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id)
if (diff > 0) {
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
}
if (item.length > stackItem.len) {
getItemCleanStart(transaction, createID(item.id.client, stackEndClock))
}
struct = item
}
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
}
})
// We want to delete in reverse order so that children are deleted before
// parents, so we have more information available when items are filtered.
for (let i = itemsToDelete.length - 1; i >= 0; i--) {
@@ -181,17 +191,17 @@ export class UndoManager extends Observable {
// neither undoing nor redoing: delete redoStack
this.redoStack = []
}
const beforeState = transaction.beforeState.get(this.doc.clientID) || 0
const afterState = transaction.afterState.get(this.doc.clientID) || 0
const beforeState = transaction.beforeState
const afterState = transaction.afterState
const now = time.getUnixTime()
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op
const lastOp = stack[stack.length - 1]
lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
lastOp.len = afterState - lastOp.start
lastOp.afterState = afterState
} else {
// create a new stack op
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState - beforeState))
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState))
}
if (!undoing && !redoing) {
this.lastChange = now

View File

@@ -0,0 +1,19 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js'
/**
* Client id should be changed when an instance receives updates from another client using the same client id.
*
* @param {t.TestCase} tc
*/
export const testClientIdDuplicateChange = tc => {
const doc1 = new Y.Doc()
doc1.clientID = 0
const doc2 = new Y.Doc()
doc2.clientID = 0
t.assert(doc2.clientID === doc1.clientID)
doc1.getArray('a').insert(0, [1, 2])
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))
t.assert(doc2.clientID !== doc1.clientID)
}

View File

@@ -5,6 +5,7 @@ import * as text from './y-text.tests.js'
import * as xml from './y-xml.tests.js'
import * as encoding from './encoding.tests.js'
import * as undoredo from './undo-redo.tests.js'
import * as consistency from './consistency.tests.js'
import { runTests } from 'lib0/testing.js'
import { isBrowser, isNode } from 'lib0/environment.js'
@@ -14,7 +15,7 @@ if (isBrowser) {
log.createVConsole(document.body)
}
runTests({
map, array, text, xml, encoding, undoredo
map, array, text, xml, consistency, encoding, undoredo
}).then(success => {
/* istanbul ignore next */
if (isNode) {

View File

@@ -1,5 +1,8 @@
import * as Y from './testHelper.js'
import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js'
import * as math from 'lib0/math.js'
const { init, compare } = Y
/**
@@ -158,3 +161,211 @@ export const testToJson = tc => {
text0.insert(0, 'abc', { bold: true })
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
}
/**
* @param {t.TestCase} tc
*/
export const testToDeltaEmbedAttributes = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 })
const delta0 = text0.toDelta()
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' }, attributes: { width: 100 } }, { insert: 'b', attributes: { bold: true } }])
}
/**
* @param {t.TestCase} tc
*/
export const testToDeltaEmbedNoAttributes = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
text0.insertEmbed(1, { image: 'imageSrc.png' })
const delta0 = text0.toDelta()
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' } }, { insert: 'b', attributes: { bold: true } }], 'toDelta does not set attributes key when no attributes are present')
}
/**
* @param {t.TestCase} tc
*/
export const testFormattingRemoved = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
text0.delete(0, 2)
t.assert(Y.getTypeChildren(text0).length === 1)
}
/**
* @param {t.TestCase} tc
*/
export const testFormattingRemovedInMidText = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, '1234')
text0.insert(2, 'ab', { bold: true })
text0.delete(2, 2)
t.assert(Y.getTypeChildren(text0).length === 3)
}
// RANDOM TESTS
let charCounter = 0
const marks = [
{ bold: true },
{ italic: true },
{ italic: true, color: '#888' }
]
const marksChoices = [
undefined,
...marks
]
/**
* @type Array<function(any,prng.PRNG):void>
*/
const qChanges = [
/**
* @param {Y.Doc} y
* @param {prng.PRNG} gen
*/
(y, gen) => { // insert text
const ytext = y.getText('text')
const insertPos = prng.int32(gen, 0, ytext.toString().length)
const attrs = prng.oneOf(gen, marksChoices)
const text = charCounter++ + prng.word(gen)
ytext.insert(insertPos, text, attrs)
},
/**
* @param {Y.Doc} y
* @param {prng.PRNG} gen
*/
(y, gen) => { // insert embed
const ytext = y.getText('text')
const insertPos = prng.int32(gen, 0, ytext.toString().length)
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
},
/**
* @param {Y.Doc} y
* @param {prng.PRNG} gen
*/
(y, gen) => { // delete text
const ytext = y.getText('text')
const contentLen = ytext.toString().length
const insertPos = prng.int32(gen, 0, contentLen)
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
ytext.delete(insertPos, overwrite)
},
/**
* @param {Y.Doc} y
* @param {prng.PRNG} gen
*/
(y, gen) => { // format text
const ytext = y.getText('text')
const contentLen = ytext.toString().length
const insertPos = prng.int32(gen, 0, contentLen)
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
const format = prng.oneOf(gen, marks)
ytext.format(insertPos, overwrite, format)
},
/**
* @param {Y.Doc} y
* @param {prng.PRNG} gen
*/
(y, gen) => { // insert codeblock
const ytext = y.getText('text')
const insertPos = prng.int32(gen, 0, ytext.toString().length)
const text = charCounter++ + prng.word(gen)
const ops = []
if (insertPos > 0) {
ops.push({ retain: insertPos })
}
ops.push({ insert: text }, { insert: '\n', format: { 'code-block': true } })
ytext.applyDelta(ops)
}
]
/**
* @param {any} result
*/
const checkResult = result => {
for (let i = 1; i < result.testObjects.length; i++) {
const p1 = result.users[i].getText('text').toDelta()
const p2 = result.users[i].getText('text').toDelta()
t.compare(p1, p2)
}
// Uncomment this to find formatting-cleanup issues
// const cleanups = Y.cleanupYTextFormatting(result.users[0].getText('text'))
// t.assert(cleanups === 0)
return result
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges1 = tc => {
const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 1))
const cleanups = Y.cleanupYTextFormatting(users[0].getText('text'))
t.assert(cleanups === 0)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges2 = tc => {
const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 2))
const cleanups = Y.cleanupYTextFormatting(users[0].getText('text'))
t.assert(cleanups === 0)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges2Repeat = tc => {
for (let i = 0; i < 1000; i++) {
const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 2))
const cleanups = Y.cleanupYTextFormatting(users[0].getText('text'))
t.assert(cleanups === 0)
}
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges3 = tc => {
checkResult(Y.applyRandomTests(tc, qChanges, 3))
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges30 = tc => {
checkResult(Y.applyRandomTests(tc, qChanges, 30))
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges40 = tc => {
checkResult(Y.applyRandomTests(tc, qChanges, 40))
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges70 = tc => {
checkResult(Y.applyRandomTests(tc, qChanges, 70))
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges100 = tc => {
checkResult(Y.applyRandomTests(tc, qChanges, 100))
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGenerateQuillChanges300 = tc => {
checkResult(Y.applyRandomTests(tc, qChanges, 300))
}

View File

@@ -39,10 +39,7 @@
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": {
"yjs": ["./src/index.js"],
"lib0/*": ["node_modules/lib0/*"],
"lib0/set.js": ["node_modules/lib0/set.js"],
"lib0/function.js": ["node_modules/lib0/function.js"]
"yjs": ["./src/index.js"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
@@ -60,9 +57,8 @@
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"maxNodeModuleJsDepth": 5,
// "maxNodeModuleJsDepth": 0,
// "types": ["./src/utils/typedefs.js"]
},
"include": ["./src/**/*.js", "./tests/**/*.js"],
"exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"]
"include": ["./src/**/*.js", "./tests/**/*.js"]
}