Compare commits
192 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc4fbd466 | ||
|
|
53e2c83f86 | ||
|
|
24bca2af43 | ||
|
|
b75682022e | ||
|
|
3d31ba8759 | ||
|
|
bd47efe0ee | ||
|
|
f5781f8366 | ||
|
|
6230abb78c | ||
|
|
4356d70ed0 | ||
|
|
0948229422 | ||
|
|
fc5e36158f | ||
|
|
d314c3e1a6 | ||
|
|
2a33507c00 | ||
|
|
40c3be1732 | ||
|
|
4a8ebc31f7 | ||
|
|
6df152c4ec | ||
|
|
fc38f3b848 | ||
|
|
a057bf1cf0 | ||
|
|
8b82c573c4 | ||
|
|
a77221ffd2 | ||
|
|
b9ccbb2dc7 | ||
|
|
a723c32557 | ||
|
|
56ab251e79 | ||
|
|
53a7b286b8 | ||
|
|
294ba351b6 | ||
|
|
610e532868 | ||
|
|
f73fb4796b | ||
|
|
32d391d7ab | ||
|
|
28e1b19e57 | ||
|
|
e90d9de5ed | ||
|
|
9a7250f192 | ||
|
|
4154b12f14 | ||
|
|
9df5016667 | ||
|
|
1becaccdd9 | ||
|
|
ea4e9a0007 | ||
|
|
a4e48d1ddf | ||
|
|
0a39a92b33 | ||
|
|
bd819243eb | ||
|
|
2ec19defcb | ||
|
|
336f7b1b1d | ||
|
|
8abf5b85ff | ||
|
|
320e8cbe18 | ||
|
|
49150f4adb | ||
|
|
e22fed7af3 | ||
|
|
c91945228f | ||
|
|
3586d91925 | ||
|
|
f915ebda1b | ||
|
|
a9b92b9099 | ||
|
|
cbddf6ef90 | ||
|
|
491cd422c4 | ||
|
|
4b88e2aac5 | ||
|
|
e33c67fc72 | ||
|
|
085dda4cbd | ||
|
|
f382846874 | ||
|
|
9afc5cf615 | ||
|
|
ca0fb4b15d | ||
|
|
d369a771a9 | ||
|
|
995fbfa4cc | ||
|
|
7486ea7148 | ||
|
|
2c80a955da | ||
|
|
233872493b | ||
|
|
64d164a904 | ||
|
|
a08e54c2fc | ||
|
|
2b377cd46d | ||
|
|
b4b8927550 | ||
|
|
b2761b50f2 | ||
|
|
28a9ce962d | ||
|
|
0ec67170d3 | ||
|
|
df9bfbe778 | ||
|
|
f1ab417570 | ||
|
|
4922eeac56 | ||
|
|
57d6c6f831 | ||
|
|
371f2b6d55 | ||
|
|
85a7ad148f | ||
|
|
7ec1b3a19e | ||
|
|
633eb9033c | ||
|
|
4707fc46ac | ||
|
|
89b4320a8e | ||
|
|
0ea0a35521 | ||
|
|
15ea4ee805 | ||
|
|
744469d363 | ||
|
|
311dd50f1b | ||
|
|
89c5541ee6 | ||
|
|
28d8db86f0 | ||
|
|
0c34216ed0 | ||
|
|
9aa518bc14 | ||
|
|
27b1190a28 | ||
|
|
f3d8db491b | ||
|
|
e9905602f8 | ||
|
|
2b8154fa16 | ||
|
|
5ddb7eefed | ||
|
|
4b35de5ad5 | ||
|
|
097b9e8208 | ||
|
|
5cac153a17 | ||
|
|
a7e4724edd | ||
|
|
71d8da6513 | ||
|
|
c72ac448e9 | ||
|
|
da21fca334 | ||
|
|
d80512d690 | ||
|
|
6886881b76 | ||
|
|
dc9717ecd0 | ||
|
|
7bd764fba7 | ||
|
|
4047890a6e | ||
|
|
1627e7b3f6 | ||
|
|
e55d3cc510 | ||
|
|
55bd0b16f7 | ||
|
|
ab7de51064 | ||
|
|
d4917bb567 | ||
|
|
4e343ccace | ||
|
|
4efd47447b | ||
|
|
5aa1aaebb3 | ||
|
|
7656f897d6 | ||
|
|
5244755879 | ||
|
|
3a7a324a24 | ||
|
|
9e98fec504 | ||
|
|
b1c7022890 | ||
|
|
c67428d715 | ||
|
|
45a9af96af | ||
|
|
249c4f9c45 | ||
|
|
cdc7d3ffe6 | ||
|
|
ac6a0e7667 | ||
|
|
12881e2be7 | ||
|
|
77958da657 | ||
|
|
8a8a60efde | ||
|
|
7a1d648e79 | ||
|
|
3af420e790 | ||
|
|
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 | ||
|
|
728bb6f1b2 | ||
|
|
fd59696b9a | ||
|
|
4c929c6808 | ||
|
|
0fc213e92e | ||
|
|
af576788f1 | ||
|
|
fbbf085278 | ||
|
|
d8868c47e1 | ||
|
|
47221c26c4 | ||
|
|
ba83398374 | ||
|
|
0b23d5aeeb | ||
|
|
072947c0bb | ||
|
|
22aef63d8a | ||
|
|
f8341220c3 | ||
|
|
004a781a56 | ||
|
|
c8534ea6bc | ||
|
|
1e0fd60df4 | ||
|
|
320da29b69 | ||
|
|
783c4d8209 | ||
|
|
2c708b647d | ||
|
|
7a45be8c88 |
@@ -34,7 +34,7 @@ Each item in a Yjs list is made up of two objects:
|
||||
|
||||
- An `Item` (*src/structs/Item.js*). This is used to relate the item to other
|
||||
adjacent items.
|
||||
- An object in the `AbstractType` heirachy (subclasses of
|
||||
- An object in the `AbstractType` hierarchy (subclasses of
|
||||
*src/types/AbstractType.js* - eg `YText`). This stores the actual content in
|
||||
the Yjs document.
|
||||
|
||||
@@ -168,7 +168,7 @@ An implementation of the syncing process is in
|
||||
## Snapshots
|
||||
|
||||
A snapshot can be used to restore an old document state. It is a `state vector`
|
||||
+ `delete set`. I client can restore an old document state by iterating through
|
||||
\+ `delete set`. I client can restore an old document state by iterating through
|
||||
the sequence CRDT and ignoring all Items that have an `id.clock >
|
||||
stateVector[id.client].clock`. Instead of using `item.deleted` the client will
|
||||
use the delete set to find out if an item was deleted or not.
|
||||
|
||||
105
README.md
105
README.md
@@ -49,6 +49,8 @@ Sponsorship also comes with special perks! [ End-to-end encrypted
|
||||
collaborative notes app.
|
||||
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
|
||||
community. :star2:
|
||||
* [Input](https://input.com/) A collaborative note taking app. :star2:
|
||||
@@ -63,9 +65,7 @@ Sponsorship also comes with special perks! [ An open collaborative wiki.
|
||||
* [Pluxbox RadioManager](https://pluxbox.com/) A web-based app to
|
||||
collaboratively organize radio broadcasts.
|
||||
* [Cattaz](http://cattaz.io/) A wiki that can run custom applications in the
|
||||
wiki pages.
|
||||
* [Alldone](https://alldoneapp.com/) A next-gen project management and
|
||||
* [Alldone](https://alldone.app/) A next-gen project management and
|
||||
collaboration platform.
|
||||
|
||||
## Table of Contents
|
||||
@@ -97,6 +97,8 @@ 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) |
|
||||
| [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) |
|
||||
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
||||
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
|
||||
|
||||
### Providers
|
||||
|
||||
@@ -128,7 +130,7 @@ network provider.
|
||||
</dd>
|
||||
<dt><a href="https://github.com/yjs/y-dat">y-dat</a></dt>
|
||||
<dd>
|
||||
[WIP] Write document updates effinciently to the dat network using
|
||||
[WIP] Write document updates efficiently to the dat network using
|
||||
<a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has
|
||||
an append-only log of CRDT local updates (hypercore). Multifeed manages and sync
|
||||
hypercores and y-dat listens to changes and applies them to the Yjs document.
|
||||
@@ -246,36 +248,36 @@ necessary.
|
||||
<dl>
|
||||
<b><code>parent:Y.AbstractType|null</code></b>
|
||||
<dd></dd>
|
||||
<b><code>insert(index:number, content:Array<object|boolean|Array|string|number|Uint8Array|Y.Type>)</code></b>
|
||||
<b><code>insert(index:number, content:Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>)</code></b>
|
||||
<dd>
|
||||
Insert content at <var>index</var>. Note that content is an array of elements.
|
||||
I.e. <code>array.insert(0, [1])</code> splices the list and inserts 1 at
|
||||
position 0.
|
||||
</dd>
|
||||
<b><code>push(Array<Object|boolean|Array|string|number|Uint8Array|Y.Type>)</code></b>
|
||||
<b><code>push(Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type>)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>unshift(Array<Object|boolean|Array|string|number|Uint8Array|Y.Type>)</code></b>
|
||||
<b><code>unshift(Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type>)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>delete(index:number, length:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>get(index:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>slice(start:number, end:number):Array<Object|boolean|Array|string|number|Uint8Array|Y.Type></code></b>
|
||||
<b><code>slice(start:number, end:number):Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type></code></b>
|
||||
<dd>Retrieve a range of content</dd>
|
||||
<b><code>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b>
|
||||
<code>
|
||||
forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type,
|
||||
forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,
|
||||
index:number, array: Y.Array))
|
||||
</code>
|
||||
</b>
|
||||
<dd></dd>
|
||||
<b><code>map(function(T, number, YArray):M):Array<M></code></b>
|
||||
<dd></dd>
|
||||
<b><code>toArray():Array<object|boolean|Array|string|number|Uint8Array|Y.Type></code></b>
|
||||
<b><code>toArray():Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type></code></b>
|
||||
<dd>Copies the content of this YArray to a new Array.</dd>
|
||||
<b><code>toJSON():Array<Object|boolean|Array|string|number></code></b>
|
||||
<b><code>toJSON():Array<Object|boolean|Array|string|number|null></code></b>
|
||||
<dd>
|
||||
Copies the content of this YArray to a new Array. It transforms all child types
|
||||
to JSON using their <code>toJSON</code> method.
|
||||
@@ -319,9 +321,11 @@ or any of its children.
|
||||
<dl>
|
||||
<b><code>parent:Y.AbstractType|null</code></b>
|
||||
<dd></dd>
|
||||
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
|
||||
<b><code>size: number</code></b>
|
||||
<dd>Total number of key/value pairs.</dd>
|
||||
<b><code>get(key:string):object|boolean|string|number|null|Uint8Array|Y.Type</code></b>
|
||||
<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|null|Uint8Array|Y.Type)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>delete(key:string)</code></b>
|
||||
<dd></dd>
|
||||
@@ -329,14 +333,16 @@ or any of its children.
|
||||
<dd></dd>
|
||||
<b><code>get(index:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>clear()</code></b>
|
||||
<dd>Removes all elements from this YMap.</dd>
|
||||
<b><code>clone():Y.Map</code></b>
|
||||
<dd>Clone this type into a fresh Yjs type.</dd>
|
||||
<b><code>toJSON():Object<string, Object|boolean|Array|string|number|Uint8Array></code></b>
|
||||
<b><code>toJSON():Object<string, Object|boolean|Array|string|number|null|Uint8Array></code></b>
|
||||
<dd>
|
||||
Copies the <code>[key,value]</code> pairs of this YMap to a new Object.It
|
||||
transforms all child types to JSON using their <code>toJSON</code> method.
|
||||
</dd>
|
||||
<b><code>forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type,
|
||||
<b><code>forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,
|
||||
key:string, map: Y.Map))</code></b>
|
||||
<dd>
|
||||
Execute the provided function once for every key-value pair.
|
||||
@@ -409,7 +415,7 @@ YTextEvents compute changes as deltas.
|
||||
<dd></dd>
|
||||
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
||||
<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>
|
||||
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
||||
Can set options for preventing remove ending newLines, default is true.
|
||||
@@ -482,6 +488,8 @@ or any of its children.
|
||||
<dd>Get the XML serialization of all descendants.</dd>
|
||||
<b><code>toJSON():string</code></b>
|
||||
<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>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
@@ -539,7 +547,7 @@ content and be actually XML compliant.
|
||||
<dd></dd>
|
||||
<b><code>getAttribute(attributeName:string):string</code></b>
|
||||
<dd></dd>
|
||||
<b><code>getAttributes(attributeName:string):Object<string,string></code></b>
|
||||
<b><code>getAttributes():Object<string,string></code></b>
|
||||
<dd></dd>
|
||||
<b><code>get(i:number):Y.XmlElement|Y.XmlText</code></b>
|
||||
<dd>Retrieve the i-th element.</dd>
|
||||
@@ -608,7 +616,10 @@ parameter that is stored on <code>transaction.origin</code> and
|
||||
</dd>
|
||||
<b><code>toJSON():any</code></b>
|
||||
<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>
|
||||
<b><code>get(string, Y.[TypeClass]):[Type]</code></b>
|
||||
<dd>Define a shared type.</dd>
|
||||
@@ -690,7 +701,7 @@ Y.applyUpdate(ydoc2, state1)
|
||||
This example shows how to sync two clients with the minimal amount of exchanged
|
||||
data by computing only the differences using the state vector of the remote
|
||||
client. Syncing clients using the state vector requires another roundtrip, but
|
||||
can safe a lot of bandwidth.
|
||||
can save a lot of bandwidth.
|
||||
|
||||
```js
|
||||
const stateVector1 = Y.encodeStateVector(ydoc1)
|
||||
@@ -701,6 +712,30 @@ Y.applyUpdate(ydoc1, diff2)
|
||||
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>
|
||||
<b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b>
|
||||
<dd>
|
||||
@@ -717,22 +752,26 @@ differences to the update message.
|
||||
</dd>
|
||||
<b><code>Y.encodeStateVector(Y.Doc):Uint8Array</code></b>
|
||||
<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>
|
||||
|
||||
### 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
|
||||
|
||||
```js
|
||||
@@ -779,7 +818,7 @@ pos.index === 2 // => true
|
||||
|
||||
### Y.UndoManager
|
||||
|
||||
Yjs ships with an Undo/Redo manager for selective undo/redo of of changes on a
|
||||
Yjs ships with an Undo/Redo manager for selective undo/redo of changes on a
|
||||
Yjs type. The changes can be optionally scoped to transaction origins.
|
||||
|
||||
```js
|
||||
@@ -948,7 +987,7 @@ order of the structs anymore (e.g. if the parent was deleted).
|
||||
**Examples:**
|
||||
|
||||
1. If a user inserts elements in sequence, the struct will be merged into a
|
||||
single struct. E.g. `array.insert(0, ['a']), array.insert(0, ['b']);` is
|
||||
single struct. E.g. `text.insert(0, 'a'), text.insert(1, 'b');` is
|
||||
first represented as two structs (`[{id: {client, clock: 0}, content: 'a'},
|
||||
{id: {client, clock: 1}, content: 'b'}`) and then merged into a single
|
||||
struct: `[{id: {client, clock: 0}, content: 'ab'}]`.
|
||||
|
||||
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')
|
||||
1928
package-lock.json
generated
1928
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@@ -1,39 +1,49 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.4.8",
|
||||
"version": "13.6.0-1",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
"unpkg": "./dist/yjs.mjs",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run dist && node ./dist/tests.cjs --repitition-time 50",
|
||||
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000",
|
||||
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
|
||||
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
|
||||
"dist": "rm -rf dist && rollup -c && tsc",
|
||||
"watch": "rollup -wc",
|
||||
"lint": "markdownlint README.md && standard && tsc",
|
||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
||||
"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 && 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'",
|
||||
"trace-deopt": "clear && rollup -c && node --trace-deopt 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",
|
||||
"./testHelper": "./dist/testHelper.mjs",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
"src/*",
|
||||
"tests/*",
|
||||
"docs/*",
|
||||
"./funding.cjs"
|
||||
"dist/yjs.*",
|
||||
"dist/src",
|
||||
"src",
|
||||
"tests/testHelper.js",
|
||||
"dist/testHelper.mjs",
|
||||
"sponsor-y.js"
|
||||
],
|
||||
"dictionaries": {
|
||||
"doc": "docs",
|
||||
"test": "tests"
|
||||
},
|
||||
"standard": {
|
||||
@@ -51,7 +61,8 @@
|
||||
"Yjs",
|
||||
"CRDT",
|
||||
"offline",
|
||||
"shared editing",
|
||||
"offline-first",
|
||||
"shared-editing",
|
||||
"concurrency",
|
||||
"collaboration"
|
||||
],
|
||||
@@ -61,22 +72,21 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/yjs/yjs/issues"
|
||||
},
|
||||
"homepage": "https://yjs.dev",
|
||||
"homepage": "https://docs.yjs.dev",
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.33"
|
||||
"lib0": "^0.2.43"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||
"concurrently": "^3.6.1",
|
||||
"http-server": "^0.12.3",
|
||||
"jsdoc": "^3.6.5",
|
||||
"jsdoc": "^3.6.7",
|
||||
"markdownlint-cli": "^0.23.2",
|
||||
"rollup": "^1.32.1",
|
||||
"rollup-cli": "^1.0.9",
|
||||
"standard": "^14.3.4",
|
||||
"rollup": "^2.60.0",
|
||||
"standard": "^16.0.4",
|
||||
"tui-jsdoc-template": "^1.2.2",
|
||||
"typescript": "^3.9.7",
|
||||
"y-protocols": "^0.2.3"
|
||||
"typescript": "^4.4.4",
|
||||
"y-protocols": "^1.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export default [{
|
||||
sourcemap: true,
|
||||
paths: path => {
|
||||
if (/^lib0\//.test(path)) {
|
||||
return `lib0/dist/${path.slice(5, -3)}.cjs`
|
||||
return `lib0/dist/${path.slice(5)}.cjs`
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -60,6 +60,23 @@ export default [{
|
||||
sourcemap: true
|
||||
},
|
||||
external: id => /^lib0\//.test(id)
|
||||
}, {
|
||||
input: './tests/testHelper.js',
|
||||
output: {
|
||||
name: 'Y',
|
||||
file: 'dist/testHelper.mjs',
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
},
|
||||
external: id => /^lib0\//.test(id) || id === 'yjs',
|
||||
plugins: [{
|
||||
resolveId (importee) {
|
||||
if (importee === '../src/index.js') {
|
||||
return 'yjs'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
input: './tests/index.js',
|
||||
output: {
|
||||
|
||||
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) { }
|
||||
47
src/index.js
47
src/index.js
@@ -1,3 +1,4 @@
|
||||
/** eslint-env browser */
|
||||
|
||||
export {
|
||||
Doc,
|
||||
@@ -26,14 +27,13 @@ export {
|
||||
ContentString,
|
||||
ContentType,
|
||||
AbstractType,
|
||||
RelativePosition,
|
||||
getTypeChildren,
|
||||
createRelativePositionFromTypeIndex,
|
||||
createRelativePositionFromJSON,
|
||||
createAbsolutePositionFromRelativePosition,
|
||||
compareRelativePositions,
|
||||
writeRelativePosition,
|
||||
readRelativePosition,
|
||||
AbsolutePosition,
|
||||
RelativePosition,
|
||||
ID,
|
||||
createID,
|
||||
compareIDs,
|
||||
@@ -42,9 +42,11 @@ export {
|
||||
createSnapshot,
|
||||
createDeleteSet,
|
||||
createDeleteSetFromStructStore,
|
||||
cleanupYTextFormatting,
|
||||
snapshot,
|
||||
emptySnapshot,
|
||||
findRootTypeKey,
|
||||
findIndexSS,
|
||||
getItem,
|
||||
typeListToArraySnapshot,
|
||||
typeMapGetSnapshot,
|
||||
@@ -57,14 +59,15 @@ export {
|
||||
encodeStateAsUpdate,
|
||||
encodeStateAsUpdateV2,
|
||||
encodeStateVector,
|
||||
encodeStateVectorV2,
|
||||
UndoManager,
|
||||
decodeSnapshot,
|
||||
encodeSnapshot,
|
||||
decodeSnapshotV2,
|
||||
encodeSnapshotV2,
|
||||
decodeStateVector,
|
||||
decodeStateVectorV2,
|
||||
logUpdate,
|
||||
logUpdateV2,
|
||||
relativePositionToJSON,
|
||||
isDeleted,
|
||||
isParentOf,
|
||||
equalSnapshots,
|
||||
@@ -72,5 +75,37 @@ export {
|
||||
tryGc,
|
||||
transact,
|
||||
AbstractConnector,
|
||||
logType
|
||||
logType,
|
||||
mergeUpdates,
|
||||
mergeUpdatesV2,
|
||||
parseUpdateMeta,
|
||||
parseUpdateMetaV2,
|
||||
encodeStateVectorFromUpdate,
|
||||
encodeStateVectorFromUpdateV2,
|
||||
encodeRelativePosition,
|
||||
decodeRelativePosition,
|
||||
diffUpdate,
|
||||
diffUpdateV2
|
||||
} from './internals.js'
|
||||
|
||||
const glo = /** @type {any} */ (typeof window !== 'undefined'
|
||||
? window
|
||||
: typeof global !== 'undefined' ? global : {})
|
||||
const importIdentifier = '__ $YJS$ __'
|
||||
|
||||
if (glo[importIdentifier] === true) {
|
||||
/**
|
||||
* Dear reader of this warning message. Please take this seriously.
|
||||
*
|
||||
* If you see this message, please make sure that you only import one version of Yjs. In many cases,
|
||||
* your package manager installs two versions of Yjs that are used by different packages within your project.
|
||||
* Another reason for this message is that some parts of your project use the commonjs version of Yjs
|
||||
* and others use the EcmaScript version of Yjs.
|
||||
*
|
||||
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
|
||||
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
|
||||
* do the constructor checks anymore - which might break the CRDT algorithm.
|
||||
*/
|
||||
console.warn('Yjs was already imported. Importing different versions of Yjs often leads to issues.')
|
||||
}
|
||||
glo[importIdentifier] = true
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from './utils/encoding.js'
|
||||
export * from './utils/EventHandler.js'
|
||||
export * from './utils/ID.js'
|
||||
export * from './utils/isParentOf.js'
|
||||
export * from './utils/ListIterator.js'
|
||||
export * from './utils/logging.js'
|
||||
export * from './utils/PermanentUserData.js'
|
||||
export * from './utils/RelativePosition.js'
|
||||
@@ -15,6 +16,7 @@ export * from './utils/Snapshot.js'
|
||||
export * from './utils/StructStore.js'
|
||||
export * from './utils/Transaction.js'
|
||||
export * from './utils/UndoManager.js'
|
||||
export * from './utils/updates.js'
|
||||
export * from './utils/YEvent.js'
|
||||
|
||||
export * from './types/AbstractType.js'
|
||||
@@ -37,5 +39,7 @@ export * from './structs/ContentFormat.js'
|
||||
export * from './structs/ContentJSON.js'
|
||||
export * from './structs/ContentAny.js'
|
||||
export * from './structs/ContentString.js'
|
||||
export * from './structs/ContentMove.js'
|
||||
export * from './structs/ContentType.js'
|
||||
export * from './structs/Item.js'
|
||||
export * from './structs/Skip.js'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
import {
|
||||
AbstractUpdateEncoder, ID, Transaction // eslint-disable-line
|
||||
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class AbstractStruct {
|
||||
/**
|
||||
@@ -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} encodingRef
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Transaction, Item, StructStore // eslint-disable-line
|
||||
UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
export class ContentAny {
|
||||
@@ -74,7 +74,7 @@ export class ContentAny {
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -95,7 +95,7 @@ export class ContentAny {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
export const readContentAny = decoder => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Item, Transaction // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class ContentBinary {
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ export class ContentBinary {
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -86,7 +86,7 @@ export class ContentBinary {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import {
|
||||
addToDeleteSet,
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Item, Transaction // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
export class ContentDeleted {
|
||||
@@ -77,7 +77,7 @@ export class ContentDeleted {
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -95,7 +95,7 @@ export class ContentDeleted {
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
|
||||
import {
|
||||
Doc, AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Transaction, Item // eslint-disable-line
|
||||
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @param {string} guid
|
||||
* @param {Object<string, any>} opts
|
||||
*/
|
||||
const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false })
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -61,7 +67,7 @@ export class ContentDoc {
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentDoc(this.doc)
|
||||
return new ContentDoc(createDocFromOpts(this.doc.guid, this.opts))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +116,7 @@ export class ContentDoc {
|
||||
gc (store) { }
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -129,7 +135,7 @@ export class ContentDoc {
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
export const readContentDoc = decoder => new ContentDoc(new Doc({ guid: decoder.readString(), ...decoder.readAny() }))
|
||||
export const readContentDoc = decoder => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny()))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
import {
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder, StructStore, Item, Transaction // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -74,7 +74,7 @@ export class ContentEmbed {
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -92,7 +92,7 @@ export class ContentEmbed {
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON())
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
import {
|
||||
AbstractType, AbstractUpdateDecoder, AbstractUpdateEncoder, Item, StructStore, Transaction // eslint-disable-line
|
||||
AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -80,7 +80,7 @@ export class ContentFormat {
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -97,7 +97,7 @@ export class ContentFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
export const readContentFormat = decoder => new ContentFormat(decoder.readString(), decoder.readJSON())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Transaction, Item, StructStore // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
@@ -77,7 +77,7 @@ export class ContentJSON {
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -100,7 +100,7 @@ export class ContentJSON {
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
export const readContentJSON = decoder => {
|
||||
|
||||
286
src/structs/ContentMove.js
Normal file
286
src/structs/ContentMove.js
Normal file
@@ -0,0 +1,286 @@
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as math from 'lib0/math'
|
||||
import {
|
||||
AbstractType, ContentType, RelativePosition, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore, getItem, getItemCleanStart, getItemCleanEnd // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
import { decodeRelativePosition, encodeRelativePosition } from 'yjs'
|
||||
|
||||
/**
|
||||
* @param {ContentMove} moved
|
||||
* @param {Transaction} tr
|
||||
* @return {{ start: Item, end: Item | null }} $start (inclusive) is the beginning and $end (exclusive) is the end of the moved area
|
||||
*/
|
||||
export const getMovedCoords = (moved, tr) => {
|
||||
let start // this (inclusive) is the beginning of the moved area
|
||||
let end // this (exclusive) is the first item after start that is not part of the moved area
|
||||
if (moved.start.item) {
|
||||
if (moved.start.assoc < 0) {
|
||||
start = getItemCleanEnd(tr, moved.start.item)
|
||||
start = start.right
|
||||
} else {
|
||||
start = getItemCleanStart(tr, moved.start.item)
|
||||
}
|
||||
} else if (moved.start.tname != null) {
|
||||
start = tr.doc.get(moved.start.tname)._start
|
||||
} else if (moved.start.type) {
|
||||
start = /** @type {ContentType} */ (getItem(tr.doc.store, moved.start.type).content).type._start
|
||||
} else {
|
||||
error.unexpectedCase()
|
||||
}
|
||||
if (moved.end.item) {
|
||||
if (moved.end.assoc < 0) {
|
||||
end = getItemCleanEnd(tr, moved.end.item)
|
||||
end = end.right
|
||||
} else {
|
||||
end = getItemCleanStart(tr, moved.end.item)
|
||||
}
|
||||
} else {
|
||||
end = null
|
||||
}
|
||||
return { start: /** @type {Item} */ (start), end }
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo remove this if not needed
|
||||
*
|
||||
* @param {ContentMove} moved
|
||||
* @param {Item} movedItem
|
||||
* @param {Transaction} tr
|
||||
* @param {function(Item):void} cb
|
||||
*/
|
||||
export const iterateMoved = (moved, movedItem, tr, cb) => {
|
||||
/**
|
||||
* @type {{ start: Item | null, end: Item | null }}
|
||||
*/
|
||||
let { start, end } = getMovedCoords(moved, tr)
|
||||
while (start !== end && start != null) {
|
||||
if (!start.deleted) {
|
||||
if (start.moved === movedItem) {
|
||||
if (start.content.constructor === ContentMove) {
|
||||
iterateMoved(start.content, start, tr, cb)
|
||||
} else {
|
||||
cb(start)
|
||||
}
|
||||
}
|
||||
}
|
||||
start = start.right
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentMove} moved
|
||||
* @param {Item} movedItem
|
||||
* @param {Set<Item>} trackedMovedItems
|
||||
* @param {Transaction} tr
|
||||
* @return {boolean} true if there is a loop
|
||||
*/
|
||||
export const findMoveLoop = (moved, movedItem, trackedMovedItems, tr) => {
|
||||
if (trackedMovedItems.has(movedItem)) {
|
||||
return true
|
||||
}
|
||||
trackedMovedItems.add(movedItem)
|
||||
/**
|
||||
* @type {{ start: Item | null, end: Item | null }}
|
||||
*/
|
||||
let { start, end } = getMovedCoords(moved, tr)
|
||||
while (start !== end && start != null) {
|
||||
if (start.deleted && start.moved === movedItem && start.content.constructor === ContentMove) {
|
||||
if (findMoveLoop(start.content, start, trackedMovedItems, tr)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
start = start.right
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentMove {
|
||||
/**
|
||||
* @param {RelativePosition} start
|
||||
* @param {RelativePosition} end
|
||||
* @param {number} priority if we want to move content that is already moved, we need to assign a higher priority to this move operation.
|
||||
*/
|
||||
constructor (start, end, priority) {
|
||||
this.start = start
|
||||
this.end = end
|
||||
this.priority = priority
|
||||
/**
|
||||
* We store which Items+ContentMove we override. Once we delete
|
||||
* this ContentMove, we need to re-integrate the overridden items.
|
||||
*
|
||||
* This representation can be improved if we ever run into memory issues because of too many overrides.
|
||||
* Ideally, we should probably just re-iterate the document and re-integrate all moved items.
|
||||
* This is fast enough and reduces memory footprint significantly.
|
||||
*
|
||||
* @type {Set<Item>}
|
||||
*/
|
||||
this.overrides = new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [null]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentMove}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentMove(this.start, this.end, this.priority)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentMove}
|
||||
*/
|
||||
splice (offset) {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentMove} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = []
|
||||
/**
|
||||
* @type {{ start: Item | null, end: Item | null }}
|
||||
*/
|
||||
let { start, end } = getMovedCoords(this, transaction)
|
||||
let maxPriority = 0
|
||||
// If this ContentMove was created locally, we set prio = -1. This indicates
|
||||
// that we want to set prio to the current prio-maximum of the moved range.
|
||||
const adaptPriority = this.priority < 0
|
||||
while (start !== end && start != null) {
|
||||
if (!start.deleted) {
|
||||
const currMoved = start.moved
|
||||
const nextPrio = currMoved ? /** @type {ContentMove} */ (currMoved.content).priority : -1
|
||||
if (currMoved === null || adaptPriority || nextPrio < this.priority || currMoved.id.client < item.id.client || (currMoved.id.client === item.id.client && currMoved.id.clock < item.id.clock)) {
|
||||
if (currMoved !== null) {
|
||||
this.overrides.add(currMoved)
|
||||
}
|
||||
maxPriority = math.max(maxPriority, nextPrio)
|
||||
// was already moved
|
||||
if (start.moved && !transaction.prevMoved.has(start)) {
|
||||
// we need to know which item previously moved an item
|
||||
transaction.prevMoved.set(start, start.moved)
|
||||
}
|
||||
start.moved = item
|
||||
} else {
|
||||
/** @type {ContentMove} */ (currMoved.content).overrides.add(item)
|
||||
}
|
||||
}
|
||||
start = start.right
|
||||
}
|
||||
if (adaptPriority) {
|
||||
this.priority = maxPriority + 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
delete (transaction, item) {
|
||||
/**
|
||||
* @type {{ start: Item | null, end: Item | null }}
|
||||
*/
|
||||
let { start, end } = getMovedCoords(this, transaction)
|
||||
while (start !== end && start != null) {
|
||||
if (start.moved === item) {
|
||||
start.moved = null
|
||||
}
|
||||
start = start.right
|
||||
}
|
||||
/**
|
||||
* @param {Item} reIntegrateItem
|
||||
*/
|
||||
const reIntegrate = reIntegrateItem => {
|
||||
const content = /** @type {ContentMove} */ (reIntegrateItem.content)
|
||||
if (reIntegrateItem.deleted) {
|
||||
// potentially we can integrate the items that reIntegrateItem overrides
|
||||
content.overrides.forEach(reIntegrate)
|
||||
} else {
|
||||
content.integrate(transaction, reIntegrateItem)
|
||||
}
|
||||
}
|
||||
this.overrides.forEach(reIntegrate)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
const isCollapsed = this.isCollapsed()
|
||||
encoding.writeUint8(encoder.restEncoder, isCollapsed ? 1 : 0)
|
||||
encoder.writeBuf(encodeRelativePosition(this.start))
|
||||
if (!isCollapsed) {
|
||||
encoder.writeBuf(encodeRelativePosition(this.end))
|
||||
}
|
||||
encoding.writeVarUint(encoder.restEncoder, this.priority)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 11
|
||||
}
|
||||
|
||||
isCollapsed () {
|
||||
return this.start.item === this.end.item && this.start.item !== null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @todo use binary encoding option for start & end relpos's
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentMove}
|
||||
*/
|
||||
export const readContentMove = decoder => {
|
||||
const isCollapsed = decoding.readUint8(decoder.restDecoder) === 1
|
||||
const start = decodeRelativePosition(decoder.readBuf())
|
||||
const end = isCollapsed ? start.clone() : decodeRelativePosition(decoder.readBuf())
|
||||
if (isCollapsed) {
|
||||
end.assoc = -1
|
||||
}
|
||||
return new ContentMove(start, end, decoding.readVarUint(decoder.restDecoder))
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Transaction, Item, StructStore // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
@@ -88,7 +88,7 @@ export class ContentString {
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -106,7 +106,7 @@ export class ContentString {
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentString}
|
||||
*/
|
||||
export const readContentString = decoder => new ContentString(decoder.readString())
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
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'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @type {Array<function(AbstractUpdateDecoder):AbstractType<any>>}
|
||||
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractType<any>>}
|
||||
* @private
|
||||
*/
|
||||
export const typeRefs = [
|
||||
@@ -148,7 +148,7 @@ export class ContentType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -166,7 +166,7 @@ export class ContentType {
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentType}
|
||||
*/
|
||||
export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
AbstractStruct,
|
||||
addStruct,
|
||||
AbstractUpdateEncoder, StructStore, Transaction, ID // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
export const structGCRefNumber = 0
|
||||
@@ -22,6 +22,9 @@ export class GC extends AbstractStruct {
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
if (this.constructor !== right.constructor) {
|
||||
return false
|
||||
}
|
||||
this.length += right.length
|
||||
return true
|
||||
}
|
||||
@@ -39,7 +42,7 @@ export class GC extends AbstractStruct {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
|
||||
@@ -21,12 +21,13 @@ import {
|
||||
createID,
|
||||
readContentFormat,
|
||||
readContentType,
|
||||
readContentMove,
|
||||
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'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as binary from 'lib0/binary.js'
|
||||
import * as error from 'lib0/error'
|
||||
import * as binary from 'lib0/binary'
|
||||
|
||||
/**
|
||||
* @todo This should return several items
|
||||
@@ -116,6 +117,12 @@ export const splitItem = (transaction, leftItem, diff) => {
|
||||
/** @type {AbstractType<any>} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
|
||||
}
|
||||
leftItem.length = diff
|
||||
if (leftItem.moved) {
|
||||
const m = transaction.prevMoved.get(leftItem)
|
||||
if (m) {
|
||||
transaction.prevMoved.set(rightItem, m)
|
||||
}
|
||||
}
|
||||
return rightItem
|
||||
}
|
||||
|
||||
@@ -125,12 +132,13 @@ export const splitItem = (transaction, leftItem, diff) => {
|
||||
* @param {Transaction} transaction The Yjs instance.
|
||||
* @param {Item} item
|
||||
* @param {Set<Item>} redoitems
|
||||
* @param {Array<Item>} itemsToDelete
|
||||
*
|
||||
* @return {Item|null}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const redoItem = (transaction, item, redoitems) => {
|
||||
export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
|
||||
const doc = transaction.doc
|
||||
const store = doc.store
|
||||
const ownClientID = doc.clientID
|
||||
@@ -170,7 +178,7 @@ export const redoItem = (transaction, item, redoitems) => {
|
||||
// make sure that parent is redone
|
||||
if (parentItem !== null && parentItem.deleted === true && parentItem.redone === null) {
|
||||
// try to undo parent if it will be undone anyway
|
||||
if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems) === null) {
|
||||
if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete) === null) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -209,6 +217,11 @@ export const redoItem = (transaction, item, redoitems) => {
|
||||
}
|
||||
right = right.right
|
||||
}
|
||||
// Iterate right while right is in itemsToDelete
|
||||
// If it is intended to delete right while item is redone, we can expect that item should replace right.
|
||||
while (left !== null && left.right !== null && left.right !== right && itemsToDelete.findIndex(d => d === /** @type {Item} */ (left).right) >= 0) {
|
||||
left = left.right
|
||||
}
|
||||
}
|
||||
const nextClock = getState(store, ownClientID)
|
||||
const nextId = createID(ownClientID, nextClock)
|
||||
@@ -275,11 +288,18 @@ export class Item extends AbstractStruct {
|
||||
*/
|
||||
this.parentSub = parentSub
|
||||
/**
|
||||
* If this type's effect is reundone this type refers to the type that undid
|
||||
* If this type's effect is reundone this type refers to the type-id that undid
|
||||
* this operation.
|
||||
*
|
||||
* @type {ID | null}
|
||||
*/
|
||||
this.redone = null
|
||||
/**
|
||||
* This property is reused by the moved prop. In this case this property refers to an Item.
|
||||
*
|
||||
* @type {Item | null}
|
||||
*/
|
||||
this.moved = null
|
||||
/**
|
||||
* @type {AbstractContent}
|
||||
*/
|
||||
@@ -365,7 +385,7 @@ export class Item extends AbstractStruct {
|
||||
// We have all missing ids, now find the items
|
||||
|
||||
if (this.origin) {
|
||||
this.left = getItemCleanEnd(transaction, store, this.origin)
|
||||
this.left = getItemCleanEnd(transaction, this.origin)
|
||||
this.origin = this.left.lastId
|
||||
}
|
||||
if (this.rightOrigin) {
|
||||
@@ -403,7 +423,7 @@ export class Item extends AbstractStruct {
|
||||
integrate (transaction, offset) {
|
||||
if (offset > 0) {
|
||||
this.id.clock += offset
|
||||
this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1))
|
||||
this.left = getItemCleanEnd(transaction, createID(this.id.client, this.id.clock - 1))
|
||||
this.origin = this.left.lastId
|
||||
this.content = this.content.splice(offset)
|
||||
this.length -= offset
|
||||
@@ -554,6 +574,7 @@ export class Item extends AbstractStruct {
|
||||
*/
|
||||
mergeWith (right) {
|
||||
if (
|
||||
this.constructor === right.constructor &&
|
||||
compareIDs(right.origin, this.lastId) &&
|
||||
this.right === right &&
|
||||
compareIDs(this.rightOrigin, right.rightOrigin) &&
|
||||
@@ -562,9 +583,23 @@ export class Item extends AbstractStruct {
|
||||
this.deleted === right.deleted &&
|
||||
this.redone === null &&
|
||||
right.redone === null &&
|
||||
this.moved === right.moved &&
|
||||
this.content.constructor === right.content.constructor &&
|
||||
this.content.mergeWith(right.content)
|
||||
) {
|
||||
if (right.marker) {
|
||||
// Right will be "forgotten", so we delete all
|
||||
// search markers that reference right.
|
||||
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker
|
||||
if (searchMarker) {
|
||||
for (let i = searchMarker.length - 1; i >= 0; i--) {
|
||||
if (searchMarker[i].nextItem === right) {
|
||||
// @todo do something more efficient than splicing..
|
||||
searchMarker.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (right.keep) {
|
||||
this.keep = true
|
||||
}
|
||||
@@ -593,7 +628,7 @@ export class Item extends AbstractStruct {
|
||||
this.markDeleted()
|
||||
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
|
||||
addChangedTypeToTransaction(transaction, parent, this.parentSub)
|
||||
this.content.delete(transaction)
|
||||
this.content.delete(transaction, this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,7 +654,7 @@ export class Item extends AbstractStruct {
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
@@ -639,6 +674,7 @@ export class Item extends AbstractStruct {
|
||||
}
|
||||
if (origin === null && rightOrigin === null) {
|
||||
const parent = /** @type {AbstractType<any>} */ (this.parent)
|
||||
if (parent._item !== undefined) {
|
||||
const parentItem = parent._item
|
||||
if (parentItem === null) {
|
||||
// parent type on y._map
|
||||
@@ -650,6 +686,15 @@ export class Item extends AbstractStruct {
|
||||
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.writeString(parent)
|
||||
} else if (parent.constructor === ID) {
|
||||
encoder.writeParentInfo(false) // write parent id
|
||||
encoder.writeLeftID(parent)
|
||||
} else {
|
||||
error.unexpectedCase()
|
||||
}
|
||||
if (parentSub !== null) {
|
||||
encoder.writeString(parentSub)
|
||||
}
|
||||
@@ -659,7 +704,7 @@ export class Item extends AbstractStruct {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @param {number} info
|
||||
*/
|
||||
export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
|
||||
@@ -667,10 +712,10 @@ export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS
|
||||
/**
|
||||
* A lookup map for reading Item content.
|
||||
*
|
||||
* @type {Array<function(AbstractUpdateDecoder):AbstractContent>}
|
||||
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractContent>}
|
||||
*/
|
||||
export const contentRefs = [
|
||||
() => { throw error.unexpectedCase() }, // GC is not ItemContent
|
||||
() => { error.unexpectedCase() }, // GC is not ItemContent
|
||||
readContentDeleted, // 1
|
||||
readContentJSON, // 2
|
||||
readContentBinary, // 3
|
||||
@@ -679,7 +724,9 @@ export const contentRefs = [
|
||||
readContentFormat, // 6
|
||||
readContentType, // 7
|
||||
readContentAny, // 8
|
||||
readContentDoc // 9
|
||||
readContentDoc, // 9
|
||||
() => { error.unexpectedCase() }, // 10 - Skip is not ItemContent
|
||||
readContentMove // 11
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -746,8 +793,9 @@ export class AbstractContent {
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
delete (transaction) {
|
||||
delete (transaction, item) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
@@ -759,7 +807,7 @@ export class AbstractContent {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} 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'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -10,78 +10,17 @@ import {
|
||||
createID,
|
||||
ContentAny,
|
||||
ContentBinary,
|
||||
getItemCleanStart,
|
||||
ContentDoc, YText, YArray, AbstractUpdateEncoder, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
||||
ListIterator,
|
||||
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as iterator from 'lib0/iterator.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as map from 'lib0/map'
|
||||
import * as iterator from 'lib0/iterator'
|
||||
import * as error from 'lib0/error'
|
||||
import * as math from 'lib0/math'
|
||||
|
||||
const maxSearchMarker = 80
|
||||
|
||||
/**
|
||||
* A unique timestamp that identifies each marker.
|
||||
*
|
||||
* Time is relative,.. this is more like an ever-increasing clock.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
let globalSearchMarkerTimestamp = 0
|
||||
|
||||
export class ArraySearchMarker {
|
||||
/**
|
||||
* @param {Item} p
|
||||
* @param {number} index
|
||||
*/
|
||||
constructor (p, index) {
|
||||
p.marker = true
|
||||
this.p = p
|
||||
this.index = index
|
||||
this.timestamp = globalSearchMarkerTimestamp++
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArraySearchMarker} marker
|
||||
*/
|
||||
const refreshMarkerTimestamp = marker => { marker.timestamp = globalSearchMarkerTimestamp++ }
|
||||
|
||||
/**
|
||||
* This is rather complex so this function is the only thing that should overwrite a marker
|
||||
*
|
||||
* @param {ArraySearchMarker} marker
|
||||
* @param {Item} p
|
||||
* @param {number} index
|
||||
*/
|
||||
const overwriteMarker = (marker, p, index) => {
|
||||
marker.p.marker = false
|
||||
marker.p = p
|
||||
p.marker = true
|
||||
marker.index = index
|
||||
marker.timestamp = globalSearchMarkerTimestamp++
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<ArraySearchMarker>} searchMarker
|
||||
* @param {Item} p
|
||||
* @param {number} index
|
||||
*/
|
||||
const markPosition = (searchMarker, p, index) => {
|
||||
if (searchMarker.length >= maxSearchMarker) {
|
||||
// override oldest marker (we don't want to create more objects)
|
||||
const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b)
|
||||
overwriteMarker(marker, p, index)
|
||||
return marker
|
||||
} else {
|
||||
// create new marker
|
||||
const pm = new ArraySearchMarker(p, index)
|
||||
searchMarker.push(pm)
|
||||
return pm
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search marker help us to find positions in the associative array faster.
|
||||
*
|
||||
@@ -89,82 +28,69 @@ const markPosition = (searchMarker, p, index) => {
|
||||
*
|
||||
* A maximum of `maxSearchMarker` objects are created.
|
||||
*
|
||||
* This function always returns a refreshed marker (updated timestamp)
|
||||
*
|
||||
* @template T
|
||||
* @param {Transaction} tr
|
||||
* @param {AbstractType<any>} yarray
|
||||
* @param {number} index
|
||||
* @param {function(ListIterator):T} f
|
||||
*/
|
||||
export const findMarker = (yarray, index) => {
|
||||
if (yarray._start === null || index === 0 || yarray._searchMarker === null) {
|
||||
return null
|
||||
export const useSearchMarker = (tr, yarray, index, f) => {
|
||||
const searchMarker = yarray._searchMarker
|
||||
if (searchMarker === null || yarray._start === null || index < 5) {
|
||||
return f(new ListIterator(yarray).forward(tr, index))
|
||||
}
|
||||
const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b)
|
||||
let p = yarray._start
|
||||
let pindex = 0
|
||||
if (marker !== null) {
|
||||
p = marker.p
|
||||
pindex = marker.index
|
||||
refreshMarkerTimestamp(marker) // we used it, we might need to use it again
|
||||
if (searchMarker.length === 0) {
|
||||
const sm = new ListIterator(yarray).forward(tr, index)
|
||||
searchMarker.push(sm)
|
||||
if (sm.nextItem) sm.nextItem.marker = true
|
||||
}
|
||||
// iterate to right if possible
|
||||
while (p.right !== null && pindex < index) {
|
||||
if (!p.deleted && p.countable) {
|
||||
if (index < pindex + p.length) {
|
||||
break
|
||||
const sm = searchMarker.reduce(
|
||||
(a, b, arrayIndex) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b
|
||||
)
|
||||
const newIsCheaper = math.abs(sm.index - index) > index
|
||||
const createFreshMarker = searchMarker.length < maxSearchMarker && (math.abs(sm.index - index) > 5 || newIsCheaper)
|
||||
const fsm = createFreshMarker ? (newIsCheaper ? new ListIterator(yarray) : sm.clone()) : sm
|
||||
const prevItem = /** @type {Item} */ (sm.nextItem)
|
||||
if (createFreshMarker) {
|
||||
searchMarker.push(fsm)
|
||||
}
|
||||
pindex += p.length
|
||||
}
|
||||
p = p.right
|
||||
}
|
||||
// iterate to left if necessary (might be that pindex > index)
|
||||
while (p.left !== null && pindex > index) {
|
||||
p = p.left
|
||||
if (!p.deleted && p.countable) {
|
||||
pindex -= p.length
|
||||
}
|
||||
}
|
||||
// we want to make sure that p can't be merged with left, because that would screw up everything
|
||||
// in that cas just return what we have (it is most likely the best marker anyway)
|
||||
// iterate to left until p can't be merged with left
|
||||
while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) {
|
||||
p = p.left
|
||||
if (!p.deleted && p.countable) {
|
||||
pindex -= p.length
|
||||
}
|
||||
}
|
||||
|
||||
// @todo remove!
|
||||
// assure position
|
||||
// {
|
||||
// let start = yarray._start
|
||||
// let pos = 0
|
||||
// while (start !== p) {
|
||||
// if (!start.deleted && start.countable) {
|
||||
// pos += start.length
|
||||
// }
|
||||
// start = /** @type {Item} */ (start.right)
|
||||
// }
|
||||
// if (pos !== pindex) {
|
||||
// debugger
|
||||
// throw new Error('Gotcha position fail!')
|
||||
// }
|
||||
// }
|
||||
// if (marker) {
|
||||
// if (window.lengthes == null) {
|
||||
// window.lengthes = []
|
||||
// window.getLengthes = () => window.lengthes.sort((a, b) => a - b)
|
||||
// }
|
||||
// window.lengthes.push(marker.index - pindex)
|
||||
// console.log('distance', marker.index - pindex, 'len', p && p.parent.length)
|
||||
// }
|
||||
if (marker !== null && math.abs(marker.index - pindex) < /** @type {YText|YArray<any>} */ (p.parent).length / maxSearchMarker) {
|
||||
// adjust existing marker
|
||||
overwriteMarker(marker, p, pindex)
|
||||
return marker
|
||||
const diff = fsm.index - index
|
||||
if (diff > 0) {
|
||||
fsm.backward(tr, diff)
|
||||
} else {
|
||||
// create new marker
|
||||
return markPosition(yarray._searchMarker, p, pindex)
|
||||
fsm.forward(tr, -diff)
|
||||
}
|
||||
// @todo remove this tests
|
||||
/*
|
||||
const otherTesting = new ListIterator(yarray)
|
||||
otherTesting.forward(tr, index)
|
||||
if (otherTesting.nextItem !== fsm.nextItem || otherTesting.index !== fsm.index || otherTesting.reachedEnd !== fsm.reachedEnd) {
|
||||
throw new Error('udtirane')
|
||||
}
|
||||
*/
|
||||
const result = f(fsm)
|
||||
if (fsm.reachedEnd) {
|
||||
fsm.reachedEnd = false
|
||||
const nextItem = /** @type {Item} */ (fsm.nextItem)
|
||||
if (nextItem.countable && !nextItem.deleted) {
|
||||
fsm.index -= nextItem.length
|
||||
}
|
||||
fsm.rel = 0
|
||||
}
|
||||
if (!createFreshMarker) {
|
||||
// reused old marker and we moved to a different position
|
||||
prevItem.marker = false
|
||||
}
|
||||
const fsmItem = fsm.nextItem
|
||||
if (fsmItem) {
|
||||
if (fsmItem.marker) {
|
||||
// already marked, forget current iterator
|
||||
searchMarker.splice(searchMarker.findIndex(m => m === fsm), 1)
|
||||
} else {
|
||||
fsmItem.marker = true
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,39 +98,25 @@ export const findMarker = (yarray, index) => {
|
||||
*
|
||||
* This should be called before doing a deletion!
|
||||
*
|
||||
* @param {Array<ArraySearchMarker>} searchMarker
|
||||
* @param {Array<ListIterator>} searchMarker
|
||||
* @param {number} index
|
||||
* @param {number} len If insertion, len is positive. If deletion, len is negative.
|
||||
* @param {ListIterator|null} origSearchMarker Do not update this searchmarker because it is the one we used to manipulate. @todo !=null for improved perf in ytext
|
||||
*/
|
||||
export const updateMarkerChanges = (searchMarker, index, len) => {
|
||||
export const updateMarkerChanges = (searchMarker, index, len, origSearchMarker) => {
|
||||
for (let i = searchMarker.length - 1; i >= 0; i--) {
|
||||
const m = searchMarker[i]
|
||||
if (len > 0) {
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let p = m.p
|
||||
p.marker = false
|
||||
// Ideally we just want to do a simple position comparison, but this will only work if
|
||||
// search markers don't point to deleted items for formats.
|
||||
// Iterate marker to prev undeleted countable position so we know what to do when updating a position
|
||||
while (p && (p.deleted || !p.countable)) {
|
||||
p = p.left
|
||||
if (p && !p.deleted && p.countable) {
|
||||
// adjust position. the loop should break now
|
||||
m.index -= p.length
|
||||
}
|
||||
}
|
||||
if (p === null || p.marker === true) {
|
||||
// remove search marker if updated position is null or if position is already marked
|
||||
const marker = searchMarker[i]
|
||||
if (marker !== origSearchMarker) {
|
||||
if (len > 0 && index === marker.index) {
|
||||
// inserting at a marked position deletes the marked position because we can't do a simple transformation
|
||||
// (we don't know whether to insert directly before or directly after the position)
|
||||
searchMarker.splice(i, 1)
|
||||
if (marker.nextItem) marker.nextItem.marker = false
|
||||
continue
|
||||
}
|
||||
m.p = p
|
||||
p.marker = true
|
||||
if (index < marker.index) { // a simple index <= m.index check would actually suffice
|
||||
marker.index = math.max(index, marker.index + len)
|
||||
}
|
||||
if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice
|
||||
m.index = math.max(index, m.index + len)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,9 +194,16 @@ export class AbstractType {
|
||||
*/
|
||||
this._dEH = createEventHandler()
|
||||
/**
|
||||
* @type {null | Array<ArraySearchMarker>}
|
||||
* @type {null | Array<ListIterator>}
|
||||
*/
|
||||
this._searchMarker = null
|
||||
/**
|
||||
* You can store custom stuff here.
|
||||
* This might be useful to associate your application state to this shared type.
|
||||
*
|
||||
* @type {Map<any, any>}
|
||||
*/
|
||||
this.meta = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,7 +243,7 @@ export class AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) { }
|
||||
|
||||
@@ -594,36 +513,11 @@ export const typeListForEachSnapshot = (type, f, snapshot) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} index
|
||||
* @return {any}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeListGet = (type, index) => {
|
||||
const marker = findMarker(type, index)
|
||||
let n = type._start
|
||||
if (marker !== null) {
|
||||
n = marker.p
|
||||
index -= marker.index
|
||||
}
|
||||
for (; n !== null; n = n.right) {
|
||||
if (!n.deleted && n.countable) {
|
||||
if (index < n.length) {
|
||||
return n.content.getContent()[index]
|
||||
}
|
||||
index -= n.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {Item?} referenceItem
|
||||
* @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content
|
||||
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} content
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@@ -635,7 +529,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
const store = doc.store
|
||||
const right = referenceItem === null ? parent._start : referenceItem.right
|
||||
/**
|
||||
* @type {Array<Object|Array<any>|number>}
|
||||
* @type {Array<Object|Array<any>|number|null>}
|
||||
*/
|
||||
let jsonContent = []
|
||||
const packJsonContent = () => {
|
||||
@@ -646,6 +540,9 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
}
|
||||
}
|
||||
content.forEach(c => {
|
||||
if (c === null) {
|
||||
jsonContent.push(c)
|
||||
} else {
|
||||
switch (c.constructor) {
|
||||
case Number:
|
||||
case Object:
|
||||
@@ -675,104 +572,11 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
packJsonContent()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {number} index
|
||||
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
||||
if (index === 0) {
|
||||
if (parent._searchMarker) {
|
||||
updateMarkerChanges(parent._searchMarker, index, content.length)
|
||||
}
|
||||
return typeListInsertGenericsAfter(transaction, parent, null, content)
|
||||
}
|
||||
const startIndex = index
|
||||
const marker = findMarker(parent, index)
|
||||
let n = parent._start
|
||||
if (marker !== null) {
|
||||
n = marker.p
|
||||
index -= marker.index
|
||||
// we need to iterate one to the left so that the algorithm works
|
||||
if (index === 0) {
|
||||
// @todo refactor this as it actually doesn't consider formats
|
||||
n = n.prev // important! get the left undeleted item so that we can actually decrease index
|
||||
index += (n && n.countable && !n.deleted) ? n.length : 0
|
||||
}
|
||||
}
|
||||
for (; n !== null; n = n.right) {
|
||||
if (!n.deleted && n.countable) {
|
||||
if (index <= n.length) {
|
||||
if (index < n.length) {
|
||||
// insert in-between
|
||||
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
|
||||
}
|
||||
break
|
||||
}
|
||||
index -= n.length
|
||||
}
|
||||
}
|
||||
if (parent._searchMarker) {
|
||||
updateMarkerChanges(parent._searchMarker, startIndex, content.length)
|
||||
}
|
||||
return typeListInsertGenericsAfter(transaction, parent, n, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {number} index
|
||||
* @param {number} length
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeListDelete = (transaction, parent, index, length) => {
|
||||
if (length === 0) { return }
|
||||
const startIndex = index
|
||||
const startLength = length
|
||||
const marker = findMarker(parent, index)
|
||||
let n = parent._start
|
||||
if (marker !== null) {
|
||||
n = marker.p
|
||||
index -= marker.index
|
||||
}
|
||||
// compute the first item to be deleted
|
||||
for (; n !== null && index > 0; n = n.right) {
|
||||
if (!n.deleted && n.countable) {
|
||||
if (index < n.length) {
|
||||
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
|
||||
}
|
||||
index -= n.length
|
||||
}
|
||||
}
|
||||
// delete all items until done
|
||||
while (length > 0 && n !== null) {
|
||||
if (!n.deleted) {
|
||||
if (length < n.length) {
|
||||
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
|
||||
}
|
||||
n.delete(transaction)
|
||||
length -= n.length
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
if (length > 0) {
|
||||
throw error.create('array length exceeded')
|
||||
}
|
||||
if (parent._searchMarker) {
|
||||
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
@@ -792,7 +596,7 @@ export const typeMapDelete = (transaction, parent, key) => {
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @param {Object|number|Array<any>|string|Uint8Array|AbstractType<any>} value
|
||||
* @param {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} value
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@@ -833,7 +637,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
|
||||
/**
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||
* @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@@ -845,7 +649,7 @@ export const typeMapGet = (parent, key) => {
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} parent
|
||||
* @return {Object<string,Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
|
||||
* @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@@ -880,7 +684,7 @@ export const typeMapHas = (parent, key) => {
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||
* @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
|
||||
@@ -5,19 +5,14 @@
|
||||
import {
|
||||
YEvent,
|
||||
AbstractType,
|
||||
typeListGet,
|
||||
typeListToArray,
|
||||
typeListForEach,
|
||||
typeListCreateIterator,
|
||||
typeListInsertGenerics,
|
||||
typeListDelete,
|
||||
typeListMap,
|
||||
YArrayRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, Transaction, Item // eslint-disable-line
|
||||
ListIterator,
|
||||
useSearchMarker,
|
||||
createRelativePositionFromTypeIndex,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
import { typeListSlice } from './AbstractType.js'
|
||||
|
||||
/**
|
||||
* Event that describes the changes on a YArray
|
||||
@@ -49,7 +44,7 @@ export class YArray extends AbstractType {
|
||||
*/
|
||||
this._prelimContent = []
|
||||
/**
|
||||
* @type {Array<ArraySearchMarker>}
|
||||
* @type {Array<ListIterator>}
|
||||
*/
|
||||
this._searchMarker = []
|
||||
}
|
||||
@@ -129,14 +124,72 @@ export class YArray extends AbstractType {
|
||||
* @param {Array<T>} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (content.length > 0) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, content)
|
||||
useSearchMarker(transaction, this, index, walker =>
|
||||
walker.insertArrayValue(transaction, content)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a single item from $index to $target.
|
||||
*
|
||||
* @todo make sure that collapsed moves are removed (i.e. when moving the same item twice)
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {number} target
|
||||
*/
|
||||
move (index, target) {
|
||||
if (index === target || index + 1 === target || index >= this.length) {
|
||||
// It doesn't make sense to move a range into the same range (it's basically a no-op).
|
||||
return
|
||||
}
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
const left = createRelativePositionFromTypeIndex(this, index, 1)
|
||||
const right = left.clone()
|
||||
right.assoc = -1
|
||||
useSearchMarker(transaction, this, target, walker => {
|
||||
walker.insertMove(transaction, left, right)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const content = /** @type {Array<any>} */ (this._prelimContent).splice(index, 1)
|
||||
;/** @type {Array<any>} */ (this._prelimContent).splice(target, 0, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} start Inclusive move-start
|
||||
* @param {number} end Inclusive move-end
|
||||
* @param {number} target
|
||||
* @param {number} assocStart >=0 if start should be associated with the right character. See relative-position assoc parameter.
|
||||
* @param {number} assocEnd >= 0 if end should be associated with the right character.
|
||||
*/
|
||||
moveRange (start, end, target, assocStart = 1, assocEnd = -1) {
|
||||
if (start <= target && target <= end) {
|
||||
// It doesn't make sense to move a range into the same range (it's basically a no-op).
|
||||
return
|
||||
}
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
const left = createRelativePositionFromTypeIndex(this, start, assocStart)
|
||||
const right = createRelativePositionFromTypeIndex(this, end + 1, assocEnd)
|
||||
useSearchMarker(transaction, this, target, walker => {
|
||||
walker.insertMove(transaction, left, right)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const content = /** @type {Array<any>} */ (this._prelimContent).splice(start, end - start + 1)
|
||||
;/** @type {Array<any>} */ (this._prelimContent).splice(target, 0, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends content to this YArray.
|
||||
@@ -165,7 +218,9 @@ export class YArray extends AbstractType {
|
||||
delete (index, length = 1) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
useSearchMarker(transaction, this, index, walker =>
|
||||
walker.delete(transaction, length)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
|
||||
@@ -179,7 +234,11 @@ export class YArray extends AbstractType {
|
||||
* @return {T}
|
||||
*/
|
||||
get (index) {
|
||||
return typeListGet(this, index)
|
||||
return transact(/** @type {Doc} */ (this.doc), transaction =>
|
||||
useSearchMarker(transaction, this, index, walker =>
|
||||
walker.slice(transaction, 1)[0]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +247,9 @@ export class YArray extends AbstractType {
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeListToArray(this)
|
||||
return transact(/** @type {Doc} */ (this.doc), tr =>
|
||||
new ListIterator(this).slice(tr, this.length)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +260,11 @@ export class YArray extends AbstractType {
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
slice (start = 0, end = this.length) {
|
||||
return typeListSlice(this, start, end)
|
||||
return transact(/** @type {Doc} */ (this.doc), transaction =>
|
||||
useSearchMarker(transaction, this, start, walker =>
|
||||
walker.slice(transaction, end < 0 ? this.length + end - start : end - start)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,13 +280,15 @@ export class YArray extends AbstractType {
|
||||
* Returns an Array with the result of calling a provided function on every
|
||||
* element of this YArray.
|
||||
*
|
||||
* @template T,M
|
||||
* @template M
|
||||
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
|
||||
* @return {Array<M>} A new array with each element being the result of the
|
||||
* callback function
|
||||
*/
|
||||
map (f) {
|
||||
return typeListMap(this, /** @type {any} */ (f))
|
||||
return transact(/** @type {Doc} */ (this.doc), tr =>
|
||||
new ListIterator(this).map(tr, f)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,18 +297,20 @@ export class YArray extends AbstractType {
|
||||
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
typeListForEach(this, f)
|
||||
return transact(/** @type {Doc} */ (this.doc), tr =>
|
||||
new ListIterator(this).forEach(tr, f)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {IterableIterator<T>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return typeListCreateIterator(this)
|
||||
return this.toArray().values()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YArrayRefID)
|
||||
@@ -249,7 +318,7 @@ export class YArray extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
YMapRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, Transaction, Item // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as iterator from 'lib0/iterator.js'
|
||||
import * as iterator from 'lib0/iterator'
|
||||
|
||||
/**
|
||||
* @template T
|
||||
@@ -36,11 +36,11 @@ export class YMapEvent extends YEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T number|string|Object|Array|Uint8Array
|
||||
* @template MapType
|
||||
* A shared Map implementation.
|
||||
*
|
||||
* @extends AbstractType<YMapEvent<T>>
|
||||
* @implements {Iterable<T>}
|
||||
* @extends AbstractType<YMapEvent<MapType>>
|
||||
* @implements {Iterable<MapType>}
|
||||
*/
|
||||
export class YMap extends AbstractType {
|
||||
/**
|
||||
@@ -85,7 +85,7 @@ export class YMap extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {YMap<T>}
|
||||
* @return {YMap<MapType>}
|
||||
*/
|
||||
clone () {
|
||||
const map = new YMap()
|
||||
@@ -108,11 +108,11 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Object<string,T>}
|
||||
* @return {Object<string,any>}
|
||||
*/
|
||||
toJSON () {
|
||||
/**
|
||||
* @type {Object<string,T>}
|
||||
* @type {Object<string,MapType>}
|
||||
*/
|
||||
const map = {}
|
||||
this._map.forEach((item, key) => {
|
||||
@@ -163,11 +163,11 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Executes a provided function on once on every key-value pair.
|
||||
*
|
||||
* @param {function(T,string,YMap<T>):void} f A function to execute on every element of this YArray.
|
||||
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
/**
|
||||
* @type {Object<string,T>}
|
||||
* @type {Object<string,MapType>}
|
||||
*/
|
||||
const map = {}
|
||||
this._map.forEach((item, key) => {
|
||||
@@ -179,7 +179,7 @@ export class YMap extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {IterableIterator<T>}
|
||||
* @return {IterableIterator<MapType>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return this.entries()
|
||||
@@ -204,7 +204,7 @@ export class YMap extends AbstractType {
|
||||
* Adds or updates an element with a specified key and value.
|
||||
*
|
||||
* @param {string} key The key of the element to add to this YMap
|
||||
* @param {T} value The value of the element to add
|
||||
* @param {MapType} value The value of the element to add
|
||||
*/
|
||||
set (key, value) {
|
||||
if (this.doc !== null) {
|
||||
@@ -221,7 +221,7 @@ export class YMap extends AbstractType {
|
||||
* Returns a specified element from this YMap.
|
||||
*
|
||||
* @param {string} key
|
||||
* @return {T|undefined}
|
||||
* @return {MapType|undefined}
|
||||
*/
|
||||
get (key) {
|
||||
return /** @type {any} */ (typeMapGet(this, key))
|
||||
@@ -238,7 +238,22 @@ export class YMap extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* Removes all elements from this YMap.
|
||||
*/
|
||||
clear () {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
this.forEach(function (value, key, map) {
|
||||
typeMapDelete(transaction, map, key)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
/** @type {Map<string, any>} */ (this._prelimContent).clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YMapRefID)
|
||||
@@ -246,7 +261,7 @@ export class YMap extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
|
||||
@@ -20,18 +20,20 @@ import {
|
||||
splitSnapshotAffectedStructs,
|
||||
iterateDeletedStructs,
|
||||
iterateStructs,
|
||||
findMarker,
|
||||
typeMapDelete,
|
||||
typeMapSet,
|
||||
typeMapGet,
|
||||
typeMapGetAll,
|
||||
updateMarkerChanges,
|
||||
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||
ContentType,
|
||||
useSearchMarker,
|
||||
findIndexCleanStart,
|
||||
ListIterator, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as object from 'lib0/object.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as object from 'lib0/object'
|
||||
import * as map from 'lib0/map'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @param {any} a
|
||||
@@ -62,17 +64,16 @@ export class ItemTextListPosition {
|
||||
error.unexpectedCase()
|
||||
}
|
||||
switch (this.right.content.constructor) {
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
if (!this.right.deleted) {
|
||||
this.index += this.right.length
|
||||
}
|
||||
break
|
||||
case ContentFormat:
|
||||
if (!this.right.deleted) {
|
||||
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (!this.right.deleted) {
|
||||
this.index += this.right.length
|
||||
}
|
||||
break
|
||||
}
|
||||
this.left = this.right
|
||||
this.right = this.right.right
|
||||
@@ -91,8 +92,12 @@ export class ItemTextListPosition {
|
||||
const findNextPosition = (transaction, pos, count) => {
|
||||
while (pos.right !== null && count > 0) {
|
||||
switch (pos.right.content.constructor) {
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
case ContentFormat:
|
||||
if (!pos.right.deleted) {
|
||||
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (!pos.right.deleted) {
|
||||
if (count < pos.right.length) {
|
||||
// split right
|
||||
@@ -102,11 +107,6 @@ const findNextPosition = (transaction, pos, count) => {
|
||||
count -= pos.right.length
|
||||
}
|
||||
break
|
||||
case ContentFormat:
|
||||
if (!pos.right.deleted) {
|
||||
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
|
||||
}
|
||||
break
|
||||
}
|
||||
pos.left = pos.right
|
||||
pos.right = pos.right.right
|
||||
@@ -126,10 +126,30 @@ const findNextPosition = (transaction, pos, count) => {
|
||||
*/
|
||||
const findPosition = (transaction, parent, index) => {
|
||||
const currentAttributes = new Map()
|
||||
const marker = findMarker(parent, index)
|
||||
if (marker) {
|
||||
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
|
||||
return findNextPosition(transaction, pos, index - marker.index)
|
||||
if (parent._searchMarker) {
|
||||
return useSearchMarker(transaction, parent, index, listIter => {
|
||||
let left, right
|
||||
if (listIter.rel > 0) {
|
||||
// must exist because rel > 0
|
||||
const nextItem = /** @type {Item} */ (listIter.nextItem)
|
||||
if (listIter.rel === nextItem.length) {
|
||||
left = nextItem
|
||||
right = left.right
|
||||
} else {
|
||||
const structs = /** @type {Array<Item|GC>} */ (transaction.doc.store.clients.get(nextItem.id.client))
|
||||
const after = /** @type {Item} */ (structs[findIndexCleanStart(transaction, structs, nextItem.id.clock + listIter.rel)])
|
||||
listIter.nextItem = after
|
||||
listIter.rel = 0
|
||||
left = listIter.left
|
||||
right = listIter.right
|
||||
}
|
||||
} else {
|
||||
left = listIter.left
|
||||
right = listIter.right
|
||||
}
|
||||
// @todo this should simply split if .rel > 0
|
||||
return new ItemTextListPosition(left, right, index, currentAttributes)
|
||||
})
|
||||
} else {
|
||||
const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes)
|
||||
return findNextPosition(transaction, pos, index)
|
||||
@@ -164,11 +184,13 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
|
||||
}
|
||||
const doc = transaction.doc
|
||||
const ownClientId = doc.clientID
|
||||
let left = currPos.left
|
||||
const right = currPos.right
|
||||
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))
|
||||
left.integrate(transaction, 0)
|
||||
const left = currPos.left
|
||||
const right = currPos.right
|
||||
const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||
nextFormat.integrate(transaction, 0)
|
||||
currPos.right = nextFormat
|
||||
currPos.forward()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -243,7 +265,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {ItemTextListPosition} currPos
|
||||
* @param {string|object} text
|
||||
* @param {string|object|AbstractType<any>} text
|
||||
* @param {Object<string,any>} attributes
|
||||
*
|
||||
* @private
|
||||
@@ -260,10 +282,10 @@ const insertText = (transaction, parent, currPos, text, attributes) => {
|
||||
minimizeAttributeChanges(currPos, attributes)
|
||||
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
|
||||
// insert content
|
||||
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
|
||||
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text))
|
||||
let { left, right, index } = currPos
|
||||
if (parent._searchMarker) {
|
||||
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
|
||||
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength(), null)
|
||||
}
|
||||
right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
|
||||
right.integrate(transaction, 0)
|
||||
@@ -306,8 +328,7 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
default:
|
||||
if (length < currPos.right.length) {
|
||||
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
|
||||
}
|
||||
@@ -346,7 +367,7 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
|
||||
* @function
|
||||
*/
|
||||
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => {
|
||||
while (end && end.content.constructor !== ContentString && end.content.constructor !== ContentEmbed) {
|
||||
while (end && (!end.countable || end.deleted)) {
|
||||
if (!end.deleted && end.content.constructor === ContentFormat) {
|
||||
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
|
||||
}
|
||||
@@ -379,12 +400,12 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
|
||||
*/
|
||||
const cleanupContextlessFormattingGap = (transaction, item) => {
|
||||
// iterate until item.right is null or content
|
||||
while (item && item.right && (item.right.deleted || (item.right.content.constructor !== ContentString && item.right.content.constructor !== ContentEmbed))) {
|
||||
while (item && item.right && (item.right.deleted || !item.right.countable)) {
|
||||
item = item.right
|
||||
}
|
||||
const attrs = new Set()
|
||||
// iterate back until a content item is found
|
||||
while (item && (item.deleted || (item.content.constructor !== ContentString && item.content.constructor !== ContentEmbed))) {
|
||||
while (item && (item.deleted || !item.countable)) {
|
||||
if (!item.deleted && item.content.constructor === ContentFormat) {
|
||||
const key = /** @type {ContentFormat} */ (item.content).key
|
||||
if (attrs.has(key)) {
|
||||
@@ -422,8 +443,7 @@ export const cleanupYTextFormatting = type => {
|
||||
case ContentFormat:
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
|
||||
break
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
default:
|
||||
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
|
||||
startAttributes = map.copy(currentAttributes)
|
||||
start = end
|
||||
@@ -452,6 +472,7 @@ const deleteText = (transaction, currPos, length) => {
|
||||
while (length > 0 && currPos.right !== null) {
|
||||
if (currPos.right.deleted === false) {
|
||||
switch (currPos.right.content.constructor) {
|
||||
case ContentType:
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
if (length < currPos.right.length) {
|
||||
@@ -469,7 +490,7 @@ const deleteText = (transaction, currPos, length) => {
|
||||
}
|
||||
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
|
||||
if (parent._searchMarker) {
|
||||
updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length)
|
||||
updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length, null)
|
||||
}
|
||||
return currPos
|
||||
}
|
||||
@@ -501,14 +522,6 @@ const deleteText = (transaction, currPos, length) => {
|
||||
* @typedef {Object} TextAttributes
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DeltaItem
|
||||
* @property {number|undefined} DeltaItem.delete
|
||||
* @property {number|undefined} DeltaItem.retain
|
||||
* @property {string|undefined} DeltaItem.insert
|
||||
* @property {Object<string,any>} DeltaItem.attributes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event that describes the changes on a YText type.
|
||||
*/
|
||||
@@ -520,10 +533,6 @@ export class YTextEvent extends YEvent {
|
||||
*/
|
||||
constructor (ytext, transaction, subs) {
|
||||
super(ytext, transaction)
|
||||
/**
|
||||
* @type {Array<DeltaItem>|null}
|
||||
*/
|
||||
this._delta = null
|
||||
/**
|
||||
* Whether the children changed.
|
||||
* @type {Boolean}
|
||||
@@ -544,20 +553,41 @@ export class YTextEvent extends YEvent {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
||||
*/
|
||||
get changes () {
|
||||
if (this._changes === null) {
|
||||
/**
|
||||
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string|AbstractType<any>|object, delete?:number, retain?:number}>}}
|
||||
*/
|
||||
const changes = {
|
||||
keys: this.keys,
|
||||
delta: this.delta,
|
||||
added: new Set(),
|
||||
deleted: new Set()
|
||||
}
|
||||
this._changes = changes
|
||||
}
|
||||
return /** @type {any} */ (this._changes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the changes in the delta format.
|
||||
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
||||
*
|
||||
* @type {Array<DeltaItem>}
|
||||
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
get delta () {
|
||||
if (this._delta === null) {
|
||||
const y = /** @type {Doc} */ (this.target.doc)
|
||||
this._delta = []
|
||||
/**
|
||||
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
|
||||
*/
|
||||
const delta = []
|
||||
transact(y, transaction => {
|
||||
const delta = /** @type {Array<DeltaItem>} */ (this._delta)
|
||||
const currentAttributes = new Map() // saves all current attributes for insert
|
||||
const oldAttributes = new Map()
|
||||
let item = this.target._start
|
||||
@@ -615,12 +645,13 @@ export class YTextEvent extends YEvent {
|
||||
}
|
||||
while (item !== null) {
|
||||
switch (item.content.constructor) {
|
||||
case ContentType:
|
||||
case ContentEmbed:
|
||||
if (this.adds(item)) {
|
||||
if (!this.deletes(item)) {
|
||||
addOp()
|
||||
action = 'insert'
|
||||
insert = /** @type {ContentEmbed} */ (item.content).embed
|
||||
insert = item.content.getContent()[0]
|
||||
addOp()
|
||||
}
|
||||
} else if (this.deletes(item)) {
|
||||
@@ -696,9 +727,9 @@ export class YTextEvent extends YEvent {
|
||||
addOp()
|
||||
}
|
||||
if (value === null) {
|
||||
attributes[key] = value
|
||||
} else {
|
||||
delete attributes[key]
|
||||
} else {
|
||||
attributes[key] = value
|
||||
}
|
||||
} else {
|
||||
item.delete(transaction)
|
||||
@@ -727,8 +758,9 @@ export class YTextEvent extends YEvent {
|
||||
}
|
||||
}
|
||||
})
|
||||
this._delta = delta
|
||||
}
|
||||
return this._delta
|
||||
return /** @type {any} */ (this._delta)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,7 +785,7 @@ export class YText extends AbstractType {
|
||||
*/
|
||||
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
||||
/**
|
||||
* @type {Array<ArraySearchMarker>}
|
||||
* @type {Array<ListIterator>}
|
||||
*/
|
||||
this._searchMarker = []
|
||||
}
|
||||
@@ -804,6 +836,7 @@ export class YText extends AbstractType {
|
||||
super._callObserver(transaction, parentSubs)
|
||||
const event = new YTextEvent(this, transaction, parentSubs)
|
||||
const doc = transaction.doc
|
||||
callTypeObservers(this, transaction, event)
|
||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||
if (!transaction.local) {
|
||||
// check if another formatting item was inserted
|
||||
@@ -852,7 +885,6 @@ export class YText extends AbstractType {
|
||||
}
|
||||
})
|
||||
}
|
||||
callTypeObservers(this, transaction, event)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -996,13 +1028,14 @@ export class YText extends AbstractType {
|
||||
str += /** @type {ContentString} */ (n.content).str
|
||||
break
|
||||
}
|
||||
case ContentType:
|
||||
case ContentEmbed: {
|
||||
packStr()
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
const op = {
|
||||
insert: /** @type {ContentEmbed} */ (n.content).embed
|
||||
insert: n.content.getContent()[0]
|
||||
}
|
||||
if (currentAttributes.size > 0) {
|
||||
const attrs = /** @type {Object<string,any>} */ ({})
|
||||
@@ -1063,16 +1096,13 @@ export class YText extends AbstractType {
|
||||
* Inserts an embed at a index.
|
||||
*
|
||||
* @param {number} index The index to insert the embed at.
|
||||
* @param {Object} embed The Object that represents the embed.
|
||||
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
|
||||
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||
* embed
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
insertEmbed (index, embed, attributes = {}) {
|
||||
if (embed.constructor !== Object) {
|
||||
throw new Error('Embed must be an Object')
|
||||
}
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
@@ -1203,7 +1233,7 @@ export class YText extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YTextRefID)
|
||||
@@ -1211,7 +1241,7 @@ export class YText extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {YText}
|
||||
*
|
||||
* @private
|
||||
|
||||
@@ -3,12 +3,13 @@ import {
|
||||
YXmlFragment,
|
||||
transact,
|
||||
typeMapDelete,
|
||||
typeMapHas,
|
||||
typeMapSet,
|
||||
typeMapGet,
|
||||
typeMapGetAll,
|
||||
typeListForEach,
|
||||
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'
|
||||
|
||||
/**
|
||||
@@ -81,7 +82,7 @@ export class YXmlElement extends YXmlFragment {
|
||||
el.setAttribute(key, attrs[key])
|
||||
}
|
||||
// @ts-ignore
|
||||
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||
return el
|
||||
}
|
||||
|
||||
@@ -160,6 +161,18 @@ export class YXmlElement extends YXmlFragment {
|
||||
return /** @type {any} */ (typeMapGet(this, attributeName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an attribute exists
|
||||
*
|
||||
* @param {String} attributeName The attribute name to check for existence.
|
||||
* @return {boolean} whether the attribute exists.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
hasAttribute (attributeName) {
|
||||
return /** @type {any} */ (typeMapHas(this, attributeName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all attribute name/value pairs in a JSON Object.
|
||||
*
|
||||
@@ -208,7 +221,7 @@ export class YXmlElement extends YXmlFragment {
|
||||
*
|
||||
* 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) {
|
||||
encoder.writeTypeRef(YXmlElementRefID)
|
||||
@@ -217,7 +230,7 @@ export class YXmlElement extends YXmlFragment {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {YXmlElement}
|
||||
*
|
||||
* @function
|
||||
|
||||
@@ -8,19 +8,17 @@ import {
|
||||
AbstractType,
|
||||
typeListMap,
|
||||
typeListForEach,
|
||||
typeListInsertGenerics,
|
||||
typeListInsertGenericsAfter,
|
||||
typeListDelete,
|
||||
typeListToArray,
|
||||
YXmlFragmentRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
typeListGet,
|
||||
typeListSlice,
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||
useSearchMarker,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* Define the elements to which a set of CSS queries apply.
|
||||
@@ -83,7 +81,7 @@ export class YXmlTreeWalker {
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let n = this._currentNode
|
||||
let type = /** @type {any} */ (n.content).type
|
||||
let type = n && n.content && /** @type {any} */ (n.content).type
|
||||
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
|
||||
do {
|
||||
type = /** @type {any} */ (n.content).type
|
||||
@@ -167,7 +165,7 @@ export class YXmlFragment extends AbstractType {
|
||||
clone () {
|
||||
const el = new YXmlFragment()
|
||||
// @ts-ignore
|
||||
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||
return el
|
||||
}
|
||||
|
||||
@@ -304,9 +302,11 @@ export class YXmlFragment extends AbstractType {
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, content)
|
||||
})
|
||||
return transact(/** @type {Doc} */ (this.doc), transaction =>
|
||||
useSearchMarker(transaction, this, index, walker =>
|
||||
walker.insertArrayValue(transaction, content)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
this._prelimContent.splice(index, 0, ...content)
|
||||
@@ -347,9 +347,11 @@ export class YXmlFragment extends AbstractType {
|
||||
*/
|
||||
delete (index, length = 1) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
})
|
||||
transact(/** @type {Doc} */ (this.doc), transaction =>
|
||||
useSearchMarker(transaction, this, index, walker =>
|
||||
walker.delete(transaction, length)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
this._prelimContent.splice(index, length)
|
||||
@@ -390,7 +392,11 @@ export class YXmlFragment extends AbstractType {
|
||||
* @return {YXmlElement|YXmlText}
|
||||
*/
|
||||
get (index) {
|
||||
return typeListGet(this, index)
|
||||
return transact(/** @type {Doc} */ (this.doc), transaction =>
|
||||
useSearchMarker(transaction, this, index, walker =>
|
||||
walker.slice(transaction, 1)[0]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,7 +416,7 @@ export class YXmlFragment extends AbstractType {
|
||||
*
|
||||
* 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) {
|
||||
encoder.writeTypeRef(YXmlFragmentRefID)
|
||||
@@ -418,7 +424,7 @@ export class YXmlFragment extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {YXmlFragment}
|
||||
*
|
||||
* @private
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
YMap,
|
||||
YXmlHookRefID,
|
||||
AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
@@ -76,7 +76,7 @@ export class YXmlHook extends YMap {
|
||||
*
|
||||
* 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) {
|
||||
encoder.writeTypeRef(YXmlHookRefID)
|
||||
@@ -85,7 +85,7 @@ export class YXmlHook extends YMap {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {YXmlHook}
|
||||
*
|
||||
* @private
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
YText,
|
||||
YXmlTextRefID,
|
||||
ContentType, YXmlElement, AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line
|
||||
ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
@@ -104,7 +104,7 @@ export class YXmlText extends YText {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YXmlTextRefID)
|
||||
@@ -112,7 +112,7 @@ export class YXmlText extends YText {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {YXmlText}
|
||||
*
|
||||
* @private
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
import { Observable } from 'lib0/observable'
|
||||
|
||||
import {
|
||||
Doc // eslint-disable-line
|
||||
|
||||
@@ -4,14 +4,15 @@ import {
|
||||
getState,
|
||||
splitItem,
|
||||
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'
|
||||
|
||||
import * as array from 'lib0/array.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as array from 'lib0/array'
|
||||
import * as math from 'lib0/math'
|
||||
import * as map from 'lib0/map'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
|
||||
export class DeleteItem {
|
||||
/**
|
||||
@@ -121,8 +122,8 @@ export const sortAndMergeDeleteSet = ds => {
|
||||
for (i = 1, j = 1; i < dels.length; i++) {
|
||||
const left = dels[j - 1]
|
||||
const right = dels[i]
|
||||
if (left.clock + left.len === right.clock) {
|
||||
left.len += right.len
|
||||
if (left.clock + left.len >= right.clock) {
|
||||
left.len = math.max(left.len, right.clock + right.len - left.clock)
|
||||
} else {
|
||||
if (j < i) {
|
||||
dels[j] = right
|
||||
@@ -195,7 +196,7 @@ export const createDeleteSetFromStructStore = ss => {
|
||||
const clock = struct.id.clock
|
||||
let len = struct.length
|
||||
if (i + 1 < structs.length) {
|
||||
for (let next = structs[i + 1]; i + 1 < structs.length && next.id.clock === clock + len && next.deleted; next = structs[++i + 1]) {
|
||||
for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) {
|
||||
len += next.length
|
||||
}
|
||||
}
|
||||
@@ -210,7 +211,7 @@ export const createDeleteSetFromStructStore = ss => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractDSEncoder} encoder
|
||||
* @param {DSEncoderV1 | DSEncoderV2} encoder
|
||||
* @param {DeleteSet} ds
|
||||
*
|
||||
* @private
|
||||
@@ -232,7 +233,7 @@ export const writeDeleteSet = (encoder, ds) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractDSDecoder} decoder
|
||||
* @param {DSDecoderV1 | DSDecoderV2} decoder
|
||||
* @return {DeleteSet}
|
||||
*
|
||||
* @private
|
||||
@@ -260,9 +261,10 @@ export const readDeleteSet = decoder => {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {AbstractDSDecoder} decoder
|
||||
* @param {DSDecoderV1 | DSDecoderV2} decoder
|
||||
* @param {Transaction} transaction
|
||||
* @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
|
||||
* @function
|
||||
@@ -315,9 +317,10 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
||||
}
|
||||
}
|
||||
if (unappliedDS.clients.size > 0) {
|
||||
// TODO: no need for encoding+decoding ds anymore
|
||||
const unappliedDSEncoder = new DSEncoderV2()
|
||||
writeDeleteSet(unappliedDSEncoder, unappliedDS)
|
||||
store.pendingDeleteReaders.push(new DSDecoderV2(decoding.createDecoder((unappliedDSEncoder.toUint8Array()))))
|
||||
const ds = new UpdateEncoderV2()
|
||||
encoding.writeVarUint(ds.restEncoder, 0) // encode 0 structs
|
||||
writeDeleteSet(ds, unappliedDS)
|
||||
return ds.toUint8Array()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
ContentDoc, Item, Transaction, YEvent // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
import * as random from 'lib0/random.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as array from 'lib0/array.js'
|
||||
import { Observable } from 'lib0/observable'
|
||||
import * as random from 'lib0/random'
|
||||
import * as map from 'lib0/map'
|
||||
import * as array from 'lib0/array'
|
||||
import * as promise from 'lib0/promise'
|
||||
|
||||
export const generateNewClientId = random.uint32
|
||||
|
||||
@@ -25,8 +26,10 @@ export const generateNewClientId = random.uint32
|
||||
* @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true)
|
||||
* @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
|
||||
* @property {string} [DocOpts.guid] Define a globally unique identifier for this document
|
||||
* @property {string | null} [DocOpts.collectionid] Associate this document with a collection. This only plays a role if your provider has a concept of collection.
|
||||
* @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well.
|
||||
* @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically.
|
||||
* @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load()
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -37,12 +40,13 @@ export class Doc extends Observable {
|
||||
/**
|
||||
* @param {DocOpts} [opts] configuration
|
||||
*/
|
||||
constructor ({ guid = random.uuidv4(), gc = true, gcFilter = () => true, meta = null, autoLoad = false } = {}) {
|
||||
constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) {
|
||||
super()
|
||||
this.gc = gc
|
||||
this.gcFilter = gcFilter
|
||||
this.clientID = generateNewClientId()
|
||||
this.guid = guid
|
||||
this.collectionid = collectionid
|
||||
/**
|
||||
* @type {Map<string, AbstractType<YEvent>>}
|
||||
*/
|
||||
@@ -65,9 +69,16 @@ export class Doc extends Observable {
|
||||
* @type {Item?}
|
||||
*/
|
||||
this._item = null
|
||||
this.shouldLoad = autoLoad
|
||||
this.shouldLoad = shouldLoad
|
||||
this.autoLoad = autoLoad
|
||||
this.meta = meta
|
||||
this.isLoaded = false
|
||||
this.whenLoaded = promise.create(resolve => {
|
||||
this.on('load', () => {
|
||||
this.isLoaded = true
|
||||
resolve(this)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,8 +205,9 @@ export class Doc extends Observable {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {string} [name]
|
||||
* @return {YMap<any>}
|
||||
* @return {YMap<T>}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -245,16 +257,12 @@ export class Doc extends Observable {
|
||||
if (item !== null) {
|
||||
this._item = null
|
||||
const content = /** @type {ContentDoc} */ (item.content)
|
||||
if (item.deleted) {
|
||||
// @ts-ignore
|
||||
content.doc = null
|
||||
} else {
|
||||
content.doc = new Doc({ guid: this.guid, ...content.opts })
|
||||
content.doc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false })
|
||||
content.doc._item = item
|
||||
}
|
||||
transact(/** @type {any} */ (item).parent.doc, transaction => {
|
||||
const doc = content.doc
|
||||
if (!item.deleted) {
|
||||
transaction.subdocsAdded.add(content.doc)
|
||||
transaction.subdocsAdded.add(doc)
|
||||
}
|
||||
transaction.subdocsRemoved.add(this)
|
||||
}, null, true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as f from 'lib0/function.js'
|
||||
import * as f from 'lib0/function'
|
||||
|
||||
/**
|
||||
* General event handler implementation.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
import { AbstractType } from '../internals.js' // eslint-disable-line
|
||||
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class ID {
|
||||
/**
|
||||
|
||||
510
src/utils/ListIterator.js
Normal file
510
src/utils/ListIterator.js
Normal file
@@ -0,0 +1,510 @@
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
import {
|
||||
getItemCleanStart,
|
||||
createID,
|
||||
getMovedCoords,
|
||||
updateMarkerChanges,
|
||||
getState,
|
||||
ContentAny,
|
||||
ContentBinary,
|
||||
ContentType,
|
||||
ContentDoc,
|
||||
Doc,
|
||||
RelativePosition, ID, AbstractContent, ContentMove, Transaction, Item, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
const lengthExceeded = error.create('Length exceeded!')
|
||||
|
||||
/**
|
||||
* @todo rename to walker?
|
||||
* @todo check that inserting character one after another always reuses ListIterators
|
||||
*/
|
||||
export class ListIterator {
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
*/
|
||||
constructor (type) {
|
||||
this.type = type
|
||||
/**
|
||||
* Current index-position
|
||||
*/
|
||||
this.index = 0
|
||||
/**
|
||||
* Relative position to the current item (if item.content.length > 1)
|
||||
*/
|
||||
this.rel = 0
|
||||
/**
|
||||
* This refers to the current right item, unless reachedEnd is true. Then it refers to the left item.
|
||||
*
|
||||
* @public
|
||||
* @type {Item | null}
|
||||
*/
|
||||
this.nextItem = type._start
|
||||
this.reachedEnd = type._start === null
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
this.currMove = null
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
this.currMoveStart = null
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
this.currMoveEnd = null
|
||||
/**
|
||||
* @type {Array<{ start: Item | null, end: Item | null, move: Item }>}
|
||||
*/
|
||||
this.movedStack = []
|
||||
}
|
||||
|
||||
clone () {
|
||||
const iter = new ListIterator(this.type)
|
||||
iter.index = this.index
|
||||
iter.rel = this.rel
|
||||
iter.nextItem = this.nextItem
|
||||
iter.reachedEnd = this.reachedEnd
|
||||
iter.currMove = this.currMove
|
||||
iter.currMoveStart = this.currMoveStart
|
||||
iter.currMoveEnd = this.currMoveEnd
|
||||
iter.movedStack = this.movedStack.slice()
|
||||
return iter
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
get left () {
|
||||
if (this.reachedEnd) {
|
||||
return this.nextItem
|
||||
} else {
|
||||
return this.nextItem && this.nextItem.left
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
get right () {
|
||||
if (this.reachedEnd) {
|
||||
return null
|
||||
} else {
|
||||
return this.nextItem
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {number} index
|
||||
*/
|
||||
moveTo (tr, index) {
|
||||
const diff = index - this.index
|
||||
if (diff > 0) {
|
||||
this.forward(tr, diff)
|
||||
} else if (diff < 0) {
|
||||
this.backward(tr, -diff)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {number} len
|
||||
*/
|
||||
forward (tr, len) {
|
||||
if (this.index + len > this.type._length) {
|
||||
throw lengthExceeded
|
||||
}
|
||||
let item = this.nextItem
|
||||
this.index += len
|
||||
if (this.rel) {
|
||||
len += this.rel
|
||||
this.rel = 0
|
||||
}
|
||||
while ((!this.reachedEnd || this.currMove !== null) && (len > 0 || (len === 0 && item && (!item.countable || item.deleted || item === this.currMoveEnd || (this.reachedEnd && this.currMoveEnd === null) || item.moved !== this.currMove)))) {
|
||||
if (item === this.currMoveEnd || (this.currMoveEnd === null && this.reachedEnd && this.currMove)) {
|
||||
item = /** @type {Item} */ (this.currMove) // we iterate to the right after the current condition
|
||||
const { start, end, move } = this.movedStack.pop() || { start: null, end: null, move: null }
|
||||
this.currMove = move
|
||||
this.currMoveStart = start
|
||||
this.currMoveEnd = end
|
||||
this.reachedEnd = false
|
||||
} else if (item === null) {
|
||||
break
|
||||
} else if (item.countable && !item.deleted && item.moved === this.currMove && len > 0) {
|
||||
len -= item.length
|
||||
if (len < 0) {
|
||||
this.rel = item.length + len
|
||||
len = 0
|
||||
break
|
||||
}
|
||||
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
|
||||
if (this.currMove) {
|
||||
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
|
||||
}
|
||||
const { start, end } = getMovedCoords(item.content, tr)
|
||||
this.currMove = item
|
||||
this.currMoveStart = start
|
||||
this.currMoveEnd = end
|
||||
item = start
|
||||
continue
|
||||
}
|
||||
if (item.right) {
|
||||
item = item.right
|
||||
} else {
|
||||
this.reachedEnd = true
|
||||
}
|
||||
}
|
||||
this.index -= len
|
||||
this.nextItem = item
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
*/
|
||||
reduceMoves (tr) {
|
||||
let item = this.nextItem
|
||||
if (item !== null) {
|
||||
while (item === this.currMoveStart) {
|
||||
item = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
|
||||
const { start, end, move } = this.movedStack.pop() || { start: null, end: null, move: null }
|
||||
this.currMove = move
|
||||
this.currMoveStart = start
|
||||
this.currMoveEnd = end
|
||||
}
|
||||
this.nextItem = item
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {number} len
|
||||
* @return {ListIterator}
|
||||
*/
|
||||
backward (tr, len) {
|
||||
if (this.index - len < 0) {
|
||||
throw lengthExceeded
|
||||
}
|
||||
this.index -= len
|
||||
if (this.reachedEnd) {
|
||||
const nextItem = /** @type {Item} */ (this.nextItem)
|
||||
this.rel = nextItem.countable && !nextItem.deleted ? nextItem.length : 0
|
||||
this.reachedEnd = false
|
||||
}
|
||||
if (this.rel >= len) {
|
||||
this.rel -= len
|
||||
return this
|
||||
}
|
||||
let item = this.nextItem && this.nextItem.left
|
||||
if (this.rel) {
|
||||
len -= this.rel
|
||||
this.rel = 0
|
||||
}
|
||||
while (item && len > 0) {
|
||||
if (item.countable && !item.deleted && item.moved === this.currMove) {
|
||||
len -= item.length
|
||||
if (len < 0) {
|
||||
this.rel = -len
|
||||
len = 0
|
||||
}
|
||||
if (len === 0) {
|
||||
break
|
||||
}
|
||||
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
|
||||
if (this.currMove) {
|
||||
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
|
||||
}
|
||||
const { start, end } = getMovedCoords(item.content, tr)
|
||||
this.currMove = item
|
||||
this.currMoveStart = start
|
||||
this.currMoveEnd = end
|
||||
item = start
|
||||
continue
|
||||
}
|
||||
if (item === this.currMoveStart) {
|
||||
item = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
|
||||
const { start, end, move } = this.movedStack.pop() || { start: null, end: null, move: null }
|
||||
this.currMove = move
|
||||
this.currMoveStart = start
|
||||
this.currMoveEnd = end
|
||||
}
|
||||
item = item.left
|
||||
}
|
||||
this.nextItem = item
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{length: number}} T
|
||||
* @param {Transaction} tr
|
||||
* @param {number} len
|
||||
* @param {T} value the initial content
|
||||
* @param {function(AbstractContent, number, number):T} slice
|
||||
* @param {function(T, T): T} concat
|
||||
*/
|
||||
_slice (tr, len, value, slice, concat) {
|
||||
this.index += len
|
||||
while (len > 0 && !this.reachedEnd) {
|
||||
while (this.nextItem && this.nextItem.countable && !this.reachedEnd && len > 0 && this.nextItem !== this.currMoveEnd) {
|
||||
if (!this.nextItem.deleted && this.nextItem.moved === this.currMove) {
|
||||
const item = this.nextItem
|
||||
const slicedContent = slice(item.content, this.rel, len)
|
||||
len -= slicedContent.length
|
||||
value = concat(value, slicedContent)
|
||||
if (item.length !== slicedContent.length) {
|
||||
if (this.rel + slicedContent.length === item.length) {
|
||||
this.rel = 0
|
||||
} else {
|
||||
this.rel += slicedContent.length
|
||||
continue // do not iterate to item.right
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.nextItem.right) {
|
||||
this.nextItem = this.nextItem.right
|
||||
} else {
|
||||
this.reachedEnd = true
|
||||
}
|
||||
}
|
||||
if (this.nextItem && (!this.reachedEnd || this.currMove !== null) && len > 0) {
|
||||
this.forward(tr, 0)
|
||||
}
|
||||
}
|
||||
if (len < 0) {
|
||||
this.index -= len
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {number} len
|
||||
*/
|
||||
delete (tr, len) {
|
||||
const startLength = len
|
||||
const sm = this.type._searchMarker
|
||||
let item = this.nextItem
|
||||
while (len > 0 && !this.reachedEnd) {
|
||||
while (item && !item.deleted && item.countable && !this.reachedEnd && len > 0) {
|
||||
if (this.rel > 0) {
|
||||
item = getItemCleanStart(tr, createID(item.id.client, item.id.clock + this.rel))
|
||||
this.rel = 0
|
||||
}
|
||||
if (len < item.length) {
|
||||
getItemCleanStart(tr, createID(item.id.client, item.id.clock + len))
|
||||
}
|
||||
len -= item.length
|
||||
item.delete(tr)
|
||||
if (item.right) {
|
||||
item = item.right
|
||||
} else {
|
||||
this.reachedEnd = true
|
||||
}
|
||||
}
|
||||
if (item && !this.reachedEnd && len > 0) {
|
||||
this.nextItem = item
|
||||
this.forward(tr, 0)
|
||||
item = this.nextItem
|
||||
}
|
||||
}
|
||||
this.nextItem = item
|
||||
if (sm) {
|
||||
updateMarkerChanges(sm, this.index, -startLength + len, this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
*/
|
||||
_splitRel (tr) {
|
||||
if (this.rel > 0) {
|
||||
/**
|
||||
* @type {ID}
|
||||
*/
|
||||
const itemid = /** @type {Item} */ (this.nextItem).id
|
||||
this.nextItem = getItemCleanStart(tr, createID(itemid.client, itemid.clock + this.rel))
|
||||
this.rel = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Important: you must update markers after calling this method!
|
||||
*
|
||||
* @param {Transaction} tr
|
||||
* @param {Array<AbstractContent>} content
|
||||
*/
|
||||
insertContents (tr, content) {
|
||||
this.reduceMoves(tr)
|
||||
this._splitRel(tr)
|
||||
const parent = this.type
|
||||
const store = tr.doc.store
|
||||
const ownClientId = tr.doc.clientID
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
const right = this.right
|
||||
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
let left = this.left
|
||||
content.forEach(c => {
|
||||
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, c)
|
||||
left.integrate(tr, 0)
|
||||
})
|
||||
if (right === null) {
|
||||
this.nextItem = left
|
||||
this.reachedEnd = true
|
||||
} else {
|
||||
this.nextItem = right
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {RelativePosition} start
|
||||
* @param {RelativePosition} end
|
||||
*/
|
||||
insertMove (tr, start, end) {
|
||||
this.insertContents(tr, [new ContentMove(start, end, -1)]) // @todo adjust priority
|
||||
// @todo is there a better alrogirthm to update searchmarkers? We could simply remove the markers that are in the updated range.
|
||||
// Also note that searchmarkers are updated in insertContents as well.
|
||||
const sm = this.type._searchMarker
|
||||
if (sm) sm.length = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} values
|
||||
*/
|
||||
insertArrayValue (tr, values) {
|
||||
this._splitRel(tr)
|
||||
const sm = this.type._searchMarker
|
||||
/**
|
||||
* @type {Array<AbstractContent>}
|
||||
*/
|
||||
const contents = []
|
||||
/**
|
||||
* @type {Array<Object|Array<any>|number|null>}
|
||||
*/
|
||||
let jsonContent = []
|
||||
const packJsonContent = () => {
|
||||
if (jsonContent.length > 0) {
|
||||
contents.push(new ContentAny(jsonContent))
|
||||
jsonContent = []
|
||||
}
|
||||
}
|
||||
values.forEach(c => {
|
||||
if (c === null) {
|
||||
jsonContent.push(c)
|
||||
} else {
|
||||
switch (c.constructor) {
|
||||
case Number:
|
||||
case Object:
|
||||
case Boolean:
|
||||
case Array:
|
||||
case String:
|
||||
jsonContent.push(c)
|
||||
break
|
||||
default:
|
||||
packJsonContent()
|
||||
switch (c.constructor) {
|
||||
case Uint8Array:
|
||||
case ArrayBuffer:
|
||||
contents.push(new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
|
||||
break
|
||||
case Doc:
|
||||
contents.push(new ContentDoc(/** @type {Doc} */ (c)))
|
||||
break
|
||||
default:
|
||||
if (c instanceof AbstractType) {
|
||||
contents.push(new ContentType(c))
|
||||
} else {
|
||||
throw new Error('Unexpected content type in insert operation')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
packJsonContent()
|
||||
this.insertContents(tr, contents)
|
||||
this.index += values.length
|
||||
if (sm) {
|
||||
updateMarkerChanges(sm, this.index - values.length, values.length, this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {number} len
|
||||
*/
|
||||
slice (tr, len) {
|
||||
return this._slice(tr, len, [], sliceArrayContent, concatArrayContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {function(any, number, any):void} f
|
||||
*/
|
||||
forEach (tr, f) {
|
||||
for (const val of this.values(tr)) {
|
||||
f(val, this.index, this.type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Transaction} tr
|
||||
* @param {function(any, number, any):T} f
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
map (tr, f) {
|
||||
const arr = new Array(this.type._length - this.index)
|
||||
let i = 0
|
||||
for (const val of this.values(tr)) {
|
||||
arr[i++] = f(val, this.index, this.type)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
*/
|
||||
values (tr) {
|
||||
return {
|
||||
[Symbol.iterator] () {
|
||||
return this
|
||||
},
|
||||
next: () => {
|
||||
if (this.reachedEnd || this.index === this.type._length) {
|
||||
return { done: true }
|
||||
}
|
||||
const [value] = this.slice(tr, 1)
|
||||
return {
|
||||
done: false,
|
||||
value: value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractContent} itemcontent
|
||||
* @param {number} start
|
||||
* @param {number} len
|
||||
*/
|
||||
const sliceArrayContent = (itemcontent, start, len) => {
|
||||
const content = itemcontent.getContent()
|
||||
return content.length <= len && start === 0 ? content : content.slice(start, start + len)
|
||||
}
|
||||
/**
|
||||
* @param {Array<any>} content
|
||||
* @param {Array<any>} added
|
||||
*/
|
||||
const concatArrayContent = (content, added) => {
|
||||
content.push(...added)
|
||||
return content
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
DSEncoderV1, DSDecoderV1, ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
|
||||
import { mergeDeleteSets, isDeleted } from './DeleteSet.js'
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
createID,
|
||||
ContentType,
|
||||
followRedone,
|
||||
transact,
|
||||
useSearchMarker,
|
||||
ID, Doc, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* A relative position is based on the Yjs model and is not affected by document changes.
|
||||
@@ -45,8 +47,9 @@ export class RelativePosition {
|
||||
* @param {ID|null} type
|
||||
* @param {string|null} tname
|
||||
* @param {ID|null} item
|
||||
* @param {number} assoc
|
||||
*/
|
||||
constructor (type, tname, item) {
|
||||
constructor (type, tname, item, assoc = 0) {
|
||||
/**
|
||||
* @type {ID|null}
|
||||
*/
|
||||
@@ -59,7 +62,44 @@ export class RelativePosition {
|
||||
* @type {ID | null}
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
clone () {
|
||||
return new RelativePosition(this.type, this.tname, this.item, this.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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,14 +108,15 @@ export class RelativePosition {
|
||||
*
|
||||
* @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 {
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} index
|
||||
* @param {number} [assoc]
|
||||
*/
|
||||
constructor (type, index) {
|
||||
constructor (type, index, assoc = 0) {
|
||||
/**
|
||||
* @type {AbstractType<any>}
|
||||
*/
|
||||
@@ -84,24 +125,27 @@ export class AbsolutePosition {
|
||||
* @type {number}
|
||||
*/
|
||||
this.index = index
|
||||
this.assoc = assoc
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} index
|
||||
* @param {number} [assoc]
|
||||
*
|
||||
* @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 {ID|null} item
|
||||
* @param {number} [assoc]
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createRelativePosition = (type, item) => {
|
||||
export const createRelativePosition = (type, item, assoc) => {
|
||||
let typeid = null
|
||||
let tname = null
|
||||
if (type._item === null) {
|
||||
@@ -109,7 +153,7 @@ export const createRelativePosition = (type, item) => {
|
||||
} else {
|
||||
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 +161,30 @@ export const createRelativePosition = (type, item) => {
|
||||
*
|
||||
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
|
||||
* @param {number} index The absolute position.
|
||||
* @param {number} [assoc]
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createRelativePositionFromTypeIndex = (type, index) => {
|
||||
let t = type._start
|
||||
while (t !== null) {
|
||||
if (!t.deleted && t.countable) {
|
||||
if (t.length > index) {
|
||||
// case 1: found position somewhere in the linked list
|
||||
return createRelativePosition(type, createID(t.id.client, t.id.clock + index))
|
||||
export const createRelativePositionFromTypeIndex = (type, index, assoc = 0) => {
|
||||
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 -= t.length
|
||||
index--
|
||||
}
|
||||
t = t.right
|
||||
return transact(/** @type {Doc} */ (type.doc), tr =>
|
||||
useSearchMarker(tr, type, index, walker => {
|
||||
if (walker.reachedEnd) {
|
||||
const item = assoc < 0 ? /** @type {Item} */ (walker.nextItem).lastId : null
|
||||
return createRelativePosition(type, item, assoc)
|
||||
} else {
|
||||
const id = /** @type {Item} */ (walker.nextItem).id
|
||||
return createRelativePosition(type, createID(id.client, id.clock + walker.rel), assoc)
|
||||
}
|
||||
return createRelativePosition(type, null)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +194,7 @@ export const createRelativePositionFromTypeIndex = (type, index) => {
|
||||
* @function
|
||||
*/
|
||||
export const writeRelativePosition = (encoder, rpos) => {
|
||||
const { type, tname, item } = rpos
|
||||
const { type, tname, item, assoc } = rpos
|
||||
if (item !== null) {
|
||||
encoding.writeVarUint(encoder, 0)
|
||||
writeID(encoder, item)
|
||||
@@ -158,6 +209,7 @@ export const writeRelativePosition = (encoder, rpos) => {
|
||||
} else {
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
encoding.writeVarInt(encoder, assoc)
|
||||
return encoder
|
||||
}
|
||||
|
||||
@@ -173,7 +225,7 @@ export const encodeRelativePosition = rpos => {
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {RelativePosition|null}
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
@@ -195,12 +247,13 @@ export const readRelativePosition = 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
|
||||
* @return {RelativePosition|null}
|
||||
* @return {RelativePosition}
|
||||
*/
|
||||
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
||||
|
||||
@@ -216,6 +269,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
const rightID = rpos.item
|
||||
const typeID = rpos.type
|
||||
const tname = rpos.tname
|
||||
const assoc = rpos.assoc
|
||||
let type = null
|
||||
let index = 0
|
||||
if (rightID !== null) {
|
||||
@@ -229,7 +283,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
}
|
||||
type = /** @type {AbstractType<any>} */ (right.parent)
|
||||
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
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable) {
|
||||
@@ -256,9 +310,13 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
} else {
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
if (assoc >= 0) {
|
||||
index = type._length
|
||||
} else {
|
||||
index = 0
|
||||
}
|
||||
return createAbsolutePosition(type, index)
|
||||
}
|
||||
return createAbsolutePosition(type, index, rpos.assoc)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,5 +327,5 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
* @function
|
||||
*/
|
||||
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,15 +14,14 @@ import {
|
||||
getState,
|
||||
findIndexSS,
|
||||
UpdateEncoderV2,
|
||||
DefaultDSEncoder,
|
||||
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'
|
||||
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as set from 'lib0/set.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as map from 'lib0/map'
|
||||
import * as set from 'lib0/set'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
||||
export class Snapshot {
|
||||
/**
|
||||
@@ -78,7 +77,7 @@ export const equalSnapshots = (snap1, snap2) => {
|
||||
|
||||
/**
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {AbstractDSEncoder} [encoder]
|
||||
* @param {DSEncoderV1 | DSEncoderV2} [encoder]
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export const encodeSnapshotV2 = (snapshot, encoder = new DSEncoderV2()) => {
|
||||
@@ -91,11 +90,11 @@ export const encodeSnapshotV2 = (snapshot, encoder = new DSEncoderV2()) => {
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export const encodeSnapshot = snapshot => encodeSnapshotV2(snapshot, new DefaultDSEncoder())
|
||||
export const encodeSnapshot = snapshot => encodeSnapshotV2(snapshot, new DSEncoderV1())
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} buf
|
||||
* @param {AbstractDSDecoder} [decoder]
|
||||
* @param {DSDecoderV1 | DSDecoderV2} [decoder]
|
||||
* @return {Snapshot}
|
||||
*/
|
||||
export const decodeSnapshotV2 = (buf, decoder = new DSDecoderV2(decoding.createDecoder(buf))) => {
|
||||
@@ -130,9 +129,9 @@ export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc
|
||||
* @protected
|
||||
* @function
|
||||
*/
|
||||
export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : (
|
||||
snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
|
||||
)
|
||||
export const isVisible = (item, snapshot) => snapshot === undefined
|
||||
? !item.deleted
|
||||
: snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
Transaction, ID, Item, DSDecoderV2 // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as math from 'lib0/math'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class StructStore {
|
||||
constructor () {
|
||||
@@ -15,24 +15,13 @@ export class StructStore {
|
||||
*/
|
||||
this.clients = new Map()
|
||||
/**
|
||||
* Store incompleted struct reads here
|
||||
* `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>}>}
|
||||
* @type {null | { missing: Map<number, number>, update: Uint8Array }}
|
||||
*/
|
||||
this.pendingClientsStructRefs = new Map()
|
||||
this.pendingStructs = null
|
||||
/**
|
||||
* Stack of pending structs waiting for struct dependencies
|
||||
* Maximum length of stack is structReaders.size
|
||||
* @type {Array<GC|Item>}
|
||||
* @type {null | Uint8Array}
|
||||
*/
|
||||
this.pendingStack = []
|
||||
/**
|
||||
* @type {Array<DSDecoderV2>}
|
||||
*/
|
||||
this.pendingDeleteReaders = []
|
||||
this.pendingDs = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,19 +199,18 @@ export const getItemCleanStart = (transaction, id) => {
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItemCleanEnd = (transaction, store, id) => {
|
||||
export const getItemCleanEnd = (transaction, id) => {
|
||||
/**
|
||||
* @type {Array<Item>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(id.client)
|
||||
const structs = transaction.doc.store.clients.get(id.client)
|
||||
const index = findIndexSS(structs, id.clock)
|
||||
const struct = structs[index]
|
||||
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
|
||||
|
||||
@@ -11,14 +11,14 @@ import {
|
||||
Item,
|
||||
generateNewClientId,
|
||||
createID,
|
||||
AbstractUpdateEncoder, GC, StructStore, UpdateEncoderV2, DefaultUpdateEncoder, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as set from 'lib0/set.js'
|
||||
import * as logging from 'lib0/logging.js'
|
||||
import { callAll } from 'lib0/function.js'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as set from 'lib0/set'
|
||||
import * as logging from 'lib0/logging'
|
||||
import { callAll } from 'lib0/function'
|
||||
|
||||
/**
|
||||
* A transaction is created for every change on the Yjs model. It is possible
|
||||
@@ -114,11 +114,19 @@ export class Transaction {
|
||||
* @type {Set<Doc>}
|
||||
*/
|
||||
this.subdocsLoaded = new Set()
|
||||
/**
|
||||
* We store the reference that last moved an item.
|
||||
* This is needed to compute the delta when multiple ContentMove move
|
||||
* the same item.
|
||||
*
|
||||
* @type {Map<Item, Item>}
|
||||
*/
|
||||
this.prevMoved = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Transaction} transaction
|
||||
* @return {boolean} Whether data was written.
|
||||
*/
|
||||
@@ -331,30 +339,38 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
}
|
||||
}
|
||||
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
|
||||
doc.clientID = generateNewClientId()
|
||||
logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
|
||||
doc.clientID = generateNewClientId()
|
||||
}
|
||||
// @todo Merge all the transactions into one and provide send the data as a single update message
|
||||
doc.emit('afterTransactionCleanup', [transaction, doc])
|
||||
if (doc._observers.has('update')) {
|
||||
const encoder = new DefaultUpdateEncoder()
|
||||
const encoder = new UpdateEncoderV1()
|
||||
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
||||
if (hasContent) {
|
||||
doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc])
|
||||
doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc, transaction])
|
||||
}
|
||||
}
|
||||
if (doc._observers.has('updateV2')) {
|
||||
const encoder = new UpdateEncoderV2()
|
||||
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
||||
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.subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc))
|
||||
|
||||
doc.emit('subdocs', [{ loaded: transaction.subdocsLoaded, added: transaction.subdocsAdded, removed: transaction.subdocsRemoved }])
|
||||
transaction.subdocsRemoved.forEach(subdoc => subdoc.destroy())
|
||||
const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction
|
||||
if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) {
|
||||
subdocsAdded.forEach(subdoc => {
|
||||
subdoc.clientID = doc.clientID
|
||||
if (subdoc.collectionid == null) {
|
||||
subdoc.collectionid = doc.collectionid
|
||||
}
|
||||
doc.subdocs.add(subdoc)
|
||||
})
|
||||
subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc))
|
||||
doc.emit('subdocs', [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc, transaction])
|
||||
subdocsRemoved.forEach(subdoc => subdoc.destroy())
|
||||
}
|
||||
|
||||
if (transactionCleanups.length <= i + 1) {
|
||||
doc._transactionCleanups = []
|
||||
@@ -369,9 +385,12 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
/**
|
||||
* Implements the functionality of `y.transact(()=>{..})`
|
||||
*
|
||||
* @template T
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @param {function(Transaction):void} f
|
||||
* @param {function(Transaction):T} f
|
||||
* @param {any} [origin=true]
|
||||
* @return {T}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
@@ -387,8 +406,9 @@ export const transact = (doc, f, origin = null, local = true) => {
|
||||
}
|
||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||
}
|
||||
let res
|
||||
try {
|
||||
f(doc._transaction)
|
||||
res = f(doc._transaction)
|
||||
} finally {
|
||||
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
||||
// The first transaction ended, now process observer calls.
|
||||
@@ -402,4 +422,5 @@ export const transact = (doc, f, origin = null, local = true) => {
|
||||
cleanupTransactions(transactionCleanups, 0)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -5,27 +5,25 @@ import {
|
||||
transact,
|
||||
createID,
|
||||
redoItem,
|
||||
iterateStructs,
|
||||
isParentOf,
|
||||
followRedone,
|
||||
getItemCleanStart,
|
||||
getState,
|
||||
ID, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
||||
isDeleted,
|
||||
addToDeleteSet,
|
||||
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as time from 'lib0/time.js'
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
import * as time from 'lib0/time'
|
||||
import { Observable } from 'lib0/observable'
|
||||
|
||||
class StackItem {
|
||||
/**
|
||||
* @param {DeleteSet} ds
|
||||
* @param {Map<number,number>} beforeState
|
||||
* @param {Map<number,number>} afterState
|
||||
* @param {DeleteSet} deletions
|
||||
* @param {DeleteSet} insertions
|
||||
*/
|
||||
constructor (ds, beforeState, afterState) {
|
||||
this.ds = ds
|
||||
this.beforeState = beforeState
|
||||
this.afterState = afterState
|
||||
constructor (deletions, insertions) {
|
||||
this.insertions = insertions
|
||||
this.deletions = deletions
|
||||
/**
|
||||
* Use this to save and restore metadata like selection range
|
||||
*/
|
||||
@@ -45,6 +43,11 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
* @type {StackItem?}
|
||||
*/
|
||||
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 scope = undoManager.scope
|
||||
transact(doc, transaction => {
|
||||
@@ -60,28 +63,13 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
*/
|
||||
const itemsToDelete = []
|
||||
let performedChange = false
|
||||
stackItem.afterState.forEach((endClock, client) => {
|
||||
const startClock = stackItem.beforeState.get(client) || 0
|
||||
const len = endClock - startClock
|
||||
// @todo iterateStructs should not need the structs parameter
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||
if (startClock !== endClock) {
|
||||
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
|
||||
// this must be executed before deleted structs are iterated.
|
||||
getItemCleanStart(transaction, createID(client, startClock))
|
||||
if (endClock < getState(doc.store, client)) {
|
||||
getItemCleanStart(transaction, createID(client, endClock))
|
||||
}
|
||||
iterateStructs(transaction, structs, startClock, len, struct => {
|
||||
iterateDeletedStructs(transaction, stackItem.insertions, 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)))) {
|
||||
@@ -89,25 +77,18 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
iterateDeletedStructs(transaction, stackItem.ds, struct => {
|
||||
const id = struct.id
|
||||
const clock = id.clock
|
||||
const client = id.client
|
||||
const startClock = stackItem.beforeState.get(client) || 0
|
||||
const endClock = stackItem.afterState.get(client) || 0
|
||||
iterateDeletedStructs(transaction, stackItem.deletions, struct => {
|
||||
if (
|
||||
struct instanceof Item &&
|
||||
scope.some(type => isParentOf(type, struct)) &&
|
||||
// Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval.
|
||||
!(clock >= startClock && clock < endClock)
|
||||
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
|
||||
!isDeleted(stackItem.insertions, struct.id)
|
||||
) {
|
||||
itemsToRedo.add(struct)
|
||||
}
|
||||
})
|
||||
itemsToRedo.forEach(struct => {
|
||||
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
|
||||
performedChange = redoItem(transaction, struct, itemsToRedo, itemsToDelete) !== null || performedChange
|
||||
})
|
||||
// We want to delete in reverse order so that children are deleted before
|
||||
// parents, so we have more information available when items are filtered.
|
||||
@@ -118,7 +99,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
performedChange = true
|
||||
}
|
||||
}
|
||||
result = stackItem
|
||||
result = performedChange ? stackItem : null
|
||||
}
|
||||
transaction.changed.forEach((subProps, type) => {
|
||||
// destroy search marker if necessary
|
||||
@@ -126,9 +107,11 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
type._searchMarker.length = 0
|
||||
}
|
||||
})
|
||||
_tr = transaction
|
||||
}, undoManager)
|
||||
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
|
||||
}
|
||||
@@ -194,17 +177,23 @@ export class UndoManager extends Observable {
|
||||
// neither undoing nor redoing: delete redoStack
|
||||
this.redoStack = []
|
||||
}
|
||||
const beforeState = transaction.beforeState
|
||||
const afterState = transaction.afterState
|
||||
const insertions = new DeleteSet()
|
||||
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()
|
||||
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||
// append change to last stack op
|
||||
const lastOp = stack[stack.length - 1]
|
||||
lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
|
||||
lastOp.afterState = afterState
|
||||
lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
|
||||
lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions])
|
||||
} else {
|
||||
// create a new stack op
|
||||
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState))
|
||||
stack.push(new StackItem(transaction.deleteSet, insertions))
|
||||
}
|
||||
if (!undoing && !redoing) {
|
||||
this.lastChange = now
|
||||
@@ -215,7 +204,7 @@ export class UndoManager extends Observable {
|
||||
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
|
||||
*/
|
||||
const clearItem = stackItem => {
|
||||
iterateDeletedStructs(transaction, stackItem.ds, item => {
|
||||
iterateDeletedStructs(transaction, stackItem.deletions, item => {
|
||||
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||
keepItem(item, false)
|
||||
}
|
||||
|
||||
@@ -1,129 +1,9 @@
|
||||
import * as buffer from 'lib0/buffer.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as buffer from 'lib0/buffer'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import {
|
||||
ID, createID
|
||||
} 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 {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
@@ -247,6 +127,9 @@ export class DSDecoderV2 {
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
constructor (decoder) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this.dsCurrVal = 0
|
||||
this.restDecoder = decoder
|
||||
}
|
||||
@@ -255,11 +138,17 @@ export class DSDecoderV2 {
|
||||
this.dsCurrVal = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
readDsClock () {
|
||||
this.dsCurrVal += decoding.readVarUint(this.restDecoder)
|
||||
return this.dsCurrVal
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
readDsLen () {
|
||||
const diff = decoding.readVarUint(this.restDecoder) + 1
|
||||
this.dsCurrVal += diff
|
||||
@@ -280,7 +169,7 @@ export class UpdateDecoderV2 extends DSDecoderV2 {
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
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.clientDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder))
|
||||
this.leftClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder))
|
||||
|
||||
@@ -1,115 +1,14 @@
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as error from 'lib0/error'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
||||
import {
|
||||
ID // eslint-disable-line
|
||||
} 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 {
|
||||
constructor () {
|
||||
this.restEncoder = new encoding.Encoder()
|
||||
this.restEncoder = encoding.createEncoder()
|
||||
}
|
||||
|
||||
toUint8Array () {
|
||||
@@ -228,7 +127,7 @@ export class UpdateEncoderV1 extends DSEncoderV1 {
|
||||
|
||||
export class DSEncoderV2 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -288,7 +187,7 @@ export class UpdateEncoderV2 extends DSEncoderV2 {
|
||||
|
||||
toUint8Array () {
|
||||
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.clientEncoder.toUint8Array())
|
||||
encoding.writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array())
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
|
||||
import {
|
||||
isDeleted,
|
||||
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||
getMovedCoords,
|
||||
ContentMove, Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as set from 'lib0/set.js'
|
||||
import * as array from 'lib0/array.js'
|
||||
import * as set from 'lib0/set'
|
||||
import * as array from 'lib0/array'
|
||||
|
||||
/**
|
||||
* YEvent describes the changes on a YType.
|
||||
@@ -35,6 +36,14 @@ export class YEvent {
|
||||
* @type {Object|null}
|
||||
*/
|
||||
this._changes = null
|
||||
/**
|
||||
* @type {null | Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
|
||||
*/
|
||||
this._keys = null
|
||||
/**
|
||||
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
|
||||
*/
|
||||
this._delta = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,79 +77,13 @@ export class YEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a struct is added by this event.
|
||||
*
|
||||
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
|
||||
*
|
||||
* @param {AbstractStruct} struct
|
||||
* @return {boolean}
|
||||
*/
|
||||
adds (struct) {
|
||||
return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert:Array<any>}|{delete:number}|{retain:number}>}}
|
||||
*/
|
||||
get changes () {
|
||||
let changes = this._changes
|
||||
if (changes === null) {
|
||||
const target = this.target
|
||||
const added = set.create()
|
||||
const deleted = set.create()
|
||||
/**
|
||||
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
|
||||
*/
|
||||
const delta = []
|
||||
/**
|
||||
* @type {Map<string,{ action: 'add' | 'update' | 'delete', oldValue: any}>}
|
||||
* @type {Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
|
||||
*/
|
||||
get keys () {
|
||||
if (this._keys === null) {
|
||||
const keys = new Map()
|
||||
changes = {
|
||||
added, deleted, delta, keys
|
||||
}
|
||||
const target = this.target
|
||||
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
||||
if (changed.has(null)) {
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let lastOp = null
|
||||
const packOp = () => {
|
||||
if (lastOp) {
|
||||
delta.push(lastOp)
|
||||
}
|
||||
}
|
||||
for (let item = target._start; item !== null; item = item.right) {
|
||||
if (item.deleted) {
|
||||
if (this.deletes(item) && !this.adds(item)) {
|
||||
if (lastOp === null || lastOp.delete === undefined) {
|
||||
packOp()
|
||||
lastOp = { delete: 0 }
|
||||
}
|
||||
lastOp.delete += item.length
|
||||
deleted.add(item)
|
||||
} // else nop
|
||||
} else {
|
||||
if (this.adds(item)) {
|
||||
if (lastOp === null || lastOp.insert === undefined) {
|
||||
packOp()
|
||||
lastOp = { insert: [] }
|
||||
}
|
||||
lastOp.insert = lastOp.insert.concat(item.content.getContent())
|
||||
added.add(item)
|
||||
} else {
|
||||
if (lastOp === null || lastOp.retain === undefined) {
|
||||
packOp()
|
||||
lastOp = { retain: 0 }
|
||||
}
|
||||
lastOp.retain += item.length
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastOp !== null && lastOp.retain === undefined) {
|
||||
packOp()
|
||||
}
|
||||
}
|
||||
changed.forEach(key => {
|
||||
if (key !== null) {
|
||||
const item = /** @type {Item} */ (target._map.get(key))
|
||||
@@ -181,7 +124,135 @@ export class YEvent {
|
||||
keys.set(key, { action, oldValue })
|
||||
}
|
||||
})
|
||||
this._keys = keys
|
||||
}
|
||||
return this._keys
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
|
||||
*/
|
||||
get delta () {
|
||||
return this.changes.delta
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a struct is added by this event.
|
||||
*
|
||||
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
|
||||
*
|
||||
* @param {AbstractStruct} struct
|
||||
* @return {boolean}
|
||||
*/
|
||||
adds (struct) {
|
||||
return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
||||
*/
|
||||
get changes () {
|
||||
let changes = this._changes
|
||||
if (changes === null) {
|
||||
this.transaction.doc.transact(tr => {
|
||||
const target = this.target
|
||||
const added = set.create()
|
||||
const deleted = set.create()
|
||||
/**
|
||||
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
|
||||
*/
|
||||
const delta = []
|
||||
changes = {
|
||||
added,
|
||||
deleted,
|
||||
delta,
|
||||
keys: this.keys
|
||||
}
|
||||
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
||||
if (changed.has(null)) {
|
||||
/**
|
||||
* @type {Array<{ end: Item | null, move: Item | null, isNew : boolean }>}
|
||||
*/
|
||||
const movedStack = []
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
let currMove = null
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
let currMoveIsNew = false
|
||||
/**
|
||||
* @type {Item | null}
|
||||
*/
|
||||
let currMoveEnd = null
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let lastOp = null
|
||||
const packOp = () => {
|
||||
if (lastOp) {
|
||||
delta.push(lastOp)
|
||||
}
|
||||
}
|
||||
for (let item = target._start; item !== null;) {
|
||||
if (item === currMoveEnd) {
|
||||
item = currMove
|
||||
const { end, move, isNew } = movedStack.pop() || { end: null, move: null, isNew: false }
|
||||
currMoveIsNew = isNew
|
||||
currMoveEnd = end
|
||||
currMove = move
|
||||
} else if (item.content.constructor === ContentMove) {
|
||||
if (item.moved === currMove) {
|
||||
movedStack.push({ end: currMoveEnd, move: currMove, isNew: currMoveIsNew })
|
||||
const { start, end } = getMovedCoords(item.content, tr)
|
||||
currMove = item
|
||||
currMoveEnd = end
|
||||
currMoveIsNew = this.adds(item)
|
||||
item = start
|
||||
continue // do not move to item.right
|
||||
}
|
||||
} else if (item.moved !== currMove) {
|
||||
if (!currMoveIsNew && item.countable && item.moved && !this.adds(item) && !this.adds(item.moved) && (this.transaction.prevMoved.get(item) || null) === currMove) {
|
||||
if (lastOp === null || lastOp.delete === undefined) {
|
||||
packOp()
|
||||
lastOp = { delete: 0 }
|
||||
}
|
||||
lastOp.delete += item.length
|
||||
}
|
||||
} else if (item.deleted) {
|
||||
if (!currMoveIsNew && this.deletes(item) && !this.adds(item)) {
|
||||
if (lastOp === null || lastOp.delete === undefined) {
|
||||
packOp()
|
||||
lastOp = { delete: 0 }
|
||||
}
|
||||
lastOp.delete += item.length
|
||||
deleted.add(item)
|
||||
}
|
||||
} else {
|
||||
if (currMoveIsNew || this.adds(item)) {
|
||||
if (lastOp === null || lastOp.insert === undefined) {
|
||||
packOp()
|
||||
lastOp = { insert: [] }
|
||||
}
|
||||
lastOp.insert = lastOp.insert.concat(item.content.getContent())
|
||||
added.add(item)
|
||||
} else {
|
||||
if (lastOp === null || lastOp.retain === undefined) {
|
||||
packOp()
|
||||
lastOp = { retain: 0 }
|
||||
}
|
||||
lastOp.retain += item.length
|
||||
}
|
||||
}
|
||||
item = /** @type {Item} */ (item).right
|
||||
}
|
||||
if (lastOp !== null && lastOp.retain === undefined) {
|
||||
packOp()
|
||||
}
|
||||
}
|
||||
this._changes = changes
|
||||
})
|
||||
}
|
||||
return /** @type {any} */ (changes)
|
||||
}
|
||||
|
||||
@@ -29,39 +29,25 @@ import {
|
||||
UpdateDecoderV2,
|
||||
UpdateEncoderV1,
|
||||
UpdateEncoderV2,
|
||||
DSDecoderV2,
|
||||
DSEncoderV2,
|
||||
DSDecoderV1,
|
||||
DSEncoderV1,
|
||||
AbstractDSEncoder, AbstractDSDecoder, AbstractUpdateEncoder, AbstractUpdateDecoder, AbstractContent, Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
|
||||
mergeUpdates,
|
||||
mergeUpdatesV2,
|
||||
Skip,
|
||||
diffUpdateV2,
|
||||
convertUpdateFormatV2ToV1,
|
||||
DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as binary from 'lib0/binary.js'
|
||||
import * as map from 'lib0/map.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
|
||||
}
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Array<GC|Item>} structs All structs by `client`
|
||||
* @param {number} client
|
||||
* @param {number} clock write structs starting with `ID(client,clock)`
|
||||
@@ -70,6 +56,7 @@ export const useV2Encoding = () => {
|
||||
*/
|
||||
const writeStructs = (encoder, structs, client, clock) => {
|
||||
// write first id
|
||||
clock = math.max(clock, structs[0].id.clock) // make sure the first id exists
|
||||
const startNewStructs = findIndexSS(structs, clock)
|
||||
// write # encoded structs
|
||||
encoding.writeVarUint(encoder.restEncoder, structs.length - startNewStructs)
|
||||
@@ -84,7 +71,7 @@ const writeStructs = (encoder, structs, client, clock) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {StructStore} store
|
||||
* @param {Map<number,number>} _sm
|
||||
*
|
||||
@@ -116,15 +103,18 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateDecoder} decoder The decoder object to read data from.
|
||||
* @param {Map<number,Array<GC|Item>>} clientRefs
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from.
|
||||
* @param {Doc} doc
|
||||
* @return {Map<number,Array<GC|Item>>}
|
||||
* @return {Map<number, { i: number, refs: Array<Item | GC> }>}
|
||||
*
|
||||
* @private
|
||||
* @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)
|
||||
for (let i = 0; i < numOfStateUpdates; i++) {
|
||||
const numberOfStructs = decoding.readVarUint(decoder.restDecoder)
|
||||
@@ -135,10 +125,24 @@ export const readClientsStructRefs = (decoder, clientRefs, doc) => {
|
||||
const client = decoder.readClient()
|
||||
let clock = decoding.readVarUint(decoder.restDecoder)
|
||||
// const start = performance.now()
|
||||
clientRefs.set(client, refs)
|
||||
clientRefs.set(client, { i: 0, refs })
|
||||
for (let i = 0; i < numberOfStructs; i++) {
|
||||
const info = decoder.readInfo()
|
||||
if ((binary.BITS5 & info) !== 0) {
|
||||
switch (binary.BITS5 & info) {
|
||||
case 0: { // GC
|
||||
const len = decoder.readLen()
|
||||
refs[i] = new GC(createID(client, clock), len)
|
||||
clock += len
|
||||
break
|
||||
}
|
||||
case 10: { // Skip Struct (nothing to apply)
|
||||
// @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing.
|
||||
const len = decoding.readVarUint(decoder.restDecoder)
|
||||
refs[i] = new Skip(createID(client, clock), len)
|
||||
clock += len
|
||||
break
|
||||
}
|
||||
default: { // Item with content
|
||||
/**
|
||||
* The optimized implementation doesn't use any variables because inlining variables is faster.
|
||||
* Below a non-optimized version is shown that implements the basic algorithm with
|
||||
@@ -186,10 +190,7 @@ export const readClientsStructRefs = (decoder, clientRefs, doc) => {
|
||||
*/
|
||||
refs[i] = struct
|
||||
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
|
||||
@@ -218,26 +219,32 @@ export const readClientsStructRefs = (decoder, clientRefs, doc) => {
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {Map<number, { i: number, refs: (GC | Item)[] }>} clientsStructRefs
|
||||
* @return { null | { update: Uint8Array, missing: Map<number,number> } }
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const resumeStructIntegration = (transaction, store) => {
|
||||
const stack = store.pendingStack // @todo don't forget to append stackhead at the end
|
||||
const clientsStructRefs = store.pendingClientsStructRefs
|
||||
const integrateStructs = (transaction, store, clientsStructRefs) => {
|
||||
/**
|
||||
* @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.
|
||||
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) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
const getNextStructTarget = () => {
|
||||
if (clientsStructRefsIds.length === 0) {
|
||||
return null
|
||||
}
|
||||
let nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
|
||||
while (nextStructsTarget.refs.length === nextStructsTarget.i) {
|
||||
clientsStructRefsIds.pop()
|
||||
if (clientsStructRefsIds.length > 0) {
|
||||
nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
|
||||
} else {
|
||||
store.pendingClientsStructRefs.clear()
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -245,49 +252,87 @@ const resumeStructIntegration = (transaction, store) => {
|
||||
}
|
||||
let curStructsTarget = getNextStructTarget()
|
||||
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}
|
||||
*/
|
||||
let stackHead = stack.length > 0
|
||||
? /** @type {GC|Item} */ (stack.pop())
|
||||
: /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++]
|
||||
let stackHead = /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++]
|
||||
// caching the state because it is used very often
|
||||
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
|
||||
while (true) {
|
||||
if (stackHead.constructor !== Skip) {
|
||||
const localClock = map.setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client))
|
||||
const offset = stackHead.id.clock < localClock ? localClock - stackHead.id.clock : 0
|
||||
if (stackHead.id.clock + offset !== localClock) {
|
||||
// A previous message from this client is missing
|
||||
// check if there is a pending structRef with a smaller clock and switch them
|
||||
const offset = localClock - stackHead.id.clock
|
||||
if (offset < 0) {
|
||||
// update from the same client is missing
|
||||
stack.push(stackHead)
|
||||
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(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
|
||||
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
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
} 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())
|
||||
@@ -302,41 +347,20 @@ const resumeStructIntegration = (transaction, store) => {
|
||||
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)
|
||||
return
|
||||
}
|
||||
stack.push(stackHead)
|
||||
stackHead = structRefs.refs[structRefs.i++]
|
||||
if (restStructs.clients.size > 0) {
|
||||
const encoder = new UpdateEncoderV2()
|
||||
writeClientsStructs(encoder, restStructs, new Map())
|
||||
// write empty deleteset
|
||||
// writeDeleteSet(encoder, new DeleteSet())
|
||||
encoding.writeVarUint(encoder.restEncoder, 0) // => no need for an extra function call, just write 0 deletes
|
||||
return { missing: missingSV, update: encoder.toUint8Array() }
|
||||
}
|
||||
}
|
||||
store.pendingClientsStructRefs.clear()
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
*
|
||||
* @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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Transaction} transaction
|
||||
*
|
||||
* @private
|
||||
@@ -344,78 +368,6 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@@ -424,14 +376,77 @@ export const readStructs = (decoder, transaction, store) => {
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Doc} ydoc
|
||||
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||
* @param {AbstractUpdateDecoder} [structDecoder]
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} [structDecoder]
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
|
||||
transact(ydoc, transaction => {
|
||||
readStructs(structDecoder, transaction, ydoc.store)
|
||||
readAndApplyDeleteSet(structDecoder, transaction, ydoc.store)
|
||||
// force that transaction.local is set to non-local
|
||||
transaction.local = false
|
||||
let retry = false
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -445,7 +460,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
|
||||
*
|
||||
* @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()`.
|
||||
@@ -475,13 +490,13 @@ export const applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = Update
|
||||
*
|
||||
* @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
|
||||
* only write the operations that are missing.
|
||||
*
|
||||
* @param {AbstractUpdateEncoder} encoder
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {Doc} doc
|
||||
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||
*
|
||||
@@ -500,15 +515,30 @@ export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map())
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @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}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector, encoder = new UpdateEncoderV2()) => {
|
||||
const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
|
||||
export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => {
|
||||
const targetStateVector = decodeStateVector(encodedTargetStateVector)
|
||||
writeStateAsUpdate(encoder, doc, targetStateVector)
|
||||
return encoder.toUint8Array()
|
||||
const updates = [encoder.toUint8Array()]
|
||||
// also add the pending updates (if there are any)
|
||||
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) {
|
||||
if (encoder.constructor === UpdateEncoderV1) {
|
||||
return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update)))
|
||||
} else if (encoder.constructor === UpdateEncoderV2) {
|
||||
return mergeUpdatesV2(updates)
|
||||
}
|
||||
}
|
||||
return updates[0]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,12 +553,12 @@ export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector, encoder = n
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @param {AbstractDSDecoder} decoder
|
||||
* @param {DSDecoderV1 | DSDecoderV2} decoder
|
||||
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||
*
|
||||
* @function
|
||||
@@ -552,7 +582,7 @@ export const readStateVector = decoder => {
|
||||
*
|
||||
* @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.
|
||||
@@ -562,16 +592,16 @@ export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoder
|
||||
*
|
||||
* @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
|
||||
* @function
|
||||
*/
|
||||
export const writeStateVector = (encoder, sv) => {
|
||||
encoding.writeVarUint(encoder.restEncoder, sv.size)
|
||||
sv.forEach((clock, client) => {
|
||||
Array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
||||
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
|
||||
encoding.writeVarUint(encoder.restEncoder, clock)
|
||||
})
|
||||
@@ -579,7 +609,7 @@ export const writeStateVector = (encoder, sv) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractDSEncoder} encoder
|
||||
* @param {DSEncoderV1 | DSEncoderV2} encoder
|
||||
* @param {Doc} doc
|
||||
*
|
||||
* @function
|
||||
@@ -589,23 +619,27 @@ export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encod
|
||||
/**
|
||||
* Encode State as Uint8Array.
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @param {AbstractDSEncoder} [encoder]
|
||||
* @param {Doc|Map<number,number>} doc
|
||||
* @param {DSEncoderV1 | DSEncoderV2} [encoder]
|
||||
* @return {Uint8Array}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => {
|
||||
if (doc instanceof Map) {
|
||||
writeStateVector(encoder, doc)
|
||||
} else {
|
||||
writeDocumentStateVector(encoder, doc)
|
||||
}
|
||||
return encoder.toUint8Array()
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode State as Uint8Array.
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @param {Doc|Map<number,number>} doc
|
||||
* @return {Uint8Array}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const encodeStateVector = doc => encodeStateVectorV2(doc, new DefaultDSEncoder())
|
||||
export const encodeStateVector = doc => encodeStateVectorV2(doc, new DSEncoderV1())
|
||||
|
||||
558
src/utils/updates.js
Normal file
558
src/utils/updates.js
Normal file
@@ -0,0 +1,558 @@
|
||||
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as logging from 'lib0/logging'
|
||||
import * as math from 'lib0/math'
|
||||
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)), false)
|
||||
let curr = updateDecoder.curr
|
||||
if (curr !== null) {
|
||||
let size = 0
|
||||
let currClient = curr.id.client
|
||||
let stopCounting = curr.id.clock !== 0 // must start at 0
|
||||
let currClock = stopCounting ? 0 : curr.id.clock + curr.length
|
||||
for (; curr !== null; curr = updateDecoder.next()) {
|
||||
if (currClient !== curr.id.client) {
|
||||
if (currClock !== 0) {
|
||||
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
|
||||
currClock = 0
|
||||
stopCounting = curr.id.clock !== 0
|
||||
}
|
||||
// we ignore skips
|
||||
if (curr.constructor === Skip) {
|
||||
stopCounting = true
|
||||
}
|
||||
if (!stopCounting) {
|
||||
currClock = curr.id.clock + curr.length
|
||||
}
|
||||
}
|
||||
// write what we have
|
||||
if (currClock !== 0) {
|
||||
size++
|
||||
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) => {
|
||||
if (updates.length === 1) {
|
||||
return updates[0]
|
||||
}
|
||||
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) {
|
||||
// @todo this incurs an exponential overhead. We could instead only sort the item that changed.
|
||||
// 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) {
|
||||
// @todo remove references to skip since the structDecoders must filter Skips.
|
||||
return dec1.curr.constructor === dec2.curr.constructor
|
||||
? 0
|
||||
: dec1.curr.constructor === Skip ? 1 : -1 // we are filtering skips anyway.
|
||||
} 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)
|
||||
let iterated = false
|
||||
|
||||
// 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()
|
||||
iterated = true
|
||||
}
|
||||
if (
|
||||
curr === null || // current decoder is empty
|
||||
curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient`
|
||||
(iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) // the above while loop was used and we are potentially missing updates
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
||||
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
|
||||
*/
|
||||
export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
|
||||
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||
const updateEncoder = new YEncoder()
|
||||
const lazyWriter = new LazyStructWriter(updateEncoder)
|
||||
|
||||
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||
writeStructToLazyStructWriter(lazyWriter, curr, 0)
|
||||
}
|
||||
finishLazyStructWriting(lazyWriter)
|
||||
const ds = readDeleteSet(updateDecoder)
|
||||
writeDeleteSet(updateEncoder, ds)
|
||||
return updateEncoder.toUint8Array()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
*/
|
||||
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} update
|
||||
*/
|
||||
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, UpdateDecoderV2, UpdateEncoderV1)
|
||||
@@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as buffer from 'lib0/buffer.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as buffer from 'lib0/buffer'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as t from 'lib0/testing'
|
||||
|
||||
/**
|
||||
* Client id should be changed when an instance receives updates from another client using the same client id.
|
||||
@@ -40,6 +40,7 @@ export const testToJSON = tc => {
|
||||
|
||||
const arr = doc.getArray('array')
|
||||
arr.push(['test1'])
|
||||
t.compare(arr.toJSON(), ['test1'])
|
||||
|
||||
const map = doc.getMap('map')
|
||||
map.set('k1', 'v1')
|
||||
@@ -88,7 +89,7 @@ export const testSubdoc = tc => {
|
||||
subdocs.get('a').load()
|
||||
t.compare(event, [[], [], ['a']])
|
||||
|
||||
subdocs.set('b', new Y.Doc({ guid: 'a' }))
|
||||
subdocs.set('b', new Y.Doc({ guid: 'a', shouldLoad: false }))
|
||||
t.compare(event, [['a'], [], []])
|
||||
subdocs.get('b').load()
|
||||
t.compare(event, [[], [], ['a']])
|
||||
@@ -124,3 +125,123 @@ export const testSubdoc = tc => {
|
||||
t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c'])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testSubdocLoadEdgeCases = tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yarray = ydoc.getArray()
|
||||
const subdoc1 = new Y.Doc()
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let lastEvent = null
|
||||
ydoc.on('subdocs', event => {
|
||||
lastEvent = event
|
||||
})
|
||||
yarray.insert(0, [subdoc1])
|
||||
t.assert(subdoc1.shouldLoad)
|
||||
t.assert(subdoc1.autoLoad === false)
|
||||
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1))
|
||||
t.assert(lastEvent !== null && lastEvent.added.has(subdoc1))
|
||||
// destroy and check whether lastEvent adds it again to added (it shouldn't)
|
||||
subdoc1.destroy()
|
||||
const subdoc2 = yarray.get(0)
|
||||
t.assert(subdoc1 !== subdoc2)
|
||||
t.assert(lastEvent !== null && lastEvent.added.has(subdoc2))
|
||||
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2))
|
||||
// load
|
||||
subdoc2.load()
|
||||
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2))
|
||||
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2))
|
||||
// apply from remote
|
||||
const ydoc2 = new Y.Doc()
|
||||
ydoc2.on('subdocs', event => {
|
||||
lastEvent = event
|
||||
})
|
||||
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc))
|
||||
const subdoc3 = ydoc2.getArray().get(0)
|
||||
t.assert(subdoc3.shouldLoad === false)
|
||||
t.assert(subdoc3.autoLoad === false)
|
||||
t.assert(lastEvent !== null && lastEvent.added.has(subdoc3))
|
||||
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc3))
|
||||
// load
|
||||
subdoc3.load()
|
||||
t.assert(subdoc3.shouldLoad)
|
||||
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc3))
|
||||
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testSubdocLoadEdgeCasesAutoload = tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yarray = ydoc.getArray()
|
||||
const subdoc1 = new Y.Doc({ autoLoad: true })
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let lastEvent = null
|
||||
ydoc.on('subdocs', event => {
|
||||
lastEvent = event
|
||||
})
|
||||
yarray.insert(0, [subdoc1])
|
||||
t.assert(subdoc1.shouldLoad)
|
||||
t.assert(subdoc1.autoLoad)
|
||||
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1))
|
||||
t.assert(lastEvent !== null && lastEvent.added.has(subdoc1))
|
||||
// destroy and check whether lastEvent adds it again to added (it shouldn't)
|
||||
subdoc1.destroy()
|
||||
const subdoc2 = yarray.get(0)
|
||||
t.assert(subdoc1 !== subdoc2)
|
||||
t.assert(lastEvent !== null && lastEvent.added.has(subdoc2))
|
||||
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2))
|
||||
// load
|
||||
subdoc2.load()
|
||||
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2))
|
||||
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2))
|
||||
// apply from remote
|
||||
const ydoc2 = new Y.Doc()
|
||||
ydoc2.on('subdocs', event => {
|
||||
lastEvent = event
|
||||
})
|
||||
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc))
|
||||
const subdoc3 = ydoc2.getArray().get(0)
|
||||
t.assert(subdoc1.shouldLoad)
|
||||
t.assert(subdoc1.autoLoad)
|
||||
t.assert(lastEvent !== null && lastEvent.added.has(subdoc3))
|
||||
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testSubdocsUndo = tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const elems = ydoc.getXmlFragment()
|
||||
const undoManager = new Y.UndoManager(elems)
|
||||
const subdoc = new Y.Doc()
|
||||
// @ts-ignore
|
||||
elems.insert(0, [subdoc])
|
||||
undoManager.undo()
|
||||
undoManager.redo()
|
||||
t.assert(elems.length === 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testLoadDocs = async tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
t.assert(ydoc.isLoaded === false)
|
||||
let loadedEvent = false
|
||||
ydoc.on('load', () => {
|
||||
loadedEvent = true
|
||||
})
|
||||
ydoc.emit('load', [ydoc])
|
||||
await ydoc.whenLoaded
|
||||
t.assert(loadedEvent)
|
||||
t.assert(ydoc.isLoaded)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as promise from 'lib0/promise.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as promise from 'lib0/promise'
|
||||
|
||||
import {
|
||||
contentRefs,
|
||||
@@ -12,17 +12,21 @@ import {
|
||||
readContentFormat,
|
||||
readContentAny,
|
||||
readContentDoc,
|
||||
readContentMove,
|
||||
Doc,
|
||||
PermanentUserData,
|
||||
encodeStateAsUpdate,
|
||||
applyUpdate
|
||||
} from '../src/internals.js'
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testStructReferences = tc => {
|
||||
t.assert(contentRefs.length === 10)
|
||||
t.assert(contentRefs.length === 12)
|
||||
// contentRefs[0] is reserved for GC
|
||||
t.assert(contentRefs[1] === readContentDeleted)
|
||||
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
|
||||
t.assert(contentRefs[3] === readContentBinary)
|
||||
@@ -32,6 +36,8 @@ export const testStructReferences = tc => {
|
||||
t.assert(contentRefs[7] === readContentType)
|
||||
t.assert(contentRefs[8] === readContentAny)
|
||||
t.assert(contentRefs[9] === readContentDoc)
|
||||
// contentRefs[10] is reserved for Skip structs
|
||||
t.assert(contentRefs[11] === readContentMove)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,3 +67,45 @@ export const testPermanentUserData = async tc => {
|
||||
const pd3 = new PermanentUserData(ydoc3)
|
||||
pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
|
||||
}
|
||||
|
||||
/**
|
||||
* Reported here: https://github.com/yjs/yjs/issues/308
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testDiffStateVectorOfUpdateIsEmpty = tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
/**
|
||||
* @type {null | Uint8Array}
|
||||
*/
|
||||
let sv = /* any */ (null)
|
||||
ydoc.getText().insert(0, 'a')
|
||||
ydoc.on('update', update => {
|
||||
sv = Y.encodeStateVectorFromUpdate(update)
|
||||
})
|
||||
// should produce an update with an empty state vector (because previous ops are missing)
|
||||
ydoc.getText().insert(0, 'a')
|
||||
t.assert(sv !== null && sv.byteLength === 1 && sv[0] === 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reported here: https://github.com/yjs/yjs/issues/308
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testDiffStateVectorOfUpdateIgnoresSkips = tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
/**
|
||||
* @type {Array<Uint8Array>}
|
||||
*/
|
||||
const updates = []
|
||||
ydoc.on('update', update => {
|
||||
updates.push(update)
|
||||
})
|
||||
ydoc.getText().insert(0, 'a')
|
||||
ydoc.getText().insert(0, 'b')
|
||||
ydoc.getText().insert(0, 'c')
|
||||
const update13 = Y.mergeUpdates([updates[0], updates[2]])
|
||||
const sv = Y.encodeStateVectorFromUpdate(update13)
|
||||
const state = Y.decodeStateVector(sv)
|
||||
t.assert(state.get(ydoc.clientID) === 1)
|
||||
t.assert(state.size === 1)
|
||||
}
|
||||
|
||||
@@ -8,16 +8,18 @@ import * as undoredo from './undo-redo.tests.js'
|
||||
import * as compatibility from './compatibility.tests.js'
|
||||
import * as doc from './doc.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 { isBrowser, isNode } from 'lib0/environment.js'
|
||||
import * as log from 'lib0/logging.js'
|
||||
import { runTests } from 'lib0/testing'
|
||||
import { isBrowser, isNode } from 'lib0/environment'
|
||||
import * as log from 'lib0/logging'
|
||||
|
||||
if (isBrowser) {
|
||||
log.createVConsole(document.body)
|
||||
}
|
||||
runTests({
|
||||
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot
|
||||
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
|
||||
}).then(success => {
|
||||
/* istanbul ignore next */
|
||||
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/index.js'
|
||||
import * as t from 'lib0/testing'
|
||||
|
||||
/**
|
||||
* @param {Y.Text} 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)
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { createDocFromSnapshot, Doc, snapshot, YMap } from '../src/internals'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import { init } from './testHelper'
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import { init } from './testHelper.js'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testBasicRestoreSnapshot = tc => {
|
||||
const doc = new Doc({ gc: false })
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['hello'])
|
||||
const snap = snapshot(doc)
|
||||
const snap = Y.snapshot(doc)
|
||||
doc.getArray('array').insert(1, ['world'])
|
||||
|
||||
const docRestored = createDocFromSnapshot(doc, snap)
|
||||
const docRestored = Y.createDocFromSnapshot(doc, snap)
|
||||
|
||||
t.compare(docRestored.getArray('array').toArray(), ['hello'])
|
||||
t.compare(doc.getArray('array').toArray(), ['hello', 'world'])
|
||||
@@ -21,19 +21,19 @@ export const testBasicRestoreSnapshot = tc => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testEmptyRestoreSnapshot = tc => {
|
||||
const doc = new Doc({ gc: false })
|
||||
const snap = snapshot(doc)
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
const snap = Y.snapshot(doc)
|
||||
snap.sv.set(9999, 0)
|
||||
doc.getArray().insert(0, ['world'])
|
||||
|
||||
const docRestored = createDocFromSnapshot(doc, snap)
|
||||
const docRestored = Y.createDocFromSnapshot(doc, snap)
|
||||
|
||||
t.compare(docRestored.getArray().toArray(), [])
|
||||
t.compare(doc.getArray().toArray(), ['world'])
|
||||
|
||||
// now this snapshot reflects the latest state. It shoult still work.
|
||||
const snap2 = snapshot(doc)
|
||||
const docRestored2 = createDocFromSnapshot(doc, snap2)
|
||||
const snap2 = Y.snapshot(doc)
|
||||
const docRestored2 = Y.createDocFromSnapshot(doc, snap2)
|
||||
t.compare(docRestored2.getArray().toArray(), ['world'])
|
||||
}
|
||||
|
||||
@@ -41,15 +41,15 @@ export const testEmptyRestoreSnapshot = tc => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testRestoreSnapshotWithSubType = tc => {
|
||||
const doc = new Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, [new YMap()])
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, [new Y.Map()])
|
||||
const subMap = doc.getArray('array').get(0)
|
||||
subMap.set('key1', 'value1')
|
||||
|
||||
const snap = snapshot(doc)
|
||||
const snap = Y.snapshot(doc)
|
||||
subMap.set('key2', 'value2')
|
||||
|
||||
const docRestored = createDocFromSnapshot(doc, snap)
|
||||
const docRestored = Y.createDocFromSnapshot(doc, snap)
|
||||
|
||||
t.compare(docRestored.getArray('array').toJSON(), [{
|
||||
key1: 'value1'
|
||||
@@ -64,13 +64,13 @@ export const testRestoreSnapshotWithSubType = tc => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testRestoreDeletedItem1 = tc => {
|
||||
const doc = new Doc({ gc: false })
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['item1', 'item2'])
|
||||
|
||||
const snap = snapshot(doc)
|
||||
const snap = Y.snapshot(doc)
|
||||
doc.getArray('array').delete(0)
|
||||
|
||||
const docRestored = createDocFromSnapshot(doc, snap)
|
||||
const docRestored = Y.createDocFromSnapshot(doc, snap)
|
||||
|
||||
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item2'])
|
||||
t.compare(doc.getArray('array').toArray(), ['item2'])
|
||||
@@ -80,15 +80,15 @@ export const testRestoreDeletedItem1 = tc => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testRestoreLeftItem = tc => {
|
||||
const doc = new Doc({ gc: false })
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['item1'])
|
||||
doc.getMap('map').set('test', 1)
|
||||
doc.getArray('array').insert(0, ['item0'])
|
||||
|
||||
const snap = snapshot(doc)
|
||||
const snap = Y.snapshot(doc)
|
||||
doc.getArray('array').delete(1)
|
||||
|
||||
const docRestored = createDocFromSnapshot(doc, snap)
|
||||
const docRestored = Y.createDocFromSnapshot(doc, snap)
|
||||
|
||||
t.compare(docRestored.getArray('array').toArray(), ['item0', 'item1'])
|
||||
t.compare(doc.getArray('array').toArray(), ['item0'])
|
||||
@@ -98,13 +98,13 @@ export const testRestoreLeftItem = tc => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testDeletedItemsBase = tc => {
|
||||
const doc = new Doc({ gc: false })
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['item1'])
|
||||
doc.getArray('array').delete(0)
|
||||
const snap = snapshot(doc)
|
||||
const snap = Y.snapshot(doc)
|
||||
doc.getArray('array').insert(0, ['item0'])
|
||||
|
||||
const docRestored = createDocFromSnapshot(doc, snap)
|
||||
const docRestored = Y.createDocFromSnapshot(doc, snap)
|
||||
|
||||
t.compare(docRestored.getArray('array').toArray(), [])
|
||||
t.compare(doc.getArray('array').toArray(), ['item0'])
|
||||
@@ -114,13 +114,13 @@ export const testDeletedItemsBase = tc => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testDeletedItems2 = tc => {
|
||||
const doc = new Doc({ gc: false })
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
|
||||
doc.getArray('array').delete(1)
|
||||
const snap = snapshot(doc)
|
||||
const snap = Y.snapshot(doc)
|
||||
doc.getArray('array').insert(0, ['item0'])
|
||||
|
||||
const docRestored = createDocFromSnapshot(doc, snap)
|
||||
const docRestored = Y.createDocFromSnapshot(doc, snap)
|
||||
|
||||
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item3'])
|
||||
t.compare(doc.getArray('array').toArray(), ['item0', 'item1', 'item3'])
|
||||
@@ -140,11 +140,11 @@ export const testDependentChanges = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @type Doc
|
||||
* @type {Y.Doc}
|
||||
*/
|
||||
const doc0 = array0.doc
|
||||
/**
|
||||
* @type Doc
|
||||
* @type {Y.Doc}
|
||||
*/
|
||||
const doc1 = array1.doc
|
||||
|
||||
@@ -156,16 +156,16 @@ export const testDependentChanges = tc => {
|
||||
array1.insert(1, ['user2item1'])
|
||||
testConnector.syncAll()
|
||||
|
||||
const snap = snapshot(array0.doc)
|
||||
const snap = Y.snapshot(array0.doc)
|
||||
|
||||
array0.insert(2, ['user1item2'])
|
||||
testConnector.syncAll()
|
||||
array1.insert(3, ['user2item2'])
|
||||
testConnector.syncAll()
|
||||
|
||||
const docRestored0 = createDocFromSnapshot(array0.doc, snap)
|
||||
const docRestored0 = Y.createDocFromSnapshot(array0.doc, snap)
|
||||
t.compare(docRestored0.getArray('array').toArray(), ['user1item1', 'user2item1'])
|
||||
|
||||
const docRestored1 = createDocFromSnapshot(array1.doc, snap)
|
||||
const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
|
||||
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as prng from 'lib0/prng.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as syncProtocol from 'y-protocols/sync.js'
|
||||
import * as object from 'lib0/object.js'
|
||||
import * as Y from '../src/internals.js'
|
||||
export * from '../src/internals.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as prng from 'lib0/prng'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as syncProtocol from 'y-protocols/sync'
|
||||
import * as object from 'lib0/object'
|
||||
import * as Y from '../src/index.js'
|
||||
export * from '../src/index.js'
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
@@ -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 {
|
||||
/**
|
||||
* @param {TestConnector} testConnector
|
||||
@@ -44,12 +77,19 @@ export class TestYInstance extends Y.Doc {
|
||||
*/
|
||||
this.receiving = new Map()
|
||||
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
|
||||
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) {
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeUpdate(encoder, update)
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||
this.updates.push(update)
|
||||
}
|
||||
})
|
||||
this.connect()
|
||||
@@ -162,6 +202,17 @@ export class TestConnector {
|
||||
// send reply message
|
||||
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 false
|
||||
@@ -228,7 +279,7 @@ export class TestConnector {
|
||||
* @param {t.TestCase} tc
|
||||
* @param {{users?:number}} conf
|
||||
* @param {InitTestObjectCallback<T>} [initTestObject]
|
||||
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.YArray<any>,array1:Y.YArray<any>,array2:Y.YArray<any>,map0:Y.YMap<any>,map1:Y.YMap<any>,map2:Y.YMap<any>,map3:Y.YMap<any>,text0:Y.YText,text1:Y.YText,text2:Y.YText,xml0:Y.YXmlElement,xml1:Y.YXmlElement,xml2:Y.YXmlElement}}
|
||||
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
|
||||
*/
|
||||
export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||
/**
|
||||
@@ -240,9 +291,9 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||
const gen = tc.prng
|
||||
// choose an encoding approach at random
|
||||
if (prng.bool(gen)) {
|
||||
Y.useV2Encoding()
|
||||
useV2Encoding()
|
||||
} else {
|
||||
Y.useV1Encoding()
|
||||
useV1Encoding()
|
||||
}
|
||||
|
||||
const testConnector = new TestConnector(gen)
|
||||
@@ -253,12 +304,12 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||
result.users.push(y)
|
||||
result['array' + i] = y.getArray('array')
|
||||
result['map' + i] = y.getMap('map')
|
||||
result['xml' + i] = y.get('xml', Y.YXmlElement)
|
||||
result['xml' + i] = y.get('xml', Y.XmlElement)
|
||||
result['text' + i] = y.getText('text')
|
||||
}
|
||||
testConnector.syncAll()
|
||||
result.testObjects = result.users.map(initTestObject || (() => null))
|
||||
Y.useV1Encoding()
|
||||
useV1Encoding()
|
||||
return /** @type {any} */ (result)
|
||||
}
|
||||
|
||||
@@ -273,15 +324,22 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||
*/
|
||||
export const compare = users => {
|
||||
users.forEach(u => u.connect())
|
||||
while (users[0].tc.flushAllMessages()) {}
|
||||
while (users[0].tc.flushAllMessages()) {} // eslint-disable-line
|
||||
// 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 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.XmlElement).toString())
|
||||
const userTextValues = users.map(u => u.getText('text').toDelta())
|
||||
for (const u of users) {
|
||||
t.assert(u.store.pendingDeleteReaders.length === 0)
|
||||
t.assert(u.store.pendingStack.length === 0)
|
||||
t.assert(u.store.pendingClientsStructRefs.size === 0)
|
||||
t.assert(u.store.pendingDs === null)
|
||||
t.assert(u.store.pendingStructs === null)
|
||||
}
|
||||
// Test Array iterator
|
||||
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
|
||||
@@ -304,10 +362,44 @@ export const compare = users => {
|
||||
t.compare(userMapValues[i], userMapValues[i + 1])
|
||||
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
||||
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
|
||||
t.compare(userTextValues[i], userTextValues[i + 1])
|
||||
t.compare(Y.getStateVector(users[i].store), Y.getStateVector(users[i + 1].store))
|
||||
t.compare(userTextValues[i], userTextValues[i + 1], '', (constructor, a, b) => {
|
||||
if (a instanceof Y.AbstractType) {
|
||||
t.compare(a.toJSON(), b.toJSON())
|
||||
} else if (a !== b) {
|
||||
t.fail('Deltas dont match')
|
||||
}
|
||||
return true
|
||||
})
|
||||
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
|
||||
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
||||
compareStructStores(users[i].store, users[i + 1].store)
|
||||
// @todo
|
||||
// test list-iterator
|
||||
// console.log('dutiraneduiaentdr', users[0].getArray('array')._searchMarker)
|
||||
/*
|
||||
{
|
||||
const user = users[0]
|
||||
user.transact(tr => {
|
||||
const type = user.getArray('array')
|
||||
Y.useSearchMarker(tr, type, type.length, walker => {
|
||||
for (let i = type.length; i >= 0; i--) {
|
||||
const otherWalker = new Y.ListIterator(type)
|
||||
otherWalker.forward(tr, walker.index)
|
||||
otherWalker.forward(tr, 0)
|
||||
walker.forward(tr, 0)
|
||||
t.assert(walker.index === i)
|
||||
t.assert(walker.left === otherWalker.left)
|
||||
t.assert(walker.right === otherWalker.right)
|
||||
t.assert(walker.nextItem === otherWalker.nextItem)
|
||||
t.assert(walker.reachedEnd === otherWalker.reachedEnd)
|
||||
if (i > 0) {
|
||||
walker.backward(tr, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
*/
|
||||
}
|
||||
users.map(u => u.destroy())
|
||||
}
|
||||
@@ -320,8 +412,8 @@ export const compare = users => {
|
||||
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
||||
|
||||
/**
|
||||
* @param {Y.StructStore} ss1
|
||||
* @param {Y.StructStore} ss2
|
||||
* @param {import('../src/internals').StructStore} ss1
|
||||
* @param {import('../src/internals').StructStore} ss2
|
||||
*/
|
||||
export const compareStructStores = (ss1, ss2) => {
|
||||
t.assert(ss1.clients.size === ss2.clients.size)
|
||||
@@ -363,13 +455,13 @@ export const compareStructStores = (ss1, ss2) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Y.DeleteSet} ds1
|
||||
* @param {Y.DeleteSet} ds2
|
||||
* @param {import('../src/internals').DeleteSet} ds1
|
||||
* @param {import('../src/internals').DeleteSet} ds2
|
||||
*/
|
||||
export const compareDS = (ds1, ds2) => {
|
||||
t.assert(ds1.clients.size === ds2.clients.size)
|
||||
ds1.clients.forEach((deleteItems1, client) => {
|
||||
const deleteItems2 = /** @type {Array<Y.DeleteItem>} */ (ds2.clients.get(client))
|
||||
const deleteItems2 = /** @type {Array<import('../src/internals').DeleteItem>} */ (ds2.clients.get(client))
|
||||
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
|
||||
for (let i = 0; i < deleteItems1.length; i++) {
|
||||
const di1 = deleteItems1[i]
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||
|
||||
import {
|
||||
UndoManager
|
||||
} from '../src/internals.js'
|
||||
import { init, compare, applyRandomTests, Doc, UndoManager } from './testHelper.js' // eslint-disable-line
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as t from 'lib0/testing'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoText = tc => {
|
||||
const { testConnector, text0, text1 } = init(tc, { users: 3 })
|
||||
const undoManager = new UndoManager(text0)
|
||||
const undoManager = new Y.UndoManager(text0)
|
||||
|
||||
// items that are added & deleted in the same transaction won't be undo
|
||||
text0.insert(0, 'test')
|
||||
@@ -81,7 +77,7 @@ export const testDoubleUndo = tc => {
|
||||
export const testUndoMap = tc => {
|
||||
const { testConnector, map0, map1 } = init(tc, { users: 2 })
|
||||
map0.set('a', 0)
|
||||
const undoManager = new UndoManager(map0)
|
||||
const undoManager = new Y.UndoManager(map0)
|
||||
map0.set('a', 1)
|
||||
undoManager.undo()
|
||||
t.assert(map0.get('a') === 0)
|
||||
@@ -120,7 +116,7 @@ export const testUndoMap = tc => {
|
||||
*/
|
||||
export const testUndoArray = tc => {
|
||||
const { testConnector, array0, array1 } = init(tc, { users: 3 })
|
||||
const undoManager = new UndoManager(array0)
|
||||
const undoManager = new Y.UndoManager(array0)
|
||||
array0.insert(0, [1, 2, 3])
|
||||
array1.insert(0, [4, 5, 6])
|
||||
testConnector.syncAll()
|
||||
@@ -171,7 +167,7 @@ export const testUndoArray = tc => {
|
||||
*/
|
||||
export const testUndoXml = tc => {
|
||||
const { xml0 } = init(tc, { users: 3 })
|
||||
const undoManager = new UndoManager(xml0)
|
||||
const undoManager = new Y.UndoManager(xml0)
|
||||
const child = new Y.XmlElement('p')
|
||||
xml0.insert(0, [child])
|
||||
const textchild = new Y.XmlText('content')
|
||||
@@ -196,15 +192,17 @@ export const testUndoXml = tc => {
|
||||
*/
|
||||
export const testUndoEvents = tc => {
|
||||
const { text0 } = init(tc, { users: 3 })
|
||||
const undoManager = new UndoManager(text0)
|
||||
const undoManager = new Y.UndoManager(text0)
|
||||
let counter = 0
|
||||
let receivedMetadata = -1
|
||||
undoManager.on('stack-item-added', /** @param {any} event */ event => {
|
||||
t.assert(event.type != null)
|
||||
t.assert(event.changedParentTypes != null && event.changedParentTypes.has(text0))
|
||||
event.stackItem.meta.set('test', counter++)
|
||||
})
|
||||
undoManager.on('stack-item-popped', /** @param {any} event */ event => {
|
||||
t.assert(event.type != null)
|
||||
t.assert(event.changedParentTypes != null && event.changedParentTypes.has(text0))
|
||||
receivedMetadata = event.stackItem.meta.get('test')
|
||||
})
|
||||
text0.insert(0, 'abc')
|
||||
@@ -220,7 +218,7 @@ export const testUndoEvents = tc => {
|
||||
export const testTrackClass = tc => {
|
||||
const { users, text0 } = init(tc, { users: 3 })
|
||||
// only track origins that are numbers
|
||||
const undoManager = new UndoManager(text0, { trackedOrigins: new Set([Number]) })
|
||||
const undoManager = new Y.UndoManager(text0, { trackedOrigins: new Set([Number]) })
|
||||
users[0].transact(() => {
|
||||
text0.insert(0, 'abc')
|
||||
}, 42)
|
||||
@@ -238,8 +236,8 @@ export const testTypeScope = tc => {
|
||||
const text0 = new Y.Text()
|
||||
const text1 = new Y.Text()
|
||||
array0.insert(0, [text0, text1])
|
||||
const undoManager = new UndoManager(text0)
|
||||
const undoManagerBoth = new UndoManager([text0, text1])
|
||||
const undoManager = new Y.UndoManager(text0)
|
||||
const undoManagerBoth = new Y.UndoManager([text0, text1])
|
||||
text1.insert(0, 'abc')
|
||||
t.assert(undoManager.undoStack.length === 0)
|
||||
t.assert(undoManagerBoth.undoStack.length === 1)
|
||||
@@ -250,6 +248,26 @@ export const testTypeScope = tc => {
|
||||
t.assert(text1.toString() === '')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoInEmbed = tc => {
|
||||
const { text0 } = init(tc, { users: 3 })
|
||||
const undoManager = new Y.UndoManager(text0)
|
||||
const nestedText = new Y.Text('initial text')
|
||||
undoManager.stopCapturing()
|
||||
text0.insertEmbed(0, nestedText, { bold: true })
|
||||
t.assert(nestedText.toString() === 'initial text')
|
||||
undoManager.stopCapturing()
|
||||
nestedText.delete(0, nestedText.length)
|
||||
nestedText.insert(0, 'other text')
|
||||
t.assert(nestedText.toString() === 'other text')
|
||||
undoManager.undo()
|
||||
t.assert(nestedText.toString() === 'initial text')
|
||||
undoManager.undo()
|
||||
t.assert(text0.length === 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -258,7 +276,7 @@ export const testUndoDeleteFilter = tc => {
|
||||
* @type {Array<Y.Map<any>>}
|
||||
*/
|
||||
const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0)
|
||||
const undoManager = new UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) })
|
||||
const undoManager = new Y.UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) })
|
||||
const map0 = new Y.Map()
|
||||
map0.set('hi', 1)
|
||||
const map1 = new Y.Map()
|
||||
@@ -268,3 +286,89 @@ export const testUndoDeleteFilter = tc => {
|
||||
array0.get(0)
|
||||
t.assert(Array.from(array0.get(0).keys()).length === 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoUntilChangePerformed = tc => {
|
||||
const doc = new Y.Doc()
|
||||
const doc2 = new Y.Doc()
|
||||
doc.on('update', update => Y.applyUpdate(doc2, update))
|
||||
doc2.on('update', update => Y.applyUpdate(doc, update))
|
||||
|
||||
const yArray = doc.getArray('array')
|
||||
const yArray2 = doc2.getArray('array')
|
||||
const yMap = new Y.Map()
|
||||
yMap.set('hello', 'world')
|
||||
yArray.push([yMap])
|
||||
const yMap2 = new Y.Map()
|
||||
yMap2.set('key', 'value')
|
||||
yArray.push([yMap2])
|
||||
|
||||
const undoManager = new Y.UndoManager([yArray], { trackedOrigins: new Set([doc.clientID]) })
|
||||
const undoManager2 = new Y.UndoManager([doc2.get('array')], { trackedOrigins: new Set([doc2.clientID]) })
|
||||
|
||||
Y.transact(doc, () => yMap2.set('key', 'value modified'), doc.clientID)
|
||||
undoManager.stopCapturing()
|
||||
Y.transact(doc, () => yMap.set('hello', 'world modified'), doc.clientID)
|
||||
Y.transact(doc2, () => yArray2.delete(0), doc2.clientID)
|
||||
undoManager2.undo()
|
||||
undoManager.undo()
|
||||
t.compareStrings(yMap2.get('key'), 'value')
|
||||
}
|
||||
|
||||
/**
|
||||
* This issue has been reported in https://github.com/yjs/yjs/issues/317
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoNestedUndoIssue = tc => {
|
||||
const doc = new Y.Doc({ gc: false })
|
||||
const design = doc.getMap()
|
||||
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
|
||||
|
||||
/**
|
||||
* @type {Y.Map<any>}
|
||||
*/
|
||||
const text = new Y.Map()
|
||||
|
||||
const blocks1 = new Y.Array()
|
||||
const blocks1block = new Y.Map()
|
||||
|
||||
doc.transact(() => {
|
||||
blocks1block.set('text', 'Type Something')
|
||||
blocks1.push([blocks1block])
|
||||
text.set('blocks', blocks1block)
|
||||
design.set('text', text)
|
||||
})
|
||||
|
||||
const blocks2 = new Y.Array()
|
||||
const blocks2block = new Y.Map()
|
||||
doc.transact(() => {
|
||||
blocks2block.set('text', 'Something')
|
||||
blocks2.push([blocks2block])
|
||||
text.set('blocks', blocks2block)
|
||||
})
|
||||
|
||||
const blocks3 = new Y.Array()
|
||||
const blocks3block = new Y.Map()
|
||||
doc.transact(() => {
|
||||
blocks3block.set('text', 'Something Else')
|
||||
blocks3.push([blocks3block])
|
||||
text.set('blocks', blocks3block)
|
||||
})
|
||||
|
||||
t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } })
|
||||
undoManager.undo()
|
||||
t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } })
|
||||
undoManager.undo()
|
||||
t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } })
|
||||
undoManager.undo()
|
||||
t.compare(design.toJSON(), { })
|
||||
undoManager.redo()
|
||||
t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } })
|
||||
undoManager.redo()
|
||||
t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } })
|
||||
undoManager.redo()
|
||||
t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } })
|
||||
}
|
||||
|
||||
288
tests/updates.tests.js
Normal file
288
tests/updates.tests.js
Normal file
@@ -0,0 +1,288 @@
|
||||
import * as t from 'lib0/testing'
|
||||
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'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
|
||||
/**
|
||||
* @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)
|
||||
t.compare(partMeta, diffedMeta)
|
||||
{
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testMergePendingUpdates = tc => {
|
||||
const yDoc = new Y.Doc()
|
||||
/**
|
||||
* @type {Array<Uint8Array>}
|
||||
*/
|
||||
const serverUpdates = []
|
||||
yDoc.on('update', (update, origin, c) => {
|
||||
serverUpdates.splice(serverUpdates.length, 0, update)
|
||||
})
|
||||
const yText = yDoc.getText('textBlock')
|
||||
yText.applyDelta([{ insert: 'r' }])
|
||||
yText.applyDelta([{ insert: 'o' }])
|
||||
yText.applyDelta([{ insert: 'n' }])
|
||||
yText.applyDelta([{ insert: 'e' }])
|
||||
yText.applyDelta([{ insert: 'n' }])
|
||||
|
||||
const yDoc1 = new Y.Doc()
|
||||
Y.applyUpdate(yDoc1, serverUpdates[0])
|
||||
const update1 = Y.encodeStateAsUpdate(yDoc1)
|
||||
|
||||
const yDoc2 = new Y.Doc()
|
||||
Y.applyUpdate(yDoc2, update1)
|
||||
Y.applyUpdate(yDoc2, serverUpdates[1])
|
||||
const update2 = Y.encodeStateAsUpdate(yDoc2)
|
||||
|
||||
const yDoc3 = new Y.Doc()
|
||||
Y.applyUpdate(yDoc3, update2)
|
||||
Y.applyUpdate(yDoc3, serverUpdates[3])
|
||||
const update3 = Y.encodeStateAsUpdate(yDoc3)
|
||||
|
||||
const yDoc4 = new Y.Doc()
|
||||
Y.applyUpdate(yDoc4, update3)
|
||||
Y.applyUpdate(yDoc4, serverUpdates[2])
|
||||
const update4 = Y.encodeStateAsUpdate(yDoc4)
|
||||
|
||||
const yDoc5 = new Y.Doc()
|
||||
Y.applyUpdate(yDoc5, update4)
|
||||
Y.applyUpdate(yDoc5, serverUpdates[4])
|
||||
// @ts-ignore
|
||||
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
|
||||
|
||||
const yText5 = yDoc5.getText('textBlock')
|
||||
t.compareStrings(yText5.toString(), 'nenor')
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||
import { init, compare, applyRandomTests, Doc, AbstractType, TestConnector } from './testHelper.js' // eslint-disable-line
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as prng from 'lib0/prng.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as prng from 'lib0/prng'
|
||||
import * as math from 'lib0/math'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
@@ -32,6 +32,78 @@ export const testSlice = tc => {
|
||||
t.compareArrays(arr.slice(0, 2), [0, 1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugging yjs#297 - a critical bug connected to the search-marker approach
|
||||
*
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testLengthIssue = tc => {
|
||||
const doc1 = new Y.Doc()
|
||||
const arr = doc1.getArray('array')
|
||||
arr.push([0, 1, 2, 3])
|
||||
arr.delete(0)
|
||||
arr.insert(0, [0])
|
||||
t.assert(arr.length === arr.toArray().length)
|
||||
doc1.transact(() => {
|
||||
arr.delete(1)
|
||||
t.assert(arr.length === arr.toArray().length)
|
||||
arr.insert(1, [1])
|
||||
t.assert(arr.length === arr.toArray().length)
|
||||
arr.delete(2)
|
||||
t.assert(arr.length === arr.toArray().length)
|
||||
arr.insert(2, [2])
|
||||
t.assert(arr.length === arr.toArray().length)
|
||||
})
|
||||
t.assert(arr.length === arr.toArray().length)
|
||||
arr.delete(1)
|
||||
t.assert(arr.length === arr.toArray().length)
|
||||
arr.insert(1, [1])
|
||||
t.assert(arr.length === arr.toArray().length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugging yjs#314
|
||||
*
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testLengthIssue2 = tc => {
|
||||
const doc = new Y.Doc()
|
||||
const next = doc.getArray()
|
||||
doc.transact(() => {
|
||||
next.insert(0, ['group2'])
|
||||
})
|
||||
doc.transact(() => {
|
||||
next.insert(1, ['rectangle3'])
|
||||
})
|
||||
doc.transact(() => {
|
||||
next.delete(0)
|
||||
next.insert(0, ['rectangle3'])
|
||||
})
|
||||
next.delete(1)
|
||||
doc.transact(() => {
|
||||
next.insert(1, ['ellipse4'])
|
||||
})
|
||||
doc.transact(() => {
|
||||
next.insert(2, ['ellipse3'])
|
||||
})
|
||||
doc.transact(() => {
|
||||
next.insert(3, ['ellipse2'])
|
||||
})
|
||||
doc.transact(() => {
|
||||
doc.transact(() => {
|
||||
t.fails(() => {
|
||||
next.insert(5, ['rectangle2'])
|
||||
})
|
||||
next.insert(4, ['rectangle2'])
|
||||
})
|
||||
doc.transact(() => {
|
||||
// this should not throw an error message
|
||||
next.delete(4)
|
||||
})
|
||||
})
|
||||
console.log(next.toArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -64,7 +136,7 @@ export const testInsertThreeElementsTryRegetProperty = tc => {
|
||||
* @param {t.TestCase} 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])
|
||||
array1.insert(0, [1])
|
||||
array2.insert(0, [2])
|
||||
@@ -107,7 +179,7 @@ export const testInsertionsInLateSync = tc => {
|
||||
* @param {t.TestCase} 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'])
|
||||
testConnector.flushAllMessages()
|
||||
users[1].disconnect()
|
||||
@@ -360,6 +432,46 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testMove = tc => {
|
||||
{
|
||||
// move in uninitialized type
|
||||
const yarr = new Y.Array()
|
||||
yarr.insert(0, [1, 2, 3])
|
||||
yarr.move(1, 0)
|
||||
// @ts-ignore
|
||||
t.compare(yarr._prelimContent, [2, 1, 3])
|
||||
}
|
||||
const { array0, array1, users } = init(tc, { users: 3 })
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let event0 = null
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let event1 = null
|
||||
array0.observe(event => {
|
||||
event0 = event
|
||||
})
|
||||
array1.observe(event => {
|
||||
event1 = event
|
||||
})
|
||||
array0.insert(0, [1, 2, 3])
|
||||
array0.move(1, 0)
|
||||
t.compare(array0.toArray(), [2, 1, 3])
|
||||
t.compare(event0.delta, [{ insert: [2] }, { retain: 1 }, { delete: 1 }])
|
||||
Y.applyUpdate(users[1], Y.encodeStateAsUpdate(users[0]))
|
||||
t.compare(array1.toArray(), [2, 1, 3])
|
||||
t.compare(event1.delta, [{ insert: [2, 1, 3] }])
|
||||
array0.move(0, 2)
|
||||
t.compare(array0.toArray(), [1, 2, 3])
|
||||
t.compare(event0.delta, [{ delete: 1 }, { retain: 1 }, { insert: [2] }])
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -384,17 +496,32 @@ const getUniqueNumber = () => _uniqueNumber++
|
||||
|
||||
/**
|
||||
* @type {Array<function(Doc,prng.PRNG,any):void>}
|
||||
*
|
||||
* @todo to replace content to a separate data structure so we know that insert & returns work as expected!!!
|
||||
*/
|
||||
const arrayTransactions = [
|
||||
function move (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
if (yarray.length === 0) {
|
||||
return
|
||||
}
|
||||
const pos = prng.int32(gen, 0, yarray.length - 1)
|
||||
const newPos = prng.int32(gen, 0, yarray.length)
|
||||
const oldContent = yarray.toArray()
|
||||
yarray.move(pos, newPos)
|
||||
const [x] = oldContent.splice(pos, 1)
|
||||
oldContent.splice(pos < newPos ? newPos - 1 : newPos, 0, x)
|
||||
t.compareArrays(yarray.toArray(), oldContent) // we want to make sure that fastSearch markers insert at the correct position
|
||||
},
|
||||
function insert (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
var uniqueNumber = getUniqueNumber()
|
||||
var content = []
|
||||
var len = prng.int32(gen, 1, 4)
|
||||
for (var i = 0; i < len; i++) {
|
||||
const uniqueNumber = getUniqueNumber()
|
||||
const content = []
|
||||
const len = prng.int32(gen, 1, 4)
|
||||
for (let i = 0; i < len; i++) {
|
||||
content.push(uniqueNumber)
|
||||
}
|
||||
var pos = prng.int32(gen, 0, yarray.length)
|
||||
const pos = prng.int32(gen, 0, yarray.length)
|
||||
const oldContent = yarray.toArray()
|
||||
yarray.insert(pos, content)
|
||||
oldContent.splice(pos, 0, ...content)
|
||||
@@ -402,29 +529,34 @@ const arrayTransactions = [
|
||||
},
|
||||
function insertTypeArray (user, gen) {
|
||||
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()])
|
||||
var array2 = yarray.get(pos)
|
||||
const array2 = yarray.get(pos)
|
||||
array2.insert(0, [1, 2, 3, 4])
|
||||
},
|
||||
function insertTypeMap (user, gen) {
|
||||
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()])
|
||||
var map = yarray.get(pos)
|
||||
const map = yarray.get(pos)
|
||||
map.set('someprop', 42)
|
||||
map.set('someprop', 43)
|
||||
map.set('someprop', 44)
|
||||
},
|
||||
function insertTypeNull (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
const pos = prng.int32(gen, 0, yarray.length)
|
||||
yarray.insert(pos, [null])
|
||||
},
|
||||
function _delete (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
var length = yarray.length
|
||||
const length = yarray.length
|
||||
if (length > 0) {
|
||||
var somePos = prng.int32(gen, 0, length - 1)
|
||||
var delLength = prng.int32(gen, 1, math.min(2, length - somePos))
|
||||
let somePos = prng.int32(gen, 0, length - 1)
|
||||
let delLength = prng.int32(gen, 1, math.min(2, length - somePos))
|
||||
if (prng.bool(gen)) {
|
||||
var type = yarray.get(somePos)
|
||||
if (type.length > 0) {
|
||||
const type = yarray.get(somePos)
|
||||
if (type instanceof Y.Array && type.length > 0) {
|
||||
somePos = prng.int32(gen, 0, type.length - 1)
|
||||
delLength = prng.int32(gen, 0, math.min(2, type.length - somePos))
|
||||
type.delete(somePos, delLength)
|
||||
@@ -439,11 +571,49 @@ const arrayTransactions = [
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {Y.Doc} user
|
||||
*/
|
||||
const monitorArrayTestObject = user => {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
const arr = []
|
||||
const yarr = user.getArray('array')
|
||||
yarr.observe(event => {
|
||||
let currpos = 0
|
||||
const delta = event.delta
|
||||
for (let i = 0; i < delta.length; i++) {
|
||||
const d = delta[i]
|
||||
if (d.insert != null) {
|
||||
arr.splice(currpos, 0, ...(/** @type {Array<any>} */ (d.insert)))
|
||||
currpos += /** @type {Array<any>} */ (d.insert).length
|
||||
} else if (d.retain != null) {
|
||||
currpos += d.retain
|
||||
} else {
|
||||
arr.splice(currpos, d.delete)
|
||||
}
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ testObjects: Array<Array<any>>, users: Array<Y.Doc> }} cmp
|
||||
*/
|
||||
const compareTestobjects = cmp => {
|
||||
const arrs = cmp.testObjects
|
||||
for (let i = 0; i < arrs.length; i++) {
|
||||
const type = cmp.users[i].getArray('array')
|
||||
t.compareArrays(arrs[i], type.toArray())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testRepeatGeneratingYarrayTests6 = tc => {
|
||||
applyRandomTests(tc, arrayTransactions, 6)
|
||||
compareTestobjects(applyRandomTests(tc, arrayTransactions, 3, monitorArrayTestObject))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
} from '../src/internals.js'
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as prng from 'lib0/prng.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as prng from 'lib0/prng'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
@@ -42,6 +42,7 @@ export const testBasicMapTests = tc => {
|
||||
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
|
||||
users[2].disconnect()
|
||||
|
||||
map0.set('null', null)
|
||||
map0.set('number', 1)
|
||||
map0.set('string', 'hello Y')
|
||||
map0.set('object', { key: { key2: 'value' } })
|
||||
@@ -54,26 +55,29 @@ export const testBasicMapTests = tc => {
|
||||
array.insert(0, [0])
|
||||
array.insert(0, [-1])
|
||||
|
||||
t.assert(map0.get('null') === null, 'client 0 computed the change (null)')
|
||||
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
|
||||
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
|
||||
t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)')
|
||||
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
|
||||
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
|
||||
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
|
||||
t.assert(map0.size === 6, 'client 0 map has correct size')
|
||||
t.assert(map0.size === 7, 'client 0 map has correct size')
|
||||
|
||||
users[2].connect()
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
t.assert(map1.get('null') === null, 'client 1 received the update (null)')
|
||||
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
|
||||
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
|
||||
t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)')
|
||||
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
|
||||
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
|
||||
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
|
||||
t.assert(map1.size === 6, 'client 1 map has correct size')
|
||||
t.assert(map1.size === 7, 'client 1 map has correct size')
|
||||
|
||||
// compare disconnected user
|
||||
t.assert(map2.get('null') === null, 'client 2 received the update (null) - was disconnected')
|
||||
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
|
||||
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
|
||||
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')
|
||||
@@ -138,7 +142,7 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
|
||||
t.compare(map0.get('stuff'), 'stuffy')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
const u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'stuffy')
|
||||
}
|
||||
compare(users)
|
||||
@@ -153,7 +157,7 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
|
||||
map1.set('stuff', 'c1')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
const u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'c1')
|
||||
}
|
||||
compare(users)
|
||||
@@ -183,12 +187,55 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
|
||||
map1.delete('stuff')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
const u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testSetAndClearOfMapProperties = tc => {
|
||||
const { testConnector, users, map0 } = init(tc, { users: 1 })
|
||||
map0.set('stuff', 'c0')
|
||||
map0.set('otherstuff', 'c1')
|
||||
map0.clear()
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
t.assert(u.get('otherstuff') === undefined)
|
||||
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testSetAndClearOfMapPropertiesWithConflicts = tc => {
|
||||
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
map0.set('otherstuff', 'c0')
|
||||
map1.set('otherstuff', 'c1')
|
||||
map2.set('otherstuff', 'c2')
|
||||
map3.set('otherstuff', 'c3')
|
||||
map3.clear()
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
t.assert(u.get('otherstuff') === undefined)
|
||||
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -200,7 +247,7 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
|
||||
map2.set('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
const u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'c3')
|
||||
}
|
||||
compare(users)
|
||||
@@ -223,7 +270,7 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
|
||||
map3.delete('stuff')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
const u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
}
|
||||
compare(users)
|
||||
@@ -296,7 +343,7 @@ export const testObserversUsingObservedeep = tc => {
|
||||
* @param {Object<string,any>} should
|
||||
*/
|
||||
const compareEvent = (is, should) => {
|
||||
for (var key in should) {
|
||||
for (const key in should) {
|
||||
t.compare(should[key], is[key])
|
||||
}
|
||||
}
|
||||
@@ -335,6 +382,30 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testThrowsDeleteEventsOnClear = tc => {
|
||||
const { users, map0 } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
let event = {}
|
||||
map0.observe(e => {
|
||||
event = e // just put it on event, should be thrown synchronously anyway
|
||||
})
|
||||
// set values
|
||||
map0.set('stuff', 4)
|
||||
map0.set('otherstuff', new Y.Array())
|
||||
// clear
|
||||
map0.clear()
|
||||
compareEvent(event, {
|
||||
keysChanged: new Set(['stuff', 'otherstuff']),
|
||||
target: map0
|
||||
})
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -474,12 +545,12 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
|
||||
const mapTransactions = [
|
||||
function set (user, gen) {
|
||||
const key = prng.oneOf(gen, ['one', 'two'])
|
||||
var value = prng.utf16String(gen)
|
||||
const value = prng.utf16String(gen)
|
||||
user.getMap('map').set(key, value)
|
||||
},
|
||||
function setType (user, gen) {
|
||||
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)
|
||||
if (type instanceof Y.Array) {
|
||||
type.insert(0, [1, 2, 3, 4])
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import * as Y from './testHelper.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as prng from 'lib0/prng.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as prng from 'lib0/prng'
|
||||
import * as math from 'lib0/math'
|
||||
|
||||
const { init, compare } = Y
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testDeltaAfterConcurrentFormatting = tc => {
|
||||
const { text0, text1, testConnector } = init(tc, { users: 2 })
|
||||
text0.insert(0, 'abcde')
|
||||
testConnector.flushAllMessages()
|
||||
text0.format(0, 3, { bold: true })
|
||||
text1.format(2, 2, { bold: true })
|
||||
let delta = null
|
||||
text1.observe(event => {
|
||||
delta = event.delta
|
||||
})
|
||||
testConnector.flushAllMessages()
|
||||
t.compare(delta, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -78,6 +95,49 @@ export const testBasicFormat = tc => {
|
||||
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
|
||||
*/
|
||||
@@ -91,6 +151,29 @@ export const testGetDeltaWithEmbeds = tc => {
|
||||
}])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testTypesAsEmbed = tc => {
|
||||
const { text0, text1, testConnector } = init(tc, { users: 2 })
|
||||
text0.applyDelta([{
|
||||
insert: new Y.Map([['key', 'val']])
|
||||
}])
|
||||
t.compare(text0.toDelta()[0].insert.toJSON(), { key: 'val' })
|
||||
let firedEvent = false
|
||||
text1.observe(event => {
|
||||
const d = event.delta
|
||||
t.assert(d.length === 1)
|
||||
t.compare(d.map(x => /** @type {Y.AbstractType<any>} */ (x.insert).toJSON()), [{ key: 'val' }])
|
||||
firedEvent = true
|
||||
})
|
||||
testConnector.flushAllMessages()
|
||||
const delta = text1.toDelta()
|
||||
t.assert(delta.length === 1)
|
||||
t.compare(delta[0].insert.toJSON(), { key: 'val' })
|
||||
t.assert(firedEvent, 'fired the event observer containing a Type-Embed')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -205,10 +288,46 @@ export const testFormattingRemovedInMidText = tc => {
|
||||
t.assert(Y.getTypeChildren(text0).length === 3)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reported in https://github.com/yjs/yjs/issues/344
|
||||
*
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
|
||||
const { text0, text1, testConnector } = init(tc, { users: 2 })
|
||||
text0.insert(0, '\n', {
|
||||
PARAGRAPH_STYLES: 'normal',
|
||||
LIST_STYLES: 'bullet'
|
||||
})
|
||||
text0.insert(1, 'abc', {
|
||||
PARAGRAPH_STYLES: 'normal'
|
||||
})
|
||||
testConnector.flushAllMessages()
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
const deltas = []
|
||||
text0.observe(event => {
|
||||
deltas.push(event.delta)
|
||||
})
|
||||
text1.observe(event => {
|
||||
deltas.push(event.delta)
|
||||
})
|
||||
text1.format(0, 1, { LIST_STYLES: 'number' })
|
||||
testConnector.flushAllMessages()
|
||||
const filteredDeltas = deltas.filter(d => d.length > 0)
|
||||
t.assert(filteredDeltas.length === 2)
|
||||
t.compare(filteredDeltas[0], [
|
||||
{ retain: 1, attributes: { LIST_STYLES: 'number' } }
|
||||
])
|
||||
t.compare(filteredDeltas[0], filteredDeltas[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testInsertAndDeleteAtRandomPositions = tc => {
|
||||
// @todo optimize to run at least as fast as previous marker approach
|
||||
const N = 100000
|
||||
const { text0 } = init(tc, { users: 1 })
|
||||
const gen = tc.prng
|
||||
@@ -286,7 +405,9 @@ export const testBestCase = tc => {
|
||||
}
|
||||
|
||||
const tryGc = () => {
|
||||
// @ts-ignore
|
||||
if (typeof global !== 'undefined' && global.gc) {
|
||||
// @ts-ignore
|
||||
global.gc()
|
||||
}
|
||||
}
|
||||
@@ -323,6 +444,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.
|
||||
*
|
||||
@@ -357,6 +514,66 @@ export const testSplitSurrogateCharacter = tc => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search marker bug https://github.com/yjs/yjs/issues/307
|
||||
*
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testSearchMarkerBug1 = tc => {
|
||||
const { users, text0, text1, testConnector } = init(tc, { users: 2 })
|
||||
|
||||
users[0].on('update', update => {
|
||||
users[0].transact(() => {
|
||||
Y.applyUpdate(users[0], update)
|
||||
})
|
||||
})
|
||||
users[0].on('update', update => {
|
||||
users[1].transact(() => {
|
||||
Y.applyUpdate(users[1], update)
|
||||
})
|
||||
})
|
||||
|
||||
text0.insert(0, 'a_a')
|
||||
testConnector.flushAllMessages()
|
||||
text0.insert(2, 's')
|
||||
testConnector.flushAllMessages()
|
||||
text1.insert(3, 'd')
|
||||
testConnector.flushAllMessages()
|
||||
text0.delete(0, 5)
|
||||
testConnector.flushAllMessages()
|
||||
text0.insert(0, 'a_a')
|
||||
testConnector.flushAllMessages()
|
||||
text0.insert(2, 's')
|
||||
testConnector.flushAllMessages()
|
||||
text1.insert(3, 'd')
|
||||
testConnector.flushAllMessages()
|
||||
t.compareStrings(text0.toString(), text1.toString())
|
||||
t.compareStrings(text0.toString(), 'a_sda')
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testFormattingBug = async tc => {
|
||||
const ydoc1 = new Y.Doc()
|
||||
const ydoc2 = new Y.Doc()
|
||||
const text1 = ydoc1.getText()
|
||||
text1.insert(0, '\n\n\n')
|
||||
text1.format(0, 3, { url: 'http://example.com' })
|
||||
ydoc1.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
|
||||
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1))
|
||||
const text2 = ydoc2.getText()
|
||||
const expectedResult = [
|
||||
{ insert: '\n', attributes: { url: 'http://example.com' } },
|
||||
{ insert: '\n', attributes: { url: 'http://docs.yjs.dev' } },
|
||||
{ insert: '\n', attributes: { url: 'http://example.com' } }
|
||||
]
|
||||
t.compare(text1.toDelta(), expectedResult)
|
||||
t.compare(text1.toDelta(), text2.toDelta())
|
||||
console.log(text1.toDelta())
|
||||
}
|
||||
|
||||
// RANDOM TESTS
|
||||
|
||||
let charCounter = 0
|
||||
@@ -492,7 +709,11 @@ const qChanges = [
|
||||
(y, gen) => { // insert embed
|
||||
const ytext = y.getText('text')
|
||||
const insertPos = prng.int32(gen, 0, ytext.length)
|
||||
if (prng.bool(gen)) {
|
||||
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
|
||||
} else {
|
||||
ytext.insertEmbed(insertPos, new Y.Map([[prng.word(gen), prng.word(gen)]]))
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {Y.Doc} y
|
||||
@@ -539,8 +760,12 @@ const qChanges = [
|
||||
*/
|
||||
const checkResult = result => {
|
||||
for (let i = 1; i < result.testObjects.length; i++) {
|
||||
const p1 = result.users[i].getText('text').toDelta()
|
||||
const p2 = result.users[i].getText('text').toDelta()
|
||||
/**
|
||||
* @param {any} d
|
||||
*/
|
||||
const typeToObject = d => d.insert instanceof Y.AbstractType ? d.insert.toJSON() : d
|
||||
const p1 = result.users[i].getText('text').toDelta().map(typeToObject)
|
||||
const p2 = result.users[i].getText('text').toDelta().map(typeToObject)
|
||||
t.compare(p1, p2)
|
||||
}
|
||||
// Uncomment this to find formatting-cleanup issues
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { init, compare } from './testHelper.js'
|
||||
import * as Y from '../src/index.js'
|
||||
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as t from 'lib0/testing'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
@@ -15,6 +15,23 @@ export const testSetProperty = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testHasProperty = tc => {
|
||||
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
|
||||
xml0.setAttribute('height', '10')
|
||||
t.assert(xml0.hasAttribute('height'), 'Simple set+has works')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(xml1.hasAttribute('height'), 'Simple set+has works (remote)')
|
||||
|
||||
xml0.removeAttribute('height')
|
||||
t.assert(!xml0.hasAttribute('height'), 'Simple set+remove+has works')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(!xml1.hasAttribute('height'), 'Simple set+remove+has works (remote)')
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -133,3 +150,36 @@ export const testInsertafter = tc => {
|
||||
el.insertAfter(deepsecond1, [new Y.XmlText()])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testClone = tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = ydoc.getXmlFragment()
|
||||
const first = new Y.XmlText('text')
|
||||
const second = new Y.XmlElement('p')
|
||||
const third = new Y.XmlElement('p')
|
||||
yxml.push([first, second, third])
|
||||
t.compareArrays(yxml.toArray(), [first, second, third])
|
||||
|
||||
const cloneYxml = yxml.clone()
|
||||
ydoc.getArray('copyarr').insert(0, [cloneYxml])
|
||||
t.assert(cloneYxml.length === 3)
|
||||
t.compare(cloneYxml.toJSON(), yxml.toJSON())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testFormattingBug = tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||
const delta = [
|
||||
{ insert: 'A', attributes: { em: {}, strong: {} } },
|
||||
{ insert: 'B', attributes: { em: {} } },
|
||||
{ insert: 'C', attributes: { em: {}, strong: {} } }
|
||||
]
|
||||
yxml.applyDelta(delta)
|
||||
t.compare(yxml.toDelta(), delta)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user