Compare commits

...

23 Commits

Author SHA1 Message Date
Kevin Jahns
1e83b9418c 13.0.0-87 2019-05-28 14:20:44 +02:00
Kevin Jahns
ac3f672c80 Merge branch 'master' of github.com:y-js/yjs 2019-05-28 14:19:11 +02:00
Kevin Jahns
2192aa5821 Use generic Item with typed content to reduce cache misses 2019-05-28 14:18:20 +02:00
Kevin Jahns
70bb523005 Merge branch 'master' of github.com:y-js/yjs 2019-05-27 12:50:21 +02:00
Kevin Jahns
10ce6de57a import statement fix 2019-05-27 12:50:12 +02:00
Kevin Jahns
3fba4f25a5 Merge pull request #153 from calibr/124-text-embeds
process embeds in YText.toDelta
2019-05-25 13:04:10 +02:00
Kevin Jahns
66c35d8499 testing: do not stringify array values before comparing 2019-05-25 12:54:30 +02:00
Kevin Jahns
4c14157dcf 13.0.0-86 2019-05-25 12:50:05 +02:00
Kevin Jahns
ef6c382e20 fix array iterator on merged content. fixes #152 2019-05-25 12:49:08 +02:00
calibr
ee45b4fdd6 process embeds in YText.toDelta 2019-05-25 13:48:57 +03:00
Kevin Jahns
668e9e8a9b 13.0.0-85 2019-05-25 03:13:54 +02:00
Kevin Jahns
37a6d68543 implement support for boolean values. fixes #151 2019-05-25 03:12:56 +02:00
Kevin Jahns
f893198769 remove examples. fixes #149 2019-05-22 17:32:51 +02:00
Kevin Jahns
d3ee1a0ec2 Add editor support to v13 readme 2019-05-22 01:26:13 +02:00
Kevin Jahns
d6593412a2 13.0.0-84 2019-05-19 21:49:36 +02:00
Kevin Jahns
d31bf36531 use generated esm module by default 2019-05-19 21:48:09 +02:00
Kevin Jahns
a485f550db 13.0.0-83 2019-05-19 20:59:56 +02:00
Kevin Jahns
0610b16227 bump lib0 for webpack compatibility 2019-05-19 20:43:18 +02:00
Kevin Jahns
72e470c5f0 Fix ytext event.delta - items that are synced and deleted
When items are added and deleted in the same transaction, event.delta would recognize them as added (though they are actually deleted). Now it just ignores them.
2019-05-19 20:42:53 +02:00
Kevin Jahns
4d12a02e2f fix offset in state vector 2019-05-16 12:31:53 +02:00
Kevin Jahns
4a7d6f0a2d fix sorting bug that only affects older node versions (probably because old sorting algorithms are not stable) 2019-05-14 15:21:34 +02:00
Kevin Jahns
c80f446b5f README: update provider tutorial 2019-05-12 11:18:43 +02:00
Kevin Jahns
81a529d8dc update *getting started* yjs version 2019-05-07 15:43:09 +02:00
61 changed files with 2164 additions and 4839 deletions

View File

@@ -40,11 +40,13 @@ This repository contains a collection of shared types that can be observed for c
| Name                                                   | Cursors | Binding | Demo | | Name                                                   | Cursors | Binding | Demo |
|---|:-:|---|---| |---|:-:|---|---|
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/y-js/y-prosemirror) | [link](https://yjs.website/tutorial-prosemirror.html) | | [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/y-js/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) |
| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/y-js/y-quill) | [link](https://yjs.website/tutorial-quill.html) | | [Quill](https://quilljs.com/) | | [y-quill](http://github.com/y-js/y-quill) | [demo](https://yjs-demos.now.sh/quill/) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/y-js/y-codemirror) | [link](https://yjs.website/tutorial-codemirror.html) | | [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/y-js/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) |
| [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) | [demo](https://yjs-demos.now.sh/monaco/) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | | [y-monaco](http://github.com/y-js/y-monaco) | [link]() | | [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/y-js/y-ace) | [demo](https://yjs-demos.now.sh/ace/) |
| [Textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) | | [y-textarea](http://github.com/y-js/y-textarea) | [demo](https://yjs-demos.now.sh/textarea/) |
| [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) | | [y-dom](http://github.com/y-js/y-dom) | [demo](https://yjs-demos.now.sh/dom/) |
### Providers ### Providers
@@ -65,7 +67,7 @@ Setting up the communication between clients, managing awareness information, an
Install Yjs and a provider with your favorite package manager. Install Yjs and a provider with your favorite package manager.
```sh ```sh
npm i yjs@13.0.0-81 y-websocket@1.0.0-2 y-textarea npm i yjs@13.0.0-82 y-websocket@1.0.0-3 y-textarea
``` ```
**Start the y-websocket server** **Start the y-websocket server**
@@ -83,8 +85,10 @@ import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket' import { WebsocketProvider } from 'y-websocket'
import { TextareaBinding } from 'y-textarea' import { TextareaBinding } from 'y-textarea'
const provider = new WebsocketProvider('http://localhost:1234') const doc = Y.Doc()
const doc = provider.get('roomname') const provider = new WebsocketProvider('http://localhost:1234', 'roomname')
// sync all document updates through the websocket connection
provider.sync('doc')
// Define a shared type on the document. // Define a shared type on the document.
const ytext = doc.getText('my resume') const ytext = doc.getText('my resume')
@@ -135,11 +139,11 @@ import * as Y from 'yjs'
</p> </p>
<pre>const yarray = new Y.Array()</pre> <pre>const yarray = new Y.Array()</pre>
<dl> <dl>
<b><code>insert(index:number, content:Array&lt;object|string|number|Uint8Array|Y.Type&gt;)</code></b> <b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd> <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. 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> </dd>
<b><code>push(Array&lt;Object|Array|string|number|Uint8Array|Y.Type&gt;)</code></b> <b><code>push(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd></dd> <dd></dd>
<b><code>delete(index:number, length:number)</code></b> <b><code>delete(index:number, length:number)</code></b>
<dd></dd> <dd></dd>
@@ -149,9 +153,9 @@ import * as Y from 'yjs'
<dd></dd> <dd></dd>
<b><code>map(function(T, number, YArray):M):Array&lt;M&gt;</code></b> <b><code>map(function(T, number, YArray):M):Array&lt;M&gt;</code></b>
<dd></dd> <dd></dd>
<b><code>toArray():Array&lt;Object|Array|string|number|Uint8Array|Y.Type&gt;</code></b> <b><code>toArray():Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;</code></b>
<dd>Copies the content of this YArray to a new Array.</dd> <dd>Copies the content of this YArray to a new Array.</dd>
<b><code>toJSON():Array&lt;Object|Array|string|number&gt;</code></b> <b><code>toJSON():Array&lt;Object|boolean|Array|string|number&gt;</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> <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> <b><code>[Symbol.Iterator]</code></b>
<dd> <dd>
@@ -184,9 +188,9 @@ import * as Y from 'yjs'
</p> </p>
<pre><code>const ymap = new Y.Map()</code></pre> <pre><code>const ymap = new Y.Map()</code></pre>
<dl> <dl>
<b><code>get(key:string):object|string|number|Uint8Array|Y.Type</code></b> <b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
<dd></dd> <dd></dd>
<b><code>set(key:string, value:object|string|number|Uint8Array|Y.Type)</code></b> <b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
<dd></dd> <dd></dd>
<b><code>delete(key:string)</code></b> <b><code>delete(key:string)</code></b>
<dd></dd> <dd></dd>
@@ -194,7 +198,7 @@ import * as Y from 'yjs'
<dd></dd> <dd></dd>
<b><code>get(index:number)</code></b> <b><code>get(index:number)</code></b>
<dd></dd> <dd></dd>
<b><code>toJSON():Object&lt;string, Object|Array|string|number&gt;</code></b> <b><code>toJSON():Object&lt;string, Object|boolean|Array|string|number&gt;</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> <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> <b><code>[Symbol.Iterator]</code></b>
<dd> <dd>
@@ -433,7 +437,7 @@ doc1.getArray('myarray').insert(0, ['Hello doc2, you got this?'])
doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?' doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?'
``` ```
Yjs internally maintains a [State Vector](#State-Vector) that denotes the next expected clock from each client. In a different interpretation it holds the number of structs created by each client. When two clients sync, you can either exchange the complete document structure or only the differences by sending the state vector to compute the differences. Yjs internally maintains a [state vector](#State-Vector) that denotes the next expected clock from each client. In a different interpretation it holds the number of structs created by each client. When two clients sync, you can either exchange the complete document structure or only the differences by sending the state vector to compute the differences.
**Example: Sync two clients by exchanging the complete document structure** **Example: Sync two clients by exchanging the complete document structure**
@@ -449,8 +453,8 @@ Y.applyUpdate(ydoc2, state1)
This example shows how to sync two clients with the minimal amount of exchanged data by computing only the differences using the state vector of the remote client. Syncing clients using the state vector requires another roundtrip, but can safe a lot of bandwidth. This example shows how to sync two clients with the minimal amount of exchanged data by computing only the differences using the state vector of the remote client. Syncing clients using the state vector requires another roundtrip, but can safe a lot of bandwidth.
```js ```js
const stateVector1 = Y.encodeDocumentStateVector(ydoc1) const stateVector1 = Y.encodeStateVector(ydoc1)
const stateVector2 = Y.encodeDocumentStateVector(ydoc2) const stateVector2 = Y.encodeStateVector(ydoc2)
const diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2) const diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2)
const diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1) const diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1)
Y.applyUpdate(ydoc1, diff2) Y.applyUpdate(ydoc1, diff2)
@@ -462,7 +466,7 @@ Y.applyUpdate(ydoc2, diff1)
<dd>Apply a document update on the shared document. Optionally you can specify <code>transactionOrigin</code> that will be stored on <code>transaction.origin</code> and <code>ydoc.on('update', (update, origin) => ..)</code>.</dd> <dd>Apply a document update on the shared document. Optionally you can specify <code>transactionOrigin</code> that will be stored on <code>transaction.origin</code> and <code>ydoc.on('update', (update, origin) => ..)</code>.</dd>
<b><code>Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array</code></b> <b><code>Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array</code></b>
<dd>Encode the document state as a single update message that can be applied on the remote document. Optionally specify the target state vector to only write the differences to the update message.</dd> <dd>Encode the document state as a single update message that can be applied on the remote document. Optionally specify the target state vector to only write the differences to the update message.</dd>
<b><code>Y.encodeDocumentStateVector(Y.Doc):Uint8Array</code></b> <b><code>Y.encodeStateVector(Y.Doc):Uint8Array</code></b>
<dd>Computes the state vector and encodes it into an Uint8Array.</dd> <dd>Computes the state vector and encodes it into an Uint8Array.</dd>
</dl> </dl>
@@ -530,9 +534,9 @@ Yjs has type descriptions. But until [this ticket](https://github.com/Microsoft/
## Yjs CRDT Algorithm ## Yjs CRDT Algorithm
*Conflict-free replicated data types* (CRDT) for collaborative editing are an alternative approach to *operational transformation* (OT). A very simple differenciation between the two approaches is that OT attempts to transform index positions to ensure convergence (all clients end up with the same content), while CRDTs use models that usually do not involve index transformations, like linked lists. OT is currently the de-facto standard for shared editing on text. OT approaches that support shared editing without a central source of truth (a central server) require too much bookkeeping to be viable in practice. CRDTs are better suited for distributed systems, provide additional guarantees that the document can be synced with remote clients, and do not require a central source of truth / central source of failure. *Conflict-free replicated data types* (CRDT) for collaborative editing are an alternative approach to *operational transformation* (OT). A very simple differenciation between the two approaches is that OT attempts to transform index positions to ensure convergence (all clients end up with the same content), while CRDTs use mathematical models that usually do not involve index transformations, like linked lists. OT is currently the de-facto standard for shared editing on text. OT approaches that support shared editing without a central source of truth (a central server) require too much bookkeeping to be viable in practice. CRDTs are better suited for distributed systems, provide additional guarantees that the document can be synced with remote clients, and do not require a central source of truth.
Yjs implements a modified version of the algorithm described in [this paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types). I will eventually publish a paper that describes the new approach. Note: Since operations make up the document structure, we prefer the term *struct* now. Yjs implements a modified version of the algorithm described in [this paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types). I will eventually publish a paper that describes why this approach works so well in practice. Note: Since operations make up the document structure, we prefer the term *struct* now.
CRDTs suitable for shared text editing suffer from the fact that they only grow in size. There are CRDTs that do not grow in size, but they do not have the characteristics that are benificial for shared text editing (like intention preservation). Yjs implements many improvements to the original algorithm that diminish the trade-off that the document only grows in size. We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs. But we can 1. merge preceeding structs into a single struct to reduce the amount of meta information, 2. we can delete content from the struct if it is deleted, and 3. we can garbage collect tombstones if we don't care about the order of the structs anymore (e.g. if the parent was deleted). CRDTs suitable for shared text editing suffer from the fact that they only grow in size. There are CRDTs that do not grow in size, but they do not have the characteristics that are benificial for shared text editing (like intention preservation). Yjs implements many improvements to the original algorithm that diminish the trade-off that the document only grows in size. We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs. But we can 1. merge preceeding structs into a single struct to reduce the amount of meta information, 2. we can delete content from the struct if it is deleted, and 3. we can garbage collect tombstones if we don't care about the order of the structs anymore (e.g. if the parent was deleted).
@@ -541,7 +545,7 @@ CRDTs suitable for shared text editing suffer from the fact that they only grow
2. When a struct that contains content (e.g. `ItemString`) is deleted, the struct will be replaced with an `ItemDeleted` that does not contain content anymore. 2. When a struct that contains content (e.g. `ItemString`) is deleted, the struct will be replaced with an `ItemDeleted` that does not contain content anymore.
3. When a type is deleted, all child elements are transformed to `GC` structs. A `GC` struct only denotes the existence of a struct and that it is deleted. `GC` structs can always be merged with other `GC` structs if the id's are adjacent. 3. When a type is deleted, all child elements are transformed to `GC` structs. A `GC` struct only denotes the existence of a struct and that it is deleted. `GC` structs can always be merged with other `GC` structs if the id's are adjacent.
Especially when working on structured content (e.g. shared editing on ProseMirror), these improvements yield very good results when [benchmarking](#Benchmarks) random document edits. In practice they show even better results, because users usually edit text in sequence, resulting in structs that can easily be merged. The benchmarks showt that even in the worst case scenario that a user edits text from right to left, Yjs achieves good performance even for huge documents. Especially when working on structured content (e.g. shared editing on ProseMirror), these improvements yield very good results when [benchmarking](https://github.com/dmonad/crdt-benchmarks) random document edits. In practice they show even better results, because users usually edit text in sequence, resulting in structs that can easily be merged. The benchmarks show that even in the worst case scenario that a user edits text from right to left, Yjs achieves good performance even for huge documents.
#### State Vector #### State Vector
Yjs has the ability to exchange only the differences when syncing two clients. We use lamport timestamps to identify structs and to track in which order a client created them. Each struct has an `struct.id = { client: number, clock: number}` that uniquely identifies a struct. We define the next expected `clock` by each client as the *state vector*. This data structure is similar to the [version vectors](https://en.wikipedia.org/wiki/Version_vector) data structure. But we use state vectors only to describe the state of the local document, so we can compute the missing struct of the remote client. We do not use it to track causality. Yjs has the ability to exchange only the differences when syncing two clients. We use lamport timestamps to identify structs and to track in which order a client created them. Each struct has an `struct.id = { client: number, clock: number}` that uniquely identifies a struct. We define the next expected `clock` by each client as the *state vector*. This data structure is similar to the [version vectors](https://en.wikipedia.org/wiki/Version_vector) data structure. But we use state vectors only to describe the state of the local document, so we can compute the missing struct of the remote client. We do not use it to track causality.

1
examples/.gitignore vendored
View File

@@ -1 +0,0 @@
build

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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'

View File

@@ -1,17 +0,0 @@
{
"codemirror": {
"title": "CodeMirror Binding"
},
"prosemirror": {
"title": "ProseMirror Binding"
},
"textarea": {
"title": "Textarea Binding"
},
"quill": {
"title": "Quill Binding"
},
"dom": {
"title": "Dom Binding"
}
}

View File

@@ -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.toUint8Array(encoder)])
}
}

View File

@@ -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 })

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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
}

2698
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,19 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-82", "version": "13.0.0-87",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.js", "main": "./dist/yjs.js",
"module": "./src/index.js", "module": "./dist/yjs.mjs",
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production", "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", "test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000",
"dist": "rm -rf dist examples/build && rollup -c", "dist": "rm -rf dist && rollup -c",
"serve-examples": "concurrently 'npm run watch' 'serve examples'",
"watch": "rollup -wc", "watch": "rollup -wc",
"lint": "standard && tsc", "lint": "standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
"serve-docs": "npm run docs && serve ./docs/", "serve-docs": "npm run docs && serve ./docs/",
"preversion": "PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.js --repitition-time 1000", "preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.js --repitition-time 1000",
"postversion": "git push && git push --tags", "postversion": "git push && git push --tags",
"debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'", "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-deopt": "clear && rollup -c && node --trace-deopt dist/test.js",
@@ -28,15 +27,13 @@
], ],
"dictionaries": { "dictionaries": {
"doc": "docs", "doc": "docs",
"example": "examples",
"test": "tests" "test": "tests"
}, },
"standard": { "standard": {
"ignore": [ "ignore": [
"/dist", "/dist",
"/node_modules", "/node_modules",
"/docs", "/docs"
"/examples/build"
] ]
}, },
"repository": { "repository": {
@@ -54,27 +51,18 @@
}, },
"homepage": "http://y-js.org", "homepage": "http://y-js.org",
"dependencies": { "dependencies": {
"lib0": "0.0.4" "lib0": "0.0.5"
}, },
"devDependencies": { "devDependencies": {
"y-protocols": "0.0.4",
"codemirror": "^5.42.0",
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"jsdoc": "^3.5.5", "jsdoc": "^3.6.2",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"prosemirror-example-setup": "^1.0.1", "rollup": "^1.11.3",
"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",
"rollup-cli": "^1.0.9", "rollup-cli": "^1.0.9",
"rollup-plugin-commonjs": "^9.2.0", "rollup-plugin-node-resolve": "^4.2.4",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-terser": "^4.0.4",
"standard": "^11.0.1", "standard": "^11.0.1",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^3.3.3333" "typescript": "^3.4.5",
"y-protocols": "0.0.6"
} }
} }

View File

@@ -1,6 +1,6 @@
import nodeResolve from 'rollup-plugin-node-resolve' import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import { terser } from 'rollup-plugin-terser' const localImports = process.env.LOCALIMPORTS
const customModules = new Set([ const customModules = new Set([
'y-websocket', 'y-websocket',
@@ -23,31 +23,18 @@ const debugResolve = {
if (importee === 'yjs') { if (importee === 'yjs') {
return `${process.cwd()}/src/index.js` return `${process.cwd()}/src/index.js`
} }
if (customModules.has(importee.split('/')[0])) { if (localImports) {
return `${process.cwd()}/../${importee}/src/${importee}.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 (customLibModules.has(importee.split('/')[0])) {
return `${process.cwd()}/../${importee}`
}
} }
return null 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 [{ export default [{
input: './src/index.js', input: './src/index.js',
output: [{ output: [{
@@ -80,26 +67,7 @@ export default [{
debugResolve, debugResolve,
nodeResolve({ nodeResolve({
sourcemap: true, sourcemap: true,
module: true, mainFields: ['module', 'browser', 'main']
browser: true })
}),
commonjs()
]
}, {
input: ['./examples/textarea.js', './examples/prosemirror.js'], // './examples/quill.js', './examples/dom.js', './examples/codemirror.js'
output: {
dir: 'examples/build',
format: 'esm',
sourcemap: true
},
plugins: [
debugResolve,
nodeResolve({
sourcemap: true,
module: true,
browser: true
}),
commonjs(),
...minificationPlugins
] ]
}] }]

View File

@@ -13,16 +13,16 @@ export {
YMapEvent, YMapEvent,
YArrayEvent, YArrayEvent,
YEvent, YEvent,
AbstractItem, Item,
AbstractStruct, AbstractStruct,
GC, GC,
ItemBinary, ContentBinary,
ItemDeleted, ContentDeleted,
ItemEmbed, ContentEmbed,
ItemFormat, ContentFormat,
ItemJSON, ContentJSON,
ItemString, ContentString,
ItemType, ContentType,
AbstractType, AbstractType,
RelativePosition, RelativePosition,
createRelativePositionFromTypeIndex, createRelativePositionFromTypeIndex,
@@ -42,5 +42,5 @@ export {
iterateDeletedStructs, iterateDeletedStructs,
applyUpdate, applyUpdate,
encodeStateAsUpdate, encodeStateAsUpdate,
encodeDocumentStateVector encodeStateVector
} from './internals.js' } from './internals.js'

View File

@@ -21,14 +21,14 @@ export * from './types/YXmlHook.js'
export * from './types/YXmlText.js' export * from './types/YXmlText.js'
export * from './structs/AbstractStruct.js' export * from './structs/AbstractStruct.js'
export * from './structs/AbstractItem.js'
export * from './structs/GC.js' export * from './structs/GC.js'
export * from './structs/ItemBinary.js' export * from './structs/ContentBinary.js'
export * from './structs/ItemDeleted.js' export * from './structs/ContentDeleted.js'
export * from './structs/ItemEmbed.js' export * from './structs/ContentEmbed.js'
export * from './structs/ItemFormat.js' export * from './structs/ContentFormat.js'
export * from './structs/ItemJSON.js' export * from './structs/ContentJSON.js'
export * from './structs/ItemString.js' export * from './structs/ContentString.js'
export * from './structs/ItemType.js' export * from './structs/ContentType.js'
export * from './structs/Item.js'
export * from './utils/encoding.js' export * from './utils/encoding.js'

View File

@@ -12,14 +12,16 @@ import * as error from 'lib0/error.js'
export class AbstractStruct { export class AbstractStruct {
/** /**
* @param {ID} id * @param {ID} id
* @param {number} length
*/ */
constructor (id) { constructor (id, length) {
/** /**
* The uniqe identifier of this struct. * The uniqe identifier of this struct.
* @type {ID} * @type {ID}
* @readonly * @readonly
*/ */
this.id = id this.id = id
this.length = length
this.deleted = false this.deleted = false
} }
/** /**
@@ -32,12 +34,6 @@ export class AbstractStruct {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/**
* @type {number}
*/
get length () {
throw error.methodUnimplemented()
}
/** /**
* @param {encoding.Encoder} encoder The encoder to write data to. * @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset * @param {number} offset
@@ -89,10 +85,4 @@ export class AbstractStructRef {
toStruct (transaction, store, offset) { toStruct (transaction, store, offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/**
* @type {number}
*/
get length () {
return 1
}
} }

View File

@@ -0,0 +1,92 @@
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)))

View File

@@ -0,0 +1,98 @@
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))

View File

@@ -0,0 +1,92 @@
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)))

View File

@@ -0,0 +1,95 @@
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)))

113
src/structs/ContentJSON.js Normal file
View File

@@ -0,0 +1,113 @@
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)
}

View File

@@ -0,0 +1,96 @@
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))

161
src/structs/ContentType.js Normal file
View File

@@ -0,0 +1,161 @@
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) {
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)
transaction.changedParentTypes.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))

View File

@@ -21,26 +21,18 @@ export class GC extends AbstractStruct {
* @param {number} length * @param {number} length
*/ */
constructor (id, length) { constructor (id, length) {
super(id) super(id, length)
/**
* @type {number}
*/
this._len = length
this.deleted = true this.deleted = true
} }
get length () {
return this._len
}
delete () {} delete () {}
/** /**
* @param {AbstractStruct} right * @param {GC} right
* @return {boolean} * @return {boolean}
*/ */
mergeWith (right) { mergeWith (right) {
this._len += right.length this.length += right.length
return true return true
} }
@@ -57,7 +49,7 @@ export class GC extends AbstractStruct {
*/ */
write (encoder, offset) { write (encoder, offset) {
encoding.writeUint8(encoder, structGCRefNumber) encoding.writeUint8(encoder, structGCRefNumber)
encoding.writeVarUint(encoder, this._len - offset) encoding.writeVarUint(encoder, this.length - offset)
} }
} }
@@ -75,15 +67,7 @@ export class GCRef extends AbstractStructRef {
/** /**
* @type {number} * @type {number}
*/ */
this._len = decoding.readVarUint(decoder) this.length = decoding.readVarUint(decoder)
}
get length () {
return this._len
}
missing () {
return [
createID(this.id.client, this.id.clock - 1)
]
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
@@ -95,11 +79,11 @@ export class GCRef extends AbstractStructRef {
if (offset > 0) { if (offset > 0) {
// @ts-ignore // @ts-ignore
this.id = createID(this.id.client, this.id.clock + offset) this.id = createID(this.id.client, this.id.clock + offset)
this._len = this._len - offset this.length -= offset
} }
return new GC( return new GC(
this.id, this.id,
this._len this.length
) )
} }
} }

View File

@@ -10,14 +10,19 @@ import {
replaceStruct, replaceStruct,
addStruct, addStruct,
addToDeleteSet, addToDeleteSet,
ItemDeleted,
findRootTypeKey, findRootTypeKey,
compareIDs, compareIDs,
getItem, getItem,
getItemType,
getItemCleanEnd, getItemCleanEnd,
getItemCleanStart, getItemCleanStart,
YEvent, StructStore, ID, AbstractType, Transaction // eslint-disable-line readContentDeleted,
readContentBinary,
readContentJSON,
readContentString,
readContentEmbed,
readContentFormat,
readContentType,
ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error.js'
@@ -27,28 +32,12 @@ import * as maplib from 'lib0/map.js'
import * as set from 'lib0/set.js' import * as set from 'lib0/set.js'
import * as binary from 'lib0/binary.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
*/
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
}
return true
}
return false
}
/** /**
* Split leftItem into two items * Split leftItem into two items
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractItem} leftItem * @param {Item} leftItem
* @param {number} diff * @param {number} diff
* @return {AbstractItem} * @return {Item}
* *
* @function * @function
* @private * @private
@@ -56,14 +45,15 @@ export const mergeItemWith = (left, right) => {
export const splitItem = (transaction, leftItem, diff) => { export const splitItem = (transaction, leftItem, diff) => {
const id = leftItem.id const id = leftItem.id
// create rightItem // create rightItem
const rightItem = leftItem.copy( const rightItem = new Item(
createID(id.client, id.clock + diff), createID(id.client, id.clock + diff),
leftItem, leftItem,
createID(id.client, id.clock + diff - 1), createID(id.client, id.clock + diff - 1),
leftItem.right, leftItem.right,
leftItem.rightOrigin, leftItem.rightOrigin,
leftItem.parent, leftItem.parent,
leftItem.parentSub leftItem.parentSub,
leftItem.content.splice(diff)
) )
if (leftItem.deleted) { if (leftItem.deleted) {
rightItem.deleted = true rightItem.deleted = true
@@ -80,24 +70,26 @@ export const splitItem = (transaction, leftItem, diff) => {
if (rightItem.parentSub !== null && rightItem.right === null) { if (rightItem.parentSub !== null && rightItem.right === null) {
rightItem.parent._map.set(rightItem.parentSub, rightItem) rightItem.parent._map.set(rightItem.parentSub, rightItem)
} }
leftItem.length = diff
return rightItem return rightItem
} }
/** /**
* Abstract class that represents any content. * Abstract class that represents any content.
*/ */
export class AbstractItem extends AbstractStruct { export class Item extends AbstractStruct {
/** /**
* @param {ID} id * @param {ID} id
* @param {AbstractItem | null} left * @param {Item | null} left
* @param {ID | null} origin * @param {ID | null} origin
* @param {AbstractItem | null} right * @param {Item | null} right
* @param {ID | null} rightOrigin * @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {string | null} parentSub * @param {string | null} parentSub
* @param {AbstractContent} content
*/ */
constructor (id, left, origin, right, rightOrigin, parent, parentSub) { constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
super(id) super(id, content.getLength())
/** /**
* The item that was originally to the left of this item. * The item that was originally to the left of this item.
* @type {ID | null} * @type {ID | null}
@@ -106,12 +98,12 @@ export class AbstractItem extends AbstractStruct {
this.origin = origin this.origin = origin
/** /**
* The item that is currently to the left of this item. * The item that is currently to the left of this item.
* @type {AbstractItem | null} * @type {Item | null}
*/ */
this.left = left this.left = left
/** /**
* The item that is currently to the right of this item. * The item that is currently to the right of this item.
* @type {AbstractItem | null} * @type {Item | null}
*/ */
this.right = right this.right = right
/** /**
@@ -143,9 +135,12 @@ export class AbstractItem extends AbstractStruct {
/** /**
* If this type's effect is reundone this type refers to the type that undid * If this type's effect is reundone this type refers to the type that undid
* this operation. * this operation.
* @type {AbstractItem | null} * @type {Item | null}
*/ */
this.redone = null this.redone = null
this.content = content
this.length = content.getLength()
this.countable = content.isCountable()
} }
/** /**
@@ -159,7 +154,7 @@ export class AbstractItem extends AbstractStruct {
const parentSub = this.parentSub const parentSub = this.parentSub
const length = this.length const length = this.length
/** /**
* @type {AbstractItem|null} * @type {Item|null}
*/ */
let o let o
// set o to the first conflicting item // set o to the first conflicting item
@@ -175,11 +170,11 @@ export class AbstractItem extends AbstractStruct {
} }
// TODO: use something like DeleteSet here (a tree implementation would be best) // TODO: use something like DeleteSet here (a tree implementation would be best)
/** /**
* @type {Set<AbstractItem>} * @type {Set<Item>}
*/ */
const conflictingItems = new Set() const conflictingItems = new Set()
/** /**
* @type {Set<AbstractItem>} * @type {Set<Item>}
*/ */
const itemsBeforeOrigin = new Set() const itemsBeforeOrigin = new Set()
// Let c in conflictingItems, b in itemsBeforeOrigin // Let c in conflictingItems, b in itemsBeforeOrigin
@@ -238,8 +233,8 @@ export class AbstractItem extends AbstractStruct {
parent._length += length parent._length += length
} }
addStruct(store, this) addStruct(store, this)
this.content.integrate(transaction, this)
maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub) maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub)
// @ts-ignore
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) { 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 // delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction) this.delete(transaction)
@@ -270,29 +265,11 @@ export class AbstractItem extends AbstractStruct {
return n 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. * Redoes the effect of this operation.
* *
* @param {Transaction} transaction The Yjs instance. * @param {Transaction} transaction The Yjs instance.
* @param {Set<AbstractItem>} redoitems * @param {Set<Item>} redoitems
* *
* @private * @private
*/ */
@@ -343,7 +320,7 @@ export class AbstractItem extends AbstractStruct {
right = right.right 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 = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, this.parentSub, this.content.copy())
this.redone.integrate(transaction) this.redone.integrate(transaction)
return true return true
} }
@@ -354,46 +331,31 @@ export class AbstractItem extends AbstractStruct {
get lastId () { get lastId () {
return createID(this.id.client, this.id.clock + this.length - 1) return createID(this.id.client, this.id.clock + this.length - 1)
} }
/** /**
* Computes the length of this Item. * Try to merge two items
*
* @param {Item} right
* @return {boolean}
*/ */
get length () { mergeWith (right) {
return 1 if (
} compareIDs(right.origin, this.lastId) &&
this.right === right &&
/** compareIDs(this.rightOrigin, right.rightOrigin) &&
* Should return false if this Item is some kind of meta information this.id.client === right.id.client &&
* (e.g. format information). this.id.clock + this.length === right.id.clock &&
* this.deleted === right.deleted &&
* * Whether this Item should be addressable via `yarray.get(i)` this.content.constructor === right.content.constructor &&
* * Whether this Item should be counted when computing yarray.length this.content.mergeWith(right.content)
*/ ) {
get countable () { this.right = right.right
return true if (this.right !== null) {
} this.right.left = this
}
/** this.length += right.length
* Do not call directly. Always split via StructStore! return true
* }
* Splits this Item so that another Item can be inserted in-between. return false
* 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')
} }
/** /**
@@ -411,16 +373,10 @@ export class AbstractItem extends AbstractStruct {
this.deleted = true this.deleted = true
addToDeleteSet(transaction.deleteSet, this.id, this.length) addToDeleteSet(transaction.deleteSet, this.id, this.length)
maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub) maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
this.content.delete(transaction)
} }
} }
/**
* @param {StructStore} store
*
* @private
*/
gcChildren (store) { }
/** /**
* @param {StructStore} store * @param {StructStore} store
* @param {boolean} parentGCd * @param {boolean} parentGCd
@@ -431,30 +387,12 @@ export class AbstractItem extends AbstractStruct {
if (!this.deleted) { if (!this.deleted) {
throw error.unexpectedCase() throw error.unexpectedCase()
} }
let r this.content.gc(store)
if (parentGCd) { if (parentGCd) {
r = new GC(this.id, this.length) replaceStruct(store, this, new GC(this.id, this.length))
} else { } else {
r = new ItemDeleted(this.id, this.left, this.origin, this.right, this.rightOrigin, this.parent, this.parentSub, this.length) this.content = new ContentDeleted(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
}
} }
replaceStruct(store, this, r)
}
/**
* @return {Array<any>}
*/
getContent () {
throw error.methodUnimplemented()
} }
/** /**
@@ -465,15 +403,14 @@ export class AbstractItem extends AbstractStruct {
* *
* @param {encoding.Encoder} encoder The encoder to write data to. * @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset * @param {number} offset
* @param {number} encodingRef
* *
* @private * @private
*/ */
write (encoder, offset, encodingRef) { write (encoder, offset) {
const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin
const rightOrigin = this.rightOrigin const rightOrigin = this.rightOrigin
const parentSub = this.parentSub const parentSub = this.parentSub
const info = (encodingRef & binary.BITS5) | const info = (this.content.getRef() & binary.BITS5) |
(origin === null ? 0 : binary.BIT8) | // origin is defined (origin === null ? 0 : binary.BIT8) | // origin is defined
(rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined (rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined
(parentSub === null ? 0 : binary.BIT6) // parentSub is non-null (parentSub === null ? 0 : binary.BIT6) // parentSub is non-null
@@ -489,26 +426,129 @@ export class AbstractItem extends AbstractStruct {
if (parent._item === null) { if (parent._item === null) {
// parent type on y._map // parent type on y._map
// find the correct key // find the correct key
// @ts-ignore we know that y exists
const ykey = findRootTypeKey(parent) const ykey = findRootTypeKey(parent)
encoding.writeVarUint(encoder, 1) // write parentYKey encoding.writeVarUint(encoder, 1) // write parentYKey
encoding.writeVarString(encoder, ykey) encoding.writeVarString(encoder, ykey)
} else { } else {
encoding.writeVarUint(encoder, 0) // write parent id encoding.writeVarUint(encoder, 0) // write parent id
// @ts-ignore _item is defined because parent is integrated
writeID(encoder, parent._item.id) writeID(encoder, parent._item.id)
} }
if (parentSub !== null) { if (parentSub !== null) {
encoding.writeVarString(encoder, parentSub) 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
]
/**
* 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 * @private
*/ */
export class AbstractItemRef extends AbstractStructRef { export class ItemRef extends AbstractStructRef {
/** /**
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {ID} id * @param {ID} id
@@ -558,85 +598,69 @@ export class AbstractItemRef extends AbstractStructRef {
if (this.parent !== null) { if (this.parent !== null) {
missing.push(this.parent) 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 {Transaction} transaction
* @param {AbstractItem?} right * @param {StructStore} store
* @param {AbstractType<YEvent>?} parent * @param {number} offset
* @param {string|null} parentSub * @return {Item|GC}
*/ */
constructor (left, right, parent, parentSub) { toStruct (transaction, store, offset) {
this.left = left if (offset > 0) {
this.right = right /**
this.parent = parent * @type {ID}
this.parentSub = parentSub */
} 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)
}
/** const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
* Outsourcing some of the logic of computing the item params from a received struct. const right = this.right === null ? null : getItemCleanStart(transaction, store, this.right)
* If parent === null, it is expected to gc the read struct. Otherwise apply it. let parent = null
* let parentSub = this.parentSub
* @param {Transaction} transaction if (this.parent !== null) {
* @param {StructStore} store const parentItem = getItem(store, this.parent)
* @param {ID|null} leftid // Edge case: toStruct is called with an offset > 0. In this case left is defined.
* @param {ID|null} rightid // Depending in which order structs arrive, left may be GC'd and the parent not
* @param {ID|null} parentid // deleted. This is why we check if left is GC'd. Strictly we don't have
* @param {string|null} parentSub // to check if right is GC'd, but we will in case we run into future issues
* @param {string|null} parentYKey if (!parentItem.deleted && (left === null || left.constructor !== GC) && (right === null || right.constructor !== GC)) {
* @return {ItemParams} parent = /** @type {ContentType} */ (parentItem.content).type
* }
* @private } else if (this.parentYKey !== null) {
* @function parent = transaction.doc.get(this.parentYKey)
*/ } else if (left !== null) {
export const computeItemParams = (transaction, store, leftid, rightid, parentid, parentSub, parentYKey) => { if (left.constructor !== GC) {
const left = leftid === null ? null : getItemCleanEnd(transaction, store, leftid) parent = left.parent
const right = rightid === null ? null : getItemCleanStart(transaction, store, rightid) parentSub = left.parentSub
let parent = null }
if (parentid !== null) { } else if (right !== null) {
const parentItem = getItemType(store, parentid) if (right.constructor !== GC) {
switch (parentItem.constructor) { parent = right.parent
case ItemDeleted: parentSub = right.parentSub
case GC: }
break } else {
default: throw error.unexpectedCase()
// 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 probably 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 = parentItem.type
}
} }
} else if (parentYKey !== null) {
parent = transaction.doc.get(parentYKey) return parent === null
} else if (left !== null) { ? new GC(this.id, this.length)
if (left.constructor !== GC) { : new Item(
parent = left.parent this.id,
parentSub = left.parentSub left,
} this.left,
} else if (right !== null) { right,
if (right.constructor !== GC) { this.right,
parent = right.parent parent,
parentSub = right.parentSub parentSub,
} this.content
} else { )
throw error.unexpectedCase()
} }
return new ItemParams(left, right, parent, parentSub)
} }

View File

@@ -1,99 +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'
import * as buffer from 'lib0/buffer.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 {Uint8Array} 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.writeVarUint8Array(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 {Uint8Array}
*/
this.content = buffer.copyUint8Array(decoding.readVarUint8Array(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
)
}
}

View File

@@ -1,155 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
changeItemRefOffset,
GC,
splitItem,
addToDeleteSet,
mergeItemWith,
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 {StructStore} store
* @param {boolean} parentGCd
*
* @private
*/
gc (store, parentGCd) {
if (parentGCd) {
super.gc(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
)
}
}

View File

@@ -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 {Object}
*/
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
)
}
}

View File

@@ -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
)
}
}

View File

@@ -1,153 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
splitItem,
changeItemRefOffset,
GC,
mergeItemWith,
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 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
)
}
}

View File

@@ -1,138 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
splitItem,
changeItemRefOffset,
GC,
mergeItemWith,
Transaction, StructStore, 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
)
}
}

View File

@@ -1,199 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText,
StructStore, 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.doc, 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 {StructStore} store
*/
gcChildren (store) {
let item = this.type._start
while (item !== null) {
item.gc(store, true)
item = item.right
}
this.type._start = null
this.type._map.forEach(item => {
while (item !== null) {
item.gc(store, true)
// @ts-ignore
item = item.left
}
})
this._map = new Map()
}
/**
* @param {StructStore} store
* @param {boolean} parentGCd
*/
gc (store, parentGCd) {
this.gcChildren(store)
super.gc(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
)
}
}

View File

@@ -4,14 +4,14 @@ import {
callEventHandlerListeners, callEventHandlerListeners,
addEventHandlerListener, addEventHandlerListener,
createEventHandler, createEventHandler,
ItemType,
nextID, nextID,
isVisible, isVisible,
ItemJSON, ContentType,
ItemBinary, ContentJSON,
ContentBinary,
createID, createID,
getItemCleanStart, getItemCleanStart,
Doc, Snapshot, Transaction, EventHandler, YEvent, AbstractItem, // eslint-disable-line Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
@@ -49,17 +49,17 @@ export const callTypeObservers = (type, transaction, event) => {
export class AbstractType { export class AbstractType {
constructor () { constructor () {
/** /**
* @type {ItemType|null} * @type {Item|null}
*/ */
this._item = null this._item = null
/** /**
* @private * @private
* @type {Map<string,AbstractItem>} * @type {Map<string,Item>}
*/ */
this._map = new Map() this._map = new Map()
/** /**
* @private * @private
* @type {AbstractItem|null} * @type {Item|null}
*/ */
this._start = null this._start = null
/** /**
@@ -88,7 +88,7 @@ export class AbstractType {
* * Observer functions are fired * * Observer functions are fired
* *
* @param {Doc} y The Yjs instance * @param {Doc} y The Yjs instance
* @param {ItemType|null} item * @param {Item|null} item
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
@@ -101,7 +101,7 @@ export class AbstractType {
* @private * @private
*/ */
_copy () { _copy () {
throw new Error('unimplemented') throw error.methodUnimplemented()
} }
/** /**
@@ -187,7 +187,7 @@ export const typeListToArray = type => {
let n = type._start let n = type._start
while (n !== null) { while (n !== null) {
if (n.countable && !n.deleted) { if (n.countable && !n.deleted) {
const c = n.getContent() const c = n.content.getContent()
for (let i = 0; i < c.length; i++) { for (let i = 0; i < c.length; i++) {
cs.push(c[i]) cs.push(c[i])
} }
@@ -210,7 +210,7 @@ export const typeListToArraySnapshot = (type, snapshot) => {
let n = type._start let n = type._start
while (n !== null) { while (n !== null) {
if (n.countable && isVisible(n, snapshot)) { if (n.countable && isVisible(n, snapshot)) {
const c = n.getContent() const c = n.content.getContent()
for (let i = 0; i < c.length; i++) { for (let i = 0; i < c.length; i++) {
cs.push(c[i]) cs.push(c[i])
} }
@@ -234,7 +234,7 @@ export const typeListForEach = (type, f) => {
let n = type._start let n = type._start
while (n !== null) { while (n !== null) {
if (n.countable && !n.deleted) { if (n.countable && !n.deleted) {
const c = n.getContent() const c = n.content.getContent()
for (let i = 0; i < c.length; i++) { for (let i = 0; i < c.length; i++) {
f(c[i], index++, type) f(c[i], index++, type)
} }
@@ -287,18 +287,15 @@ export const typeListCreateIterator = type => {
while (n !== null && n.deleted) { while (n !== null && n.deleted) {
n = n.right n = n.right
} }
} // check if we reached the end, no need to check currentContent, because it does not exist
// check if we reached the end, no need to check currentContent, because it does not exist if (n === null) {
if (n === null) { return {
return { done: true,
done: true, value: undefined
value: undefined }
} }
}
// currentContent could exist from the last iteration
if (currentContent === null) {
// we found n, so we can set currentContent // we found n, so we can set currentContent
currentContent = n.getContent() currentContent = n.content.getContent()
currentContentIndex = 0 currentContentIndex = 0
n = n.right // we used the content of n, now iterate to next n = n.right // we used the content of n, now iterate to next
} }
@@ -331,7 +328,7 @@ export const typeListForEachSnapshot = (type, f, snapshot) => {
let n = type._start let n = type._start
while (n !== null) { while (n !== null) {
if (n.countable && isVisible(n, snapshot)) { if (n.countable && isVisible(n, snapshot)) {
const c = n.getContent() const c = n.content.getContent()
for (let i = 0; i < c.length; i++) { for (let i = 0; i < c.length; i++) {
f(c[i], index++, type) f(c[i], index++, type)
} }
@@ -352,7 +349,7 @@ export const typeListGet = (type, index) => {
for (let n = type._start; n !== null; n = n.right) { for (let n = type._start; n !== null; n = n.right) {
if (!n.deleted && n.countable) { if (!n.deleted && n.countable) {
if (index < n.length) { if (index < n.length) {
return n.getContent()[index] return n.content.getContent()[index]
} }
index -= n.length index -= n.length
} }
@@ -362,8 +359,8 @@ export const typeListGet = (type, index) => {
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem?} referenceItem * @param {Item?} referenceItem
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content * @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content
* *
* @private * @private
* @function * @function
@@ -377,7 +374,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
let jsonContent = [] let jsonContent = []
const packJsonContent = () => { const packJsonContent = () => {
if (jsonContent.length > 0) { 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 ContentJSON(jsonContent))
left.integrate(transaction) left.integrate(transaction)
jsonContent = [] jsonContent = []
} }
@@ -386,6 +383,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
switch (c.constructor) { switch (c.constructor) {
case Number: case Number:
case Object: case Object:
case Boolean:
case Array: case Array:
case String: case String:
jsonContent.push(c) jsonContent.push(c)
@@ -395,12 +393,12 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
switch (c.constructor) { switch (c.constructor) {
case Uint8Array: case Uint8Array:
case ArrayBuffer: case ArrayBuffer:
left = new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new Uint8Array(/** @type {Uint8Array} */ (c))) 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) left.integrate(transaction)
break break
default: default:
if (c instanceof AbstractType) { 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) left.integrate(transaction)
} else { } else {
throw new Error('Unexpected content type in insert operation') throw new Error('Unexpected content type in insert operation')
@@ -503,27 +501,30 @@ export const typeMapDelete = (transaction, parent, key) => {
*/ */
export const typeMapSet = (transaction, parent, key, value) => { export const typeMapSet = (transaction, parent, key, value) => {
const left = parent._map.get(key) || null const left = parent._map.get(key) || null
let content
if (value == null) { if (value == null) {
new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, [value]).integrate(transaction) content = new ContentJSON([value])
return } else {
} switch (value.constructor) {
switch (value.constructor) { case Number:
case Number: case Object:
case Object: case Boolean:
case Array: case Array:
case String: case String:
new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, [value]).integrate(transaction) content = new ContentJSON([value])
break break
case Uint8Array: case Uint8Array:
new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, value).integrate(transaction) content = new ContentBinary(value)
break break
default: default:
if (value instanceof AbstractType) { if (value instanceof AbstractType) {
new ItemType(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, value).integrate(transaction) content = new ContentType(value)
} else { } else {
throw new Error('Unexpected content type') throw new Error('Unexpected content type')
} }
}
} }
new Item(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, content).integrate(transaction)
} }
/** /**
@@ -536,7 +537,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
*/ */
export const typeMapGet = (parent, key) => { export const typeMapGet = (parent, key) => {
const val = parent._map.get(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
} }
/** /**
@@ -553,7 +554,7 @@ export const typeMapGetAll = (parent) => {
let res = {} let res = {}
for (const [key, value] of parent._map) { for (const [key, value] of parent._map) {
if (!value.deleted) { if (!value.deleted) {
res[key] = value.getContent()[value.length - 1] res[key] = value.content.getContent()[value.length - 1]
} }
} }
return res return res
@@ -586,11 +587,11 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => {
while (v !== null && (!snapshot.sm.has(v.id.client) || v.id.clock >= (snapshot.sm.get(v.id.client) || 0))) { while (v !== null && (!snapshot.sm.has(v.id.client) || v.id.clock >= (snapshot.sm.get(v.id.client) || 0))) {
v = v.left 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>>} * @return {IterableIterator<Array<any>>}
* *
* @private * @private

View File

@@ -15,7 +15,7 @@ import {
YArrayRefID, YArrayRefID,
callTypeObservers, callTypeObservers,
transact, transact,
Doc, Transaction, ItemType, // eslint-disable-line Doc, Transaction, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' // eslint-disable-line
@@ -59,14 +59,13 @@ export class YArray extends AbstractType {
* * Observer functions are fired * * Observer functions are fired
* *
* @param {Doc} y The Yjs instance * @param {Doc} y The Yjs instance
* @param {ItemType} item * @param {Item} item
* *
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
// @ts-ignore this.insert(0, /** @type {Array} */ (this._prelimContent))
this.insert(0, this._prelimContent)
this._prelimContent = null this._prelimContent = null
} }
get length () { get length () {
@@ -106,8 +105,7 @@ export class YArray extends AbstractType {
typeListInsertGenerics(transaction, this, index, content) typeListInsertGenerics(transaction, this, index, content)
}) })
} else { } else {
// @ts-ignore _prelimContent is defined because this is not yet integrated /** @type {Array} */ (this._prelimContent).splice(index, 0, ...content)
this._prelimContent.splice(index, 0, ...content)
} }
} }
@@ -132,8 +130,7 @@ export class YArray extends AbstractType {
typeListDelete(transaction, this, index, length) typeListDelete(transaction, this, index, length)
}) })
} else { } else {
// @ts-ignore _prelimContent is defined because this is not yet integrated /** @type {Array} */ (this._prelimContent).splice(index, length)
this._prelimContent.splice(index, length)
} }
} }
@@ -175,8 +172,7 @@ export class YArray extends AbstractType {
* callback function * callback function
*/ */
map (f) { map (f) {
// @ts-ignore return typeListMap(this, /** @type {any} */ (f))
return typeListMap(this, f)
} }
/** /**

View File

@@ -14,7 +14,7 @@ import {
YMapRefID, YMapRefID,
callTypeObservers, callTypeObservers,
transact, transact,
Doc, Transaction, ItemType, // eslint-disable-line Doc, Transaction, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -61,14 +61,13 @@ export class YMap extends AbstractType {
* * Observer functions are fired * * Observer functions are fired
* *
* @param {Doc} y The Yjs instance * @param {Doc} y The Yjs instance
* @param {ItemType} item * @param {Item} item
* *
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
// @ts-ignore for (let [key, value] of /** @type {Map<string, any>} */ (this._prelimContent)) {
for (let [key, value] of this._prelimContent) {
this.set(key, value) this.set(key, value)
} }
this._prelimContent = null this._prelimContent = null
@@ -97,7 +96,7 @@ export class YMap extends AbstractType {
const map = {} const map = {}
for (let [key, item] of this._map) { for (let [key, item] of this._map) {
if (!item.deleted) { if (!item.deleted) {
const v = item.getContent()[0] const v = item.content.getContent()[item.length - 1]
map[key] = v instanceof AbstractType ? v.toJSON() : v map[key] = v instanceof AbstractType ? v.toJSON() : v
} }
} }
@@ -119,7 +118,7 @@ export class YMap extends AbstractType {
* @return {Iterator<string>} * @return {Iterator<string>}
*/ */
values () { 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 +127,7 @@ export class YMap extends AbstractType {
* @return {IterableIterator<any>} * @return {IterableIterator<any>}
*/ */
entries () { 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]])
} }
/** /**
@@ -149,8 +148,7 @@ export class YMap extends AbstractType {
typeMapDelete(transaction, this, key) typeMapDelete(transaction, this, key)
}) })
} else { } else {
// @ts-ignore /** @type {Map<string, any>} */ (this._prelimContent).delete(key)
this._prelimContent.delete(key)
} }
} }
@@ -166,8 +164,7 @@ export class YMap extends AbstractType {
typeMapSet(transaction, this, key, value) typeMapSet(transaction, this, key, value)
}) })
} else { } else {
// @ts-ignore /** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
this._prelimContent.set(key, value)
} }
return value return value
} }
@@ -179,8 +176,7 @@ export class YMap extends AbstractType {
* @return {T|undefined} * @return {T|undefined}
*/ */
get (key) { get (key) {
// @ts-ignore return /** @type {any} */ (typeMapGet(this, key))
return typeMapGet(this, key)
} }
/** /**

View File

@@ -5,9 +5,6 @@
import { import {
YEvent, YEvent,
ItemEmbed,
ItemString,
ItemFormat,
AbstractType, AbstractType,
nextID, nextID,
createID, createID,
@@ -16,7 +13,10 @@ import {
YTextRefID, YTextRefID,
callTypeObservers, callTypeObservers,
transact, transact,
Doc, ItemType, AbstractItem, Snapshot, StructStore, Transaction // eslint-disable-line ContentEmbed,
ContentFormat,
ContentString,
Doc, Item, Snapshot, StructStore, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' // eslint-disable-line
@@ -24,8 +24,8 @@ import * as encoding from 'lib0/encoding.js'
export class ItemListPosition { export class ItemListPosition {
/** /**
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
*/ */
constructor (left, right) { constructor (left, right) {
this.left = left this.left = left
@@ -35,8 +35,8 @@ export class ItemListPosition {
export class ItemTextListPosition extends ItemListPosition { export class ItemTextListPosition extends ItemListPosition {
/** /**
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
*/ */
constructor (left, right, currentAttributes) { constructor (left, right, currentAttributes) {
@@ -47,8 +47,8 @@ export class ItemTextListPosition extends ItemListPosition {
export class ItemInsertionResult extends ItemListPosition { export class ItemInsertionResult extends ItemListPosition {
/** /**
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} negatedAttributes * @param {Map<string,any>} negatedAttributes
*/ */
constructor (left, right, negatedAttributes) { constructor (left, right, negatedAttributes) {
@@ -61,8 +61,8 @@ export class ItemInsertionResult extends ItemListPosition {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {number} count * @param {number} count
* @return {ItemTextListPosition} * @return {ItemTextListPosition}
* *
@@ -71,9 +71,9 @@ export class ItemInsertionResult extends ItemListPosition {
*/ */
const findNextPosition = (transaction, store, currentAttributes, left, right, count) => { const findNextPosition = (transaction, store, currentAttributes, left, right, count) => {
while (right !== null && count > 0) { while (right !== null && count > 0) {
switch (right.constructor) { switch (right.content.constructor) {
case ItemEmbed: case ContentEmbed:
case ItemString: case ContentString:
if (!right.deleted) { if (!right.deleted) {
if (count < right.length) { if (count < right.length) {
// split right // split right
@@ -82,10 +82,9 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
count -= right.length count -= right.length
} }
break break
case ItemFormat: case ContentFormat:
if (!right.deleted) { if (!right.deleted) {
// @ts-ignore right is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
updateCurrentAttributes(currentAttributes, right)
} }
break break
} }
@@ -117,8 +116,8 @@ const findPosition = (transaction, store, parent, index) => {
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} negatedAttributes * @param {Map<string,any>} negatedAttributes
* @return {ItemListPosition} * @return {ItemListPosition}
* *
@@ -130,21 +129,19 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
while ( while (
right !== null && ( right !== null && (
right.deleted === true || ( right.deleted === true || (
right.constructor === ItemFormat && right.content.constructor === ContentFormat &&
// @ts-ignore right is ItemFormat (negatedAttributes.get(/** @type {ContentFormat} */ (right.content).key) === /** @type {ContentFormat} */ (right.content).value)
(negatedAttributes.get(right.key) === right.value)
) )
) )
) { ) {
if (!right.deleted) { if (!right.deleted) {
// @ts-ignore right is ItemFormat negatedAttributes.delete(/** @type {ContentFormat} */ (right.content).key)
negatedAttributes.delete(right.key)
} }
left = right left = right
right = right.right right = right.right
} }
for (let [key, val] of negatedAttributes) { 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) 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) left.integrate(transaction)
} }
return { left, right } return { left, right }
@@ -152,14 +149,13 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
/** /**
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {ItemFormat} item * @param {ContentFormat} format
* *
* @private * @private
* @function * @function
*/ */
const updateCurrentAttributes = (currentAttributes, item) => { const updateCurrentAttributes = (currentAttributes, format) => {
const value = item.value const { key, value } = format
const key = item.key
if (value === null) { if (value === null) {
currentAttributes.delete(key) currentAttributes.delete(key)
} else { } else {
@@ -168,8 +164,8 @@ const updateCurrentAttributes = (currentAttributes, item) => {
} }
/** /**
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
* @return {ItemListPosition} * @return {ItemListPosition}
@@ -184,11 +180,9 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
break break
} else if (right.deleted) { } else if (right.deleted) {
// continue // continue
// @ts-ignore right is ItemFormat } else if (right.content.constructor === ContentFormat && (attributes[(/** @type {ContentFormat} */ (right.content)).key] || null) === /** @type {ContentFormat} */ (right.content).value) {
} else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) {
// found a format, update currentAttributes and continue // found a format, update currentAttributes and continue
// @ts-ignore right is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
updateCurrentAttributes(currentAttributes, right)
} else { } else {
break break
} }
@@ -201,8 +195,8 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
* @return {ItemInsertionResult} * @return {ItemInsertionResult}
@@ -219,7 +213,7 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
if (currentVal !== val) { if (currentVal !== val) {
// save negated attribute (set null if currentVal undefined) // save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal) 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) left.integrate(transaction)
} }
} }
@@ -229,8 +223,8 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {string} text * @param {string} text
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
@@ -250,11 +244,8 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
left = insertPos.left left = insertPos.left
right = insertPos.right right = insertPos.right
// insert content // insert content
if (text.constructor === String) { const content = text.constructor === String ? new ContentString(text) : new ContentEmbed(text)
left = new ItemString(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, text) left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, content)
} else {
left = new ItemEmbed(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, text)
}
left.integrate(transaction) left.integrate(transaction)
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes) return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
} }
@@ -262,8 +253,8 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {number} length * @param {number} length
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
@@ -282,26 +273,22 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
// delete all formats with attributes[format.key] != null // delete all formats with attributes[format.key] != null
while (length > 0 && right !== null) { while (length > 0 && right !== null) {
if (right.deleted === false) { if (right.deleted === false) {
switch (right.constructor) { switch (right.content.constructor) {
case ItemFormat: case ContentFormat:
// @ts-ignore right is ItemFormat const { key, value } = /** @type {ContentFormat} */ (right.content)
const attr = attributes[right.key] const attr = attributes[key]
if (attr !== undefined) { if (attr !== undefined) {
// @ts-ignore right is ItemFormat if (attr === value) {
if (attr === right.value) { negatedAttributes.delete(key)
// @ts-ignore right is ItemFormat
negatedAttributes.delete(right.key)
} else { } else {
// @ts-ignore right is ItemFormat negatedAttributes.set(key, value)
negatedAttributes.set(right.key, right.value)
} }
right.delete(transaction) right.delete(transaction)
} }
// @ts-ignore right is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
updateCurrentAttributes(currentAttributes, right)
break break
case ItemEmbed: case ContentEmbed:
case ItemString: case ContentString:
if (length < right.length) { if (length < right.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length)) getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length))
} }
@@ -317,8 +304,8 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {number} length * @param {number} length
* @return {ItemListPosition} * @return {ItemListPosition}
@@ -329,13 +316,12 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
const deleteText = (transaction, left, right, currentAttributes, length) => { const deleteText = (transaction, left, right, currentAttributes, length) => {
while (length > 0 && right !== null) { while (length > 0 && right !== null) {
if (right.deleted === false) { if (right.deleted === false) {
switch (right.constructor) { switch (right.content.constructor) {
case ItemFormat: case ContentFormat:
// @ts-ignore right is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
updateCurrentAttributes(currentAttributes, right)
break break
case ItemEmbed: case ContentEmbed:
case ItemString: case ContentString:
if (length < right.length) { if (length < right.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length)) getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length))
} }
@@ -388,7 +374,7 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
/** /**
* Event that describes the changes on a YText type. * Event that describes the changes on a YText type.
*/ */
class YTextEvent extends YEvent { export class YTextEvent extends YEvent {
/** /**
* @param {YText} ytext * @param {YText} ytext
* @param {Transaction} transaction * @param {Transaction} transaction
@@ -411,13 +397,10 @@ class YTextEvent extends YEvent {
*/ */
get delta () { get delta () {
if (this._delta === null) { if (this._delta === null) {
const y = this.target.doc const y = /** @type {Doc} */ (this.target.doc)
// @ts-ignore this._delta = []
transact(y, transaction => { transact(y, transaction => {
/** const delta = /** @type {Array<DeltaItem>} */ (this._delta)
* @type {Array<DeltaItem>}
*/
const delta = []
const currentAttributes = new Map() // saves all current attributes for insert const currentAttributes = new Map() // saves all current attributes for insert
const oldAttributes = new Map() const oldAttributes = new Map()
let item = this.target._start let item = this.target._start
@@ -432,7 +415,6 @@ class YTextEvent extends YEvent {
let insert = '' let insert = ''
let retain = 0 let retain = 0
let deleteLen = 0 let deleteLen = 0
this._delta = delta
const addOp = () => { const addOp = () => {
if (action !== null) { if (action !== null) {
/** /**
@@ -472,14 +454,15 @@ class YTextEvent extends YEvent {
} }
} }
while (item !== null) { while (item !== null) {
switch (item.constructor) { switch (item.content.constructor) {
case ItemEmbed: case ContentEmbed:
if (this.adds(item)) { if (this.adds(item)) {
addOp() if (!this.deletes(item)) {
action = 'insert' addOp()
// @ts-ignore item is ItemFormat action = 'insert'
insert = item.embed insert = /** @type {ContentEmbed} */ (item.content).embed
addOp() addOp()
}
} else if (this.deletes(item)) { } else if (this.deletes(item)) {
if (action !== 'delete') { if (action !== 'delete') {
addOp() addOp()
@@ -494,14 +477,15 @@ class YTextEvent extends YEvent {
retain += 1 retain += 1
} }
break break
case ItemString: case ContentString:
if (this.adds(item)) { if (this.adds(item)) {
if (action !== 'insert') { if (!this.deletes(item)) {
addOp() if (action !== 'insert') {
action = 'insert' addOp()
action = 'insert'
}
insert += /** @type {ContentString} */ (item.content).str
} }
// @ts-ignore
insert += item.string
} else if (this.deletes(item)) { } else if (this.deletes(item)) {
if (action !== 'delete') { if (action !== 'delete') {
addOp() addOp()
@@ -516,57 +500,45 @@ class YTextEvent extends YEvent {
retain += item.length retain += item.length
} }
break break
case ItemFormat: case ContentFormat:
const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) { if (this.adds(item)) {
// @ts-ignore item is ItemFormat if (!this.deletes(item)) {
const curVal = currentAttributes.get(item.key) || null const curVal = currentAttributes.get(key) || null
// @ts-ignore item is ItemFormat if (curVal !== value) {
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 (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
// @ts-ignore item is ItemFormat if (value === (oldAttributes.get(key) || null)) {
if (item.value === null) { delete attributes[key]
// @ts-ignore item is ItemFormat
attributes[item.key] = item.value
} else { } else {
// @ts-ignore item is ItemFormat attributes[key] = value
delete attributes[item.key] }
} else {
item.delete(transaction)
}
}
} else if (this.deletes(item)) {
oldAttributes.set(key, value)
const curVal = currentAttributes.get(key) || null
if (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 (attr !== value) {
if (action === 'retain') {
addOp()
}
if (value === null) {
attributes[key] = value
} else {
delete attributes[key]
} }
} else { } else {
item.delete(transaction) item.delete(transaction)
@@ -577,26 +549,24 @@ class YTextEvent extends YEvent {
if (action === 'insert') { if (action === 'insert') {
addOp() addOp()
} }
// @ts-ignore item is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content))
updateCurrentAttributes(currentAttributes, item)
} }
break break
} }
item = item.right item = item.right
} }
addOp() addOp()
while (this._delta.length > 0) { while (delta.length > 0) {
let lastOp = this._delta[this._delta.length - 1] let lastOp = delta[delta.length - 1]
if (lastOp.retain !== undefined && lastOp.attributes === undefined) { if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
// retain delta's if they don't assign attributes // retain delta's if they don't assign attributes
this._delta.pop() delta.pop()
} else { } else {
break break
} }
} }
}) })
} }
// @ts-ignore _delta is defined above
return this._delta return this._delta
} }
} }
@@ -630,15 +600,14 @@ export class YText extends AbstractType {
/** /**
* @param {Doc} y * @param {Doc} y
* @param {ItemType} item * @param {Item} item
* *
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
try { try {
// @ts-ignore this._prelimContent is still defined /** @type {Array<function>} */ (this._pending).forEach(f => f())
this._pending.forEach(f => f())
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@@ -665,13 +634,12 @@ export class YText extends AbstractType {
toString () { toString () {
let str = '' let str = ''
/** /**
* @type {AbstractItem|null} * @type {Item|null}
*/ */
let n = this._start let n = this._start
while (n !== null) { while (n !== null) {
if (!n.deleted && n.countable && n.constructor === ItemString) { if (!n.deleted && n.countable && n.content.constructor === ContentString) {
// @ts-ignore str += /** @type {ContentString} */ (n.content).str
str += n.string
} }
n = n.right n = n.right
} }
@@ -705,8 +673,7 @@ export class YText extends AbstractType {
} }
}) })
} else { } else {
// @ts-ignore /** @type {Array<function>} */ (this._pending).push(() => this.applyDelta(delta))
this._pending.push(() => this.applyDelta(delta))
} }
} }
@@ -726,10 +693,6 @@ export class YText extends AbstractType {
const ops = [] const ops = []
const currentAttributes = new Map() const currentAttributes = new Map()
let str = '' let str = ''
/**
* @type {AbstractItem|null}
*/
// @ts-ignore
let n = this._start let n = this._start
function packStr () { function packStr () {
if (str.length > 0) { if (str.length > 0) {
@@ -756,8 +719,8 @@ export class YText extends AbstractType {
} }
while (n !== null) { while (n !== null) {
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) { if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
switch (n.constructor) { switch (n.content.constructor) {
case ItemString: case ContentString:
const cur = currentAttributes.get('ychange') const cur = currentAttributes.get('ychange')
if (snapshot !== undefined && !isVisible(n, snapshot)) { if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') { if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
@@ -773,13 +736,17 @@ export class YText extends AbstractType {
packStr() packStr()
currentAttributes.delete('ychange') currentAttributes.delete('ychange')
} }
// @ts-ignore str += /** @type {ContentString} */ (n.content).str
str += n.string
break break
case ItemFormat: case ContentEmbed:
packStr() packStr()
// @ts-ignore ops.push({
updateCurrentAttributes(currentAttributes, n) insert: /** @type {ContentEmbed} */ (n.content).embed
})
break
case ContentFormat:
packStr()
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
break break
} }
} }
@@ -810,8 +777,7 @@ export class YText extends AbstractType {
insertText(transaction, this, left, right, currentAttributes, text, attributes) insertText(transaction, this, left, right, currentAttributes, text, attributes)
}) })
} else { } else {
// @ts-ignore /** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
this._pending.push(() => this.insert(index, text, attributes))
} }
} }
@@ -836,8 +802,7 @@ export class YText extends AbstractType {
insertText(transaction, this, left, right, currentAttributes, embed, attributes) insertText(transaction, this, left, right, currentAttributes, embed, attributes)
}) })
} else { } else {
// @ts-ignore /** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
this._pending.push(() => this.insertEmbed(index, embed, attributes))
} }
} }
@@ -860,8 +825,7 @@ export class YText extends AbstractType {
deleteText(transaction, left, right, currentAttributes, length) deleteText(transaction, left, right, currentAttributes, length)
}) })
} else { } else {
// @ts-ignore /** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
this._pending.push(() => this.delete(index, length))
} }
} }
@@ -886,8 +850,7 @@ export class YText extends AbstractType {
formatText(transaction, this, left, right, currentAttributes, length, attributes) formatText(transaction, this, left, right, currentAttributes, length, attributes)
}) })
} else { } else {
// @ts-ignore /** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))
this._pending.push(() => this.format(index, length, attributes))
} }
} }

View File

@@ -8,7 +8,7 @@ import {
typeMapGetAll, typeMapGetAll,
typeListForEach, typeListForEach,
YXmlElementRefID, YXmlElementRefID,
Snapshot, Doc, ItemType // eslint-disable-line Snapshot, Doc, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -40,16 +40,14 @@ export class YXmlElement extends YXmlFragment {
* * Observer functions are fired * * Observer functions are fired
* *
* @param {Doc} y The Yjs instance * @param {Doc} y The Yjs instance
* @param {ItemType} item * @param {Item} item
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
// @ts-ignore this.insert(0, /** @type {Array} */ (this._prelimContent))
this.insert(0, this._prelimContent)
this._prelimContent = null this._prelimContent = null
// @ts-ignore ;(/** @type {Map<string, any>} */ (this._prelimAttrs)).forEach((value, key) => {
this._prelimAttrs.forEach((value, key) => {
this.setAttribute(key, value) this.setAttribute(key, value)
}) })
this._prelimContent = null this._prelimContent = null
@@ -105,8 +103,7 @@ export class YXmlElement extends YXmlFragment {
typeMapDelete(transaction, this, attributeName) typeMapDelete(transaction, this, attributeName)
}) })
} else { } else {
// @ts-ignore /** @type {Map<string,any>} */ (this._prelimAttrs).delete(attributeName)
this._prelimAttrs.delete(attributeName)
} }
} }
@@ -124,8 +121,7 @@ export class YXmlElement extends YXmlFragment {
typeMapSet(transaction, this, attributeName, attributeValue) typeMapSet(transaction, this, attributeName, attributeValue)
}) })
} else { } else {
// @ts-ignore /** @type {Map<string, any>} */ (this._prelimAttrs).set(attributeName, attributeValue)
this._prelimAttrs.set(attributeName, attributeValue)
} }
} }
@@ -139,8 +135,7 @@ export class YXmlElement extends YXmlFragment {
* @public * @public
*/ */
getAttribute (attributeName) { getAttribute (attributeName) {
// @ts-ignore return /** @type {any} */ (typeMapGet(this, attributeName))
return typeMapGet(this, attributeName)
} }
/** /**

View File

@@ -14,7 +14,7 @@ import {
YXmlFragmentRefID, YXmlFragmentRefID,
callTypeObservers, callTypeObservers,
transact, transact,
Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -59,10 +59,9 @@ export class YXmlTreeWalker {
this._filter = f this._filter = f
this._root = root this._root = root
/** /**
* @type {ItemType | null} * @type {Item}
*/ */
// @ts-ignore this._currentNode = /** @type {Item} */ (root._start)
this._currentNode = root._start
this._firstCall = true this._firstCall = true
} }
@@ -77,18 +76,21 @@ export class YXmlTreeWalker {
* @public * @public
*/ */
next () { next () {
/**
* @type {Item|null}
*/
let n = this._currentNode 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 { 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 // walk down in the tree
// @ts-ignore n = type._start
n = n.type._start
} else { } else {
// walk right or up in the tree // walk right or up in the tree
while (n !== null) { while (n !== null) {
if (n.right !== null) { if (n.right !== null) {
// @ts-ignore
n = n.right n = n.right
break break
} else if (n.parent === this._root) { } else if (n.parent === this._root) {
@@ -98,16 +100,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._firstCall = false
this._currentNode = n
if (n === null) { if (n === null) {
// @ts-ignore return undefined if done=true (the expected result) // @ts-ignore
return { value: undefined, done: true } return { value: undefined, done: true }
} }
// @ts-ignore this._currentNode = n
return { value: n.type, done: false } return { value: /** @type {any} */ (n.content).type, done: false }
} }
} }

View File

@@ -3,7 +3,8 @@ import {
findIndexSS, findIndexSS,
createID, createID,
getState, getState,
AbstractStruct, AbstractItem, StructStore, Transaction, ID // eslint-disable-line splitItem,
Item, AbstractStruct, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
@@ -11,7 +12,7 @@ import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
class DeleteItem { export class DeleteItem {
/** /**
* @param {number} clock * @param {number} clock
* @param {number} len * @param {number} len
@@ -235,13 +236,13 @@ export const readDeleteSet = (decoder, transaction, store) => {
let index = findIndexSS(structs, clock) let index = findIndexSS(structs, clock)
/** /**
* We can ignore the case of GC and Delete structs, because we are going to skip them * We can ignore the case of GC and Delete structs, because we are going to skip them
* @type {AbstractItem} * @type {Item}
*/ */
// @ts-ignore // @ts-ignore
let struct = structs[index] let struct = structs[index]
// split the first item if necessary // split the first item if necessary
if (!struct.deleted && struct.id.clock < clock) { 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 index++ // increase we now want to use the next struct
} }
while (index < structs.length) { while (index < structs.length) {
@@ -250,7 +251,7 @@ export const readDeleteSet = (decoder, transaction, store) => {
if (struct.id.clock < clock + len) { if (struct.id.clock < clock + len) {
if (!struct.deleted) { if (!struct.deleted) {
if (clock + len < struct.id.clock + struct.length) { 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) struct.delete(transaction)
} }

View File

@@ -10,7 +10,7 @@ import {
YMap, YMap,
YXmlFragment, YXmlFragment,
transact, transact,
AbstractItem, Transaction, YEvent // eslint-disable-line Item, Transaction, YEvent // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import { Observable } from 'lib0/observable.js' import { Observable } from 'lib0/observable.js'
@@ -97,7 +97,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
const t = new TypeConstructor() const t = new TypeConstructor()
t._map = type._map t._map = type._map
type._map.forEach(/** @param {AbstractItem?} n */ n => { type._map.forEach(/** @param {Item?} n */ n => {
for (; n !== null; n = n.left) { for (; n !== null; n = n.left) {
n.parent = t n.parent = t
} }

View File

@@ -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 decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'

View File

@@ -1,16 +1,15 @@
import { import {
getItem, getItem,
getItemType,
createID, createID,
writeID, writeID,
readID, readID,
compareIDs, compareIDs,
getState, getState,
findRootTypeKey, findRootTypeKey,
AbstractItem, Item,
ItemType, ContentType,
ID, StructStore, Doc, AbstractType // eslint-disable-line ID, Doc, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -224,7 +223,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
return null return null
} }
const right = getItem(store, rightID) const right = getItem(store, rightID)
if (!(right instanceof AbstractItem)) { if (!(right instanceof Item)) {
return null return null
} }
index = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock index = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
@@ -244,9 +243,9 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
// type does not exist yet // type does not exist yet
return null return null
} }
const struct = getItemType(store, typeID) const struct = getItem(store, typeID)
if (struct instanceof ItemType) { if (struct instanceof Item && struct.content instanceof ContentType) {
type = struct.type type = struct.content.type
} else { } else {
// struct is garbage collected // struct is garbage collected
return null return null

View File

@@ -1,7 +1,7 @@
import { import {
isDeleted, isDeleted,
DeleteSet, AbstractItem // eslint-disable-line DeleteSet, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
export class Snapshot { export class Snapshot {
@@ -31,7 +31,7 @@ export class Snapshot {
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm) export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
/** /**
* @param {AbstractItem} item * @param {Item} item
* @param {Snapshot|undefined} snapshot * @param {Snapshot|undefined} snapshot
* *
* @protected * @protected

View File

@@ -1,7 +1,8 @@
import { import {
GC, GC,
Transaction, AbstractStructRef, ID, ItemType, AbstractItem, AbstractStruct // eslint-disable-line splitItem,
GCRef, ItemRef, Transaction, ID, Item, AbstractStruct // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
@@ -21,14 +22,14 @@ export class StructStore {
* We could shift the array of refs instead, but shift is incredible * We could shift the array of refs instead, but shift is incredible
* slow in Chrome for arrays with more than 100k elements * slow in Chrome for arrays with more than 100k elements
* @see tryResumePendingStructRefs * @see tryResumePendingStructRefs
* @type {Map<number,{i:number,refs:Array<AbstractStructRef>}>} * @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
* @private * @private
*/ */
this.pendingClientsStructRefs = new Map() this.pendingClientsStructRefs = new Map()
/** /**
* Stack of pending structs waiting for struct dependencies * Stack of pending structs waiting for struct dependencies
* Maximum length of stack is structReaders.size * Maximum length of stack is structReaders.size
* @type {Array<AbstractStructRef>} * @type {Array<GCRef|ItemRef>}
* @private * @private
*/ */
this.pendingStack = [] this.pendingStack = []
@@ -169,7 +170,7 @@ export const find = (store, id) => {
* *
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {AbstractItem} * @return {Item}
* *
* @private * @private
* @function * @function
@@ -177,43 +178,23 @@ export const find = (store, id) => {
// @ts-ignore // @ts-ignore
export const getItem = (store, id) => 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
*/
// @ts-ignore
export const getItemType = (store, id) => find(store, id)
/** /**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise. * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {AbstractItem} * @return {Item}
* *
* @private * @private
* @function * @function
*/ */
export const getItemCleanStart = (transaction, store, id) => { export const getItemCleanStart = (transaction, store, id) => {
/** const structs = /** @type {Array<Item>} */ (store.clients.get(id.client))
* @type {Array<AbstractItem>}
*/
// @ts-ignore
const structs = store.clients.get(id.client)
const index = findIndexSS(structs, id.clock) const index = findIndexSS(structs, id.clock)
/**
* @type {AbstractItem}
*/
let struct = structs[index] let struct = structs[index]
if (struct.id.clock < id.clock && struct.constructor !== GC) { if (struct.id.clock < id.clock && struct.constructor !== GC) {
struct = struct.splitAt(transaction, id.clock - struct.id.clock) struct = splitItem(transaction, struct, id.clock - struct.id.clock)
structs.splice(index + 1, 0, struct) structs.splice(index + 1, 0, struct)
} }
return struct return struct
@@ -225,21 +206,21 @@ export const getItemCleanStart = (transaction, store, id) => {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {AbstractItem} * @return {Item}
* *
* @private * @private
* @function * @function
*/ */
export const getItemCleanEnd = (transaction, store, id) => { export const getItemCleanEnd = (transaction, store, id) => {
/** /**
* @type {Array<AbstractItem>} * @type {Array<Item>}
*/ */
// @ts-ignore // @ts-ignore
const structs = store.clients.get(id.client) const structs = store.clients.get(id.client)
const index = findIndexSS(structs, id.clock) const index = findIndexSS(structs, id.clock)
const struct = structs[index] const struct = structs[index]
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) { 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 return struct
} }
@@ -254,10 +235,6 @@ export const getItemCleanEnd = (transaction, store, id) => {
* @function * @function
*/ */
export const replaceStruct = (store, struct, newStruct) => { export const replaceStruct = (store, struct, newStruct) => {
/** const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(struct.id.client))
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(struct.id.client)
structs[findIndexSS(structs, struct.id.clock)] = newStruct structs[findIndexSS(structs, struct.id.clock)] = newStruct
} }

View File

@@ -9,7 +9,7 @@ import {
getStateVector, getStateVector,
findIndexSS, findIndexSS,
callEventHandlerListeners, callEventHandlerListeners,
AbstractItem, Item,
ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -179,20 +179,15 @@ export const transact = (doc, f, origin = null) => {
if (left.deleted === right.deleted && left.constructor === right.constructor) { if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) { if (left.mergeWith(right)) {
structs.splice(pos, 1) structs.splice(pos, 1)
if (right instanceof AbstractItem && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) { if (right instanceof Item && 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, /** @type {Item} */ (left))
right.parent._map.set(right.parentSub, left)
} }
} }
} }
} }
// replace deleted items with ItemDeleted / GC // replace deleted items with ItemDeleted / GC
for (const [client, deleteItems] of ds.clients) { for (const [client, deleteItems] of ds.clients) {
/** const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
for (let di = deleteItems.length - 1; di >= 0; di--) { for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di] const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len const endDeleteItemClock = deleteItem.clock + deleteItem.len
@@ -205,7 +200,7 @@ export const transact = (doc, f, origin = null) => {
if (deleteItem.clock + deleteItem.len <= struct.id.clock) { if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break break
} }
if (struct.deleted && struct instanceof AbstractItem) { if (struct.deleted && struct instanceof Item) {
struct.gc(store, false) struct.gc(store, false)
} }
} }
@@ -214,11 +209,7 @@ export const transact = (doc, f, origin = null) => {
// try to merge deleted / gc'd items // try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets // merge from right to left for better efficiecy and so we don't miss any merge targets
for (const [client, deleteItems] of ds.clients) { for (const [client, deleteItems] of ds.clients) {
/** const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
for (let di = deleteItems.length - 1; di >= 0; di--) { for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di] const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item // start with merging the item next to the last deleted item
@@ -237,11 +228,7 @@ export const transact = (doc, f, origin = null) => {
for (const [client, clock] of transaction.afterState) { for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0 const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) { if (beforeClock !== clock) {
/** const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
// we iterate from right to left so we can safely remove entries // we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1) const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) { for (let i = structs.length - 1; i >= firstChangePos; i--) {
@@ -255,11 +242,7 @@ export const transact = (doc, f, origin = null) => {
for (const mid of transaction._mergeStructs) { for (const mid of transaction._mergeStructs) {
const client = mid.client const client = mid.client
const clock = mid.clock const clock = mid.clock
/** const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
const replacedStructPos = findIndexSS(structs, clock) const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) { if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1) tryToMergeWithLeft(structs, replacedStructPos + 1)

View File

@@ -200,4 +200,3 @@ export class UndoManager {
return performedRedo return performedRedo
} }
} }
}

View File

@@ -1,18 +1,23 @@
/** /**
* @module encoding * @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 { import {
findIndexSS, findIndexSS,
GCRef, GCRef,
ItemBinaryRef, ItemRef,
ItemDeletedRef,
ItemEmbedRef,
ItemFormatRef,
ItemJSONRef,
ItemStringRef,
ItemTypeRef,
writeID, writeID,
createID, createID,
readID, readID,
@@ -21,27 +26,13 @@ import {
readDeleteSet, readDeleteSet,
writeDeleteSet, writeDeleteSet,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
Doc, Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary.js'
/**
* @private
*/
export const structRefs = [
GCRef,
ItemBinaryRef,
ItemDeletedRef,
ItemEmbedRef,
ItemFormatRef,
ItemJSONRef,
ItemStringRef,
ItemTypeRef
]
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {Array<AbstractStruct>} structs All structs by `client` * @param {Array<AbstractStruct>} structs All structs by `client`
@@ -68,19 +59,19 @@ const writeStructs = (encoder, structs, client, clock) => {
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {number} numOfStructs * @param {number} numOfStructs
* @param {ID} nextID * @param {ID} nextID
* @return {Array<AbstractStructRef>} * @return {Array<GCRef|ItemRef>}
* *
* @private * @private
* @function * @function
*/ */
const readStructRefs = (decoder, numOfStructs, nextID) => { const readStructRefs = (decoder, numOfStructs, nextID) => {
/** /**
* @type {Array<AbstractStructRef>} * @type {Array<GCRef|ItemRef>}
*/ */
const refs = [] const refs = []
for (let i = 0; i < numOfStructs; i++) { for (let i = 0; i < numOfStructs; i++) {
const info = decoding.readUint8(decoder) 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) nextID = createID(nextID.client, nextID.clock + ref.length)
refs.push(ref) refs.push(ref)
} }
@@ -119,14 +110,14 @@ export const writeClientsStructs = (encoder, store, _sm) => {
/** /**
* @param {decoding.Decoder} decoder The decoder object to read data from. * @param {decoding.Decoder} decoder The decoder object to read data from.
* @return {Map<number,Array<AbstractStructRef>>} * @return {Map<number,Array<GCRef|ItemRef>>}
* *
* @private * @private
* @function * @function
*/ */
export const readClientsStructRefs = decoder => { export const readClientsStructRefs = decoder => {
/** /**
* @type {Map<number,Array<AbstractStructRef>>} * @type {Map<number,Array<GCRef|ItemRef>>}
*/ */
const clientRefs = new Map() const clientRefs = new Map()
const numOfStateUpdates = decoding.readVarUint(decoder) const numOfStateUpdates = decoding.readVarUint(decoder)
@@ -193,7 +184,7 @@ const resumeStructIntegration = (transaction, store) => {
structRefs.refs[structRefs.i] = ref structRefs.refs[structRefs.i] = ref
stack[stack.length - 1] = r stack[stack.length - 1] = r
// sort the set because this approach might bring the list out of order // 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 structRefs.i = 0
continue continue
} }
@@ -254,7 +245,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
/** /**
* @param {StructStore} store * @param {StructStore} store
* @param {Map<number, Array<AbstractStructRef>>} clientsStructsRefs * @param {Map<number, Array<GCRef|ItemRef>>} clientsStructsRefs
* *
* @private * @private
* @function * @function
@@ -401,9 +392,10 @@ export const decodeStateVector = decodedState => readStateVector(decoding.create
export const writeDocumentStateVector = (encoder, doc) => { export const writeDocumentStateVector = (encoder, doc) => {
encoding.writeVarUint(encoder, doc.store.clients.size) encoding.writeVarUint(encoder, doc.store.clients.size)
doc.store.clients.forEach((structs, client) => { doc.store.clients.forEach((structs, client) => {
const id = structs[structs.length - 1].id const struct = structs[structs.length - 1]
const id = struct.id
encoding.writeVarUint(encoder, id.client) encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock) encoding.writeVarUint(encoder, id.clock + struct.length)
}) })
return encoder return encoder
} }
@@ -416,7 +408,7 @@ export const writeDocumentStateVector = (encoder, doc) => {
* *
* @function * @function
*/ */
export const encodeDocumentStateVector = doc => { export const encodeStateVector = doc => {
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
writeDocumentStateVector(encoder, doc) writeDocumentStateVector(encoder, doc)
return encoding.toUint8Array(encoder) return encoding.toUint8Array(encoder)

View File

@@ -1,36 +1,26 @@
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing.js'
import { import {
structRefs, contentRefs,
structGCRefNumber, readContentBinary,
structBinaryRefNumber, readContentDeleted,
structDeletedRefNumber, readContentString,
structEmbedRefNumber, readContentJSON,
structFormatRefNumber, readContentEmbed,
structJSONRefNumber, readContentType,
structStringRefNumber, readContentFormat
structTypeRefNumber,
GCRef,
ItemBinaryRef,
ItemDeletedRef,
ItemEmbedRef,
ItemFormatRef,
ItemJSONRef,
ItemStringRef,
ItemTypeRef
} from '../src/internals.js' } from '../src/internals.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testStructReferences = tc => { export const testStructReferences = tc => {
t.assert(structRefs.length === 8) t.assert(contentRefs.length === 8)
t.assert(structRefs[structGCRefNumber] === GCRef) t.assert(contentRefs[1] === readContentDeleted)
t.assert(structRefs[structBinaryRefNumber] === ItemBinaryRef) t.assert(contentRefs[2] === readContentJSON)
t.assert(structRefs[structDeletedRefNumber] === ItemDeletedRef) t.assert(contentRefs[3] === readContentBinary)
t.assert(structRefs[structEmbedRefNumber] === ItemEmbedRef) t.assert(contentRefs[4] === readContentString)
t.assert(structRefs[structFormatRefNumber] === ItemFormatRef) t.assert(contentRefs[5] === readContentEmbed)
t.assert(structRefs[structJSONRefNumber] === ItemJSONRef) t.assert(contentRefs[6] === readContentFormat)
t.assert(structRefs[structStringRefNumber] === ItemStringRef) t.assert(contentRefs[7] === readContentType)
t.assert(structRefs[structTypeRefNumber] === ItemTypeRef)
} }

View File

@@ -3,8 +3,8 @@ import * as Y from '../src/index.js'
import { import {
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
getStateVector, getStateVector,
AbstractItem, Item,
DeleteSet, StructStore, Doc // eslint-disable-line DeleteItem, DeleteSet, StructStore, Doc // eslint-disable-line
} from '../src/internals.js' } from '../src/internals.js'
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing.js'
@@ -12,6 +12,7 @@ import * as prng from 'lib0/prng.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
import * as syncProtocol from 'y-protocols/sync.js' import * as syncProtocol from 'y-protocols/sync.js'
export * from '../src/internals.js'
/** /**
* @param {TestYInstance} y // publish message created by `y` to all other online clients * @param {TestYInstance} y // publish message created by `y` to all other online clients
@@ -240,8 +241,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
} }
testConnector.syncAll() testConnector.syncAll()
result.testObjects = result.users.map(initTestObject || (() => null)) result.testObjects = result.users.map(initTestObject || (() => null))
// @ts-ignore return /** @type {any} */ (result)
return result
} }
/** /**
@@ -256,7 +256,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
export const compare = users => { export const compare = users => {
users.forEach(u => u.connect()) users.forEach(u => u.connect())
while (users[0].tc.flushAllMessages()) {} while (users[0].tc.flushAllMessages()) {}
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 userMapValues = users.map(u => u.getMap('map').toJSON())
const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString()) const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString())
const userTextValues = users.map(u => u.getText('text').toDelta()) const userTextValues = users.map(u => u.getText('text').toDelta())
@@ -265,13 +265,24 @@ export const compare = users => {
t.assert(u.store.pendingStack.length === 0) t.assert(u.store.pendingStack.length === 0)
t.assert(u.store.pendingClientsStructRefs.size === 0) t.assert(u.store.pendingClientsStructRefs.size === 0)
} }
// Test Array iterator
t.compare(userArrayValues[0], Array.from(users[0].getArray('array').toJSON()))
// Test Map iterator
/**
* @type {Object<string,any>}
*/
const mapRes = {}
for (let [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++) { for (let i = 0; i < users.length - 1; i++) {
t.compare(userArrayValues[i].length, users[i].getArray('array').length) t.compare(userArrayValues[i].length, users[i].getArray('array').length)
t.compare(userArrayValues[i], userArrayValues[i + 1]) t.compare(userArrayValues[i], userArrayValues[i + 1])
t.compare(userMapValues[i], userMapValues[i + 1]) t.compare(userMapValues[i], userMapValues[i + 1])
t.compare(userXmlValues[i], userXmlValues[i + 1]) t.compare(userXmlValues[i], userXmlValues[i + 1])
// @ts-ignore t.compare(userTextValues[i].map(/** @param {any} a */ a => a.insert).join('').length, users[i].getText('text').length)
t.compare(userTextValues[i].map(a => a.insert).join('').length, users[i].getText('text').length)
t.compare(userTextValues[i], userTextValues[i + 1]) t.compare(userTextValues[i], userTextValues[i + 1])
t.compare(getStateVector(users[i].store), getStateVector(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)) compareDS(createDeleteSetFromStructStore(users[i].store), createDeleteSetFromStructStore(users[i + 1].store))
@@ -281,8 +292,8 @@ export const compare = users => {
} }
/** /**
* @param {AbstractItem?} a * @param {Item?} a
* @param {AbstractItem?} b * @param {Item?} b
* @return {boolean} * @return {boolean}
*/ */
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id)) export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
@@ -294,11 +305,10 @@ export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y
export const compareStructStores = (ss1, ss2) => { export const compareStructStores = (ss1, ss2) => {
t.assert(ss1.clients.size === ss2.clients.size) t.assert(ss1.clients.size === ss2.clients.size)
for (const [client, structs1] of ss1.clients) { 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) t.assert(structs2 !== undefined && structs1.length === structs2.length)
for (let i = 0; i < structs1.length; i++) { for (let i = 0; i < structs1.length; i++) {
const s1 = structs1[i] const s1 = structs1[i]
// @ts-ignore
const s2 = structs2[i] const s2 = structs2[i]
// checks for abstract struct // checks for abstract struct
if ( if (
@@ -309,9 +319,9 @@ export const compareStructStores = (ss1, ss2) => {
) { ) {
t.fail('Structs dont match') t.fail('Structs dont match')
} }
if (s1 instanceof AbstractItem) { if (s1 instanceof Item) {
if ( 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))) || !((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) || !compareItemIDs(s1.right, s2.right) ||
!Y.compareIDs(s1.origin, s2.origin) || !Y.compareIDs(s1.origin, s2.origin) ||
@@ -337,11 +347,10 @@ export const compareStructStores = (ss1, ss2) => {
export const compareDS = (ds1, ds2) => { export const compareDS = (ds1, ds2) => {
t.assert(ds1.clients.size === ds2.clients.size) t.assert(ds1.clients.size === ds2.clients.size)
for (const [client, deleteItems1] of ds1.clients) { 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) t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
for (let i = 0; i < deleteItems1.length; i++) { for (let i = 0; i < deleteItems1.length; i++) {
const di1 = deleteItems1[i] const di1 = deleteItems1[i]
// @ts-ignore
const di2 = deleteItems2[i] const di2 = deleteItems2[i]
if (di1.clock !== di2.clock || di1.len !== di2.len) { if (di1.clock !== di2.clock || di1.len !== di2.len) {
t.fail('DeleteSets dont match') t.fail('DeleteSets dont match')
@@ -360,7 +369,7 @@ export const compareDS = (ds1, ds2) => {
/** /**
* @template T * @template T
* @param {t.TestCase} tc * @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 {number} iterations
* @param {InitTestObjectCallback<T>} [initTestObject] * @param {InitTestObjectCallback<T>} [initTestObject]
*/ */

View File

@@ -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 * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing.js'
@@ -25,10 +25,10 @@ export const testDeleteInsert = tc => {
*/ */
export const testInsertThreeElementsTryRegetProperty = tc => { export const testInsertThreeElementsTryRegetProperty = tc => {
const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, [1, 2, 3]) array0.insert(0, [1, true, false])
t.compare(array0.toJSON(), [1, 2, 3], '.toJSON() works') t.compare(array0.toJSON(), [1, true, false], '.toJSON() works')
testConnector.flushAllMessages() 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) compare(users)
} }
@@ -282,7 +282,7 @@ let _uniqueNumber = 0
const getUniqueNumber = () => _uniqueNumber++ const getUniqueNumber = () => _uniqueNumber++
/** /**
* @type {Array<function(TestYInstance,prng.PRNG):void>} * @type {Array<function(Doc,prng.PRNG,any):void>}
*/ */
const arrayTransactions = [ const arrayTransactions = [
function insert (user, gen) { function insert (user, gen) {

View File

@@ -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 { import {
compareIDs compareIDs
@@ -19,6 +19,8 @@ export const testBasicMapTests = tc => {
map0.set('string', 'hello Y') map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } }) map0.set('object', { key: { key2: 'value' } })
map0.set('y-map', new Y.Map()) map0.set('y-map', new Y.Map())
map0.set('boolean1', true)
map0.set('boolean0', false)
const map = map0.get('y-map') const map = map0.get('y-map')
map.set('y-array', new Y.Array()) map.set('y-array', new Y.Array())
const array = map.get('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('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('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.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)') t.assert(map0.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('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('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.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)') t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
// compare disconnected user // compare disconnected user
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected') t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected') t.assert(map2.get('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.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') t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
compare(users) compare(users)
@@ -320,7 +328,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
} }
/** /**
* @type {Array<function(TestYInstance,prng.PRNG):void>} * @type {Array<function(Doc,prng.PRNG):void>}
*/ */
const mapTransactions = [ const mapTransactions = [
function set (user, gen) { function set (user, gen) {

View File

@@ -27,6 +27,13 @@ export const testBasicInsertAndDelete = tc => {
text0.delete(1, 1) text0.delete(1, 1)
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)') t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
t.compare(delta, [{ retain: 1 }, { delete: 1 }]) t.compare(delta, [{ retain: 1 }, { delete: 1 }])
users[0].transact(() => {
text0.insert(0, '1')
text0.delete(0, 1)
})
t.compare(delta, [])
compare(users) compare(users)
} }
@@ -56,7 +63,7 @@ export const testBasicFormat = tc => {
t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }]) t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }])
t.compare(delta, [{ insert: 'z', attributes: { bold: true } }]) t.compare(delta, [{ insert: 'z', attributes: { bold: true } }])
// @ts-ignore // @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') text0.insert(0, 'y')
t.assert(text0.toString() === 'yzb') t.assert(text0.toString() === 'yzb')
t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }]) t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }])
@@ -67,3 +74,16 @@ export const testBasicFormat = tc => {
t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }]) t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }])
compare(users) 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'}
}])
}

View File

@@ -36,8 +36,10 @@
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ "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'. */ "paths": {
"yjs": ["./src/index.js"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "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. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */