Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f2d13e3ce | ||
|
|
e0b76cd2f4 | ||
|
|
d812636c5b | ||
|
|
21fee0fe96 | ||
|
|
fab14a09de | ||
|
|
710b4ba145 | ||
|
|
34091ae614 | ||
|
|
feb8ec1afc | ||
|
|
ce9139c9f4 | ||
|
|
e2e5d0870c | ||
|
|
04cff60931 | ||
|
|
5dfe4e8af2 | ||
|
|
05ca0b0208 | ||
|
|
ee7c189fdc | ||
|
|
01c08ef202 | ||
|
|
894c0d7731 | ||
|
|
fdf632f03e | ||
|
|
ce80cb4a0d | ||
|
|
ae3c4cc050 | ||
|
|
27a78047c5 | ||
|
|
7a128c271b | ||
|
|
263cc0856e | ||
|
|
2199ac3e4e | ||
|
|
275d52b19d | ||
|
|
7edbb2485f | ||
|
|
304812fb07 | ||
|
|
baca852733 | ||
|
|
7cbf204143 | ||
|
|
c8a59118b5 | ||
|
|
bee397f1e5 | ||
|
|
1e97cf8323 | ||
|
|
c28ad0608e | ||
|
|
e19f16f22c | ||
|
|
6f074a873d | ||
|
|
4af04d6a29 | ||
|
|
97d9714710 | ||
|
|
ca667be68b | ||
|
|
8086a4f816 | ||
|
|
186f7140b6 | ||
|
|
edc1f9418f | ||
|
|
32b734b24d | ||
|
|
656328631c | ||
|
|
dbd1b3cb59 | ||
|
|
8fadec4dcd | ||
|
|
8013b4ef5c | ||
|
|
0a40b541e8 | ||
|
|
4c929c6808 | ||
|
|
0fc213e92e | ||
|
|
af576788f1 | ||
|
|
fbbf085278 | ||
|
|
d8868c47e1 | ||
|
|
47221c26c4 | ||
|
|
ba83398374 | ||
|
|
0b23d5aeeb | ||
|
|
072947c0bb | ||
|
|
22aef63d8a | ||
|
|
f8341220c3 | ||
|
|
004a781a56 | ||
|
|
c8534ea6bc | ||
|
|
1e0fd60df4 | ||
|
|
320da29b69 | ||
|
|
783c4d8209 | ||
|
|
2c708b647d | ||
|
|
7a45be8c88 |
66
README.md
66
README.md
@@ -97,6 +97,7 @@ are implemented in separate modules.
|
|||||||
| [Quill](https://quilljs.com/) | ✔ | [y-quill](https://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) |
|
| [Quill](https://quilljs.com/) | ✔ | [y-quill](https://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) |
|
||||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
||||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
||||||
|
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
||||||
|
|
||||||
### Providers
|
### Providers
|
||||||
|
|
||||||
@@ -319,6 +320,8 @@ or any of its children.
|
|||||||
<dl>
|
<dl>
|
||||||
<b><code>parent:Y.AbstractType|null</code></b>
|
<b><code>parent:Y.AbstractType|null</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>size: number</code></b>
|
||||||
|
<dd>Total number of key/value pairs.</dd>
|
||||||
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
|
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
|
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
|
||||||
@@ -409,7 +412,7 @@ YTextEvents compute changes as deltas.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
||||||
<dd>Assign formatting attributes to a range in the text</dd>
|
<dd>Assign formatting attributes to a range in the text</dd>
|
||||||
<b><code>applyDelta(delta, opts:Object<string,any>)</code></b>
|
<b><code>applyDelta(delta: Delta, opts:Object<string,any>)</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
||||||
Can set options for preventing remove ending newLines, default is true.
|
Can set options for preventing remove ending newLines, default is true.
|
||||||
@@ -482,6 +485,8 @@ or any of its children.
|
|||||||
<dd>Get the XML serialization of all descendants.</dd>
|
<dd>Get the XML serialization of all descendants.</dd>
|
||||||
<b><code>toJSON():string</code></b>
|
<b><code>toJSON():string</code></b>
|
||||||
<dd>See <code>toString</code>.</dd>
|
<dd>See <code>toString</code>.</dd>
|
||||||
|
<b><code>createTreeWalker(filter: function(AbstractType<any>):boolean):Iterable</code></b>
|
||||||
|
<dd>Create an Iterable that walks through the children.</dd>
|
||||||
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
|
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
Adds an event listener to this type that will be called synchronously every time
|
Adds an event listener to this type that will be called synchronously every time
|
||||||
@@ -539,7 +544,7 @@ content and be actually XML compliant.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>getAttribute(attributeName:string):string</code></b>
|
<b><code>getAttribute(attributeName:string):string</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>getAttributes(attributeName:string):Object<string,string></code></b>
|
<b><code>getAttributes():Object<string,string></code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>get(i:number):Y.XmlElement|Y.XmlText</code></b>
|
<b><code>get(i:number):Y.XmlElement|Y.XmlText</code></b>
|
||||||
<dd>Retrieve the i-th element.</dd>
|
<dd>Retrieve the i-th element.</dd>
|
||||||
@@ -608,7 +613,10 @@ parameter that is stored on <code>transaction.origin</code> and
|
|||||||
</dd>
|
</dd>
|
||||||
<b><code>toJSON():any</code></b>
|
<b><code>toJSON():any</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
Converts the entire document into a js object, recursively traversing each yjs type.
|
Deprecated: It is recommended to call toJSON directly on the shared types.
|
||||||
|
Converts the entire document into a js object, recursively traversing each yjs
|
||||||
|
type. Doesn't log types that have not been defined (using
|
||||||
|
<code>ydoc.getType(..)</code>).
|
||||||
</dd>
|
</dd>
|
||||||
<b><code>get(string, Y.[TypeClass]):[Type]</code></b>
|
<b><code>get(string, Y.[TypeClass]):[Type]</code></b>
|
||||||
<dd>Define a shared type.</dd>
|
<dd>Define a shared type.</dd>
|
||||||
@@ -701,6 +709,30 @@ Y.applyUpdate(ydoc1, diff2)
|
|||||||
Y.applyUpdate(ydoc2, diff1)
|
Y.applyUpdate(ydoc2, diff1)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Example: Syncing clients without loading the Y.Doc
|
||||||
|
|
||||||
|
It is possible to sync clients and compute delta updates without loading the Yjs
|
||||||
|
document to memory. Yjs exposes an API to compute the differences directly on the
|
||||||
|
binary document updates.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// encode the current state as a binary buffer
|
||||||
|
let currentState1 = Y.encodeStateAsUpdate(ydoc1)
|
||||||
|
let currentState2 = Y.encodeStateAsUpdate(ydoc2)
|
||||||
|
// now we can continue syncing clients using state vectors without using the Y.Doc
|
||||||
|
ydoc1.destroy()
|
||||||
|
ydoc2.destroy()
|
||||||
|
|
||||||
|
const stateVector1 = Y.encodeStateVectorFromUpdate(currentState1)
|
||||||
|
const stateVector2 = Y.encodeStateVectorFromUpdate(currentState2)
|
||||||
|
const diff1 = Y.diffUpdate(currentState1, stateVector2)
|
||||||
|
const diff2 = Y.diffUpdate(currentState2, stateVector1)
|
||||||
|
|
||||||
|
// sync clients
|
||||||
|
currentState1 = Y.mergeUpdates([currentState1, diff2])
|
||||||
|
currentState1 = Y.mergeUpdates([currentState1, diff1])
|
||||||
|
```
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
<b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b>
|
<b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
@@ -717,22 +749,26 @@ differences to the update message.
|
|||||||
</dd>
|
</dd>
|
||||||
<b><code>Y.encodeStateVector(Y.Doc):Uint8Array</code></b>
|
<b><code>Y.encodeStateVector(Y.Doc):Uint8Array</code></b>
|
||||||
<dd>Computes the state vector and encodes it into an Uint8Array.</dd>
|
<dd>Computes the state vector and encodes it into an Uint8Array.</dd>
|
||||||
|
<b><code>Y.mergeUpdates(Array<Uint8Array>)</code></b>
|
||||||
|
<dd>
|
||||||
|
Merge several document updates into a single document update while removing
|
||||||
|
duplicate information. The merged document update is always smaller than
|
||||||
|
the separate updates because of the compressed encoding.
|
||||||
|
</dd>
|
||||||
|
<b><code>Y.encodeStateVectorFromUpdate(Uint8Array): Uint8Array</code></b>
|
||||||
|
<dd>
|
||||||
|
Computes the state vector from a document update and encodes it into an Uint8Array.
|
||||||
|
</dd>
|
||||||
|
<b><code>Y.diffUpdate(update: Uint8Array, stateVector: Uint8Array): Uint8Array</code></b>
|
||||||
|
<dd>
|
||||||
|
Encode the missing differences to another update message. This function works
|
||||||
|
similarly to <code>Y.encodeStateAsUpdate(ydoc, stateVector)</code> but works
|
||||||
|
on updates instead.
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
### Relative Positions
|
### Relative Positions
|
||||||
|
|
||||||
> This API is not stable yet
|
|
||||||
|
|
||||||
This feature is intended for managing selections / cursors. When working with
|
|
||||||
other users that manipulate the shared document, you can't trust that an index
|
|
||||||
position (an integer) will stay at the intended location. A *relative position*
|
|
||||||
is fixated to an element in the shared document and is not affected by remote
|
|
||||||
changes. I.e. given the document `"a|c"`, the relative position is attached to
|
|
||||||
`c`. When a remote user modifies the document by inserting a character before
|
|
||||||
the cursor, the cursor will stay attached to the character `c`. `insert(1,
|
|
||||||
'x')("a|c") = "ax|c"`. When the *relative position* is set to the end of the
|
|
||||||
document, it will stay attached to the end of the document.
|
|
||||||
|
|
||||||
#### Example: Transform to RelativePosition and back
|
#### Example: Transform to RelativePosition and back
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
10
funding.cjs
10
funding.cjs
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
const log = require('lib0/dist/logging.cjs')
|
|
||||||
|
|
||||||
log.print()
|
|
||||||
log.print(log.BOLD, log.GREEN, log.BOLD, 'Thank you for using Yjs ', log.RED, '❤\n')
|
|
||||||
log.print(
|
|
||||||
log.GREY,
|
|
||||||
'The project has grown considerably in the past year. Too much for me to maintain\nin my spare time. Several companies built their products with Yjs.\nYet, this project receives very little funding. Yjs is far from done. I want to\ncreate more awesome extensions and work on the growing number of open issues.\n', log.BOLD, 'Dear user, the future of this project entirely depends on you.\n')
|
|
||||||
log.print(log.BLUE, log.BOLD, 'Please start funding the project now: https://github.com/sponsors/dmonad \n')
|
|
||||||
log.print(log.GREY, '(This message will be removed when I achieved my funding goal)\n\n')
|
|
||||||
198
package-lock.json
generated
198
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.4.9",
|
"version": "13.5.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -31,59 +31,95 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/parser": {
|
"@babel/parser": {
|
||||||
"version": "7.11.5",
|
"version": "7.12.15",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz",
|
||||||
"integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==",
|
"integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@rollup/plugin-commonjs": {
|
"@rollup/plugin-commonjs": {
|
||||||
"version": "11.1.0",
|
"version": "17.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz",
|
||||||
"integrity": "sha512-Ycr12N3ZPN96Fw2STurD21jMqzKwL9QuFhms3SD7KKRK7oaXUsBU9Zt0jL/rOPHiPYisI21/rXGO3jr9BnLHUA==",
|
"integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@rollup/pluginutils": "^3.0.8",
|
"@rollup/pluginutils": "^3.1.0",
|
||||||
"commondir": "^1.0.1",
|
"commondir": "^1.0.1",
|
||||||
"estree-walker": "^1.0.1",
|
"estree-walker": "^2.0.1",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.6",
|
||||||
"is-reference": "^1.1.2",
|
"is-reference": "^1.2.1",
|
||||||
"magic-string": "^0.25.2",
|
"magic-string": "^0.25.7",
|
||||||
"resolve": "^1.11.0"
|
"resolve": "^1.17.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": {
|
||||||
|
"version": "7.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
|
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resolve": {
|
||||||
|
"version": "1.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
|
||||||
|
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-core-module": "^2.1.0",
|
||||||
|
"path-parse": "^1.0.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@rollup/plugin-node-resolve": {
|
"@rollup/plugin-node-resolve": {
|
||||||
"version": "7.1.3",
|
"version": "11.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.0.tgz",
|
||||||
"integrity": "sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q==",
|
"integrity": "sha512-qHjNIKYt5pCcn+5RUBQxK8krhRvf1HnyVgUCcFFcweDS7fhkOLZeYh0mhHK6Ery8/bb9tvN/ubPzmfF0qjDCTA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@rollup/pluginutils": "^3.0.8",
|
"@rollup/pluginutils": "^3.1.0",
|
||||||
"@types/resolve": "0.0.8",
|
"@types/resolve": "1.17.1",
|
||||||
"builtin-modules": "^3.1.0",
|
"builtin-modules": "^3.1.0",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
"is-module": "^1.0.0",
|
"is-module": "^1.0.0",
|
||||||
"resolve": "^1.14.2"
|
"resolve": "^1.19.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.17.0",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
|
||||||
"integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
|
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"is-core-module": "^2.2.0",
|
||||||
"path-parse": "^1.0.6"
|
"path-parse": "^1.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@rollup/pluginutils": {
|
"@rollup/pluginutils": {
|
||||||
"version": "3.0.10",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
|
||||||
"integrity": "sha512-d44M7t+PjmMrASHbhgpSbVgtL6EFyX7J4mYxwQ/c5eoaE6N2VgCgEcWVzNnwycIloti+/MpwFr8qfw+nRw00sw==",
|
"integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/estree": "0.0.39",
|
"@types/estree": "0.0.39",
|
||||||
"estree-walker": "^1.0.1",
|
"estree-walker": "^1.0.1",
|
||||||
"picomatch": "^2.2.2"
|
"picomatch": "^2.2.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"estree-walker": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/color-name": {
|
"@types/color-name": {
|
||||||
@@ -99,15 +135,15 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "14.0.9",
|
"version": "14.14.31",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
|
||||||
"integrity": "sha512-0sCTiXKXELOBxvZLN4krQ0FPOAA7ij+6WwvD0k/PHd9/KAkr4dXel5J9fh6F4x1FwAQILqAWkmpeuS6mjf1iKA==",
|
"integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/resolve": {
|
"@types/resolve": {
|
||||||
"version": "0.0.8",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||||
"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
|
"integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -239,9 +275,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"builtin-modules": {
|
"builtin-modules": {
|
||||||
"version": "3.1.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
|
||||||
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
|
"integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"callsites": {
|
"callsites": {
|
||||||
@@ -458,6 +494,12 @@
|
|||||||
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"deepmerge": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"define-properties": {
|
"define-properties": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||||
@@ -946,9 +988,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"estree-walker": {
|
"estree-walker": {
|
||||||
"version": "1.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"esutils": {
|
"esutils": {
|
||||||
@@ -1057,6 +1099,13 @@
|
|||||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"function-bind": {
|
"function-bind": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
@@ -1342,6 +1391,15 @@
|
|||||||
"integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==",
|
"integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-core-module": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"is-date-object": {
|
"is-date-object": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
|
||||||
@@ -1376,20 +1434,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"is-reference": {
|
"is-reference": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||||
"integrity": "sha512-ZVxq+5TkOx6GQdnoMm2aRdCKADdcrOWXLGzGT+vIA8DMpqEJaRk5AL1bS80zJ2bjHunVmjdzfCt0e4BymIEqKQ==",
|
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/estree": "0.0.44"
|
"@types/estree": "*"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": {
|
|
||||||
"version": "0.0.44",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.44.tgz",
|
|
||||||
"integrity": "sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is-regex": {
|
"is-regex": {
|
||||||
@@ -1429,9 +1479,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"isomorphic.js": {
|
"isomorphic.js": {
|
||||||
"version": "0.1.4",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.0.tgz",
|
||||||
"integrity": "sha512-t9zbgkjE7f9f2M6OSW49YEq0lUrSdAllBbWFUZoeck/rnnFae6UlhmDtXWs48VJY3ZpryCoZsRiAiKD44hPIGQ=="
|
"integrity": "sha512-U7JlVUbmVYvV1ddGLhc7pvoMNp+1uGwE3sCIGHEh9I4ldSJJ9LtLog5/H8GzZkwLjmbeEvIbQb50nsre57nDuw=="
|
||||||
},
|
},
|
||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -1459,9 +1509,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jsdoc": {
|
"jsdoc": {
|
||||||
"version": "3.6.5",
|
"version": "3.6.6",
|
||||||
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.6.tgz",
|
||||||
"integrity": "sha512-SbY+i9ONuxSK35cgVHaI8O9senTE4CDYAmGSDJ5l3+sfe62Ff4gy96osy6OW84t4K4A8iGnMrlRrsSItSNp3RQ==",
|
"integrity": "sha512-znR99e1BHeyEkSvgDDpX0sTiTu+8aQyDl9DawrkOGZTTW8hv0deIFXx87114zJ7gRaDZKVQD/4tr1ifmJp9xhQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/parser": "^7.9.4",
|
"@babel/parser": "^7.9.4",
|
||||||
@@ -1548,11 +1598,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib0": {
|
"lib0": {
|
||||||
"version": "0.2.33",
|
"version": "0.2.38",
|
||||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.33.tgz",
|
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.38.tgz",
|
||||||
"integrity": "sha512-Pnm8FzjUr+aTYkEu2A20c1EfVHla8GbVX+GXn6poxx0gcmEuCs+XszjLmtEbI9xYOoI/83xVi7VOIoyHgOO87w==",
|
"integrity": "sha512-ZxnX62R5weebi8bH/Ipc6JBiQIsiQ1D7p3r96zulSSu1byW6DDWSBeI8WC/W5UGtkZ80GktX3JNY2pqhNiXWGA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"isomorphic.js": "^0.1.3"
|
"isomorphic.js": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"linkify-it": {
|
"linkify-it": {
|
||||||
@@ -2380,22 +2430,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"version": "1.32.1",
|
"version": "2.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.39.0.tgz",
|
||||||
"integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==",
|
"integrity": "sha512-+WR3bttcq7zE+BntH09UxaW3bQo3vItuYeLsyk4dL2tuwbeSKJuvwiawyhEnvRdRgrII0Uzk00FpctHO/zB1kw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/estree": "*",
|
"fsevents": "~2.3.1"
|
||||||
"@types/node": "*",
|
|
||||||
"acorn": "^7.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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
|
|
||||||
},
|
|
||||||
"run-async": {
|
"run-async": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||||
@@ -2786,9 +2828,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "3.9.7",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz",
|
||||||
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
|
"integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"uc.micro": {
|
"uc.micro": {
|
||||||
@@ -2898,12 +2940,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"y-protocols": {
|
"y-protocols": {
|
||||||
"version": "0.2.3",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.4.tgz",
|
||||||
"integrity": "sha512-mJ838iW7XgMQqlv+9DtH7QyLqflZoy/VvaUWRIpwawee4mQiFJcEXazCmSYUHEbXIUuVNNc70FnuNSMWDC5vKQ==",
|
"integrity": "sha512-5/Hd6DJ5Y2SlbqLIKq86BictdOS0iAcWJZCVop8MKqx0XWwA+BbMn4538n4Z0CGjFMUGnG1kGzagk3BKGz5SvQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lib0": "^0.2.20"
|
"lib0": "^0.2.35"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
package.json
46
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.4.9",
|
"version": "13.5.2",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
@@ -12,28 +12,35 @@
|
|||||||
"url": "https://github.com/sponsors/dmonad"
|
"url": "https://github.com/sponsors/dmonad"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run dist && node ./dist/tests.cjs --repitition-time 50",
|
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
|
||||||
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000",
|
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
|
||||||
"dist": "rm -rf dist && rollup -c && tsc",
|
"dist": "rm -rf dist && rollup -c && tsc",
|
||||||
"watch": "rollup -wc",
|
"watch": "rollup -wc",
|
||||||
"lint": "markdownlint README.md && standard && tsc",
|
"lint": "markdownlint README.md && standard && tsc",
|
||||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
||||||
"serve-docs": "npm run docs && http-server ./docs/",
|
"serve-docs": "npm run docs && http-server ./docs/",
|
||||||
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repitition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
|
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
|
||||||
"debug": "concurrently 'http-server -o test.html' 'npm run watch'",
|
"debug": "concurrently 'http-server -o test.html' 'npm run watch'",
|
||||||
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
|
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
|
||||||
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
|
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
|
||||||
"postinstall": "node ./funding.cjs"
|
"postinstall": "node ./sponsor-y.js"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/yjs.mjs",
|
||||||
|
"require": "./dist/yjs.cjs"
|
||||||
|
},
|
||||||
|
"./src/index.js": "./src/index.js",
|
||||||
|
"./tests/testHelper.js": "./tests/testHelper.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/*",
|
"dist/yjs.*",
|
||||||
"src/*",
|
"dist/src",
|
||||||
"tests/*",
|
"src",
|
||||||
"docs/*",
|
"tests/testHelper.js",
|
||||||
"funding.cjs"
|
"sponsor-y.js"
|
||||||
],
|
],
|
||||||
"dictionaries": {
|
"dictionaries": {
|
||||||
"doc": "docs",
|
|
||||||
"test": "tests"
|
"test": "tests"
|
||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
@@ -61,22 +68,21 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/yjs/yjs/issues"
|
"url": "https://github.com/yjs/yjs/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://yjs.dev",
|
"homepage": "https://docs.yjs.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.33"
|
"lib0": "^0.2.38"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
"@rollup/plugin-node-resolve": "^11.2.0",
|
||||||
"concurrently": "^3.6.1",
|
"concurrently": "^3.6.1",
|
||||||
"http-server": "^0.12.3",
|
"http-server": "^0.12.3",
|
||||||
"jsdoc": "^3.6.5",
|
"jsdoc": "^3.6.6",
|
||||||
"markdownlint-cli": "^0.23.2",
|
"markdownlint-cli": "^0.23.2",
|
||||||
"rollup": "^1.32.1",
|
"rollup": "^2.39.0",
|
||||||
"rollup-cli": "^1.0.9",
|
|
||||||
"standard": "^14.3.4",
|
"standard": "^14.3.4",
|
||||||
"tui-jsdoc-template": "^1.2.2",
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^4.1.5",
|
||||||
"y-protocols": "^0.2.3"
|
"y-protocols": "^1.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
sponsor-y.js
Normal file
12
sponsor-y.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
try {
|
||||||
|
const log = require('lib0/dist/logging.cjs')
|
||||||
|
|
||||||
|
log.print()
|
||||||
|
log.print(log.BOLD, log.GREEN, log.BOLD, 'Thank you for using Yjs ', log.RED, '❤\n')
|
||||||
|
log.print(
|
||||||
|
log.GREY,
|
||||||
|
'The project has grown considerably in the past year. Too much for me to maintain\nin my spare time. Several companies built their products with Yjs.\nYet, this project receives very little funding. Yjs is far from done. I want to\ncreate more awesome extensions and work on the growing number of open issues.\n', log.BOLD, 'Dear user, the future of this project entirely depends on you.\n')
|
||||||
|
log.print(log.BLUE, log.BOLD, 'Please start funding the project now: https://github.com/sponsors/dmonad \n')
|
||||||
|
log.print(log.GREY, '(This message will be removed when I achieved my funding goal)\n\n')
|
||||||
|
} catch (e) { }
|
||||||
19
src/index.js
19
src/index.js
@@ -32,8 +32,6 @@ export {
|
|||||||
createRelativePositionFromJSON,
|
createRelativePositionFromJSON,
|
||||||
createAbsolutePositionFromRelativePosition,
|
createAbsolutePositionFromRelativePosition,
|
||||||
compareRelativePositions,
|
compareRelativePositions,
|
||||||
writeRelativePosition,
|
|
||||||
readRelativePosition,
|
|
||||||
ID,
|
ID,
|
||||||
createID,
|
createID,
|
||||||
compareIDs,
|
compareIDs,
|
||||||
@@ -57,14 +55,15 @@ export {
|
|||||||
encodeStateAsUpdate,
|
encodeStateAsUpdate,
|
||||||
encodeStateAsUpdateV2,
|
encodeStateAsUpdateV2,
|
||||||
encodeStateVector,
|
encodeStateVector,
|
||||||
encodeStateVectorV2,
|
|
||||||
UndoManager,
|
UndoManager,
|
||||||
decodeSnapshot,
|
decodeSnapshot,
|
||||||
encodeSnapshot,
|
encodeSnapshot,
|
||||||
decodeSnapshotV2,
|
decodeSnapshotV2,
|
||||||
encodeSnapshotV2,
|
encodeSnapshotV2,
|
||||||
decodeStateVector,
|
decodeStateVector,
|
||||||
decodeStateVectorV2,
|
logUpdate,
|
||||||
|
logUpdateV2,
|
||||||
|
relativePositionToJSON,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
isParentOf,
|
isParentOf,
|
||||||
equalSnapshots,
|
equalSnapshots,
|
||||||
@@ -72,5 +71,15 @@ export {
|
|||||||
tryGc,
|
tryGc,
|
||||||
transact,
|
transact,
|
||||||
AbstractConnector,
|
AbstractConnector,
|
||||||
logType
|
logType,
|
||||||
|
mergeUpdates,
|
||||||
|
mergeUpdatesV2,
|
||||||
|
parseUpdateMeta,
|
||||||
|
parseUpdateMetaV2,
|
||||||
|
encodeStateVectorFromUpdate,
|
||||||
|
encodeStateVectorFromUpdateV2,
|
||||||
|
encodeRelativePosition,
|
||||||
|
decodeRelativePosition,
|
||||||
|
diffUpdate,
|
||||||
|
diffUpdateV2
|
||||||
} from './internals.js'
|
} from './internals.js'
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export * from './utils/Snapshot.js'
|
|||||||
export * from './utils/StructStore.js'
|
export * from './utils/StructStore.js'
|
||||||
export * from './utils/Transaction.js'
|
export * from './utils/Transaction.js'
|
||||||
export * from './utils/UndoManager.js'
|
export * from './utils/UndoManager.js'
|
||||||
|
export * from './utils/updates.js'
|
||||||
export * from './utils/YEvent.js'
|
export * from './utils/YEvent.js'
|
||||||
|
|
||||||
export * from './types/AbstractType.js'
|
export * from './types/AbstractType.js'
|
||||||
@@ -39,3 +40,4 @@ export * from './structs/ContentAny.js'
|
|||||||
export * from './structs/ContentString.js'
|
export * from './structs/ContentString.js'
|
||||||
export * from './structs/ContentType.js'
|
export * from './structs/ContentType.js'
|
||||||
export * from './structs/Item.js'
|
export * from './structs/Item.js'
|
||||||
|
export * from './structs/Skip.js'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractUpdateEncoder, ID, Transaction // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
@@ -34,7 +34,7 @@ export class AbstractStruct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder The encoder to write data to.
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
* @param {number} encodingRef
|
* @param {number} encodingRef
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Transaction, Item, StructStore // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
export class ContentAny {
|
export class ContentAny {
|
||||||
@@ -74,7 +74,7 @@ export class ContentAny {
|
|||||||
*/
|
*/
|
||||||
gc (store) {}
|
gc (store) {}
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -95,7 +95,7 @@ export class ContentAny {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {ContentAny}
|
* @return {ContentAny}
|
||||||
*/
|
*/
|
||||||
export const readContentAny = decoder => {
|
export const readContentAny = decoder => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Item, Transaction // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
@@ -70,7 +70,7 @@ export class ContentBinary {
|
|||||||
*/
|
*/
|
||||||
gc (store) {}
|
gc (store) {}
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -86,7 +86,7 @@ export class ContentBinary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||||
* @return {ContentBinary}
|
* @return {ContentBinary}
|
||||||
*/
|
*/
|
||||||
export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())
|
export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
addToDeleteSet,
|
addToDeleteSet,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Item, Transaction // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
export class ContentDeleted {
|
export class ContentDeleted {
|
||||||
@@ -77,7 +77,7 @@ export class ContentDeleted {
|
|||||||
*/
|
*/
|
||||||
gc (store) {}
|
gc (store) {}
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -95,7 +95,7 @@ export class ContentDeleted {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||||
* @return {ContentDeleted}
|
* @return {ContentDeleted}
|
||||||
*/
|
*/
|
||||||
export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())
|
export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Doc, AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Transaction, Item // eslint-disable-line
|
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
@@ -110,7 +110,7 @@ export class ContentDoc {
|
|||||||
gc (store) { }
|
gc (store) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -129,7 +129,7 @@ export class ContentDoc {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {ContentDoc}
|
* @return {ContentDoc}
|
||||||
*/
|
*/
|
||||||
export const readContentDoc = decoder => new ContentDoc(new Doc({ guid: decoder.readString(), ...decoder.readAny() }))
|
export const readContentDoc = decoder => new ContentDoc(new Doc({ guid: decoder.readString(), ...decoder.readAny() }))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Item, Transaction // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
@@ -74,7 +74,7 @@ export class ContentEmbed {
|
|||||||
*/
|
*/
|
||||||
gc (store) {}
|
gc (store) {}
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -92,7 +92,7 @@ export class ContentEmbed {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {ContentEmbed}
|
* @return {ContentEmbed}
|
||||||
*/
|
*/
|
||||||
export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON())
|
export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractType, AbstractUpdateDecoder, AbstractUpdateEncoder, Item, StructStore, Transaction // eslint-disable-line
|
AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
@@ -80,7 +80,7 @@ export class ContentFormat {
|
|||||||
*/
|
*/
|
||||||
gc (store) {}
|
gc (store) {}
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -97,7 +97,7 @@ export class ContentFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {ContentFormat}
|
* @return {ContentFormat}
|
||||||
*/
|
*/
|
||||||
export const readContentFormat = decoder => new ContentFormat(decoder.readString(), decoder.readJSON())
|
export const readContentFormat = decoder => new ContentFormat(decoder.readString(), decoder.readJSON())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Transaction, Item, StructStore // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +77,7 @@ export class ContentJSON {
|
|||||||
*/
|
*/
|
||||||
gc (store) {}
|
gc (store) {}
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -100,7 +100,7 @@ export class ContentJSON {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {ContentJSON}
|
* @return {ContentJSON}
|
||||||
*/
|
*/
|
||||||
export const readContentJSON = decoder => {
|
export const readContentJSON = decoder => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Transaction, Item, StructStore // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +88,7 @@ export class ContentString {
|
|||||||
*/
|
*/
|
||||||
gc (store) {}
|
gc (store) {}
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -106,7 +106,7 @@ export class ContentString {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {ContentString}
|
* @return {ContentString}
|
||||||
*/
|
*/
|
||||||
export const readContentString = decoder => new ContentString(decoder.readString())
|
export const readContentString = decoder => new ContentString(decoder.readString())
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import {
|
|||||||
readYXmlFragment,
|
readYXmlFragment,
|
||||||
readYXmlHook,
|
readYXmlHook,
|
||||||
readYXmlText,
|
readYXmlText,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Array<function(AbstractUpdateDecoder):AbstractType<any>>}
|
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractType<any>>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export const typeRefs = [
|
export const typeRefs = [
|
||||||
@@ -148,7 +148,7 @@ export class ContentType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -166,7 +166,7 @@ export class ContentType {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {ContentType}
|
* @return {ContentType}
|
||||||
*/
|
*/
|
||||||
export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder))
|
export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
addStruct,
|
addStruct,
|
||||||
AbstractUpdateEncoder, StructStore, Transaction, ID // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
export const structGCRefNumber = 0
|
export const structGCRefNumber = 0
|
||||||
@@ -22,6 +22,9 @@ export class GC extends AbstractStruct {
|
|||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
mergeWith (right) {
|
mergeWith (right) {
|
||||||
|
if (this.constructor !== right.constructor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
this.length += right.length
|
this.length += right.length
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,7 @@ export class GC extends AbstractStruct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
readContentFormat,
|
readContentFormat,
|
||||||
readContentType,
|
readContentType,
|
||||||
addChangedTypeToTransaction,
|
addChangedTypeToTransaction,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
@@ -554,6 +554,7 @@ export class Item extends AbstractStruct {
|
|||||||
*/
|
*/
|
||||||
mergeWith (right) {
|
mergeWith (right) {
|
||||||
if (
|
if (
|
||||||
|
this.constructor === right.constructor &&
|
||||||
compareIDs(right.origin, this.lastId) &&
|
compareIDs(right.origin, this.lastId) &&
|
||||||
this.right === right &&
|
this.right === right &&
|
||||||
compareIDs(this.rightOrigin, right.rightOrigin) &&
|
compareIDs(this.rightOrigin, right.rightOrigin) &&
|
||||||
@@ -619,7 +620,7 @@ export class Item extends AbstractStruct {
|
|||||||
*
|
*
|
||||||
* This is called when this Item is sent to a remote peer.
|
* This is called when this Item is sent to a remote peer.
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateEncoder} encoder The encoder to write data to.
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
@@ -639,16 +640,26 @@ export class Item extends AbstractStruct {
|
|||||||
}
|
}
|
||||||
if (origin === null && rightOrigin === null) {
|
if (origin === null && rightOrigin === null) {
|
||||||
const parent = /** @type {AbstractType<any>} */ (this.parent)
|
const parent = /** @type {AbstractType<any>} */ (this.parent)
|
||||||
const parentItem = parent._item
|
if (parent._item !== undefined) {
|
||||||
if (parentItem === null) {
|
const parentItem = parent._item
|
||||||
// parent type on y._map
|
if (parentItem === null) {
|
||||||
// find the correct key
|
// parent type on y._map
|
||||||
const ykey = findRootTypeKey(parent)
|
// find the correct key
|
||||||
|
const ykey = findRootTypeKey(parent)
|
||||||
|
encoder.writeParentInfo(true) // write parentYKey
|
||||||
|
encoder.writeString(ykey)
|
||||||
|
} else {
|
||||||
|
encoder.writeParentInfo(false) // write parent id
|
||||||
|
encoder.writeLeftID(parentItem.id)
|
||||||
|
}
|
||||||
|
} else if (parent.constructor === String) { // this edge case was added by differential updates
|
||||||
encoder.writeParentInfo(true) // write parentYKey
|
encoder.writeParentInfo(true) // write parentYKey
|
||||||
encoder.writeString(ykey)
|
encoder.writeString(parent)
|
||||||
} else {
|
} else if (parent.constructor === ID) {
|
||||||
encoder.writeParentInfo(false) // write parent id
|
encoder.writeParentInfo(false) // write parent id
|
||||||
encoder.writeLeftID(parentItem.id)
|
encoder.writeLeftID(parent)
|
||||||
|
} else {
|
||||||
|
error.unexpectedCase()
|
||||||
}
|
}
|
||||||
if (parentSub !== null) {
|
if (parentSub !== null) {
|
||||||
encoder.writeString(parentSub)
|
encoder.writeString(parentSub)
|
||||||
@@ -659,7 +670,7 @@ export class Item extends AbstractStruct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @param {number} info
|
* @param {number} info
|
||||||
*/
|
*/
|
||||||
export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
|
export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
|
||||||
@@ -667,10 +678,10 @@ export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS
|
|||||||
/**
|
/**
|
||||||
* A lookup map for reading Item content.
|
* A lookup map for reading Item content.
|
||||||
*
|
*
|
||||||
* @type {Array<function(AbstractUpdateDecoder):AbstractContent>}
|
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractContent>}
|
||||||
*/
|
*/
|
||||||
export const contentRefs = [
|
export const contentRefs = [
|
||||||
() => { throw error.unexpectedCase() }, // GC is not ItemContent
|
() => { error.unexpectedCase() }, // GC is not ItemContent
|
||||||
readContentDeleted, // 1
|
readContentDeleted, // 1
|
||||||
readContentJSON, // 2
|
readContentJSON, // 2
|
||||||
readContentBinary, // 3
|
readContentBinary, // 3
|
||||||
@@ -679,7 +690,8 @@ export const contentRefs = [
|
|||||||
readContentFormat, // 6
|
readContentFormat, // 6
|
||||||
readContentType, // 7
|
readContentType, // 7
|
||||||
readContentAny, // 8
|
readContentAny, // 8
|
||||||
readContentDoc // 9
|
readContentDoc, // 9
|
||||||
|
() => { error.unexpectedCase() } // 10 - Skip is not ItemContent
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -759,7 +771,7 @@ export class AbstractContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
|
|||||||
60
src/structs/Skip.js
Normal file
60
src/structs/Skip.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
AbstractStruct,
|
||||||
|
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
|
||||||
|
export const structSkipRefNumber = 10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export class Skip extends AbstractStruct {
|
||||||
|
get deleted () {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
delete () {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Skip} right
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
mergeWith (right) {
|
||||||
|
if (this.constructor !== right.constructor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.length += right.length
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
integrate (transaction, offset) {
|
||||||
|
// skip structs cannot be integrated
|
||||||
|
error.unexpectedCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
encoder.writeInfo(structSkipRefNumber)
|
||||||
|
// write as VarUint because Skips can't make use of predictable length-encoding
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, this.length - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @return {null | number}
|
||||||
|
*/
|
||||||
|
getMissing (transaction, store) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
ContentAny,
|
ContentAny,
|
||||||
ContentBinary,
|
ContentBinary,
|
||||||
getItemCleanStart,
|
getItemCleanStart,
|
||||||
ContentDoc, YText, YArray, AbstractUpdateEncoder, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as map from 'lib0/map.js'
|
import * as map from 'lib0/map.js'
|
||||||
@@ -324,7 +324,7 @@ export class AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
*/
|
*/
|
||||||
_write (encoder) { }
|
_write (encoder) { }
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
YArrayRefID,
|
YArrayRefID,
|
||||||
callTypeObservers,
|
callTypeObservers,
|
||||||
transact,
|
transact,
|
||||||
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, Transaction, Item // eslint-disable-line
|
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
import { typeListSlice } from './AbstractType.js'
|
import { typeListSlice } from './AbstractType.js'
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
*/
|
*/
|
||||||
_write (encoder) {
|
_write (encoder) {
|
||||||
encoder.writeTypeRef(YArrayRefID)
|
encoder.writeTypeRef(YArrayRefID)
|
||||||
@@ -249,7 +249,7 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
YMapRefID,
|
YMapRefID,
|
||||||
callTypeObservers,
|
callTypeObservers,
|
||||||
transact,
|
transact,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, Transaction, Item // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as iterator from 'lib0/iterator.js'
|
import * as iterator from 'lib0/iterator.js'
|
||||||
@@ -238,7 +238,7 @@ export class YMap extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
*/
|
*/
|
||||||
_write (encoder) {
|
_write (encoder) {
|
||||||
encoder.writeTypeRef(YMapRefID)
|
encoder.writeTypeRef(YMapRefID)
|
||||||
@@ -246,7 +246,7 @@ export class YMap extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
typeMapGet,
|
typeMapGet,
|
||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
updateMarkerChanges,
|
updateMarkerChanges,
|
||||||
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as object from 'lib0/object.js'
|
import * as object from 'lib0/object.js'
|
||||||
@@ -164,11 +164,12 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
|
|||||||
}
|
}
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
const ownClientId = doc.clientID
|
const ownClientId = doc.clientID
|
||||||
let left = currPos.left
|
let nextFormat = currPos.left
|
||||||
const right = currPos.right
|
const right = currPos.right
|
||||||
negatedAttributes.forEach((val, key) => {
|
negatedAttributes.forEach((val, key) => {
|
||||||
left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), nextFormat, nextFormat && nextFormat.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||||
left.integrate(transaction, 0)
|
nextFormat.integrate(transaction, 0)
|
||||||
|
currPos.right = nextFormat
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1203,7 +1204,7 @@ export class YText extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
*/
|
*/
|
||||||
_write (encoder) {
|
_write (encoder) {
|
||||||
encoder.writeTypeRef(YTextRefID)
|
encoder.writeTypeRef(YTextRefID)
|
||||||
@@ -1211,7 +1212,7 @@ export class YText extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {YText}
|
* @return {YText}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
typeListForEach,
|
typeListForEach,
|
||||||
YXmlElementRefID,
|
YXmlElementRefID,
|
||||||
YXmlText, ContentType, AbstractType, AbstractUpdateDecoder, AbstractUpdateEncoder, Snapshot, Doc, Item // eslint-disable-line
|
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Snapshot, Doc, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,7 +208,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
*
|
*
|
||||||
* This is called when this Item is sent to a remote peer.
|
* This is called when this Item is sent to a remote peer.
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateEncoder} encoder The encoder to write data to.
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||||
*/
|
*/
|
||||||
_write (encoder) {
|
_write (encoder) {
|
||||||
encoder.writeTypeRef(YXmlElementRefID)
|
encoder.writeTypeRef(YXmlElementRefID)
|
||||||
@@ -217,7 +217,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {YXmlElement}
|
* @return {YXmlElement}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
transact,
|
transact,
|
||||||
typeListGet,
|
typeListGet,
|
||||||
typeListSlice,
|
typeListSlice,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
@@ -410,7 +410,7 @@ export class YXmlFragment extends AbstractType {
|
|||||||
*
|
*
|
||||||
* This is called when this Item is sent to a remote peer.
|
* This is called when this Item is sent to a remote peer.
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateEncoder} encoder The encoder to write data to.
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||||
*/
|
*/
|
||||||
_write (encoder) {
|
_write (encoder) {
|
||||||
encoder.writeTypeRef(YXmlFragmentRefID)
|
encoder.writeTypeRef(YXmlFragmentRefID)
|
||||||
@@ -418,7 +418,7 @@ export class YXmlFragment extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {YXmlFragment}
|
* @return {YXmlFragment}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
YMap,
|
YMap,
|
||||||
YXmlHookRefID,
|
YXmlHookRefID,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +76,7 @@ export class YXmlHook extends YMap {
|
|||||||
*
|
*
|
||||||
* This is called when this Item is sent to a remote peer.
|
* This is called when this Item is sent to a remote peer.
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateEncoder} encoder The encoder to write data to.
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||||
*/
|
*/
|
||||||
_write (encoder) {
|
_write (encoder) {
|
||||||
encoder.writeTypeRef(YXmlHookRefID)
|
encoder.writeTypeRef(YXmlHookRefID)
|
||||||
@@ -85,7 +85,7 @@ export class YXmlHook extends YMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {YXmlHook}
|
* @return {YXmlHook}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
YText,
|
YText,
|
||||||
YXmlTextRefID,
|
YXmlTextRefID,
|
||||||
ContentType, YXmlElement, AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line
|
ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,7 +104,7 @@ export class YXmlText extends YText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
*/
|
*/
|
||||||
_write (encoder) {
|
_write (encoder) {
|
||||||
encoder.writeTypeRef(YXmlTextRefID)
|
encoder.writeTypeRef(YXmlTextRefID)
|
||||||
@@ -112,7 +112,7 @@ export class YXmlText extends YText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {YXmlText}
|
* @return {YXmlText}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
getState,
|
getState,
|
||||||
splitItem,
|
splitItem,
|
||||||
iterateStructs,
|
iterateStructs,
|
||||||
AbstractUpdateDecoder, AbstractDSDecoder, AbstractDSEncoder, DSDecoderV2, DSEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line
|
UpdateEncoderV2,
|
||||||
|
DSDecoderV1, DSEncoderV1, DSDecoderV2, DSEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as array from 'lib0/array.js'
|
import * as array from 'lib0/array.js'
|
||||||
@@ -121,8 +122,8 @@ export const sortAndMergeDeleteSet = ds => {
|
|||||||
for (i = 1, j = 1; i < dels.length; i++) {
|
for (i = 1, j = 1; i < dels.length; i++) {
|
||||||
const left = dels[j - 1]
|
const left = dels[j - 1]
|
||||||
const right = dels[i]
|
const right = dels[i]
|
||||||
if (left.clock + left.len === right.clock) {
|
if (left.clock + left.len >= right.clock) {
|
||||||
left.len += right.len
|
left.len = math.max(left.len, right.clock + right.len - left.clock)
|
||||||
} else {
|
} else {
|
||||||
if (j < i) {
|
if (j < i) {
|
||||||
dels[j] = right
|
dels[j] = right
|
||||||
@@ -210,7 +211,7 @@ export const createDeleteSetFromStructStore = ss => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractDSEncoder} encoder
|
* @param {DSEncoderV1 | DSEncoderV2} encoder
|
||||||
* @param {DeleteSet} ds
|
* @param {DeleteSet} ds
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
@@ -232,7 +233,7 @@ export const writeDeleteSet = (encoder, ds) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractDSDecoder} decoder
|
* @param {DSDecoderV1 | DSDecoderV2} decoder
|
||||||
* @return {DeleteSet}
|
* @return {DeleteSet}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
@@ -260,9 +261,10 @@ export const readDeleteSet = decoder => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractDSDecoder} decoder
|
* @param {DSDecoderV1 | DSDecoderV2} decoder
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {StructStore} store
|
* @param {StructStore} store
|
||||||
|
* @return {Uint8Array|null} Returns a v2 update containing all deletes that couldn't be applied yet; or null if all deletes were applied successfully.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
@@ -315,9 +317,10 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (unappliedDS.clients.size > 0) {
|
if (unappliedDS.clients.size > 0) {
|
||||||
// TODO: no need for encoding+decoding ds anymore
|
const ds = new UpdateEncoderV2()
|
||||||
const unappliedDSEncoder = new DSEncoderV2()
|
encoding.writeVarUint(ds.restEncoder, 0) // encode 0 structs
|
||||||
writeDeleteSet(unappliedDSEncoder, unappliedDS)
|
writeDeleteSet(ds, unappliedDS)
|
||||||
store.pendingDeleteReaders.push(new DSDecoderV2(decoding.createDecoder((unappliedDSEncoder.toUint8Array()))))
|
return ds.toUint8Array()
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ export class RelativePosition {
|
|||||||
* @param {ID|null} type
|
* @param {ID|null} type
|
||||||
* @param {string|null} tname
|
* @param {string|null} tname
|
||||||
* @param {ID|null} item
|
* @param {ID|null} item
|
||||||
|
* @param {number} assoc
|
||||||
*/
|
*/
|
||||||
constructor (type, tname, item) {
|
constructor (type, tname, item, assoc = 0) {
|
||||||
/**
|
/**
|
||||||
* @type {ID|null}
|
* @type {ID|null}
|
||||||
*/
|
*/
|
||||||
@@ -59,23 +60,57 @@ export class RelativePosition {
|
|||||||
* @type {ID | null}
|
* @type {ID | null}
|
||||||
*/
|
*/
|
||||||
this.item = item
|
this.item = item
|
||||||
|
/**
|
||||||
|
* A relative position is associated to a specific character. By default
|
||||||
|
* assoc >= 0, the relative position is associated to the character
|
||||||
|
* after the meant position.
|
||||||
|
* I.e. position 1 in 'ab' is associated to character 'b'.
|
||||||
|
*
|
||||||
|
* If assoc < 0, then the relative position is associated to the caharacter
|
||||||
|
* before the meant position.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.assoc = assoc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RelativePosition} rpos
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
|
export const relativePositionToJSON = rpos => {
|
||||||
|
const json = {}
|
||||||
|
if (rpos.type) {
|
||||||
|
json.type = rpos.type
|
||||||
|
}
|
||||||
|
if (rpos.tname) {
|
||||||
|
json.tname = rpos.tname
|
||||||
|
}
|
||||||
|
if (rpos.item) {
|
||||||
|
json.item = rpos.item
|
||||||
|
}
|
||||||
|
if (rpos.assoc != null) {
|
||||||
|
json.assoc = rpos.assoc
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} json
|
* @param {any} json
|
||||||
* @return {RelativePosition}
|
* @return {RelativePosition}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock))
|
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock), json.assoc == null ? 0 : json.assoc)
|
||||||
|
|
||||||
export class AbsolutePosition {
|
export class AbsolutePosition {
|
||||||
/**
|
/**
|
||||||
* @param {AbstractType<any>} type
|
* @param {AbstractType<any>} type
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
|
* @param {number} [assoc]
|
||||||
*/
|
*/
|
||||||
constructor (type, index) {
|
constructor (type, index, assoc = 0) {
|
||||||
/**
|
/**
|
||||||
* @type {AbstractType<any>}
|
* @type {AbstractType<any>}
|
||||||
*/
|
*/
|
||||||
@@ -84,24 +119,27 @@ export class AbsolutePosition {
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.index = index
|
this.index = index
|
||||||
|
this.assoc = assoc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractType<any>} type
|
* @param {AbstractType<any>} type
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
|
* @param {number} [assoc]
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const createAbsolutePosition = (type, index) => new AbsolutePosition(type, index)
|
export const createAbsolutePosition = (type, index, assoc = 0) => new AbsolutePosition(type, index, assoc)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractType<any>} type
|
* @param {AbstractType<any>} type
|
||||||
* @param {ID|null} item
|
* @param {ID|null} item
|
||||||
|
* @param {number} [assoc]
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const createRelativePosition = (type, item) => {
|
export const createRelativePosition = (type, item, assoc) => {
|
||||||
let typeid = null
|
let typeid = null
|
||||||
let tname = null
|
let tname = null
|
||||||
if (type._item === null) {
|
if (type._item === null) {
|
||||||
@@ -109,7 +147,7 @@ export const createRelativePosition = (type, item) => {
|
|||||||
} else {
|
} else {
|
||||||
typeid = createID(type._item.id.client, type._item.id.clock)
|
typeid = createID(type._item.id.client, type._item.id.clock)
|
||||||
}
|
}
|
||||||
return new RelativePosition(typeid, tname, item)
|
return new RelativePosition(typeid, tname, item, assoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,23 +155,35 @@ export const createRelativePosition = (type, item) => {
|
|||||||
*
|
*
|
||||||
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
|
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
|
||||||
* @param {number} index The absolute position.
|
* @param {number} index The absolute position.
|
||||||
|
* @param {number} [assoc]
|
||||||
* @return {RelativePosition}
|
* @return {RelativePosition}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const createRelativePositionFromTypeIndex = (type, index) => {
|
export const createRelativePositionFromTypeIndex = (type, index, assoc = 0) => {
|
||||||
let t = type._start
|
let t = type._start
|
||||||
|
if (assoc < 0) {
|
||||||
|
// associated to the left character or the beginning of a type, increment index if possible.
|
||||||
|
if (index === 0) {
|
||||||
|
return createRelativePosition(type, null, assoc)
|
||||||
|
}
|
||||||
|
index--
|
||||||
|
}
|
||||||
while (t !== null) {
|
while (t !== null) {
|
||||||
if (!t.deleted && t.countable) {
|
if (!t.deleted && t.countable) {
|
||||||
if (t.length > index) {
|
if (t.length > index) {
|
||||||
// case 1: found position somewhere in the linked list
|
// case 1: found position somewhere in the linked list
|
||||||
return createRelativePosition(type, createID(t.id.client, t.id.clock + index))
|
return createRelativePosition(type, createID(t.id.client, t.id.clock + index), assoc)
|
||||||
}
|
}
|
||||||
index -= t.length
|
index -= t.length
|
||||||
}
|
}
|
||||||
|
if (t.right === null && assoc < 0) {
|
||||||
|
// left-associated position, return last available id
|
||||||
|
return createRelativePosition(type, t.lastId, assoc)
|
||||||
|
}
|
||||||
t = t.right
|
t = t.right
|
||||||
}
|
}
|
||||||
return createRelativePosition(type, null)
|
return createRelativePosition(type, null, assoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,7 +193,7 @@ export const createRelativePositionFromTypeIndex = (type, index) => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const writeRelativePosition = (encoder, rpos) => {
|
export const writeRelativePosition = (encoder, rpos) => {
|
||||||
const { type, tname, item } = rpos
|
const { type, tname, item, assoc } = rpos
|
||||||
if (item !== null) {
|
if (item !== null) {
|
||||||
encoding.writeVarUint(encoder, 0)
|
encoding.writeVarUint(encoder, 0)
|
||||||
writeID(encoder, item)
|
writeID(encoder, item)
|
||||||
@@ -158,6 +208,7 @@ export const writeRelativePosition = (encoder, rpos) => {
|
|||||||
} else {
|
} else {
|
||||||
throw error.unexpectedCase()
|
throw error.unexpectedCase()
|
||||||
}
|
}
|
||||||
|
encoding.writeVarInt(encoder, assoc)
|
||||||
return encoder
|
return encoder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +224,7 @@ export const encodeRelativePosition = rpos => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
* @return {RelativePosition|null}
|
* @return {RelativePosition}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
@@ -195,12 +246,13 @@ export const readRelativePosition = decoder => {
|
|||||||
type = readID(decoder)
|
type = readID(decoder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new RelativePosition(type, tname, itemID)
|
const assoc = decoding.hasContent(decoder) ? decoding.readVarInt(decoder) : 0
|
||||||
|
return new RelativePosition(type, tname, itemID, assoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} uint8Array
|
* @param {Uint8Array} uint8Array
|
||||||
* @return {RelativePosition|null}
|
* @return {RelativePosition}
|
||||||
*/
|
*/
|
||||||
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
||||||
|
|
||||||
@@ -216,6 +268,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
|||||||
const rightID = rpos.item
|
const rightID = rpos.item
|
||||||
const typeID = rpos.type
|
const typeID = rpos.type
|
||||||
const tname = rpos.tname
|
const tname = rpos.tname
|
||||||
|
const assoc = rpos.assoc
|
||||||
let type = null
|
let type = null
|
||||||
let index = 0
|
let index = 0
|
||||||
if (rightID !== null) {
|
if (rightID !== null) {
|
||||||
@@ -229,7 +282,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
|||||||
}
|
}
|
||||||
type = /** @type {AbstractType<any>} */ (right.parent)
|
type = /** @type {AbstractType<any>} */ (right.parent)
|
||||||
if (type._item === null || !type._item.deleted) {
|
if (type._item === null || !type._item.deleted) {
|
||||||
index = right.deleted || !right.countable ? 0 : res.diff
|
index = (right.deleted || !right.countable) ? 0 : (res.diff + (assoc >= 0 ? 0 : 1)) // adjust position based on left association if necessary
|
||||||
let n = right.left
|
let n = right.left
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
if (!n.deleted && n.countable) {
|
if (!n.deleted && n.countable) {
|
||||||
@@ -256,9 +309,13 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
|||||||
} else {
|
} else {
|
||||||
throw error.unexpectedCase()
|
throw error.unexpectedCase()
|
||||||
}
|
}
|
||||||
index = type._length
|
if (assoc >= 0) {
|
||||||
|
index = type._length
|
||||||
|
} else {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return createAbsolutePosition(type, index)
|
return createAbsolutePosition(type, index, rpos.assoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,5 +326,5 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const compareRelativePositions = (a, b) => a === b || (
|
export const compareRelativePositions = (a, b) => a === b || (
|
||||||
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type)
|
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type) && a.assoc === b.assoc
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ import {
|
|||||||
getState,
|
getState,
|
||||||
findIndexSS,
|
findIndexSS,
|
||||||
UpdateEncoderV2,
|
UpdateEncoderV2,
|
||||||
DefaultDSEncoder,
|
|
||||||
applyUpdateV2,
|
applyUpdateV2,
|
||||||
AbstractDSDecoder, AbstractDSEncoder, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
|
DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as map from 'lib0/map.js'
|
import * as map from 'lib0/map.js'
|
||||||
@@ -78,7 +77,7 @@ export const equalSnapshots = (snap1, snap2) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Snapshot} snapshot
|
* @param {Snapshot} snapshot
|
||||||
* @param {AbstractDSEncoder} [encoder]
|
* @param {DSEncoderV1 | DSEncoderV2} [encoder]
|
||||||
* @return {Uint8Array}
|
* @return {Uint8Array}
|
||||||
*/
|
*/
|
||||||
export const encodeSnapshotV2 = (snapshot, encoder = new DSEncoderV2()) => {
|
export const encodeSnapshotV2 = (snapshot, encoder = new DSEncoderV2()) => {
|
||||||
@@ -91,11 +90,11 @@ export const encodeSnapshotV2 = (snapshot, encoder = new DSEncoderV2()) => {
|
|||||||
* @param {Snapshot} snapshot
|
* @param {Snapshot} snapshot
|
||||||
* @return {Uint8Array}
|
* @return {Uint8Array}
|
||||||
*/
|
*/
|
||||||
export const encodeSnapshot = snapshot => encodeSnapshotV2(snapshot, new DefaultDSEncoder())
|
export const encodeSnapshot = snapshot => encodeSnapshotV2(snapshot, new DSEncoderV1())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} buf
|
* @param {Uint8Array} buf
|
||||||
* @param {AbstractDSDecoder} [decoder]
|
* @param {DSDecoderV1 | DSDecoderV2} [decoder]
|
||||||
* @return {Snapshot}
|
* @return {Snapshot}
|
||||||
*/
|
*/
|
||||||
export const decodeSnapshotV2 = (buf, decoder = new DSDecoderV2(decoding.createDecoder(buf))) => {
|
export const decodeSnapshotV2 = (buf, decoder = new DSDecoderV2(decoding.createDecoder(buf))) => {
|
||||||
|
|||||||
@@ -15,24 +15,13 @@ export class StructStore {
|
|||||||
*/
|
*/
|
||||||
this.clients = new Map()
|
this.clients = new Map()
|
||||||
/**
|
/**
|
||||||
* Store incompleted struct reads here
|
* @type {null | { missing: Map<number, number>, update: Uint8Array }}
|
||||||
* `i` denotes to the next read operation
|
|
||||||
* We could shift the array of refs instead, but shift is incredible
|
|
||||||
* slow in Chrome for arrays with more than 100k elements
|
|
||||||
* @see tryResumePendingStructRefs
|
|
||||||
* @type {Map<number,{i:number,refs:Array<GC|Item>}>}
|
|
||||||
*/
|
*/
|
||||||
this.pendingClientsStructRefs = new Map()
|
this.pendingStructs = null
|
||||||
/**
|
/**
|
||||||
* Stack of pending structs waiting for struct dependencies
|
* @type {null | Uint8Array}
|
||||||
* Maximum length of stack is structReaders.size
|
|
||||||
* @type {Array<GC|Item>}
|
|
||||||
*/
|
*/
|
||||||
this.pendingStack = []
|
this.pendingDs = null
|
||||||
/**
|
|
||||||
* @type {Array<DSDecoderV2>}
|
|
||||||
*/
|
|
||||||
this.pendingDeleteReaders = []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Item,
|
Item,
|
||||||
generateNewClientId,
|
generateNewClientId,
|
||||||
createID,
|
createID,
|
||||||
AbstractUpdateEncoder, GC, StructStore, UpdateEncoderV2, DefaultUpdateEncoder, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as map from 'lib0/map.js'
|
import * as map from 'lib0/map.js'
|
||||||
@@ -118,7 +118,7 @@ export class Transaction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @return {boolean} Whether data was written.
|
* @return {boolean} Whether data was written.
|
||||||
*/
|
*/
|
||||||
@@ -337,17 +337,17 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
// @todo Merge all the transactions into one and provide send the data as a single update message
|
// @todo Merge all the transactions into one and provide send the data as a single update message
|
||||||
doc.emit('afterTransactionCleanup', [transaction, doc])
|
doc.emit('afterTransactionCleanup', [transaction, doc])
|
||||||
if (doc._observers.has('update')) {
|
if (doc._observers.has('update')) {
|
||||||
const encoder = new DefaultUpdateEncoder()
|
const encoder = new UpdateEncoderV1()
|
||||||
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
||||||
if (hasContent) {
|
if (hasContent) {
|
||||||
doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc])
|
doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc, transaction])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (doc._observers.has('updateV2')) {
|
if (doc._observers.has('updateV2')) {
|
||||||
const encoder = new UpdateEncoderV2()
|
const encoder = new UpdateEncoderV2()
|
||||||
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
||||||
if (hasContent) {
|
if (hasContent) {
|
||||||
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc])
|
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transaction.subdocsAdded.forEach(subdoc => doc.subdocs.add(subdoc))
|
transaction.subdocsAdded.forEach(subdoc => doc.subdocs.add(subdoc))
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {
|
|||||||
transact,
|
transact,
|
||||||
createID,
|
createID,
|
||||||
redoItem,
|
redoItem,
|
||||||
iterateStructs,
|
|
||||||
isParentOf,
|
isParentOf,
|
||||||
followRedone,
|
followRedone,
|
||||||
getItemCleanStart,
|
getItemCleanStart,
|
||||||
getState,
|
isDeleted,
|
||||||
ID, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
addToDeleteSet,
|
||||||
|
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as time from 'lib0/time.js'
|
import * as time from 'lib0/time.js'
|
||||||
@@ -18,14 +18,12 @@ import { Observable } from 'lib0/observable.js'
|
|||||||
|
|
||||||
class StackItem {
|
class StackItem {
|
||||||
/**
|
/**
|
||||||
* @param {DeleteSet} ds
|
* @param {DeleteSet} deletions
|
||||||
* @param {Map<number,number>} beforeState
|
* @param {DeleteSet} insertions
|
||||||
* @param {Map<number,number>} afterState
|
|
||||||
*/
|
*/
|
||||||
constructor (ds, beforeState, afterState) {
|
constructor (deletions, insertions) {
|
||||||
this.ds = ds
|
this.insertions = insertions
|
||||||
this.beforeState = beforeState
|
this.deletions = deletions
|
||||||
this.afterState = afterState
|
|
||||||
/**
|
/**
|
||||||
* Use this to save and restore metadata like selection range
|
* Use this to save and restore metadata like selection range
|
||||||
*/
|
*/
|
||||||
@@ -45,6 +43,11 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
* @type {StackItem?}
|
* @type {StackItem?}
|
||||||
*/
|
*/
|
||||||
let result = null
|
let result = null
|
||||||
|
/**
|
||||||
|
* Keep a reference to the transaction so we can fire the event with the changedParentTypes
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let _tr = null
|
||||||
const doc = undoManager.doc
|
const doc = undoManager.doc
|
||||||
const scope = undoManager.scope
|
const scope = undoManager.scope
|
||||||
transact(doc, transaction => {
|
transact(doc, transaction => {
|
||||||
@@ -60,48 +63,26 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
*/
|
*/
|
||||||
const itemsToDelete = []
|
const itemsToDelete = []
|
||||||
let performedChange = false
|
let performedChange = false
|
||||||
stackItem.afterState.forEach((endClock, client) => {
|
iterateDeletedStructs(transaction, stackItem.insertions, struct => {
|
||||||
const startClock = stackItem.beforeState.get(client) || 0
|
if (struct instanceof Item) {
|
||||||
const len = endClock - startClock
|
if (struct.redone !== null) {
|
||||||
// @todo iterateStructs should not need the structs parameter
|
let { item, diff } = followRedone(store, struct.id)
|
||||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
if (diff > 0) {
|
||||||
if (startClock !== endClock) {
|
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
struct = item
|
||||||
|
}
|
||||||
|
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
|
||||||
|
itemsToDelete.push(struct)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
iterateDeletedStructs(transaction, stackItem.ds, struct => {
|
iterateDeletedStructs(transaction, stackItem.deletions, 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 (
|
if (
|
||||||
struct instanceof Item &&
|
struct instanceof Item &&
|
||||||
scope.some(type => isParentOf(type, struct)) &&
|
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.
|
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
|
||||||
!(clock >= startClock && clock < endClock)
|
!isDeleted(stackItem.insertions, struct.id)
|
||||||
) {
|
) {
|
||||||
itemsToRedo.add(struct)
|
itemsToRedo.add(struct)
|
||||||
}
|
}
|
||||||
@@ -126,9 +107,11 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
type._searchMarker.length = 0
|
type._searchMarker.length = 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
_tr = transaction
|
||||||
}, undoManager)
|
}, undoManager)
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
|
const changedParentTypes = _tr.changedParentTypes
|
||||||
|
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager])
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -194,17 +177,23 @@ export class UndoManager extends Observable {
|
|||||||
// neither undoing nor redoing: delete redoStack
|
// neither undoing nor redoing: delete redoStack
|
||||||
this.redoStack = []
|
this.redoStack = []
|
||||||
}
|
}
|
||||||
const beforeState = transaction.beforeState
|
const insertions = new DeleteSet()
|
||||||
const afterState = transaction.afterState
|
transaction.afterState.forEach((endClock, client) => {
|
||||||
|
const startClock = transaction.beforeState.get(client) || 0
|
||||||
|
const len = endClock - startClock
|
||||||
|
if (len > 0) {
|
||||||
|
addToDeleteSet(insertions, client, startClock, len)
|
||||||
|
}
|
||||||
|
})
|
||||||
const now = time.getUnixTime()
|
const now = time.getUnixTime()
|
||||||
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||||
// append change to last stack op
|
// append change to last stack op
|
||||||
const lastOp = stack[stack.length - 1]
|
const lastOp = stack[stack.length - 1]
|
||||||
lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
|
lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
|
||||||
lastOp.afterState = afterState
|
lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions])
|
||||||
} else {
|
} else {
|
||||||
// create a new stack op
|
// create a new stack op
|
||||||
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState))
|
stack.push(new StackItem(transaction.deleteSet, insertions))
|
||||||
}
|
}
|
||||||
if (!undoing && !redoing) {
|
if (!undoing && !redoing) {
|
||||||
this.lastChange = now
|
this.lastChange = now
|
||||||
@@ -215,7 +204,7 @@ export class UndoManager extends Observable {
|
|||||||
keepItem(item, true)
|
keepItem(item, true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo' }, this])
|
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +214,7 @@ export class UndoManager extends Observable {
|
|||||||
* @param {StackItem} stackItem
|
* @param {StackItem} stackItem
|
||||||
*/
|
*/
|
||||||
const clearItem = stackItem => {
|
const clearItem = stackItem => {
|
||||||
iterateDeletedStructs(transaction, stackItem.ds, item => {
|
iterateDeletedStructs(transaction, stackItem.deletions, item => {
|
||||||
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||||
keepItem(item, false)
|
keepItem(item, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +1,9 @@
|
|||||||
import * as buffer from 'lib0/buffer.js'
|
import * as buffer from 'lib0/buffer.js'
|
||||||
import * as error from 'lib0/error.js'
|
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import {
|
import {
|
||||||
ID, createID
|
ID, createID
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
export class AbstractDSDecoder {
|
|
||||||
/**
|
|
||||||
* @param {decoding.Decoder} decoder
|
|
||||||
*/
|
|
||||||
constructor (decoder) {
|
|
||||||
this.restDecoder = decoder
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDsCurVal () { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
readDsClock () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
readDsLen () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AbstractUpdateDecoder extends AbstractDSDecoder {
|
|
||||||
/**
|
|
||||||
* @return {ID}
|
|
||||||
*/
|
|
||||||
readLeftID () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {ID}
|
|
||||||
*/
|
|
||||||
readRightID () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the next client id.
|
|
||||||
* Use this in favor of readID whenever possible to reduce the number of objects created.
|
|
||||||
*
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
readClient () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {number} info An unsigned 8-bit integer
|
|
||||||
*/
|
|
||||||
readInfo () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
readString () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {boolean} isKey
|
|
||||||
*/
|
|
||||||
readParentInfo () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {number} info An unsigned 8-bit integer
|
|
||||||
*/
|
|
||||||
readTypeRef () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write len of a struct - well suited for Opt RLE encoder.
|
|
||||||
*
|
|
||||||
* @return {number} len
|
|
||||||
*/
|
|
||||||
readLen () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {any}
|
|
||||||
*/
|
|
||||||
readAny () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {Uint8Array}
|
|
||||||
*/
|
|
||||||
readBuf () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy implementation uses JSON parse. We use any-decoding in v2.
|
|
||||||
*
|
|
||||||
* @return {any}
|
|
||||||
*/
|
|
||||||
readJSON () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
readKey () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DSDecoderV1 {
|
export class DSDecoderV1 {
|
||||||
/**
|
/**
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
@@ -247,6 +127,9 @@ export class DSDecoderV2 {
|
|||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
*/
|
*/
|
||||||
constructor (decoder) {
|
constructor (decoder) {
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
this.dsCurrVal = 0
|
this.dsCurrVal = 0
|
||||||
this.restDecoder = decoder
|
this.restDecoder = decoder
|
||||||
}
|
}
|
||||||
@@ -255,11 +138,17 @@ export class DSDecoderV2 {
|
|||||||
this.dsCurrVal = 0
|
this.dsCurrVal = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
readDsClock () {
|
readDsClock () {
|
||||||
this.dsCurrVal += decoding.readVarUint(this.restDecoder)
|
this.dsCurrVal += decoding.readVarUint(this.restDecoder)
|
||||||
return this.dsCurrVal
|
return this.dsCurrVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
readDsLen () {
|
readDsLen () {
|
||||||
const diff = decoding.readVarUint(this.restDecoder) + 1
|
const diff = decoding.readVarUint(this.restDecoder) + 1
|
||||||
this.dsCurrVal += diff
|
this.dsCurrVal += diff
|
||||||
@@ -280,7 +169,7 @@ export class UpdateDecoderV2 extends DSDecoderV2 {
|
|||||||
* @type {Array<string>}
|
* @type {Array<string>}
|
||||||
*/
|
*/
|
||||||
this.keys = []
|
this.keys = []
|
||||||
decoding.readUint8(decoder) // read feature flag - currently unused
|
decoding.readVarUint(decoder) // read feature flag - currently unused
|
||||||
this.keyClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder))
|
this.keyClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder))
|
||||||
this.clientDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder))
|
this.clientDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder))
|
||||||
this.leftClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder))
|
this.leftClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder))
|
||||||
|
|||||||
@@ -6,110 +6,9 @@ import {
|
|||||||
ID // eslint-disable-line
|
ID // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
export class AbstractDSEncoder {
|
|
||||||
constructor () {
|
|
||||||
this.restEncoder = encoding.createEncoder()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {Uint8Array}
|
|
||||||
*/
|
|
||||||
toUint8Array () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the ds value to 0.
|
|
||||||
* The v2 encoder uses this information to reset the initial diff value.
|
|
||||||
*/
|
|
||||||
resetDsCurVal () { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} clock
|
|
||||||
*/
|
|
||||||
writeDsClock (clock) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} len
|
|
||||||
*/
|
|
||||||
writeDsLen (len) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AbstractUpdateEncoder extends AbstractDSEncoder {
|
|
||||||
/**
|
|
||||||
* @return {Uint8Array}
|
|
||||||
*/
|
|
||||||
toUint8Array () {
|
|
||||||
error.methodUnimplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ID} id
|
|
||||||
*/
|
|
||||||
writeLeftID (id) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ID} id
|
|
||||||
*/
|
|
||||||
writeRightID (id) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use writeClient and writeClock instead of writeID if possible.
|
|
||||||
* @param {number} client
|
|
||||||
*/
|
|
||||||
writeClient (client) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} info An unsigned 8-bit integer
|
|
||||||
*/
|
|
||||||
writeInfo (info) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} s
|
|
||||||
*/
|
|
||||||
writeString (s) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} isYKey
|
|
||||||
*/
|
|
||||||
writeParentInfo (isYKey) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} info An unsigned 8-bit integer
|
|
||||||
*/
|
|
||||||
writeTypeRef (info) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write len of a struct - well suited for Opt RLE encoder.
|
|
||||||
*
|
|
||||||
* @param {number} len
|
|
||||||
*/
|
|
||||||
writeLen (len) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} any
|
|
||||||
*/
|
|
||||||
writeAny (any) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Uint8Array} buf
|
|
||||||
*/
|
|
||||||
writeBuf (buf) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} embed
|
|
||||||
*/
|
|
||||||
writeJSON (embed) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} key
|
|
||||||
*/
|
|
||||||
writeKey (key) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DSEncoderV1 {
|
export class DSEncoderV1 {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.restEncoder = new encoding.Encoder()
|
this.restEncoder = encoding.createEncoder()
|
||||||
}
|
}
|
||||||
|
|
||||||
toUint8Array () {
|
toUint8Array () {
|
||||||
@@ -228,7 +127,7 @@ export class UpdateEncoderV1 extends DSEncoderV1 {
|
|||||||
|
|
||||||
export class DSEncoderV2 {
|
export class DSEncoderV2 {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.restEncoder = new encoding.Encoder() // encodes all the rest / non-optimized
|
this.restEncoder = encoding.createEncoder() // encodes all the rest / non-optimized
|
||||||
this.dsCurrVal = 0
|
this.dsCurrVal = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +187,7 @@ export class UpdateEncoderV2 extends DSEncoderV2 {
|
|||||||
|
|
||||||
toUint8Array () {
|
toUint8Array () {
|
||||||
const encoder = encoding.createEncoder()
|
const encoder = encoding.createEncoder()
|
||||||
encoding.writeUint8(encoder, 0) // this is a feature flag that we might use in the future
|
encoding.writeVarUint(encoder, 0) // this is a feature flag that we might use in the future
|
||||||
encoding.writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array())
|
encoding.writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array())
|
||||||
encoding.writeVarUint8Array(encoder, this.clientEncoder.toUint8Array())
|
encoding.writeVarUint8Array(encoder, this.clientEncoder.toUint8Array())
|
||||||
encoding.writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array())
|
encoding.writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array())
|
||||||
|
|||||||
@@ -29,39 +29,23 @@ import {
|
|||||||
UpdateDecoderV2,
|
UpdateDecoderV2,
|
||||||
UpdateEncoderV1,
|
UpdateEncoderV1,
|
||||||
UpdateEncoderV2,
|
UpdateEncoderV2,
|
||||||
DSDecoderV2,
|
|
||||||
DSEncoderV2,
|
DSEncoderV2,
|
||||||
DSDecoderV1,
|
DSDecoderV1,
|
||||||
DSEncoderV1,
|
DSEncoderV1,
|
||||||
AbstractDSEncoder, AbstractDSDecoder, AbstractUpdateEncoder, AbstractUpdateDecoder, AbstractContent, Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
|
mergeUpdatesV2,
|
||||||
|
Skip,
|
||||||
|
diffUpdateV2,
|
||||||
|
DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import * as binary from 'lib0/binary.js'
|
import * as binary from 'lib0/binary.js'
|
||||||
import * as map from 'lib0/map.js'
|
import * as map from 'lib0/map.js'
|
||||||
|
import * as math from 'lib0/math.js'
|
||||||
export let DefaultDSEncoder = DSEncoderV1
|
|
||||||
export let DefaultDSDecoder = DSDecoderV1
|
|
||||||
export let DefaultUpdateEncoder = UpdateEncoderV1
|
|
||||||
export let DefaultUpdateDecoder = UpdateDecoderV1
|
|
||||||
|
|
||||||
export const useV1Encoding = () => {
|
|
||||||
DefaultDSEncoder = DSEncoderV1
|
|
||||||
DefaultDSDecoder = DSDecoderV1
|
|
||||||
DefaultUpdateEncoder = UpdateEncoderV1
|
|
||||||
DefaultUpdateDecoder = UpdateDecoderV1
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useV2Encoding = () => {
|
|
||||||
DefaultDSEncoder = DSEncoderV2
|
|
||||||
DefaultDSDecoder = DSDecoderV2
|
|
||||||
DefaultUpdateEncoder = UpdateEncoderV2
|
|
||||||
DefaultUpdateDecoder = UpdateDecoderV2
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {Array<GC|Item>} structs All structs by `client`
|
* @param {Array<GC|Item>} structs All structs by `client`
|
||||||
* @param {number} client
|
* @param {number} client
|
||||||
* @param {number} clock write structs starting with `ID(client,clock)`
|
* @param {number} clock write structs starting with `ID(client,clock)`
|
||||||
@@ -70,6 +54,7 @@ export const useV2Encoding = () => {
|
|||||||
*/
|
*/
|
||||||
const writeStructs = (encoder, structs, client, clock) => {
|
const writeStructs = (encoder, structs, client, clock) => {
|
||||||
// write first id
|
// write first id
|
||||||
|
clock = math.max(clock, structs[0].id.clock) // make sure the first id exists
|
||||||
const startNewStructs = findIndexSS(structs, clock)
|
const startNewStructs = findIndexSS(structs, clock)
|
||||||
// write # encoded structs
|
// write # encoded structs
|
||||||
encoding.writeVarUint(encoder.restEncoder, structs.length - startNewStructs)
|
encoding.writeVarUint(encoder.restEncoder, structs.length - startNewStructs)
|
||||||
@@ -84,7 +69,7 @@ const writeStructs = (encoder, structs, client, clock) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {StructStore} store
|
* @param {StructStore} store
|
||||||
* @param {Map<number,number>} _sm
|
* @param {Map<number,number>} _sm
|
||||||
*
|
*
|
||||||
@@ -116,15 +101,18 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateDecoder} decoder The decoder object to read data from.
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from.
|
||||||
* @param {Map<number,Array<GC|Item>>} clientRefs
|
|
||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
* @return {Map<number,Array<GC|Item>>}
|
* @return {Map<number, { i: number, refs: Array<Item | GC> }>}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const readClientsStructRefs = (decoder, clientRefs, doc) => {
|
export const readClientsStructRefs = (decoder, doc) => {
|
||||||
|
/**
|
||||||
|
* @type {Map<number, { i: number, refs: Array<Item | GC> }>}
|
||||||
|
*/
|
||||||
|
const clientRefs = map.create()
|
||||||
const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder)
|
const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder)
|
||||||
for (let i = 0; i < numOfStateUpdates; i++) {
|
for (let i = 0; i < numOfStateUpdates; i++) {
|
||||||
const numberOfStructs = decoding.readVarUint(decoder.restDecoder)
|
const numberOfStructs = decoding.readVarUint(decoder.restDecoder)
|
||||||
@@ -135,61 +123,72 @@ export const readClientsStructRefs = (decoder, clientRefs, doc) => {
|
|||||||
const client = decoder.readClient()
|
const client = decoder.readClient()
|
||||||
let clock = decoding.readVarUint(decoder.restDecoder)
|
let clock = decoding.readVarUint(decoder.restDecoder)
|
||||||
// const start = performance.now()
|
// const start = performance.now()
|
||||||
clientRefs.set(client, refs)
|
clientRefs.set(client, { i: 0, refs })
|
||||||
for (let i = 0; i < numberOfStructs; i++) {
|
for (let i = 0; i < numberOfStructs; i++) {
|
||||||
const info = decoder.readInfo()
|
const info = decoder.readInfo()
|
||||||
if ((binary.BITS5 & info) !== 0) {
|
switch (binary.BITS5 & info) {
|
||||||
/**
|
case 0: { // GC
|
||||||
* The optimized implementation doesn't use any variables because inlining variables is faster.
|
const len = decoder.readLen()
|
||||||
* Below a non-optimized version is shown that implements the basic algorithm with
|
refs[i] = new GC(createID(client, clock), len)
|
||||||
* a few comments
|
clock += len
|
||||||
*/
|
break
|
||||||
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
}
|
||||||
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
case 10: { // Skip Struct (nothing to apply)
|
||||||
// and we read the next string as parentYKey.
|
// @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing.
|
||||||
// It indicates how we store/retrieve parent from `y.share`
|
const len = decoding.readVarUint(decoder.restDecoder)
|
||||||
// @type {string|null}
|
refs[i] = new Skip(createID(client, clock), len)
|
||||||
const struct = new Item(
|
clock += len
|
||||||
createID(client, clock),
|
break
|
||||||
null, // leftd
|
}
|
||||||
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
|
default: { // Item with content
|
||||||
null, // right
|
/**
|
||||||
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
|
* The optimized implementation doesn't use any variables because inlining variables is faster.
|
||||||
cantCopyParentInfo ? (decoder.readParentInfo() ? doc.get(decoder.readString()) : decoder.readLeftID()) : null, // parent
|
* Below a non-optimized version is shown that implements the basic algorithm with
|
||||||
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
|
* a few comments
|
||||||
readItemContent(decoder, info) // item content
|
*/
|
||||||
)
|
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
||||||
/* A non-optimized implementation of the above algorithm:
|
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
||||||
|
// and we read the next string as parentYKey.
|
||||||
|
// It indicates how we store/retrieve parent from `y.share`
|
||||||
|
// @type {string|null}
|
||||||
|
const struct = new Item(
|
||||||
|
createID(client, clock),
|
||||||
|
null, // leftd
|
||||||
|
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
|
||||||
|
null, // right
|
||||||
|
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
|
||||||
|
cantCopyParentInfo ? (decoder.readParentInfo() ? doc.get(decoder.readString()) : decoder.readLeftID()) : null, // parent
|
||||||
|
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
|
||||||
|
readItemContent(decoder, info) // item content
|
||||||
|
)
|
||||||
|
/* A non-optimized implementation of the above algorithm:
|
||||||
|
|
||||||
// The item that was originally to the left of this item.
|
// The item that was originally to the left of this item.
|
||||||
const origin = (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null
|
const origin = (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null
|
||||||
// The item that was originally to the right of this item.
|
// The item that was originally to the right of this item.
|
||||||
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null
|
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null
|
||||||
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
||||||
const hasParentYKey = cantCopyParentInfo ? decoder.readParentInfo() : false
|
const hasParentYKey = cantCopyParentInfo ? decoder.readParentInfo() : false
|
||||||
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
||||||
// and we read the next string as parentYKey.
|
// and we read the next string as parentYKey.
|
||||||
// It indicates how we store/retrieve parent from `y.share`
|
// It indicates how we store/retrieve parent from `y.share`
|
||||||
// @type {string|null}
|
// @type {string|null}
|
||||||
const parentYKey = cantCopyParentInfo && hasParentYKey ? decoder.readString() : null
|
const parentYKey = cantCopyParentInfo && hasParentYKey ? decoder.readString() : null
|
||||||
|
|
||||||
const struct = new Item(
|
const struct = new Item(
|
||||||
createID(client, clock),
|
createID(client, clock),
|
||||||
null, // leftd
|
null, // leftd
|
||||||
origin, // origin
|
origin, // origin
|
||||||
null, // right
|
null, // right
|
||||||
rightOrigin, // right origin
|
rightOrigin, // right origin
|
||||||
cantCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent
|
cantCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent
|
||||||
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
|
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
|
||||||
readItemContent(decoder, info) // item content
|
readItemContent(decoder, info) // item content
|
||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
refs[i] = struct
|
refs[i] = struct
|
||||||
clock += struct.length
|
clock += struct.length
|
||||||
} else {
|
}
|
||||||
const len = decoder.readLen()
|
|
||||||
refs[i] = new GC(createID(client, clock), len)
|
|
||||||
clock += len
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log('time to read: ', performance.now() - start) // @todo remove
|
// console.log('time to read: ', performance.now() - start) // @todo remove
|
||||||
@@ -218,26 +217,32 @@ export const readClientsStructRefs = (decoder, clientRefs, doc) => {
|
|||||||
*
|
*
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {StructStore} store
|
* @param {StructStore} store
|
||||||
|
* @param {Map<number, { i: number, refs: (GC | Item)[] }>} clientsStructRefs
|
||||||
|
* @return { null | { update: Uint8Array, missing: Map<number,number> } }
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const resumeStructIntegration = (transaction, store) => {
|
const integrateStructs = (transaction, store, clientsStructRefs) => {
|
||||||
const stack = store.pendingStack // @todo don't forget to append stackhead at the end
|
/**
|
||||||
const clientsStructRefs = store.pendingClientsStructRefs
|
* @type {Array<Item | GC>}
|
||||||
|
*/
|
||||||
|
const stack = []
|
||||||
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
|
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
|
||||||
const clientsStructRefsIds = Array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
|
let clientsStructRefsIds = Array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
|
||||||
if (clientsStructRefsIds.length === 0) {
|
if (clientsStructRefsIds.length === 0) {
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
const getNextStructTarget = () => {
|
const getNextStructTarget = () => {
|
||||||
|
if (clientsStructRefsIds.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
let nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
|
let nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
|
||||||
while (nextStructsTarget.refs.length === nextStructsTarget.i) {
|
while (nextStructsTarget.refs.length === nextStructsTarget.i) {
|
||||||
clientsStructRefsIds.pop()
|
clientsStructRefsIds.pop()
|
||||||
if (clientsStructRefsIds.length > 0) {
|
if (clientsStructRefsIds.length > 0) {
|
||||||
nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
|
nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
|
||||||
} else {
|
} else {
|
||||||
store.pendingClientsStructRefs.clear()
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,98 +250,115 @@ const resumeStructIntegration = (transaction, store) => {
|
|||||||
}
|
}
|
||||||
let curStructsTarget = getNextStructTarget()
|
let curStructsTarget = getNextStructTarget()
|
||||||
if (curStructsTarget === null && stack.length === 0) {
|
if (curStructsTarget === null && stack.length === 0) {
|
||||||
return
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {StructStore}
|
||||||
|
*/
|
||||||
|
const restStructs = new StructStore()
|
||||||
|
const missingSV = new Map()
|
||||||
|
/**
|
||||||
|
* @param {number} client
|
||||||
|
* @param {number} clock
|
||||||
|
*/
|
||||||
|
const updateMissingSv = (client, clock) => {
|
||||||
|
const mclock = missingSV.get(client)
|
||||||
|
if (mclock == null || mclock > clock) {
|
||||||
|
missingSV.set(client, clock)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @type {GC|Item}
|
* @type {GC|Item}
|
||||||
*/
|
*/
|
||||||
let stackHead = stack.length > 0
|
let stackHead = /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++]
|
||||||
? /** @type {GC|Item} */ (stack.pop())
|
|
||||||
: /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++]
|
|
||||||
// caching the state because it is used very often
|
// caching the state because it is used very often
|
||||||
const state = new Map()
|
const state = new Map()
|
||||||
|
|
||||||
|
const addStackToRestSS = () => {
|
||||||
|
for (const item of stack) {
|
||||||
|
const client = item.id.client
|
||||||
|
const unapplicableItems = clientsStructRefs.get(client)
|
||||||
|
if (unapplicableItems) {
|
||||||
|
// decrement because we weren't able to apply previous operation
|
||||||
|
unapplicableItems.i--
|
||||||
|
restStructs.clients.set(client, unapplicableItems.refs.slice(unapplicableItems.i))
|
||||||
|
clientsStructRefs.delete(client)
|
||||||
|
unapplicableItems.i = 0
|
||||||
|
unapplicableItems.refs = []
|
||||||
|
} else {
|
||||||
|
// item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue
|
||||||
|
restStructs.clients.set(client, [item])
|
||||||
|
}
|
||||||
|
// remove client from clientsStructRefsIds to prevent users from applying the same update again
|
||||||
|
clientsStructRefsIds = clientsStructRefsIds.filter(c => c !== client)
|
||||||
|
}
|
||||||
|
stack.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
// iterate over all struct readers until we are done
|
// iterate over all struct readers until we are done
|
||||||
while (true) {
|
while (true) {
|
||||||
const localClock = map.setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client))
|
if (stackHead.constructor !== Skip) {
|
||||||
const offset = stackHead.id.clock < localClock ? localClock - stackHead.id.clock : 0
|
const localClock = map.setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client))
|
||||||
if (stackHead.id.clock + offset !== localClock) {
|
const offset = localClock - stackHead.id.clock
|
||||||
// A previous message from this client is missing
|
if (offset < 0) {
|
||||||
// check if there is a pending structRef with a smaller clock and switch them
|
// update from the same client is missing
|
||||||
/**
|
|
||||||
* @type {{ refs: Array<GC|Item>, i: number }}
|
|
||||||
*/
|
|
||||||
const structRefs = clientsStructRefs.get(stackHead.id.client) || { refs: [], i: 0 }
|
|
||||||
if (structRefs.refs.length !== structRefs.i) {
|
|
||||||
const r = structRefs.refs[structRefs.i]
|
|
||||||
if (r.id.clock < stackHead.id.clock) {
|
|
||||||
// put ref with smaller clock on stack instead and continue
|
|
||||||
structRefs.refs[structRefs.i] = stackHead
|
|
||||||
stackHead = r
|
|
||||||
// sort the set because this approach might bring the list out of order
|
|
||||||
structRefs.refs = structRefs.refs.slice(structRefs.i).sort((r1, r2) => r1.id.clock - r2.id.clock)
|
|
||||||
structRefs.i = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// wait until missing struct is available
|
|
||||||
stack.push(stackHead)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const missing = stackHead.getMissing(transaction, store)
|
|
||||||
if (missing === null) {
|
|
||||||
if (offset === 0 || offset < stackHead.length) {
|
|
||||||
stackHead.integrate(transaction, offset)
|
|
||||||
state.set(stackHead.id.client, stackHead.id.clock + stackHead.length)
|
|
||||||
}
|
|
||||||
// iterate to next stackHead
|
|
||||||
if (stack.length > 0) {
|
|
||||||
stackHead = /** @type {GC|Item} */ (stack.pop())
|
|
||||||
} else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) {
|
|
||||||
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++])
|
|
||||||
} else {
|
|
||||||
curStructsTarget = getNextStructTarget()
|
|
||||||
if (curStructsTarget === null) {
|
|
||||||
// we are done!
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// get the struct reader that has the missing struct
|
|
||||||
/**
|
|
||||||
* @type {{ refs: Array<GC|Item>, i: number }}
|
|
||||||
*/
|
|
||||||
const structRefs = clientsStructRefs.get(missing) || { refs: [], i: 0 }
|
|
||||||
if (structRefs.refs.length === structRefs.i) {
|
|
||||||
// This update message causally depends on another update message.
|
|
||||||
stack.push(stackHead)
|
stack.push(stackHead)
|
||||||
return
|
updateMissingSv(stackHead.id.client, stackHead.id.clock - 1)
|
||||||
|
// hid a dead wall, add all items from stack to restSS
|
||||||
|
addStackToRestSS()
|
||||||
|
} else {
|
||||||
|
const missing = stackHead.getMissing(transaction, store)
|
||||||
|
if (missing !== null) {
|
||||||
|
stack.push(stackHead)
|
||||||
|
// get the struct reader that has the missing struct
|
||||||
|
/**
|
||||||
|
* @type {{ refs: Array<GC|Item>, i: number }}
|
||||||
|
*/
|
||||||
|
const structRefs = clientsStructRefs.get(/** @type {number} */ (missing)) || { refs: [], i: 0 }
|
||||||
|
if (structRefs.refs.length === structRefs.i) {
|
||||||
|
// This update message causally depends on another update message that doesn't exist yet
|
||||||
|
updateMissingSv(/** @type {number} */ (missing), getState(store, missing))
|
||||||
|
addStackToRestSS()
|
||||||
|
} else {
|
||||||
|
stackHead = structRefs.refs[structRefs.i++]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if (offset === 0 || offset < stackHead.length) {
|
||||||
|
// all fine, apply the stackhead
|
||||||
|
stackHead.integrate(transaction, offset)
|
||||||
|
state.set(stackHead.id.client, stackHead.id.clock + stackHead.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// iterate to next stackHead
|
||||||
|
if (stack.length > 0) {
|
||||||
|
stackHead = /** @type {GC|Item} */ (stack.pop())
|
||||||
|
} else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) {
|
||||||
|
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++])
|
||||||
|
} else {
|
||||||
|
curStructsTarget = getNextStructTarget()
|
||||||
|
if (curStructsTarget === null) {
|
||||||
|
// we are done!
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++])
|
||||||
}
|
}
|
||||||
stack.push(stackHead)
|
|
||||||
stackHead = structRefs.refs[structRefs.i++]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
store.pendingClientsStructRefs.clear()
|
if (restStructs.clients.size > 0) {
|
||||||
}
|
const encoder = new UpdateEncoderV2()
|
||||||
|
writeClientsStructs(encoder, restStructs, new Map())
|
||||||
/**
|
// write empty deleteset
|
||||||
* @param {Transaction} transaction
|
// writeDeleteSet(encoder, new DeleteSet())
|
||||||
* @param {StructStore} store
|
encoding.writeVarUint(encoder.restEncoder, 0) // => no need for an extra function call, just write 0 deletes
|
||||||
*
|
return { missing: missingSV, update: encoder.toUint8Array() }
|
||||||
* @private
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
export const tryResumePendingDeleteReaders = (transaction, store) => {
|
|
||||||
const pendingReaders = store.pendingDeleteReaders
|
|
||||||
store.pendingDeleteReaders = []
|
|
||||||
for (let i = 0; i < pendingReaders.length; i++) {
|
|
||||||
readAndApplyDeleteSet(pendingReaders[i], transaction, store)
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
@@ -344,78 +366,6 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
|
|||||||
*/
|
*/
|
||||||
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
|
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {StructStore} store
|
|
||||||
* @param {Map<number, Array<GC|Item>>} clientsStructsRefs
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
const mergeReadStructsIntoPendingReads = (store, clientsStructsRefs) => {
|
|
||||||
const pendingClientsStructRefs = store.pendingClientsStructRefs
|
|
||||||
clientsStructsRefs.forEach((structRefs, client) => {
|
|
||||||
const pendingStructRefs = pendingClientsStructRefs.get(client)
|
|
||||||
if (pendingStructRefs === undefined) {
|
|
||||||
pendingClientsStructRefs.set(client, { refs: structRefs, i: 0 })
|
|
||||||
} else {
|
|
||||||
// merge into existing structRefs
|
|
||||||
const merged = pendingStructRefs.i > 0 ? pendingStructRefs.refs.slice(pendingStructRefs.i) : pendingStructRefs.refs
|
|
||||||
for (let i = 0; i < structRefs.length; i++) {
|
|
||||||
merged.push(structRefs[i])
|
|
||||||
}
|
|
||||||
pendingStructRefs.i = 0
|
|
||||||
pendingStructRefs.refs = merged.sort((r1, r2) => r1.id.clock - r2.id.clock)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Map<number,{refs:Array<GC|Item>,i:number}>} pendingClientsStructRefs
|
|
||||||
*/
|
|
||||||
const cleanupPendingStructs = pendingClientsStructRefs => {
|
|
||||||
// cleanup pendingClientsStructs if not fully finished
|
|
||||||
pendingClientsStructRefs.forEach((refs, client) => {
|
|
||||||
if (refs.i === refs.refs.length) {
|
|
||||||
pendingClientsStructRefs.delete(client)
|
|
||||||
} else {
|
|
||||||
refs.refs.splice(0, refs.i)
|
|
||||||
refs.i = 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the next Item in a Decoder and fill this Item with the read data.
|
|
||||||
*
|
|
||||||
* This is called when data is received from a remote peer.
|
|
||||||
*
|
|
||||||
* @param {AbstractUpdateDecoder} decoder The decoder object to read data from.
|
|
||||||
* @param {Transaction} transaction
|
|
||||||
* @param {StructStore} store
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
export const readStructs = (decoder, transaction, store) => {
|
|
||||||
const clientsStructRefs = new Map()
|
|
||||||
// let start = performance.now()
|
|
||||||
readClientsStructRefs(decoder, clientsStructRefs, transaction.doc)
|
|
||||||
// console.log('time to read structs: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
mergeReadStructsIntoPendingReads(store, clientsStructRefs)
|
|
||||||
// console.log('time to merge: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
resumeStructIntegration(transaction, store)
|
|
||||||
// console.log('time to integrate: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
cleanupPendingStructs(store.pendingClientsStructRefs)
|
|
||||||
// console.log('time to cleanup: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
tryResumePendingDeleteReaders(transaction, store)
|
|
||||||
// console.log('time to resume delete readers: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read and apply a document update.
|
* Read and apply a document update.
|
||||||
*
|
*
|
||||||
@@ -424,14 +374,75 @@ export const readStructs = (decoder, transaction, store) => {
|
|||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
* @param {Doc} ydoc
|
* @param {Doc} ydoc
|
||||||
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||||
* @param {AbstractUpdateDecoder} [structDecoder]
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} [structDecoder]
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
|
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
|
||||||
transact(ydoc, transaction => {
|
transact(ydoc, transaction => {
|
||||||
readStructs(structDecoder, transaction, ydoc.store)
|
let retry = false
|
||||||
readAndApplyDeleteSet(structDecoder, transaction, ydoc.store)
|
const doc = transaction.doc
|
||||||
|
const store = doc.store
|
||||||
|
// let start = performance.now()
|
||||||
|
const ss = readClientsStructRefs(structDecoder, doc)
|
||||||
|
// console.log('time to read structs: ', performance.now() - start) // @todo remove
|
||||||
|
// start = performance.now()
|
||||||
|
// console.log('time to merge: ', performance.now() - start) // @todo remove
|
||||||
|
// start = performance.now()
|
||||||
|
const restStructs = integrateStructs(transaction, store, ss)
|
||||||
|
const pending = store.pendingStructs
|
||||||
|
if (pending) {
|
||||||
|
// check if we can apply something
|
||||||
|
for (const [client, clock] of pending.missing) {
|
||||||
|
if (clock < getState(store, client)) {
|
||||||
|
retry = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (restStructs) {
|
||||||
|
// merge restStructs into store.pending
|
||||||
|
for (const [client, clock] of restStructs.missing) {
|
||||||
|
const mclock = pending.missing.get(client)
|
||||||
|
if (mclock == null || mclock > clock) {
|
||||||
|
pending.missing.set(client, clock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending.update = mergeUpdatesV2([pending.update, restStructs.update])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
store.pendingStructs = restStructs
|
||||||
|
}
|
||||||
|
// console.log('time to integrate: ', performance.now() - start) // @todo remove
|
||||||
|
// start = performance.now()
|
||||||
|
const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store)
|
||||||
|
if (store.pendingDs) {
|
||||||
|
// @todo we could make a lower-bound state-vector check as we do above
|
||||||
|
const pendingDSUpdate = new UpdateDecoderV2(decoding.createDecoder(store.pendingDs))
|
||||||
|
decoding.readVarUint(pendingDSUpdate.restDecoder) // read 0 structs, because we only encode deletes in pendingdsupdate
|
||||||
|
const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store)
|
||||||
|
if (dsRest && dsRest2) {
|
||||||
|
// case 1: ds1 != null && ds2 != null
|
||||||
|
store.pendingDs = mergeUpdatesV2([dsRest, dsRest2])
|
||||||
|
} else {
|
||||||
|
// case 2: ds1 != null
|
||||||
|
// case 3: ds2 != null
|
||||||
|
// case 4: ds1 == null && ds2 == null
|
||||||
|
store.pendingDs = dsRest || dsRest2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Either dsRest == null && pendingDs == null OR dsRest != null
|
||||||
|
store.pendingDs = dsRest
|
||||||
|
}
|
||||||
|
// console.log('time to cleanup: ', performance.now() - start) // @todo remove
|
||||||
|
// start = performance.now()
|
||||||
|
|
||||||
|
// console.log('time to resume delete readers: ', performance.now() - start) // @todo remove
|
||||||
|
// start = performance.now()
|
||||||
|
if (retry) {
|
||||||
|
const update = /** @type {{update: Uint8Array}} */ (store.pendingStructs).update
|
||||||
|
store.pendingStructs = null
|
||||||
|
applyUpdateV2(transaction.doc, update)
|
||||||
|
}
|
||||||
}, transactionOrigin, false)
|
}, transactionOrigin, false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -445,7 +456,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
|
|||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const readUpdate = (decoder, ydoc, transactionOrigin) => readUpdateV2(decoder, ydoc, transactionOrigin, new DefaultUpdateDecoder(decoder))
|
export const readUpdate = (decoder, ydoc, transactionOrigin) => readUpdateV2(decoder, ydoc, transactionOrigin, new UpdateDecoderV1(decoder))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
|
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
|
||||||
@@ -475,13 +486,13 @@ export const applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = Update
|
|||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, DefaultUpdateDecoder)
|
export const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
|
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
|
||||||
* only write the operations that are missing.
|
* only write the operations that are missing.
|
||||||
*
|
*
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||||
*
|
*
|
||||||
@@ -500,15 +511,29 @@ export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map())
|
|||||||
*
|
*
|
||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||||
* @param {AbstractUpdateEncoder} [encoder]
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} [encoder]
|
||||||
* @return {Uint8Array}
|
* @return {Uint8Array}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector, encoder = new UpdateEncoderV2()) => {
|
export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => {
|
||||||
const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
|
const targetStateVector = decodeStateVector(encodedTargetStateVector)
|
||||||
writeStateAsUpdate(encoder, doc, targetStateVector)
|
writeStateAsUpdate(encoder, doc, targetStateVector)
|
||||||
return encoder.toUint8Array()
|
const updates = [encoder.toUint8Array()]
|
||||||
|
// also add the pending updates (if there are any)
|
||||||
|
// @todo support diffirent encoders
|
||||||
|
if (encoder.constructor === UpdateEncoderV2) {
|
||||||
|
if (doc.store.pendingDs) {
|
||||||
|
updates.push(doc.store.pendingDs)
|
||||||
|
}
|
||||||
|
if (doc.store.pendingStructs) {
|
||||||
|
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
|
||||||
|
}
|
||||||
|
if (updates.length > 1) {
|
||||||
|
return mergeUpdatesV2(updates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updates[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -523,12 +548,12 @@ export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector, encoder = n
|
|||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new DefaultUpdateEncoder())
|
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new UpdateEncoderV1())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read state vector from Decoder and return as Map
|
* Read state vector from Decoder and return as Map
|
||||||
*
|
*
|
||||||
* @param {AbstractDSDecoder} decoder
|
* @param {DSDecoderV1 | DSDecoderV2} decoder
|
||||||
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
@@ -552,7 +577,7 @@ export const readStateVector = decoder => {
|
|||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState)))
|
// export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState)))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read decodedState and return State as Map.
|
* Read decodedState and return State as Map.
|
||||||
@@ -562,10 +587,10 @@ export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoder
|
|||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const decodeStateVector = decodedState => readStateVector(new DefaultDSDecoder(decoding.createDecoder(decodedState)))
|
export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1(decoding.createDecoder(decodedState)))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractDSEncoder} encoder
|
* @param {DSEncoderV1 | DSEncoderV2} encoder
|
||||||
* @param {Map<number,number>} sv
|
* @param {Map<number,number>} sv
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
@@ -579,7 +604,7 @@ export const writeStateVector = (encoder, sv) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractDSEncoder} encoder
|
* @param {DSEncoderV1 | DSEncoderV2} encoder
|
||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
@@ -589,23 +614,27 @@ export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encod
|
|||||||
/**
|
/**
|
||||||
* Encode State as Uint8Array.
|
* Encode State as Uint8Array.
|
||||||
*
|
*
|
||||||
* @param {Doc} doc
|
* @param {Doc|Map<number,number>} doc
|
||||||
* @param {AbstractDSEncoder} [encoder]
|
* @param {DSEncoderV1 | DSEncoderV2} [encoder]
|
||||||
* @return {Uint8Array}
|
* @return {Uint8Array}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => {
|
export const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => {
|
||||||
writeDocumentStateVector(encoder, doc)
|
if (doc instanceof Map) {
|
||||||
|
writeStateVector(encoder, doc)
|
||||||
|
} else {
|
||||||
|
writeDocumentStateVector(encoder, doc)
|
||||||
|
}
|
||||||
return encoder.toUint8Array()
|
return encoder.toUint8Array()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode State as Uint8Array.
|
* Encode State as Uint8Array.
|
||||||
*
|
*
|
||||||
* @param {Doc} doc
|
* @param {Doc|Map<number,number>} doc
|
||||||
* @return {Uint8Array}
|
* @return {Uint8Array}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const encodeStateVector = doc => encodeStateVectorV2(doc, new DefaultDSEncoder())
|
export const encodeStateVector = doc => encodeStateVectorV2(doc, new DSEncoderV1())
|
||||||
|
|||||||
510
src/utils/updates.js
Normal file
510
src/utils/updates.js
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
|
||||||
|
import * as binary from 'lib0/binary.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as logging from 'lib0/logging.js'
|
||||||
|
import * as math from 'lib0/math.js'
|
||||||
|
import {
|
||||||
|
createID,
|
||||||
|
readItemContent,
|
||||||
|
readDeleteSet,
|
||||||
|
writeDeleteSet,
|
||||||
|
Skip,
|
||||||
|
mergeDeleteSets,
|
||||||
|
DSEncoderV1,
|
||||||
|
DSEncoderV2,
|
||||||
|
decodeStateVector,
|
||||||
|
Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
|
*/
|
||||||
|
function * lazyStructReaderGenerator (decoder) {
|
||||||
|
const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder)
|
||||||
|
for (let i = 0; i < numOfStateUpdates; i++) {
|
||||||
|
const numberOfStructs = decoding.readVarUint(decoder.restDecoder)
|
||||||
|
const client = decoder.readClient()
|
||||||
|
let clock = decoding.readVarUint(decoder.restDecoder)
|
||||||
|
for (let i = 0; i < numberOfStructs; i++) {
|
||||||
|
const info = decoder.readInfo()
|
||||||
|
// @todo use switch instead of ifs
|
||||||
|
if (info === 10) {
|
||||||
|
const len = decoding.readVarUint(decoder.restDecoder)
|
||||||
|
yield new Skip(createID(client, clock), len)
|
||||||
|
clock += len
|
||||||
|
} else if ((binary.BITS5 & info) !== 0) {
|
||||||
|
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
||||||
|
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
||||||
|
// and we read the next string as parentYKey.
|
||||||
|
// It indicates how we store/retrieve parent from `y.share`
|
||||||
|
// @type {string|null}
|
||||||
|
const struct = new Item(
|
||||||
|
createID(client, clock),
|
||||||
|
null, // left
|
||||||
|
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
|
||||||
|
null, // right
|
||||||
|
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
|
||||||
|
// @ts-ignore Force writing a string here.
|
||||||
|
cantCopyParentInfo ? (decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID()) : null, // parent
|
||||||
|
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
|
||||||
|
readItemContent(decoder, info) // item content
|
||||||
|
)
|
||||||
|
yield struct
|
||||||
|
clock += struct.length
|
||||||
|
} else {
|
||||||
|
const len = decoder.readLen()
|
||||||
|
yield new GC(createID(client, clock), len)
|
||||||
|
clock += len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LazyStructReader {
|
||||||
|
/**
|
||||||
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
|
* @param {boolean} filterSkips
|
||||||
|
*/
|
||||||
|
constructor (decoder, filterSkips) {
|
||||||
|
this.gen = lazyStructReaderGenerator(decoder)
|
||||||
|
/**
|
||||||
|
* @type {null | Item | Skip | GC}
|
||||||
|
*/
|
||||||
|
this.curr = null
|
||||||
|
this.done = false
|
||||||
|
this.filterSkips = filterSkips
|
||||||
|
this.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Item | GC | Skip |null}
|
||||||
|
*/
|
||||||
|
next () {
|
||||||
|
// ignore "Skip" structs
|
||||||
|
do {
|
||||||
|
this.curr = this.gen.next().value || null
|
||||||
|
} while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip)
|
||||||
|
return this.curr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const logUpdate = update => logUpdateV2(update, UpdateDecoderV1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const logUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
|
||||||
|
const structs = []
|
||||||
|
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||||
|
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||||
|
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||||
|
structs.push(curr)
|
||||||
|
}
|
||||||
|
logging.print('Structs: ', structs)
|
||||||
|
const ds = readDeleteSet(updateDecoder)
|
||||||
|
logging.print('DeleteSet: ', ds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LazyStructWriter {
|
||||||
|
/**
|
||||||
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
|
*/
|
||||||
|
constructor (encoder) {
|
||||||
|
this.currClient = 0
|
||||||
|
this.startClock = 0
|
||||||
|
this.written = 0
|
||||||
|
this.encoder = encoder
|
||||||
|
/**
|
||||||
|
* We want to write operations lazily, but also we need to know beforehand how many operations we want to write for each client.
|
||||||
|
*
|
||||||
|
* This kind of meta-information (#clients, #structs-per-client-written) is written to the restEncoder.
|
||||||
|
*
|
||||||
|
* We fragment the restEncoder and store a slice of it per-client until we know how many clients there are.
|
||||||
|
* When we flush (toUint8Array) we write the restEncoder using the fragments and the meta-information.
|
||||||
|
*
|
||||||
|
* @type {Array<{ written: number, restEncoder: Uint8Array }>}
|
||||||
|
*/
|
||||||
|
this.clientStructs = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<Uint8Array>} updates
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {typeof DSEncoderV1 | typeof DSEncoderV2} YEncoder
|
||||||
|
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} YDecoder
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
|
||||||
|
const encoder = new YEncoder()
|
||||||
|
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), true)
|
||||||
|
let curr = updateDecoder.curr
|
||||||
|
if (curr !== null) {
|
||||||
|
let size = 1
|
||||||
|
let currClient = curr.id.client
|
||||||
|
let currClock = curr.id.clock
|
||||||
|
let stopCounting = false
|
||||||
|
for (; curr !== null; curr = updateDecoder.next()) {
|
||||||
|
if (currClient !== curr.id.client) {
|
||||||
|
size++
|
||||||
|
// We found a new client
|
||||||
|
// write what we have to the encoder
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, currClient)
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, currClock)
|
||||||
|
currClient = curr.id.client
|
||||||
|
stopCounting = false
|
||||||
|
}
|
||||||
|
if (curr.constructor === Skip) {
|
||||||
|
stopCounting = true
|
||||||
|
}
|
||||||
|
if (!stopCounting) {
|
||||||
|
currClock = curr.id.clock + curr.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// write what we have
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, currClient)
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, currClock)
|
||||||
|
// prepend the size of the state vector
|
||||||
|
const enc = encoding.createEncoder()
|
||||||
|
encoding.writeVarUint(enc, size)
|
||||||
|
encoding.writeBinaryEncoder(enc, encoder.restEncoder)
|
||||||
|
encoder.restEncoder = enc
|
||||||
|
return encoder.toUint8Array()
|
||||||
|
} else {
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, 0)
|
||||||
|
return encoder.toUint8Array()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export const encodeStateVectorFromUpdate = update => encodeStateVectorFromUpdateV2(update, DSEncoderV1, UpdateDecoderV1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} YDecoder
|
||||||
|
* @return {{ from: Map<number,number>, to: Map<number,number> }}
|
||||||
|
*/
|
||||||
|
export const parseUpdateMetaV2 = (update, YDecoder = UpdateDecoderV2) => {
|
||||||
|
/**
|
||||||
|
* @type {Map<number, number>}
|
||||||
|
*/
|
||||||
|
const from = new Map()
|
||||||
|
/**
|
||||||
|
* @type {Map<number, number>}
|
||||||
|
*/
|
||||||
|
const to = new Map()
|
||||||
|
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false)
|
||||||
|
let curr = updateDecoder.curr
|
||||||
|
if (curr !== null) {
|
||||||
|
let currClient = curr.id.client
|
||||||
|
let currClock = curr.id.clock
|
||||||
|
// write the beginning to `from`
|
||||||
|
from.set(currClient, currClock)
|
||||||
|
for (; curr !== null; curr = updateDecoder.next()) {
|
||||||
|
if (currClient !== curr.id.client) {
|
||||||
|
// We found a new client
|
||||||
|
// write the end to `to`
|
||||||
|
to.set(currClient, currClock)
|
||||||
|
// write the beginning to `from`
|
||||||
|
from.set(curr.id.client, curr.id.clock)
|
||||||
|
// update currClient
|
||||||
|
currClient = curr.id.client
|
||||||
|
}
|
||||||
|
currClock = curr.id.clock + curr.length
|
||||||
|
}
|
||||||
|
// write the end to `to`
|
||||||
|
to.set(currClient, currClock)
|
||||||
|
}
|
||||||
|
return { from, to }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @return {{ from: Map<number,number>, to: Map<number,number> }}
|
||||||
|
*/
|
||||||
|
export const parseUpdateMeta = update => parseUpdateMetaV2(update, UpdateDecoderV1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is intended to slice any kind of struct and retrieve the right part.
|
||||||
|
* It does not handle side-effects, so it should only be used by the lazy-encoder.
|
||||||
|
*
|
||||||
|
* @param {Item | GC | Skip} left
|
||||||
|
* @param {number} diff
|
||||||
|
* @return {Item | GC}
|
||||||
|
*/
|
||||||
|
const sliceStruct = (left, diff) => {
|
||||||
|
if (left.constructor === GC) {
|
||||||
|
const { client, clock } = left.id
|
||||||
|
return new GC(createID(client, clock + diff), left.length - diff)
|
||||||
|
} else if (left.constructor === Skip) {
|
||||||
|
const { client, clock } = left.id
|
||||||
|
return new Skip(createID(client, clock + diff), left.length - diff)
|
||||||
|
} else {
|
||||||
|
const leftItem = /** @type {Item} */ (left)
|
||||||
|
const { client, clock } = leftItem.id
|
||||||
|
return new Item(
|
||||||
|
createID(client, clock + diff),
|
||||||
|
null,
|
||||||
|
createID(client, clock + diff - 1),
|
||||||
|
null,
|
||||||
|
leftItem.rightOrigin,
|
||||||
|
leftItem.parent,
|
||||||
|
leftItem.parentSub,
|
||||||
|
leftItem.content.splice(diff)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* This function works similarly to `readUpdateV2`.
|
||||||
|
*
|
||||||
|
* @param {Array<Uint8Array>} updates
|
||||||
|
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
|
||||||
|
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
|
||||||
|
const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
|
||||||
|
let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo we don't need offset because we always slice before
|
||||||
|
* @type {null | { struct: Item | GC | Skip, offset: number }}
|
||||||
|
*/
|
||||||
|
let currWrite = null
|
||||||
|
|
||||||
|
const updateEncoder = new YEncoder()
|
||||||
|
// write structs lazily
|
||||||
|
const lazyStructEncoder = new LazyStructWriter(updateEncoder)
|
||||||
|
|
||||||
|
// Note: We need to ensure that all lazyStructDecoders are fully consumed
|
||||||
|
// Note: Should merge document updates whenever possible - even from different updates
|
||||||
|
// Note: Should handle that some operations cannot be applied yet ()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Write higher clients first ⇒ sort by clientID & clock and remove decoders without content
|
||||||
|
lazyStructDecoders = lazyStructDecoders.filter(dec => dec.curr !== null)
|
||||||
|
lazyStructDecoders.sort(
|
||||||
|
/** @type {function(any,any):number} */ (dec1, dec2) => {
|
||||||
|
if (dec1.curr.id.client === dec2.curr.id.client) {
|
||||||
|
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock
|
||||||
|
if (clockDiff === 0) {
|
||||||
|
return dec1.curr.constructor === dec2.curr.constructor ? 0 : (
|
||||||
|
dec1.curr.constructor === Skip ? 1 : -1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return clockDiff
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return dec2.curr.id.client - dec1.curr.id.client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (lazyStructDecoders.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const currDecoder = lazyStructDecoders[0]
|
||||||
|
// write from currDecoder until the next operation is from another client or if filler-struct
|
||||||
|
// then we need to reorder the decoders and find the next operation to write
|
||||||
|
const firstClient = /** @type {Item | GC} */ (currDecoder.curr).id.client
|
||||||
|
|
||||||
|
if (currWrite !== null) {
|
||||||
|
let curr = /** @type {Item | GC | null} */ (currDecoder.curr)
|
||||||
|
|
||||||
|
// iterate until we find something that we haven't written already
|
||||||
|
// remember: first the high client-ids are written
|
||||||
|
while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) {
|
||||||
|
curr = currDecoder.next()
|
||||||
|
}
|
||||||
|
if (curr === null || curr.id.client !== firstClient) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstClient !== currWrite.struct.id.client) {
|
||||||
|
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
||||||
|
currWrite = { struct: curr, offset: 0 }
|
||||||
|
currDecoder.next()
|
||||||
|
} else {
|
||||||
|
if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) {
|
||||||
|
// @todo write currStruct & set currStruct = Skip(clock = currStruct.id.clock + currStruct.length, length = curr.id.clock - self.clock)
|
||||||
|
if (currWrite.struct.constructor === Skip) {
|
||||||
|
// extend existing skip
|
||||||
|
currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock
|
||||||
|
} else {
|
||||||
|
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
||||||
|
const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length
|
||||||
|
/**
|
||||||
|
* @type {Skip}
|
||||||
|
*/
|
||||||
|
const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff)
|
||||||
|
currWrite = { struct, offset: 0 }
|
||||||
|
}
|
||||||
|
} else { // if (currWrite.struct.id.clock + currWrite.struct.length >= curr.id.clock) {
|
||||||
|
const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock
|
||||||
|
if (diff > 0) {
|
||||||
|
if (currWrite.struct.constructor === Skip) {
|
||||||
|
// prefer to slice Skip because the other struct might contain more information
|
||||||
|
currWrite.struct.length -= diff
|
||||||
|
} else {
|
||||||
|
curr = sliceStruct(curr, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!currWrite.struct.mergeWith(/** @type {any} */ (curr))) {
|
||||||
|
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
||||||
|
currWrite = { struct: curr, offset: 0 }
|
||||||
|
currDecoder.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currWrite = { struct: /** @type {Item | GC} */ (currDecoder.curr), offset: 0 }
|
||||||
|
currDecoder.next()
|
||||||
|
}
|
||||||
|
for (
|
||||||
|
let next = currDecoder.curr;
|
||||||
|
next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip;
|
||||||
|
next = currDecoder.next()
|
||||||
|
) {
|
||||||
|
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
||||||
|
currWrite = { struct: next, offset: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currWrite !== null) {
|
||||||
|
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
||||||
|
currWrite = null
|
||||||
|
}
|
||||||
|
finishLazyStructWriting(lazyStructEncoder)
|
||||||
|
|
||||||
|
const dss = updateDecoders.map(decoder => readDeleteSet(decoder))
|
||||||
|
const ds = mergeDeleteSets(dss)
|
||||||
|
writeDeleteSet(updateEncoder, ds)
|
||||||
|
return updateEncoder.toUint8Array()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {Uint8Array} sv
|
||||||
|
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
|
||||||
|
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
|
||||||
|
*/
|
||||||
|
export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
|
||||||
|
const state = decodeStateVector(sv)
|
||||||
|
const encoder = new YEncoder()
|
||||||
|
const lazyStructWriter = new LazyStructWriter(encoder)
|
||||||
|
const decoder = new YDecoder(decoding.createDecoder(update))
|
||||||
|
const reader = new LazyStructReader(decoder, false)
|
||||||
|
while (reader.curr) {
|
||||||
|
const curr = reader.curr
|
||||||
|
const currClient = curr.id.client
|
||||||
|
const svClock = state.get(currClient) || 0
|
||||||
|
if (reader.curr.constructor === Skip) {
|
||||||
|
// the first written struct shouldn't be a skip
|
||||||
|
reader.next()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (curr.id.clock + curr.length > svClock) {
|
||||||
|
writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0))
|
||||||
|
reader.next()
|
||||||
|
while (reader.curr && reader.curr.id.client === currClient) {
|
||||||
|
writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0)
|
||||||
|
reader.next()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// read until something new comes up
|
||||||
|
while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) {
|
||||||
|
reader.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finishLazyStructWriting(lazyStructWriter)
|
||||||
|
// write ds
|
||||||
|
const ds = readDeleteSet(decoder)
|
||||||
|
writeDeleteSet(encoder, ds)
|
||||||
|
return encoder.toUint8Array()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {Uint8Array} sv
|
||||||
|
*/
|
||||||
|
export const diffUpdate = (update, sv) => diffUpdateV2(update, sv, UpdateDecoderV1, UpdateEncoderV1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {LazyStructWriter} lazyWriter
|
||||||
|
*/
|
||||||
|
const flushLazyStructWriter = lazyWriter => {
|
||||||
|
if (lazyWriter.written > 0) {
|
||||||
|
lazyWriter.clientStructs.push({ written: lazyWriter.written, restEncoder: encoding.toUint8Array(lazyWriter.encoder.restEncoder) })
|
||||||
|
lazyWriter.encoder.restEncoder = encoding.createEncoder()
|
||||||
|
lazyWriter.written = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {LazyStructWriter} lazyWriter
|
||||||
|
* @param {Item | GC} struct
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
const writeStructToLazyStructWriter = (lazyWriter, struct, offset) => {
|
||||||
|
// flush curr if we start another client
|
||||||
|
if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) {
|
||||||
|
flushLazyStructWriter(lazyWriter)
|
||||||
|
}
|
||||||
|
if (lazyWriter.written === 0) {
|
||||||
|
lazyWriter.currClient = struct.id.client
|
||||||
|
// write next client
|
||||||
|
lazyWriter.encoder.writeClient(struct.id.client)
|
||||||
|
// write startClock
|
||||||
|
encoding.writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset)
|
||||||
|
}
|
||||||
|
struct.write(lazyWriter.encoder, offset)
|
||||||
|
lazyWriter.written++
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Call this function when we collected all parts and want to
|
||||||
|
* put all the parts together. After calling this method,
|
||||||
|
* you can continue using the UpdateEncoder.
|
||||||
|
*
|
||||||
|
* @param {LazyStructWriter} lazyWriter
|
||||||
|
*/
|
||||||
|
const finishLazyStructWriting = (lazyWriter) => {
|
||||||
|
flushLazyStructWriter(lazyWriter)
|
||||||
|
|
||||||
|
// this is a fresh encoder because we called flushCurr
|
||||||
|
const restEncoder = lazyWriter.encoder.restEncoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now we put all the fragments together.
|
||||||
|
* This works similarly to `writeClientsStructs`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// write # states that were updated - i.e. the clients
|
||||||
|
encoding.writeVarUint(restEncoder, lazyWriter.clientStructs.length)
|
||||||
|
|
||||||
|
for (let i = 0; i < lazyWriter.clientStructs.length; i++) {
|
||||||
|
const partStructs = lazyWriter.clientStructs[i]
|
||||||
|
/**
|
||||||
|
* Works similarly to `writeStructs`
|
||||||
|
*/
|
||||||
|
// write # encoded structs
|
||||||
|
encoding.writeVarUint(restEncoder, partStructs.written)
|
||||||
|
// write the rest of the fragment
|
||||||
|
encoding.writeUint8Array(restEncoder, partStructs.restEncoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testStructReferences = tc => {
|
export const testStructReferences = tc => {
|
||||||
t.assert(contentRefs.length === 10)
|
t.assert(contentRefs.length === 11)
|
||||||
t.assert(contentRefs[1] === readContentDeleted)
|
t.assert(contentRefs[1] === readContentDeleted)
|
||||||
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
|
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
|
||||||
t.assert(contentRefs[3] === readContentBinary)
|
t.assert(contentRefs[3] === readContentBinary)
|
||||||
@@ -32,6 +32,7 @@ export const testStructReferences = tc => {
|
|||||||
t.assert(contentRefs[7] === readContentType)
|
t.assert(contentRefs[7] === readContentType)
|
||||||
t.assert(contentRefs[8] === readContentAny)
|
t.assert(contentRefs[8] === readContentAny)
|
||||||
t.assert(contentRefs[9] === readContentDoc)
|
t.assert(contentRefs[9] === readContentDoc)
|
||||||
|
// contentRefs[10] is reserved for Skip structs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import * as undoredo from './undo-redo.tests.js'
|
|||||||
import * as compatibility from './compatibility.tests.js'
|
import * as compatibility from './compatibility.tests.js'
|
||||||
import * as doc from './doc.tests.js'
|
import * as doc from './doc.tests.js'
|
||||||
import * as snapshot from './snapshot.tests.js'
|
import * as snapshot from './snapshot.tests.js'
|
||||||
|
import * as updates from './updates.tests.js'
|
||||||
|
import * as relativePositions from './relativePositions.tests.js'
|
||||||
|
|
||||||
import { runTests } from 'lib0/testing.js'
|
import { runTests } from 'lib0/testing.js'
|
||||||
import { isBrowser, isNode } from 'lib0/environment.js'
|
import { isBrowser, isNode } from 'lib0/environment.js'
|
||||||
@@ -17,7 +19,7 @@ if (isBrowser) {
|
|||||||
log.createVConsole(document.body)
|
log.createVConsole(document.body)
|
||||||
}
|
}
|
||||||
runTests({
|
runTests({
|
||||||
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot
|
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
|
||||||
}).then(success => {
|
}).then(success => {
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
|
|||||||
104
tests/relativePositions.tests.js
Normal file
104
tests/relativePositions.tests.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
import * as Y from '../src/internals'
|
||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Y.YText} ytext
|
||||||
|
*/
|
||||||
|
const checkRelativePositions = ytext => {
|
||||||
|
// test if all positions are encoded and restored correctly
|
||||||
|
for (let i = 0; i < ytext.length; i++) {
|
||||||
|
// for all types of associations..
|
||||||
|
for (let assoc = -1; assoc < 2; assoc++) {
|
||||||
|
const rpos = Y.createRelativePositionFromTypeIndex(ytext, i, assoc)
|
||||||
|
const encodedRpos = Y.encodeRelativePosition(rpos)
|
||||||
|
const decodedRpos = Y.decodeRelativePosition(encodedRpos)
|
||||||
|
const absPos = /** @type {Y.AbsolutePosition} */ (Y.createAbsolutePositionFromRelativePosition(decodedRpos, /** @type {Y.Doc} */ (ytext.doc)))
|
||||||
|
t.assert(absPos.index === i)
|
||||||
|
t.assert(absPos.assoc === assoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRelativePositionCase1 = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText()
|
||||||
|
ytext.insert(0, '1')
|
||||||
|
ytext.insert(0, 'abc')
|
||||||
|
ytext.insert(0, 'z')
|
||||||
|
ytext.insert(0, 'y')
|
||||||
|
ytext.insert(0, 'x')
|
||||||
|
checkRelativePositions(ytext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRelativePositionCase2 = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText()
|
||||||
|
ytext.insert(0, 'abc')
|
||||||
|
checkRelativePositions(ytext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRelativePositionCase3 = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText()
|
||||||
|
ytext.insert(0, 'abc')
|
||||||
|
ytext.insert(0, '1')
|
||||||
|
ytext.insert(0, 'xyz')
|
||||||
|
checkRelativePositions(ytext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRelativePositionCase4 = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText()
|
||||||
|
ytext.insert(0, '1')
|
||||||
|
checkRelativePositions(ytext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRelativePositionCase5 = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText()
|
||||||
|
ytext.insert(0, '2')
|
||||||
|
ytext.insert(0, '1')
|
||||||
|
checkRelativePositions(ytext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRelativePositionCase6 = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText()
|
||||||
|
checkRelativePositions(ytext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRelativePositionAssociationDifference = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText()
|
||||||
|
ytext.insert(0, '2')
|
||||||
|
ytext.insert(0, '1')
|
||||||
|
const rposRight = Y.createRelativePositionFromTypeIndex(ytext, 1, 0)
|
||||||
|
const rposLeft = Y.createRelativePositionFromTypeIndex(ytext, 1, -1)
|
||||||
|
ytext.insert(1, 'x')
|
||||||
|
const posRight = Y.createAbsolutePositionFromRelativePosition(rposRight, ydoc)
|
||||||
|
const posLeft = Y.createAbsolutePositionFromRelativePosition(rposLeft, ydoc)
|
||||||
|
t.assert(posRight != null && posRight.index === 2)
|
||||||
|
t.assert(posLeft != null && posLeft.index === 1)
|
||||||
|
}
|
||||||
@@ -27,6 +27,39 @@ const broadcastMessage = (y, m) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let useV2 = false
|
||||||
|
|
||||||
|
export const encV1 = {
|
||||||
|
encodeStateAsUpdate: Y.encodeStateAsUpdate,
|
||||||
|
mergeUpdates: Y.mergeUpdates,
|
||||||
|
applyUpdate: Y.applyUpdate,
|
||||||
|
logUpdate: Y.logUpdate,
|
||||||
|
updateEventName: 'update',
|
||||||
|
diffUpdate: Y.diffUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
export const encV2 = {
|
||||||
|
encodeStateAsUpdate: Y.encodeStateAsUpdateV2,
|
||||||
|
mergeUpdates: Y.mergeUpdatesV2,
|
||||||
|
applyUpdate: Y.applyUpdateV2,
|
||||||
|
logUpdate: Y.logUpdateV2,
|
||||||
|
updateEventName: 'updateV2',
|
||||||
|
diffUpdate: Y.diffUpdateV2
|
||||||
|
}
|
||||||
|
|
||||||
|
export let enc = encV1
|
||||||
|
|
||||||
|
const useV1Encoding = () => {
|
||||||
|
useV2 = false
|
||||||
|
enc = encV1
|
||||||
|
}
|
||||||
|
|
||||||
|
const useV2Encoding = () => {
|
||||||
|
console.error('sync protocol doesnt support v2 protocol yet, fallback to v1 encoding') // @Todo
|
||||||
|
useV2 = false
|
||||||
|
enc = encV1
|
||||||
|
}
|
||||||
|
|
||||||
export class TestYInstance extends Y.Doc {
|
export class TestYInstance extends Y.Doc {
|
||||||
/**
|
/**
|
||||||
* @param {TestConnector} testConnector
|
* @param {TestConnector} testConnector
|
||||||
@@ -44,12 +77,19 @@ export class TestYInstance extends Y.Doc {
|
|||||||
*/
|
*/
|
||||||
this.receiving = new Map()
|
this.receiving = new Map()
|
||||||
testConnector.allConns.add(this)
|
testConnector.allConns.add(this)
|
||||||
|
/**
|
||||||
|
* The list of received updates.
|
||||||
|
* We are going to merge them later using Y.mergeUpdates and check if the resulting document is correct.
|
||||||
|
* @type {Array<Uint8Array>}
|
||||||
|
*/
|
||||||
|
this.updates = []
|
||||||
// set up observe on local model
|
// set up observe on local model
|
||||||
this.on('update', /** @param {Uint8Array} update @param {any} origin */ (update, origin) => {
|
this.on(enc.updateEventName, /** @param {Uint8Array} update @param {any} origin */ (update, origin) => {
|
||||||
if (origin !== testConnector) {
|
if (origin !== testConnector) {
|
||||||
const encoder = encoding.createEncoder()
|
const encoder = encoding.createEncoder()
|
||||||
syncProtocol.writeUpdate(encoder, update)
|
syncProtocol.writeUpdate(encoder, update)
|
||||||
broadcastMessage(this, encoding.toUint8Array(encoder))
|
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||||
|
this.updates.push(update)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.connect()
|
this.connect()
|
||||||
@@ -162,6 +202,17 @@ export class TestConnector {
|
|||||||
// send reply message
|
// send reply message
|
||||||
sender._receive(encoding.toUint8Array(encoder), receiver)
|
sender._receive(encoding.toUint8Array(encoder), receiver)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
// If update message, add the received message to the list of received messages
|
||||||
|
const decoder = decoding.createDecoder(m)
|
||||||
|
const messageType = decoding.readVarUint(decoder)
|
||||||
|
switch (messageType) {
|
||||||
|
case syncProtocol.messageYjsUpdate:
|
||||||
|
case syncProtocol.messageYjsSyncStep2:
|
||||||
|
receiver.updates.push(decoding.readVarUint8Array(decoder))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -240,9 +291,9 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
|||||||
const gen = tc.prng
|
const gen = tc.prng
|
||||||
// choose an encoding approach at random
|
// choose an encoding approach at random
|
||||||
if (prng.bool(gen)) {
|
if (prng.bool(gen)) {
|
||||||
Y.useV2Encoding()
|
useV2Encoding()
|
||||||
} else {
|
} else {
|
||||||
Y.useV1Encoding()
|
useV1Encoding()
|
||||||
}
|
}
|
||||||
|
|
||||||
const testConnector = new TestConnector(gen)
|
const testConnector = new TestConnector(gen)
|
||||||
@@ -258,7 +309,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
|||||||
}
|
}
|
||||||
testConnector.syncAll()
|
testConnector.syncAll()
|
||||||
result.testObjects = result.users.map(initTestObject || (() => null))
|
result.testObjects = result.users.map(initTestObject || (() => null))
|
||||||
Y.useV1Encoding()
|
useV1Encoding()
|
||||||
return /** @type {any} */ (result)
|
return /** @type {any} */ (result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,14 +325,21 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
|||||||
export const compare = users => {
|
export const compare = users => {
|
||||||
users.forEach(u => u.connect())
|
users.forEach(u => u.connect())
|
||||||
while (users[0].tc.flushAllMessages()) {}
|
while (users[0].tc.flushAllMessages()) {}
|
||||||
|
// For each document, merge all received document updates with Y.mergeUpdates and create a new document which will be added to the list of "users"
|
||||||
|
// This ensures that mergeUpdates works correctly
|
||||||
|
const mergedDocs = users.map(user => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
enc.applyUpdate(ydoc, enc.mergeUpdates(user.updates))
|
||||||
|
return ydoc
|
||||||
|
})
|
||||||
|
users.push(.../** @type {any} */(mergedDocs))
|
||||||
const userArrayValues = users.map(u => u.getArray('array').toJSON())
|
const userArrayValues = users.map(u => u.getArray('array').toJSON())
|
||||||
const userMapValues = users.map(u => u.getMap('map').toJSON())
|
const userMapValues = users.map(u => u.getMap('map').toJSON())
|
||||||
const userXmlValues = users.map(u => u.get('xml', Y.YXmlElement).toString())
|
const userXmlValues = users.map(u => u.get('xml', Y.YXmlElement).toString())
|
||||||
const userTextValues = users.map(u => u.getText('text').toDelta())
|
const userTextValues = users.map(u => u.getText('text').toDelta())
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
t.assert(u.store.pendingDeleteReaders.length === 0)
|
t.assert(u.store.pendingDs === null)
|
||||||
t.assert(u.store.pendingStack.length === 0)
|
t.assert(u.store.pendingStructs === null)
|
||||||
t.assert(u.store.pendingClientsStructRefs.size === 0)
|
|
||||||
}
|
}
|
||||||
// Test Array iterator
|
// Test Array iterator
|
||||||
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
|
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
|
||||||
|
|||||||
@@ -201,10 +201,12 @@ export const testUndoEvents = tc => {
|
|||||||
let receivedMetadata = -1
|
let receivedMetadata = -1
|
||||||
undoManager.on('stack-item-added', /** @param {any} event */ event => {
|
undoManager.on('stack-item-added', /** @param {any} event */ event => {
|
||||||
t.assert(event.type != null)
|
t.assert(event.type != null)
|
||||||
|
t.assert(event.changedParentTypes != null && event.changedParentTypes.has(text0))
|
||||||
event.stackItem.meta.set('test', counter++)
|
event.stackItem.meta.set('test', counter++)
|
||||||
})
|
})
|
||||||
undoManager.on('stack-item-popped', /** @param {any} event */ event => {
|
undoManager.on('stack-item-popped', /** @param {any} event */ event => {
|
||||||
t.assert(event.type != null)
|
t.assert(event.type != null)
|
||||||
|
t.assert(event.changedParentTypes != null && event.changedParentTypes.has(text0))
|
||||||
receivedMetadata = event.stackItem.meta.get('test')
|
receivedMetadata = event.stackItem.meta.get('test')
|
||||||
})
|
})
|
||||||
text0.insert(0, 'abc')
|
text0.insert(0, 'abc')
|
||||||
|
|||||||
246
tests/updates.tests.js
Normal file
246
tests/updates.tests.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
import { init, compare } from './testHelper.js' // eslint-disable-line
|
||||||
|
import * as Y from '../src/index.js'
|
||||||
|
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Enc
|
||||||
|
* @property {function(Array<Uint8Array>):Uint8Array} Enc.mergeUpdates
|
||||||
|
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateAsUpdate
|
||||||
|
* @property {function(Y.Doc, Uint8Array):void} Enc.applyUpdate
|
||||||
|
* @property {function(Uint8Array):void} Enc.logUpdate
|
||||||
|
* @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
|
||||||
|
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
|
||||||
|
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
|
||||||
|
* @property {string} Enc.updateEventName
|
||||||
|
* @property {string} Enc.description
|
||||||
|
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Enc}
|
||||||
|
*/
|
||||||
|
const encV1 = {
|
||||||
|
mergeUpdates: Y.mergeUpdates,
|
||||||
|
encodeStateAsUpdate: Y.encodeStateAsUpdate,
|
||||||
|
applyUpdate: Y.applyUpdate,
|
||||||
|
logUpdate: Y.logUpdate,
|
||||||
|
parseUpdateMeta: Y.parseUpdateMeta,
|
||||||
|
encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdate,
|
||||||
|
encodeStateVector: Y.encodeStateVector,
|
||||||
|
updateEventName: 'update',
|
||||||
|
description: 'V1',
|
||||||
|
diffUpdate: Y.diffUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Enc}
|
||||||
|
*/
|
||||||
|
const encV2 = {
|
||||||
|
mergeUpdates: Y.mergeUpdatesV2,
|
||||||
|
encodeStateAsUpdate: Y.encodeStateAsUpdateV2,
|
||||||
|
applyUpdate: Y.applyUpdateV2,
|
||||||
|
logUpdate: Y.logUpdateV2,
|
||||||
|
parseUpdateMeta: Y.parseUpdateMetaV2,
|
||||||
|
encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2,
|
||||||
|
encodeStateVector: Y.encodeStateVector,
|
||||||
|
updateEventName: 'updateV2',
|
||||||
|
description: 'V2',
|
||||||
|
diffUpdate: Y.diffUpdateV2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Enc}
|
||||||
|
*/
|
||||||
|
const encDoc = {
|
||||||
|
mergeUpdates: (updates) => {
|
||||||
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
|
updates.forEach(update => {
|
||||||
|
Y.applyUpdateV2(ydoc, update)
|
||||||
|
})
|
||||||
|
return Y.encodeStateAsUpdateV2(ydoc)
|
||||||
|
},
|
||||||
|
encodeStateAsUpdate: Y.encodeStateAsUpdateV2,
|
||||||
|
applyUpdate: Y.applyUpdateV2,
|
||||||
|
logUpdate: Y.logUpdateV2,
|
||||||
|
parseUpdateMeta: Y.parseUpdateMetaV2,
|
||||||
|
encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2,
|
||||||
|
encodeStateVector: Y.encodeStateVector,
|
||||||
|
updateEventName: 'updateV2',
|
||||||
|
description: 'Merge via Y.Doc',
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {Uint8Array} sv
|
||||||
|
*/
|
||||||
|
diffUpdate: (update, sv) => {
|
||||||
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
|
Y.applyUpdateV2(ydoc, update)
|
||||||
|
return Y.encodeStateAsUpdateV2(ydoc, sv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoders = [encV1, encV2, encDoc]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<Y.Doc>} users
|
||||||
|
* @param {Enc} enc
|
||||||
|
*/
|
||||||
|
const fromUpdates = (users, enc) => {
|
||||||
|
const updates = users.map(user =>
|
||||||
|
enc.encodeStateAsUpdate(user)
|
||||||
|
)
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
enc.applyUpdate(ydoc, enc.mergeUpdates(updates))
|
||||||
|
return ydoc
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testMergeUpdates = tc => {
|
||||||
|
const { users, array0, array1 } = init(tc, { users: 3 })
|
||||||
|
|
||||||
|
array0.insert(0, [1])
|
||||||
|
array1.insert(0, [2])
|
||||||
|
|
||||||
|
compare(users)
|
||||||
|
encoders.forEach(enc => {
|
||||||
|
const merged = fromUpdates(users, enc)
|
||||||
|
t.compareArrays(array0.toArray(), merged.getArray('array').toArray())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Y.Doc} ydoc
|
||||||
|
* @param {Array<Uint8Array>} updates - expecting at least 4 updates
|
||||||
|
* @param {Enc} enc
|
||||||
|
* @param {boolean} hasDeletes
|
||||||
|
*/
|
||||||
|
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
||||||
|
const cases = []
|
||||||
|
|
||||||
|
// Case 1: Simple case, simply merge everything
|
||||||
|
cases.push(enc.mergeUpdates(updates))
|
||||||
|
|
||||||
|
// Case 2: Overlapping updates
|
||||||
|
cases.push(enc.mergeUpdates([
|
||||||
|
enc.mergeUpdates(updates.slice(2)),
|
||||||
|
enc.mergeUpdates(updates.slice(0, 2))
|
||||||
|
]))
|
||||||
|
|
||||||
|
// Case 3: Overlapping updates
|
||||||
|
cases.push(enc.mergeUpdates([
|
||||||
|
enc.mergeUpdates(updates.slice(2)),
|
||||||
|
enc.mergeUpdates(updates.slice(1, 3)),
|
||||||
|
updates[0]
|
||||||
|
]))
|
||||||
|
|
||||||
|
// Case 4: Separated updates (containing skips)
|
||||||
|
cases.push(enc.mergeUpdates([
|
||||||
|
enc.mergeUpdates([updates[0], updates[2]]),
|
||||||
|
enc.mergeUpdates([updates[1], updates[3]]),
|
||||||
|
enc.mergeUpdates(updates.slice(4))
|
||||||
|
]))
|
||||||
|
|
||||||
|
// Case 5: overlapping with many duplicates
|
||||||
|
cases.push(enc.mergeUpdates(cases))
|
||||||
|
|
||||||
|
// const targetState = enc.encodeStateAsUpdate(ydoc)
|
||||||
|
// t.info('Target State: ')
|
||||||
|
// enc.logUpdate(targetState)
|
||||||
|
|
||||||
|
cases.forEach((mergedUpdates, i) => {
|
||||||
|
// t.info('State Case $' + i + ':')
|
||||||
|
// enc.logUpdate(updates)
|
||||||
|
const merged = new Y.Doc({ gc: false })
|
||||||
|
enc.applyUpdate(merged, mergedUpdates)
|
||||||
|
t.compareArrays(merged.getArray().toArray(), ydoc.getArray().toArray())
|
||||||
|
t.compare(enc.encodeStateVector(merged), enc.encodeStateVectorFromUpdate(mergedUpdates))
|
||||||
|
|
||||||
|
if (enc.updateEventName !== 'update') { // @todo should this also work on legacy updates?
|
||||||
|
for (let j = 1; j < updates.length; j++) {
|
||||||
|
const partMerged = enc.mergeUpdates(updates.slice(j))
|
||||||
|
const partMeta = enc.parseUpdateMeta(partMerged)
|
||||||
|
const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j)))
|
||||||
|
const diffed = enc.diffUpdate(mergedUpdates, targetSV)
|
||||||
|
const diffedMeta = enc.parseUpdateMeta(diffed)
|
||||||
|
const decDiffedSV = Y.decodeStateVector(enc.encodeStateVectorFromUpdate(diffed))
|
||||||
|
t.compare(partMeta, diffedMeta)
|
||||||
|
t.compare(decDiffedSV, partMeta.to)
|
||||||
|
{
|
||||||
|
// We can'd do the following
|
||||||
|
// - t.compare(diffed, mergedDeletes)
|
||||||
|
// because diffed contains the set of all deletes.
|
||||||
|
// So we add all deletes from `diffed` to `partDeletes` and compare then
|
||||||
|
const decoder = decoding.createDecoder(diffed)
|
||||||
|
const updateDecoder = new UpdateDecoderV2(decoder)
|
||||||
|
readClientsStructRefs(updateDecoder, new Y.Doc())
|
||||||
|
const ds = readDeleteSet(updateDecoder)
|
||||||
|
const updateEncoder = new UpdateEncoderV2()
|
||||||
|
encoding.writeVarUint(updateEncoder.restEncoder, 0) // 0 structs
|
||||||
|
writeDeleteSet(updateEncoder, ds)
|
||||||
|
const deletesUpdate = updateEncoder.toUint8Array()
|
||||||
|
const mergedDeletes = Y.mergeUpdatesV2([deletesUpdate, partMerged])
|
||||||
|
if (!hasDeletes || enc !== encDoc) {
|
||||||
|
// deletes will almost definitely lead to different encoders because of the mergeStruct feature that is present in encDoc
|
||||||
|
t.compare(diffed, mergedDeletes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = enc.parseUpdateMeta(mergedUpdates)
|
||||||
|
meta.from.forEach((clock, client) => t.assert(clock === 0))
|
||||||
|
meta.to.forEach((clock, client) => {
|
||||||
|
const structs = /** @type {Array<Y.Item>} */ (merged.store.clients.get(client))
|
||||||
|
const lastStruct = structs[structs.length - 1]
|
||||||
|
t.assert(lastStruct.id.clock + lastStruct.length === clock)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testMergeUpdates1 = tc => {
|
||||||
|
encoders.forEach((enc, i) => {
|
||||||
|
t.info(`Using encoder: ${enc.description}`)
|
||||||
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
|
const updates = /** @type {Array<Uint8Array>} */ ([])
|
||||||
|
ydoc.on(enc.updateEventName, update => { updates.push(update) })
|
||||||
|
|
||||||
|
const array = ydoc.getArray()
|
||||||
|
array.insert(0, [1])
|
||||||
|
array.insert(0, [2])
|
||||||
|
array.insert(0, [3])
|
||||||
|
array.insert(0, [4])
|
||||||
|
|
||||||
|
checkUpdateCases(ydoc, updates, enc, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testMergeUpdates2 = tc => {
|
||||||
|
encoders.forEach((enc, i) => {
|
||||||
|
t.info(`Using encoder: ${enc.description}`)
|
||||||
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
|
const updates = /** @type {Array<Uint8Array>} */ ([])
|
||||||
|
ydoc.on(enc.updateEventName, update => { updates.push(update) })
|
||||||
|
|
||||||
|
const array = ydoc.getArray()
|
||||||
|
array.insert(0, [1, 2])
|
||||||
|
array.delete(1, 1)
|
||||||
|
array.insert(0, [3, 4])
|
||||||
|
array.delete(1, 2)
|
||||||
|
|
||||||
|
checkUpdateCases(ydoc, updates, enc, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo be able to apply Skip structs to Yjs docs
|
||||||
|
*/
|
||||||
@@ -64,7 +64,7 @@ export const testInsertThreeElementsTryRegetProperty = tc => {
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testConcurrentInsertWithThreeConflicts = tc => {
|
export const testConcurrentInsertWithThreeConflicts = tc => {
|
||||||
var { users, array0, array1, array2 } = init(tc, { users: 3 })
|
const { users, array0, array1, array2 } = init(tc, { users: 3 })
|
||||||
array0.insert(0, [0])
|
array0.insert(0, [0])
|
||||||
array1.insert(0, [1])
|
array1.insert(0, [1])
|
||||||
array2.insert(0, [2])
|
array2.insert(0, [2])
|
||||||
@@ -107,7 +107,7 @@ export const testInsertionsInLateSync = tc => {
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testDisconnectReallyPreventsSendingMessages = tc => {
|
export const testDisconnectReallyPreventsSendingMessages = tc => {
|
||||||
var { testConnector, users, array0, array1 } = init(tc, { users: 3 })
|
const { testConnector, users, array0, array1 } = init(tc, { users: 3 })
|
||||||
array0.insert(0, ['x', 'y'])
|
array0.insert(0, ['x', 'y'])
|
||||||
testConnector.flushAllMessages()
|
testConnector.flushAllMessages()
|
||||||
users[1].disconnect()
|
users[1].disconnect()
|
||||||
@@ -388,13 +388,13 @@ const getUniqueNumber = () => _uniqueNumber++
|
|||||||
const arrayTransactions = [
|
const arrayTransactions = [
|
||||||
function insert (user, gen) {
|
function insert (user, gen) {
|
||||||
const yarray = user.getArray('array')
|
const yarray = user.getArray('array')
|
||||||
var uniqueNumber = getUniqueNumber()
|
const uniqueNumber = getUniqueNumber()
|
||||||
var content = []
|
const content = []
|
||||||
var len = prng.int32(gen, 1, 4)
|
const len = prng.int32(gen, 1, 4)
|
||||||
for (var i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
content.push(uniqueNumber)
|
content.push(uniqueNumber)
|
||||||
}
|
}
|
||||||
var pos = prng.int32(gen, 0, yarray.length)
|
const pos = prng.int32(gen, 0, yarray.length)
|
||||||
const oldContent = yarray.toArray()
|
const oldContent = yarray.toArray()
|
||||||
yarray.insert(pos, content)
|
yarray.insert(pos, content)
|
||||||
oldContent.splice(pos, 0, ...content)
|
oldContent.splice(pos, 0, ...content)
|
||||||
@@ -402,28 +402,28 @@ const arrayTransactions = [
|
|||||||
},
|
},
|
||||||
function insertTypeArray (user, gen) {
|
function insertTypeArray (user, gen) {
|
||||||
const yarray = user.getArray('array')
|
const yarray = user.getArray('array')
|
||||||
var pos = prng.int32(gen, 0, yarray.length)
|
const pos = prng.int32(gen, 0, yarray.length)
|
||||||
yarray.insert(pos, [new Y.Array()])
|
yarray.insert(pos, [new Y.Array()])
|
||||||
var array2 = yarray.get(pos)
|
const array2 = yarray.get(pos)
|
||||||
array2.insert(0, [1, 2, 3, 4])
|
array2.insert(0, [1, 2, 3, 4])
|
||||||
},
|
},
|
||||||
function insertTypeMap (user, gen) {
|
function insertTypeMap (user, gen) {
|
||||||
const yarray = user.getArray('array')
|
const yarray = user.getArray('array')
|
||||||
var pos = prng.int32(gen, 0, yarray.length)
|
const pos = prng.int32(gen, 0, yarray.length)
|
||||||
yarray.insert(pos, [new Y.Map()])
|
yarray.insert(pos, [new Y.Map()])
|
||||||
var map = yarray.get(pos)
|
const map = yarray.get(pos)
|
||||||
map.set('someprop', 42)
|
map.set('someprop', 42)
|
||||||
map.set('someprop', 43)
|
map.set('someprop', 43)
|
||||||
map.set('someprop', 44)
|
map.set('someprop', 44)
|
||||||
},
|
},
|
||||||
function _delete (user, gen) {
|
function _delete (user, gen) {
|
||||||
const yarray = user.getArray('array')
|
const yarray = user.getArray('array')
|
||||||
var length = yarray.length
|
const length = yarray.length
|
||||||
if (length > 0) {
|
if (length > 0) {
|
||||||
var somePos = prng.int32(gen, 0, length - 1)
|
let somePos = prng.int32(gen, 0, length - 1)
|
||||||
var delLength = prng.int32(gen, 1, math.min(2, length - somePos))
|
let delLength = prng.int32(gen, 1, math.min(2, length - somePos))
|
||||||
if (prng.bool(gen)) {
|
if (prng.bool(gen)) {
|
||||||
var type = yarray.get(somePos)
|
const type = yarray.get(somePos)
|
||||||
if (type.length > 0) {
|
if (type.length > 0) {
|
||||||
somePos = prng.int32(gen, 0, type.length - 1)
|
somePos = prng.int32(gen, 0, type.length - 1)
|
||||||
delLength = prng.int32(gen, 0, math.min(2, type.length - somePos))
|
delLength = prng.int32(gen, 0, math.min(2, type.length - somePos))
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
|
|||||||
t.compare(map0.get('stuff'), 'stuffy')
|
t.compare(map0.get('stuff'), 'stuffy')
|
||||||
testConnector.flushAllMessages()
|
testConnector.flushAllMessages()
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
var u = user.getMap('map')
|
const u = user.getMap('map')
|
||||||
t.compare(u.get('stuff'), 'stuffy')
|
t.compare(u.get('stuff'), 'stuffy')
|
||||||
}
|
}
|
||||||
compare(users)
|
compare(users)
|
||||||
@@ -153,7 +153,7 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
|
|||||||
map1.set('stuff', 'c1')
|
map1.set('stuff', 'c1')
|
||||||
testConnector.flushAllMessages()
|
testConnector.flushAllMessages()
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
var u = user.getMap('map')
|
const u = user.getMap('map')
|
||||||
t.compare(u.get('stuff'), 'c1')
|
t.compare(u.get('stuff'), 'c1')
|
||||||
}
|
}
|
||||||
compare(users)
|
compare(users)
|
||||||
@@ -183,7 +183,7 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
|
|||||||
map1.delete('stuff')
|
map1.delete('stuff')
|
||||||
testConnector.flushAllMessages()
|
testConnector.flushAllMessages()
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
var u = user.getMap('map')
|
const u = user.getMap('map')
|
||||||
t.assert(u.get('stuff') === undefined)
|
t.assert(u.get('stuff') === undefined)
|
||||||
}
|
}
|
||||||
compare(users)
|
compare(users)
|
||||||
@@ -200,7 +200,7 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
|
|||||||
map2.set('stuff', 'c3')
|
map2.set('stuff', 'c3')
|
||||||
testConnector.flushAllMessages()
|
testConnector.flushAllMessages()
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
var u = user.getMap('map')
|
const u = user.getMap('map')
|
||||||
t.compare(u.get('stuff'), 'c3')
|
t.compare(u.get('stuff'), 'c3')
|
||||||
}
|
}
|
||||||
compare(users)
|
compare(users)
|
||||||
@@ -223,7 +223,7 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
|
|||||||
map3.delete('stuff')
|
map3.delete('stuff')
|
||||||
testConnector.flushAllMessages()
|
testConnector.flushAllMessages()
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
var u = user.getMap('map')
|
const u = user.getMap('map')
|
||||||
t.assert(u.get('stuff') === undefined)
|
t.assert(u.get('stuff') === undefined)
|
||||||
}
|
}
|
||||||
compare(users)
|
compare(users)
|
||||||
@@ -296,7 +296,7 @@ export const testObserversUsingObservedeep = tc => {
|
|||||||
* @param {Object<string,any>} should
|
* @param {Object<string,any>} should
|
||||||
*/
|
*/
|
||||||
const compareEvent = (is, should) => {
|
const compareEvent = (is, should) => {
|
||||||
for (var key in should) {
|
for (const key in should) {
|
||||||
t.compare(should[key], is[key])
|
t.compare(should[key], is[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,12 +474,12 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
|
|||||||
const mapTransactions = [
|
const mapTransactions = [
|
||||||
function set (user, gen) {
|
function set (user, gen) {
|
||||||
const key = prng.oneOf(gen, ['one', 'two'])
|
const key = prng.oneOf(gen, ['one', 'two'])
|
||||||
var value = prng.utf16String(gen)
|
const value = prng.utf16String(gen)
|
||||||
user.getMap('map').set(key, value)
|
user.getMap('map').set(key, value)
|
||||||
},
|
},
|
||||||
function setType (user, gen) {
|
function setType (user, gen) {
|
||||||
const key = prng.oneOf(gen, ['one', 'two'])
|
const key = prng.oneOf(gen, ['one', 'two'])
|
||||||
var type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
|
const type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
|
||||||
user.getMap('map').set(key, type)
|
user.getMap('map').set(key, type)
|
||||||
if (type instanceof Y.Array) {
|
if (type instanceof Y.Array) {
|
||||||
type.insert(0, [1, 2, 3, 4])
|
type.insert(0, [1, 2, 3, 4])
|
||||||
|
|||||||
@@ -78,6 +78,49 @@ export const testBasicFormat = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testMultilineFormat = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const testText = ydoc.getText('test')
|
||||||
|
testText.insert(0, 'Test\nMulti-line\nFormatting')
|
||||||
|
testText.applyDelta([
|
||||||
|
{ retain: 4, attributes: { bold: true } },
|
||||||
|
{ retain: 1 }, // newline character
|
||||||
|
{ retain: 10, attributes: { bold: true } },
|
||||||
|
{ retain: 1 }, // newline character
|
||||||
|
{ retain: 10, attributes: { bold: true } }
|
||||||
|
])
|
||||||
|
t.compare(testText.toDelta(), [
|
||||||
|
{ insert: 'Test', attributes: { bold: true } },
|
||||||
|
{ insert: '\n' },
|
||||||
|
{ insert: 'Multi-line', attributes: { bold: true } },
|
||||||
|
{ insert: '\n' },
|
||||||
|
{ insert: 'Formatting', attributes: { bold: true } }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testNotMergeEmptyLinesFormat = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const testText = ydoc.getText('test')
|
||||||
|
testText.applyDelta([
|
||||||
|
{ insert: 'Text' },
|
||||||
|
{ insert: '\n', attributes: { title: true } },
|
||||||
|
{ insert: '\nText' },
|
||||||
|
{ insert: '\n', attributes: { title: true } }
|
||||||
|
])
|
||||||
|
t.compare(testText.toDelta(), [
|
||||||
|
{ insert: 'Text' },
|
||||||
|
{ insert: '\n', attributes: { title: true } },
|
||||||
|
{ insert: '\nText' },
|
||||||
|
{ insert: '\n', attributes: { title: true } }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -286,7 +329,9 @@ export const testBestCase = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tryGc = () => {
|
const tryGc = () => {
|
||||||
|
// @ts-ignore
|
||||||
if (typeof global !== 'undefined' && global.gc) {
|
if (typeof global !== 'undefined' && global.gc) {
|
||||||
|
// @ts-ignore
|
||||||
global.gc()
|
global.gc()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,6 +368,42 @@ export const testLargeFragmentedDocument = tc => {
|
|||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testIncrementalUpdatesPerformanceOnLargeFragmentedDocument = tc => {
|
||||||
|
const itemsToInsert = largeDocumentSize
|
||||||
|
const updates = /** @type {Array<Uint8Array>} */ ([])
|
||||||
|
;(() => {
|
||||||
|
const doc1 = new Y.Doc()
|
||||||
|
doc1.on('update', update => {
|
||||||
|
updates.push(update)
|
||||||
|
})
|
||||||
|
const text0 = doc1.getText('txt')
|
||||||
|
tryGc()
|
||||||
|
t.measureTime(`time to insert ${itemsToInsert} items`, () => {
|
||||||
|
doc1.transact(() => {
|
||||||
|
for (let i = 0; i < itemsToInsert; i++) {
|
||||||
|
text0.insert(0, '0')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
tryGc()
|
||||||
|
})()
|
||||||
|
;(() => {
|
||||||
|
t.measureTime(`time to merge ${itemsToInsert} updates (differential updates)`, () => {
|
||||||
|
Y.mergeUpdates(updates)
|
||||||
|
})
|
||||||
|
tryGc()
|
||||||
|
t.measureTime(`time to merge ${itemsToInsert} updates (ydoc updates)`, () => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
updates.forEach(update => {
|
||||||
|
Y.applyUpdate(ydoc, update)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splitting surrogates can lead to invalid encoded documents.
|
* Splitting surrogates can lead to invalid encoded documents.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user