Compare commits
124 Commits
v13.0.0-81
...
v13.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9f9e08450 | ||
|
|
e3c59b0aa7 | ||
|
|
705dce7838 | ||
|
|
0fb55981ba | ||
|
|
89378e29ae | ||
|
|
cce35270ec | ||
|
|
d78180bf97 | ||
|
|
0ab415de3e | ||
|
|
ff3969caeb | ||
|
|
c82cc9f8d6 | ||
|
|
ef5c71bd8b | ||
|
|
bd6be3d23b | ||
|
|
0e6deab9c9 | ||
|
|
6cd9e2be32 | ||
|
|
ac8dab1e88 | ||
|
|
38ed725c2c | ||
|
|
a210bad25e | ||
|
|
6929a4f0f8 | ||
|
|
52dacfa5f2 | ||
|
|
27efe86f9c | ||
|
|
882b9055c7 | ||
|
|
e089089413 | ||
|
|
197932752e | ||
|
|
f0b2bdaf34 | ||
|
|
b96362c0f1 | ||
|
|
67f241cd7a | ||
|
|
c8af0bebf7 | ||
|
|
4f35e799a6 | ||
|
|
eb2a52dd26 | ||
|
|
189b1068ae | ||
|
|
7a3b60a5d7 | ||
|
|
99f06fc093 | ||
|
|
22917bca19 | ||
|
|
7f0e25dcba | ||
|
|
d90c9b1cb2 | ||
|
|
c426055f17 | ||
|
|
18c9010b63 | ||
|
|
c3edac62ef | ||
|
|
755de18fd5 | ||
|
|
641dc25076 | ||
|
|
1d58ea785f | ||
|
|
f53dff5043 | ||
|
|
74d1a31f49 | ||
|
|
d1063ab70b | ||
|
|
f4c919d9ec | ||
|
|
aeb23dbaa9 | ||
|
|
6d4f0c0cdd | ||
|
|
303138f309 | ||
|
|
ad373a3dce | ||
|
|
2150fa58f2 | ||
|
|
ece4841b5c | ||
|
|
8103220c05 | ||
|
|
66d500f08d | ||
|
|
5f8e7c7ba7 | ||
|
|
7b8eee6b25 | ||
|
|
1d5947c602 | ||
|
|
53e4028952 | ||
|
|
b38a8d99e5 | ||
|
|
6c4971ae25 | ||
|
|
d1f5ff0f59 | ||
|
|
1d297601e8 | ||
|
|
d9fface0be | ||
|
|
7d5db917da | ||
|
|
6e7529723d | ||
|
|
6cb64b3707 | ||
|
|
bb1c0b809f | ||
|
|
8bcff6138c | ||
|
|
e78d84ee59 | ||
|
|
c23bcb66ce | ||
|
|
5fddcef3ea | ||
|
|
e1e46c6eb1 | ||
|
|
13ad0c8464 | ||
|
|
7700b50470 | ||
|
|
fc4d6165b4 | ||
|
|
251c8aaefc | ||
|
|
1337d38ada | ||
|
|
f5c66e41cb | ||
|
|
0e7da017fe | ||
|
|
f0262ffaae | ||
|
|
36203af88e | ||
|
|
dd2b8bc6c7 | ||
|
|
463065ac21 | ||
|
|
d064e6e96e | ||
|
|
b1ed2df208 | ||
|
|
1fe4ef135c | ||
|
|
e376b5d472 | ||
|
|
952a9b2c41 | ||
|
|
03458dc641 | ||
|
|
14df5b72af | ||
|
|
338968031b | ||
|
|
1aac245b93 | ||
|
|
1faff323c1 | ||
|
|
e7280c7ae2 | ||
|
|
4c38619b5d | ||
|
|
b4e5c5cc1f | ||
|
|
b0dbd84f7f | ||
|
|
4a990963d9 | ||
|
|
7e7c9d5b11 | ||
|
|
775f6eed1d | ||
|
|
1e83b9418c | ||
|
|
ac3f672c80 | ||
|
|
2192aa5821 | ||
|
|
70bb523005 | ||
|
|
10ce6de57a | ||
|
|
3fba4f25a5 | ||
|
|
66c35d8499 | ||
|
|
4c14157dcf | ||
|
|
ef6c382e20 | ||
|
|
ee45b4fdd6 | ||
|
|
668e9e8a9b | ||
|
|
37a6d68543 | ||
|
|
f893198769 | ||
|
|
d3ee1a0ec2 | ||
|
|
d6593412a2 | ||
|
|
d31bf36531 | ||
|
|
a485f550db | ||
|
|
0610b16227 | ||
|
|
72e470c5f0 | ||
|
|
4d12a02e2f | ||
|
|
4a7d6f0a2d | ||
|
|
c80f446b5f | ||
|
|
81a529d8dc | ||
|
|
4f0ab78914 | ||
|
|
8c36f67f0b |
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: dmonad
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -17,7 +17,7 @@
|
||||
"useCollapsibles": true,
|
||||
"collapse": true,
|
||||
"resources": {
|
||||
"y-js.org": "yjs.website"
|
||||
"yjs.dev": "Yjs website"
|
||||
},
|
||||
"logo": {
|
||||
"url": "https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png",
|
||||
|
||||
4
.markdownlint.json
Normal file
4
.markdownlint.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"default": true,
|
||||
"no-inline-html": false
|
||||
}
|
||||
404
README.v13.md
404
README.v13.md
@@ -1,404 +0,0 @@
|
||||
# 
|
||||
> The shared editing library
|
||||
|
||||
Yjs is a library for automatic conflict resolution on shared state. It implements an [operation-based CRDT](#Yjs-CRDT-Algorithm) and exposes its internal model as shared types. Shared types are common data types like `Map` or `Array` with superpowers: changes are automatically distributed to other peers and merged without merge conflicts.
|
||||
|
||||
Yjs is **network agnostic** (p2p!), supports many existing **rich text editors**, **offline editing**, **version snapshots**, **shared cursors**, and encodes update messages using **binary protocol encoding**.
|
||||
|
||||
* Chat: [https://gitter.im/y-js/yjs](https://gitter.im/y-js/yjs)
|
||||
* Demos: [https://github.com/y-js/yjs-demos](https://github.com/y-js/yjs-demos)
|
||||
* API Docs: [https://yjs.website/](https://yjs.website/)
|
||||
|
||||
# Table of Contents
|
||||
|
||||
* [Overview](#Overview)
|
||||
* [Bindings](#Bindings)
|
||||
* [Providers](#Providers)
|
||||
* [Getting Started](#Getting-Started)
|
||||
* [API](#API)
|
||||
* [Transaction](#Transaction)
|
||||
* [Offline Editing](#Offline-Editing)
|
||||
* [Awareness](#Awareness)
|
||||
* [Working with Yjs](#Working-with-Yjs)
|
||||
* [Typescript Declarations](#Typescript-Declarations)
|
||||
* [Binary Protocols](#Binary-Protocols)
|
||||
* [Sync Protocol](#Sync-Protocols)
|
||||
* [Awareness Protocol](#Awareness-Protocols)
|
||||
* [Auth Protocol](#Auth-Protocol)
|
||||
* [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm)
|
||||
* [Evaluation](#Evaluation)
|
||||
* [Existing shared editing libraries](#Exisisting-Javascript-Products)
|
||||
* [CRDT Algorithms](#CRDT-Algorithms)
|
||||
* [Comparison of CRDT with OT](#Comparing-CRDT-with-OT)
|
||||
* [Comparison of CRDT Algorithms](#Comparing-CRDT-Algorithms)
|
||||
* [Comparison of Yjs with other Implementations](#Comparing-Yjs-with-other-Implementations)
|
||||
* [License and Author](#License-and-Author)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains a collection of shared types that can be observed for changes and manipulated concurrently. Network functionality and two-way-bindings are implemented in separate modules.
|
||||
|
||||
### Bindings
|
||||
|
||||
| Name | Cursors | Binding | Demo |
|
||||
|---|:-:|---|---|
|
||||
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/y-js/y-prosemirror) | [link](https://yjs.website/tutorial-prosemirror.html) |
|
||||
| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/y-js/y-quill) | [link](https://yjs.website/tutorial-quill.html) |
|
||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/y-js/y-codemirror) | [link](https://yjs.website/tutorial-codemirror.html) |
|
||||
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/y-js/y-ace) | [link]() |
|
||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | | [y-monaco](http://github.com/y-js/y-monaco) | [link]() |
|
||||
|
||||
|
||||
### Providers
|
||||
|
||||
Setting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. **Providers** manage all that for you and are a good off-the-shelf solution.
|
||||
|
||||
<dl>
|
||||
<dt><a href="http://github.com/y-js/y-websocket">y-websocket</a></dt>
|
||||
<dd>A module that contains a simple websocket backend and a websocket client that connects to that backend. The backend can be extended to persist updates in a leveldb database.</dd>
|
||||
<dt><a href="http://github.com/y-js/y-mesh">y-mesh</a></dt>
|
||||
<dd>[WIP] Creates a connected graph of webrtc connections with a high <a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It requires a signalling server that connects a client to the first peer. But after that the network manages itself. It is well suited for large and small networks.</dd>
|
||||
<dt><a href="http://github.com/y-js/y-dat">y-dat</a></dt>
|
||||
<dd>[WIP] Writes updates effinciently in 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.</dd>
|
||||
</dl>
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install Yjs and a provider with your favorite package manager. In this section we are going to bind a YText to a DOM textarea.
|
||||
|
||||
```sh
|
||||
npm i yjs@13.0.0-80 y-websocket@1.0.0-1 y-textarea
|
||||
```
|
||||
|
||||
**Start the y-websocket server**
|
||||
|
||||
```sh
|
||||
PORT=1234 node ./node_modules/y-websocket/bin/server.js
|
||||
```
|
||||
|
||||
**Textarea Binding Example**
|
||||
|
||||
```js
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { TextareaBinding } from 'y-textarea'
|
||||
|
||||
const provider = new WebsocketProvider('http://localhost:1234')
|
||||
const sharedDocument = provider.get('my-favourites')
|
||||
|
||||
// Define a shared type on the document.
|
||||
const ytext = sharedDocument.getText('my resume')
|
||||
|
||||
// bind to a textarea
|
||||
const binding = new TextareaBinding(ytext, document.querySelector('textarea'))
|
||||
```
|
||||
|
||||
Now you understand how types are defined on a shared document. Next you can jump to the [demo repository](https://github.com/y-js/yjs-demos) or continue reading the API docs.
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
import * as Y from 'yjs'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><b>Y.Array</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A shareable Array-like type that supports efficient insert/delete of elements at any position. Internally it uses a linked list of Arrays that is split when necessary.
|
||||
</p>
|
||||
<pre>const yarray = new Y.Array()</pre>
|
||||
<dl>
|
||||
<b><code>insert(index:number, content:Array<object|string|number|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|Array|string|number|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>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b><code>map(function(T, number, YArray):M):Array<M></code></b>
|
||||
<dd></dd>
|
||||
<b><code>toArray():Array<Object|Array|string|number|Y.Type></code></b>
|
||||
<dd>Copies the content of this YArray to a new Array.</dd>
|
||||
<b><code>toJSON():Array<Object|Array|string|number></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.</dd>
|
||||
<b><code>[Symbol.Iterator]</code></b>
|
||||
<dd>
|
||||
Returns an YArray Iterator that contains the values for each index in the array.
|
||||
<pre>for (let value of yarray) { .. }</pre>
|
||||
</dd>
|
||||
<b><code>observe(function(YArrayEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YArrayEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
<details>
|
||||
<summary><b>Y.Map</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A shareable Map type.
|
||||
</p>
|
||||
<pre><code>const ymap = new Y.Map()</code></pre>
|
||||
<dl>
|
||||
<b><code>get(key:string):object|string|number|Y.Type</code></b>
|
||||
<dd></dd>
|
||||
<b><code>set(key:string, value:object|string|number|Y.Type)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>delete(key:string)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>has(key:string):boolean</code></b>
|
||||
<dd></dd>
|
||||
<b><code>get(index:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>toJSON():Object<string, Object|Array|string|number></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>[Symbol.Iterator]</code></b>
|
||||
<dd>
|
||||
Returns an Iterator of <code>[key, value]</code> pairs.
|
||||
<pre>for (let [key, value] of ymap) { .. }</pre>
|
||||
</dd>
|
||||
<b><code>entries()</code></b>
|
||||
<dd>
|
||||
Returns an Iterator of <code>[key, value]</code> pairs.
|
||||
</dd>
|
||||
<b><code>values()</code></b>
|
||||
<dd>
|
||||
Returns an Iterator of all values.
|
||||
</dd>
|
||||
<b><code>keys()</code></b>
|
||||
<dd>
|
||||
Returns an Iterator of all keys.
|
||||
</dd>
|
||||
<b><code>observe(function(YMapEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YMapEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Y.Text</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A shareable type that is optimized for shared editing on text. It allows to assign properties to ranges in the text. This makes it possible to implement rich-text bindings to this type.
|
||||
</p>
|
||||
<p>
|
||||
This type can also be transformed to the <a href="https://quilljs.com/docs/delta">delta format</a>. Similarly the YTextEvents compute changes as deltas.
|
||||
</p>
|
||||
<pre>const ytext = new Y.Text()</pre>
|
||||
<dl>
|
||||
<b><code>insert(index:number, content:string, [formattingAttributes:Object<string,string>])</code></b>
|
||||
<dd>
|
||||
Insert a string at <var>index</var> and assign formatting attributes to it.
|
||||
<pre>ytext.insert(0, 'bold text', { bold: true })</pre>
|
||||
</dd>
|
||||
<b><code>delete(index:number, length:number)</code></b>
|
||||
<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)</code></b>
|
||||
<dd>See <a href="https://quilljs.com/docs/delta/">Quill Delta</a></dd>
|
||||
<b><code>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b><code>toString():string</code></b>
|
||||
<dd>Transforms this type, without formatting options, into a string.</dd>
|
||||
<b><code>toJSON():string</code></b>
|
||||
<dd>See <code>toString</code></dd>
|
||||
<b><code>toDelta():Delta</code></b>
|
||||
<dd>Transforms this type to a <a href="https://quilljs.com/docs/delta/">Quill Delta</a></dd>
|
||||
<b><code>observe(function(YTextEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YTextEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>YXmlFragment</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A container that holds an Array of Y.XmlElements.
|
||||
</p>
|
||||
<pre><code>const yxml = new Y.XmlFragment()</code></pre>
|
||||
<dl>
|
||||
<b><code>insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)</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>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
||||
<dd>Copies the children to a new Array.</dd>
|
||||
<b><code>toDOM():DocumentFragment</code></b>
|
||||
<dd>Transforms this type and all children to new DOM elements.</dd>
|
||||
<b><code>toString():string</code></b>
|
||||
<dd>Get the XML serialization of all descendants.</dd>
|
||||
<b><code>toJSON():string</code></b>
|
||||
<dd>See <code>toString</code>.</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 this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YXmlEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Y.XmlElement</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A shareable type that represents an XML Element. It has a <code>nodeName</code>, attributes, and a list of children. But it makes no effort to validate its content and be actually XML compliant.
|
||||
</p>
|
||||
<pre><code>const yxml = new Y.XmlElement()</code></pre>
|
||||
<dl>
|
||||
<b><code>insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)</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>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b><code>setAttribute(attributeName:string, attributeValue:string)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>removeAttribute(attributeName:string)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>getAttribute(attributeName:string):string</code></b>
|
||||
<dd></dd>
|
||||
<b><code>getAttributes(attributeName:string):Object<string,string></code></b>
|
||||
<dd></dd>
|
||||
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
||||
<dd>Copies the children to a new Array.</dd>
|
||||
<b><code>toDOM():Element</code></b>
|
||||
<dd>Transforms this type and all children to a new DOM element.</dd>
|
||||
<b><code>toString():string</code></b>
|
||||
<dd>Get the XML serialization of all descendants.</dd>
|
||||
<b><code>toJSON():string</code></b>
|
||||
<dd>See <code>toString</code>.</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 this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YXmlEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
### Custom Types
|
||||
|
||||
## Bindings
|
||||
|
||||
## Transaction
|
||||
|
||||
## Binary Encoding Protocols
|
||||
|
||||
### Sync Protocol
|
||||
|
||||
Sync steps
|
||||
|
||||
### Awareness Protocol
|
||||
|
||||
### Auth Protocol
|
||||
|
||||
## Offline Editing
|
||||
|
||||
It is trivial with Yjs to persist the local state to indexeddb, so it is always available when working offline. But there are two non-trivial questions that need to answered when implementing a professional offline editing app:
|
||||
|
||||
1. How does a client sync down all rooms that were modified while offline?
|
||||
2. How does a client sync up all rooms that were modified while offline?
|
||||
|
||||
Assuming 5000 documents are stored on each client for offline usage. How do we sync up/down each of those documents after a client comes online? It would be inefficient to sync each of those rooms separately. The only provider that currently supports syncing many rooms efficiently is Ydb, because its database layer is optimized to sync many rooms with each client.
|
||||
|
||||
If you do not care about 1. and 2. you can use `/persistences/indexeddb.js` to mirror the local state to indexeddb.
|
||||
|
||||
## Working with Yjs
|
||||
|
||||
### Typescript Declarations
|
||||
|
||||
Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the only way to get type declarations is by adding Yjs to the list of checked files:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
},
|
||||
"maxNodeModuleJsDepth": 5
|
||||
}
|
||||
```
|
||||
|
||||
## Yjs CRDT Algorithm
|
||||
|
||||
## License and Author
|
||||
|
||||
Yjs and all related projects are [**MIT licensed**](./LICENSE).
|
||||
|
||||
Yjs is based on the research I did as a student at the [RWTH i5](http://dbis.rwth-aachen.de/). Now I am working on Yjs in my spare time.
|
||||
|
||||
Support me on [Patreon](https://www.patreon.com/dmonad) to fund this project or hire [me](https://github.com/dmonad) for professional support.
|
||||
1
examples/.gitignore
vendored
1
examples/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
build
|
||||
@@ -1,70 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yjs CodeMirror Example</title>
|
||||
<link rel=stylesheet href="https://codemirror.net/lib/codemirror.css">
|
||||
<style>
|
||||
#container {
|
||||
border: grey;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>This example shows how to bind a YText type to <a href="https://codemirror.net/">CodeMirror</a> editor.</p>
|
||||
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||
<div class="code-html">
|
||||
<style>
|
||||
.remote-caret {
|
||||
position: absolute;
|
||||
border-left: black;
|
||||
border-left-style: solid;
|
||||
border-left-width: 2px;
|
||||
height: 1em;
|
||||
}
|
||||
.remote-caret > div {
|
||||
position: relative;
|
||||
top: -1.05em;
|
||||
font-size: 13px;
|
||||
background-color: rgb(250, 129, 0);
|
||||
font-family: serif;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: normal;
|
||||
user-select: none;
|
||||
color: white;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
</style>
|
||||
<div id="container"></div>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./codemirror.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/codemirror.js" type="module">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { CodeMirrorBinding } from 'yjs/bindings/codemirror.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror/mode/javascript/javascript.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('codemirror')
|
||||
const ytext = ydocument.define('codemirror', Y.Text)
|
||||
|
||||
const editor = new CodeMirror(document.querySelector('#container'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
|
||||
const binding = new CodeMirrorBinding(ytext, editor)
|
||||
|
||||
window.codemirrorExample = {
|
||||
binding, editor, ytext, ydocument
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,22 +0,0 @@
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { CodeMirrorBinding } from 'y-codemirror'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror/mode/javascript/javascript.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('codemirror')
|
||||
const ytext = ydocument.getText('codemirror')
|
||||
|
||||
const editor = new CodeMirror(document.querySelector('#container'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
|
||||
const binding = new CodeMirrorBinding(ytext, editor)
|
||||
|
||||
window.codemirrorExample = {
|
||||
binding, editor, ytext, ydocument
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yjs Prosemirror Example</title>
|
||||
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
||||
<style>
|
||||
#content {
|
||||
min-height: 500px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>This example shows how to bind a YXmlFragment type to an arbitrary DOM element. We set the DOM element to contenteditable so it basically behaves like a very powerful rich-text editor.</p>
|
||||
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||
<hr>
|
||||
<div class="code-html">
|
||||
|
||||
<div id="content" contenteditable=""></div>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./dom.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/dom.js" type="module">
|
||||
import * as Y from 'yjs/index.js'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { DomBinding } from 'yjs/bindings/dom.js'
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('dom')
|
||||
const type = ydocument.define('xml', Y.XmlFragment)
|
||||
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
||||
|
||||
window.example = {
|
||||
provider, ydocument, type, binding
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as Y from '../src/index.js'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { DomBinding } from 'y-dom'
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('dom')
|
||||
const type = ydocument.define('xml', Y.XmlFragment)
|
||||
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
||||
|
||||
window.example = {
|
||||
provider, ydocument, type, binding
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
const isDeployed = location.hostname === 'yjs.website'
|
||||
|
||||
if (!isDeployed) {
|
||||
console.log('%cYjs: Start your local websocket server by running %c`npm run websocket-server`', 'color:blue', 'color: grey; font-weight: bold')
|
||||
}
|
||||
|
||||
export const serverAddress = isDeployed ? 'wss://api.yjs.website' : 'ws://localhost:1234'
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"codemirror": {
|
||||
"title": "CodeMirror Binding"
|
||||
},
|
||||
"prosemirror": {
|
||||
"title": "ProseMirror Binding"
|
||||
},
|
||||
"textarea": {
|
||||
"title": "Textarea Binding"
|
||||
},
|
||||
"quill": {
|
||||
"title": "Quill Binding"
|
||||
},
|
||||
"dom": {
|
||||
"title": "Dom Binding"
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
|
||||
import { Plugin } from 'prosemirror-state'
|
||||
import crel from 'crel'
|
||||
import * as Y from '../src/index.js'
|
||||
import { prosemirrorPluginKey } from 'y-prosemirror'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as historyProtocol from 'y-protocols/history.js'
|
||||
|
||||
const niceColors = ['#3cb44b', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#008080', '#9a6324', '#800000', '#808000', '#000075', '#808080']
|
||||
|
||||
const createUserCSS = (userid, username, color = 'rgb(250, 129, 0)', color2 = 'rgba(250, 129, 0, .41)') => `
|
||||
[ychange_state][ychange_user="${userid}"]:hover::before {
|
||||
content: "${username}" !important;
|
||||
background-color: ${color} !important;
|
||||
}
|
||||
[ychange_state="added"][ychange_user="${userid}"] {
|
||||
background-color: ${color2} !important;
|
||||
}
|
||||
[ychange_state="removed"][ychange_user="${userid}"] {
|
||||
color: ${color} !important;
|
||||
}
|
||||
`
|
||||
|
||||
export const noteHistoryPlugin = new Plugin({
|
||||
state: {
|
||||
init (initargs, state) {
|
||||
return new NoteHistoryPlugin()
|
||||
},
|
||||
apply (tr, pluginState) {
|
||||
return pluginState
|
||||
}
|
||||
},
|
||||
view (editorView) {
|
||||
const hstate = noteHistoryPlugin.getState(editorView.state)
|
||||
hstate.init(editorView)
|
||||
return {
|
||||
destroy: hstate.destroy.bind(hstate)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const createWrapper = () => {
|
||||
const wrapper = crel('div', { style: 'display: flex;' })
|
||||
const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
|
||||
wrapper.insertBefore(historyContainer, null)
|
||||
const userStyleContainer = crel('style')
|
||||
wrapper.insertBefore(userStyleContainer, null)
|
||||
return { wrapper, historyContainer, userStyleContainer }
|
||||
}
|
||||
|
||||
class NoteHistoryPlugin {
|
||||
init (editorView) {
|
||||
this.editorView = editorView
|
||||
const { historyContainer, wrapper, userStyleContainer } = createWrapper()
|
||||
this.userStyleContainer = userStyleContainer
|
||||
this.wrapper = wrapper
|
||||
this.historyContainer = historyContainer
|
||||
const n = editorView.dom.parentNode.parentNode
|
||||
n.parentNode.replaceChild(this.wrapper, n)
|
||||
n.style['flex-grow'] = '1'
|
||||
wrapper.insertBefore(n, this.wrapper.firstChild)
|
||||
this.render()
|
||||
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
||||
const history = y.define('history', Y.Array)
|
||||
history.observe(this.render.bind(this))
|
||||
}
|
||||
destroy () {
|
||||
this.wrapper.parentNode.replaceChild(this.wrapper.firstChild, this.wrapper)
|
||||
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
||||
const history = y.define('history', Y.Array)
|
||||
history.unobserve(this.render)
|
||||
}
|
||||
render () {
|
||||
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
||||
const history = y.define('history', Y.Array).toArray()
|
||||
const fragment = document.createDocumentFragment()
|
||||
const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
|
||||
fragment.insertBefore(snapshotBtn, null)
|
||||
let _prevSnap = null // empty
|
||||
snapshotBtn.addEventListener('click', () => {
|
||||
const awareness = y.getAwarenessInfo()
|
||||
const userMap = new Map()
|
||||
const aw = y.getLocalAwarenessInfo()
|
||||
userMap.set(y.userID, aw.name || 'unknown')
|
||||
awareness.forEach((a, userID) => {
|
||||
userMap.set(userID, a.name || 'Unknown')
|
||||
})
|
||||
this.snapshot(userMap)
|
||||
})
|
||||
history.forEach(buf => {
|
||||
const decoder = decoding.createDecoder(buf)
|
||||
const snapshot = historyProtocol.readHistorySnapshot(decoder)
|
||||
const date = new Date(decoding.readUint32(decoder) * 1000)
|
||||
const restoreBtn = crel('button', { type: 'button' }, ['restore'])
|
||||
const a = crel('a', [
|
||||
'• ' + date.toUTCString(), restoreBtn
|
||||
])
|
||||
const el = crel('div', [ a ])
|
||||
let prevSnapshot = _prevSnap // rebind to new variable
|
||||
restoreBtn.addEventListener('click', event => {
|
||||
if (prevSnapshot === null) {
|
||||
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
|
||||
}
|
||||
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot, restore: true }))
|
||||
event.stopPropagation()
|
||||
})
|
||||
a.addEventListener('click', () => {
|
||||
console.log('setting snapshot')
|
||||
if (prevSnapshot === null) {
|
||||
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
|
||||
}
|
||||
this.renderSnapshot(snapshot, prevSnapshot)
|
||||
})
|
||||
fragment.insertBefore(el, null)
|
||||
_prevSnap = snapshot
|
||||
})
|
||||
this.historyContainer.innerHTML = ''
|
||||
this.historyContainer.insertBefore(fragment, null)
|
||||
}
|
||||
renderSnapshot (snapshot, prevSnapshot) {
|
||||
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot }))
|
||||
/**
|
||||
* @type {Array<string|null>}
|
||||
*/
|
||||
let colors = niceColors.slice()
|
||||
let style = ''
|
||||
snapshot.userMap.forEach((name, userid) => {
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const randInt = name.split('').map(s => s.charCodeAt(0)).reduce((a, b) => a + b)
|
||||
let color = null
|
||||
let i = 0
|
||||
for (; i < colors.length && color === null; i++) {
|
||||
color = colors[(randInt + i) % colors.length]
|
||||
}
|
||||
if (color === null) {
|
||||
colors = niceColors.slice()
|
||||
i = 0
|
||||
color = colors[randInt % colors.length]
|
||||
}
|
||||
colors[randInt % colors.length] = null
|
||||
style += createUserCSS(userid, name, color, color + '69')
|
||||
})
|
||||
this.userStyleContainer.innerHTML = style
|
||||
}
|
||||
/**
|
||||
* @param {Map<number, string>} [updatedUserMap] Maps from userid (yjs model) to account name (e.g. mail address)
|
||||
*/
|
||||
snapshot (updatedUserMap = new Map()) {
|
||||
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
||||
const history = y.define('history', Y.Array)
|
||||
const encoder = encoding.createEncoder()
|
||||
historyProtocol.writeHistorySnapshot(encoder, y, updatedUserMap)
|
||||
encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
|
||||
history.push([encoding.toBuffer(encoder)])
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
import { Schema } from 'prosemirror-model'
|
||||
|
||||
const brDOM = ['br']
|
||||
|
||||
const calcYchangeDomAttrs = (attrs, domAttrs = {}) => {
|
||||
domAttrs = Object.assign({}, domAttrs)
|
||||
if (attrs.ychange !== null) {
|
||||
domAttrs.ychange_user = attrs.ychange.user
|
||||
domAttrs.ychange_state = attrs.ychange.state
|
||||
}
|
||||
return domAttrs
|
||||
}
|
||||
|
||||
// :: Object
|
||||
// [Specs](#model.NodeSpec) for the nodes defined in this schema.
|
||||
export const nodes = {
|
||||
// :: NodeSpec The top level document node.
|
||||
doc: {
|
||||
content: 'block+'
|
||||
},
|
||||
|
||||
// :: NodeSpec A plain paragraph textblock. Represented in the DOM
|
||||
// as a `<p>` element.
|
||||
paragraph: {
|
||||
attrs: { ychange: { default: null } },
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
parseDOM: [{ tag: 'p' }],
|
||||
toDOM (node) { return ['p', calcYchangeDomAttrs(node.attrs), 0] }
|
||||
},
|
||||
|
||||
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
|
||||
blockquote: {
|
||||
attrs: { ychange: { default: null } },
|
||||
content: 'block+',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'blockquote' }],
|
||||
toDOM (node) { return ['blockquote', calcYchangeDomAttrs(node.attrs), 0] }
|
||||
},
|
||||
|
||||
// :: NodeSpec A horizontal rule (`<hr>`).
|
||||
horizontal_rule: {
|
||||
attrs: { ychange: { default: null } },
|
||||
group: 'block',
|
||||
parseDOM: [{ tag: 'hr' }],
|
||||
toDOM (node) {
|
||||
return ['hr', calcYchangeDomAttrs(node.attrs)]
|
||||
}
|
||||
},
|
||||
|
||||
// :: NodeSpec A heading textblock, with a `level` attribute that
|
||||
// should hold the number 1 to 6. Parsed and serialized as `<h1>` to
|
||||
// `<h6>` elements.
|
||||
heading: {
|
||||
attrs: {
|
||||
level: { default: 1 },
|
||||
ychange: { default: null }
|
||||
},
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'h1', attrs: { level: 1 } },
|
||||
{ tag: 'h2', attrs: { level: 2 } },
|
||||
{ tag: 'h3', attrs: { level: 3 } },
|
||||
{ tag: 'h4', attrs: { level: 4 } },
|
||||
{ tag: 'h5', attrs: { level: 5 } },
|
||||
{ tag: 'h6', attrs: { level: 6 } }],
|
||||
toDOM (node) { return ['h' + node.attrs.level, calcYchangeDomAttrs(node.attrs), 0] }
|
||||
},
|
||||
|
||||
// :: NodeSpec A code listing. Disallows marks or non-text inline
|
||||
// nodes by default. Represented as a `<pre>` element with a
|
||||
// `<code>` element inside of it.
|
||||
code_block: {
|
||||
attrs: { ychange: { default: null } },
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
code: true,
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
|
||||
toDOM (node) { return ['pre', calcYchangeDomAttrs(node.attrs), ['code', 0]] }
|
||||
},
|
||||
|
||||
// :: NodeSpec The text node.
|
||||
text: {
|
||||
group: 'inline'
|
||||
},
|
||||
|
||||
// :: NodeSpec An inline image (`<img>`) node. Supports `src`,
|
||||
// `alt`, and `href` attributes. The latter two default to the empty
|
||||
// string.
|
||||
image: {
|
||||
inline: true,
|
||||
attrs: {
|
||||
ychange: { default: null },
|
||||
src: {},
|
||||
alt: { default: null },
|
||||
title: { default: null }
|
||||
},
|
||||
group: 'inline',
|
||||
draggable: true,
|
||||
parseDOM: [{ tag: 'img[src]',
|
||||
getAttrs (dom) {
|
||||
return {
|
||||
src: dom.getAttribute('src'),
|
||||
title: dom.getAttribute('title'),
|
||||
alt: dom.getAttribute('alt')
|
||||
}
|
||||
} }],
|
||||
toDOM (node) {
|
||||
const domAttrs = {
|
||||
src: node.attrs.src,
|
||||
title: node.attrs.title,
|
||||
alt: node.attrs.alt
|
||||
}
|
||||
return ['img', calcYchangeDomAttrs(node.attrs, domAttrs)]
|
||||
}
|
||||
},
|
||||
|
||||
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
|
||||
hard_break: {
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
selectable: false,
|
||||
parseDOM: [{ tag: 'br' }],
|
||||
toDOM () { return brDOM }
|
||||
}
|
||||
}
|
||||
|
||||
const emDOM = ['em', 0]; const strongDOM = ['strong', 0]; const codeDOM = ['code', 0]
|
||||
|
||||
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
|
||||
export const marks = {
|
||||
// :: MarkSpec A link. Has `href` and `title` attributes. `title`
|
||||
// defaults to the empty string. Rendered and parsed as an `<a>`
|
||||
// element.
|
||||
link: {
|
||||
attrs: {
|
||||
href: {},
|
||||
title: { default: null }
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [{ tag: 'a[href]',
|
||||
getAttrs (dom) {
|
||||
return { href: dom.getAttribute('href'), title: dom.getAttribute('title') }
|
||||
} }],
|
||||
toDOM (node) { return ['a', node.attrs, 0] }
|
||||
},
|
||||
|
||||
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
|
||||
// Has parse rules that also match `<i>` and `font-style: italic`.
|
||||
em: {
|
||||
parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }],
|
||||
toDOM () { return emDOM }
|
||||
},
|
||||
|
||||
// :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
|
||||
// also match `<b>` and `font-weight: bold`.
|
||||
strong: {
|
||||
parseDOM: [{ tag: 'strong' },
|
||||
// This works around a Google Docs misbehavior where
|
||||
// pasted content will be inexplicably wrapped in `<b>`
|
||||
// tags with a font-weight normal.
|
||||
{ tag: 'b', getAttrs: node => node.style.fontWeight !== 'normal' && null },
|
||||
{ style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null }],
|
||||
toDOM () { return strongDOM }
|
||||
},
|
||||
|
||||
// :: MarkSpec Code font mark. Represented as a `<code>` element.
|
||||
code: {
|
||||
parseDOM: [{ tag: 'code' }],
|
||||
toDOM () { return codeDOM }
|
||||
},
|
||||
ychange: {
|
||||
attrs: {
|
||||
user: { default: null },
|
||||
state: { default: null }
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [{ tag: 'ychange' }],
|
||||
toDOM (node) {
|
||||
return ['ychange', { ychange_user: node.attrs.user, ychange_state: node.attrs.state }, 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// :: Schema
|
||||
// This schema rougly corresponds to the document schema used by
|
||||
// [CommonMark](http://commonmark.org/), minus the list elements,
|
||||
// which are defined in the [`prosemirror-schema-list`](#schema-list)
|
||||
// module.
|
||||
//
|
||||
// To reuse elements from this schema, extend or read from its
|
||||
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
|
||||
export const schema = new Schema({ nodes, marks })
|
||||
@@ -1,330 +0,0 @@
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::selection { background: transparent; }
|
||||
.ProseMirror-hideselection *::-moz-selection { background: transparent; }
|
||||
.ProseMirror-hideselection { caret-color: transparent; }
|
||||
|
||||
.ProseMirror-selectednode {
|
||||
outline: 2px solid #8cf;
|
||||
}
|
||||
|
||||
/* Make sure li selections wrap around markers */
|
||||
|
||||
li.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
li.ProseMirror-selectednode:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
right: -2px; top: -2px; bottom: -2px;
|
||||
border: 2px solid #8cf;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ProseMirror-textblock-dropdown {
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu {
|
||||
margin: 0 -4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ProseMirror-tooltip .ProseMirror-menu {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
margin-right: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ProseMirror-menuseparator {
|
||||
border-right: 1px solid #ddd;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown {
|
||||
vertical-align: 1px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-wrap {
|
||||
padding: 1px 0 1px 4px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown:after {
|
||||
content: "";
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 2px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
color: #666;
|
||||
border: 1px solid #aaa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
z-index: 15;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item {
|
||||
cursor: pointer;
|
||||
padding: 2px 8px 2px 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item:hover {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap {
|
||||
position: relative;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label:after {
|
||||
content: "";
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu {
|
||||
display: none;
|
||||
min-width: 4em;
|
||||
left: 100%;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
position: relative;
|
||||
min-height: 1em;
|
||||
color: #666;
|
||||
padding: 1px 6px;
|
||||
top: 0; left: 0; right: 0;
|
||||
border-bottom: 1px solid silver;
|
||||
background: white;
|
||||
z-index: 10;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
display: inline-block;
|
||||
line-height: .8;
|
||||
vertical-align: -2px; /* Compensate for padding */
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-icon svg {
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.ProseMirror-icon span {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
.ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid black;
|
||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes ProseMirror-cursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||
display: block;
|
||||
}
|
||||
/* Add space around the hr to make clicking it easier */
|
||||
|
||||
.ProseMirror-example-setup-style hr {
|
||||
padding: 2px 10px;
|
||||
border: none;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style hr:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: silver;
|
||||
line-height: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror ul, .ProseMirror ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.ProseMirror blockquote {
|
||||
padding-left: 1em;
|
||||
border-left: 3px solid #eee;
|
||||
margin-left: 0; margin-right: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style img {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
background: white;
|
||||
padding: 5px 10px 5px 15px;
|
||||
border: 1px solid silver;
|
||||
position: fixed;
|
||||
border-radius: 3px;
|
||||
z-index: 11;
|
||||
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.ProseMirror-prompt h5 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 100%;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"],
|
||||
.ProseMirror-prompt textarea {
|
||||
background: #eee;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"] {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close {
|
||||
position: absolute;
|
||||
left: 2px; top: 1px;
|
||||
color: #666;
|
||||
border: none; background: transparent; padding: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close:after {
|
||||
content: "✕";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ProseMirror-invalid {
|
||||
background: #ffc;
|
||||
border: 1px solid #cc7;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-buttons {
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
#editor, .editor {
|
||||
background: white;
|
||||
color: black;
|
||||
background-clip: padding-box;
|
||||
border-radius: 4px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
padding: 5px 0;
|
||||
margin-bottom: 23px;
|
||||
}
|
||||
|
||||
.ProseMirror p:first-child,
|
||||
.ProseMirror h1:first-child,
|
||||
.ProseMirror h2:first-child,
|
||||
.ProseMirror h3:first-child,
|
||||
.ProseMirror h4:first-child,
|
||||
.ProseMirror h5:first-child,
|
||||
.ProseMirror h6:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding: 4px 8px 4px 14px;
|
||||
line-height: 1.2;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror p { margin-bottom: 1em }
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yjs Prosemirror Example</title>
|
||||
<link rel=stylesheet href="./prosemirror.css">
|
||||
<style>
|
||||
placeholder {
|
||||
display: inline;
|
||||
border: 1px solid #ccc;
|
||||
color: #ccc;
|
||||
}
|
||||
placeholder:after {
|
||||
content: "☁";
|
||||
font-size: 200%;
|
||||
line-height: 0.1;
|
||||
font-weight: bold;
|
||||
}
|
||||
.ProseMirror img { max-width: 100px }
|
||||
/* this is a rough fix for the first cursor position when the first paragraph is empty */
|
||||
.ProseMirror > .ProseMirror-yjs-cursor:first-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child {
|
||||
margin-top: 16px
|
||||
}
|
||||
.ProseMirror-yjs-cursor {
|
||||
position: absolute;
|
||||
border-left: black;
|
||||
border-left-style: solid;
|
||||
border-left-width: 2px;
|
||||
border-color: orange;
|
||||
height: 1em;
|
||||
word-break: normal;
|
||||
}
|
||||
.ProseMirror-yjs-cursor > div {
|
||||
position: relative;
|
||||
top: -1.05em;
|
||||
font-size: 13px;
|
||||
background-color: rgb(250, 129, 0);
|
||||
font-family: serif;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: normal;
|
||||
user-select: none;
|
||||
color: white;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
[ychange_state] {
|
||||
position: relative;
|
||||
}
|
||||
[ychange_state]:hover::before {
|
||||
content: attr(ychange_user);
|
||||
background-color: #fa8100;
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
right: 0;
|
||||
font-size: 12px;
|
||||
padding: 0 2px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
color: #fdfdfe;
|
||||
user-select: none;
|
||||
word-break: normal;
|
||||
}
|
||||
*[ychange_state='added'] {
|
||||
background-color: #fa810069;
|
||||
}
|
||||
ychange[ychange_state='removed'] {
|
||||
color: rgb(250, 129, 0);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
*:not(ychange)[ychange_state='removed'] {
|
||||
background-color: #ff9494c9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
img[ychange_state='removed'] {
|
||||
padding: 2px;
|
||||
}
|
||||
.y-connect-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button type="button" class="y-connect-btn">Disconnect</button>
|
||||
<p>This example shows how to bind a YXmlFragment to a <a href="http://prosemirror.net">Prosemirror</a> editor using <a href="https://github.com/y-js/y-prosemirror">y-prosemirror</a>.</p>
|
||||
<p>The content of this editor is shared with every client that visits this domain.</p>
|
||||
<div class="code-html">
|
||||
|
||||
<div id="editor" style="margin-bottom: 23px"></div>
|
||||
<div style="display: none" id="content"></div>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./prosemirror.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/prosemirror.js" type="module">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { prosemirrorPlugin, cursorPlugin } from 'yjs/bindings/prosemirror'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { DOMParser } from 'prosemirror-model'
|
||||
import { schema } from 'prosemirror-schema-basic'
|
||||
import { exampleSetup } from 'prosemirror-example-setup'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('prosemirror')
|
||||
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
||||
|
||||
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
|
||||
state: EditorState.create({
|
||||
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
|
||||
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin])
|
||||
})
|
||||
})
|
||||
|
||||
window.example = { provider, ydocument, type, prosemirrorView }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,36 +0,0 @@
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { prosemirrorPlugin, cursorPlugin } from 'y-prosemirror'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { DOMParser } from 'prosemirror-model'
|
||||
import { schema } from './prosemirror-schema.js'
|
||||
import { exampleSetup } from 'prosemirror-example-setup'
|
||||
// import { noteHistoryPlugin } from './prosemirror-history.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('prosemirror' /*, { gc: false } */)
|
||||
const type = ydocument.get('prosemirror', Y.XmlFragment)
|
||||
|
||||
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
|
||||
state: EditorState.create({
|
||||
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
|
||||
plugins: exampleSetup({ schema }).concat([prosemirrorPlugin(type), cursorPlugin])
|
||||
})
|
||||
})
|
||||
|
||||
const connectBtn = document.querySelector('.y-connect-btn')
|
||||
connectBtn.addEventListener('click', () => {
|
||||
if (ydocument.wsconnected) {
|
||||
ydocument.disconnect()
|
||||
connectBtn.textContent = 'Connect'
|
||||
} else {
|
||||
ydocument.connect()
|
||||
connectBtn.textContent = 'Disconnect'
|
||||
}
|
||||
})
|
||||
|
||||
window.example = { provider, ydocument, type, prosemirrorView }
|
||||
@@ -1,51 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yjs Quill Example</title>
|
||||
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css">
|
||||
</head>
|
||||
<body>
|
||||
<p>This example shows how to bind a YText type to <a href="https://quilljs.com">Quill</a> editor.</p>
|
||||
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||
<div class="code-html">
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./quill.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/quill.js" type="module">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { QuillBinding } from 'yjs/bindings/quill.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import Quill from 'quill'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('quill')
|
||||
const ytext = ydocument.define('quill', Y.Text)
|
||||
|
||||
const quill = new Quill('#quill-container', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
||||
]
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
})
|
||||
|
||||
window.quillBinding = new QuillBinding(ytext, quill)
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as Y from '../src/index.js'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { QuillBinding } from 'y-quill'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import Quill from 'quill'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('quill')
|
||||
const ytext = ydocument.define('quill', Y.Text)
|
||||
|
||||
const quill = new Quill('#quill-container', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
||||
]
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
})
|
||||
|
||||
window.quillBinding = new QuillBinding(ytext, quill)
|
||||
@@ -1,29 +0,0 @@
|
||||
footer img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav .title h1 a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #b93c1d;
|
||||
}
|
||||
|
||||
#resizer {
|
||||
background-color: #b93c1d;
|
||||
}
|
||||
|
||||
.main section article.readme h1:first-child img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main section article.readme h1:first-child {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.main section article.readme h1:first-child::before {
|
||||
content: "Yjs";
|
||||
font-size: 2em;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yjs Textarea Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>This example shows how to bind a YText type to a DOM Textarea.</p>
|
||||
<p>The content of this textarea is shared with every client who visits this domain.</p>
|
||||
<div class="code-html">
|
||||
|
||||
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./textarea.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/textarea.js" type="module">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { TextareaBinding } from 'yjs/bindings/textarea.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('textarea')
|
||||
const type = ydocument.define('textarea', Y.Text)
|
||||
const textarea = document.querySelector('textarea')
|
||||
const binding = new TextareaBinding(type, textarea)
|
||||
|
||||
window.textareaExample = {
|
||||
provider, ydocument, type, textarea, binding
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,14 +0,0 @@
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { TextareaBinding } from 'y-textarea'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('textarea')
|
||||
const type = ydocument.getText('textarea')
|
||||
const textarea = document.querySelector('textarea')
|
||||
const binding = new TextareaBinding(type, textarea)
|
||||
|
||||
window.textareaExample = {
|
||||
provider, ydocument, type, textarea, binding
|
||||
}
|
||||
5589
package-lock.json
generated
5589
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
73
package.json
73
package.json
@@ -1,24 +1,23 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-81",
|
||||
"version": "13.0.1",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.js",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production",
|
||||
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000",
|
||||
"dist": "rm -rf dist examples/build && rollup -c",
|
||||
"serve-examples": "concurrently 'npm run watch' 'serve examples'",
|
||||
"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",
|
||||
"dist": "rm -rf dist && rollup -c && tsc",
|
||||
"watch": "rollup -wc",
|
||||
"lint": "standard && tsc",
|
||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
||||
"serve-docs": "npm run docs && serve ./docs/",
|
||||
"preversion": "PRODUCTION=1 npm run dist && node ./dist/tests.js --repitition-time 1000",
|
||||
"postversion": "git push && git push --tags",
|
||||
"debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'",
|
||||
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js",
|
||||
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.js"
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
@@ -28,53 +27,49 @@
|
||||
],
|
||||
"dictionaries": {
|
||||
"doc": "docs",
|
||||
"example": "examples",
|
||||
"test": "tests"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"/dist",
|
||||
"/node_modules",
|
||||
"/docs",
|
||||
"/examples/build"
|
||||
"/docs"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/y-js/yjs.git"
|
||||
"url": "https://github.com/yjs/yjs.git"
|
||||
},
|
||||
"keywords": [
|
||||
"crdt"
|
||||
"Yjs",
|
||||
"CRDT",
|
||||
"offline",
|
||||
"shared editing",
|
||||
"concurrency",
|
||||
"collaboration"
|
||||
],
|
||||
"author": "Kevin Jahns",
|
||||
"email": "kevin.jahns@rwth-aachen.de",
|
||||
"email": "kevin.jahns@protonmail.com",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/y-js/yjs/issues"
|
||||
"url": "https://github.com/yjs/yjs/issues"
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"homepage": "https://yjs.dev",
|
||||
"dependencies": {
|
||||
"lib0": "0.0.2"
|
||||
"lib0": "^0.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"y-protocols": "0.0.3",
|
||||
"codemirror": "^5.42.0",
|
||||
"@rollup/plugin-commonjs": "^11.0.1",
|
||||
"@rollup/plugin-node-resolve": "^7.0.0",
|
||||
"concurrently": "^3.6.1",
|
||||
"jsdoc": "^3.5.5",
|
||||
"live-server": "^1.2.1",
|
||||
"prosemirror-example-setup": "^1.0.1",
|
||||
"prosemirror-schema-basic": "^1.0.0",
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-view": "^1.6.5",
|
||||
"quill": "^1.3.6",
|
||||
"quill-cursors": "^1.0.3",
|
||||
"rollup": "^1.1.2",
|
||||
"http-server": "^0.12.1",
|
||||
"jsdoc": "^3.6.3",
|
||||
"markdownlint-cli": "^0.19.0",
|
||||
"rollup": "^1.29.1",
|
||||
"rollup-cli": "^1.0.9",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"standard": "^11.0.1",
|
||||
"standard": "^14.0.0",
|
||||
"tui-jsdoc-template": "^1.2.2",
|
||||
"typescript": "^3.3.3333"
|
||||
"typescript": "^3.7.5",
|
||||
"y-protocols": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import nodeResolve from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
|
||||
const localImports = process.env.LOCALIMPORTS
|
||||
|
||||
const customModules = new Set([
|
||||
'y-websocket',
|
||||
@@ -23,50 +24,32 @@ const debugResolve = {
|
||||
if (importee === 'yjs') {
|
||||
return `${process.cwd()}/src/index.js`
|
||||
}
|
||||
if (customModules.has(importee.split('/')[0])) {
|
||||
return `${process.cwd()}/../${importee}/src/${importee}.js`
|
||||
}
|
||||
if (customLibModules.has(importee.split('/')[0])) {
|
||||
return `${process.cwd()}/../${importee}`
|
||||
if (localImports) {
|
||||
if (customModules.has(importee.split('/')[0])) {
|
||||
return `${process.cwd()}/../${importee}/src/${importee}.js`
|
||||
}
|
||||
if (customLibModules.has(importee.split('/')[0])) {
|
||||
return `${process.cwd()}/../${importee}`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const minificationPlugins = process.env.PRODUCTION ? [terser({
|
||||
module: true,
|
||||
compress: {
|
||||
hoist_vars: true,
|
||||
module: true,
|
||||
passes: 5,
|
||||
pure_getters: true,
|
||||
unsafe_comps: true,
|
||||
unsafe_undefined: true
|
||||
},
|
||||
mangle: {
|
||||
toplevel: true
|
||||
}
|
||||
})] : []
|
||||
|
||||
export default [{
|
||||
input: './src/index.js',
|
||||
output: [{
|
||||
output: {
|
||||
name: 'Y',
|
||||
file: 'dist/yjs.js',
|
||||
file: 'dist/yjs.cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
paths: path => {
|
||||
if (/^lib0\//.test(path)) {
|
||||
return `lib0/dist/${path.slice(5)}`
|
||||
return `lib0/dist/${path.slice(5, -3)}.cjs`
|
||||
}
|
||||
return path
|
||||
}
|
||||
}, {
|
||||
name: 'Y',
|
||||
file: 'dist/yjs.mjs',
|
||||
format: 'es',
|
||||
sourcemap: true
|
||||
}],
|
||||
},
|
||||
external: id => /^lib0\//.test(id)
|
||||
}, {
|
||||
input: './tests/index.js',
|
||||
@@ -80,26 +63,25 @@ export default [{
|
||||
debugResolve,
|
||||
nodeResolve({
|
||||
sourcemap: true,
|
||||
module: true,
|
||||
browser: true
|
||||
mainFields: ['module', 'browser', 'main']
|
||||
}),
|
||||
commonjs()
|
||||
]
|
||||
}, {
|
||||
input: ['./examples/textarea.js', './examples/prosemirror.js'], // './examples/quill.js', './examples/dom.js', './examples/codemirror.js'
|
||||
input: './tests/index.js',
|
||||
output: {
|
||||
dir: 'examples/build',
|
||||
format: 'esm',
|
||||
name: 'test',
|
||||
file: 'dist/tests.cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
debugResolve,
|
||||
nodeResolve({
|
||||
sourcemap: true,
|
||||
module: true,
|
||||
browser: true
|
||||
mainFields: ['module', 'main']
|
||||
}),
|
||||
commonjs(),
|
||||
...minificationPlugins
|
||||
]
|
||||
commonjs()
|
||||
],
|
||||
external: ['isomorphic.js']
|
||||
}]
|
||||
|
||||
58
src/index.js
58
src/index.js
@@ -1,6 +1,6 @@
|
||||
|
||||
export {
|
||||
Y,
|
||||
Doc,
|
||||
Transaction,
|
||||
YArray as Array,
|
||||
YMap as Map,
|
||||
@@ -13,36 +13,48 @@ export {
|
||||
YMapEvent,
|
||||
YArrayEvent,
|
||||
YEvent,
|
||||
AbstractItem,
|
||||
Item,
|
||||
AbstractStruct,
|
||||
GC,
|
||||
ItemBinary,
|
||||
ItemDeleted,
|
||||
ItemEmbed,
|
||||
ItemFormat,
|
||||
ItemJSON,
|
||||
ItemString,
|
||||
ItemType,
|
||||
ContentBinary,
|
||||
ContentDeleted,
|
||||
ContentEmbed,
|
||||
ContentFormat,
|
||||
ContentJSON,
|
||||
ContentAny,
|
||||
ContentString,
|
||||
ContentType,
|
||||
AbstractType,
|
||||
compareCursors,
|
||||
Cursor,
|
||||
createCursorFromTypeOffset,
|
||||
createCursorFromJSON,
|
||||
createAbsolutePositionFromCursor,
|
||||
writeCursor,
|
||||
readCursor,
|
||||
RelativePosition,
|
||||
createRelativePositionFromTypeIndex,
|
||||
createRelativePositionFromJSON,
|
||||
createAbsolutePositionFromRelativePosition,
|
||||
compareRelativePositions,
|
||||
writeRelativePosition,
|
||||
readRelativePosition,
|
||||
ID,
|
||||
createID,
|
||||
compareIDs,
|
||||
getState,
|
||||
getStates,
|
||||
readStatesAsMap,
|
||||
writeStates,
|
||||
writeModel,
|
||||
readModel,
|
||||
Snapshot,
|
||||
createSnapshot,
|
||||
createDeleteSet,
|
||||
createDeleteSetFromStructStore,
|
||||
snapshot,
|
||||
emptySnapshot,
|
||||
findRootTypeKey,
|
||||
typeArrayToArraySnapshot,
|
||||
typeListToArraySnapshot,
|
||||
typeMapGetSnapshot,
|
||||
iterateDeletedStructs
|
||||
iterateDeletedStructs,
|
||||
applyUpdate,
|
||||
encodeStateAsUpdate,
|
||||
encodeStateVector,
|
||||
UndoManager,
|
||||
decodeSnapshot,
|
||||
encodeSnapshot,
|
||||
isDeleted,
|
||||
isParentOf,
|
||||
equalSnapshots,
|
||||
PermanentUserData, // @TODO experimental
|
||||
tryGc
|
||||
} from './internals.js'
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
|
||||
export * from './utils/DeleteSet.js'
|
||||
export * from './utils/Doc.js'
|
||||
export * from './utils/encoding.js'
|
||||
export * from './utils/EventHandler.js'
|
||||
export * from './utils/ID.js'
|
||||
export * from './utils/isParentOf.js'
|
||||
export * from './utils/cursor.js'
|
||||
export * from './utils/PermanentUserData.js'
|
||||
export * from './utils/RelativePosition.js'
|
||||
export * from './utils/Snapshot.js'
|
||||
export * from './utils/StructStore.js'
|
||||
export * from './utils/Transaction.js'
|
||||
// export * from './utils/UndoManager.js'
|
||||
export * from './utils/Y.js'
|
||||
export * from './utils/UndoManager.js'
|
||||
export * from './utils/YEvent.js'
|
||||
|
||||
export * from './types/AbstractType.js'
|
||||
@@ -21,14 +24,13 @@ export * from './types/YXmlHook.js'
|
||||
export * from './types/YXmlText.js'
|
||||
|
||||
export * from './structs/AbstractStruct.js'
|
||||
export * from './structs/AbstractItem.js'
|
||||
export * from './structs/GC.js'
|
||||
export * from './structs/ItemBinary.js'
|
||||
export * from './structs/ItemDeleted.js'
|
||||
export * from './structs/ItemEmbed.js'
|
||||
export * from './structs/ItemFormat.js'
|
||||
export * from './structs/ItemJSON.js'
|
||||
export * from './structs/ItemString.js'
|
||||
export * from './structs/ItemType.js'
|
||||
|
||||
export * from './utils/encoding.js'
|
||||
export * from './structs/ContentBinary.js'
|
||||
export * from './structs/ContentDeleted.js'
|
||||
export * from './structs/ContentEmbed.js'
|
||||
export * from './structs/ContentFormat.js'
|
||||
export * from './structs/ContentJSON.js'
|
||||
export * from './structs/ContentAny.js'
|
||||
export * from './structs/ContentString.js'
|
||||
export * from './structs/ContentType.js'
|
||||
export * from './structs/Item.js'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import {
|
||||
Y, StructStore, ID, Transaction // eslint-disable-line
|
||||
StructStore, ID, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||
@@ -12,16 +12,19 @@ import * as error from 'lib0/error.js'
|
||||
export class AbstractStruct {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {number} length
|
||||
*/
|
||||
constructor (id) {
|
||||
constructor (id, length) {
|
||||
/**
|
||||
* The uniqe identifier of this struct.
|
||||
* @type {ID}
|
||||
* @readonly
|
||||
*/
|
||||
this.id = id
|
||||
this.length = length
|
||||
this.deleted = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge this struct with the item to the right.
|
||||
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
|
||||
@@ -32,12 +35,7 @@ export class AbstractStruct {
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get length () {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
* @param {number} offset
|
||||
@@ -47,6 +45,7 @@ export class AbstractStruct {
|
||||
write (encoder, offset, encodingRef) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
@@ -73,6 +72,7 @@ export class AbstractStructRef {
|
||||
*/
|
||||
this.id = id
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @return {Array<ID|null>}
|
||||
@@ -80,6 +80,7 @@ export class AbstractStructRef {
|
||||
getMissing (transaction) {
|
||||
return this._missing
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
@@ -89,10 +90,4 @@ export class AbstractStructRef {
|
||||
toStruct (transaction, store, offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get length () {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
116
src/structs/ContentAny.js
Normal file
116
src/structs/ContentAny.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentAny {
|
||||
/**
|
||||
* @param {Array<any>} arr
|
||||
*/
|
||||
constructor (arr) {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
this.arr = arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.arr.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return this.arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentAny(this.arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentAny(this.arr.slice(offset))
|
||||
this.arr = this.arr.slice(0, offset)
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentAny} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.arr = this.arr.concat(right.arr)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
const len = this.arr.length
|
||||
encoding.writeVarUint(encoder, len - offset)
|
||||
for (let i = offset; i < len; i++) {
|
||||
const c = this.arr[i]
|
||||
encoding.writeAny(encoder, c)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
export const readContentAny = decoder => {
|
||||
const len = decoding.readVarUint(decoder)
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
cs.push(decoding.readAny(decoder))
|
||||
}
|
||||
return new ContentAny(cs)
|
||||
}
|
||||
100
src/structs/ContentBinary.js
Normal file
100
src/structs/ContentBinary.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as buffer from 'lib0/buffer.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentBinary {
|
||||
/**
|
||||
* @param {Uint8Array} content
|
||||
*/
|
||||
constructor (content) {
|
||||
this.content = content
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.content]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentBinary(this.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentBinary} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoding.writeVarUint8Array(encoder, this.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
export const readContentBinary = decoder => new ContentBinary(buffer.copyUint8Array(decoding.readVarUint8Array(decoder)))
|
||||
107
src/structs/ContentDeleted.js
Normal file
107
src/structs/ContentDeleted.js
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
import {
|
||||
addToDeleteSet,
|
||||
StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentDeleted {
|
||||
/**
|
||||
* @param {number} len
|
||||
*/
|
||||
constructor (len) {
|
||||
this.len = len
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.len
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentDeleted(this.len)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentDeleted(this.len - offset)
|
||||
this.len = offset
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentDeleted} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.len += right.len
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
addToDeleteSet(transaction.deleteSet, item.id, this.len)
|
||||
item.deleted = true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoding.writeVarUint(encoder, this.len - offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
export const readContentDeleted = decoder => new ContentDeleted(decoding.readVarUint(decoder))
|
||||
100
src/structs/ContentEmbed.js
Normal file
100
src/structs/ContentEmbed.js
Normal file
@@ -0,0 +1,100 @@
|
||||
|
||||
import {
|
||||
StructStore, Item, Transaction // 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'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentEmbed {
|
||||
/**
|
||||
* @param {Object} embed
|
||||
*/
|
||||
constructor (embed) {
|
||||
this.embed = embed
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.embed]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentEmbed(this.embed)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentEmbed} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.embed))
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
export const readContentEmbed = decoder => new ContentEmbed(JSON.parse(decoding.readVarString(decoder)))
|
||||
103
src/structs/ContentFormat.js
Normal file
103
src/structs/ContentFormat.js
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import {
|
||||
Item, StructStore, Transaction // 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'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentFormat {
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Object} value
|
||||
*/
|
||||
constructor (key, value) {
|
||||
this.key = key
|
||||
this.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentFormat(this.key, this.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentFormat} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoding.writeVarString(encoder, this.key)
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.value))
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 6
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
export const readContentFormat = decoder => new ContentFormat(decoding.readVarString(decoder), JSON.parse(decoding.readVarString(decoder)))
|
||||
121
src/structs/ContentJSON.js
Normal file
121
src/structs/ContentJSON.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentJSON {
|
||||
/**
|
||||
* @param {Array<any>} arr
|
||||
*/
|
||||
constructor (arr) {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
this.arr = arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.arr.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return this.arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentJSON(this.arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentJSON(this.arr.slice(offset))
|
||||
this.arr = this.arr.slice(0, offset)
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentJSON} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.arr = this.arr.concat(right.arr)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
const len = this.arr.length
|
||||
encoding.writeVarUint(encoder, len - offset)
|
||||
for (let i = offset; i < len; i++) {
|
||||
const c = this.arr[i]
|
||||
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
export const readContentJSON = decoder => {
|
||||
const len = decoding.readVarUint(decoder)
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = decoding.readVarString(decoder)
|
||||
if (c === 'undefined') {
|
||||
cs.push(undefined)
|
||||
} else {
|
||||
cs.push(JSON.parse(c))
|
||||
}
|
||||
}
|
||||
return new ContentJSON(cs)
|
||||
}
|
||||
104
src/structs/ContentString.js
Normal file
104
src/structs/ContentString.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentString {
|
||||
/**
|
||||
* @param {string} str
|
||||
*/
|
||||
constructor (str) {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this.str = str
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.str.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return this.str.split('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentString}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentString(this.str)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentString}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentString(this.str.slice(offset))
|
||||
this.str = this.str.slice(0, offset)
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentString} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.str += right.str
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoding.writeVarString(encoder, offset === 0 ? this.str : this.str.slice(offset))
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ContentString}
|
||||
*/
|
||||
export const readContentString = decoder => new ContentString(decoding.readVarString(decoder))
|
||||
174
src/structs/ContentType.js
Normal file
174
src/structs/ContentType.js
Normal file
@@ -0,0 +1,174 @@
|
||||
|
||||
import {
|
||||
readYArray,
|
||||
readYMap,
|
||||
readYText,
|
||||
readYXmlElement,
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
readYXmlText,
|
||||
StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
|
||||
/**
|
||||
* @type {Array<function(decoding.Decoder):AbstractType<any>>}
|
||||
* @private
|
||||
*/
|
||||
export const typeRefs = [
|
||||
readYArray,
|
||||
readYMap,
|
||||
readYText,
|
||||
readYXmlElement,
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
readYXmlText
|
||||
]
|
||||
|
||||
export const YArrayRefID = 0
|
||||
export const YMapRefID = 1
|
||||
export const YTextRefID = 2
|
||||
export const YXmlElementRefID = 3
|
||||
export const YXmlFragmentRefID = 4
|
||||
export const YXmlHookRefID = 5
|
||||
export const YXmlTextRefID = 6
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentType {
|
||||
/**
|
||||
* @param {AbstractType<YEvent>} type
|
||||
*/
|
||||
constructor (type) {
|
||||
/**
|
||||
* @type {AbstractType<any>}
|
||||
*/
|
||||
this.type = type
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.type]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentType}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentType(this.type._copy())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentType}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentType} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
this.type._integrate(transaction.doc, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {
|
||||
let item = this.type._start
|
||||
while (item !== null) {
|
||||
if (!item.deleted) {
|
||||
item.delete(transaction)
|
||||
} else {
|
||||
// Whis will be gc'd later and we want to merge it if possible
|
||||
// We try to merge all deleted items after each transaction,
|
||||
// but we have no knowledge about that this needs to be merged
|
||||
// since it is not in transaction.ds. Hence we add it to transaction._mergeStructs
|
||||
transaction._mergeStructs.add(item.id)
|
||||
}
|
||||
item = item.right
|
||||
}
|
||||
this.type._map.forEach(item => {
|
||||
if (!item.deleted) {
|
||||
item.delete(transaction)
|
||||
} else {
|
||||
// same as above
|
||||
transaction._mergeStructs.add(item.id)
|
||||
}
|
||||
})
|
||||
transaction.changed.delete(this.type)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {
|
||||
let item = this.type._start
|
||||
while (item !== null) {
|
||||
item.gc(store, true)
|
||||
item = item.right
|
||||
}
|
||||
this.type._start = null
|
||||
this.type._map.forEach(/** @param {Item | null} item */ (item) => {
|
||||
while (item !== null) {
|
||||
item.gc(store, true)
|
||||
item = item.left
|
||||
}
|
||||
})
|
||||
this.type._map = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
this.type._write(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 7
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ContentType}
|
||||
*/
|
||||
export const readContentType = decoder => new ContentType(typeRefs[decoding.readVarUint(decoder)](decoder))
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
AbstractStruct,
|
||||
createID,
|
||||
addStruct,
|
||||
Y, StructStore, Transaction, ID // eslint-disable-line
|
||||
StructStore, Transaction, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
@@ -21,26 +21,18 @@ export class GC extends AbstractStruct {
|
||||
* @param {number} length
|
||||
*/
|
||||
constructor (id, length) {
|
||||
super(id)
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this._len = length
|
||||
super(id, length)
|
||||
this.deleted = true
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this._len
|
||||
}
|
||||
|
||||
delete () {}
|
||||
|
||||
/**
|
||||
* @param {AbstractStruct} right
|
||||
* @param {GC} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this._len += right.length
|
||||
this.length += right.length
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -48,7 +40,7 @@ export class GC extends AbstractStruct {
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
integrate (transaction) {
|
||||
addStruct(transaction.y.store, this)
|
||||
addStruct(transaction.doc.store, this)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +49,7 @@ export class GC extends AbstractStruct {
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoding.writeUint8(encoder, structGCRefNumber)
|
||||
encoding.writeVarUint(encoder, this._len - offset)
|
||||
encoding.writeVarUint(encoder, this.length - offset)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +67,9 @@ export class GCRef extends AbstractStructRef {
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this._len = decoding.readVarUint(decoder)
|
||||
}
|
||||
get length () {
|
||||
return this._len
|
||||
}
|
||||
missing () {
|
||||
return [
|
||||
createID(this.id.client, this.id.clock - 1)
|
||||
]
|
||||
this.length = decoding.readVarUint(decoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
@@ -95,11 +80,11 @@ export class GCRef extends AbstractStructRef {
|
||||
if (offset > 0) {
|
||||
// @ts-ignore
|
||||
this.id = createID(this.id.client, this.id.clock + offset)
|
||||
this._len = this._len - offset
|
||||
this.length -= offset
|
||||
}
|
||||
return new GC(
|
||||
this.id,
|
||||
this._len
|
||||
this.length
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,21 @@ import {
|
||||
replaceStruct,
|
||||
addStruct,
|
||||
addToDeleteSet,
|
||||
ItemDeleted,
|
||||
findRootTypeKey,
|
||||
compareIDs,
|
||||
getItem,
|
||||
getItemType,
|
||||
getItemCleanEnd,
|
||||
getItemCleanStart,
|
||||
YEvent, StructStore, ID, AbstractType, Y, Transaction // eslint-disable-line
|
||||
readContentDeleted,
|
||||
readContentBinary,
|
||||
readContentJSON,
|
||||
readContentAny,
|
||||
readContentString,
|
||||
readContentEmbed,
|
||||
readContentFormat,
|
||||
readContentType,
|
||||
addChangedTypeToTransaction,
|
||||
ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error.js'
|
||||
@@ -28,27 +35,54 @@ import * as set from 'lib0/set.js'
|
||||
import * as binary from 'lib0/binary.js'
|
||||
|
||||
/**
|
||||
* @param {AbstractItem} left
|
||||
* @param {AbstractItem} right
|
||||
* @return {boolean} If true, right is removed from the linked list and should be discarded
|
||||
* @todo This should return several items
|
||||
*
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {{item:Item, diff:number}}
|
||||
*/
|
||||
export const mergeItemWith = (left, right) => {
|
||||
if (compareIDs(right.origin, left.lastId) && left.right === right && compareIDs(left.rightOrigin, right.rightOrigin)) {
|
||||
left.right = right.right
|
||||
if (left.right !== null) {
|
||||
left.right.left = left
|
||||
export const followRedone = (store, id) => {
|
||||
/**
|
||||
* @type {ID|null}
|
||||
*/
|
||||
let nextID = id
|
||||
let diff = 0
|
||||
let item
|
||||
do {
|
||||
if (diff > 0) {
|
||||
nextID = createID(nextID.client, nextID.clock + diff)
|
||||
}
|
||||
return true
|
||||
item = getItem(store, nextID)
|
||||
diff = nextID.clock - item.id.clock
|
||||
nextID = item.redone
|
||||
} while (nextID !== null && item instanceof Item)
|
||||
return {
|
||||
item, diff
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that neither item nor any of its parents is ever deleted.
|
||||
*
|
||||
* This property does not persist when storing it into a database or when
|
||||
* sending it to other peers
|
||||
*
|
||||
* @param {Item|null} item
|
||||
* @param {boolean} keep
|
||||
*/
|
||||
export const keepItem = (item, keep) => {
|
||||
while (item !== null && item.keep !== keep) {
|
||||
item.keep = keep
|
||||
item = item.parent._item
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Split leftItem into two items
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractItem} leftItem
|
||||
* @param {Item} leftItem
|
||||
* @param {number} diff
|
||||
* @return {AbstractItem}
|
||||
* @return {Item}
|
||||
*
|
||||
* @function
|
||||
* @private
|
||||
@@ -56,18 +90,25 @@ export const mergeItemWith = (left, right) => {
|
||||
export const splitItem = (transaction, leftItem, diff) => {
|
||||
const id = leftItem.id
|
||||
// create rightItem
|
||||
const rightItem = leftItem.copy(
|
||||
const rightItem = new Item(
|
||||
createID(id.client, id.clock + diff),
|
||||
leftItem,
|
||||
createID(id.client, id.clock + diff - 1),
|
||||
leftItem.right,
|
||||
leftItem.rightOrigin,
|
||||
leftItem.parent,
|
||||
leftItem.parentSub
|
||||
leftItem.parentSub,
|
||||
leftItem.content.splice(diff)
|
||||
)
|
||||
if (leftItem.deleted) {
|
||||
rightItem.deleted = true
|
||||
}
|
||||
if (leftItem.keep) {
|
||||
rightItem.keep = true
|
||||
}
|
||||
if (leftItem.redone !== null) {
|
||||
rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff)
|
||||
}
|
||||
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
|
||||
leftItem.right = rightItem
|
||||
// update right
|
||||
@@ -80,24 +121,127 @@ export const splitItem = (transaction, leftItem, diff) => {
|
||||
if (rightItem.parentSub !== null && rightItem.right === null) {
|
||||
rightItem.parent._map.set(rightItem.parentSub, rightItem)
|
||||
}
|
||||
leftItem.length = diff
|
||||
return rightItem
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the effect of this operation.
|
||||
*
|
||||
* @param {Transaction} transaction The Yjs instance.
|
||||
* @param {Item} item
|
||||
* @param {Set<Item>} redoitems
|
||||
*
|
||||
* @return {Item|null}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const redoItem = (transaction, item, redoitems) => {
|
||||
if (item.redone !== null) {
|
||||
return getItemCleanStart(transaction, item.redone)
|
||||
}
|
||||
let parentItem = item.parent._item
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let left
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let right
|
||||
if (item.parentSub === null) {
|
||||
// Is an array item. Insert at the old position
|
||||
left = item.left
|
||||
right = item
|
||||
} else {
|
||||
// Is a map item. Insert as current value
|
||||
left = item
|
||||
while (left.right !== null) {
|
||||
left = left.right
|
||||
if (left.id.client !== transaction.doc.clientID) {
|
||||
// It is not possible to redo this item because it conflicts with a
|
||||
// change from another client
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (left.right !== null) {
|
||||
left = /** @type {Item} */ (item.parent._map.get(item.parentSub))
|
||||
}
|
||||
right = null
|
||||
}
|
||||
// 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) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (parentItem !== null && parentItem.redone !== null) {
|
||||
while (parentItem.redone !== null) {
|
||||
parentItem = getItemCleanStart(transaction, parentItem.redone)
|
||||
}
|
||||
// find next cloned_redo items
|
||||
while (left !== null) {
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let leftTrace = left
|
||||
// trace redone until parent matches
|
||||
while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
|
||||
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
|
||||
}
|
||||
if (leftTrace !== null && leftTrace.parent._item === parentItem) {
|
||||
left = leftTrace
|
||||
break
|
||||
}
|
||||
left = left.left
|
||||
}
|
||||
while (right !== null) {
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let rightTrace = right
|
||||
// trace redone until parent matches
|
||||
while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
|
||||
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
|
||||
}
|
||||
if (rightTrace !== null && rightTrace.parent._item === parentItem) {
|
||||
right = rightTrace
|
||||
break
|
||||
}
|
||||
right = right.right
|
||||
}
|
||||
}
|
||||
const redoneItem = new Item(
|
||||
nextID(transaction),
|
||||
left, left === null ? null : left.lastId,
|
||||
right, right === null ? null : right.id,
|
||||
parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type,
|
||||
item.parentSub,
|
||||
item.content.copy()
|
||||
)
|
||||
item.redone = redoneItem.id
|
||||
keepItem(redoneItem, true)
|
||||
redoneItem.integrate(transaction)
|
||||
return redoneItem
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class that represents any content.
|
||||
*/
|
||||
export class AbstractItem extends AbstractStruct {
|
||||
export class Item extends AbstractStruct {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {Item | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {Item | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {AbstractContent} content
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
super(id)
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
|
||||
super(id, content.getLength())
|
||||
/**
|
||||
* The item that was originally to the left of this item.
|
||||
* @type {ID | null}
|
||||
@@ -106,12 +250,12 @@ export class AbstractItem extends AbstractStruct {
|
||||
this.origin = origin
|
||||
/**
|
||||
* The item that is currently to the left of this item.
|
||||
* @type {AbstractItem | null}
|
||||
* @type {Item | null}
|
||||
*/
|
||||
this.left = left
|
||||
/**
|
||||
* The item that is currently to the right of this item.
|
||||
* @type {AbstractItem | null}
|
||||
* @type {Item | null}
|
||||
*/
|
||||
this.right = right
|
||||
/**
|
||||
@@ -143,9 +287,19 @@ export class AbstractItem extends AbstractStruct {
|
||||
/**
|
||||
* If this type's effect is reundone this type refers to the type that undid
|
||||
* this operation.
|
||||
* @type {AbstractItem | null}
|
||||
* @type {ID | null}
|
||||
*/
|
||||
this.redone = null
|
||||
/**
|
||||
* @type {AbstractContent}
|
||||
*/
|
||||
this.content = content
|
||||
this.length = content.getLength()
|
||||
this.countable = content.isCountable()
|
||||
/**
|
||||
* If true, do not garbage collect this Item.
|
||||
*/
|
||||
this.keep = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,13 +307,13 @@ export class AbstractItem extends AbstractStruct {
|
||||
* @private
|
||||
*/
|
||||
integrate (transaction) {
|
||||
const store = transaction.y.store
|
||||
const store = transaction.doc.store
|
||||
const id = this.id
|
||||
const parent = this.parent
|
||||
const parentSub = this.parentSub
|
||||
const length = this.length
|
||||
/**
|
||||
* @type {AbstractItem|null}
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let o
|
||||
// set o to the first conflicting item
|
||||
@@ -175,11 +329,11 @@ export class AbstractItem extends AbstractStruct {
|
||||
}
|
||||
// TODO: use something like DeleteSet here (a tree implementation would be best)
|
||||
/**
|
||||
* @type {Set<AbstractItem>}
|
||||
* @type {Set<Item>}
|
||||
*/
|
||||
const conflictingItems = new Set()
|
||||
/**
|
||||
* @type {Set<AbstractItem>}
|
||||
* @type {Set<Item>}
|
||||
*/
|
||||
const itemsBeforeOrigin = new Set()
|
||||
// Let c in conflictingItems, b in itemsBeforeOrigin
|
||||
@@ -238,8 +392,9 @@ export class AbstractItem extends AbstractStruct {
|
||||
parent._length += length
|
||||
}
|
||||
addStruct(store, this)
|
||||
maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub)
|
||||
// @ts-ignore
|
||||
this.content.integrate(transaction, this)
|
||||
// add parent to transaction.changed
|
||||
addChangedTypeToTransaction(transaction, parent, parentSub)
|
||||
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
|
||||
// delete if parent is deleted or if this is not the current attribute value of parent
|
||||
this.delete(transaction)
|
||||
@@ -270,84 +425,6 @@ export class AbstractItem extends AbstractStruct {
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Item with the same effect as this Item (without position effect)
|
||||
*
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @return {AbstractItem}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
throw new Error('unimplemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the effect of this operation.
|
||||
*
|
||||
* @param {Transaction} transaction The Yjs instance.
|
||||
* @param {Set<AbstractItem>} redoitems
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
redo (transaction, redoitems) {
|
||||
if (this.redone !== null) {
|
||||
return this.redone
|
||||
}
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let parent = this.parent
|
||||
if (parent === null) {
|
||||
return
|
||||
}
|
||||
let left, right
|
||||
if (this.parentSub === null) {
|
||||
// Is an array item. Insert at the old position
|
||||
left = this.left
|
||||
right = this
|
||||
} else {
|
||||
// Is a map item. Insert as current value
|
||||
left = parent.type._map.get(this.parentSub)
|
||||
right = null
|
||||
}
|
||||
// make sure that parent is redone
|
||||
if (parent._deleted === true && parent.redone === null) {
|
||||
// try to undo parent if it will be undone anyway
|
||||
if (!redoitems.has(parent) || !parent.redo(transaction, redoitems)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (parent.redone !== null) {
|
||||
while (parent.redone !== null) {
|
||||
parent = parent.redone
|
||||
}
|
||||
// find next cloned_redo items
|
||||
while (left !== null) {
|
||||
if (left.redone !== null && left.redone.parent === parent) {
|
||||
left = left.redone
|
||||
break
|
||||
}
|
||||
left = left.left
|
||||
}
|
||||
while (right !== null) {
|
||||
if (right.redone !== null && right.redone.parent === parent) {
|
||||
right = right.redone
|
||||
}
|
||||
right = right.right
|
||||
}
|
||||
}
|
||||
this.redone = this.copy(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, this.parentSub)
|
||||
this.redone.integrate(transaction)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the last content address of this Item.
|
||||
*/
|
||||
@@ -356,44 +433,35 @@ export class AbstractItem extends AbstractStruct {
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the length of this Item.
|
||||
* Try to merge two items
|
||||
*
|
||||
* @param {Item} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
get length () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return false if this Item is some kind of meta information
|
||||
* (e.g. format information).
|
||||
*
|
||||
* * Whether this Item should be addressable via `yarray.get(i)`
|
||||
* * Whether this Item should be counted when computing yarray.length
|
||||
*/
|
||||
get countable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not call directly. Always split via StructStore!
|
||||
*
|
||||
* Splits this Item so that another Item can be inserted in-between.
|
||||
* This must be overwritten if _length > 1
|
||||
* Returns right part after split
|
||||
*
|
||||
* (see {@link ItemJSON}/{@link ItemString} for implementation)
|
||||
*
|
||||
* Does not integrate the struct, nor store it in struct store.
|
||||
*
|
||||
* This method should only be cally by StructStore.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {number} diff
|
||||
* @return {AbstractItem}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
splitAt (transaction, diff) {
|
||||
throw new Error('unimplemented')
|
||||
mergeWith (right) {
|
||||
if (
|
||||
compareIDs(right.origin, this.lastId) &&
|
||||
this.right === right &&
|
||||
compareIDs(this.rightOrigin, right.rightOrigin) &&
|
||||
this.id.client === right.id.client &&
|
||||
this.id.clock + this.length === right.id.clock &&
|
||||
this.deleted === right.deleted &&
|
||||
this.redone === null &&
|
||||
right.redone === null &&
|
||||
this.content.constructor === right.content.constructor &&
|
||||
this.content.mergeWith(right.content)
|
||||
) {
|
||||
if (right.keep) {
|
||||
this.keep = true
|
||||
}
|
||||
this.right = right.right
|
||||
if (this.right !== null) {
|
||||
this.right.left = this
|
||||
}
|
||||
this.length += right.length
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,54 +479,26 @@ export class AbstractItem extends AbstractStruct {
|
||||
this.deleted = true
|
||||
addToDeleteSet(transaction.deleteSet, this.id, this.length)
|
||||
maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
|
||||
this.content.delete(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
gcChildren (transaction, store) { }
|
||||
|
||||
/**
|
||||
* @todo remove transaction param
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {boolean} parentGCd
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
gc (transaction, store, parentGCd) {
|
||||
gc (store, parentGCd) {
|
||||
if (!this.deleted) {
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
let r
|
||||
this.content.gc(store)
|
||||
if (parentGCd) {
|
||||
r = new GC(this.id, this.length)
|
||||
replaceStruct(store, this, new GC(this.id, this.length))
|
||||
} else {
|
||||
r = new ItemDeleted(this.id, this.left, this.origin, this.right, this.rightOrigin, this.parent, this.parentSub, this.length)
|
||||
if (r.right !== null) {
|
||||
r.right.left = r
|
||||
} else if (r.parentSub !== null) {
|
||||
r.parent._map.set(r.parentSub, r)
|
||||
}
|
||||
if (r.left !== null) {
|
||||
r.left.right = r
|
||||
} else if (r.parentSub === null) {
|
||||
r.parent._start = r
|
||||
}
|
||||
this.content = new ContentDeleted(this.length)
|
||||
}
|
||||
replaceStruct(store, this, r)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,15 +509,14 @@ export class AbstractItem extends AbstractStruct {
|
||||
*
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
* @param {number} offset
|
||||
* @param {number} encodingRef
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
write (encoder, offset, encodingRef) {
|
||||
write (encoder, offset) {
|
||||
const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin
|
||||
const rightOrigin = this.rightOrigin
|
||||
const parentSub = this.parentSub
|
||||
const info = (encodingRef & binary.BITS5) |
|
||||
const info = (this.content.getRef() & binary.BITS5) |
|
||||
(origin === null ? 0 : binary.BIT8) | // origin is defined
|
||||
(rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined
|
||||
(parentSub === null ? 0 : binary.BIT6) // parentSub is non-null
|
||||
@@ -493,26 +532,140 @@ export class AbstractItem extends AbstractStruct {
|
||||
if (parent._item === null) {
|
||||
// parent type on y._map
|
||||
// find the correct key
|
||||
// @ts-ignore we know that y exists
|
||||
const ykey = findRootTypeKey(parent)
|
||||
encoding.writeVarUint(encoder, 1) // write parentYKey
|
||||
encoding.writeVarString(encoder, ykey)
|
||||
} else {
|
||||
encoding.writeVarUint(encoder, 0) // write parent id
|
||||
// @ts-ignore _item is defined because parent is integrated
|
||||
writeID(encoder, parent._item.id)
|
||||
}
|
||||
if (parentSub !== null) {
|
||||
encoding.writeVarString(encoder, parentSub)
|
||||
}
|
||||
}
|
||||
this.content.write(encoder, offset)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {number} info
|
||||
*/
|
||||
const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
|
||||
|
||||
/**
|
||||
* A lookup map for reading Item content.
|
||||
*
|
||||
* @type {Array<function(decoding.Decoder):AbstractContent>}
|
||||
*/
|
||||
export const contentRefs = [
|
||||
() => { throw error.unexpectedCase() }, // GC is not ItemContent
|
||||
readContentDeleted,
|
||||
readContentJSON,
|
||||
readContentBinary,
|
||||
readContentString,
|
||||
readContentEmbed,
|
||||
readContentFormat,
|
||||
readContentType,
|
||||
readContentAny
|
||||
]
|
||||
|
||||
/**
|
||||
* Do not implement this class!
|
||||
*/
|
||||
export class AbstractContent {
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return false if this Item is some kind of meta information
|
||||
* (e.g. format information).
|
||||
*
|
||||
* * Whether this Item should be addressable via `yarray.get(i)`
|
||||
* * Whether this Item should be counted when computing yarray.length
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {AbstractContent}
|
||||
*/
|
||||
copy () {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {AbstractContent}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractContent} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class AbstractItemRef extends AbstractStructRef {
|
||||
export class ItemRef extends AbstractStructRef {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {ID} id
|
||||
@@ -562,81 +715,71 @@ export class AbstractItemRef extends AbstractStructRef {
|
||||
if (this.parent !== null) {
|
||||
missing.push(this.parent)
|
||||
}
|
||||
/**
|
||||
* @type {AbstractContent}
|
||||
*/
|
||||
this.content = readItemContent(decoder, info)
|
||||
this.length = this.content.getLength()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractItemRef} item
|
||||
* @param {number} offset
|
||||
*
|
||||
* @function
|
||||
* @private
|
||||
*/
|
||||
export const changeItemRefOffset = (item, offset) => {
|
||||
item.id = createID(item.id.client, item.id.clock + offset)
|
||||
item.left = createID(item.id.client, item.id.clock - 1)
|
||||
}
|
||||
|
||||
export class ItemParams {
|
||||
/**
|
||||
* @param {AbstractItem?} left
|
||||
* @param {AbstractItem?} right
|
||||
* @param {AbstractType<YEvent>?} parent
|
||||
* @param {string|null} parentSub
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {number} offset
|
||||
* @return {Item|GC}
|
||||
*/
|
||||
constructor (left, right, parent, parentSub) {
|
||||
this.left = left
|
||||
this.right = right
|
||||
this.parent = parent
|
||||
this.parentSub = parentSub
|
||||
}
|
||||
}
|
||||
toStruct (transaction, store, offset) {
|
||||
if (offset > 0) {
|
||||
/**
|
||||
* @type {ID}
|
||||
*/
|
||||
const id = this.id
|
||||
this.id = createID(id.client, id.clock + offset)
|
||||
this.left = createID(this.id.client, this.id.clock - 1)
|
||||
this.content = this.content.splice(offset)
|
||||
this.length -= offset
|
||||
}
|
||||
|
||||
/**
|
||||
* Outsourcing some of the logic of computing the item params from a received struct.
|
||||
* If parent === null, it is expected to gc the read struct. Otherwise apply it.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {ID|null} leftid
|
||||
* @param {ID|null} rightid
|
||||
* @param {ID|null} parentid
|
||||
* @param {string|null} parentSub
|
||||
* @param {string|null} parentYKey
|
||||
* @return {ItemParams}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const computeItemParams = (transaction, store, leftid, rightid, parentid, parentSub, parentYKey) => {
|
||||
const left = leftid === null ? null : getItemCleanEnd(transaction, store, leftid)
|
||||
const right = rightid === null ? null : getItemCleanStart(transaction, store, rightid)
|
||||
let parent = null
|
||||
if (parentid !== null) {
|
||||
const parentItem = getItemType(store, parentid)
|
||||
switch (parentItem.constructor) {
|
||||
case ItemDeleted:
|
||||
case GC:
|
||||
break
|
||||
default:
|
||||
if (!parentItem.deleted && (left === null || left.constructor !== GC) && (right === null || right.constructor !== GC)) {
|
||||
parent = parentItem.type
|
||||
}
|
||||
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
|
||||
const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
|
||||
let parent = null
|
||||
let parentSub = this.parentSub
|
||||
if (this.parent !== null) {
|
||||
const parentItem = getItem(store, this.parent)
|
||||
// Edge case: toStruct is called with an offset > 0. In this case left is defined.
|
||||
// Depending in which order structs arrive, left may be GC'd and the parent not
|
||||
// deleted. This is why we check if left is GC'd. Strictly we don't have
|
||||
// to check if right is GC'd, but we will in case we run into future issues
|
||||
if (!parentItem.deleted && (left === null || left.constructor !== GC) && (right === null || right.constructor !== GC)) {
|
||||
parent = /** @type {ContentType} */ (parentItem.content).type
|
||||
}
|
||||
} else if (this.parentYKey !== null) {
|
||||
parent = transaction.doc.get(this.parentYKey)
|
||||
} else if (left !== null) {
|
||||
if (left.constructor !== GC) {
|
||||
parent = left.parent
|
||||
parentSub = left.parentSub
|
||||
}
|
||||
} else if (right !== null) {
|
||||
if (right.constructor !== GC) {
|
||||
parent = right.parent
|
||||
parentSub = right.parentSub
|
||||
}
|
||||
} else {
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
} else if (parentYKey !== null) {
|
||||
parent = transaction.y.get(parentYKey)
|
||||
} else if (left !== null) {
|
||||
if (left.constructor !== GC) {
|
||||
parent = left.parent
|
||||
parentSub = left.parentSub
|
||||
}
|
||||
} else if (right !== null) {
|
||||
if (right.constructor !== GC) {
|
||||
parent = right.parent
|
||||
parentSub = right.parentSub
|
||||
}
|
||||
} else {
|
||||
throw error.unexpectedCase()
|
||||
|
||||
return parent === null
|
||||
? new GC(this.id, this.length)
|
||||
: new Item(
|
||||
this.id,
|
||||
left,
|
||||
this.left,
|
||||
right,
|
||||
this.right,
|
||||
parent,
|
||||
parentSub,
|
||||
this.content
|
||||
)
|
||||
}
|
||||
return new ItemParams(left, right, parent, parentSub)
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
|
||||
import {
|
||||
AbstractItem,
|
||||
AbstractItemRef,
|
||||
computeItemParams,
|
||||
GC,
|
||||
StructStore, Transaction, AbstractType, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const structBinaryRefNumber = 1
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemBinary extends AbstractItem {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {ArrayBuffer} content
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
|
||||
super(id, left, origin, right, rightOrigin, parent, parentSub)
|
||||
this.content = content
|
||||
}
|
||||
getContent () {
|
||||
return [this.content]
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
*/
|
||||
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
return new ItemBinary(id, left, origin, right, rightOrigin, parent, parentSub, this.content)
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structBinaryRefNumber)
|
||||
encoding.writePayload(encoder, this.content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemBinaryRef extends AbstractItemRef {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {ID} id
|
||||
* @param {number} info
|
||||
*/
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
/**
|
||||
* @type {ArrayBuffer}
|
||||
*/
|
||||
this.content = decoding.readPayload(decoder)
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {number} offset
|
||||
* @return {ItemBinary|GC}
|
||||
*/
|
||||
toStruct (transaction, store, offset) {
|
||||
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
|
||||
return parent === null
|
||||
? new GC(this.id, this.length)
|
||||
: new ItemBinary(
|
||||
this.id,
|
||||
left,
|
||||
this.left,
|
||||
right,
|
||||
this.right,
|
||||
parent,
|
||||
parentSub,
|
||||
this.content
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
|
||||
import {
|
||||
AbstractItem,
|
||||
AbstractItemRef,
|
||||
computeItemParams,
|
||||
changeItemRefOffset,
|
||||
GC,
|
||||
splitItem,
|
||||
addToDeleteSet,
|
||||
mergeItemWith,
|
||||
Y, StructStore, Transaction, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const structDeletedRefNumber = 2
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemDeleted extends AbstractItem {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {number} length
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, length) {
|
||||
super(id, left, origin, right, rightOrigin, parent, parentSub)
|
||||
this._len = length
|
||||
this.deleted = true
|
||||
}
|
||||
get length () {
|
||||
return this._len
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
*/
|
||||
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
return new ItemDeleted(id, left, origin, right, rightOrigin, parent, parentSub, this.length)
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
integrate (transaction) {
|
||||
super.integrate(transaction)
|
||||
addToDeleteSet(transaction.deleteSet, this.id, this.length)
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {number} diff
|
||||
*/
|
||||
splitAt (transaction, diff) {
|
||||
/**
|
||||
* @type {ItemDeleted}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const right = splitItem(transaction, this, diff)
|
||||
right._len -= diff
|
||||
this._len = diff
|
||||
return right
|
||||
}
|
||||
/**
|
||||
* @param {ItemDeleted} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
if (mergeItemWith(this, right)) {
|
||||
this._len += right._len
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {boolean} parentGCd
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
gc (transaction, store, parentGCd) {
|
||||
if (parentGCd) {
|
||||
super.gc(transaction, store, parentGCd)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structDeletedRefNumber)
|
||||
encoding.writeVarUint(encoder, this.length - offset)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemDeletedRef extends AbstractItemRef {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {ID} id
|
||||
* @param {number} info
|
||||
*/
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this.len = decoding.readVarUint(decoder)
|
||||
}
|
||||
get length () {
|
||||
return this.len
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {number} offset
|
||||
* @return {ItemDeleted|GC}
|
||||
*/
|
||||
toStruct (transaction, store, offset) {
|
||||
if (offset > 0) {
|
||||
changeItemRefOffset(this, offset)
|
||||
this.len = this.len - offset
|
||||
}
|
||||
|
||||
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
|
||||
return parent === null
|
||||
? new GC(this.id, this.length)
|
||||
: new ItemDeleted(
|
||||
this.id,
|
||||
left,
|
||||
this.left,
|
||||
right,
|
||||
this.right,
|
||||
parent,
|
||||
parentSub,
|
||||
this.len
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
|
||||
import {
|
||||
AbstractItem,
|
||||
AbstractItemRef,
|
||||
computeItemParams,
|
||||
GC,
|
||||
Transaction, StructStore, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const structEmbedRefNumber = 3
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemEmbed extends AbstractItem {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {Object} embed
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, embed) {
|
||||
super(id, left, origin, right, rightOrigin, parent, parentSub)
|
||||
this.embed = embed
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
*/
|
||||
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
return new ItemEmbed(id, left, origin, right, rightOrigin, parent, parentSub, this.embed)
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structEmbedRefNumber)
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.embed))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemEmbedRef extends AbstractItemRef {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {ID} id
|
||||
* @param {number} info
|
||||
*/
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
/**
|
||||
* @type {ArrayBuffer}
|
||||
*/
|
||||
this.embed = JSON.parse(decoding.readVarString(decoder))
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {number} offset
|
||||
* @return {ItemEmbed|GC}
|
||||
*/
|
||||
toStruct (transaction, store, offset) {
|
||||
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
|
||||
return parent === null
|
||||
? new GC(this.id, this.length)
|
||||
: new ItemEmbed(
|
||||
this.id,
|
||||
left,
|
||||
this.left,
|
||||
right,
|
||||
this.right,
|
||||
parent,
|
||||
parentSub,
|
||||
this.embed
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
|
||||
import {
|
||||
AbstractItem,
|
||||
AbstractItemRef,
|
||||
computeItemParams,
|
||||
GC,
|
||||
Transaction, StructStore, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const structFormatRefNumber = 4
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemFormat extends AbstractItem {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, key, value) {
|
||||
super(id, left, origin, right, rightOrigin, parent, parentSub)
|
||||
this.key = key
|
||||
this.value = value
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
*/
|
||||
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
return new ItemFormat(id, left, origin, right, rightOrigin, parent, parentSub, this.key, this.value)
|
||||
}
|
||||
get countable () {
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structFormatRefNumber)
|
||||
encoding.writeVarString(encoder, this.key)
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemFormatRef extends AbstractItemRef {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {ID} id
|
||||
* @param {number} info
|
||||
*/
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this.key = decoding.readVarString(decoder)
|
||||
this.value = JSON.parse(decoding.readVarString(decoder))
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {number} offset
|
||||
* @return {ItemFormat|GC}
|
||||
*/
|
||||
toStruct (transaction, store, offset) {
|
||||
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
|
||||
return parent === null
|
||||
? new GC(this.id, this.length)
|
||||
: new ItemFormat(
|
||||
this.id,
|
||||
left,
|
||||
this.left,
|
||||
right,
|
||||
this.right,
|
||||
parent,
|
||||
parentSub,
|
||||
this.key,
|
||||
this.value
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
|
||||
import {
|
||||
AbstractItem,
|
||||
AbstractItemRef,
|
||||
computeItemParams,
|
||||
splitItem,
|
||||
changeItemRefOffset,
|
||||
GC,
|
||||
mergeItemWith,
|
||||
Transaction, StructStore, Y, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const structJSONRefNumber = 5
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemJSON extends AbstractItem {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {Array<any>} content
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
|
||||
super(id, left, origin, right, rightOrigin, parent, parentSub)
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
this.content = content
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
*/
|
||||
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
return new ItemJSON(id, left, origin, right, rightOrigin, parent, parentSub, this.content)
|
||||
}
|
||||
get length () {
|
||||
return this.content.length
|
||||
}
|
||||
getContent () {
|
||||
return this.content
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {number} diff
|
||||
*/
|
||||
splitAt (transaction, diff) {
|
||||
/**
|
||||
* @type {ItemJSON}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const right = splitItem(transaction, this, diff)
|
||||
right.content = this.content.splice(diff)
|
||||
return right
|
||||
}
|
||||
/**
|
||||
* @param {ItemJSON} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
if (mergeItemWith(this, right)) {
|
||||
this.content = this.content.concat(right.content)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structJSONRefNumber)
|
||||
const len = this.content.length
|
||||
encoding.writeVarUint(encoder, len - offset)
|
||||
for (let i = offset; i < len; i++) {
|
||||
const c = this.content[i]
|
||||
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemJSONRef extends AbstractItemRef {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {ID} id
|
||||
* @param {number} info
|
||||
*/
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
const len = decoding.readVarUint(decoder)
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = decoding.readVarString(decoder)
|
||||
if (c === 'undefined') {
|
||||
cs.push(undefined)
|
||||
} else {
|
||||
cs.push(JSON.parse(c))
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
this.content = cs
|
||||
}
|
||||
get length () {
|
||||
return this.content.length
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {number} offset
|
||||
* @return {ItemJSON|GC}
|
||||
*/
|
||||
toStruct (transaction, store, offset) {
|
||||
if (offset > 0) {
|
||||
changeItemRefOffset(this, offset)
|
||||
this.content = this.content.slice(offset)
|
||||
}
|
||||
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
|
||||
return parent === null
|
||||
? new GC(this.id, this.length)
|
||||
: new ItemJSON(
|
||||
this.id,
|
||||
left,
|
||||
this.left,
|
||||
right,
|
||||
this.right,
|
||||
parent,
|
||||
parentSub,
|
||||
this.content
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
|
||||
import {
|
||||
AbstractItem,
|
||||
AbstractItemRef,
|
||||
computeItemParams,
|
||||
splitItem,
|
||||
changeItemRefOffset,
|
||||
GC,
|
||||
mergeItemWith,
|
||||
Transaction, StructStore, Y, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
export const structStringRefNumber = 6
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemString extends AbstractItem {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {string} string
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, string) {
|
||||
super(id, left, origin, right, rightOrigin, parent, parentSub)
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this.string = string
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
*/
|
||||
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
return new ItemString(id, left, origin, right, rightOrigin, parent, parentSub, this.string)
|
||||
}
|
||||
getContent () {
|
||||
return this.string.split('')
|
||||
}
|
||||
get length () {
|
||||
return this.string.length
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {number} diff
|
||||
* @return {ItemString}
|
||||
*/
|
||||
splitAt (transaction, diff) {
|
||||
/**
|
||||
* @type {ItemString}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const right = splitItem(transaction, this, diff)
|
||||
right.string = this.string.slice(diff)
|
||||
this.string = this.string.slice(0, diff)
|
||||
return right
|
||||
}
|
||||
/**
|
||||
* @param {ItemString} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
if (mergeItemWith(this, right)) {
|
||||
this.string += right.string
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structStringRefNumber)
|
||||
encoding.writeVarString(encoder, offset === 0 ? this.string : this.string.slice(offset))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemStringRef extends AbstractItemRef {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {ID} id
|
||||
* @param {number} info
|
||||
*/
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this.string = decoding.readVarString(decoder)
|
||||
}
|
||||
get length () {
|
||||
return this.string.length
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {number} offset
|
||||
* @return {ItemString|GC}
|
||||
*/
|
||||
toStruct (transaction, store, offset) {
|
||||
if (offset > 0) {
|
||||
changeItemRefOffset(this, offset)
|
||||
this.string = this.string.slice(offset)
|
||||
}
|
||||
|
||||
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
|
||||
return parent === null
|
||||
? new GC(this.id, this.length)
|
||||
: new ItemString(
|
||||
this.id,
|
||||
left,
|
||||
this.left,
|
||||
right,
|
||||
this.right,
|
||||
parent,
|
||||
parentSub,
|
||||
this.string
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
|
||||
import {
|
||||
AbstractItem,
|
||||
AbstractItemRef,
|
||||
computeItemParams,
|
||||
readYArray,
|
||||
readYMap,
|
||||
readYText,
|
||||
readYXmlElement,
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
readYXmlText,
|
||||
StructStore, Y, GC, Transaction, ID, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const structTypeRefNumber = 7
|
||||
|
||||
/**
|
||||
* @type {Array<function(decoding.Decoder):AbstractType<any>>}
|
||||
* @private
|
||||
*/
|
||||
export const typeRefs = [
|
||||
readYArray,
|
||||
readYMap,
|
||||
readYText,
|
||||
readYXmlElement,
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
readYXmlText
|
||||
]
|
||||
|
||||
export const YArrayRefID = 0
|
||||
export const YMapRefID = 1
|
||||
export const YTextRefID = 2
|
||||
export const YXmlElementRefID = 3
|
||||
export const YXmlFragmentRefID = 4
|
||||
export const YXmlHookRefID = 5
|
||||
export const YXmlTextRefID = 6
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemType extends AbstractItem {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @param {AbstractType<any>} type
|
||||
*/
|
||||
constructor (id, left, origin, right, rightOrigin, parent, parentSub, type) {
|
||||
super(id, left, origin, right, rightOrigin, parent, parentSub)
|
||||
this.type = type
|
||||
}
|
||||
|
||||
getContent () {
|
||||
return [this.type]
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {AbstractItem | null} left
|
||||
* @param {ID | null} origin
|
||||
* @param {AbstractItem | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string | null} parentSub
|
||||
* @return {ItemType}
|
||||
*/
|
||||
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
|
||||
return new ItemType(id, left, origin, right, rightOrigin, parent, parentSub, this.type._copy())
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
integrate (transaction) {
|
||||
super.integrate(transaction)
|
||||
this.type._integrate(transaction.y, this)
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structTypeRefNumber)
|
||||
this.type._write(encoder)
|
||||
}
|
||||
/**
|
||||
* Mark this Item as deleted.
|
||||
*
|
||||
* @param {Transaction} transaction The Yjs instance
|
||||
* @private
|
||||
*/
|
||||
delete (transaction) {
|
||||
if (!this.deleted) {
|
||||
super.delete(transaction)
|
||||
let item = this.type._start
|
||||
while (item !== null) {
|
||||
if (!item.deleted) {
|
||||
item.delete(transaction)
|
||||
} else {
|
||||
// Whis will be gc'd later and we want to merge it if possible
|
||||
// We try to merge all deleted items after each transaction,
|
||||
// but we have no knowledge about that this needs to be merged
|
||||
// since it is not in transaction.ds. Hence we add it to transaction._mergeStructs
|
||||
transaction._mergeStructs.add(item.id)
|
||||
}
|
||||
item = item.right
|
||||
}
|
||||
this.type._map.forEach(item => {
|
||||
if (!item.deleted) {
|
||||
item.delete(transaction)
|
||||
} else {
|
||||
// same as above
|
||||
transaction._mergeStructs.add(item.id)
|
||||
}
|
||||
})
|
||||
transaction.changed.delete(this.type)
|
||||
transaction.changedParentTypes.delete(this.type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gcChildren (transaction, store) {
|
||||
let item = this.type._start
|
||||
while (item !== null) {
|
||||
item.gc(transaction, store, true)
|
||||
item = item.right
|
||||
}
|
||||
this.type._start = null
|
||||
this.type._map.forEach(item => {
|
||||
while (item !== null) {
|
||||
item.gc(transaction, store, true)
|
||||
// @ts-ignore
|
||||
item = item.left
|
||||
}
|
||||
})
|
||||
this._map = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {boolean} parentGCd
|
||||
*/
|
||||
gc (transaction, store, parentGCd) {
|
||||
this.gcChildren(transaction, store)
|
||||
super.gc(transaction, store, parentGCd)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ItemTypeRef extends AbstractItemRef {
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {ID} id
|
||||
* @param {number} info
|
||||
*/
|
||||
constructor (decoder, id, info) {
|
||||
super(decoder, id, info)
|
||||
const typeRef = decoding.readVarUint(decoder)
|
||||
/**
|
||||
* @type {AbstractType<any>}
|
||||
*/
|
||||
this.type = typeRefs[typeRef](decoder)
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {number} offset
|
||||
* @return {ItemType|GC}
|
||||
*/
|
||||
toStruct (transaction, store, offset) {
|
||||
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
|
||||
return parent === null
|
||||
? new GC(this.id, this.length)
|
||||
: new ItemType(
|
||||
this.id,
|
||||
left,
|
||||
this.left,
|
||||
right,
|
||||
this.right,
|
||||
parent,
|
||||
parentSub,
|
||||
this.type
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
callEventHandlerListeners,
|
||||
addEventHandlerListener,
|
||||
createEventHandler,
|
||||
ItemType,
|
||||
nextID,
|
||||
isVisible,
|
||||
ItemJSON,
|
||||
ItemBinary,
|
||||
ContentType,
|
||||
ContentAny,
|
||||
ContentBinary,
|
||||
createID,
|
||||
getItemCleanStart,
|
||||
Y, Snapshot, Transaction, EventHandler, YEvent, AbstractItem, // eslint-disable-line
|
||||
Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as map from 'lib0/map.js'
|
||||
@@ -30,7 +30,7 @@ import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||
* @param {EventType} event
|
||||
*/
|
||||
export const callTypeObservers = (type, transaction, event) => {
|
||||
callEventHandlerListeners(type._eH, event, transaction)
|
||||
const changedType = type
|
||||
const changedParentTypes = transaction.changedParentTypes
|
||||
while (true) {
|
||||
// @ts-ignore
|
||||
@@ -40,6 +40,7 @@ export const callTypeObservers = (type, transaction, event) => {
|
||||
}
|
||||
type = type._item.parent
|
||||
}
|
||||
callEventHandlerListeners(changedType._eH, event, transaction)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,24 +50,24 @@ export const callTypeObservers = (type, transaction, event) => {
|
||||
export class AbstractType {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {ItemType|null}
|
||||
* @type {Item|null}
|
||||
*/
|
||||
this._item = null
|
||||
/**
|
||||
* @private
|
||||
* @type {Map<string,AbstractItem>}
|
||||
* @type {Map<string,Item>}
|
||||
*/
|
||||
this._map = new Map()
|
||||
/**
|
||||
* @private
|
||||
* @type {AbstractItem|null}
|
||||
* @type {Item|null}
|
||||
*/
|
||||
this._start = null
|
||||
/**
|
||||
* @private
|
||||
* @type {Y|null}
|
||||
* @type {Doc|null}
|
||||
*/
|
||||
this._y = null
|
||||
this.doc = null
|
||||
this._length = 0
|
||||
/**
|
||||
* Event handlers
|
||||
@@ -87,12 +88,12 @@ export class AbstractType {
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {ItemType|null} item
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item|null} item
|
||||
* @private
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
this._y = y
|
||||
this.doc = y
|
||||
this._item = item
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ export class AbstractType {
|
||||
* @private
|
||||
*/
|
||||
_copy () {
|
||||
throw new Error('unimplemented')
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +171,7 @@ export class AbstractType {
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {Object | Array | number | string}
|
||||
* @return {any}
|
||||
*/
|
||||
toJSON () {}
|
||||
}
|
||||
@@ -182,12 +183,12 @@ export class AbstractType {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayToArray = type => {
|
||||
export const typeListToArray = type => {
|
||||
const cs = []
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
if (n.countable && !n.deleted) {
|
||||
const c = n.getContent()
|
||||
const c = n.content.getContent()
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
cs.push(c[i])
|
||||
}
|
||||
@@ -205,12 +206,12 @@ export const typeArrayToArray = type => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayToArraySnapshot = (type, snapshot) => {
|
||||
export const typeListToArraySnapshot = (type, snapshot) => {
|
||||
const cs = []
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
if (n.countable && isVisible(n, snapshot)) {
|
||||
const c = n.getContent()
|
||||
const c = n.content.getContent()
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
cs.push(c[i])
|
||||
}
|
||||
@@ -229,12 +230,12 @@ export const typeArrayToArraySnapshot = (type, snapshot) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayForEach = (type, f) => {
|
||||
export const typeListForEach = (type, f) => {
|
||||
let index = 0
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
if (n.countable && !n.deleted) {
|
||||
const c = n.getContent()
|
||||
const c = n.content.getContent()
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
f(c[i], index++, type)
|
||||
}
|
||||
@@ -252,12 +253,12 @@ export const typeArrayForEach = (type, f) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayMap = (type, f) => {
|
||||
export const typeListMap = (type, f) => {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
const result = []
|
||||
typeArrayForEach(type, (c, i) => {
|
||||
typeListForEach(type, (c, i) => {
|
||||
result.push(f(c, i, type))
|
||||
})
|
||||
return result
|
||||
@@ -270,7 +271,7 @@ export const typeArrayMap = (type, f) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayCreateIterator = type => {
|
||||
export const typeListCreateIterator = type => {
|
||||
let n = type._start
|
||||
/**
|
||||
* @type {Array<any>|null}
|
||||
@@ -287,18 +288,15 @@ export const typeArrayCreateIterator = type => {
|
||||
while (n !== null && n.deleted) {
|
||||
n = n.right
|
||||
}
|
||||
}
|
||||
// check if we reached the end, no need to check currentContent, because it does not exist
|
||||
if (n === null) {
|
||||
return {
|
||||
done: true,
|
||||
value: undefined
|
||||
// check if we reached the end, no need to check currentContent, because it does not exist
|
||||
if (n === null) {
|
||||
return {
|
||||
done: true,
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
// currentContent could exist from the last iteration
|
||||
if (currentContent === null) {
|
||||
// we found n, so we can set currentContent
|
||||
currentContent = n.getContent()
|
||||
currentContent = n.content.getContent()
|
||||
currentContentIndex = 0
|
||||
n = n.right // we used the content of n, now iterate to next
|
||||
}
|
||||
@@ -326,12 +324,12 @@ export const typeArrayCreateIterator = type => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayForEachSnapshot = (type, f, snapshot) => {
|
||||
export const typeListForEachSnapshot = (type, f, snapshot) => {
|
||||
let index = 0
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
if (n.countable && isVisible(n, snapshot)) {
|
||||
const c = n.getContent()
|
||||
const c = n.content.getContent()
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
f(c[i], index++, type)
|
||||
}
|
||||
@@ -348,11 +346,11 @@ export const typeArrayForEachSnapshot = (type, f, snapshot) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayGet = (type, index) => {
|
||||
export const typeListGet = (type, index) => {
|
||||
for (let n = type._start; n !== null; n = n.right) {
|
||||
if (!n.deleted && n.countable) {
|
||||
if (index < n.length) {
|
||||
return n.getContent()[index]
|
||||
return n.content.getContent()[index]
|
||||
}
|
||||
index -= n.length
|
||||
}
|
||||
@@ -362,22 +360,22 @@ export const typeArrayGet = (type, index) => {
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {AbstractItem?} referenceItem
|
||||
* @param {Array<Object<string,any>|Array<any>|number|string|ArrayBuffer>} content
|
||||
* @param {Item?} referenceItem
|
||||
* @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
|
||||
export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
|
||||
let left = referenceItem
|
||||
const right = referenceItem === null ? parent._start : referenceItem.right
|
||||
/**
|
||||
* @type {Array<Object|Array|number>}
|
||||
* @type {Array<Object|Array<any>|number>}
|
||||
*/
|
||||
let jsonContent = []
|
||||
const packJsonContent = () => {
|
||||
if (jsonContent.length > 0) {
|
||||
left = new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, jsonContent)
|
||||
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentAny(jsonContent))
|
||||
left.integrate(transaction)
|
||||
jsonContent = []
|
||||
}
|
||||
@@ -386,6 +384,7 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
switch (c.constructor) {
|
||||
case Number:
|
||||
case Object:
|
||||
case Boolean:
|
||||
case Array:
|
||||
case String:
|
||||
jsonContent.push(c)
|
||||
@@ -393,15 +392,14 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
default:
|
||||
packJsonContent()
|
||||
switch (c.constructor) {
|
||||
case Uint8Array:
|
||||
case ArrayBuffer:
|
||||
// @ts-ignore c is definitely an ArrayBuffer
|
||||
left = new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, c)
|
||||
// @ts-ignore
|
||||
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
|
||||
left.integrate(transaction)
|
||||
break
|
||||
default:
|
||||
if (c instanceof AbstractType) {
|
||||
left = new ItemType(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, c)
|
||||
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentType(c))
|
||||
left.integrate(transaction)
|
||||
} else {
|
||||
throw new Error('Unexpected content type in insert operation')
|
||||
@@ -416,14 +414,14 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {number} index
|
||||
* @param {Array<Object<string,any>|Array<any>|number|string|ArrayBuffer>} content
|
||||
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayInsertGenerics = (transaction, parent, index, content) => {
|
||||
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
||||
if (index === 0) {
|
||||
return typeArrayInsertGenericsAfter(transaction, parent, null, content)
|
||||
return typeListInsertGenericsAfter(transaction, parent, null, content)
|
||||
}
|
||||
let n = parent._start
|
||||
for (; n !== null; n = n.right) {
|
||||
@@ -431,14 +429,14 @@ export const typeArrayInsertGenerics = (transaction, parent, index, content) =>
|
||||
if (index <= n.length) {
|
||||
if (index < n.length) {
|
||||
// insert in-between
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + index))
|
||||
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
|
||||
}
|
||||
break
|
||||
}
|
||||
index -= n.length
|
||||
}
|
||||
}
|
||||
return typeArrayInsertGenericsAfter(transaction, parent, n, content)
|
||||
return typeListInsertGenericsAfter(transaction, parent, n, content)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,14 +448,14 @@ export const typeArrayInsertGenerics = (transaction, parent, index, content) =>
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeArrayDelete = (transaction, parent, index, length) => {
|
||||
export const typeListDelete = (transaction, parent, index, length) => {
|
||||
if (length === 0) { return }
|
||||
let n = parent._start
|
||||
// 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, transaction.y.store, createID(n.id.client, n.id.clock + index))
|
||||
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
|
||||
}
|
||||
index -= n.length
|
||||
}
|
||||
@@ -466,7 +464,7 @@ export const typeArrayDelete = (transaction, parent, index, length) => {
|
||||
while (length > 0 && n !== null) {
|
||||
if (!n.deleted) {
|
||||
if (length < n.length) {
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + length))
|
||||
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
|
||||
}
|
||||
n.delete(transaction)
|
||||
length -= n.length
|
||||
@@ -497,52 +495,55 @@ export const typeMapDelete = (transaction, parent, key) => {
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @param {Object|number|Array<any>|string|ArrayBuffer|AbstractType<any>} value
|
||||
* @param {Object|number|Array<any>|string|Uint8Array|AbstractType<any>} value
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeMapSet = (transaction, parent, key, value) => {
|
||||
const left = parent._map.get(key) || null
|
||||
let content
|
||||
if (value == null) {
|
||||
new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, [value]).integrate(transaction)
|
||||
return
|
||||
}
|
||||
switch (value.constructor) {
|
||||
case Number:
|
||||
case Object:
|
||||
case Array:
|
||||
case String:
|
||||
new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, [value]).integrate(transaction)
|
||||
break
|
||||
case ArrayBuffer:
|
||||
new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, value).integrate(transaction)
|
||||
break
|
||||
default:
|
||||
if (value instanceof AbstractType) {
|
||||
new ItemType(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, value).integrate(transaction)
|
||||
} else {
|
||||
throw new Error('Unexpected content type')
|
||||
}
|
||||
content = new ContentAny([value])
|
||||
} else {
|
||||
switch (value.constructor) {
|
||||
case Number:
|
||||
case Object:
|
||||
case Boolean:
|
||||
case Array:
|
||||
case String:
|
||||
content = new ContentAny([value])
|
||||
break
|
||||
case Uint8Array:
|
||||
content = new ContentBinary(/** @type {Uint8Array} */ (value))
|
||||
break
|
||||
default:
|
||||
if (value instanceof AbstractType) {
|
||||
content = new ContentType(value)
|
||||
} else {
|
||||
throw new Error('Unexpected content type')
|
||||
}
|
||||
}
|
||||
}
|
||||
new Item(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, content).integrate(transaction)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined}
|
||||
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeMapGet = (parent, key) => {
|
||||
const val = parent._map.get(key)
|
||||
return val !== undefined && !val.deleted ? val.getContent()[0] : undefined
|
||||
return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} parent
|
||||
* @return {Object<string,Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined>}
|
||||
* @return {Object<string,Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@@ -551,10 +552,10 @@ export const typeMapGetAll = (parent) => {
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
let res = {}
|
||||
const res = {}
|
||||
for (const [key, value] of parent._map) {
|
||||
if (!value.deleted) {
|
||||
res[key] = value.getContent()[value.length - 1]
|
||||
res[key] = value.content.getContent()[value.length - 1]
|
||||
}
|
||||
}
|
||||
return res
|
||||
@@ -577,21 +578,21 @@ export const typeMapHas = (parent, key) => {
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {string} key
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined}
|
||||
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeMapGetSnapshot = (parent, key, snapshot) => {
|
||||
let v = parent._map.get(key) || null
|
||||
while (v !== null && (!snapshot.sm.has(v.id.client) || v.id.clock >= (snapshot.sm.get(v.id.client) || 0))) {
|
||||
while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
|
||||
v = v.left
|
||||
}
|
||||
return v !== null && isVisible(v, snapshot) ? v.getContent()[v.length - 1] : undefined
|
||||
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string,AbstractItem>} map
|
||||
* @param {Map<string,Item>} map
|
||||
* @return {IterableIterator<Array<any>>}
|
||||
*
|
||||
* @private
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
import {
|
||||
YEvent,
|
||||
AbstractType,
|
||||
typeArrayGet,
|
||||
typeArrayToArray,
|
||||
typeArrayForEach,
|
||||
typeArrayCreateIterator,
|
||||
typeArrayInsertGenerics,
|
||||
typeArrayDelete,
|
||||
typeArrayMap,
|
||||
typeListGet,
|
||||
typeListToArray,
|
||||
typeListForEach,
|
||||
typeListCreateIterator,
|
||||
typeListInsertGenerics,
|
||||
typeListDelete,
|
||||
typeListMap,
|
||||
YArrayRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
Y, Transaction, ItemType, // eslint-disable-line
|
||||
Doc, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
@@ -51,6 +51,7 @@ export class YArray extends AbstractType {
|
||||
*/
|
||||
this._prelimContent = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate this type into the Yjs instance.
|
||||
*
|
||||
@@ -58,20 +59,25 @@ export class YArray extends AbstractType {
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {ItemType} item
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item} item
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
// @ts-ignore
|
||||
this.insert(0, this._prelimContent)
|
||||
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
_copy () {
|
||||
return new YArray()
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates YArrayEvent and calls observers.
|
||||
*
|
||||
@@ -101,13 +107,12 @@ export class YArray extends AbstractType {
|
||||
* @param {Array<T>} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
typeArrayInsertGenerics(transaction, this, index, content)
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, content)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
this._prelimContent.splice(index, 0, ...content)
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,13 +132,12 @@ export class YArray extends AbstractType {
|
||||
* @param {number} length The number of elements to remove. Defaults to 1.
|
||||
*/
|
||||
delete (index, length = 1) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
typeArrayDelete(transaction, this, index, length)
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
this._prelimContent.splice(index, length)
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +148,7 @@ export class YArray extends AbstractType {
|
||||
* @return {T}
|
||||
*/
|
||||
get (index) {
|
||||
return typeArrayGet(this, index)
|
||||
return typeListGet(this, index)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +157,7 @@ export class YArray extends AbstractType {
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeArrayToArray(this)
|
||||
return typeListToArray(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,8 +179,7 @@ export class YArray extends AbstractType {
|
||||
* callback function
|
||||
*/
|
||||
map (f) {
|
||||
// @ts-ignore
|
||||
return typeArrayMap(this, f)
|
||||
return typeListMap(this, /** @type {any} */ (f))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,14 +188,14 @@ 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) {
|
||||
typeArrayForEach(this, f)
|
||||
typeListForEach(this, f)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {IterableIterator<T>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return typeArrayCreateIterator(this)
|
||||
return typeListCreateIterator(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
YMapRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
Y, Transaction, ItemType, // eslint-disable-line
|
||||
Doc, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -38,7 +38,7 @@ export class YMapEvent extends YEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T number|string|Object|Array|ArrayBuffer
|
||||
* @template T number|string|Object|Array|Uint8Array
|
||||
* A shared Map implementation.
|
||||
*
|
||||
* @extends AbstractType<YMapEvent<T>>
|
||||
@@ -53,6 +53,7 @@ export class YMap extends AbstractType {
|
||||
*/
|
||||
this._prelimContent = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate this type into the Yjs instance.
|
||||
*
|
||||
@@ -60,19 +61,23 @@ export class YMap extends AbstractType {
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {ItemType} item
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item} item
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
// @ts-ignore
|
||||
for (let [key, value] of this._prelimContent) {
|
||||
for (const [key, value] of /** @type {Map<string, any>} */ (this._prelimContent)) {
|
||||
this.set(key, value)
|
||||
}
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
_copy () {
|
||||
return new YMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates YMapEvent and calls observers.
|
||||
*
|
||||
@@ -95,9 +100,9 @@ export class YMap extends AbstractType {
|
||||
* @type {Object<string,T>}
|
||||
*/
|
||||
const map = {}
|
||||
for (let [key, item] of this._map) {
|
||||
for (const [key, item] of this._map) {
|
||||
if (!item.deleted) {
|
||||
const v = item.getContent()[0]
|
||||
const v = item.content.getContent()[item.length - 1]
|
||||
map[key] = v instanceof AbstractType ? v.toJSON() : v
|
||||
}
|
||||
}
|
||||
@@ -107,7 +112,7 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Returns the keys for each element in the YMap Type.
|
||||
*
|
||||
* @return {Iterator<string>}
|
||||
* @return {IterableIterator<string>}
|
||||
*/
|
||||
keys () {
|
||||
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[0])
|
||||
@@ -116,10 +121,10 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Returns the keys for each element in the YMap Type.
|
||||
*
|
||||
* @return {Iterator<string>}
|
||||
* @return {IterableIterator<string>}
|
||||
*/
|
||||
values () {
|
||||
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].getContent()[v[1].length - 1])
|
||||
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +133,25 @@ export class YMap extends AbstractType {
|
||||
* @return {IterableIterator<any>}
|
||||
*/
|
||||
entries () {
|
||||
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].getContent()[v[1].length - 1]])
|
||||
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]])
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function on once on overy key-value pair.
|
||||
*
|
||||
* @param {function(T,string,YMap<T>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
/**
|
||||
* @type {Object<string,T>}
|
||||
*/
|
||||
const map = {}
|
||||
for (const [key, item] of this._map) {
|
||||
if (!item.deleted) {
|
||||
f(item.content.getContent()[item.length - 1], key, this)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,13 +167,12 @@ export class YMap extends AbstractType {
|
||||
* @param {string} key The key of the element to remove.
|
||||
*/
|
||||
delete (key) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapDelete(transaction, this, key)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._prelimContent.delete(key)
|
||||
/** @type {Map<string, any>} */ (this._prelimContent).delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,13 +183,12 @@ export class YMap extends AbstractType {
|
||||
* @param {T} value The value of the element to add
|
||||
*/
|
||||
set (key, value) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, key, value)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._prelimContent.set(key, value)
|
||||
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -179,8 +200,7 @@ export class YMap extends AbstractType {
|
||||
* @return {T|undefined}
|
||||
*/
|
||||
get (key) {
|
||||
// @ts-ignore
|
||||
return typeMapGet(this, key)
|
||||
return /** @type {any} */ (typeMapGet(this, key))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
|
||||
import {
|
||||
YEvent,
|
||||
ItemEmbed,
|
||||
ItemString,
|
||||
ItemFormat,
|
||||
AbstractType,
|
||||
nextID,
|
||||
createID,
|
||||
@@ -16,16 +13,28 @@ import {
|
||||
YTextRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
Y, ItemType, AbstractItem, Snapshot, StructStore, Transaction // eslint-disable-line
|
||||
ContentEmbed,
|
||||
ContentFormat,
|
||||
ContentString,
|
||||
splitSnapshotAffectedStructs,
|
||||
ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as object from 'lib0/object.js'
|
||||
|
||||
/**
|
||||
* @param {any} a
|
||||
* @param {any} b
|
||||
* @return {boolean}
|
||||
*/
|
||||
const equalAttrs = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
|
||||
|
||||
export class ItemListPosition {
|
||||
/**
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
*/
|
||||
constructor (left, right) {
|
||||
this.left = left
|
||||
@@ -35,8 +44,8 @@ export class ItemListPosition {
|
||||
|
||||
export class ItemTextListPosition extends ItemListPosition {
|
||||
/**
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
*/
|
||||
constructor (left, right, currentAttributes) {
|
||||
@@ -47,8 +56,8 @@ export class ItemTextListPosition extends ItemListPosition {
|
||||
|
||||
export class ItemInsertionResult extends ItemListPosition {
|
||||
/**
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {Map<string,any>} negatedAttributes
|
||||
*/
|
||||
constructor (left, right, negatedAttributes) {
|
||||
@@ -59,33 +68,31 @@ export class ItemInsertionResult extends ItemListPosition {
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {number} count
|
||||
* @return {ItemTextListPosition}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const findNextPosition = (transaction, store, currentAttributes, left, right, count) => {
|
||||
const findNextPosition = (transaction, currentAttributes, left, right, count) => {
|
||||
while (right !== null && count > 0) {
|
||||
switch (right.constructor) {
|
||||
case ItemEmbed:
|
||||
case ItemString:
|
||||
switch (right.content.constructor) {
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
if (!right.deleted) {
|
||||
if (count < right.length) {
|
||||
// split right
|
||||
getItemCleanStart(transaction, store, createID(right.id.client, right.id.clock + count))
|
||||
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + count))
|
||||
}
|
||||
count -= right.length
|
||||
}
|
||||
break
|
||||
case ItemFormat:
|
||||
case ContentFormat:
|
||||
if (!right.deleted) {
|
||||
// @ts-ignore right is ItemFormat
|
||||
updateCurrentAttributes(currentAttributes, right)
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -97,7 +104,6 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {number} index
|
||||
* @return {ItemTextListPosition}
|
||||
@@ -105,11 +111,10 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const findPosition = (transaction, store, parent, index) => {
|
||||
let currentAttributes = new Map()
|
||||
let left = null
|
||||
let right = parent._start
|
||||
return findNextPosition(transaction, store, currentAttributes, left, right, index)
|
||||
const findPosition = (transaction, parent, index) => {
|
||||
const currentAttributes = new Map()
|
||||
const right = parent._start
|
||||
return findNextPosition(transaction, currentAttributes, null, right, index)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,8 +122,8 @@ const findPosition = (transaction, store, parent, index) => {
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {Map<string,any>} negatedAttributes
|
||||
* @return {ItemListPosition}
|
||||
*
|
||||
@@ -130,21 +135,19 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
|
||||
while (
|
||||
right !== null && (
|
||||
right.deleted === true || (
|
||||
right.constructor === ItemFormat &&
|
||||
// @ts-ignore right is ItemFormat
|
||||
(negatedAttributes.get(right.key) === right.value)
|
||||
right.content.constructor === ContentFormat &&
|
||||
equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (right.content).key), /** @type {ContentFormat} */ (right.content).value)
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (!right.deleted) {
|
||||
// @ts-ignore right is ItemFormat
|
||||
negatedAttributes.delete(right.key)
|
||||
negatedAttributes.delete(/** @type {ContentFormat} */ (right.content).key)
|
||||
}
|
||||
left = right
|
||||
right = right.right
|
||||
}
|
||||
for (let [key, val] of negatedAttributes) {
|
||||
left = new ItemFormat(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, key, val)
|
||||
for (const [key, val] of negatedAttributes) {
|
||||
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
|
||||
left.integrate(transaction)
|
||||
}
|
||||
return { left, right }
|
||||
@@ -152,14 +155,13 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
|
||||
|
||||
/**
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {ItemFormat} item
|
||||
* @param {ContentFormat} format
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const updateCurrentAttributes = (currentAttributes, item) => {
|
||||
const value = item.value
|
||||
const key = item.key
|
||||
const updateCurrentAttributes = (currentAttributes, format) => {
|
||||
const { key, value } = format
|
||||
if (value === null) {
|
||||
currentAttributes.delete(key)
|
||||
} else {
|
||||
@@ -168,8 +170,8 @@ const updateCurrentAttributes = (currentAttributes, item) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {Object<string,any>} attributes
|
||||
* @return {ItemListPosition}
|
||||
@@ -184,11 +186,9 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
|
||||
break
|
||||
} else if (right.deleted) {
|
||||
// continue
|
||||
// @ts-ignore right is ItemFormat
|
||||
} else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) {
|
||||
} else if (right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (right.content)).key] || null, /** @type {ContentFormat} */ (right.content).value)) {
|
||||
// found a format, update currentAttributes and continue
|
||||
// @ts-ignore right is ItemFormat
|
||||
updateCurrentAttributes(currentAttributes, right)
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -201,8 +201,8 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {Object<string,any>} attributes
|
||||
* @return {ItemInsertionResult}
|
||||
@@ -213,13 +213,13 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
|
||||
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
|
||||
const negatedAttributes = new Map()
|
||||
// insert format-start items
|
||||
for (let key in attributes) {
|
||||
for (const key in attributes) {
|
||||
const val = attributes[key]
|
||||
const currentVal = currentAttributes.get(key) || null
|
||||
if (currentVal !== val) {
|
||||
if (!equalAttrs(currentVal, val)) {
|
||||
// save negated attribute (set null if currentVal undefined)
|
||||
negatedAttributes.set(key, currentVal)
|
||||
left = new ItemFormat(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, key, val)
|
||||
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
|
||||
left.integrate(transaction)
|
||||
}
|
||||
}
|
||||
@@ -229,10 +229,10 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {string} text
|
||||
* @param {string|object} text
|
||||
* @param {Object<string,any>} attributes
|
||||
* @return {ItemListPosition}
|
||||
*
|
||||
@@ -240,7 +240,7 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
|
||||
* @function
|
||||
**/
|
||||
const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => {
|
||||
for (let [key] of currentAttributes) {
|
||||
for (const [key] of currentAttributes) {
|
||||
if (attributes[key] === undefined) {
|
||||
attributes[key] = null
|
||||
}
|
||||
@@ -250,11 +250,8 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
|
||||
left = insertPos.left
|
||||
right = insertPos.right
|
||||
// insert content
|
||||
if (text.constructor === String) {
|
||||
left = new ItemString(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, text)
|
||||
} else {
|
||||
left = new ItemEmbed(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, text)
|
||||
}
|
||||
const content = text.constructor === String ? new ContentString(text) : new ContentEmbed(text)
|
||||
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, content)
|
||||
left.integrate(transaction)
|
||||
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
|
||||
}
|
||||
@@ -262,8 +259,8 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {number} length
|
||||
* @param {Object<string,any>} attributes
|
||||
@@ -281,29 +278,26 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
|
||||
// iterate until first non-format or null is found
|
||||
// delete all formats with attributes[format.key] != null
|
||||
while (length > 0 && right !== null) {
|
||||
if (right.deleted === false) {
|
||||
switch (right.constructor) {
|
||||
case ItemFormat:
|
||||
// @ts-ignore right is ItemFormat
|
||||
const attr = attributes[right.key]
|
||||
if (!right.deleted) {
|
||||
switch (right.content.constructor) {
|
||||
case ContentFormat: {
|
||||
const { key, value } = /** @type {ContentFormat} */ (right.content)
|
||||
const attr = attributes[key]
|
||||
if (attr !== undefined) {
|
||||
// @ts-ignore right is ItemFormat
|
||||
if (attr === right.value) {
|
||||
// @ts-ignore right is ItemFormat
|
||||
negatedAttributes.delete(right.key)
|
||||
if (equalAttrs(attr, value)) {
|
||||
negatedAttributes.delete(key)
|
||||
} else {
|
||||
// @ts-ignore right is ItemFormat
|
||||
negatedAttributes.set(right.key, right.value)
|
||||
negatedAttributes.set(key, value)
|
||||
}
|
||||
right.delete(transaction)
|
||||
}
|
||||
// @ts-ignore right is ItemFormat
|
||||
updateCurrentAttributes(currentAttributes, right)
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
|
||||
break
|
||||
case ItemEmbed:
|
||||
case ItemString:
|
||||
}
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
if (length < right.length) {
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(right.id.client, right.id.clock + length))
|
||||
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
|
||||
}
|
||||
length -= right.length
|
||||
break
|
||||
@@ -312,13 +306,24 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
|
||||
left = right
|
||||
right = right.right
|
||||
}
|
||||
// Quill just assumes that the editor starts with a newline and that it always
|
||||
// ends with a newline. We only insert that newline when a new newline is
|
||||
// inserted - i.e when length is bigger than type.length
|
||||
if (length > 0) {
|
||||
let newlines = ''
|
||||
for (; length > 0; length--) {
|
||||
newlines += '\n'
|
||||
}
|
||||
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentString(newlines))
|
||||
left.integrate(transaction)
|
||||
}
|
||||
return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractItem|null} left
|
||||
* @param {AbstractItem|null} right
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {number} length
|
||||
* @return {ItemListPosition}
|
||||
@@ -329,15 +334,14 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
|
||||
const deleteText = (transaction, left, right, currentAttributes, length) => {
|
||||
while (length > 0 && right !== null) {
|
||||
if (right.deleted === false) {
|
||||
switch (right.constructor) {
|
||||
case ItemFormat:
|
||||
// @ts-ignore right is ItemFormat
|
||||
updateCurrentAttributes(currentAttributes, right)
|
||||
switch (right.content.constructor) {
|
||||
case ContentFormat:
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
|
||||
break
|
||||
case ItemEmbed:
|
||||
case ItemString:
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
if (length < right.length) {
|
||||
getItemCleanStart(transaction, transaction.y.store, createID(right.id.client, right.id.clock + length))
|
||||
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
|
||||
}
|
||||
length -= right.length
|
||||
right.delete(transaction)
|
||||
@@ -388,7 +392,7 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
|
||||
/**
|
||||
* Event that describes the changes on a YText type.
|
||||
*/
|
||||
class YTextEvent extends YEvent {
|
||||
export class YTextEvent extends YEvent {
|
||||
/**
|
||||
* @param {YText} ytext
|
||||
* @param {Transaction} transaction
|
||||
@@ -401,6 +405,7 @@ class YTextEvent extends YEvent {
|
||||
*/
|
||||
this._delta = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the changes in the delta format.
|
||||
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
||||
@@ -411,13 +416,10 @@ class YTextEvent extends YEvent {
|
||||
*/
|
||||
get delta () {
|
||||
if (this._delta === null) {
|
||||
const y = this.target._y
|
||||
// @ts-ignore
|
||||
const y = /** @type {Doc} */ (this.target.doc)
|
||||
this._delta = []
|
||||
transact(y, transaction => {
|
||||
/**
|
||||
* @type {Array<DeltaItem>}
|
||||
*/
|
||||
const delta = []
|
||||
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
|
||||
@@ -428,11 +430,13 @@ class YTextEvent extends YEvent {
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
let attributes = {} // counts added or removed new attributes for retain
|
||||
const attributes = {} // counts added or removed new attributes for retain
|
||||
/**
|
||||
* @type {string|object}
|
||||
*/
|
||||
let insert = ''
|
||||
let retain = 0
|
||||
let deleteLen = 0
|
||||
this._delta = delta
|
||||
const addOp = () => {
|
||||
if (action !== null) {
|
||||
/**
|
||||
@@ -448,7 +452,7 @@ class YTextEvent extends YEvent {
|
||||
op = { insert }
|
||||
if (currentAttributes.size > 0) {
|
||||
op.attributes = {}
|
||||
for (let [key, value] of currentAttributes) {
|
||||
for (const [key, value] of currentAttributes) {
|
||||
if (value !== null) {
|
||||
op.attributes[key] = value
|
||||
}
|
||||
@@ -460,7 +464,7 @@ class YTextEvent extends YEvent {
|
||||
op = { retain }
|
||||
if (Object.keys(attributes).length > 0) {
|
||||
op.attributes = {}
|
||||
for (let key in attributes) {
|
||||
for (const key in attributes) {
|
||||
op.attributes[key] = attributes[key]
|
||||
}
|
||||
}
|
||||
@@ -472,14 +476,15 @@ class YTextEvent extends YEvent {
|
||||
}
|
||||
}
|
||||
while (item !== null) {
|
||||
switch (item.constructor) {
|
||||
case ItemEmbed:
|
||||
switch (item.content.constructor) {
|
||||
case ContentEmbed:
|
||||
if (this.adds(item)) {
|
||||
addOp()
|
||||
action = 'insert'
|
||||
// @ts-ignore item is ItemFormat
|
||||
insert = item.embed
|
||||
addOp()
|
||||
if (!this.deletes(item)) {
|
||||
addOp()
|
||||
action = 'insert'
|
||||
insert = /** @type {ContentEmbed} */ (item.content).embed
|
||||
addOp()
|
||||
}
|
||||
} else if (this.deletes(item)) {
|
||||
if (action !== 'delete') {
|
||||
addOp()
|
||||
@@ -494,14 +499,15 @@ class YTextEvent extends YEvent {
|
||||
retain += 1
|
||||
}
|
||||
break
|
||||
case ItemString:
|
||||
case ContentString:
|
||||
if (this.adds(item)) {
|
||||
if (action !== 'insert') {
|
||||
addOp()
|
||||
action = 'insert'
|
||||
if (!this.deletes(item)) {
|
||||
if (action !== 'insert') {
|
||||
addOp()
|
||||
action = 'insert'
|
||||
}
|
||||
insert += /** @type {ContentString} */ (item.content).str
|
||||
}
|
||||
// @ts-ignore
|
||||
insert += item.string
|
||||
} else if (this.deletes(item)) {
|
||||
if (action !== 'delete') {
|
||||
addOp()
|
||||
@@ -516,57 +522,45 @@ class YTextEvent extends YEvent {
|
||||
retain += item.length
|
||||
}
|
||||
break
|
||||
case ItemFormat:
|
||||
case ContentFormat: {
|
||||
const { key, value } = /** @type {ContentFormat} */ (item.content)
|
||||
if (this.adds(item)) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
const curVal = currentAttributes.get(item.key) || null
|
||||
// @ts-ignore item is ItemFormat
|
||||
if (curVal !== item.value) {
|
||||
if (action === 'retain') {
|
||||
addOp()
|
||||
}
|
||||
// @ts-ignore item is ItemFormat
|
||||
if (item.value === (oldAttributes.get(item.key) || null)) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
delete attributes[item.key]
|
||||
} else {
|
||||
// @ts-ignore item is ItemFormat
|
||||
attributes[item.key] = item.value
|
||||
}
|
||||
} else {
|
||||
item.delete(transaction)
|
||||
}
|
||||
} else if (this.deletes(item)) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
oldAttributes.set(item.key, item.value)
|
||||
// @ts-ignore item is ItemFormat
|
||||
const curVal = currentAttributes.get(item.key) || null
|
||||
// @ts-ignore item is ItemFormat
|
||||
if (curVal !== item.value) {
|
||||
if (action === 'retain') {
|
||||
addOp()
|
||||
}
|
||||
// @ts-ignore item is ItemFormat
|
||||
attributes[item.key] = curVal
|
||||
}
|
||||
} else if (!item.deleted) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
oldAttributes.set(item.key, item.value)
|
||||
// @ts-ignore item is ItemFormat
|
||||
const attr = attributes[item.key]
|
||||
if (attr !== undefined) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
if (attr !== item.value) {
|
||||
if (!this.deletes(item)) {
|
||||
const curVal = currentAttributes.get(key) || null
|
||||
if (!equalAttrs(curVal, value)) {
|
||||
if (action === 'retain') {
|
||||
addOp()
|
||||
}
|
||||
// @ts-ignore item is ItemFormat
|
||||
if (item.value === null) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
attributes[item.key] = item.value
|
||||
if (equalAttrs(value, (oldAttributes.get(key) || null))) {
|
||||
delete attributes[key]
|
||||
} else {
|
||||
// @ts-ignore item is ItemFormat
|
||||
delete attributes[item.key]
|
||||
attributes[key] = value
|
||||
}
|
||||
} else {
|
||||
item.delete(transaction)
|
||||
}
|
||||
}
|
||||
} else if (this.deletes(item)) {
|
||||
oldAttributes.set(key, value)
|
||||
const curVal = currentAttributes.get(key) || null
|
||||
if (!equalAttrs(curVal, value)) {
|
||||
if (action === 'retain') {
|
||||
addOp()
|
||||
}
|
||||
attributes[key] = curVal
|
||||
}
|
||||
} else if (!item.deleted) {
|
||||
oldAttributes.set(key, value)
|
||||
const attr = attributes[key]
|
||||
if (attr !== undefined) {
|
||||
if (!equalAttrs(attr, value)) {
|
||||
if (action === 'retain') {
|
||||
addOp()
|
||||
}
|
||||
if (value === null) {
|
||||
attributes[key] = value
|
||||
} else {
|
||||
delete attributes[key]
|
||||
}
|
||||
} else {
|
||||
item.delete(transaction)
|
||||
@@ -577,26 +571,25 @@ class YTextEvent extends YEvent {
|
||||
if (action === 'insert') {
|
||||
addOp()
|
||||
}
|
||||
// @ts-ignore item is ItemFormat
|
||||
updateCurrentAttributes(currentAttributes, item)
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
item = item.right
|
||||
}
|
||||
addOp()
|
||||
while (this._delta.length > 0) {
|
||||
let lastOp = this._delta[this._delta.length - 1]
|
||||
while (delta.length > 0) {
|
||||
const lastOp = delta[delta.length - 1]
|
||||
if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
|
||||
// retain delta's if they don't assign attributes
|
||||
this._delta.pop()
|
||||
delta.pop()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// @ts-ignore _delta is defined above
|
||||
return this._delta
|
||||
}
|
||||
}
|
||||
@@ -629,22 +622,25 @@ export class YText extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {ItemType} item
|
||||
* @param {Doc} y
|
||||
* @param {Item} item
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
try {
|
||||
// @ts-ignore this._prelimContent is still defined
|
||||
this._pending.forEach(f => f())
|
||||
/** @type {Array<function>} */ (this._pending).forEach(f => f())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this._pending = null
|
||||
}
|
||||
|
||||
_copy () {
|
||||
return new YText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates YTextEvent and calls observers.
|
||||
*
|
||||
@@ -665,19 +661,28 @@ export class YText extends AbstractType {
|
||||
toString () {
|
||||
let str = ''
|
||||
/**
|
||||
* @type {AbstractItem|null}
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable && n.constructor === ItemString) {
|
||||
// @ts-ignore
|
||||
str += n.string
|
||||
if (!n.deleted && n.countable && n.content.constructor === ContentString) {
|
||||
str += /** @type {ContentString} */ (n.content).str
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unformatted string representation of this YText type.
|
||||
*
|
||||
* @return {string}
|
||||
* @public
|
||||
*/
|
||||
toJSON () {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a {@link Delta} on this shared YText type.
|
||||
*
|
||||
@@ -686,8 +691,8 @@ export class YText extends AbstractType {
|
||||
* @public
|
||||
*/
|
||||
applyDelta (delta) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
/**
|
||||
* @type {ItemListPosition}
|
||||
*/
|
||||
@@ -696,7 +701,15 @@ export class YText extends AbstractType {
|
||||
for (let i = 0; i < delta.length; i++) {
|
||||
const op = delta[i]
|
||||
if (op.insert !== undefined) {
|
||||
pos = insertText(transaction, this, pos.left, pos.right, currentAttributes, op.insert, op.attributes || {})
|
||||
// Quill assumes that the content starts with an empty paragraph.
|
||||
// Yjs/Y.Text assumes that it starts empty. We always hide that
|
||||
// there is a newline at the end of the content.
|
||||
// If we omit this step, clients will see a different number of
|
||||
// paragraphs, but nothing bad will happen.
|
||||
const ins = (typeof op.insert === 'string' && i === delta.length - 1 && pos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
|
||||
if (typeof ins !== 'string' || ins.length > 0) {
|
||||
pos = insertText(transaction, this, pos.left, pos.right, currentAttributes, ins, op.attributes || {})
|
||||
}
|
||||
} else if (op.retain !== undefined) {
|
||||
pos = formatText(transaction, this, pos.left, pos.right, currentAttributes, op.retain, op.attributes || {})
|
||||
} else if (op.delete !== undefined) {
|
||||
@@ -705,8 +718,7 @@ export class YText extends AbstractType {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._pending.push(() => this.applyDelta(delta))
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.applyDelta(delta))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,21 +727,19 @@ export class YText extends AbstractType {
|
||||
*
|
||||
* @param {Snapshot} [snapshot]
|
||||
* @param {Snapshot} [prevSnapshot]
|
||||
* @param {function('removed' | 'added', ID):any} [computeYChange]
|
||||
* @return {any} The Delta representation of this type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
toDelta (snapshot, prevSnapshot) {
|
||||
toDelta (snapshot, prevSnapshot, computeYChange) {
|
||||
/**
|
||||
* @type{Array<any>}
|
||||
*/
|
||||
const ops = []
|
||||
const currentAttributes = new Map()
|
||||
const doc = /** @type {Doc} */ (this.doc)
|
||||
let str = ''
|
||||
/**
|
||||
* @type {AbstractItem|null}
|
||||
*/
|
||||
// @ts-ignore
|
||||
let n = this._start
|
||||
function packStr () {
|
||||
if (str.length > 0) {
|
||||
@@ -739,7 +749,7 @@ export class YText extends AbstractType {
|
||||
*/
|
||||
const attributes = {}
|
||||
let addAttributes = false
|
||||
for (let [key, value] of currentAttributes) {
|
||||
for (const [key, value] of currentAttributes) {
|
||||
addAttributes = true
|
||||
attributes[key] = value
|
||||
}
|
||||
@@ -754,38 +764,55 @@ export class YText extends AbstractType {
|
||||
str = ''
|
||||
}
|
||||
}
|
||||
while (n !== null) {
|
||||
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
||||
switch (n.constructor) {
|
||||
case ItemString:
|
||||
const cur = currentAttributes.get('ychange')
|
||||
if (snapshot !== undefined && !isVisible(n, snapshot)) {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
|
||||
packStr()
|
||||
currentAttributes.set('ychange', { user: n.id.client, state: 'removed' })
|
||||
}
|
||||
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
|
||||
packStr()
|
||||
currentAttributes.set('ychange', { user: n.id.client, state: 'added' })
|
||||
}
|
||||
} else if (cur !== undefined) {
|
||||
packStr()
|
||||
currentAttributes.delete('ychange')
|
||||
}
|
||||
// @ts-ignore
|
||||
str += n.string
|
||||
break
|
||||
case ItemFormat:
|
||||
packStr()
|
||||
// @ts-ignore
|
||||
updateCurrentAttributes(currentAttributes, n)
|
||||
break
|
||||
}
|
||||
// snapshots are merged again after the transaction, so we need to keep the
|
||||
// transalive until we are done
|
||||
transact(doc, transaction => {
|
||||
if (snapshot) {
|
||||
splitSnapshotAffectedStructs(transaction, snapshot)
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
packStr()
|
||||
if (prevSnapshot) {
|
||||
splitSnapshotAffectedStructs(transaction, prevSnapshot)
|
||||
}
|
||||
while (n !== null) {
|
||||
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
||||
switch (n.content.constructor) {
|
||||
case ContentString: {
|
||||
const cur = currentAttributes.get('ychange')
|
||||
if (snapshot !== undefined && !isVisible(n, snapshot)) {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
|
||||
packStr()
|
||||
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
|
||||
}
|
||||
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
|
||||
packStr()
|
||||
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
|
||||
}
|
||||
} else if (cur !== undefined) {
|
||||
packStr()
|
||||
currentAttributes.delete('ychange')
|
||||
}
|
||||
str += /** @type {ContentString} */ (n.content).str
|
||||
break
|
||||
}
|
||||
case ContentEmbed:
|
||||
packStr()
|
||||
ops.push({
|
||||
insert: /** @type {ContentEmbed} */ (n.content).embed
|
||||
})
|
||||
break
|
||||
case ContentFormat:
|
||||
if (isVisible(n, snapshot)) {
|
||||
packStr()
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
packStr()
|
||||
}, splitSnapshotAffectedStructs)
|
||||
return ops
|
||||
}
|
||||
|
||||
@@ -794,24 +821,28 @@ export class YText extends AbstractType {
|
||||
*
|
||||
* @param {number} index The index at which to start inserting.
|
||||
* @param {String} text The text to insert at the specified position.
|
||||
* @param {TextAttributes} attributes Optionally define some formatting
|
||||
* @param {TextAttributes} [attributes] Optionally define some formatting
|
||||
* information to apply on the inserted
|
||||
* Text.
|
||||
* @public
|
||||
*/
|
||||
insert (index, text, attributes = {}) {
|
||||
insert (index, text, attributes) {
|
||||
if (text.length <= 0) {
|
||||
return
|
||||
}
|
||||
const y = this._y
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
|
||||
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||
if (!attributes) {
|
||||
attributes = {}
|
||||
// @ts-ignore
|
||||
currentAttributes.forEach((v, k) => { attributes[k] = v })
|
||||
}
|
||||
insertText(transaction, this, left, right, currentAttributes, text, attributes)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._pending.push(() => this.insert(index, text, attributes))
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -829,15 +860,14 @@ export class YText extends AbstractType {
|
||||
if (embed.constructor !== Object) {
|
||||
throw new Error('Embed must be an Object')
|
||||
}
|
||||
const y = this._y
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
|
||||
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||
insertText(transaction, this, left, right, currentAttributes, embed, attributes)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._pending.push(() => this.insertEmbed(index, embed, attributes))
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,15 +883,14 @@ export class YText extends AbstractType {
|
||||
if (length === 0) {
|
||||
return
|
||||
}
|
||||
const y = this._y
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
|
||||
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||
deleteText(transaction, left, right, currentAttributes, length)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._pending.push(() => this.delete(index, length))
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,18 +905,17 @@ export class YText extends AbstractType {
|
||||
* @public
|
||||
*/
|
||||
format (index, length, attributes) {
|
||||
const y = this._y
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
|
||||
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||
if (right === null) {
|
||||
return
|
||||
}
|
||||
formatText(transaction, this, left, right, currentAttributes, length, attributes)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._pending.push(() => this.format(index, length, attributes))
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
typeMapSet,
|
||||
typeMapGet,
|
||||
typeMapGetAll,
|
||||
typeArrayForEach,
|
||||
typeListForEach,
|
||||
YXmlElementRefID,
|
||||
Snapshot, Y, ItemType // eslint-disable-line
|
||||
Snapshot, Doc, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -24,7 +24,7 @@ import * as decoding from 'lib0/decoding.js'
|
||||
export class YXmlElement extends YXmlFragment {
|
||||
constructor (nodeName = 'UNDEFINED') {
|
||||
super()
|
||||
this.nodeName = nodeName.toUpperCase()
|
||||
this.nodeName = nodeName
|
||||
/**
|
||||
* @type {Map<string, any>|null}
|
||||
* @private
|
||||
@@ -39,20 +39,16 @@ export class YXmlElement extends YXmlFragment {
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {ItemType} item
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item} item
|
||||
* @private
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
// @ts-ignore
|
||||
this.insert(0, this._prelimContent)
|
||||
this._prelimContent = null
|
||||
// @ts-ignore
|
||||
this._prelimAttrs.forEach((value, key) => {
|
||||
;(/** @type {Map<string, any>} */ (this._prelimAttrs)).forEach((value, key) => {
|
||||
this.setAttribute(key, value)
|
||||
})
|
||||
this._prelimContent = null
|
||||
this._prelimAttrs = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,7 +74,7 @@ export class YXmlElement extends YXmlFragment {
|
||||
const attrs = this.getAttributes()
|
||||
const stringBuilder = []
|
||||
const keys = []
|
||||
for (let key in attrs) {
|
||||
for (const key in attrs) {
|
||||
keys.push(key)
|
||||
}
|
||||
keys.sort()
|
||||
@@ -100,13 +96,12 @@ export class YXmlElement extends YXmlFragment {
|
||||
* @public
|
||||
*/
|
||||
removeAttribute (attributeName) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapDelete(transaction, this, attributeName)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._prelimAttrs.delete(attributeName)
|
||||
/** @type {Map<string,any>} */ (this._prelimAttrs).delete(attributeName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,13 +114,12 @@ export class YXmlElement extends YXmlFragment {
|
||||
* @public
|
||||
*/
|
||||
setAttribute (attributeName, attributeValue) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, attributeName, attributeValue)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this._prelimAttrs.set(attributeName, attributeValue)
|
||||
/** @type {Map<string, any>} */ (this._prelimAttrs).set(attributeName, attributeValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,15 +133,14 @@ export class YXmlElement extends YXmlFragment {
|
||||
* @public
|
||||
*/
|
||||
getAttribute (attributeName) {
|
||||
// @ts-ignore
|
||||
return typeMapGet(this, attributeName)
|
||||
return /** @type {any} */ (typeMapGet(this, attributeName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all attribute name/value pairs in a JSON Object.
|
||||
*
|
||||
* @param {Snapshot} [snapshot]
|
||||
* @return {Object} A JSON Object that describes the attributes.
|
||||
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -172,11 +165,11 @@ export class YXmlElement extends YXmlFragment {
|
||||
*/
|
||||
toDOM (_document = document, hooks = {}, binding) {
|
||||
const dom = _document.createElement(this.nodeName)
|
||||
let attrs = this.getAttributes()
|
||||
for (let key in attrs) {
|
||||
const attrs = this.getAttributes()
|
||||
for (const key in attrs) {
|
||||
dom.setAttribute(key, attrs[key])
|
||||
}
|
||||
typeArrayForEach(this, yxml => {
|
||||
typeListForEach(this, yxml => {
|
||||
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
||||
})
|
||||
if (binding !== undefined) {
|
||||
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
YXmlEvent,
|
||||
YXmlElement,
|
||||
AbstractType,
|
||||
typeArrayMap,
|
||||
typeArrayForEach,
|
||||
typeArrayInsertGenerics,
|
||||
typeArrayDelete,
|
||||
typeArrayToArray,
|
||||
typeListMap,
|
||||
typeListForEach,
|
||||
typeListInsertGenerics,
|
||||
typeListDelete,
|
||||
typeListToArray,
|
||||
YXmlFragmentRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||
Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -59,16 +59,16 @@ export class YXmlTreeWalker {
|
||||
this._filter = f
|
||||
this._root = root
|
||||
/**
|
||||
* @type {ItemType | null}
|
||||
* @type {Item}
|
||||
*/
|
||||
// @ts-ignore
|
||||
this._currentNode = root._start
|
||||
this._currentNode = /** @type {Item} */ (root._start)
|
||||
this._firstCall = true
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next node.
|
||||
*
|
||||
@@ -77,18 +77,21 @@ export class YXmlTreeWalker {
|
||||
* @public
|
||||
*/
|
||||
next () {
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let n = this._currentNode
|
||||
if (n !== null && (!this._firstCall || n.deleted || !this._filter(n.type))) { // if first call, we check if we can use the first item
|
||||
let type = /** @type {ContentType} */ (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 {
|
||||
if (!n.deleted && (n.type.constructor === YXmlElement || n.type.constructor === YXmlFragment) && n.type._start !== null) {
|
||||
type = /** @type {ContentType} */ (n.content).type
|
||||
if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) {
|
||||
// walk down in the tree
|
||||
// @ts-ignore
|
||||
n = n.type._start
|
||||
n = type._start
|
||||
} else {
|
||||
// walk right or up in the tree
|
||||
while (n !== null) {
|
||||
if (n.right !== null) {
|
||||
// @ts-ignore
|
||||
n = n.right
|
||||
break
|
||||
} else if (n.parent === this._root) {
|
||||
@@ -98,16 +101,15 @@ export class YXmlTreeWalker {
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (n !== null && (n.deleted || !this._filter(n.type)))
|
||||
} while (n !== null && (n.deleted || !this._filter(/** @type {ContentType} */ (n.content).type)))
|
||||
}
|
||||
this._firstCall = false
|
||||
this._currentNode = n
|
||||
if (n === null) {
|
||||
// @ts-ignore return undefined if done=true (the expected result)
|
||||
// @ts-ignore
|
||||
return { value: undefined, done: true }
|
||||
}
|
||||
// @ts-ignore
|
||||
return { value: n.type, done: false }
|
||||
this._currentNode = n
|
||||
return { value: /** @type {any} */ (n.content).type, done: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +131,32 @@ export class YXmlFragment extends AbstractType {
|
||||
*/
|
||||
this._prelimContent = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate this type into the Yjs instance.
|
||||
*
|
||||
* * Save this struct in the os
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item} item
|
||||
* @private
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
_copy () {
|
||||
return new YXmlFragment()
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subtree of childNodes.
|
||||
*
|
||||
@@ -168,7 +196,7 @@ export class YXmlFragment extends AbstractType {
|
||||
querySelector (query) {
|
||||
query = query.toUpperCase()
|
||||
// @ts-ignore
|
||||
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
|
||||
const iterator = new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query)
|
||||
const next = iterator.next()
|
||||
if (next.done) {
|
||||
return null
|
||||
@@ -191,7 +219,7 @@ export class YXmlFragment extends AbstractType {
|
||||
querySelectorAll (query) {
|
||||
query = query.toUpperCase()
|
||||
// @ts-ignore
|
||||
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
|
||||
return Array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,9 +239,12 @@ export class YXmlFragment extends AbstractType {
|
||||
* @return {string} The string representation of all children.
|
||||
*/
|
||||
toString () {
|
||||
return typeArrayMap(this, xml => xml.toString()).join('')
|
||||
return typeListMap(this, xml => xml.toString()).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
toJSON () {
|
||||
return this.toString()
|
||||
}
|
||||
@@ -238,7 +269,7 @@ export class YXmlFragment extends AbstractType {
|
||||
if (binding !== undefined) {
|
||||
binding._createAssociation(fragment, this)
|
||||
}
|
||||
typeArrayForEach(this, xmlType => {
|
||||
typeListForEach(this, xmlType => {
|
||||
fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null)
|
||||
})
|
||||
return fragment
|
||||
@@ -255,9 +286,9 @@ export class YXmlFragment extends AbstractType {
|
||||
* @param {Array<YXmlElement|YXmlText>} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
typeArrayInsertGenerics(transaction, this, index, content)
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, content)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
@@ -272,23 +303,25 @@ export class YXmlFragment extends AbstractType {
|
||||
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
|
||||
*/
|
||||
delete (index, length = 1) {
|
||||
if (this._y !== null) {
|
||||
transact(this._y, transaction => {
|
||||
typeArrayDelete(transaction, this, index, length)
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
this._prelimContent.splice(index, length)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
*
|
||||
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeArrayToArray(this)
|
||||
return typeListToArray(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the properties of this type to binary and write it to an
|
||||
* BinaryEncoder.
|
||||
|
||||
@@ -9,6 +9,10 @@ import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
* simple formatting information like bold and italic.
|
||||
*/
|
||||
export class YXmlText extends YText {
|
||||
_copy () {
|
||||
return new YXmlText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Dom Element that mirrors this YXmlText.
|
||||
*
|
||||
@@ -36,9 +40,9 @@ export class YXmlText extends YText {
|
||||
// @ts-ignore
|
||||
return this.toDelta().map(delta => {
|
||||
const nestedNodes = []
|
||||
for (let nodeName in delta.attributes) {
|
||||
for (const nodeName in delta.attributes) {
|
||||
const attrs = []
|
||||
for (let key in delta.attributes[nodeName]) {
|
||||
for (const key in delta.attributes[nodeName]) {
|
||||
attrs.push({ key, value: delta.attributes[nodeName][key] })
|
||||
}
|
||||
// sort attributes to get a unique order
|
||||
@@ -53,7 +57,7 @@ export class YXmlText extends YText {
|
||||
const node = nestedNodes[i]
|
||||
str += `<${node.nodeName}`
|
||||
for (let j = 0; j < node.attrs.length; j++) {
|
||||
const attr = node.attrs[i]
|
||||
const attr = node.attrs[j]
|
||||
str += ` ${attr.key}="${attr.value}"`
|
||||
}
|
||||
str += '>'
|
||||
@@ -66,6 +70,9 @@ export class YXmlText extends YText {
|
||||
}).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
toJSON () {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
@@ -3,15 +3,18 @@ import {
|
||||
findIndexSS,
|
||||
createID,
|
||||
getState,
|
||||
AbstractStruct, AbstractItem, StructStore, Transaction, ID // eslint-disable-line
|
||||
splitItem,
|
||||
iterateStructs,
|
||||
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'
|
||||
|
||||
class DeleteItem {
|
||||
export class DeleteItem {
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
@@ -46,29 +49,20 @@ export class DeleteSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over all structs that were deleted.
|
||||
*
|
||||
* This function expects that the deletes structs are not deleted. Hence, you can
|
||||
* probably only use it in type observes and `afterTransaction` events. But not
|
||||
* in `afterTransactionCleanup`.
|
||||
* Iterate over all structs that the DeleteSet gc's.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {DeleteSet} ds
|
||||
* @param {StructStore} store
|
||||
* @param {function(AbstractStruct):void} f
|
||||
* @param {function(GC|Item):void} f
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const iterateDeletedStructs = (ds, store, f) =>
|
||||
export const iterateDeletedStructs = (transaction, ds, f) =>
|
||||
ds.clients.forEach((deletes, clientid) => {
|
||||
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(clientid))
|
||||
const structs = /** @type {Array<GC|Item>} */ (transaction.doc.store.clients.get(clientid))
|
||||
for (let i = 0; i < deletes.length; i++) {
|
||||
const del = deletes[i]
|
||||
let index = findIndexSS(structs, del.clock)
|
||||
let struct
|
||||
do {
|
||||
struct = structs[index++]
|
||||
f(struct)
|
||||
} while (index < structs.length && structs[index].id.clock < del.clock + del.len)
|
||||
iterateStructs(transaction, structs, del.clock, del.len, f)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -142,6 +136,32 @@ export const sortAndMergeDeleteSet = ds => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<DeleteSet>} dss
|
||||
* @return {DeleteSet} A fresh DeleteSet
|
||||
*/
|
||||
export const mergeDeleteSets = dss => {
|
||||
const merged = new DeleteSet()
|
||||
for (let dssI = 0; dssI < dss.length; dssI++) {
|
||||
dss[dssI].clients.forEach((delsLeft, client) => {
|
||||
if (!merged.clients.has(client)) {
|
||||
// Write all missing keys from current ds and all following.
|
||||
// If merged already contains `client` current ds has already been added.
|
||||
/**
|
||||
* @type {Array<DeleteItem>}
|
||||
*/
|
||||
const dels = delsLeft.slice()
|
||||
for (let i = dssI + 1; i < dss.length; i++) {
|
||||
array.appendTo(dels, dss[i].clients.get(client) || [])
|
||||
}
|
||||
merged.clients.set(client, dels)
|
||||
}
|
||||
})
|
||||
}
|
||||
sortAndMergeDeleteSet(merged)
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds
|
||||
* @param {ID} id
|
||||
@@ -154,6 +174,8 @@ export const addToDeleteSet = (ds, id, length) => {
|
||||
map.setIfUndefined(ds.clients, id.client, () => []).push(new DeleteItem(id.clock, length))
|
||||
}
|
||||
|
||||
export const createDeleteSet = () => new DeleteSet()
|
||||
|
||||
/**
|
||||
* @param {StructStore} ss
|
||||
* @return {DeleteSet} Merged and sorted DeleteSet
|
||||
@@ -162,7 +184,7 @@ export const addToDeleteSet = (ds, id, length) => {
|
||||
* @function
|
||||
*/
|
||||
export const createDeleteSetFromStructStore = ss => {
|
||||
const ds = new DeleteSet()
|
||||
const ds = createDeleteSet()
|
||||
ss.clients.forEach((structs, client) => {
|
||||
/**
|
||||
* @type {Array<DeleteItem>}
|
||||
@@ -209,6 +231,26 @@ export const writeDeleteSet = (encoder, ds) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {DeleteSet}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readDeleteSet = decoder => {
|
||||
const ds = new DeleteSet()
|
||||
const numClients = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < numClients; i++) {
|
||||
const client = decoding.readVarUint(decoder)
|
||||
const numberOfDeletes = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < numberOfDeletes; i++) {
|
||||
addToDeleteSet(ds, createID(client, decoding.readVarUint(decoder)), decoding.readVarUint(decoder))
|
||||
}
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Transaction} transaction
|
||||
@@ -217,7 +259,7 @@ export const writeDeleteSet = (encoder, ds) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readDeleteSet = (decoder, transaction, store) => {
|
||||
export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
||||
const unappliedDS = new DeleteSet()
|
||||
const numClients = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < numClients; i++) {
|
||||
@@ -235,13 +277,13 @@ export const readDeleteSet = (decoder, transaction, store) => {
|
||||
let index = findIndexSS(structs, clock)
|
||||
/**
|
||||
* We can ignore the case of GC and Delete structs, because we are going to skip them
|
||||
* @type {AbstractItem}
|
||||
* @type {Item}
|
||||
*/
|
||||
// @ts-ignore
|
||||
let struct = structs[index]
|
||||
// split the first item if necessary
|
||||
if (!struct.deleted && struct.id.clock < clock) {
|
||||
structs.splice(index + 1, 0, struct.splitAt(transaction, clock - struct.id.clock))
|
||||
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
|
||||
index++ // increase we now want to use the next struct
|
||||
}
|
||||
while (index < structs.length) {
|
||||
@@ -250,7 +292,7 @@ export const readDeleteSet = (decoder, transaction, store) => {
|
||||
if (struct.id.clock < clock + len) {
|
||||
if (!struct.deleted) {
|
||||
if (clock + len < struct.id.clock + struct.length) {
|
||||
structs.splice(index, 0, struct.splitAt(transaction, clock + len - struct.id.clock))
|
||||
structs.splice(index, 0, splitItem(transaction, struct, clock + len - struct.id.clock))
|
||||
}
|
||||
struct.delete(transaction)
|
||||
}
|
||||
@@ -264,8 +306,9 @@ export const readDeleteSet = (decoder, transaction, store) => {
|
||||
}
|
||||
}
|
||||
if (unappliedDS.clients.size > 0) {
|
||||
// TODO: no need for encoding+decoding ds anymore
|
||||
const unappliedDSEncoder = encoding.createEncoder()
|
||||
writeDeleteSet(unappliedDSEncoder, unappliedDS)
|
||||
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toBuffer(unappliedDSEncoder)))
|
||||
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toUint8Array(unappliedDSEncoder)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,26 +10,27 @@ import {
|
||||
YMap,
|
||||
YXmlFragment,
|
||||
transact,
|
||||
Transaction, YEvent // eslint-disable-line
|
||||
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'
|
||||
|
||||
// @todo rename to shared document
|
||||
|
||||
/**
|
||||
* A Yjs instance handles the state of shared data.
|
||||
* @extends Observable<string>
|
||||
*/
|
||||
export class Y extends Observable {
|
||||
export class Doc extends Observable {
|
||||
/**
|
||||
* @param {Object|undefined} conf configuration
|
||||
* @param {Object} conf configuration
|
||||
* @param {boolean} [conf.gc] Disable garbage collection (default: gc=true)
|
||||
* @param {function(Item):boolean} [conf.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
|
||||
*/
|
||||
constructor (conf = {}) {
|
||||
constructor ({ gc = true, gcFilter = () => true } = {}) {
|
||||
super()
|
||||
// todo: change to clientId
|
||||
this.gc = gc
|
||||
this.gcFilter = gcFilter
|
||||
this.clientID = random.uint32()
|
||||
/**
|
||||
* @type {Map<string, AbstractType<YEvent>>}
|
||||
@@ -47,6 +48,7 @@ export class Y extends Observable {
|
||||
*/
|
||||
this._transactionCleanups = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes that happen inside of a transaction are bundled. This means that
|
||||
* the observer fires _after_ the transaction is finished and that all changes
|
||||
@@ -61,6 +63,7 @@ export class Y extends Observable {
|
||||
transact (f, origin = null) {
|
||||
transact(this, f, origin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a shared data type.
|
||||
*
|
||||
@@ -82,7 +85,7 @@ export class Y extends Observable {
|
||||
* }
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Function} TypeConstructor The constructor of the type definition
|
||||
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
||||
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
|
||||
*
|
||||
* @public
|
||||
@@ -97,9 +100,18 @@ export class Y extends Observable {
|
||||
const Constr = type.constructor
|
||||
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
|
||||
if (Constr === AbstractType) {
|
||||
const t = new Constr()
|
||||
// @ts-ignore
|
||||
const t = new TypeConstructor()
|
||||
t._map = type._map
|
||||
type._map.forEach(/** @param {Item?} n */ n => {
|
||||
for (; n !== null; n = n.left) {
|
||||
n.parent = t
|
||||
}
|
||||
})
|
||||
t._start = type._start
|
||||
for (let n = t._start; n !== null; n = n.right) {
|
||||
n.parent = t
|
||||
}
|
||||
t._length = type._length
|
||||
this.share.set(name, t)
|
||||
t._integrate(this, null)
|
||||
@@ -110,6 +122,7 @@ export class Y extends Observable {
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {string} name
|
||||
@@ -121,6 +134,7 @@ export class Y extends Observable {
|
||||
// @ts-ignore
|
||||
return this.get(name, YArray)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {YText}
|
||||
@@ -131,6 +145,7 @@ export class Y extends Observable {
|
||||
// @ts-ignore
|
||||
return this.get(name, YText)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {YMap<any>}
|
||||
@@ -141,6 +156,7 @@ export class Y extends Observable {
|
||||
// @ts-ignore
|
||||
return this.get(name, YMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {YXmlFragment}
|
||||
@@ -151,6 +167,7 @@ export class Y extends Observable {
|
||||
// @ts-ignore
|
||||
return this.get(name, YXmlFragment)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit `destroy` event and unregister all event handlers.
|
||||
*
|
||||
@@ -160,6 +177,7 @@ export class Y extends Observable {
|
||||
this.emit('destroyed', [true])
|
||||
super.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} eventName
|
||||
* @param {function} f
|
||||
@@ -167,6 +185,7 @@ export class Y extends Observable {
|
||||
on (eventName, f) {
|
||||
super.on(eventName, f)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} eventName
|
||||
* @param {function} f
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { AbstractType } from '../internals' // eslint-disable-line
|
||||
import { AbstractType } from '../internals.js' // eslint-disable-line
|
||||
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -22,16 +22,6 @@ export class ID {
|
||||
*/
|
||||
this.clock = clock
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* @todo remove and adapt relative position implementation
|
||||
*/
|
||||
toJSON () {
|
||||
return {
|
||||
client: this.client,
|
||||
clock: this.clock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +81,7 @@ export const readID = decoder =>
|
||||
*/
|
||||
export const findRootTypeKey = type => {
|
||||
// @ts-ignore _y must be defined, otherwise unexpected case
|
||||
for (let [key, value] of type._y.share) {
|
||||
for (const [key, value] of type.doc.share) {
|
||||
if (value === type) {
|
||||
return key
|
||||
}
|
||||
|
||||
142
src/utils/PermanentUserData.js
Normal file
142
src/utils/PermanentUserData.js
Normal file
@@ -0,0 +1,142 @@
|
||||
|
||||
import {
|
||||
YArray,
|
||||
YMap,
|
||||
readDeleteSet,
|
||||
writeDeleteSet,
|
||||
createDeleteSet,
|
||||
ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import { mergeDeleteSets, isDeleted } from './DeleteSet.js'
|
||||
|
||||
export class PermanentUserData {
|
||||
/**
|
||||
* @param {Doc} doc
|
||||
* @param {YMap<any>} [storeType]
|
||||
*/
|
||||
constructor (doc, storeType = doc.getMap('users')) {
|
||||
/**
|
||||
* @type {Map<string,DeleteSet>}
|
||||
*/
|
||||
const dss = new Map()
|
||||
this.yusers = storeType
|
||||
this.doc = doc
|
||||
/**
|
||||
* Maps from clientid to userDescription
|
||||
*
|
||||
* @type {Map<number,string>}
|
||||
*/
|
||||
this.clients = new Map()
|
||||
this.dss = dss
|
||||
/**
|
||||
* @param {YMap<any>} user
|
||||
* @param {string} userDescription
|
||||
*/
|
||||
const initUser = (user, userDescription) => {
|
||||
/**
|
||||
* @type {YArray<Uint8Array>}
|
||||
*/
|
||||
const ds = user.get('ds')
|
||||
const ids = user.get('ids')
|
||||
const addClientId = /** @param {number} clientid */ clientid => this.clients.set(clientid, userDescription)
|
||||
ds.observe(/** @param {YArrayEvent<any>} event */ event => {
|
||||
event.changes.added.forEach(item => {
|
||||
item.content.getContent().forEach(encodedDs => {
|
||||
if (encodedDs instanceof Uint8Array) {
|
||||
this.dss.set(userDescription, mergeDeleteSets([this.dss.get(userDescription) || createDeleteSet(), readDeleteSet(decoding.createDecoder(encodedDs))]))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
this.dss.set(userDescription, mergeDeleteSets(ds.map(encodedDs => readDeleteSet(decoding.createDecoder(encodedDs)))))
|
||||
ids.observe(/** @param {YArrayEvent<any>} event */ event =>
|
||||
event.changes.added.forEach(item => item.content.getContent().forEach(addClientId))
|
||||
)
|
||||
ids.forEach(addClientId)
|
||||
}
|
||||
// observe users
|
||||
storeType.observe(event => {
|
||||
event.keysChanged.forEach(userDescription =>
|
||||
initUser(storeType.get(userDescription), userDescription)
|
||||
)
|
||||
})
|
||||
// add intial data
|
||||
storeType.forEach(initUser)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Doc} doc
|
||||
* @param {number} clientid
|
||||
* @param {string} userDescription
|
||||
* @param {Object} [conf]
|
||||
* @param {function(Transaction, DeleteSet):boolean} [conf.filter]
|
||||
*/
|
||||
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
|
||||
const users = this.yusers
|
||||
let user = users.get(userDescription)
|
||||
if (!user) {
|
||||
user = new YMap()
|
||||
user.set('ids', new YArray())
|
||||
user.set('ds', new YArray())
|
||||
users.set(userDescription, user)
|
||||
}
|
||||
user.get('ids').push([clientid])
|
||||
users.observe(event => {
|
||||
setTimeout(() => {
|
||||
const userOverwrite = users.get(userDescription)
|
||||
if (userOverwrite !== user) {
|
||||
// user was overwritten, port all data over to the next user object
|
||||
// @todo Experiment with Y.Sets here
|
||||
user = userOverwrite
|
||||
// @todo iterate over old type
|
||||
this.clients.forEach((_userDescription, clientid) => {
|
||||
if (userDescription === _userDescription) {
|
||||
user.get('ids').push([clientid])
|
||||
}
|
||||
})
|
||||
const encoder = encoding.createEncoder()
|
||||
const ds = this.dss.get(userDescription)
|
||||
if (ds) {
|
||||
writeDeleteSet(encoder, ds)
|
||||
user.get('ds').push([encoding.toUint8Array(encoder)])
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
|
||||
setTimeout(() => {
|
||||
const yds = user.get('ds')
|
||||
const ds = transaction.deleteSet
|
||||
if (transaction.local && ds.clients.size > 0 && filter(transaction, ds)) {
|
||||
const encoder = encoding.createEncoder()
|
||||
writeDeleteSet(encoder, ds)
|
||||
yds.push([encoding.toUint8Array(encoder)])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} clientid
|
||||
* @return {any}
|
||||
*/
|
||||
getUserByClientId (clientid) {
|
||||
return this.clients.get(clientid) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @return {string | null}
|
||||
*/
|
||||
getUserByDeletedId (id) {
|
||||
for (const [userDescription, ds] of this.dss) {
|
||||
if (isDeleted(ds, id)) {
|
||||
return userDescription
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
/**
|
||||
* @module Cursors
|
||||
*/
|
||||
|
||||
import {
|
||||
getItem,
|
||||
getItemType,
|
||||
createID,
|
||||
writeID,
|
||||
readID,
|
||||
compareIDs,
|
||||
getState,
|
||||
findRootTypeKey,
|
||||
AbstractItem,
|
||||
ItemType,
|
||||
ID, StructStore, Y, AbstractType // eslint-disable-line
|
||||
Item,
|
||||
ContentType,
|
||||
followRedone,
|
||||
ID, Doc, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -21,29 +17,30 @@ import * as decoding from 'lib0/decoding.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
|
||||
/**
|
||||
* A Cursor is a relative position that is based on the Yjs model. In contrast to an
|
||||
* absolute position (position by index), the Cursor can be
|
||||
* recomputed when remote changes are received. For example:
|
||||
* A relative position is based on the Yjs model and is not affected by document changes.
|
||||
* E.g. If you place a relative position before a certain character, it will always point to this character.
|
||||
* If you place a relative position at the end of a type, it will always point to the end of the type.
|
||||
*
|
||||
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the cursor position.
|
||||
* A numeric position is often unsuited for user selections, because it does not change when content is inserted
|
||||
* before or after.
|
||||
*
|
||||
* A relative cursor position can be obtained with the function
|
||||
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position.
|
||||
*
|
||||
* One of the properties must be defined.
|
||||
*
|
||||
* @example
|
||||
* // Current cursor position is at position 10
|
||||
* const relativePosition = createCursorFromOffset(yText, 10)
|
||||
* const relativePosition = createRelativePositionFromIndex(yText, 10)
|
||||
* // modify yText
|
||||
* yText.insert(0, 'abc')
|
||||
* yText.delete(3, 10)
|
||||
* // Compute the cursor position
|
||||
* const absolutePosition = toAbsolutePosition(y, relativePosition)
|
||||
* const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition)
|
||||
* absolutePosition.type === yText // => true
|
||||
* console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3
|
||||
* console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3
|
||||
*
|
||||
*/
|
||||
export class Cursor {
|
||||
export class RelativePosition {
|
||||
/**
|
||||
* @param {ID|null} type
|
||||
* @param {string|null} tname
|
||||
@@ -63,35 +60,22 @@ export class Cursor {
|
||||
*/
|
||||
this.item = item
|
||||
}
|
||||
toJSON () {
|
||||
const json = {}
|
||||
if (this.type !== null) {
|
||||
json.type = this.type.toJSON()
|
||||
}
|
||||
if (this.tname !== null) {
|
||||
json.tname = this.tname
|
||||
}
|
||||
if (this.item !== null) {
|
||||
json.item = this.item.toJSON()
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} json
|
||||
* @return {Cursor}
|
||||
* @param {any} json
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursorFromJSON = json => new Cursor(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))
|
||||
|
||||
export class AbsolutePosition {
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} offset
|
||||
* @param {number} index
|
||||
*/
|
||||
constructor (type, offset) {
|
||||
constructor (type, index) {
|
||||
/**
|
||||
* @type {AbstractType<any>}
|
||||
*/
|
||||
@@ -99,17 +83,17 @@ export class AbsolutePosition {
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this.offset = offset
|
||||
this.index = index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} offset
|
||||
* @param {number} index
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset)
|
||||
export const createAbsolutePosition = (type, index) => new AbsolutePosition(type, index)
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
@@ -117,7 +101,7 @@ export const createAbsolutePosition = (type, offset) => new AbsolutePosition(typ
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursor = (type, item) => {
|
||||
export const createRelativePosition = (type, item) => {
|
||||
let typeid = null
|
||||
let tname = null
|
||||
if (type._item === null) {
|
||||
@@ -125,40 +109,40 @@ export const createCursor = (type, item) => {
|
||||
} else {
|
||||
typeid = type._item.id
|
||||
}
|
||||
return new Cursor(typeid, tname, item)
|
||||
return new RelativePosition(typeid, tname, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a relativePosition based on a absolute position.
|
||||
*
|
||||
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
|
||||
* @param {number} offset The absolute position.
|
||||
* @return {Cursor}
|
||||
* @param {number} index The absolute position.
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursorFromTypeOffset = (type, offset) => {
|
||||
export const createRelativePositionFromTypeIndex = (type, index) => {
|
||||
let t = type._start
|
||||
while (t !== null) {
|
||||
if (!t.deleted && t.countable) {
|
||||
if (t.length > offset) {
|
||||
if (t.length > index) {
|
||||
// case 1: found position somewhere in the linked list
|
||||
return createCursor(type, createID(t.id.client, t.id.clock + offset))
|
||||
return createRelativePosition(type, createID(t.id.client, t.id.clock + index))
|
||||
}
|
||||
offset -= t.length
|
||||
index -= t.length
|
||||
}
|
||||
t = t.right
|
||||
}
|
||||
return createCursor(type, null)
|
||||
return createRelativePosition(type, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Cursor} rpos
|
||||
* @param {RelativePosition} rpos
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeCursor = (encoder, rpos) => {
|
||||
export const writeRelativePosition = (encoder, rpos) => {
|
||||
const { type, tname, item } = rpos
|
||||
if (item !== null) {
|
||||
encoding.writeVarUint(encoder, 0)
|
||||
@@ -177,15 +161,23 @@ export const writeCursor = (encoder, rpos) => {
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RelativePosition} rpos
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export const encodeRelativePosition = rpos => {
|
||||
const encoder = encoding.createEncoder()
|
||||
writeRelativePosition(encoder, rpos)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @param {StructStore} store
|
||||
* @return {Cursor|null}
|
||||
* @return {RelativePosition|null}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readCursor = (decoder, y, store) => {
|
||||
export const readRelativePosition = decoder => {
|
||||
let type = null
|
||||
let tname = null
|
||||
let itemID = null
|
||||
@@ -203,51 +195,60 @@ export const readCursor = (decoder, y, store) => {
|
||||
type = readID(decoder)
|
||||
}
|
||||
}
|
||||
return new Cursor(type, tname, itemID)
|
||||
return new RelativePosition(type, tname, itemID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cursor} cursor
|
||||
* @param {Y} y
|
||||
* @param {Uint8Array} uint8Array
|
||||
* @return {RelativePosition|null}
|
||||
*/
|
||||
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
||||
|
||||
/**
|
||||
* @param {RelativePosition} rpos
|
||||
* @param {Doc} doc
|
||||
* @return {AbsolutePosition|null}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createAbsolutePositionFromCursor = (cursor, y) => {
|
||||
const store = y.store
|
||||
const rightID = cursor.item
|
||||
const typeID = cursor.type
|
||||
const tname = cursor.tname
|
||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
const store = doc.store
|
||||
const rightID = rpos.item
|
||||
const typeID = rpos.type
|
||||
const tname = rpos.tname
|
||||
let type = null
|
||||
let offset = 0
|
||||
let index = 0
|
||||
if (rightID !== null) {
|
||||
if (getState(store, rightID.client) <= rightID.clock) {
|
||||
return null
|
||||
}
|
||||
const right = getItem(store, rightID)
|
||||
if (!(right instanceof AbstractItem)) {
|
||||
const res = followRedone(store, rightID)
|
||||
const right = res.item
|
||||
if (!(right instanceof Item)) {
|
||||
return null
|
||||
}
|
||||
offset = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
|
||||
let n = right.left
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable) {
|
||||
offset += n.length
|
||||
}
|
||||
n = n.left
|
||||
}
|
||||
type = right.parent
|
||||
if (type._item === null || !type._item.deleted) {
|
||||
index = right.deleted || !right.countable ? 0 : res.diff
|
||||
let n = right.left
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable) {
|
||||
index += n.length
|
||||
}
|
||||
n = n.left
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tname !== null) {
|
||||
type = y.get(tname)
|
||||
type = doc.get(tname)
|
||||
} else if (typeID !== null) {
|
||||
if (getState(store, typeID.client) <= typeID.clock) {
|
||||
// type does not exist yet
|
||||
return null
|
||||
}
|
||||
const struct = getItemType(store, typeID)
|
||||
if (struct instanceof ItemType) {
|
||||
type = struct.type
|
||||
const { item } = followRedone(store, typeID)
|
||||
if (item instanceof Item && item.content instanceof ContentType) {
|
||||
type = item.content.type
|
||||
} else {
|
||||
// struct is garbage collected
|
||||
return null
|
||||
@@ -255,20 +256,17 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
|
||||
} else {
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
offset = type._length
|
||||
index = type._length
|
||||
}
|
||||
if (type._item !== null && type._item.deleted) {
|
||||
return null
|
||||
}
|
||||
return createAbsolutePosition(type, offset)
|
||||
return createAbsolutePosition(type, index)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cursor|null} a
|
||||
* @param {Cursor|null} b
|
||||
* @param {RelativePosition|null} a
|
||||
* @param {RelativePosition|null} b
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const compareCursors = (a, b) => a === b || (
|
||||
export const compareRelativePositions = (a, b) => a === b || (
|
||||
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type)
|
||||
)
|
||||
@@ -1,17 +1,31 @@
|
||||
|
||||
import {
|
||||
isDeleted,
|
||||
DeleteSet, AbstractItem // eslint-disable-line
|
||||
createDeleteSetFromStructStore,
|
||||
getStateVector,
|
||||
getItemCleanStart,
|
||||
createID,
|
||||
iterateDeletedStructs,
|
||||
writeDeleteSet,
|
||||
writeStateVector,
|
||||
readDeleteSet,
|
||||
readStateVector,
|
||||
createDeleteSet,
|
||||
getState,
|
||||
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 encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
|
||||
export class Snapshot {
|
||||
/**
|
||||
* @param {DeleteSet} ds delete store
|
||||
* @param {Map<number,number>} sm state map
|
||||
* @param {Map<number,string>} userMap
|
||||
* @private
|
||||
* @param {DeleteSet} ds
|
||||
* @param {Map<number,number>} sv state map
|
||||
*/
|
||||
constructor (ds, sm, userMap) {
|
||||
constructor (ds, sv) {
|
||||
/**
|
||||
* @type {DeleteSet}
|
||||
* @private
|
||||
@@ -22,22 +36,105 @@ export class Snapshot {
|
||||
* @type {Map<number,number>}
|
||||
* @private
|
||||
*/
|
||||
this.sm = sm
|
||||
/**
|
||||
* @type {Map<number,string>}
|
||||
* @private
|
||||
*/
|
||||
this.userMap = userMap
|
||||
this.sv = sv
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractItem} item
|
||||
* @param {Snapshot} snap1
|
||||
* @param {Snapshot} snap2
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const equalSnapshots = (snap1, snap2) => {
|
||||
const ds1 = snap1.ds.clients
|
||||
const ds2 = snap2.ds.clients
|
||||
const sv1 = snap1.sv
|
||||
const sv2 = snap2.sv
|
||||
if (sv1.size !== sv2.size || ds1.size !== ds2.size) {
|
||||
return false
|
||||
}
|
||||
for (const [key, value] of sv1) {
|
||||
if (sv2.get(key) !== value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const [client, dsitems1] of ds1) {
|
||||
const dsitems2 = ds2.get(client) || []
|
||||
if (dsitems1.length !== dsitems2.length) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < dsitems1.length; i++) {
|
||||
const dsitem1 = dsitems1[i]
|
||||
const dsitem2 = dsitems2[i]
|
||||
if (dsitem1.clock !== dsitem2.clock || dsitem1.len !== dsitem2.len) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export const encodeSnapshot = snapshot => {
|
||||
const encoder = encoding.createEncoder()
|
||||
writeDeleteSet(encoder, snapshot.ds)
|
||||
writeStateVector(encoder, snapshot.sv)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} buf
|
||||
* @return {Snapshot}
|
||||
*/
|
||||
export const decodeSnapshot = buf => {
|
||||
const decoder = decoding.createDecoder(buf)
|
||||
return new Snapshot(readDeleteSet(decoder), readStateVector(decoder))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds
|
||||
* @param {Map<number,number>} sm
|
||||
* @return {Snapshot}
|
||||
*/
|
||||
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
|
||||
|
||||
export const emptySnapshot = createSnapshot(createDeleteSet(), new Map())
|
||||
|
||||
/**
|
||||
* @param {Doc} doc
|
||||
* @return {Snapshot}
|
||||
*/
|
||||
export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store))
|
||||
|
||||
/**
|
||||
* @param {Item} item
|
||||
* @param {Snapshot|undefined} snapshot
|
||||
*
|
||||
* @protected
|
||||
* @function
|
||||
*/
|
||||
export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : (
|
||||
snapshot.sm.has(item.id.client) && (snapshot.sm.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
|
||||
snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Snapshot} snapshot
|
||||
*/
|
||||
export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
||||
const meta = map.setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, set.create)
|
||||
const store = transaction.doc.store
|
||||
// check if we already split for this snapshot
|
||||
if (!meta.has(snapshot)) {
|
||||
snapshot.sv.forEach((clock, client) => {
|
||||
if (clock < getState(store, client)) {
|
||||
getItemCleanStart(transaction, createID(client, clock))
|
||||
}
|
||||
})
|
||||
iterateDeletedStructs(transaction, snapshot.ds, item => {})
|
||||
meta.add(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
|
||||
import {
|
||||
GC,
|
||||
Transaction, AbstractStructRef, ID, ItemType, AbstractItem, AbstractStruct // eslint-disable-line
|
||||
splitItem,
|
||||
GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
|
||||
export class StructStore {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {Map<number,Array<AbstractStruct>>}
|
||||
* @type {Map<number,Array<GC|Item>>}
|
||||
* @private
|
||||
*/
|
||||
this.clients = new Map()
|
||||
@@ -22,14 +22,14 @@ export class StructStore {
|
||||
* 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<AbstractStructRef>}>}
|
||||
* @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
|
||||
* @private
|
||||
*/
|
||||
this.pendingClientsStructRefs = new Map()
|
||||
/**
|
||||
* Stack of pending structs waiting for struct dependencies
|
||||
* Maximum length of stack is structReaders.size
|
||||
* @type {Array<AbstractStructRef>}
|
||||
* @type {Array<GCRef|ItemRef>}
|
||||
* @private
|
||||
*/
|
||||
this.pendingStack = []
|
||||
@@ -51,7 +51,7 @@ export class StructStore {
|
||||
* @public
|
||||
* @function
|
||||
*/
|
||||
export const getStates = store => {
|
||||
export const getStateVector = store => {
|
||||
const sm = new Map()
|
||||
store.clients.forEach((structs, client) => {
|
||||
const struct = structs[structs.length - 1]
|
||||
@@ -97,7 +97,7 @@ export const integretyCheck = store => {
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
* @param {AbstractStruct} struct
|
||||
* @param {GC|Item} struct
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@@ -151,14 +151,14 @@ export const findIndexSS = (structs, clock) => {
|
||||
*
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {AbstractStruct}
|
||||
* @return {GC|Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const find = (store, id) => {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
* @type {Array<GC|Item>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(id.client)
|
||||
@@ -170,7 +170,7 @@ export const find = (store, id) => {
|
||||
*
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {AbstractItem}
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@@ -179,45 +179,33 @@ export const find = (store, id) => {
|
||||
export const getItem = (store, id) => find(store, id)
|
||||
|
||||
/**
|
||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||
*
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {ItemType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @param {Transaction} transaction
|
||||
* @param {Array<Item|GC>} structs
|
||||
* @param {number} clock
|
||||
*/
|
||||
// @ts-ignore
|
||||
export const getItemType = (store, id) => find(store, id)
|
||||
export const findIndexCleanStart = (transaction, structs, clock) => {
|
||||
const index = findIndexSS(structs, clock)
|
||||
const struct = structs[index]
|
||||
if (struct.id.clock < clock && struct instanceof Item) {
|
||||
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
|
||||
return index + 1
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {AbstractItem}
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItemCleanStart = (transaction, store, id) => {
|
||||
/**
|
||||
* @type {Array<AbstractItem>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(id.client)
|
||||
const index = findIndexSS(structs, id.clock)
|
||||
/**
|
||||
* @type {AbstractItem}
|
||||
*/
|
||||
let struct = structs[index]
|
||||
if (struct.id.clock < id.clock && struct.constructor !== GC) {
|
||||
struct = struct.splitAt(transaction, id.clock - struct.id.clock)
|
||||
structs.splice(index + 1, 0, struct)
|
||||
}
|
||||
return struct
|
||||
export const getItemCleanStart = (transaction, id) => {
|
||||
const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
|
||||
return structs[findIndexCleanStart(transaction, structs, id.clock)]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,21 +214,21 @@ export const getItemCleanStart = (transaction, store, id) => {
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {AbstractItem}
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItemCleanEnd = (transaction, store, id) => {
|
||||
/**
|
||||
* @type {Array<AbstractItem>}
|
||||
* @type {Array<Item>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = 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) {
|
||||
structs.splice(index + 1, 0, struct.splitAt(transaction, id.clock - struct.id.clock + 1))
|
||||
structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1))
|
||||
}
|
||||
return struct
|
||||
}
|
||||
@@ -248,56 +236,40 @@ export const getItemCleanEnd = (transaction, store, id) => {
|
||||
/**
|
||||
* Replace `item` with `newitem` in store
|
||||
* @param {StructStore} store
|
||||
* @param {AbstractStruct} struct
|
||||
* @param {AbstractStruct} newStruct
|
||||
* @param {GC|Item} struct
|
||||
* @param {GC|Item} newStruct
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const replaceStruct = (store, struct, newStruct) => {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(struct.id.client)
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(struct.id.client))
|
||||
structs[findIndexSS(structs, struct.id.clock)] = newStruct
|
||||
}
|
||||
|
||||
/**
|
||||
* Read StateMap from Decoder and return as Map
|
||||
* Iterate over a range of structs
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {Map<number,number>}
|
||||
* @param {Transaction} transaction
|
||||
* @param {Array<Item|GC>} structs
|
||||
* @param {number} clockStart Inclusive start
|
||||
* @param {number} len
|
||||
* @param {function(GC|Item):void} f
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readStatesAsMap = decoder => {
|
||||
const ss = new Map()
|
||||
const ssLength = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
const client = decoding.readVarUint(decoder)
|
||||
const clock = decoding.readVarUint(decoder)
|
||||
ss.set(client, clock)
|
||||
export const iterateStructs = (transaction, structs, clockStart, len, f) => {
|
||||
if (len === 0) {
|
||||
return
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
* Write StateMap to Encoder
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {StructStore} store
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStates = (encoder, store) => {
|
||||
encoding.writeVarUint(encoder, store.clients.size)
|
||||
store.clients.forEach((structs, client) => {
|
||||
const id = structs[structs.length - 1].id
|
||||
encoding.writeVarUint(encoder, id.client)
|
||||
encoding.writeVarUint(encoder, id.clock)
|
||||
})
|
||||
return encoder
|
||||
const clockEnd = clockStart + len
|
||||
let index = findIndexCleanStart(transaction, structs, clockStart)
|
||||
let struct
|
||||
do {
|
||||
struct = structs[index++]
|
||||
if (clockEnd < struct.id.clock + struct.length) {
|
||||
findIndexCleanStart(transaction, structs, clockEnd)
|
||||
}
|
||||
f(struct)
|
||||
} while (index < structs.length && structs[index].id.clock < clockEnd)
|
||||
}
|
||||
|
||||
@@ -6,16 +6,18 @@ import {
|
||||
writeDeleteSet,
|
||||
DeleteSet,
|
||||
sortAndMergeDeleteSet,
|
||||
getStates,
|
||||
getStateVector,
|
||||
findIndexSS,
|
||||
callEventHandlerListeners,
|
||||
AbstractItem,
|
||||
ID, AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line
|
||||
Item,
|
||||
StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as set from 'lib0/set.js'
|
||||
import { callAll } from 'lib0/function.js'
|
||||
|
||||
/**
|
||||
* A transaction is created for every change on the Yjs model. It is possible
|
||||
@@ -43,15 +45,16 @@ import * as math from 'lib0/math.js'
|
||||
*/
|
||||
export class Transaction {
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {Doc} doc
|
||||
* @param {any} origin
|
||||
* @param {boolean} local
|
||||
*/
|
||||
constructor (y, origin) {
|
||||
constructor (doc, origin, local) {
|
||||
/**
|
||||
* The Yjs instance.
|
||||
* @type {Y}
|
||||
* @type {Doc}
|
||||
*/
|
||||
this.y = y
|
||||
this.doc = doc
|
||||
/**
|
||||
* Describes the set of deleted items by ids
|
||||
* @type {DeleteSet}
|
||||
@@ -61,7 +64,7 @@ export class Transaction {
|
||||
* Holds the state before the transaction started.
|
||||
* @type {Map<Number,Number>}
|
||||
*/
|
||||
this.beforeState = getStates(y.store)
|
||||
this.beforeState = getStateVector(doc.store)
|
||||
/**
|
||||
* Holds the state after the transaction.
|
||||
* @type {Map<Number,Number>}
|
||||
@@ -70,7 +73,7 @@ export class Transaction {
|
||||
/**
|
||||
* All types that were directly modified (property added or child
|
||||
* inserted/deleted). New types are not included in this Set.
|
||||
* Maps from type to parentSubs (`item._parentSub = null` for YArray)
|
||||
* Maps from type to parentSubs (`item.parentSub = null` for YArray)
|
||||
* @type {Map<AbstractType<YEvent>,Set<String|null>>}
|
||||
*/
|
||||
this.changed = new Map()
|
||||
@@ -80,11 +83,6 @@ export class Transaction {
|
||||
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
|
||||
*/
|
||||
this.changedParentTypes = new Map()
|
||||
/**
|
||||
* @type {encoding.Encoder|null}
|
||||
* @private
|
||||
*/
|
||||
this._updateMessage = null
|
||||
/**
|
||||
* @type {Set<ID>}
|
||||
* @private
|
||||
@@ -94,22 +92,31 @@ export class Transaction {
|
||||
* @type {any}
|
||||
*/
|
||||
this.origin = origin
|
||||
/**
|
||||
* Stores meta information on the transaction
|
||||
* @type {Map<any,any>}
|
||||
*/
|
||||
this.meta = new Map()
|
||||
/**
|
||||
* Whether this change originates from this doc.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.local = local
|
||||
}
|
||||
/**
|
||||
* @type {encoding.Encoder|null}
|
||||
* @public
|
||||
*/
|
||||
get updateMessage () {
|
||||
// only create if content was added in transaction (state or ds changed)
|
||||
if (this._updateMessage === null && (this.deleteSet.clients.size > 0 || map.any(this.afterState, (clock, client) => this.beforeState.get(client) !== clock))) {
|
||||
const encoder = encoding.createEncoder()
|
||||
sortAndMergeDeleteSet(this.deleteSet)
|
||||
writeStructsFromTransaction(encoder, this)
|
||||
writeDeleteSet(encoder, this.deleteSet)
|
||||
this._updateMessage = encoder
|
||||
}
|
||||
return this._updateMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
export const computeUpdateMessageFromTransaction = transaction => {
|
||||
if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
|
||||
return null
|
||||
}
|
||||
const encoder = encoding.createEncoder()
|
||||
sortAndMergeDeleteSet(transaction.deleteSet)
|
||||
writeStructsFromTransaction(encoder, transaction)
|
||||
writeDeleteSet(encoder, transaction.deleteSet)
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,167 +126,243 @@ export class Transaction {
|
||||
* @function
|
||||
*/
|
||||
export const nextID = transaction => {
|
||||
const y = transaction.y
|
||||
const y = transaction.doc
|
||||
return createID(y.clientID, getState(y.store, y.clientID))
|
||||
}
|
||||
|
||||
/**
|
||||
* If `type.parent` was added in current transaction, `type` technically
|
||||
* did not change, it was just added and we should not fire events for `type`.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<YEvent>} type
|
||||
* @param {string|null} parentSub
|
||||
*/
|
||||
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
|
||||
const item = type._item
|
||||
if (item === null || (item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted)) {
|
||||
map.setIfUndefined(transaction.changed, type, set.create).add(parentSub)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
*/
|
||||
const tryToMergeWithLeft = (structs, pos) => {
|
||||
const left = structs[pos - 1]
|
||||
const right = structs[pos]
|
||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||
if (left.mergeWith(right)) {
|
||||
structs.splice(pos, 1)
|
||||
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
|
||||
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds
|
||||
* @param {StructStore} store
|
||||
* @param {function(Item):boolean} gcFilter
|
||||
*/
|
||||
const tryGcDeleteSet = (ds, store, gcFilter) => {
|
||||
for (const [client, deleteItems] of ds.clients) {
|
||||
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
const deleteItem = deleteItems[di]
|
||||
const endDeleteItemClock = deleteItem.clock + deleteItem.len
|
||||
for (
|
||||
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
|
||||
si < structs.length && struct.id.clock < endDeleteItemClock;
|
||||
struct = structs[++si]
|
||||
) {
|
||||
const struct = structs[si]
|
||||
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
|
||||
break
|
||||
}
|
||||
if (struct instanceof Item && struct.deleted && !struct.keep && gcFilter(struct)) {
|
||||
struct.gc(store, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
const tryMergeDeleteSet = (ds, store) => {
|
||||
// try to merge deleted / gc'd items
|
||||
// merge from right to left for better efficiecy and so we don't miss any merge targets
|
||||
for (const [client, deleteItems] of ds.clients) {
|
||||
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
const deleteItem = deleteItems[di]
|
||||
// start with merging the item next to the last deleted item
|
||||
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
|
||||
for (
|
||||
let si = mostRightIndexToCheck, struct = structs[si];
|
||||
si > 0 && struct.id.clock >= deleteItem.clock;
|
||||
struct = structs[--si]
|
||||
) {
|
||||
tryToMergeWithLeft(structs, si)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds
|
||||
* @param {StructStore} store
|
||||
* @param {function(Item):boolean} gcFilter
|
||||
*/
|
||||
export const tryGc = (ds, store, gcFilter) => {
|
||||
tryGcDeleteSet(ds, store, gcFilter)
|
||||
tryMergeDeleteSet(ds, store)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Transaction>} transactionCleanups
|
||||
* @param {number} i
|
||||
*/
|
||||
const cleanupTransactions = (transactionCleanups, i) => {
|
||||
if (i < transactionCleanups.length) {
|
||||
const transaction = transactionCleanups[i]
|
||||
const doc = transaction.doc
|
||||
const store = doc.store
|
||||
const ds = transaction.deleteSet
|
||||
try {
|
||||
sortAndMergeDeleteSet(ds)
|
||||
transaction.afterState = getStateVector(transaction.doc.store)
|
||||
doc._transaction = null
|
||||
doc.emit('beforeObserverCalls', [transaction, doc])
|
||||
/**
|
||||
* An array of event callbacks.
|
||||
*
|
||||
* Each callback is called even if the other ones throw errors.
|
||||
*
|
||||
* @type {Array<function():void>}
|
||||
*/
|
||||
const fs = []
|
||||
// observe events on changed types
|
||||
transaction.changed.forEach((subs, itemtype) =>
|
||||
fs.push(() => {
|
||||
if (itemtype._item === null || !itemtype._item.deleted) {
|
||||
itemtype._callObserver(transaction, subs)
|
||||
}
|
||||
})
|
||||
)
|
||||
fs.push(() => {
|
||||
// deep observe events
|
||||
transaction.changedParentTypes.forEach((events, type) =>
|
||||
fs.push(() => {
|
||||
// We need to think about the possibility that the user transforms the
|
||||
// Y.Doc in the event.
|
||||
if (type._item === null || !type._item.deleted) {
|
||||
events = events
|
||||
.filter(event =>
|
||||
event.target._item === null || !event.target._item.deleted
|
||||
)
|
||||
events
|
||||
.forEach(event => {
|
||||
event.currentTarget = type
|
||||
})
|
||||
// We don't need to check for events.length
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, events, transaction)
|
||||
}
|
||||
})
|
||||
)
|
||||
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
||||
})
|
||||
callAll(fs, [])
|
||||
} finally {
|
||||
// Replace deleted items with ItemDeleted / GC.
|
||||
// This is where content is actually remove from the Yjs Doc.
|
||||
if (doc.gc) {
|
||||
tryGcDeleteSet(ds, store, doc.gcFilter)
|
||||
}
|
||||
tryMergeDeleteSet(ds, store)
|
||||
|
||||
// on all affected store.clients props, try to merge
|
||||
for (const [client, clock] of transaction.afterState) {
|
||||
const beforeClock = transaction.beforeState.get(client) || 0
|
||||
if (beforeClock !== clock) {
|
||||
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||
// we iterate from right to left so we can safely remove entries
|
||||
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
|
||||
for (let i = structs.length - 1; i >= firstChangePos; i--) {
|
||||
tryToMergeWithLeft(structs, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
// try to merge mergeStructs
|
||||
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
|
||||
// but at the moment DS does not handle duplicates
|
||||
for (const mid of transaction._mergeStructs) {
|
||||
const client = mid.client
|
||||
const clock = mid.clock
|
||||
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||
const replacedStructPos = findIndexSS(structs, clock)
|
||||
if (replacedStructPos + 1 < structs.length) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos + 1)
|
||||
}
|
||||
if (replacedStructPos > 0) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos)
|
||||
}
|
||||
}
|
||||
// @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 updateMessage = computeUpdateMessageFromTransaction(transaction)
|
||||
if (updateMessage !== null) {
|
||||
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
|
||||
}
|
||||
}
|
||||
if (transactionCleanups.length <= i + 1) {
|
||||
doc._transactionCleanups = []
|
||||
} else {
|
||||
cleanupTransactions(transactionCleanups, i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the functionality of `y.transact(()=>{..})`
|
||||
*
|
||||
* @param {Y} y
|
||||
* @param {Doc} doc
|
||||
* @param {function(Transaction):void} f
|
||||
* @param {any} [origin]
|
||||
* @param {any} [origin=true]
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const transact = (y, f, origin = null) => {
|
||||
const transactionCleanups = y._transactionCleanups
|
||||
export const transact = (doc, f, origin = null, local = true) => {
|
||||
const transactionCleanups = doc._transactionCleanups
|
||||
let initialCall = false
|
||||
if (y._transaction === null) {
|
||||
if (doc._transaction === null) {
|
||||
initialCall = true
|
||||
y._transaction = new Transaction(y, origin)
|
||||
transactionCleanups.push(y._transaction)
|
||||
y.emit('beforeTransaction', [y._transaction, y])
|
||||
doc._transaction = new Transaction(doc, origin, local)
|
||||
transactionCleanups.push(doc._transaction)
|
||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||
}
|
||||
try {
|
||||
f(y._transaction)
|
||||
f(doc._transaction)
|
||||
} finally {
|
||||
// @todo set after state here
|
||||
if (initialCall && transactionCleanups[0] === y._transaction) {
|
||||
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
||||
// The first transaction ended, now process observer calls.
|
||||
// Observer call may create new transactions for which we need to call the observers and do cleanup.
|
||||
// We don't want to nest these calls, so we execute these calls one after another
|
||||
for (let i = 0; i < transactionCleanups.length; i++) {
|
||||
const transaction = transactionCleanups[i]
|
||||
const store = transaction.y.store
|
||||
const ds = transaction.deleteSet
|
||||
sortAndMergeDeleteSet(ds)
|
||||
transaction.afterState = getStates(transaction.y.store)
|
||||
y._transaction = null
|
||||
y.emit('beforeObserverCalls', [transaction, y])
|
||||
// emit change events on changed types
|
||||
transaction.changed.forEach((subs, itemtype) => {
|
||||
itemtype._callObserver(transaction, subs)
|
||||
})
|
||||
transaction.changedParentTypes.forEach((events, type) => {
|
||||
events = events
|
||||
.filter(event =>
|
||||
event.target._item === null || !event.target._item.deleted
|
||||
)
|
||||
events
|
||||
.forEach(event => {
|
||||
event.currentTarget = type
|
||||
})
|
||||
// we don't need to check for events.length
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, events, transaction)
|
||||
})
|
||||
y.emit('afterTransaction', [transaction, y])
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
*/
|
||||
const tryToMergeWithLeft = (structs, pos) => {
|
||||
const left = structs[pos - 1]
|
||||
const right = structs[pos]
|
||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||
if (left.mergeWith(right)) {
|
||||
structs.splice(pos, 1)
|
||||
if (right instanceof AbstractItem && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
|
||||
// @ts-ignore we already did a constructor check above
|
||||
right.parent._map.set(right.parentSub, left)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// replace deleted items with ItemDeleted / GC
|
||||
for (const [client, deleteItems] of ds.clients) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
const deleteItem = deleteItems[di]
|
||||
const endDeleteItemClock = deleteItem.clock + deleteItem.len
|
||||
for (
|
||||
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
|
||||
si < structs.length && struct.id.clock < endDeleteItemClock;
|
||||
struct = structs[++si]
|
||||
) {
|
||||
const struct = structs[si]
|
||||
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
|
||||
break
|
||||
}
|
||||
if (struct.deleted && struct instanceof AbstractItem) {
|
||||
struct.gc(transaction, store, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// try to merge deleted / gc'd items
|
||||
// merge from right to left for better efficiecy and so we don't miss any merge targets
|
||||
for (const [client, deleteItems] of ds.clients) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
const deleteItem = deleteItems[di]
|
||||
// start with merging the item next to the last deleted item
|
||||
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
|
||||
for (
|
||||
let si = mostRightIndexToCheck, struct = structs[si];
|
||||
si > 0 && struct.id.clock >= deleteItem.clock;
|
||||
struct = structs[--si]
|
||||
) {
|
||||
tryToMergeWithLeft(structs, si)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// on all affected store.clients props, try to merge
|
||||
for (const [client, clock] of transaction.afterState) {
|
||||
const beforeClock = transaction.beforeState.get(client) || 0
|
||||
if (beforeClock !== clock) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
// we iterate from right to left so we can safely remove entries
|
||||
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
|
||||
for (let i = structs.length - 1; i >= firstChangePos; i--) {
|
||||
tryToMergeWithLeft(structs, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
// try to merge mergeStructs
|
||||
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
|
||||
// but at the moment DS does not handle duplicates
|
||||
for (const mid of transaction._mergeStructs) {
|
||||
const client = mid.client
|
||||
const clock = mid.clock
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
const replacedStructPos = findIndexSS(structs, clock)
|
||||
if (replacedStructPos + 1 < structs.length) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos + 1)
|
||||
}
|
||||
if (replacedStructPos > 0) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos)
|
||||
}
|
||||
}
|
||||
// @todo Merge all the transactions into one and provide send the data as a single update message
|
||||
// @todo implement a dedicatet event that we can use to send updates to other peer
|
||||
y.emit('afterTransactionCleanup', [transaction, y])
|
||||
}
|
||||
y._transactionCleanups = []
|
||||
// We don't want to nest these calls, so we execute these calls one after
|
||||
// another.
|
||||
// Also we need to ensure that all cleanups are called, even if the
|
||||
// observes throw errors.
|
||||
// This file is full of hacky try {} finally {} blocks to ensure that an
|
||||
// event can throw errors and also that the cleanup is called.
|
||||
cleanupTransactions(transactionCleanups, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,202 +1,283 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
mergeDeleteSets,
|
||||
iterateDeletedStructs,
|
||||
keepItem,
|
||||
transact,
|
||||
redoItem,
|
||||
iterateStructs,
|
||||
isParentOf,
|
||||
createID,
|
||||
transact
|
||||
followRedone,
|
||||
getItemCleanStart,
|
||||
getState,
|
||||
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
class ReverseOperation {
|
||||
constructor (y, transaction, bindingInfos) {
|
||||
this.created = new Date()
|
||||
const beforeState = transaction.beforeState
|
||||
if (beforeState.has(y.userID)) {
|
||||
this.toState = createID(y.userID, y.ss.getState(y.userID) - 1)
|
||||
this.fromState = createID(y.userID, beforeState.get(y.userID))
|
||||
} else {
|
||||
this.toState = null
|
||||
this.fromState = null
|
||||
}
|
||||
this.deletedStructs = new Set()
|
||||
transaction.deletedStructs.forEach(struct => {
|
||||
this.deletedStructs.add({
|
||||
from: struct._id,
|
||||
len: struct._length
|
||||
})
|
||||
})
|
||||
/**
|
||||
* Maps from binding to binding information (e.g. cursor information)
|
||||
*/
|
||||
this.bindingInfos = bindingInfos
|
||||
}
|
||||
}
|
||||
import * as time from 'lib0/time.js'
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
function applyReverseOperation (y, scope, reverseBuffer) {
|
||||
let performedUndo = false
|
||||
let undoOp = null
|
||||
transact(y, () => {
|
||||
while (!performedUndo && reverseBuffer.length > 0) {
|
||||
undoOp = reverseBuffer.pop()
|
||||
// make sure that it is possible to iterate {from}-{to}
|
||||
if (undoOp.fromState !== null) {
|
||||
y.os.getItemCleanStart(undoOp.fromState)
|
||||
y.os.getItemCleanEnd(undoOp.toState)
|
||||
y.os.iterate(undoOp.fromState, undoOp.toState, op => {
|
||||
while (op._deleted && op._redone !== null) {
|
||||
op = op._redone
|
||||
}
|
||||
if (op._deleted === false && isParentOf(scope, op)) {
|
||||
performedUndo = true
|
||||
op._delete(y)
|
||||
}
|
||||
})
|
||||
}
|
||||
const redoitems = new Set()
|
||||
for (let del of undoOp.deletedStructs) {
|
||||
const fromState = del.from
|
||||
const toState = createID(fromState.user, fromState.clock + del.len - 1)
|
||||
y.os.getItemCleanStart(fromState)
|
||||
y.os.getItemCleanEnd(toState)
|
||||
y.os.iterate(fromState, toState, op => {
|
||||
if (
|
||||
isParentOf(scope, op) &&
|
||||
op._parent !== y &&
|
||||
(
|
||||
op._id.user !== y.userID ||
|
||||
undoOp.fromState === null ||
|
||||
op._id.clock < undoOp.fromState.clock ||
|
||||
op._id.clock > undoOp.toState.clock
|
||||
)
|
||||
) {
|
||||
redoitems.add(op)
|
||||
}
|
||||
})
|
||||
}
|
||||
redoitems.forEach(op => {
|
||||
const opUndone = op._redo(y, redoitems)
|
||||
performedUndo = performedUndo || opUndone
|
||||
})
|
||||
}
|
||||
})
|
||||
if (performedUndo && undoOp !== null) {
|
||||
// should be performed after the undo transaction
|
||||
undoOp.bindingInfos.forEach((info, binding) => {
|
||||
binding._restoreUndoStackInfo(info)
|
||||
})
|
||||
}
|
||||
return performedUndo
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a history of locally applied operations. The UndoManager handles the
|
||||
* undoing and redoing of locally created changes.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export class UndoManager {
|
||||
class StackItem {
|
||||
/**
|
||||
* @param {YType} scope The scope on which to listen for changes.
|
||||
* @param {Object} options Optionally provided configuration.
|
||||
* @param {DeleteSet} ds
|
||||
* @param {number} start clock start of the local client
|
||||
* @param {number} len
|
||||
*/
|
||||
constructor (scope, options = {}) {
|
||||
this.options = options
|
||||
this._bindings = new Set(options.bindings)
|
||||
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
|
||||
this._undoBuffer = []
|
||||
this._redoBuffer = []
|
||||
this._scope = scope
|
||||
this._undoing = false
|
||||
this._redoing = false
|
||||
this._lastTransactionWasUndo = false
|
||||
const y = scope._y
|
||||
this.y = y
|
||||
let bindingInfos
|
||||
y.on('beforeTransaction', (y, transaction, remote) => {
|
||||
if (!remote) {
|
||||
// Store binding information before transaction is executed
|
||||
// By restoring the binding information, we can make sure that the state
|
||||
// before the transaction can be recovered
|
||||
bindingInfos = new Map()
|
||||
this._bindings.forEach(binding => {
|
||||
bindingInfos.set(binding, binding._getUndoStackInfo())
|
||||
})
|
||||
}
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction, remote) => {
|
||||
if (!remote && transaction.changedParentTypes.has(scope)) {
|
||||
let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
|
||||
if (!this._undoing) {
|
||||
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
|
||||
if (
|
||||
this._redoing === false &&
|
||||
this._lastTransactionWasUndo === false &&
|
||||
lastUndoOp !== null &&
|
||||
((options.captureTimeout < 0) || (reverseOperation.created.getTime() - lastUndoOp.created.getTime()) <= options.captureTimeout)
|
||||
) {
|
||||
lastUndoOp.created = reverseOperation.created
|
||||
if (reverseOperation.toState !== null) {
|
||||
lastUndoOp.toState = reverseOperation.toState
|
||||
if (lastUndoOp.fromState === null) {
|
||||
lastUndoOp.fromState = reverseOperation.fromState
|
||||
}
|
||||
}
|
||||
reverseOperation.deletedStructs.forEach(lastUndoOp.deletedStructs.add, lastUndoOp.deletedStructs)
|
||||
} else {
|
||||
this._lastTransactionWasUndo = false
|
||||
this._undoBuffer.push(reverseOperation)
|
||||
}
|
||||
if (!this._redoing) {
|
||||
this._redoBuffer = []
|
||||
}
|
||||
} else {
|
||||
this._lastTransactionWasUndo = true
|
||||
this._redoBuffer.push(reverseOperation)
|
||||
constructor (ds, start, len) {
|
||||
this.ds = ds
|
||||
this.start = start
|
||||
this.len = len
|
||||
/**
|
||||
* Use this to save and restore metadata like selection range
|
||||
*/
|
||||
this.meta = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UndoManager} undoManager
|
||||
* @param {Array<StackItem>} stack
|
||||
* @param {string} eventType
|
||||
* @return {StackItem?}
|
||||
*/
|
||||
const popStackItem = (undoManager, stack, eventType) => {
|
||||
/**
|
||||
* Whether a change happened
|
||||
* @type {StackItem?}
|
||||
*/
|
||||
let result = null
|
||||
const doc = undoManager.doc
|
||||
const scope = undoManager.scope
|
||||
transact(doc, transaction => {
|
||||
while (stack.length > 0 && result === null) {
|
||||
const store = doc.store
|
||||
const clientID = doc.clientID
|
||||
const stackItem = /** @type {StackItem} */ (stack.pop())
|
||||
const stackStartClock = stackItem.start
|
||||
const stackEndClock = stackItem.start + stackItem.len
|
||||
const itemsToRedo = new Set()
|
||||
// @todo iterateStructs should not need the structs parameter
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientID))
|
||||
let performedChange = false
|
||||
if (stackStartClock !== stackEndClock) {
|
||||
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
|
||||
getItemCleanStart(transaction, createID(clientID, stackStartClock))
|
||||
if (stackEndClock < getState(doc.store, clientID)) {
|
||||
getItemCleanStart(transaction, createID(clientID, stackEndClock))
|
||||
}
|
||||
}
|
||||
iterateDeletedStructs(transaction, stackItem.ds, 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.
|
||||
!(struct.id.client === clientID && struct.id.clock >= stackStartClock && struct.id.clock < stackEndClock)
|
||||
) {
|
||||
itemsToRedo.add(struct)
|
||||
}
|
||||
})
|
||||
itemsToRedo.forEach(struct => {
|
||||
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
|
||||
})
|
||||
/**
|
||||
* @type {Array<Item>}
|
||||
*/
|
||||
const itemsToDelete = []
|
||||
iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => {
|
||||
if (struct instanceof Item) {
|
||||
if (struct.redone !== null) {
|
||||
let { item, diff } = followRedone(store, struct.id)
|
||||
if (diff > 0) {
|
||||
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
|
||||
}
|
||||
if (item.length > stackItem.len) {
|
||||
getItemCleanStart(transaction, createID(item.id.client, stackEndClock))
|
||||
}
|
||||
struct = item
|
||||
}
|
||||
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
|
||||
itemsToDelete.push(struct)
|
||||
}
|
||||
}
|
||||
})
|
||||
// We want to delete in reverse order so that children are deleted before
|
||||
// parents, so we have more information available when items are filtered.
|
||||
for (let i = itemsToDelete.length - 1; i >= 0; i--) {
|
||||
const item = itemsToDelete[i]
|
||||
if (undoManager.deleteFilter(item)) {
|
||||
item.delete(transaction)
|
||||
performedChange = true
|
||||
}
|
||||
}
|
||||
result = stackItem
|
||||
if (result != null) {
|
||||
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
|
||||
}
|
||||
}
|
||||
}, undoManager)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} UndoManagerOptions
|
||||
* @property {number} [UndoManagerOptions.captureTimeout=500]
|
||||
* @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
|
||||
* it is necessary to filter whan an Undo/Redo operation can delete. If this
|
||||
* filter returns false, the type/item won't be deleted even it is in the
|
||||
* undo/redo scope.
|
||||
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires 'stack-item-added' event when a stack item was added to either the undo- or
|
||||
* the redo-stack. You may store additional stack information via the
|
||||
* metadata property on `event.stackItem.meta` (it is a `Map` of metadata properties).
|
||||
* Fires 'stack-item-popped' event when a stack item was popped from either the
|
||||
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
|
||||
*
|
||||
* @extends {Observable<'stack-item-added'|'stack-item-popped'>}
|
||||
*/
|
||||
export class UndoManager extends Observable {
|
||||
/**
|
||||
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
|
||||
* @param {UndoManagerOptions} options
|
||||
*/
|
||||
constructor (typeScope, { captureTimeout, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) {
|
||||
if (captureTimeout == null) {
|
||||
captureTimeout = 500
|
||||
}
|
||||
super()
|
||||
this.scope = typeScope instanceof Array ? typeScope : [typeScope]
|
||||
this.deleteFilter = deleteFilter
|
||||
trackedOrigins.add(this)
|
||||
this.trackedOrigins = trackedOrigins
|
||||
/**
|
||||
* @type {Array<StackItem>}
|
||||
*/
|
||||
this.undoStack = []
|
||||
/**
|
||||
* @type {Array<StackItem>}
|
||||
*/
|
||||
this.redoStack = []
|
||||
/**
|
||||
* Whether the client is currently undoing (calling UndoManager.undo)
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.undoing = false
|
||||
this.redoing = false
|
||||
this.doc = /** @type {Doc} */ (this.scope[0].doc)
|
||||
this.lastChange = 0
|
||||
this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
|
||||
// Only track certain transactions
|
||||
if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) {
|
||||
return
|
||||
}
|
||||
const undoing = this.undoing
|
||||
const redoing = this.redoing
|
||||
const stack = undoing ? this.redoStack : this.undoStack
|
||||
if (undoing) {
|
||||
this.stopCapturing() // next undo should not be appended to last stack item
|
||||
} else if (!redoing) {
|
||||
// neither undoing nor redoing: delete redoStack
|
||||
this.redoStack = []
|
||||
}
|
||||
const beforeState = transaction.beforeState.get(this.doc.clientID) || 0
|
||||
const afterState = transaction.afterState.get(this.doc.clientID) || 0
|
||||
const now = time.getUnixTime()
|
||||
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||
// append change to last stack op
|
||||
const lastOp = stack[stack.length - 1]
|
||||
lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
|
||||
lastOp.len = afterState - lastOp.start
|
||||
} else {
|
||||
// create a new stack op
|
||||
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState - beforeState))
|
||||
}
|
||||
if (!undoing && !redoing) {
|
||||
this.lastChange = now
|
||||
}
|
||||
// make sure that deleted structs are not gc'd
|
||||
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
|
||||
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||
keepItem(item, true)
|
||||
}
|
||||
})
|
||||
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo' }, this])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce that the next change is created as a separate item in the undo stack
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
flushChanges () {
|
||||
this._lastTransactionWasUndo = true
|
||||
clear () {
|
||||
this.doc.transact(transaction => {
|
||||
/**
|
||||
* @param {StackItem} stackItem
|
||||
*/
|
||||
const clearItem = stackItem => {
|
||||
iterateDeletedStructs(transaction, stackItem.ds, item => {
|
||||
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||
keepItem(item, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
this.undoStack.forEach(clearItem)
|
||||
this.redoStack.forEach(clearItem)
|
||||
})
|
||||
this.undoStack = []
|
||||
this.redoStack = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last locally created change.
|
||||
* UndoManager merges Undo-StackItem if they are created within time-gap
|
||||
* smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next
|
||||
* StackItem won't be merged.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*
|
||||
* @example
|
||||
* // without stopCapturing
|
||||
* ytext.insert(0, 'a')
|
||||
* ytext.insert(1, 'b')
|
||||
* um.undo()
|
||||
* ytext.toString() // => '' (note that 'ab' was removed)
|
||||
* // with stopCapturing
|
||||
* ytext.insert(0, 'a')
|
||||
* um.stopCapturing()
|
||||
* ytext.insert(0, 'b')
|
||||
* um.undo()
|
||||
* ytext.toString() // => 'a' (note that only 'b' was removed)
|
||||
*
|
||||
*/
|
||||
stopCapturing () {
|
||||
this.lastChange = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo last changes on type.
|
||||
*
|
||||
* @return {StackItem?} Returns StackItem if a change was applied
|
||||
*/
|
||||
undo () {
|
||||
this._undoing = true
|
||||
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
|
||||
this._undoing = false
|
||||
return performedUndo
|
||||
this.undoing = true
|
||||
let res
|
||||
try {
|
||||
res = popStackItem(this, this.undoStack, 'undo')
|
||||
} finally {
|
||||
this.undoing = false
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last locally created change.
|
||||
* Redo last undo operation.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
* @return {StackItem?} Returns StackItem if a change was applied
|
||||
*/
|
||||
redo () {
|
||||
this._redoing = true
|
||||
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
|
||||
this._redoing = false
|
||||
return performedRedo
|
||||
this.redoing = true
|
||||
let res
|
||||
try {
|
||||
res = popStackItem(this, this.redoStack, 'redo')
|
||||
} finally {
|
||||
this.redoing = false
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
|
||||
import {
|
||||
isDeleted,
|
||||
AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as set from 'lib0/set.js'
|
||||
import * as array from 'lib0/array.js'
|
||||
|
||||
/**
|
||||
* YEvent describes the changes on a YType.
|
||||
*/
|
||||
@@ -28,6 +31,10 @@ export class YEvent {
|
||||
* @type {Transaction}
|
||||
*/
|
||||
this.transaction = transaction
|
||||
/**
|
||||
* @type {Object|null}
|
||||
*/
|
||||
this._changes = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,6 +56,8 @@ export class YEvent {
|
||||
/**
|
||||
* Check if a struct is deleted 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}
|
||||
*/
|
||||
@@ -59,12 +68,121 @@ 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>,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}>}
|
||||
*/
|
||||
const keys = new Map()
|
||||
changes = {
|
||||
added, deleted, delta, keys
|
||||
}
|
||||
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))
|
||||
/**
|
||||
* @type {'delete' | 'add' | 'update'}
|
||||
*/
|
||||
let action
|
||||
let oldValue
|
||||
if (this.adds(item)) {
|
||||
let prev = item.left
|
||||
while (prev !== null && this.adds(prev)) {
|
||||
prev = prev.left
|
||||
}
|
||||
if (this.deletes(item)) {
|
||||
if (prev !== null && this.deletes(prev)) {
|
||||
action = 'delete'
|
||||
oldValue = array.last(prev.content.getContent())
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (prev !== null && this.deletes(prev)) {
|
||||
action = 'update'
|
||||
oldValue = array.last(prev.content.getContent())
|
||||
} else {
|
||||
action = 'add'
|
||||
oldValue = undefined
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.deletes(item)) {
|
||||
action = 'delete'
|
||||
oldValue = array.last(/** @type {Item} */ item.content.getContent())
|
||||
} else {
|
||||
return // nop
|
||||
}
|
||||
}
|
||||
keys.set(key, { action, oldValue })
|
||||
}
|
||||
})
|
||||
this._changes = changes
|
||||
}
|
||||
return /** @type {any} */ (changes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,47 +1,39 @@
|
||||
|
||||
/**
|
||||
* @module encoding
|
||||
*
|
||||
* We use the first five bits in the info flag for determining the type of the struct.
|
||||
*
|
||||
* 0: GC
|
||||
* 1: Item with Deleted content
|
||||
* 2: Item with JSON content
|
||||
* 3: Item with Binary content
|
||||
* 4: Item with String content
|
||||
* 5: Item with Embed content (for richtext content)
|
||||
* 6: Item with Format content (a formatting marker for richtext content)
|
||||
* 7: Item with Type
|
||||
*/
|
||||
|
||||
import {
|
||||
findIndexSS,
|
||||
GCRef,
|
||||
ItemBinaryRef,
|
||||
ItemDeletedRef,
|
||||
ItemEmbedRef,
|
||||
ItemFormatRef,
|
||||
ItemJSONRef,
|
||||
ItemStringRef,
|
||||
ItemTypeRef,
|
||||
ItemRef,
|
||||
writeID,
|
||||
createID,
|
||||
readID,
|
||||
getState,
|
||||
getStates,
|
||||
readDeleteSet,
|
||||
getStateVector,
|
||||
readAndApplyDeleteSet,
|
||||
writeDeleteSet,
|
||||
createDeleteSetFromStructStore,
|
||||
Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line
|
||||
transact,
|
||||
Doc, Transaction, AbstractStruct, StructStore, ID // 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'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export const structRefs = [
|
||||
GCRef,
|
||||
ItemBinaryRef,
|
||||
ItemDeletedRef,
|
||||
ItemEmbedRef,
|
||||
ItemFormatRef,
|
||||
ItemJSONRef,
|
||||
ItemStringRef,
|
||||
ItemTypeRef
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Array<AbstractStruct>} structs All structs by `client`
|
||||
@@ -68,19 +60,19 @@ const writeStructs = (encoder, structs, client, clock) => {
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {number} numOfStructs
|
||||
* @param {ID} nextID
|
||||
* @return {Array<AbstractStructRef>}
|
||||
* @return {Array<GCRef|ItemRef>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const readStructRefs = (decoder, numOfStructs, nextID) => {
|
||||
/**
|
||||
* @type {Array<AbstractStructRef>}
|
||||
* @type {Array<GCRef|ItemRef>}
|
||||
*/
|
||||
const refs = []
|
||||
for (let i = 0; i < numOfStructs; i++) {
|
||||
const info = decoding.readUint8(decoder)
|
||||
const ref = new structRefs[binary.BITS5 & info](decoder, nextID, info)
|
||||
const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, nextID, info) : new ItemRef(decoder, nextID, info)
|
||||
nextID = createID(nextID.client, nextID.clock + ref.length)
|
||||
refs.push(ref)
|
||||
}
|
||||
@@ -104,7 +96,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
sm.set(client, clock)
|
||||
}
|
||||
})
|
||||
getStates(store).forEach((clock, client) => {
|
||||
getStateVector(store).forEach((clock, client) => {
|
||||
if (!_sm.has(client)) {
|
||||
sm.set(client, 0)
|
||||
}
|
||||
@@ -119,14 +111,14 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
* @return {Map<number,Array<AbstractStructRef>>}
|
||||
* @return {Map<number,Array<GCRef|ItemRef>>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readClientsStructRefs = decoder => {
|
||||
/**
|
||||
* @type {Map<number,Array<AbstractStructRef>>}
|
||||
* @type {Map<number,Array<GCRef|ItemRef>>}
|
||||
*/
|
||||
const clientRefs = new Map()
|
||||
const numOfStateUpdates = decoding.readVarUint(decoder)
|
||||
@@ -193,7 +185,7 @@ const resumeStructIntegration = (transaction, store) => {
|
||||
structRefs.refs[structRefs.i] = ref
|
||||
stack[stack.length - 1] = 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.client - r2.id.client)
|
||||
structRefs.refs = structRefs.refs.slice(structRefs.i).sort((r1, r2) => r1.id.clock - r2.id.clock)
|
||||
structRefs.i = 0
|
||||
continue
|
||||
}
|
||||
@@ -239,7 +231,7 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
|
||||
const pendingReaders = store.pendingDeleteReaders
|
||||
store.pendingDeleteReaders = []
|
||||
for (let i = 0; i < pendingReaders.length; i++) {
|
||||
readDeleteSet(pendingReaders[i], transaction, store)
|
||||
readAndApplyDeleteSet(pendingReaders[i], transaction, store)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,11 +242,11 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.y.store, transaction.beforeState)
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
* @param {Map<number, Array<AbstractStructRef>>} clientsStructsRefs
|
||||
* @param {Map<number, Array<GCRef|ItemRef>>} clientsStructsRefs
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
@@ -297,25 +289,135 @@ export const readStructs = (decoder, transaction, store) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and apply a document update.
|
||||
*
|
||||
* This function has the same effect as `applyUpdate` but accepts an decoder.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {Doc} ydoc
|
||||
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readModel = (decoder, transaction, store) => {
|
||||
readStructs(decoder, transaction, store)
|
||||
readDeleteSet(decoder, transaction, store)
|
||||
export const readUpdate = (decoder, ydoc, transactionOrigin) =>
|
||||
transact(ydoc, transaction => {
|
||||
readStructs(decoder, transaction, ydoc.store)
|
||||
readAndApplyDeleteSet(decoder, transaction, ydoc.store)
|
||||
}, transactionOrigin, false)
|
||||
|
||||
/**
|
||||
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
|
||||
*
|
||||
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
|
||||
*
|
||||
* @param {Doc} ydoc
|
||||
* @param {Uint8Array} update
|
||||
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const applyUpdate = (ydoc, update, transactionOrigin) =>
|
||||
readUpdate(decoding.createDecoder(update), ydoc, transactionOrigin)
|
||||
|
||||
/**
|
||||
* 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 {encoding.Encoder} 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
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => {
|
||||
writeClientsStructs(encoder, doc.store, targetStateVector)
|
||||
writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {StructStore} store
|
||||
* @param {Map<number,number>} [targetState] The state of the target that receives the update. Leave empty to write all known structs
|
||||
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
|
||||
* only write the operations that are missing.
|
||||
*
|
||||
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||
* @return {Uint8Array}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeModel = (encoder, store, targetState = new Map()) => {
|
||||
writeClientsStructs(encoder, store, targetState)
|
||||
writeDeleteSet(encoder, createDeleteSetFromStructStore(store))
|
||||
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => {
|
||||
const encoder = encoding.createEncoder()
|
||||
const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
|
||||
writeStateAsUpdate(encoder, doc, targetStateVector)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read state vector from Decoder and return as Map
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readStateVector = decoder => {
|
||||
const ss = new Map()
|
||||
const ssLength = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
const client = decoding.readVarUint(decoder)
|
||||
const clock = decoding.readVarUint(decoder)
|
||||
ss.set(client, clock)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
* Read decodedState and return State as Map.
|
||||
*
|
||||
* @param {Uint8Array} decodedState
|
||||
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const decodeStateVector = decodedState => readStateVector(decoding.createDecoder(decodedState))
|
||||
|
||||
/**
|
||||
* Write State Vector to `lib0/encoding.js#Encoder`.
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Map<number,number>} sv
|
||||
* @function
|
||||
*/
|
||||
export const writeStateVector = (encoder, sv) => {
|
||||
encoding.writeVarUint(encoder, sv.size)
|
||||
sv.forEach((clock, client) => {
|
||||
encoding.writeVarUint(encoder, client)
|
||||
encoding.writeVarUint(encoder, clock)
|
||||
})
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
* Write State Vector to `lib0/encoding.js#Encoder`.
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Doc} doc
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encoder, getStateVector(doc.store))
|
||||
|
||||
/**
|
||||
* Encode State as Uint8Array.
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @return {Uint8Array}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const encodeStateVector = doc => {
|
||||
const encoder = encoding.createEncoder()
|
||||
writeDocumentStateVector(encoder, doc)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
|
||||
import { AbstractType } from '../internals.js' // eslint-disable-line
|
||||
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Check if `parent` is a parent of `child`.
|
||||
*
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {AbstractType<any>} child
|
||||
* @param {Item|null} child
|
||||
* @return {Boolean} Whether `parent` is a parent of `child`.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const isParentOf = (parent, child) => {
|
||||
while (child._item !== null) {
|
||||
if (child === parent) {
|
||||
while (child !== null) {
|
||||
if (child.parent === parent) {
|
||||
return true
|
||||
}
|
||||
child = child._item.parent
|
||||
child = child.parent._item
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
import * as t from 'lib0/testing.js'
|
||||
|
||||
import {
|
||||
structRefs,
|
||||
structGCRefNumber,
|
||||
structBinaryRefNumber,
|
||||
structDeletedRefNumber,
|
||||
structEmbedRefNumber,
|
||||
structFormatRefNumber,
|
||||
structJSONRefNumber,
|
||||
structStringRefNumber,
|
||||
structTypeRefNumber,
|
||||
GCRef,
|
||||
ItemBinaryRef,
|
||||
ItemDeletedRef,
|
||||
ItemEmbedRef,
|
||||
ItemFormatRef,
|
||||
ItemJSONRef,
|
||||
ItemStringRef,
|
||||
ItemTypeRef
|
||||
contentRefs,
|
||||
readContentBinary,
|
||||
readContentDeleted,
|
||||
readContentString,
|
||||
readContentJSON,
|
||||
readContentEmbed,
|
||||
readContentType,
|
||||
readContentFormat,
|
||||
readContentAny
|
||||
} from '../src/internals.js'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testStructReferences = tc => {
|
||||
t.assert(structRefs.length === 8)
|
||||
t.assert(structRefs[structGCRefNumber] === GCRef)
|
||||
t.assert(structRefs[structBinaryRefNumber] === ItemBinaryRef)
|
||||
t.assert(structRefs[structDeletedRefNumber] === ItemDeletedRef)
|
||||
t.assert(structRefs[structEmbedRefNumber] === ItemEmbedRef)
|
||||
t.assert(structRefs[structFormatRefNumber] === ItemFormatRef)
|
||||
t.assert(structRefs[structJSONRefNumber] === ItemJSONRef)
|
||||
t.assert(structRefs[structStringRefNumber] === ItemStringRef)
|
||||
t.assert(structRefs[structTypeRefNumber] === ItemTypeRef)
|
||||
t.assert(contentRefs.length === 9)
|
||||
t.assert(contentRefs[1] === readContentDeleted)
|
||||
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
|
||||
t.assert(contentRefs[3] === readContentBinary)
|
||||
t.assert(contentRefs[4] === readContentString)
|
||||
t.assert(contentRefs[5] === readContentEmbed)
|
||||
t.assert(contentRefs[6] === readContentFormat)
|
||||
t.assert(contentRefs[7] === readContentType)
|
||||
t.assert(contentRefs[8] === readContentAny)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as map from './y-map.tests.js'
|
||||
import * as text from './y-text.tests.js'
|
||||
import * as xml from './y-xml.tests.js'
|
||||
import * as encoding from './encoding.tests.js'
|
||||
import * as undoredo from './undo-redo.tests.js'
|
||||
|
||||
import { runTests } from 'lib0/testing.js'
|
||||
import { isBrowser, isNode } from 'lib0/environment.js'
|
||||
@@ -13,7 +14,7 @@ if (isBrowser) {
|
||||
log.createVConsole(document.body)
|
||||
}
|
||||
runTests({
|
||||
map, array, text, xml, encoding
|
||||
map, array, text, xml, encoding, undoredo
|
||||
}).then(success => {
|
||||
/* istanbul ignore next */
|
||||
if (isNode) {
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as Y from '../src/index.js'
|
||||
|
||||
import {
|
||||
createDeleteSetFromStructStore,
|
||||
getStates,
|
||||
AbstractItem,
|
||||
DeleteSet, StructStore // eslint-disable-line
|
||||
getStateVector,
|
||||
Item,
|
||||
DeleteItem, DeleteSet, StructStore, Doc // eslint-disable-line
|
||||
} from '../src/internals.js'
|
||||
|
||||
import * as t from 'lib0/testing.js'
|
||||
@@ -12,25 +12,12 @@ 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'
|
||||
|
||||
/**
|
||||
* @param {Y.Transaction} transaction
|
||||
* @param {TestYInstance} y
|
||||
*/
|
||||
const afterTransaction = (transaction, y) => {
|
||||
if (transaction.origin !== y.tc) {
|
||||
const m = transaction.updateMessage
|
||||
if (m !== null) {
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeUpdate(encoder, m)
|
||||
broadcastMessage(y, encoding.toBuffer(encoder))
|
||||
}
|
||||
}
|
||||
}
|
||||
import * as object from 'lib0/object.js'
|
||||
export * from '../src/internals.js'
|
||||
|
||||
/**
|
||||
* @param {TestYInstance} y // publish message created by `y` to all other online clients
|
||||
* @param {ArrayBuffer} m
|
||||
* @param {Uint8Array} m
|
||||
*/
|
||||
const broadcastMessage = (y, m) => {
|
||||
if (y.tc.onlineConns.has(y)) {
|
||||
@@ -42,7 +29,7 @@ const broadcastMessage = (y, m) => {
|
||||
}
|
||||
}
|
||||
|
||||
export class TestYInstance extends Y.Y {
|
||||
export class TestYInstance extends Doc {
|
||||
/**
|
||||
* @param {TestConnector} testConnector
|
||||
* @param {number} clientID
|
||||
@@ -55,14 +42,21 @@ export class TestYInstance extends Y.Y {
|
||||
*/
|
||||
this.tc = testConnector
|
||||
/**
|
||||
* @type {Map<TestYInstance, Array<ArrayBuffer>>}
|
||||
* @type {Map<TestYInstance, Array<Uint8Array>>}
|
||||
*/
|
||||
this.receiving = new Map()
|
||||
testConnector.allConns.add(this)
|
||||
// set up observe on local model
|
||||
this.on('afterTransactionCleanup', afterTransaction)
|
||||
this.on('update', /** @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.connect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from TestConnector.
|
||||
*/
|
||||
@@ -70,6 +64,7 @@ export class TestYInstance extends Y.Y {
|
||||
this.receiving = new Map()
|
||||
this.tc.onlineConns.delete(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Append yourself to the list of known Y instances in testconnector.
|
||||
* Also initiate sync with all clients.
|
||||
@@ -78,24 +73,25 @@ export class TestYInstance extends Y.Y {
|
||||
if (!this.tc.onlineConns.has(this)) {
|
||||
this.tc.onlineConns.add(this)
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeSyncStep1(encoder, this.store)
|
||||
syncProtocol.writeSyncStep1(encoder, this)
|
||||
// publish SyncStep1
|
||||
broadcastMessage(this, encoding.toBuffer(encoder))
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||
this.tc.onlineConns.forEach(remoteYInstance => {
|
||||
if (remoteYInstance !== this) {
|
||||
// remote instance sends instance to this instance
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeSyncStep1(encoder, remoteYInstance.store)
|
||||
this._receive(encoding.toBuffer(encoder), remoteYInstance)
|
||||
syncProtocol.writeSyncStep1(encoder, remoteYInstance)
|
||||
this._receive(encoding.toUint8Array(encoder), remoteYInstance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive a message from another client. This message is only appended to the list of receiving messages.
|
||||
* TestConnector decides when this client actually reads this message.
|
||||
*
|
||||
* @param {ArrayBuffer} message
|
||||
* @param {Uint8Array} message
|
||||
* @param {TestYInstance} remoteClient
|
||||
*/
|
||||
_receive (message, remoteClient) {
|
||||
@@ -132,6 +128,7 @@ export class TestConnector {
|
||||
*/
|
||||
this.prng = gen
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Y instance and add it to the list of connections
|
||||
* @param {number} clientID
|
||||
@@ -139,6 +136,7 @@ export class TestConnector {
|
||||
createY (clientID) {
|
||||
return new TestYInstance(this, clientID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose random connection and flush a random message from a random sender.
|
||||
*
|
||||
@@ -164,12 +162,13 @@ export class TestConnector {
|
||||
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc)
|
||||
if (encoding.length(encoder) > 0) {
|
||||
// send reply message
|
||||
sender._receive(encoding.toBuffer(encoder), receiver)
|
||||
sender._receive(encoding.toUint8Array(encoder), receiver)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} True iff this function actually flushed something
|
||||
*/
|
||||
@@ -180,16 +179,20 @@ export class TestConnector {
|
||||
}
|
||||
return didSomething
|
||||
}
|
||||
|
||||
reconnectAll () {
|
||||
this.allConns.forEach(conn => conn.connect())
|
||||
}
|
||||
|
||||
disconnectAll () {
|
||||
this.allConns.forEach(conn => conn.disconnect())
|
||||
}
|
||||
|
||||
syncAll () {
|
||||
this.reconnectAll()
|
||||
this.flushAllMessages()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether it was possible to disconnect a randon connection.
|
||||
*/
|
||||
@@ -200,6 +203,7 @@ export class TestConnector {
|
||||
prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether it was possible to reconnect a random connection.
|
||||
*/
|
||||
@@ -249,8 +253,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||
}
|
||||
testConnector.syncAll()
|
||||
result.testObjects = result.users.map(initTestObject || (() => null))
|
||||
// @ts-ignore
|
||||
return result
|
||||
return /** @type {any} */ (result)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,7 +268,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||
export const compare = users => {
|
||||
users.forEach(u => u.connect())
|
||||
while (users[0].tc.flushAllMessages()) {}
|
||||
const userArrayValues = users.map(u => u.getArray('array').toJSON().map(val => JSON.stringify(val)))
|
||||
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.XmlElement).toString())
|
||||
const userTextValues = users.map(u => u.getText('text').toDelta())
|
||||
@@ -274,15 +277,29 @@ export const compare = users => {
|
||||
t.assert(u.store.pendingStack.length === 0)
|
||||
t.assert(u.store.pendingClientsStructRefs.size === 0)
|
||||
}
|
||||
// Test Array iterator
|
||||
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
|
||||
// Test Map iterator
|
||||
const ymapkeys = Array.from(users[0].getMap('map').keys())
|
||||
t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
|
||||
ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key)))
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
const mapRes = {}
|
||||
for (const [k, v] of users[0].getMap('map')) {
|
||||
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
|
||||
}
|
||||
t.compare(userMapValues[0], mapRes)
|
||||
// Compare all users
|
||||
for (let i = 0; i < users.length - 1; i++) {
|
||||
t.compare(userArrayValues[i].length, users[i].getArray('array').length)
|
||||
t.compare(userArrayValues[i], userArrayValues[i + 1])
|
||||
t.compare(userMapValues[i], userMapValues[i + 1])
|
||||
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
||||
// @ts-ignore
|
||||
t.compare(userTextValues[i].map(a => a.insert).join('').length, users[i].getText('text').length)
|
||||
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(getStates(users[i].store), getStates(users[i + 1].store))
|
||||
t.compare(getStateVector(users[i].store), getStateVector(users[i + 1].store))
|
||||
compareDS(createDeleteSetFromStructStore(users[i].store), createDeleteSetFromStructStore(users[i + 1].store))
|
||||
compareStructStores(users[i].store, users[i + 1].store)
|
||||
}
|
||||
@@ -290,8 +307,8 @@ export const compare = users => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractItem?} a
|
||||
* @param {AbstractItem?} b
|
||||
* @param {Item?} a
|
||||
* @param {Item?} b
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
||||
@@ -303,11 +320,10 @@ export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y
|
||||
export const compareStructStores = (ss1, ss2) => {
|
||||
t.assert(ss1.clients.size === ss2.clients.size)
|
||||
for (const [client, structs1] of ss1.clients) {
|
||||
const structs2 = ss2.clients.get(client)
|
||||
const structs2 = /** @type {Array<Y.AbstractStruct>} */ (ss2.clients.get(client))
|
||||
t.assert(structs2 !== undefined && structs1.length === structs2.length)
|
||||
for (let i = 0; i < structs1.length; i++) {
|
||||
const s1 = structs1[i]
|
||||
// @ts-ignore
|
||||
const s2 = structs2[i]
|
||||
// checks for abstract struct
|
||||
if (
|
||||
@@ -318,9 +334,9 @@ export const compareStructStores = (ss1, ss2) => {
|
||||
) {
|
||||
t.fail('Structs dont match')
|
||||
}
|
||||
if (s1 instanceof AbstractItem) {
|
||||
if (s1 instanceof Item) {
|
||||
if (
|
||||
!(s2 instanceof AbstractItem) ||
|
||||
!(s2 instanceof Item) ||
|
||||
!((s1.left === null && s2.left === null) || (s1.left !== null && s2.left !== null && Y.compareIDs(s1.left.lastId, s2.left.lastId))) ||
|
||||
!compareItemIDs(s1.right, s2.right) ||
|
||||
!Y.compareIDs(s1.origin, s2.origin) ||
|
||||
@@ -346,11 +362,10 @@ export const compareStructStores = (ss1, ss2) => {
|
||||
export const compareDS = (ds1, ds2) => {
|
||||
t.assert(ds1.clients.size === ds2.clients.size)
|
||||
for (const [client, deleteItems1] of ds1.clients) {
|
||||
const deleteItems2 = ds2.clients.get(client)
|
||||
const deleteItems2 = /** @type {Array<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]
|
||||
// @ts-ignore
|
||||
const di2 = deleteItems2[i]
|
||||
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||
t.fail('DeleteSets dont match')
|
||||
@@ -369,13 +384,13 @@ export const compareDS = (ds1, ds2) => {
|
||||
/**
|
||||
* @template T
|
||||
* @param {t.TestCase} tc
|
||||
* @param {Array<function(TestYInstance,prng.PRNG,T):void>} mods
|
||||
* @param {Array<function(Y.Doc,prng.PRNG,T):void>} mods
|
||||
* @param {number} iterations
|
||||
* @param {InitTestObjectCallback<T>} [initTestObject]
|
||||
*/
|
||||
export const applyRandomTests = (tc, mods, iterations, initTestObject) => {
|
||||
const gen = tc.prng
|
||||
const result = init(tc, { users: 5 }, initTestObject || (() => null))
|
||||
const result = init(tc, { users: 5 }, initTestObject)
|
||||
const { testConnector, users } = result
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
if (prng.int31(gen, 0, 100) <= 2) {
|
||||
|
||||
248
tests/undo-redo.tests.js
Normal file
248
tests/undo-redo.tests.js
Normal file
@@ -0,0 +1,248 @@
|
||||
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||
|
||||
import {
|
||||
UndoManager
|
||||
} from '../src/internals.js'
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoText = tc => {
|
||||
const { testConnector, text0, text1 } = init(tc, { users: 3 })
|
||||
const undoManager = new UndoManager(text0)
|
||||
|
||||
// items that are added & deleted in the same transaction won't be undo
|
||||
text0.insert(0, 'test')
|
||||
text0.delete(0, 4)
|
||||
undoManager.undo()
|
||||
t.assert(text0.toString() === '')
|
||||
|
||||
// follow redone items
|
||||
text0.insert(0, 'a')
|
||||
undoManager.stopCapturing()
|
||||
text0.delete(0, 1)
|
||||
undoManager.stopCapturing()
|
||||
undoManager.undo()
|
||||
t.assert(text0.toString() === 'a')
|
||||
undoManager.undo()
|
||||
t.assert(text0.toString() === '')
|
||||
|
||||
text0.insert(0, 'abc')
|
||||
text1.insert(0, 'xyz')
|
||||
testConnector.syncAll()
|
||||
undoManager.undo()
|
||||
t.assert(text0.toString() === 'xyz')
|
||||
undoManager.redo()
|
||||
t.assert(text0.toString() === 'abcxyz')
|
||||
testConnector.syncAll()
|
||||
text1.delete(0, 1)
|
||||
testConnector.syncAll()
|
||||
undoManager.undo()
|
||||
t.assert(text0.toString() === 'xyz')
|
||||
undoManager.redo()
|
||||
t.assert(text0.toString() === 'bcxyz')
|
||||
// test marks
|
||||
text0.format(1, 3, { bold: true })
|
||||
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])
|
||||
undoManager.undo()
|
||||
t.compare(text0.toDelta(), [{ insert: 'bcxyz' }])
|
||||
undoManager.redo()
|
||||
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoMap = tc => {
|
||||
const { testConnector, map0, map1 } = init(tc, { users: 2 })
|
||||
map0.set('a', 0)
|
||||
const undoManager = new UndoManager(map0)
|
||||
map0.set('a', 1)
|
||||
undoManager.undo()
|
||||
t.assert(map0.get('a') === 0)
|
||||
undoManager.redo()
|
||||
t.assert(map0.get('a') === 1)
|
||||
// testing sub-types and if it can restore a whole type
|
||||
const subType = new Y.Map()
|
||||
map0.set('a', subType)
|
||||
subType.set('x', 42)
|
||||
t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
|
||||
undoManager.undo()
|
||||
t.assert(map0.get('a') === 1)
|
||||
undoManager.redo()
|
||||
t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
|
||||
testConnector.syncAll()
|
||||
// if content is overwritten by another user, undo operations should be skipped
|
||||
map1.set('a', 44)
|
||||
testConnector.syncAll()
|
||||
undoManager.undo()
|
||||
t.assert(map0.get('a') === 44)
|
||||
undoManager.redo()
|
||||
t.assert(map0.get('a') === 44)
|
||||
|
||||
// test setting value multiple times
|
||||
map0.set('b', 'initial')
|
||||
undoManager.stopCapturing()
|
||||
map0.set('b', 'val1')
|
||||
map0.set('b', 'val2')
|
||||
undoManager.stopCapturing()
|
||||
undoManager.undo()
|
||||
t.assert(map0.get('b') === 'initial')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoArray = tc => {
|
||||
const { testConnector, array0, array1 } = init(tc, { users: 3 })
|
||||
const undoManager = new UndoManager(array0)
|
||||
array0.insert(0, [1, 2, 3])
|
||||
array1.insert(0, [4, 5, 6])
|
||||
testConnector.syncAll()
|
||||
t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6])
|
||||
undoManager.undo()
|
||||
t.compare(array0.toArray(), [4, 5, 6])
|
||||
undoManager.redo()
|
||||
t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6])
|
||||
testConnector.syncAll()
|
||||
array1.delete(0, 1) // user1 deletes [1]
|
||||
testConnector.syncAll()
|
||||
undoManager.undo()
|
||||
t.compare(array0.toArray(), [4, 5, 6])
|
||||
undoManager.redo()
|
||||
t.compare(array0.toArray(), [2, 3, 4, 5, 6])
|
||||
array0.delete(0, 5)
|
||||
// test nested structure
|
||||
const ymap = new Y.Map()
|
||||
array0.insert(0, [ymap])
|
||||
t.compare(array0.toJSON(), [{}])
|
||||
undoManager.stopCapturing()
|
||||
ymap.set('a', 1)
|
||||
t.compare(array0.toJSON(), [{ a: 1 }])
|
||||
undoManager.undo()
|
||||
t.compare(array0.toJSON(), [{}])
|
||||
undoManager.undo()
|
||||
t.compare(array0.toJSON(), [2, 3, 4, 5, 6])
|
||||
undoManager.redo()
|
||||
t.compare(array0.toJSON(), [{}])
|
||||
undoManager.redo()
|
||||
t.compare(array0.toJSON(), [{ a: 1 }])
|
||||
testConnector.syncAll()
|
||||
array1.get(0).set('b', 2)
|
||||
testConnector.syncAll()
|
||||
t.compare(array0.toJSON(), [{ a: 1, b: 2 }])
|
||||
undoManager.undo()
|
||||
t.compare(array0.toJSON(), [{ b: 2 }])
|
||||
undoManager.undo()
|
||||
t.compare(array0.toJSON(), [2, 3, 4, 5, 6])
|
||||
undoManager.redo()
|
||||
t.compare(array0.toJSON(), [{ b: 2 }])
|
||||
undoManager.redo()
|
||||
t.compare(array0.toJSON(), [{ a: 1, b: 2 }])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoXml = tc => {
|
||||
const { xml0 } = init(tc, { users: 3 })
|
||||
const undoManager = new UndoManager(xml0)
|
||||
const child = new Y.XmlElement('p')
|
||||
xml0.insert(0, [child])
|
||||
const textchild = new Y.XmlText('content')
|
||||
child.insert(0, [textchild])
|
||||
t.assert(xml0.toString() === '<undefined><p>content</p></undefined>')
|
||||
// format textchild and revert that change
|
||||
undoManager.stopCapturing()
|
||||
textchild.format(3, 4, { bold: {} })
|
||||
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
|
||||
undoManager.undo()
|
||||
t.assert(xml0.toString() === '<undefined><p>content</p></undefined>')
|
||||
undoManager.redo()
|
||||
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
|
||||
xml0.delete(0, 1)
|
||||
t.assert(xml0.toString() === '<undefined></undefined>')
|
||||
undoManager.undo()
|
||||
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testUndoEvents = tc => {
|
||||
const { text0 } = init(tc, { users: 3 })
|
||||
const undoManager = new UndoManager(text0)
|
||||
let counter = 0
|
||||
let receivedMetadata = -1
|
||||
undoManager.on('stack-item-added', /** @param {any} event */ event => {
|
||||
t.assert(event.type != null)
|
||||
event.stackItem.meta.set('test', counter++)
|
||||
})
|
||||
undoManager.on('stack-item-popped', /** @param {any} event */ event => {
|
||||
t.assert(event.type != null)
|
||||
receivedMetadata = event.stackItem.meta.get('test')
|
||||
})
|
||||
text0.insert(0, 'abc')
|
||||
undoManager.undo()
|
||||
t.assert(receivedMetadata === 0)
|
||||
undoManager.redo()
|
||||
t.assert(receivedMetadata === 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} 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]) })
|
||||
users[0].transact(() => {
|
||||
text0.insert(0, 'abc')
|
||||
}, 42)
|
||||
t.assert(text0.toString() === 'abc')
|
||||
undoManager.undo()
|
||||
t.assert(text0.toString() === '')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testTypeScope = tc => {
|
||||
const { array0 } = init(tc, { users: 3 })
|
||||
// only track origins that are numbers
|
||||
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])
|
||||
text1.insert(0, 'abc')
|
||||
t.assert(undoManager.undoStack.length === 0)
|
||||
t.assert(undoManagerBoth.undoStack.length === 1)
|
||||
t.assert(text1.toString() === 'abc')
|
||||
undoManager.undo()
|
||||
t.assert(text1.toString() === 'abc')
|
||||
undoManagerBoth.undo()
|
||||
t.assert(text1.toString() === '')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
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 map0 = new Y.Map()
|
||||
map0.set('hi', 1)
|
||||
const map1 = new Y.Map()
|
||||
array0.insert(0, [map0, map1])
|
||||
undoManager.undo()
|
||||
t.assert(array0.length === 1)
|
||||
array0.get(0)
|
||||
t.assert(Array.from(array0.get(0).keys()).length === 1)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { init, compare, applyRandomTests, TestYInstance } from './testHelper.js' // eslint-disable-line
|
||||
import { init, compare, applyRandomTests, Doc } 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'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
@@ -25,10 +26,10 @@ export const testDeleteInsert = tc => {
|
||||
*/
|
||||
export const testInsertThreeElementsTryRegetProperty = tc => {
|
||||
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
|
||||
array0.insert(0, [1, 2, 3])
|
||||
t.compare(array0.toJSON(), [1, 2, 3], '.toJSON() works')
|
||||
array0.insert(0, [1, true, false])
|
||||
t.compare(array0.toJSON(), [1, true, false], '.toJSON() works')
|
||||
testConnector.flushAllMessages()
|
||||
t.compare(array1.toJSON(), [1, 2, 3], '.toJSON() works after sync')
|
||||
t.compare(array1.toJSON(), [1, true, false], '.toJSON() works after sync')
|
||||
compare(users)
|
||||
}
|
||||
|
||||
@@ -191,6 +192,33 @@ export const testInsertAndDeleteEventsForTypes = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testChangeEvent = tc => {
|
||||
const { array0, users } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let changes = null
|
||||
array0.observe(e => {
|
||||
changes = e.changes
|
||||
})
|
||||
const newArr = new Y.Array()
|
||||
array0.insert(0, [newArr, 4, 'dtrn'])
|
||||
t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0)
|
||||
t.compare(changes.delta, [{ insert: [newArr, 4, 'dtrn'] }])
|
||||
changes = null
|
||||
array0.delete(0, 2)
|
||||
t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2)
|
||||
t.compare(changes.delta, [{ delete: 2 }])
|
||||
changes = null
|
||||
array0.insert(1, [0.1])
|
||||
t.assert(changes !== null && changes.added.size === 1 && changes.deleted.size === 0)
|
||||
t.compare(changes.delta, [{ retain: 1 }, { insert: [0.1] }])
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -199,7 +227,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
|
||||
/**
|
||||
* @type {Array<Object<string,any>>}
|
||||
*/
|
||||
let events = []
|
||||
const events = []
|
||||
array0.observe(e => {
|
||||
events.push(e)
|
||||
})
|
||||
@@ -210,6 +238,24 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* This issue has been reported here https://github.com/yjs/yjs/issues/155
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testNewChildDoesNotEmitEventInTransaction = tc => {
|
||||
const { array0, users } = init(tc, { users: 2 })
|
||||
let fired = false
|
||||
users[0].transact(() => {
|
||||
const newMap = new Y.Map()
|
||||
newMap.observe(() => {
|
||||
fired = true
|
||||
})
|
||||
array0.insert(0, [newMap])
|
||||
newMap.set('tst', 42)
|
||||
})
|
||||
t.assert(!fired, 'Event does not trigger')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -263,7 +309,7 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testIteratingArrayContainingTypes = tc => {
|
||||
const y = new Y.Y()
|
||||
const y = new Y.Doc()
|
||||
const arr = y.getArray('arr')
|
||||
const numItems = 10
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
@@ -272,7 +318,7 @@ export const testIteratingArrayContainingTypes = tc => {
|
||||
arr.push([map])
|
||||
}
|
||||
let cnt = 0
|
||||
for (let item of arr) {
|
||||
for (const item of arr) {
|
||||
t.assert(item.get('value') === cnt++, 'value is correct')
|
||||
}
|
||||
y.destroy()
|
||||
@@ -282,7 +328,7 @@ let _uniqueNumber = 0
|
||||
const getUniqueNumber = () => _uniqueNumber++
|
||||
|
||||
/**
|
||||
* @type {Array<function(TestYInstance,prng.PRNG):void>}
|
||||
* @type {Array<function(Doc,prng.PRNG,any):void>}
|
||||
*/
|
||||
const arrayTransactions = [
|
||||
function insert (user, gen) {
|
||||
@@ -317,12 +363,12 @@ const arrayTransactions = [
|
||||
var length = yarray.length
|
||||
if (length > 0) {
|
||||
var somePos = prng.int31(gen, 0, length - 1)
|
||||
var delLength = prng.int31(gen, 1, Math.min(2, length - somePos))
|
||||
var delLength = prng.int31(gen, 1, math.min(2, length - somePos))
|
||||
if (prng.bool(gen)) {
|
||||
var type = yarray.get(somePos)
|
||||
if (type.length > 0) {
|
||||
somePos = prng.int31(gen, 0, type.length - 1)
|
||||
delLength = prng.int31(gen, 0, Math.min(2, type.length - somePos))
|
||||
delLength = prng.int31(gen, 0, math.min(2, type.length - somePos))
|
||||
type.delete(somePos, delLength)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { init, compare, applyRandomTests, TestYInstance } from './testHelper.js' // eslint-disable-line
|
||||
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||
|
||||
import {
|
||||
compareIDs
|
||||
@@ -19,6 +19,8 @@ export const testBasicMapTests = tc => {
|
||||
map0.set('string', 'hello Y')
|
||||
map0.set('object', { key: { key2: 'value' } })
|
||||
map0.set('y-map', new Y.Map())
|
||||
map0.set('boolean1', true)
|
||||
map0.set('boolean0', false)
|
||||
const map = map0.get('y-map')
|
||||
map.set('y-array', new Y.Array())
|
||||
const array = map.get('y-array')
|
||||
@@ -27,6 +29,8 @@ export const testBasicMapTests = tc => {
|
||||
|
||||
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)')
|
||||
|
||||
@@ -35,12 +39,16 @@ export const testBasicMapTests = tc => {
|
||||
|
||||
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)')
|
||||
|
||||
// compare disconnected user
|
||||
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)')
|
||||
t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)')
|
||||
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
|
||||
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
|
||||
compare(users)
|
||||
@@ -58,7 +66,7 @@ export const testGetAndSetOfMapProperty = tc => {
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
for (let user of users) {
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'stuffy')
|
||||
t.assert(u.get('undefined') === undefined, 'undefined')
|
||||
@@ -100,7 +108,7 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
|
||||
map0.set('stuff', 'stuffy')
|
||||
t.compare(map0.get('stuff'), 'stuffy')
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'stuffy')
|
||||
}
|
||||
@@ -115,7 +123,7 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'c1')
|
||||
}
|
||||
@@ -131,7 +139,7 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
|
||||
map1.set('stuff', 'c1')
|
||||
map1.delete('stuff')
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
}
|
||||
@@ -148,7 +156,7 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'c3')
|
||||
}
|
||||
@@ -171,7 +179,7 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
|
||||
map3.set('stuff', 'c3')
|
||||
map3.delete('stuff')
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
for (const user of users) {
|
||||
var u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
}
|
||||
@@ -284,6 +292,104 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testChangeEvent = tc => {
|
||||
const { map0, users } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let changes = null
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let keyChange = null
|
||||
map0.observe(e => {
|
||||
changes = e.changes
|
||||
})
|
||||
map0.set('a', 1)
|
||||
keyChange = changes.keys.get('a')
|
||||
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
|
||||
map0.set('a', 2)
|
||||
keyChange = changes.keys.get('a')
|
||||
t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 1)
|
||||
users[0].transact(() => {
|
||||
map0.set('a', 3)
|
||||
map0.set('a', 4)
|
||||
})
|
||||
keyChange = changes.keys.get('a')
|
||||
t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 2)
|
||||
users[0].transact(() => {
|
||||
map0.set('b', 1)
|
||||
map0.set('b', 2)
|
||||
})
|
||||
keyChange = changes.keys.get('b')
|
||||
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
|
||||
users[0].transact(() => {
|
||||
map0.set('c', 1)
|
||||
map0.delete('c')
|
||||
})
|
||||
t.assert(changes !== null && changes.keys.size === 0)
|
||||
users[0].transact(() => {
|
||||
map0.set('d', 1)
|
||||
map0.set('d', 2)
|
||||
})
|
||||
keyChange = changes.keys.get('d')
|
||||
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testYmapEventExceptionsShouldCompleteTransaction = tc => {
|
||||
const doc = new Y.Doc()
|
||||
const map = doc.getMap('map')
|
||||
|
||||
let updateCalled = false
|
||||
let throwingObserverCalled = false
|
||||
let throwingDeepObserverCalled = false
|
||||
doc.on('update', () => {
|
||||
updateCalled = true
|
||||
})
|
||||
|
||||
const throwingObserver = () => {
|
||||
throwingObserverCalled = true
|
||||
throw new Error('Failure')
|
||||
}
|
||||
|
||||
const throwingDeepObserver = () => {
|
||||
throwingDeepObserverCalled = true
|
||||
throw new Error('Failure')
|
||||
}
|
||||
|
||||
map.observe(throwingObserver)
|
||||
map.observeDeep(throwingDeepObserver)
|
||||
|
||||
t.fails(() => {
|
||||
map.set('y', '2')
|
||||
})
|
||||
|
||||
t.assert(updateCalled)
|
||||
t.assert(throwingObserverCalled)
|
||||
t.assert(throwingDeepObserverCalled)
|
||||
|
||||
// check if it works again
|
||||
updateCalled = false
|
||||
throwingObserverCalled = false
|
||||
throwingDeepObserverCalled = false
|
||||
t.fails(() => {
|
||||
map.set('z', '3')
|
||||
})
|
||||
|
||||
t.assert(updateCalled)
|
||||
t.assert(throwingObserverCalled)
|
||||
t.assert(throwingDeepObserverCalled)
|
||||
|
||||
t.assert(map.get('z') === '3')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@@ -320,16 +426,16 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Array<function(TestYInstance,prng.PRNG):void>}
|
||||
* @type {Array<function(Doc,prng.PRNG):void>}
|
||||
*/
|
||||
const mapTransactions = [
|
||||
function set (user, gen) {
|
||||
let key = prng.oneOf(gen, ['one', 'two'])
|
||||
const key = prng.oneOf(gen, ['one', 'two'])
|
||||
var value = prng.utf16String(gen)
|
||||
user.getMap('map').set(key, value)
|
||||
},
|
||||
function setType (user, gen) {
|
||||
let key = prng.oneOf(gen, ['one', 'two'])
|
||||
const key = prng.oneOf(gen, ['one', 'two'])
|
||||
var type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
|
||||
user.getMap('map').set(key, type)
|
||||
if (type instanceof Y.Array) {
|
||||
@@ -339,7 +445,7 @@ const mapTransactions = [
|
||||
}
|
||||
},
|
||||
function _delete (user, gen) {
|
||||
let key = prng.oneOf(gen, ['one', 'two'])
|
||||
const key = prng.oneOf(gen, ['one', 'two'])
|
||||
user.getMap('map').delete(key)
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { init, compare } from './testHelper.js'
|
||||
|
||||
import * as Y from './testHelper.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
const { init, compare } = Y
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
@@ -27,6 +27,13 @@ export const testBasicInsertAndDelete = tc => {
|
||||
text0.delete(1, 1)
|
||||
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
|
||||
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
|
||||
|
||||
users[0].transact(() => {
|
||||
text0.insert(0, '1')
|
||||
text0.delete(0, 1)
|
||||
})
|
||||
t.compare(delta, [])
|
||||
|
||||
compare(users)
|
||||
}
|
||||
|
||||
@@ -56,7 +63,7 @@ export const testBasicFormat = tc => {
|
||||
t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }])
|
||||
t.compare(delta, [{ insert: 'z', attributes: { bold: true } }])
|
||||
// @ts-ignore
|
||||
t.assert(text0._start.right.right.right.string === 'b', 'Does not insert duplicate attribute marker')
|
||||
t.assert(text0._start.right.right.right.content.str === 'b', 'Does not insert duplicate attribute marker')
|
||||
text0.insert(0, 'y')
|
||||
t.assert(text0.toString() === 'yzb')
|
||||
t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }])
|
||||
@@ -67,3 +74,87 @@ export const testBasicFormat = tc => {
|
||||
t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }])
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testGetDeltaWithEmbeds = tc => {
|
||||
const { text0 } = init(tc, { users: 1 })
|
||||
text0.applyDelta([{
|
||||
insert: { linebreak: 's' }
|
||||
}])
|
||||
t.compare(text0.toDelta(), [{
|
||||
insert: { linebreak: 's' }
|
||||
}])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testSnapshot = tc => {
|
||||
const { text0 } = init(tc, { users: 1 })
|
||||
const doc0 = /** @type {Y.Doc} */ (text0.doc)
|
||||
doc0.gc = false
|
||||
text0.applyDelta([{
|
||||
insert: 'abcd'
|
||||
}])
|
||||
const snapshot1 = Y.snapshot(doc0)
|
||||
text0.applyDelta([{
|
||||
retain: 1
|
||||
}, {
|
||||
insert: 'x'
|
||||
}, {
|
||||
delete: 1
|
||||
}])
|
||||
const snapshot2 = Y.snapshot(doc0)
|
||||
text0.applyDelta([{
|
||||
retain: 2
|
||||
}, {
|
||||
delete: 3
|
||||
}, {
|
||||
insert: 'x'
|
||||
}, {
|
||||
delete: 1
|
||||
}])
|
||||
const state1 = text0.toDelta(snapshot1)
|
||||
t.compare(state1, [{ insert: 'abcd' }])
|
||||
const state2 = text0.toDelta(snapshot2)
|
||||
t.compare(state2, [{ insert: 'axcd' }])
|
||||
const state2Diff = text0.toDelta(snapshot2, snapshot1)
|
||||
// @ts-ignore Remove userid info
|
||||
state2Diff.forEach(v => {
|
||||
if (v.attributes && v.attributes.ychange) {
|
||||
delete v.attributes.ychange.user
|
||||
}
|
||||
})
|
||||
t.compare(state2Diff, [{ insert: 'a' }, { insert: 'x', attributes: { ychange: { type: 'added' } } }, { insert: 'b', attributes: { ychange: { type: 'removed' } } }, { insert: 'cd' }])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testSnapshotDeleteAfter = tc => {
|
||||
const { text0 } = init(tc, { users: 1 })
|
||||
const doc0 = /** @type {Y.Doc} */ (text0.doc)
|
||||
doc0.gc = false
|
||||
text0.applyDelta([{
|
||||
insert: 'abcd'
|
||||
}])
|
||||
const snapshot1 = Y.snapshot(doc0)
|
||||
text0.applyDelta([{
|
||||
retain: 4
|
||||
}, {
|
||||
insert: 'e'
|
||||
}])
|
||||
const state1 = text0.toDelta(snapshot1)
|
||||
t.compare(state1, [{ insert: 'abcd' }])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testToJson = tc => {
|
||||
const { text0 } = init(tc, { users: 1 })
|
||||
text0.insert(0, 'abc', { bold: true })
|
||||
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
|
||||
}
|
||||
|
||||
@@ -60,13 +60,13 @@ export const testEvents = tc => {
|
||||
*/
|
||||
export const testTreewalker = tc => {
|
||||
const { users, xml0 } = init(tc, { users: 3 })
|
||||
let paragraph1 = new Y.XmlElement('p')
|
||||
let paragraph2 = new Y.XmlElement('p')
|
||||
let text1 = new Y.XmlText('init')
|
||||
let text2 = new Y.XmlText('text')
|
||||
const paragraph1 = new Y.XmlElement('p')
|
||||
const paragraph2 = new Y.XmlElement('p')
|
||||
const text1 = new Y.XmlText('init')
|
||||
const text2 = new Y.XmlText('text')
|
||||
paragraph1.insert(0, [text1, text2])
|
||||
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
|
||||
let allParagraphs = xml0.querySelectorAll('p')
|
||||
const allParagraphs = xml0.querySelectorAll('p')
|
||||
t.assert(allParagraphs.length === 2, 'found exactly two paragraphs')
|
||||
t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1')
|
||||
t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2')
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
"checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
// "outDir": "./build", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "outFile": "./dist/yjs.js", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
"noEmit": true, /* Do not emit outputs. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
@@ -22,6 +22,7 @@
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"emitDeclarationOnly": true,
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
@@ -36,8 +37,13 @@
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
"paths": {
|
||||
"yjs": ["./src/index.js"],
|
||||
"lib0/*": ["node_modules/lib0/*"],
|
||||
"lib0/set.js": ["node_modules/lib0/set.js"],
|
||||
"lib0/function.js": ["node_modules/lib0/function.js"]
|
||||
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
@@ -54,9 +60,9 @@
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
"maxNodeModuleJsDepth": 1,
|
||||
"maxNodeModuleJsDepth": 5,
|
||||
// "types": ["./src/utils/typedefs.js"]
|
||||
},
|
||||
"include": ["./src/**/*", "./tests/**/*"],
|
||||
"include": ["./src/**/*.js", "./tests/**/*.js"],
|
||||
"exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user