Compare commits

..

63 Commits

Author SHA1 Message Date
Kevin Jahns
76c389dba0 13.0.2 2020-02-03 12:23:39 +01:00
Kevin Jahns
78fa98c000 add type definition for YText.length 2020-02-03 12:22:35 +01:00
Kevin Jahns
e9f9e08450 13.0.1 2020-01-27 03:43:45 +01:00
Kevin Jahns
e3c59b0aa7 more options to gc data (undomanager.clear and tryGc) 2020-01-27 03:42:32 +01:00
Kevin Jahns
705dce7838 add y-indexeddb section 2020-01-23 22:49:04 +01:00
Kevin Jahns
0fb55981ba 13.0.0 2020-01-23 21:53:02 +01:00
Kevin Jahns
89378e29ae publish stable Yjs release 🎆 2020-01-23 21:51:26 +01:00
Kevin Jahns
cce35270ec typescript typingis!!! fixes #180 2020-01-23 21:45:56 +01:00
Kevin Jahns
d78180bf97 make opts optional in PermanentUserData 2020-01-23 18:05:12 +01:00
Kevin Jahns
0ab415de3e 13.0.0-108 2020-01-23 05:01:05 +01:00
Kevin Jahns
ff3969caeb dedupe npm 2020-01-23 05:00:11 +01:00
Kevin Jahns
c82cc9f8d6 lint 2020-01-23 04:59:17 +01:00
Kevin Jahns
ef5c71bd8b PermanentUserData fixes 2020-01-23 04:58:02 +01:00
Kevin Jahns
bd6be3d23b 13.0.0-107 2020-01-22 16:45:48 +01:00
Kevin Jahns
0e6deab9c9 type toJSON returns 2020-01-22 16:44:30 +01:00
Kevin Jahns
6cd9e2be32 lint 2020-01-22 16:42:16 +01:00
Kevin Jahns
ac8dab1e88 Merge pull request #179 from garth/text-tojson
basic Y.Text toJSON returns {unformatted:string}
2020-01-22 16:19:01 +01:00
Garth Williams
38ed725c2c basic Y.Text toJSON returns unformatted string
This avoids text nodes in nested structures returning undefined when toJSON is called by a parent.
2020-01-22 13:34:13 +01:00
Kevin Jahns
a210bad25e update keywords 2020-01-19 00:43:23 +01:00
Kevin Jahns
6929a4f0f8 13.0.0-106 2020-01-14 05:16:43 +01:00
Kevin Jahns
52dacfa5f2 update package-lock 2020-01-14 05:15:36 +01:00
Kevin Jahns
27efe86f9c isParentOf 2020-01-14 05:13:51 +01:00
Kevin Jahns
882b9055c7 fix localimports path ending 2020-01-14 02:36:29 +01:00
Kevin Jahns
e089089413 fix debug resolve 2020-01-13 17:03:56 +01:00
Kevin Jahns
197932752e 13.0.0-105 2020-01-13 14:55:05 +01:00
Kevin Jahns
f0b2bdaf34 revert to classic cjs module 2020-01-13 14:54:07 +01:00
Kevin Jahns
b96362c0f1 use correct module script 2020-01-13 07:55:58 +01:00
Kevin Jahns
67f241cd7a 13.0.0-104 2020-01-13 07:48:47 +01:00
Kevin Jahns
c8af0bebf7 fix preversion script 2020-01-13 07:47:43 +01:00
Kevin Jahns
4f35e799a6 update to lib0@.2 2020-01-13 07:41:31 +01:00
Kevin Jahns
eb2a52dd26 update README with podcast links, consulting info, and y-webrtc 2019-12-11 13:26:46 +01:00
Kevin Jahns
189b1068ae 13.0.0-103 2019-12-10 20:52:20 +01:00
Kevin Jahns
7a3b60a5d7 add markdownlint-cli as dep 2019-12-10 20:51:07 +01:00
Kevin Jahns
99f06fc093 bump lib0 for improved encoding performance 2019-12-10 20:46:58 +01:00
Kevin Jahns
22917bca19 fix gc & proper options typings for Y.Doc, fixes #176 2019-12-10 17:51:49 +01:00
Kevin Jahns
7f0e25dcba permanent user store writes updates in separate transaction 2019-12-10 17:18:57 +01:00
Kevin Jahns
d90c9b1cb2 bump lib0 for faster text encoding 2019-12-10 00:26:28 +01:00
Kevin Jahns
c426055f17 spelling 2019-12-10 00:19:02 +01:00
Kevin Jahns
18c9010b63 Merge branch 'master' of github.com:y-js/yjs 2019-11-26 13:02:49 +01:00
Kevin Jahns
c3edac62ef doc typo 2019-11-26 13:02:43 +01:00
Kevin Jahns
755de18fd5 Create Funding.yml 2019-11-07 14:41:50 +01:00
Kevin Jahns
641dc25076 13.0.0-102 2019-10-25 23:47:23 +02:00
Kevin Jahns
1d58ea785f Merge branch 'master' of github.com:yjs/yjs 2019-10-25 23:45:50 +02:00
Kevin Jahns
f53dff5043 delay errors in observe callbacks to throw after cleanup is done 2019-10-25 23:44:09 +02:00
Kevin Jahns
74d1a31f49 Merge pull request #174 from boschDev/master
Fix attrs loop in yXmlText
2019-10-15 17:19:30 +02:00
Roeland Bosch
d1063ab70b Fix attrs loop in yXmlText 2019-10-15 17:07:20 +02:00
Kevin Jahns
f4c919d9ec 13.0.0-101 2019-10-08 18:33:50 +02:00
Kevin Jahns
aeb23dbaa9 follow redone items to prevent some undo-redo issues. Fixes #162 2019-10-08 18:31:56 +02:00
Kevin Jahns
6d4f0c0cdd 13.0.0-100 2019-10-08 17:40:32 +02:00
Kevin Jahns
303138f309 sanitize items before undoing. fixes #165 2019-10-08 17:36:00 +02:00
Kevin Jahns
ad373a3dce Merge pull request #172 from istvank/patch-1
Fixing Y.Map's documentation of forEach
2019-10-05 20:09:53 +02:00
István Koren
2150fa58f2 Fixing Y.Map's documentation of forEach
fixes #171 As always, it's an honor to submit a PR! 🐒 There was also a missing dot in the Y.XmlFragment title.
2019-10-05 15:14:30 +02:00
Kevin Jahns
ece4841b5c update stackItem.meta doc 2019-10-03 22:06:07 +02:00
Kevin Jahns
8103220c05 Merge branch 'master' of github.com:yjs/yjs 2019-09-30 11:10:13 +02:00
Kevin Jahns
66d500f08d YEvent: consider case that item was added & removed in the same transaction 2019-09-30 11:10:03 +02:00
Kevin Jahns
5f8e7c7ba7 Merge pull request #169 from yjs/improve-readme
update quill cursors support
2019-09-23 11:22:51 +02:00
Nik Graf
7b8eee6b25 update quill cursors support 2019-09-23 11:22:24 +02:00
Kevin Jahns
1d5947c602 13.0.0-99 2019-09-23 11:11:45 +02:00
Kevin Jahns
53e4028952 Merge pull request #168 from yjs/fix-absolute-position-calculation
fix absolute position calculation
2019-09-23 11:09:48 +02:00
Nik Graf
b38a8d99e5 fix absolute position calculation 2019-09-23 11:05:50 +02:00
Kevin Jahns
6c4971ae25 13.0.0-98 2019-09-17 18:55:04 +02:00
Kevin Jahns
d1f5ff0f59 implement PermanentUserData storage prototype 2019-09-17 18:53:59 +02:00
Kevin Jahns
1d297601e8 export .createDeleteSet functionality 2019-09-04 22:08:05 +02:00
44 changed files with 1899 additions and 3250 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: dmonad
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -17,9 +17,11 @@ suited for even large documents.
* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev) * Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev)
* Benchmarks: * Benchmarks:
[https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks) [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
* Podcast [**"Yjs Deep Dive into real time collaborative editing solutions":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0)
* Podcast [**"Google Docs-style editing in Gutenberg with the YJS framework":**](https://publishpress.com/blog/yjs/)
:warning: This is the documentation for v13 (still in alpha). For the stable v12 :construction_worker_woman: If you are looking for professional support to build
release checkout the [v12 docs](./README.v12.md) :warning: collaborative or distributed applications ping us at <yjs@tag1consulting.com>.
## Table of Contents ## Table of Contents
@@ -55,7 +57,7 @@ are implemented in separate modules.
| Name | Cursors | Binding | Demo | | Name | Cursors | Binding | Demo |
|---|:-:|---|---| |---|:-:|---|---|
| [ProseMirror](https://prosemirror.net/) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) | | [ProseMirror](https://prosemirror.net/) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) |
| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) | | [Quill](https://quilljs.com/) | | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) | | [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) | | [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) |
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) | | [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) |
@@ -70,18 +72,25 @@ manage all that for you and are the perfect starting point for your
collaborative app. collaborative app.
<dl> <dl>
<dt><a href="http://github.com/yjs/y-webrtc">y-webrtc</a></dt>
<dd>
Propagates document updates peer-to-peer using WebRTC. The peers exchange
signaling data over signaling servers. Publically available signaling servers
are available. Communication over the signaling servers can be encrypted by
providing a shared secret, keeping the connection information and the shared
document private.
</dd>
<dt><a href="http://github.com/yjs/y-websocket">y-websocket</a></dt> <dt><a href="http://github.com/yjs/y-websocket">y-websocket</a></dt>
<dd> <dd>
A module that contains a simple websocket backend and a websocket client that A module that contains a simple websocket backend and a websocket client that
connects to that backend. The backend can be extended to persist updates in a connects to that backend. The backend can be extended to persist updates in a
leveldb database. leveldb database.
</dd> </dd>
<dt><a href="http://github.com/yjs/y-mesh">y-mesh</a></dt> <dt><a href="http://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
<dd> <dd>
[WIP] Creates a connected graph of webrtc connections with a high Efficiently persists document updates to the browsers indexeddb database.
<a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It The document is immediately available and only diffs need to be synced through the
requires a signalling server that connects a client to the first peer. But after network provider.
that the network manages itself. It is well suited for large and small networks.
</dd> </dd>
<dt><a href="http://github.com/yjs/y-dat">y-dat</a></dt> <dt><a href="http://github.com/yjs/y-dat">y-dat</a></dt>
<dd> <dd>
@@ -97,7 +106,7 @@ hypercores and y-dat listens to changes and applies them to the Yjs document.
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-97 y-websocket@1.0.0-6 npm i yjs y-websocket
``` ```
Start the y-websocket server: Start the y-websocket server:
@@ -235,7 +244,8 @@ or any of its children.
Copies the <code>[key,value]</code> pairs of this YMap to a new Object.It 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. transforms all child types to JSON using their <code>toJSON</code> method.
</dd> </dd>
<b><code>forEach(function(key:string,value:object|boolean|Array|string|number|Uint8Array|Y.Type))</code></b> <b><code>forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type,
key:string, map: Y.Map))</code></b>
<dd> <dd>
Execute the provided function once for every key-value pair. Execute the provided function once for every key-value pair.
</dd> </dd>
@@ -343,7 +353,7 @@ or any of its children.
</details> </details>
<details> <details>
<summary><b>YXmlFragment</b></summary> <summary><b>Y.XmlFragment</b></summary>
<br> <br>
<p> <p>
A container that holds an Array of Y.XmlElements. A container that holds an Array of Y.XmlElements.
@@ -670,7 +680,7 @@ undo- or the redo-stack.
<code> <code>
on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo' on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
| 'redo' }) | 'redo' })
</code> </code>
</b> </b>
<dd> <dd>
Register an event that is called when a <code>StackItem</code> is popped from Register an event that is called when a <code>StackItem</code> is popped from

View File

@@ -1,305 +0,0 @@
# ![Yjs](https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png)
Yjs is a framework for offline-first p2p shared editing on structured data like
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
most of the complexity of concurrent editing. For additional information, demos,
and tutorials visit [y-js.org](http://y-js.org/).
:warning: Checkout the [v13 docs](./README.md) for the upcoming release :warning:
### Extensions
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
* *Connector* - a communication protocol that propagates changes to the clients
* *Database* - a database to store your changes
* one or more *Types* - that represent the shared data
Connectors, Databases, and Types are available as modules that extend Yjs. Here
is a list of the modules we know of:
##### Connectors
|Name | Description |
|----------------|-----------------------------------|
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!|
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
##### Database adapters
|Name | Description |
|----------------|-----------------------------------|
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps |
##### Types
| Name | Description |
|----------|-------------------|
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to the [Ace Editor](https://ace.c9.io), [CodeMirror](https://codemirror.net/), [Monaco](https://github.com/Microsoft/monaco-editor), textareas, input elements, and HTML elements (e.g. <*h1*>, or <*p*>) |
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
##### Other
| Name | Description |
|-----------|-------------------|
|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element |
## Use it!
Install Yjs, and its modules with [bower](http://bower.io/), or
[npm](https://www.npmjs.org/package/yjs).
### Bower
```
bower install --save yjs y-array % add all y-* modules you want to use
```
You only need to include the `y.js` file. Yjs is able to automatically require
missing modules.
```
<script src="./bower_components/yjs/y.js"></script>
```
### CDN
```
<script src="https://cdn.jsdelivr.net/npm/yjs@12/dist/y.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
// ..
// do the same for all modules you want to use
```
### Npm
```
npm install --save yjs % add all y-* modules you want to use
```
If you don't include via script tag, you have to explicitly include all modules!
(Same goes for other module systems)
```
var Y = require('yjs')
require('y-array')(Y) // add the y-array type to Yjs
require('y-websockets-client')(Y)
require('y-memory')(Y)
require('y-map')(Y)
require('y-text')(Y)
// ..
// do the same for all modules you want to use
```
### ES6 Syntax
```
import Y from 'yjs'
import yArray from 'y-array'
import yWebsocketsClient from 'y-webrtc'
import yMemory from 'y-memory'
import yMap from 'y-map'
import yText from 'y-text'
// ..
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
```
# Text editing example
Install dependencies
```
bower i yjs y-memory y-webrtc y-array y-text
```
Here is a simple example of a shared textarea
```HTML
<!DOCTYPE html>
<html>
<body>
<script src="./bower_components/yjs/y.js"></script>
<!-- Yjs automatically includes all missing dependencies (browser only) -->
<script>
Y({
db: {
name: 'memory' // use memory database adapter.
// name: 'indexeddb' // use indexeddb database adapter instead for offline apps
},
connector: {
name: 'webrtc', // use webrtc connector
// name: 'websockets-client'
// name: 'xmpp'
room: 'my-room' // clients connecting to the same room share data
},
sourceDir: './bower_components', // location of the y-* modules (browser only)
share: {
textarea: 'Text' // y.share.textarea is of type y-text
}
}).then(function (y) {
// The Yjs instance `y` is available
// y.share.* contains the shared types
// Bind `y.share.textarea` to `<textarea/>`
y.share.textarea.bind(document.querySelector('textarea'))
})
</script>
<textarea></textarea>
</body>
</html>
```
## Get Help & Give Help
There are some friendly people on [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join!
Report _any_ issues to the
[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very
soon, if possible.
# API
### Y(options)
* Y.extend(module1, module2, ..)
* Add extensions to Y
* `Y.extend(require('y-webrtc'))` has the same semantics as
`require('y-webrtc')(Y)`
* options.db
* Will be forwarded to the database adapter. Specify the database adaper on
`options.db.name`.
* Have a look at the used database adapter repository to see all available
options.
* options.connector
* Will be forwarded to the connector adapter. Specify the connector adaper on
`options.connector.name`.
* All our connectors implement a `room` property. Clients that specify the
same room share the same data.
* All of our connectors specify an `url` property that defines the connection
endpoint of the used connector.
* All of our connectors also have a default connection endpoint that you can
use for development.
* Set `options.connector.generateUserId = true` in order to genenerate a
userid, instead of receiving one from the server. This way the `Y(..)` is
immediately going to be resolved, without waiting for any confirmation from
the server. Use with caution.
* Have a look at the used connector repository to see all available options.
* *Only if you know what you are doing:* Set
`options.connector.preferUntransformed = true` in order receive the shared
data untransformed. This is very efficient as the database content is simply
copied to this client. This does only work if this client receives content
from only one client.
* options.sourceDir (browser only)
* Path where all y-* modules are stored
* Defaults to `/bower_components`
* Not required when running on `nodejs` / `iojs`
* When using nodejs you need to manually extend Yjs:
```
var Y = require('yjs')
// you have to require a db, connector, and *all* types you use!
require('y-memory')(Y)
require('y-webrtc')(Y)
require('y-map')(Y)
// ..
```
* options.share
* Specify on `options.share[arbitraryName]` types that are shared among all
users.
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and
create an y-array type on `y.share[arbitraryName]`.
* If userA doesn't specify `options.share[arbitraryName]`, it won't be
available for userA.
* If userB specifies `options.share[arbitraryName]`, it still won't be
available for userA. But all the updates are send from userB to userA.
* In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted.
Instead, they are merged among all users. This feature is only available on
`y.share.*`
* Weird behavior: It is supported that two users specify different types with
the same property name.
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies
`options.share.x = 'Text'`. But they only share data if they specified the
same type with the same property name
* options.type (browser only)
* Array of modules that Yjs needs to require, before instantiating a shared
type.
* By default Yjs requires the specified database adapter, the specified
connector, and all modules that are used in `options.share.*`
* Put all types here that you intend to use, but are not used in y.share.*
### Instantiated Y object (y)
`Y(options)` returns a promise that is fulfilled when..
* All modules are loaded
* The specified database adapter is loaded
* The specified connector is loaded
* All types are included
* The connector is initialized, and a unique user id is set (received from the
server)
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
The promise returns an instance of Y. We denote it with a lower case `y`.
* y.share.*
* Instances of the types you specified on options.share.*
* y.share.* can only be defined once when you instantiate Y!
* y.connector is an instance of Y.AbstractConnector
* y.connector.onUserEvent(function (event) {..})
* Observe user events (event.action is either 'userLeft' or 'userJoined')
* y.connector.whenSynced(listener)
* `listener` is executed when y synced with at least one user.
* `listener` is not called when no other user is in the same room.
* y-websockets-client aways waits to sync with the server
* y.connector.disconnect()
* Force to disconnect this instance from the other instances
* y.connector.connect()
* Try to reconnect to the other instances (needs to be supported by the
connector)
* Not supported by y-xmpp
* y.close()
* Destroy this object.
* Destroys all types (they will throw weird errors if you still use them)
* Disconnects from the other instances (via connector)
* Returns a promise
* y.destroy()
* calls y.close()
* Removes all data from the database
* Returns a promise
* y.db.stopGarbageCollector()
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
collection
* y.db.gc :: Boolean
* Whether gc is turned on
* y.db.gcTimeout :: Number (defaults to 50000 ms)
* Time interval between two garbage collect cycles
* It is required that all instances exchanged all messages after two garbage
collect cycles (after 100000 ms per default)
* y.db.userId :: String
* The used user id for this client. **Never overwrite this**
### Logging
Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag
`y*` enables logging for all y-* components. You can selectively remove
components you are not interested in: E.g. The flag `y*,-y:connector-message`
will not log the long `y:connector-message` messages.
##### Enable logging in Node.js
```sh
DEBUG=y* node app.js
```
Remove the colors in order to log to a file:
```sh
DEBUG_COLORS=0 DEBUG=y* node app.js > log
```
##### Enable logging in the browser
```js
localStorage.debug = 'y*'
```
## License
Yjs is licensed under the [MIT License](./LICENSE).

3647
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,23 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-97", "version": "13.0.2",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.js", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./src/index.js",
"types": "./dist/src/index.d.ts",
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production", "test": "npm run dist && node ./dist/tests.cjs --repitition-time 50",
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000", "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000",
"dist": "rm -rf dist && rollup -c", "dist": "rm -rf dist && rollup -c && tsc",
"watch": "rollup -wc", "watch": "rollup -wc",
"lint": "markdownlint README.md && standard && tsc", "lint": "markdownlint README.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
"serve-docs": "npm run docs && serve ./docs/", "serve-docs": "npm run docs && http-server ./docs/",
"preversion": "npm run lint && 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.cjs --repitition-time 1000",
"postversion": "git push && git push --tags", "debug": "concurrently 'http-server -o 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.cjs",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js", "trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs"
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.js"
}, },
"files": [ "files": [
"dist/*", "dist/*",
@@ -41,7 +41,12 @@
"url": "https://github.com/yjs/yjs.git" "url": "https://github.com/yjs/yjs.git"
}, },
"keywords": [ "keywords": [
"crdt" "Yjs",
"CRDT",
"offline",
"shared editing",
"concurrency",
"collaboration"
], ],
"author": "Kevin Jahns", "author": "Kevin Jahns",
"email": "kevin.jahns@protonmail.com", "email": "kevin.jahns@protonmail.com",
@@ -51,18 +56,20 @@
}, },
"homepage": "https://yjs.dev", "homepage": "https://yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.1.0" "lib0": "^0.2.12"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^11.0.1",
"@rollup/plugin-node-resolve": "^7.0.0",
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"http-server": "^0.12.1",
"jsdoc": "^3.6.3", "jsdoc": "^3.6.3",
"live-server": "^1.2.1", "markdownlint-cli": "^0.19.0",
"rollup": "^1.20.3", "rollup": "^1.30.0",
"rollup-cli": "^1.0.9", "rollup-cli": "^1.0.9",
"rollup-plugin-node-resolve": "^4.2.4", "standard": "^14.0.0",
"standard": "^11.0.1",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^3.6.2", "typescript": "^3.7.5",
"y-protocols": "0.0.6" "y-protocols": "^0.2.0"
} }
} }

View File

@@ -1,4 +1,5 @@
import nodeResolve from 'rollup-plugin-node-resolve' import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
const localImports = process.env.LOCALIMPORTS const localImports = process.env.LOCALIMPORTS
@@ -37,23 +38,18 @@ const debugResolve = {
export default [{ export default [{
input: './src/index.js', input: './src/index.js',
output: [{ output: {
name: 'Y', name: 'Y',
file: 'dist/yjs.js', file: 'dist/yjs.cjs',
format: 'cjs', format: 'cjs',
sourcemap: true, sourcemap: true,
paths: path => { paths: path => {
if (/^lib0\//.test(path)) { if (/^lib0\//.test(path)) {
return `lib0/dist/${path.slice(5)}` return `lib0/dist/${path.slice(5, -3)}.cjs`
} }
return path return path
} }
}, { },
name: 'Y',
file: 'dist/yjs.mjs',
format: 'es',
sourcemap: true
}],
external: id => /^lib0\//.test(id) external: id => /^lib0\//.test(id)
}, { }, {
input: './tests/index.js', input: './tests/index.js',
@@ -68,6 +64,24 @@ export default [{
nodeResolve({ nodeResolve({
sourcemap: true, sourcemap: true,
mainFields: ['module', 'browser', 'main'] mainFields: ['module', 'browser', 'main']
}) }),
commonjs()
] ]
}, {
input: './tests/index.js',
output: {
name: 'test',
file: 'dist/tests.cjs',
format: 'cjs',
sourcemap: true
},
plugins: [
debugResolve,
nodeResolve({
sourcemap: true,
mainFields: ['module', 'main']
}),
commonjs()
],
external: ['isomorphic.js']
}] }]

View File

@@ -38,6 +38,8 @@ export {
getState, getState,
Snapshot, Snapshot,
createSnapshot, createSnapshot,
createDeleteSet,
createDeleteSetFromStructStore,
snapshot, snapshot,
emptySnapshot, emptySnapshot,
findRootTypeKey, findRootTypeKey,
@@ -51,5 +53,8 @@ export {
decodeSnapshot, decodeSnapshot,
encodeSnapshot, encodeSnapshot,
isDeleted, isDeleted,
equalSnapshots isParentOf,
equalSnapshots,
PermanentUserData, // @TODO experimental
tryGc
} from './internals.js' } from './internals.js'

View File

@@ -1,13 +1,16 @@
export * from './utils/DeleteSet.js' export * from './utils/DeleteSet.js'
export * from './utils/Doc.js'
export * from './utils/encoding.js'
export * from './utils/EventHandler.js' export * from './utils/EventHandler.js'
export * from './utils/ID.js' export * from './utils/ID.js'
export * from './utils/isParentOf.js' export * from './utils/isParentOf.js'
export * from './utils/PermanentUserData.js'
export * from './utils/RelativePosition.js' export * from './utils/RelativePosition.js'
export * from './utils/Snapshot.js' export * from './utils/Snapshot.js'
export * from './utils/StructStore.js' export * from './utils/StructStore.js'
export * from './utils/Transaction.js' export * from './utils/Transaction.js'
export * from './utils/UndoManager.js' export * from './utils/UndoManager.js'
export * from './utils/Doc.js'
export * from './utils/YEvent.js' export * from './utils/YEvent.js'
export * from './types/AbstractType.js' export * from './types/AbstractType.js'
@@ -31,5 +34,3 @@ export * from './structs/ContentAny.js'
export * from './structs/ContentString.js' export * from './structs/ContentString.js'
export * from './structs/ContentType.js' export * from './structs/ContentType.js'
export * from './structs/Item.js' export * from './structs/Item.js'
export * from './utils/encoding.js'

View File

@@ -24,6 +24,7 @@ export class AbstractStruct {
this.length = length this.length = length
this.deleted = false this.deleted = false
} }
/** /**
* Merge this struct with the item to the right. * Merge this struct with the item to the right.
* This method is already assuming that `this.id.clock + this.length === this.id.clock`. * This method is already assuming that `this.id.clock + this.length === this.id.clock`.
@@ -34,6 +35,7 @@ export class AbstractStruct {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @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
@@ -43,6 +45,7 @@ export class AbstractStruct {
write (encoder, offset, encodingRef) { write (encoder, offset, encodingRef) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
@@ -69,6 +72,7 @@ export class AbstractStructRef {
*/ */
this.id = id this.id = id
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {Array<ID|null>} * @return {Array<ID|null>}
@@ -76,6 +80,7 @@ export class AbstractStructRef {
getMissing (transaction) { getMissing (transaction) {
return this._missing return this._missing
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store

View File

@@ -18,30 +18,35 @@ export class ContentAny {
*/ */
this.arr = arr this.arr = arr
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return this.arr.length return this.arr.length
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return this.arr return this.arr
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentAny} * @return {ContentAny}
*/ */
copy () { copy () {
return new ContentAny(this.arr) return new ContentAny(this.arr)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentAny} * @return {ContentAny}
@@ -51,6 +56,7 @@ export class ContentAny {
this.arr = this.arr.slice(0, offset) this.arr = this.arr.slice(0, offset)
return right return right
} }
/** /**
* @param {ContentAny} right * @param {ContentAny} right
* @return {boolean} * @return {boolean}
@@ -59,6 +65,7 @@ export class ContentAny {
this.arr = this.arr.concat(right.arr) this.arr = this.arr.concat(right.arr)
return true return true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -84,6 +91,7 @@ export class ContentAny {
encoding.writeAny(encoder, c) encoding.writeAny(encoder, c)
} }
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -17,30 +17,35 @@ export class ContentBinary {
constructor (content) { constructor (content) {
this.content = content this.content = content
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return 1 return 1
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [this.content] return [this.content]
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentBinary} * @return {ContentBinary}
*/ */
copy () { copy () {
return new ContentBinary(this.content) return new ContentBinary(this.content)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentBinary} * @return {ContentBinary}
@@ -48,6 +53,7 @@ export class ContentBinary {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {ContentBinary} right * @param {ContentBinary} right
* @return {boolean} * @return {boolean}
@@ -55,6 +61,7 @@ export class ContentBinary {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -75,6 +82,7 @@ export class ContentBinary {
write (encoder, offset) { write (encoder, offset) {
encoding.writeVarUint8Array(encoder, this.content) encoding.writeVarUint8Array(encoder, this.content)
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -17,30 +17,35 @@ export class ContentDeleted {
constructor (len) { constructor (len) {
this.len = len this.len = len
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return this.len return this.len
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [] return []
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return false return false
} }
/** /**
* @return {ContentDeleted} * @return {ContentDeleted}
*/ */
copy () { copy () {
return new ContentDeleted(this.len) return new ContentDeleted(this.len)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentDeleted} * @return {ContentDeleted}
@@ -50,6 +55,7 @@ export class ContentDeleted {
this.len = offset this.len = offset
return right return right
} }
/** /**
* @param {ContentDeleted} right * @param {ContentDeleted} right
* @return {boolean} * @return {boolean}
@@ -58,6 +64,7 @@ export class ContentDeleted {
this.len += right.len this.len += right.len
return true return true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -66,6 +73,7 @@ export class ContentDeleted {
addToDeleteSet(transaction.deleteSet, item.id, this.len) addToDeleteSet(transaction.deleteSet, item.id, this.len)
item.deleted = true item.deleted = true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
@@ -81,6 +89,7 @@ export class ContentDeleted {
write (encoder, offset) { write (encoder, offset) {
encoding.writeVarUint(encoder, this.len - offset) encoding.writeVarUint(encoder, this.len - offset)
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -17,30 +17,35 @@ export class ContentEmbed {
constructor (embed) { constructor (embed) {
this.embed = embed this.embed = embed
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return 1 return 1
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [this.embed] return [this.embed]
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentEmbed} * @return {ContentEmbed}
*/ */
copy () { copy () {
return new ContentEmbed(this.embed) return new ContentEmbed(this.embed)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentEmbed} * @return {ContentEmbed}
@@ -48,6 +53,7 @@ export class ContentEmbed {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {ContentEmbed} right * @param {ContentEmbed} right
* @return {boolean} * @return {boolean}
@@ -55,6 +61,7 @@ export class ContentEmbed {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -75,6 +82,7 @@ export class ContentEmbed {
write (encoder, offset) { write (encoder, offset) {
encoding.writeVarString(encoder, JSON.stringify(this.embed)) encoding.writeVarString(encoder, JSON.stringify(this.embed))
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -19,30 +19,35 @@ export class ContentFormat {
this.key = key this.key = key
this.value = value this.value = value
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return 1 return 1
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [] return []
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return false return false
} }
/** /**
* @return {ContentFormat} * @return {ContentFormat}
*/ */
copy () { copy () {
return new ContentFormat(this.key, this.value) return new ContentFormat(this.key, this.value)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentFormat} * @return {ContentFormat}
@@ -50,6 +55,7 @@ export class ContentFormat {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {ContentFormat} right * @param {ContentFormat} right
* @return {boolean} * @return {boolean}
@@ -57,6 +63,7 @@ export class ContentFormat {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -78,6 +85,7 @@ export class ContentFormat {
encoding.writeVarString(encoder, this.key) encoding.writeVarString(encoder, this.key)
encoding.writeVarString(encoder, JSON.stringify(this.value)) encoding.writeVarString(encoder, JSON.stringify(this.value))
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -18,30 +18,35 @@ export class ContentJSON {
*/ */
this.arr = arr this.arr = arr
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return this.arr.length return this.arr.length
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return this.arr return this.arr
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentJSON} * @return {ContentJSON}
*/ */
copy () { copy () {
return new ContentJSON(this.arr) return new ContentJSON(this.arr)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentJSON} * @return {ContentJSON}
@@ -51,6 +56,7 @@ export class ContentJSON {
this.arr = this.arr.slice(0, offset) this.arr = this.arr.slice(0, offset)
return right return right
} }
/** /**
* @param {ContentJSON} right * @param {ContentJSON} right
* @return {boolean} * @return {boolean}
@@ -59,6 +65,7 @@ export class ContentJSON {
this.arr = this.arr.concat(right.arr) this.arr = this.arr.concat(right.arr)
return true return true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -84,6 +91,7 @@ export class ContentJSON {
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c)) encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
} }
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -18,30 +18,35 @@ export class ContentString {
*/ */
this.str = str this.str = str
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return this.str.length return this.str.length
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return this.str.split('') return this.str.split('')
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentString} * @return {ContentString}
*/ */
copy () { copy () {
return new ContentString(this.str) return new ContentString(this.str)
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentString} * @return {ContentString}
@@ -51,6 +56,7 @@ export class ContentString {
this.str = this.str.slice(0, offset) this.str = this.str.slice(0, offset)
return right return right
} }
/** /**
* @param {ContentString} right * @param {ContentString} right
* @return {boolean} * @return {boolean}
@@ -59,6 +65,7 @@ export class ContentString {
this.str += right.str this.str += right.str
return true return true
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -79,6 +86,7 @@ export class ContentString {
write (encoder, offset) { write (encoder, offset) {
encoding.writeVarString(encoder, offset === 0 ? this.str : this.str.slice(offset)) encoding.writeVarString(encoder, offset === 0 ? this.str : this.str.slice(offset))
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -49,30 +49,35 @@ export class ContentType {
*/ */
this.type = type this.type = type
} }
/** /**
* @return {number} * @return {number}
*/ */
getLength () { getLength () {
return 1 return 1
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
return [this.type] return [this.type]
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isCountable () { isCountable () {
return true return true
} }
/** /**
* @return {ContentType} * @return {ContentType}
*/ */
copy () { copy () {
return new ContentType(this.type._copy()) return new ContentType(this.type._copy())
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {ContentType} * @return {ContentType}
@@ -80,6 +85,7 @@ export class ContentType {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {ContentType} right * @param {ContentType} right
* @return {boolean} * @return {boolean}
@@ -87,6 +93,7 @@ export class ContentType {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -94,6 +101,7 @@ export class ContentType {
integrate (transaction, item) { integrate (transaction, item) {
this.type._integrate(transaction.doc, item) this.type._integrate(transaction.doc, item)
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
@@ -121,6 +129,7 @@ export class ContentType {
}) })
transaction.changed.delete(this.type) transaction.changed.delete(this.type)
} }
/** /**
* @param {StructStore} store * @param {StructStore} store
*/ */
@@ -139,6 +148,7 @@ export class ContentType {
}) })
this.type._map = new Map() this.type._map = new Map()
} }
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {number} offset * @param {number} offset
@@ -146,6 +156,7 @@ export class ContentType {
write (encoder, offset) { write (encoder, offset) {
this.type._write(encoder) this.type._write(encoder)
} }
/** /**
* @return {number} * @return {number}
*/ */

View File

@@ -69,6 +69,7 @@ export class GCRef extends AbstractStructRef {
*/ */
this.length = decoding.readVarUint(decoder) this.length = decoding.readVarUint(decoder)
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store

View File

@@ -35,6 +35,8 @@ import * as set from 'lib0/set.js'
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary.js'
/** /**
* @todo This should return several items
*
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {{item:Item, diff:number}} * @return {{item:Item, diff:number}}
@@ -53,7 +55,7 @@ export const followRedone = (store, id) => {
item = getItem(store, nextID) item = getItem(store, nextID)
diff = nextID.clock - item.id.clock diff = nextID.clock - item.id.clock
nextID = item.redone nextID = item.redone
} while (nextID !== null) } while (nextID !== null && item instanceof Item)
return { return {
item, diff item, diff
} }
@@ -66,10 +68,11 @@ export const followRedone = (store, id) => {
* sending it to other peers * sending it to other peers
* *
* @param {Item|null} item * @param {Item|null} item
* @param {boolean} keep
*/ */
export const keepItem = item => { export const keepItem = (item, keep) => {
while (item !== null && !item.keep) { while (item !== null && item.keep !== keep) {
item.keep = true item.keep = keep
item = item.parent._item item = item.parent._item
} }
} }
@@ -135,7 +138,7 @@ export const splitItem = (transaction, leftItem, diff) => {
*/ */
export const redoItem = (transaction, item, redoitems) => { export const redoItem = (transaction, item, redoitems) => {
if (item.redone !== null) { if (item.redone !== null) {
return getItemCleanStart(transaction, transaction.doc.store, item.redone) return getItemCleanStart(transaction, item.redone)
} }
let parentItem = item.parent._item let parentItem = item.parent._item
/** /**
@@ -175,7 +178,7 @@ export const redoItem = (transaction, item, redoitems) => {
} }
if (parentItem !== null && parentItem.redone !== null) { if (parentItem !== null && parentItem.redone !== null) {
while (parentItem.redone !== null) { while (parentItem.redone !== null) {
parentItem = getItemCleanStart(transaction, transaction.doc.store, parentItem.redone) parentItem = getItemCleanStart(transaction, parentItem.redone)
} }
// find next cloned_redo items // find next cloned_redo items
while (left !== null) { while (left !== null) {
@@ -185,7 +188,7 @@ export const redoItem = (transaction, item, redoitems) => {
let leftTrace = left let leftTrace = left
// trace redone until parent matches // trace redone until parent matches
while (leftTrace !== null && leftTrace.parent._item !== parentItem) { while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, transaction.doc.store, leftTrace.redone) leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
} }
if (leftTrace !== null && leftTrace.parent._item === parentItem) { if (leftTrace !== null && leftTrace.parent._item === parentItem) {
left = leftTrace left = leftTrace
@@ -200,7 +203,7 @@ export const redoItem = (transaction, item, redoitems) => {
let rightTrace = right let rightTrace = right
// trace redone until parent matches // trace redone until parent matches
while (rightTrace !== null && rightTrace.parent._item !== parentItem) { while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, transaction.doc.store, rightTrace.redone) rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
} }
if (rightTrace !== null && rightTrace.parent._item === parentItem) { if (rightTrace !== null && rightTrace.parent._item === parentItem) {
right = rightTrace right = rightTrace
@@ -218,7 +221,7 @@ export const redoItem = (transaction, item, redoitems) => {
item.content.copy() item.content.copy()
) )
item.redone = redoneItem.id item.redone = redoneItem.id
keepItem(redoneItem) keepItem(redoneItem, true)
redoneItem.integrate(transaction) redoneItem.integrate(transaction)
return redoneItem return redoneItem
} }
@@ -428,6 +431,7 @@ export class Item 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)
} }
/** /**
* Try to merge two items * Try to merge two items
* *
@@ -576,12 +580,14 @@ export class AbstractContent {
getLength () { getLength () {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @return {Array<any>} * @return {Array<any>}
*/ */
getContent () { getContent () {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* Should return false if this Item is some kind of meta information * Should return false if this Item is some kind of meta information
* (e.g. format information). * (e.g. format information).
@@ -594,12 +600,14 @@ export class AbstractContent {
isCountable () { isCountable () {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @return {AbstractContent} * @return {AbstractContent}
*/ */
copy () { copy () {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {number} offset * @param {number} offset
* @return {AbstractContent} * @return {AbstractContent}
@@ -607,6 +615,7 @@ export class AbstractContent {
splice (offset) { splice (offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {AbstractContent} right * @param {AbstractContent} right
* @return {boolean} * @return {boolean}
@@ -614,6 +623,7 @@ export class AbstractContent {
mergeWith (right) { mergeWith (right) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} item * @param {Item} item
@@ -621,18 +631,21 @@ export class AbstractContent {
integrate (transaction, item) { integrate (transaction, item) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
delete (transaction) { delete (transaction) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {StructStore} store * @param {StructStore} store
*/ */
gc (store) { gc (store) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {number} offset * @param {number} offset
@@ -640,6 +653,7 @@ export class AbstractContent {
write (encoder, offset) { write (encoder, offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**
* @return {number} * @return {number}
*/ */
@@ -707,6 +721,7 @@ export class ItemRef extends AbstractStructRef {
this.content = readItemContent(decoder, info) this.content = readItemContent(decoder, info)
this.length = this.content.getLength() this.length = this.content.getLength()
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store
@@ -726,7 +741,7 @@ export class ItemRef extends AbstractStructRef {
} }
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left) const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
const right = this.right === null ? null : getItemCleanStart(transaction, store, this.right) const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
let parent = null let parent = null
let parentSub = this.parentSub let parentSub = this.parentSub
if (this.parent !== null) { if (this.parent !== null) {

View File

@@ -30,7 +30,7 @@ import * as encoding from 'lib0/encoding.js' // eslint-disable-line
* @param {EventType} event * @param {EventType} event
*/ */
export const callTypeObservers = (type, transaction, event) => { export const callTypeObservers = (type, transaction, event) => {
callEventHandlerListeners(type._eH, event, transaction) const changedType = type
const changedParentTypes = transaction.changedParentTypes const changedParentTypes = transaction.changedParentTypes
while (true) { while (true) {
// @ts-ignore // @ts-ignore
@@ -40,6 +40,7 @@ export const callTypeObservers = (type, transaction, event) => {
} }
type = type._item.parent type = type._item.parent
} }
callEventHandlerListeners(changedType._eH, event, transaction)
} }
/** /**
@@ -170,7 +171,7 @@ export class AbstractType {
/** /**
* @abstract * @abstract
* @return {Object | Array | number | string} * @return {any}
*/ */
toJSON () {} toJSON () {}
} }
@@ -369,7 +370,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
let left = referenceItem let left = referenceItem
const right = referenceItem === null ? parent._start : referenceItem.right const right = referenceItem === null ? parent._start : referenceItem.right
/** /**
* @type {Array<Object|Array|number>} * @type {Array<Object|Array<any>|number>}
*/ */
let jsonContent = [] let jsonContent = []
const packJsonContent = () => { const packJsonContent = () => {
@@ -428,7 +429,7 @@ export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index <= n.length) { if (index <= n.length) {
if (index < n.length) { if (index < n.length) {
// insert in-between // insert in-between
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
} }
break break
} }
@@ -454,7 +455,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
for (; n !== null && index > 0; n = n.right) { for (; n !== null && index > 0; n = n.right) {
if (!n.deleted && n.countable) { if (!n.deleted && n.countable) {
if (index < n.length) { if (index < n.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
} }
index -= n.length index -= n.length
} }
@@ -463,7 +464,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
while (length > 0 && n !== null) { while (length > 0 && n !== null) {
if (!n.deleted) { if (!n.deleted) {
if (length < n.length) { if (length < n.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + length)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
} }
n.delete(transaction) n.delete(transaction)
length -= n.length length -= n.length
@@ -514,7 +515,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
content = new ContentAny([value]) content = new ContentAny([value])
break break
case Uint8Array: case Uint8Array:
content = new ContentBinary(value) content = new ContentBinary(/** @type {Uint8Array} */ (value))
break break
default: default:
if (value instanceof AbstractType) { if (value instanceof AbstractType) {
@@ -551,7 +552,7 @@ export const typeMapGetAll = (parent) => {
/** /**
* @type {Object<string,any>} * @type {Object<string,any>}
*/ */
let res = {} const res = {}
for (const [key, value] of parent._map) { for (const [key, value] of parent._map) {
if (!value.deleted) { if (!value.deleted) {
res[key] = value.content.getContent()[value.length - 1] res[key] = value.content.getContent()[value.length - 1]

View File

@@ -51,6 +51,7 @@ export class YArray extends AbstractType {
*/ */
this._prelimContent = [] this._prelimContent = []
} }
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *
@@ -65,7 +66,7 @@ export class YArray extends AbstractType {
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
this.insert(0, /** @type {Array} */ (this._prelimContent)) this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null this._prelimContent = null
} }
@@ -76,6 +77,7 @@ export class YArray extends AbstractType {
get length () { get length () {
return this._prelimContent === null ? this._length : this._prelimContent.length return this._prelimContent === null ? this._length : this._prelimContent.length
} }
/** /**
* Creates YArrayEvent and calls observers. * Creates YArrayEvent and calls observers.
* *
@@ -110,7 +112,7 @@ export class YArray extends AbstractType {
typeListInsertGenerics(transaction, this, index, content) typeListInsertGenerics(transaction, this, index, content)
}) })
} else { } else {
/** @type {Array} */ (this._prelimContent).splice(index, 0, ...content) /** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
} }
} }
@@ -135,7 +137,7 @@ export class YArray extends AbstractType {
typeListDelete(transaction, this, index, length) typeListDelete(transaction, this, index, length)
}) })
} else { } else {
/** @type {Array} */ (this._prelimContent).splice(index, length) /** @type {Array<any>} */ (this._prelimContent).splice(index, length)
} }
} }

View File

@@ -53,6 +53,7 @@ export class YMap extends AbstractType {
*/ */
this._prelimContent = new Map() this._prelimContent = new Map()
} }
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *
@@ -67,7 +68,7 @@ export class YMap extends AbstractType {
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
for (let [key, value] of /** @type {Map<string, any>} */ (this._prelimContent)) { for (const [key, value] of /** @type {Map<string, any>} */ (this._prelimContent)) {
this.set(key, value) this.set(key, value)
} }
this._prelimContent = null this._prelimContent = null
@@ -99,7 +100,7 @@ export class YMap extends AbstractType {
* @type {Object<string,T>} * @type {Object<string,T>}
*/ */
const map = {} const map = {}
for (let [key, item] of this._map) { for (const [key, item] of this._map) {
if (!item.deleted) { if (!item.deleted) {
const v = item.content.getContent()[item.length - 1] const v = item.content.getContent()[item.length - 1]
map[key] = v instanceof AbstractType ? v.toJSON() : v map[key] = v instanceof AbstractType ? v.toJSON() : v
@@ -145,7 +146,7 @@ export class YMap extends AbstractType {
* @type {Object<string,T>} * @type {Object<string,T>}
*/ */
const map = {} const map = {}
for (let [key, item] of this._map) { for (const [key, item] of this._map) {
if (!item.deleted) { if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this) f(item.content.getContent()[item.length - 1], key, this)
} }

View File

@@ -17,7 +17,7 @@ import {
ContentFormat, ContentFormat,
ContentString, ContentString,
splitSnapshotAffectedStructs, splitSnapshotAffectedStructs,
Doc, Item, Snapshot, StructStore, Transaction // eslint-disable-line ID, Doc, Item, Snapshot, 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
@@ -68,7 +68,6 @@ export class ItemInsertionResult extends ItemListPosition {
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {Item|null} left * @param {Item|null} left
* @param {Item|null} right * @param {Item|null} right
@@ -78,7 +77,7 @@ export class ItemInsertionResult extends ItemListPosition {
* @private * @private
* @function * @function
*/ */
const findNextPosition = (transaction, store, currentAttributes, left, right, count) => { const findNextPosition = (transaction, currentAttributes, left, right, count) => {
while (right !== null && count > 0) { while (right !== null && count > 0) {
switch (right.content.constructor) { switch (right.content.constructor) {
case ContentEmbed: case ContentEmbed:
@@ -86,7 +85,7 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
if (!right.deleted) { if (!right.deleted) {
if (count < right.length) { if (count < right.length) {
// split right // split right
getItemCleanStart(transaction, store, createID(right.id.client, right.id.clock + count)) getItemCleanStart(transaction, createID(right.id.client, right.id.clock + count))
} }
count -= right.length count -= right.length
} }
@@ -105,7 +104,6 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {number} index * @param {number} index
* @return {ItemTextListPosition} * @return {ItemTextListPosition}
@@ -113,11 +111,10 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
* @private * @private
* @function * @function
*/ */
const findPosition = (transaction, store, parent, index) => { const findPosition = (transaction, parent, index) => {
let currentAttributes = new Map() const currentAttributes = new Map()
let left = null const right = parent._start
let right = parent._start return findNextPosition(transaction, currentAttributes, null, right, index)
return findNextPosition(transaction, store, currentAttributes, left, right, index)
} }
/** /**
@@ -149,7 +146,7 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
left = right left = right
right = right.right right = right.right
} }
for (let [key, val] of negatedAttributes) { for (const [key, val] of negatedAttributes) {
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val)) left = 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)
} }
@@ -216,7 +213,7 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => { const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
const negatedAttributes = new Map() const negatedAttributes = new Map()
// insert format-start items // insert format-start items
for (let key in attributes) { for (const key in attributes) {
const val = attributes[key] const val = attributes[key]
const currentVal = currentAttributes.get(key) || null const currentVal = currentAttributes.get(key) || null
if (!equalAttrs(currentVal, val)) { if (!equalAttrs(currentVal, val)) {
@@ -243,7 +240,7 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
* @function * @function
**/ **/
const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => { const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => {
for (let [key] of currentAttributes) { for (const [key] of currentAttributes) {
if (attributes[key] === undefined) { if (attributes[key] === undefined) {
attributes[key] = null attributes[key] = null
} }
@@ -283,7 +280,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
while (length > 0 && right !== null) { while (length > 0 && right !== null) {
if (!right.deleted) { if (!right.deleted) {
switch (right.content.constructor) { switch (right.content.constructor) {
case ContentFormat: case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (right.content) const { key, value } = /** @type {ContentFormat} */ (right.content)
const attr = attributes[key] const attr = attributes[key]
if (attr !== undefined) { if (attr !== undefined) {
@@ -296,10 +293,11 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
} }
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content)) updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
break break
}
case ContentEmbed: case ContentEmbed:
case ContentString: 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, createID(right.id.client, right.id.clock + length))
} }
length -= right.length length -= right.length
break break
@@ -343,7 +341,7 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
case ContentEmbed: case ContentEmbed:
case ContentString: 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, createID(right.id.client, right.id.clock + length))
} }
length -= right.length length -= right.length
right.delete(transaction) right.delete(transaction)
@@ -407,6 +405,7 @@ export class YTextEvent extends YEvent {
*/ */
this._delta = null this._delta = null
} }
/** /**
* Compute the changes in the delta format. * Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
@@ -431,7 +430,10 @@ export class YTextEvent extends YEvent {
/** /**
* @type {Object<string,any>} * @type {Object<string,any>}
*/ */
let attributes = {} // counts added or removed new attributes for retain const attributes = {} // counts added or removed new attributes for retain
/**
* @type {string|object}
*/
let insert = '' let insert = ''
let retain = 0 let retain = 0
let deleteLen = 0 let deleteLen = 0
@@ -450,7 +452,7 @@ export class YTextEvent extends YEvent {
op = { insert } op = { insert }
if (currentAttributes.size > 0) { if (currentAttributes.size > 0) {
op.attributes = {} op.attributes = {}
for (let [key, value] of currentAttributes) { for (const [key, value] of currentAttributes) {
if (value !== null) { if (value !== null) {
op.attributes[key] = value op.attributes[key] = value
} }
@@ -462,7 +464,7 @@ export class YTextEvent extends YEvent {
op = { retain } op = { retain }
if (Object.keys(attributes).length > 0) { if (Object.keys(attributes).length > 0) {
op.attributes = {} op.attributes = {}
for (let key in attributes) { for (const key in attributes) {
op.attributes[key] = attributes[key] op.attributes[key] = attributes[key]
} }
} }
@@ -520,7 +522,7 @@ export class YTextEvent extends YEvent {
retain += item.length retain += item.length
} }
break break
case ContentFormat: case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (item.content) const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) { if (this.adds(item)) {
if (!this.deletes(item)) { if (!this.deletes(item)) {
@@ -572,12 +574,13 @@ export class YTextEvent extends YEvent {
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content)) updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content))
} }
break break
}
} }
item = item.right item = item.right
} }
addOp() addOp()
while (delta.length > 0) { while (delta.length > 0) {
let lastOp = delta[delta.length - 1] const 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
delta.pop() delta.pop()
@@ -614,6 +617,11 @@ export class YText extends AbstractType {
this._pending = string !== undefined ? [() => this.insert(0, string)] : [] this._pending = string !== undefined ? [() => this.insert(0, string)] : []
} }
/**
* Number of characters of this text type.
*
* @type {number}
*/
get length () { get length () {
return this._length return this._length
} }
@@ -670,6 +678,16 @@ export class YText extends AbstractType {
return str return str
} }
/**
* Returns the unformatted string representation of this YText type.
*
* @return {string}
* @public
*/
toJSON () {
return this.toString()
}
/** /**
* Apply a {@link Delta} on this shared YText type. * Apply a {@link Delta} on this shared YText type.
* *
@@ -714,11 +732,12 @@ export class YText extends AbstractType {
* *
* @param {Snapshot} [snapshot] * @param {Snapshot} [snapshot]
* @param {Snapshot} [prevSnapshot] * @param {Snapshot} [prevSnapshot]
* @param {function('removed' | 'added', ID):any} [computeYChange]
* @return {any} The Delta representation of this type. * @return {any} The Delta representation of this type.
* *
* @public * @public
*/ */
toDelta (snapshot, prevSnapshot) { toDelta (snapshot, prevSnapshot, computeYChange) {
/** /**
* @type{Array<any>} * @type{Array<any>}
*/ */
@@ -735,7 +754,7 @@ export class YText extends AbstractType {
*/ */
const attributes = {} const attributes = {}
let addAttributes = false let addAttributes = false
for (let [key, value] of currentAttributes) { for (const [key, value] of currentAttributes) {
addAttributes = true addAttributes = true
attributes[key] = value attributes[key] = value
} }
@@ -762,17 +781,17 @@ 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.content.constructor) { switch (n.content.constructor) {
case ContentString: 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') {
packStr() packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'removed' }) currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
} }
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) { } else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') { if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
packStr() packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'added' }) currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
} }
} else if (cur !== undefined) { } else if (cur !== undefined) {
packStr() packStr()
@@ -780,6 +799,7 @@ export class YText extends AbstractType {
} }
str += /** @type {ContentString} */ (n.content).str str += /** @type {ContentString} */ (n.content).str
break break
}
case ContentEmbed: case ContentEmbed:
packStr() packStr()
ops.push({ ops.push({
@@ -818,9 +838,10 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (!attributes) { if (!attributes) {
attributes = {} attributes = {}
// @ts-ignore
currentAttributes.forEach((v, k) => { attributes[k] = v }) currentAttributes.forEach((v, k) => { attributes[k] = v })
} }
insertText(transaction, this, left, right, currentAttributes, text, attributes) insertText(transaction, this, left, right, currentAttributes, text, attributes)
@@ -847,7 +868,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
insertText(transaction, this, left, right, currentAttributes, embed, attributes) insertText(transaction, this, left, right, currentAttributes, embed, attributes)
}) })
} else { } else {
@@ -870,7 +891,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
deleteText(transaction, left, right, currentAttributes, length) deleteText(transaction, left, right, currentAttributes, length)
}) })
} else { } else {
@@ -892,7 +913,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (right === null) { if (right === null) {
return return
} }

View File

@@ -74,7 +74,7 @@ export class YXmlElement extends YXmlFragment {
const attrs = this.getAttributes() const attrs = this.getAttributes()
const stringBuilder = [] const stringBuilder = []
const keys = [] const keys = []
for (let key in attrs) { for (const key in attrs) {
keys.push(key) keys.push(key)
} }
keys.sort() keys.sort()
@@ -140,7 +140,7 @@ export class YXmlElement extends YXmlFragment {
* Returns all attribute name/value pairs in a JSON Object. * Returns all attribute name/value pairs in a JSON Object.
* *
* @param {Snapshot} [snapshot] * @param {Snapshot} [snapshot]
* @return {Object} A JSON Object that describes the attributes. * @return {Object<string, any>} A JSON Object that describes the attributes.
* *
* @public * @public
*/ */
@@ -165,8 +165,8 @@ export class YXmlElement extends YXmlFragment {
*/ */
toDOM (_document = document, hooks = {}, binding) { toDOM (_document = document, hooks = {}, binding) {
const dom = _document.createElement(this.nodeName) const dom = _document.createElement(this.nodeName)
let attrs = this.getAttributes() const attrs = this.getAttributes()
for (let key in attrs) { for (const key in attrs) {
dom.setAttribute(key, attrs[key]) dom.setAttribute(key, attrs[key])
} }
typeListForEach(this, yxml => { typeListForEach(this, yxml => {

View File

@@ -68,6 +68,7 @@ export class YXmlTreeWalker {
[Symbol.iterator] () { [Symbol.iterator] () {
return this return this
} }
/** /**
* Get the next node. * Get the next node.
* *
@@ -130,6 +131,7 @@ export class YXmlFragment extends AbstractType {
*/ */
this._prelimContent = [] this._prelimContent = []
} }
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *
@@ -143,7 +145,7 @@ export class YXmlFragment extends AbstractType {
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
this.insert(0, /** @type {Array} */ (this._prelimContent)) this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null this._prelimContent = null
} }
@@ -240,6 +242,9 @@ export class YXmlFragment extends AbstractType {
return typeListMap(this, xml => xml.toString()).join('') return typeListMap(this, xml => xml.toString()).join('')
} }
/**
* @return {string}
*/
toJSON () { toJSON () {
return this.toString() return this.toString()
} }
@@ -307,6 +312,7 @@ export class YXmlFragment extends AbstractType {
this._prelimContent.splice(index, length) this._prelimContent.splice(index, length)
} }
} }
/** /**
* Transforms this YArray to a JavaScript Array. * Transforms this YArray to a JavaScript Array.
* *
@@ -315,6 +321,7 @@ export class YXmlFragment extends AbstractType {
toArray () { toArray () {
return typeListToArray(this) return typeListToArray(this)
} }
/** /**
* Transform the properties of this type to binary and write it to an * Transform the properties of this type to binary and write it to an
* BinaryEncoder. * BinaryEncoder.

View File

@@ -12,6 +12,7 @@ export class YXmlText extends YText {
_copy () { _copy () {
return new YXmlText() return new YXmlText()
} }
/** /**
* Creates a Dom Element that mirrors this YXmlText. * Creates a Dom Element that mirrors this YXmlText.
* *
@@ -39,9 +40,9 @@ export class YXmlText extends YText {
// @ts-ignore // @ts-ignore
return this.toDelta().map(delta => { return this.toDelta().map(delta => {
const nestedNodes = [] const nestedNodes = []
for (let nodeName in delta.attributes) { for (const nodeName in delta.attributes) {
const attrs = [] const attrs = []
for (let key in delta.attributes[nodeName]) { for (const key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] }) attrs.push({ key, value: delta.attributes[nodeName][key] })
} }
// sort attributes to get a unique order // sort attributes to get a unique order
@@ -56,7 +57,7 @@ export class YXmlText extends YText {
const node = nestedNodes[i] const node = nestedNodes[i]
str += `<${node.nodeName}` str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) { for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[i] const attr = node.attrs[j]
str += ` ${attr.key}="${attr.value}"` str += ` ${attr.key}="${attr.value}"`
} }
str += '>' str += '>'
@@ -69,6 +70,9 @@ export class YXmlText extends YText {
}).join('') }).join('')
} }
/**
* @return {string}
*/
toJSON () { toJSON () {
return this.toString() return this.toString()
} }

View File

@@ -8,6 +8,7 @@ import {
Item, GC, StructStore, Transaction, ID // eslint-disable-line Item, GC, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as array from 'lib0/array.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -52,14 +53,13 @@ export class DeleteSet {
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {DeleteSet} ds * @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(GC|Item):void} f * @param {function(GC|Item):void} f
* *
* @function * @function
*/ */
export const iterateDeletedStructs = (transaction, ds, store, f) => export const iterateDeletedStructs = (transaction, ds, f) =>
ds.clients.forEach((deletes, clientid) => { ds.clients.forEach((deletes, clientid) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientid)) const structs = /** @type {Array<GC|Item>} */ (transaction.doc.store.clients.get(clientid))
for (let i = 0; i < deletes.length; i++) { for (let i = 0; i < deletes.length; i++) {
const del = deletes[i] const del = deletes[i]
iterateStructs(transaction, structs, del.clock, del.len, f) iterateStructs(transaction, structs, del.clock, del.len, f)
@@ -137,22 +137,27 @@ export const sortAndMergeDeleteSet = ds => {
} }
/** /**
* @param {DeleteSet} ds1 * @param {Array<DeleteSet>} dss
* @param {DeleteSet} ds2
* @return {DeleteSet} A fresh DeleteSet * @return {DeleteSet} A fresh DeleteSet
*/ */
export const mergeDeleteSets = (ds1, ds2) => { export const mergeDeleteSets = dss => {
const merged = new DeleteSet() const merged = new DeleteSet()
// Write all keys from ds1 to merged. If ds2 has the same key, combine the sets. for (let dssI = 0; dssI < dss.length; dssI++) {
ds1.clients.forEach((dels1, client) => dss[dssI].clients.forEach((delsLeft, client) => {
merged.clients.set(client, dels1.concat(ds2.clients.get(client) || [])) if (!merged.clients.has(client)) {
) // Write all missing keys from current ds and all following.
// Write all missing keys from ds2 to merged. // If merged already contains `client` current ds has already been added.
ds2.clients.forEach((dels2, client) => { /**
if (!merged.clients.has(client)) { * @type {Array<DeleteItem>}
merged.clients.set(client, dels2) */
} const dels = delsLeft.slice()
}) for (let i = dssI + 1; i < dss.length; i++) {
array.appendTo(dels, dss[i].clients.get(client) || [])
}
merged.clients.set(client, dels)
}
})
}
sortAndMergeDeleteSet(merged) sortAndMergeDeleteSet(merged)
return merged return merged
} }

View File

@@ -23,11 +23,14 @@ import * as map from 'lib0/map.js'
*/ */
export class Doc extends Observable { export class Doc extends Observable {
/** /**
* @param {Object|undefined} conf configuration * @param {Object} conf configuration
* @param {boolean} [conf.gc] Disable garbage collection (default: gc=true)
* @param {function(Item):boolean} [conf.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
*/ */
constructor (conf = {}) { constructor ({ gc = true, gcFilter = () => true } = {}) {
super() super()
this.gc = conf.gc || true this.gc = gc
this.gcFilter = gcFilter
this.clientID = random.uint32() this.clientID = random.uint32()
/** /**
* @type {Map<string, AbstractType<YEvent>>} * @type {Map<string, AbstractType<YEvent>>}
@@ -45,6 +48,7 @@ export class Doc extends Observable {
*/ */
this._transactionCleanups = [] this._transactionCleanups = []
} }
/** /**
* Changes that happen inside of a transaction are bundled. This means that * Changes that happen inside of a transaction are bundled. This means that
* the observer fires _after_ the transaction is finished and that all changes * the observer fires _after_ the transaction is finished and that all changes
@@ -59,6 +63,7 @@ export class Doc extends Observable {
transact (f, origin = null) { transact (f, origin = null) {
transact(this, f, origin) transact(this, f, origin)
} }
/** /**
* Define a shared data type. * Define a shared data type.
* *
@@ -117,6 +122,7 @@ export class Doc extends Observable {
} }
return type return type
} }
/** /**
* @template T * @template T
* @param {string} name * @param {string} name
@@ -128,6 +134,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
return this.get(name, YArray) return this.get(name, YArray)
} }
/** /**
* @param {string} name * @param {string} name
* @return {YText} * @return {YText}
@@ -138,6 +145,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
return this.get(name, YText) return this.get(name, YText)
} }
/** /**
* @param {string} name * @param {string} name
* @return {YMap<any>} * @return {YMap<any>}
@@ -148,6 +156,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
return this.get(name, YMap) return this.get(name, YMap)
} }
/** /**
* @param {string} name * @param {string} name
* @return {YXmlFragment} * @return {YXmlFragment}
@@ -158,6 +167,7 @@ export class Doc extends Observable {
// @ts-ignore // @ts-ignore
return this.get(name, YXmlFragment) return this.get(name, YXmlFragment)
} }
/** /**
* Emit `destroy` event and unregister all event handlers. * Emit `destroy` event and unregister all event handlers.
* *
@@ -167,6 +177,7 @@ export class Doc extends Observable {
this.emit('destroyed', [true]) this.emit('destroyed', [true])
super.destroy() super.destroy()
} }
/** /**
* @param {string} eventName * @param {string} eventName
* @param {function} f * @param {function} f
@@ -174,6 +185,7 @@ export class Doc extends Observable {
on (eventName, f) { on (eventName, f) {
super.on(eventName, f) super.on(eventName, f)
} }
/** /**
* @param {string} eventName * @param {string} eventName
* @param {function} f * @param {function} f

View File

@@ -81,7 +81,7 @@ export const readID = decoder =>
*/ */
export const findRootTypeKey = type => { export const findRootTypeKey = type => {
// @ts-ignore _y must be defined, otherwise unexpected case // @ts-ignore _y must be defined, otherwise unexpected case
for (let [key, value] of type.doc.share) { for (const [key, value] of type.doc.share) {
if (value === type) { if (value === type) {
return key return key
} }

View File

@@ -0,0 +1,142 @@
import {
YArray,
YMap,
readDeleteSet,
writeDeleteSet,
createDeleteSet,
ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line
} from '../internals.js'
import * as decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js'
import { mergeDeleteSets, isDeleted } from './DeleteSet.js'
export class PermanentUserData {
/**
* @param {Doc} doc
* @param {YMap<any>} [storeType]
*/
constructor (doc, storeType = doc.getMap('users')) {
/**
* @type {Map<string,DeleteSet>}
*/
const dss = new Map()
this.yusers = storeType
this.doc = doc
/**
* Maps from clientid to userDescription
*
* @type {Map<number,string>}
*/
this.clients = new Map()
this.dss = dss
/**
* @param {YMap<any>} user
* @param {string} userDescription
*/
const initUser = (user, userDescription) => {
/**
* @type {YArray<Uint8Array>}
*/
const ds = user.get('ds')
const ids = user.get('ids')
const addClientId = /** @param {number} clientid */ clientid => this.clients.set(clientid, userDescription)
ds.observe(/** @param {YArrayEvent<any>} event */ event => {
event.changes.added.forEach(item => {
item.content.getContent().forEach(encodedDs => {
if (encodedDs instanceof Uint8Array) {
this.dss.set(userDescription, mergeDeleteSets([this.dss.get(userDescription) || createDeleteSet(), readDeleteSet(decoding.createDecoder(encodedDs))]))
}
})
})
})
this.dss.set(userDescription, mergeDeleteSets(ds.map(encodedDs => readDeleteSet(decoding.createDecoder(encodedDs)))))
ids.observe(/** @param {YArrayEvent<any>} event */ event =>
event.changes.added.forEach(item => item.content.getContent().forEach(addClientId))
)
ids.forEach(addClientId)
}
// observe users
storeType.observe(event => {
event.keysChanged.forEach(userDescription =>
initUser(storeType.get(userDescription), userDescription)
)
})
// add intial data
storeType.forEach(initUser)
}
/**
* @param {Doc} doc
* @param {number} clientid
* @param {string} userDescription
* @param {Object} [conf]
* @param {function(Transaction, DeleteSet):boolean} [conf.filter]
*/
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
const users = this.yusers
let user = users.get(userDescription)
if (!user) {
user = new YMap()
user.set('ids', new YArray())
user.set('ds', new YArray())
users.set(userDescription, user)
}
user.get('ids').push([clientid])
users.observe(event => {
setTimeout(() => {
const userOverwrite = users.get(userDescription)
if (userOverwrite !== user) {
// user was overwritten, port all data over to the next user object
// @todo Experiment with Y.Sets here
user = userOverwrite
// @todo iterate over old type
this.clients.forEach((_userDescription, clientid) => {
if (userDescription === _userDescription) {
user.get('ids').push([clientid])
}
})
const encoder = encoding.createEncoder()
const ds = this.dss.get(userDescription)
if (ds) {
writeDeleteSet(encoder, ds)
user.get('ds').push([encoding.toUint8Array(encoder)])
}
}
}, 0)
})
doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
setTimeout(() => {
const yds = user.get('ds')
const ds = transaction.deleteSet
if (transaction.local && ds.clients.size > 0 && filter(transaction, ds)) {
const encoder = encoding.createEncoder()
writeDeleteSet(encoder, ds)
yds.push([encoding.toUint8Array(encoder)])
}
})
})
}
/**
* @param {number} clientid
* @return {any}
*/
getUserByClientId (clientid) {
return this.clients.get(clientid) || null
}
/**
* @param {ID} id
* @return {string | null}
*/
getUserByDeletedId (id) {
for (const [userDescription, ds] of this.dss) {
if (isDeleted(ds, id)) {
return userDescription
}
}
return null
}
}

View File

@@ -63,7 +63,7 @@ export class RelativePosition {
} }
/** /**
* @param {Object} json * @param {any} json
* @return {RelativePosition} * @return {RelativePosition}
* *
* @function * @function
@@ -228,7 +228,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
return null return null
} }
type = right.parent type = right.parent
if (type._item !== null && !type._item.deleted) { if (type._item === null || !type._item.deleted) {
index = right.deleted || !right.countable ? 0 : res.diff index = right.deleted || !right.countable ? 0 : res.diff
let n = right.left let n = right.left
while (n !== null) { while (n !== null) {

View File

@@ -131,10 +131,10 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
if (!meta.has(snapshot)) { if (!meta.has(snapshot)) {
snapshot.sv.forEach((clock, client) => { snapshot.sv.forEach((clock, client) => {
if (clock < getState(store, client)) { if (clock < getState(store, client)) {
getItemCleanStart(transaction, store, createID(client, clock)) getItemCleanStart(transaction, createID(client, clock))
} }
}) })
iterateDeletedStructs(transaction, snapshot.ds, store, item => {}) iterateDeletedStructs(transaction, snapshot.ds, item => {})
meta.add(snapshot) meta.add(snapshot)
} }
} }

View File

@@ -185,7 +185,7 @@ export const getItem = (store, id) => find(store, id)
*/ */
export const findIndexCleanStart = (transaction, structs, clock) => { export const findIndexCleanStart = (transaction, structs, clock) => {
const index = findIndexSS(structs, clock) const index = findIndexSS(structs, clock)
let struct = structs[index] const struct = structs[index]
if (struct.id.clock < clock && struct instanceof Item) { if (struct.id.clock < clock && struct instanceof Item) {
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)) structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
return index + 1 return index + 1
@@ -197,16 +197,15 @@ export const findIndexCleanStart = (transaction, structs, clock) => {
* 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 {ID} id * @param {ID} id
* @return {Item} * @return {Item}
* *
* @private * @private
* @function * @function
*/ */
export const getItemCleanStart = (transaction, store, id) => { export const getItemCleanStart = (transaction, id) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(id.client)) const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
return /** @type {Item} */ (structs[findIndexCleanStart(transaction, structs, id.clock)]) return structs[findIndexCleanStart(transaction, structs, id.clock)]
} }
/** /**

View File

@@ -10,13 +10,14 @@ import {
findIndexSS, findIndexSS,
callEventHandlerListeners, callEventHandlerListeners,
Item, Item,
ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // 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 map from 'lib0/map.js' import * as map from 'lib0/map.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
import * as set from 'lib0/set.js' import * as set from 'lib0/set.js'
import { callAll } from 'lib0/function.js'
/** /**
* A transaction is created for every change on the Yjs model. It is possible * A transaction is created for every change on the Yjs model. It is possible
@@ -46,8 +47,9 @@ export class Transaction {
/** /**
* @param {Doc} doc * @param {Doc} doc
* @param {any} origin * @param {any} origin
* @param {boolean} local
*/ */
constructor (doc, origin) { constructor (doc, origin, local) {
/** /**
* The Yjs instance. * The Yjs instance.
* @type {Doc} * @type {Doc}
@@ -71,7 +73,7 @@ export class Transaction {
/** /**
* All types that were directly modified (property added or child * All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set. * inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item._parentSub = null` for YArray) * Maps from type to parentSubs (`item.parentSub = null` for YArray)
* @type {Map<AbstractType<YEvent>,Set<String|null>>} * @type {Map<AbstractType<YEvent>,Set<String|null>>}
*/ */
this.changed = new Map() this.changed = new Map()
@@ -95,6 +97,11 @@ export class Transaction {
* @type {Map<any,any>} * @type {Map<any,any>}
*/ */
this.meta = new Map() this.meta = new Map()
/**
* Whether this change originates from this doc.
* @type {boolean}
*/
this.local = local
} }
} }
@@ -138,22 +145,208 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
} }
} }
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
}
}
}
}
/**
* @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(Item):boolean} gcFilter
*/
const tryGcDeleteSet = (ds, store, gcFilter) => {
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len
for (
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
si < structs.length && struct.id.clock < endDeleteItemClock;
struct = structs[++si]
) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep && gcFilter(struct)) {
struct.gc(store, false)
}
}
}
}
}
/**
* @param {DeleteSet} ds
* @param {StructStore} store
*/
const tryMergeDeleteSet = (ds, store) => {
// try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
) {
tryToMergeWithLeft(structs, si)
}
}
}
}
/**
* @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(Item):boolean} gcFilter
*/
export const tryGc = (ds, store, gcFilter) => {
tryGcDeleteSet(ds, store, gcFilter)
tryMergeDeleteSet(ds, store)
}
/**
* @param {Array<Transaction>} transactionCleanups
* @param {number} i
*/
const cleanupTransactions = (transactionCleanups, i) => {
if (i < transactionCleanups.length) {
const transaction = transactionCleanups[i]
const doc = transaction.doc
const store = doc.store
const ds = transaction.deleteSet
try {
sortAndMergeDeleteSet(ds)
transaction.afterState = getStateVector(transaction.doc.store)
doc._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
/**
* An array of event callbacks.
*
* Each callback is called even if the other ones throw errors.
*
* @type {Array<function():void>}
*/
const fs = []
// observe events on changed types
transaction.changed.forEach((subs, itemtype) =>
fs.push(() => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs)
}
})
)
fs.push(() => {
// deep observe events
transaction.changedParentTypes.forEach((events, type) =>
fs.push(() => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
}
})
)
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
})
callAll(fs, [])
} finally {
// Replace deleted items with ItemDeleted / GC.
// This is where content is actually remove from the Yjs Doc.
if (doc.gc) {
tryGcDeleteSet(ds, store, doc.gcFilter)
}
tryMergeDeleteSet(ds, store)
// on all affected store.clients props, try to merge
for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
}
}
}
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {
const updateMessage = computeUpdateMessageFromTransaction(transaction)
if (updateMessage !== null) {
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
}
}
if (transactionCleanups.length <= i + 1) {
doc._transactionCleanups = []
} else {
cleanupTransactions(transactionCleanups, i + 1)
}
}
}
}
/** /**
* Implements the functionality of `y.transact(()=>{..})` * Implements the functionality of `y.transact(()=>{..})`
* *
* @param {Doc} doc * @param {Doc} doc
* @param {function(Transaction):void} f * @param {function(Transaction):void} f
* @param {any} [origin] * @param {any} [origin=true]
* *
* @private * @private
* @function * @function
*/ */
export const transact = (doc, f, origin = null) => { export const transact = (doc, f, origin = null, local = true) => {
const transactionCleanups = doc._transactionCleanups const transactionCleanups = doc._transactionCleanups
let initialCall = false let initialCall = false
if (doc._transaction === null) { if (doc._transaction === null) {
initialCall = true initialCall = true
doc._transaction = new Transaction(doc, origin) doc._transaction = new Transaction(doc, origin, local)
transactionCleanups.push(doc._transaction) transactionCleanups.push(doc._transaction)
doc.emit('beforeTransaction', [doc._transaction, doc]) doc.emit('beforeTransaction', [doc._transaction, doc])
} }
@@ -163,134 +356,13 @@ export const transact = (doc, f, origin = null) => {
if (initialCall && transactionCleanups[0] === doc._transaction) { if (initialCall && transactionCleanups[0] === doc._transaction) {
// The first transaction ended, now process observer calls. // The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup. // Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after another // We don't want to nest these calls, so we execute these calls one after
for (let i = 0; i < transactionCleanups.length; i++) { // another.
const transaction = transactionCleanups[i] // Also we need to ensure that all cleanups are called, even if the
const store = transaction.doc.store // observes throw errors.
const ds = transaction.deleteSet // This file is full of hacky try {} finally {} blocks to ensure that an
sortAndMergeDeleteSet(ds) // event can throw errors and also that the cleanup is called.
transaction.afterState = getStateVector(transaction.doc.store) cleanupTransactions(transactionCleanups, 0)
doc._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
// emit change events on changed types
transaction.changed.forEach((subs, itemtype) => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs)
}
})
transaction.changedParentTypes.forEach((events, type) => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
}
})
doc.emit('afterTransaction', [transaction, doc])
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
}
}
}
}
// Replace deleted items with ItemDeleted / GC.
// This is where content is actually remove from the Yjs Doc.
if (doc.gc) {
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len
for (
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
si < structs.length && struct.id.clock < endDeleteItemClock;
struct = structs[++si]
) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep) {
struct.gc(store, false)
}
}
}
}
}
// try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
) {
tryToMergeWithLeft(structs, si)
}
}
}
// on all affected store.clients props, try to merge
for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
}
}
}
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {
const updateMessage = computeUpdateMessageFromTransaction(transaction)
if (updateMessage !== null) {
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
}
}
}
doc._transactionCleanups = []
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import {
createID, createID,
followRedone, followRedone,
getItemCleanStart, getItemCleanStart,
getState,
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -49,35 +50,53 @@ const popStackItem = (undoManager, stack, eventType) => {
transact(doc, transaction => { transact(doc, transaction => {
while (stack.length > 0 && result === null) { while (stack.length > 0 && result === null) {
const store = doc.store const store = doc.store
const clientID = doc.clientID
const stackItem = /** @type {StackItem} */ (stack.pop()) const stackItem = /** @type {StackItem} */ (stack.pop())
const stackStartClock = stackItem.start
const stackEndClock = stackItem.start + stackItem.len
const itemsToRedo = new Set() const itemsToRedo = new Set()
// @todo iterateStructs should not need the structs parameter
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientID))
let performedChange = false let performedChange = false
iterateDeletedStructs(transaction, stackItem.ds, store, struct => { if (stackStartClock !== stackEndClock) {
if (struct instanceof Item && scope.some(type => isParentOf(type, struct))) { // make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
getItemCleanStart(transaction, createID(clientID, stackStartClock))
if (stackEndClock < getState(doc.store, clientID)) {
getItemCleanStart(transaction, createID(clientID, stackEndClock))
}
}
iterateDeletedStructs(transaction, stackItem.ds, struct => {
if (
struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) &&
// Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval.
!(struct.id.client === clientID && struct.id.clock >= stackStartClock && struct.id.clock < stackEndClock)
) {
itemsToRedo.add(struct) itemsToRedo.add(struct)
} }
}) })
itemsToRedo.forEach(item => { itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, item, itemsToRedo) !== null || performedChange performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
}) })
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(doc.clientID))
/** /**
* @type {Array<Item>} * @type {Array<Item>}
*/ */
const itemsToDelete = [] const itemsToDelete = []
iterateStructs(transaction, structs, stackItem.start, stackItem.len, struct => { iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => {
if (struct instanceof Item && !struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { if (struct instanceof Item) {
if (struct.redone !== null) { if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id) let { item, diff } = followRedone(store, struct.id)
if (diff > 0) { if (diff > 0) {
item = getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + diff)) item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
} }
if (item.length > stackItem.len) { if (item.length > stackItem.len) {
getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + stackItem.len)) getItemCleanStart(transaction, createID(item.id.client, stackEndClock))
} }
struct = item struct = item
} }
itemsToDelete.push(struct) if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
} }
}) })
// We want to delete in reverse order so that children are deleted before // We want to delete in reverse order so that children are deleted before
@@ -111,9 +130,9 @@ const popStackItem = (undoManager, stack, eventType) => {
/** /**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or * Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the * the redo-stack. You may store additional stack information via the
* metadata property on `event.stackItem.metadata` (it is a `Map` of metadata properties). * metadata property on `event.stackItem.meta` (it is a `Map` of metadata properties).
* Fires 'stack-item-popped' event when a stack item was popped from either the * Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.metadata`. * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
* *
* @extends {Observable<'stack-item-added'|'stack-item-popped'>} * @extends {Observable<'stack-item-added'|'stack-item-popped'>}
*/ */
@@ -168,7 +187,7 @@ export class UndoManager extends Observable {
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) { if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op // append change to last stack op
const lastOp = stack[stack.length - 1] const lastOp = stack[stack.length - 1]
lastOp.ds = mergeDeleteSets(lastOp.ds, transaction.deleteSet) lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
lastOp.len = afterState - lastOp.start lastOp.len = afterState - lastOp.start
} else { } else {
// create a new stack op // create a new stack op
@@ -178,15 +197,34 @@ export class UndoManager extends Observable {
this.lastChange = now this.lastChange = now
} }
// make sure that deleted structs are not gc'd // make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, transaction.doc.store, /** @param {Item|GC} item */ item => { iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) { if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item) keepItem(item, true)
} }
}) })
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo' }, this]) this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo' }, this])
}) })
} }
clear () {
this.doc.transact(transaction => {
/**
* @param {StackItem} stackItem
*/
const clearItem = stackItem => {
iterateDeletedStructs(transaction, stackItem.ds, item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item, false)
}
})
}
this.undoStack.forEach(clearItem)
this.redoStack.forEach(clearItem)
})
this.undoStack = []
this.redoStack = []
}
/** /**
* UndoManager merges Undo-StackItem if they are created within time-gap * UndoManager merges Undo-StackItem if they are created within time-gap
* smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next * smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next

View File

@@ -56,6 +56,8 @@ export class YEvent {
/** /**
* Check if a struct is deleted by this event. * Check if a struct is deleted by this event.
* *
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct * @param {AbstractStruct} struct
* @return {boolean} * @return {boolean}
*/ */
@@ -66,6 +68,8 @@ export class YEvent {
/** /**
* Check if a struct is added by this event. * Check if a struct is added by this event.
* *
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct * @param {AbstractStruct} struct
* @return {boolean} * @return {boolean}
*/ */
@@ -106,7 +110,7 @@ export class YEvent {
} }
for (let item = target._start; item !== null; item = item.right) { for (let item = target._start; item !== null; item = item.right) {
if (item.deleted) { if (item.deleted) {
if (this.deletes(item)) { if (this.deletes(item) && !this.adds(item)) {
if (lastOp === null || lastOp.delete === undefined) { if (lastOp === null || lastOp.delete === undefined) {
packOp() packOp()
lastOp = { delete: 0 } lastOp = { delete: 0 }
@@ -177,7 +181,7 @@ export class YEvent {
}) })
this._changes = changes this._changes = changes
} }
return changes return /** @type {any} */ (changes)
} }
} }

View File

@@ -26,6 +26,7 @@ import {
readAndApplyDeleteSet, readAndApplyDeleteSet,
writeDeleteSet, writeDeleteSet,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
transact,
Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -299,10 +300,10 @@ export const readStructs = (decoder, transaction, store) => {
* @function * @function
*/ */
export const readUpdate = (decoder, ydoc, transactionOrigin) => export const readUpdate = (decoder, ydoc, transactionOrigin) =>
ydoc.transact(transaction => { transact(ydoc, transaction => {
readStructs(decoder, transaction, ydoc.store) readStructs(decoder, transaction, ydoc.store)
readAndApplyDeleteSet(decoder, transaction, ydoc.store) readAndApplyDeleteSet(decoder, transaction, ydoc.store)
}, transactionOrigin) }, transactionOrigin, false)
/** /**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`. * Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.

View File

@@ -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'
import * as object from 'lib0/object.js'
export * from '../src/internals.js' export * from '../src/internals.js'
/** /**
@@ -55,6 +56,7 @@ export class TestYInstance extends Doc {
}) })
this.connect() this.connect()
} }
/** /**
* Disconnect from TestConnector. * Disconnect from TestConnector.
*/ */
@@ -62,6 +64,7 @@ export class TestYInstance extends Doc {
this.receiving = new Map() this.receiving = new Map()
this.tc.onlineConns.delete(this) this.tc.onlineConns.delete(this)
} }
/** /**
* Append yourself to the list of known Y instances in testconnector. * Append yourself to the list of known Y instances in testconnector.
* Also initiate sync with all clients. * Also initiate sync with all clients.
@@ -83,6 +86,7 @@ export class TestYInstance extends Doc {
}) })
} }
} }
/** /**
* Receive a message from another client. This message is only appended to the list of receiving messages. * Receive a message from another client. This message is only appended to the list of receiving messages.
* TestConnector decides when this client actually reads this message. * TestConnector decides when this client actually reads this message.
@@ -124,6 +128,7 @@ export class TestConnector {
*/ */
this.prng = gen this.prng = gen
} }
/** /**
* Create a new Y instance and add it to the list of connections * Create a new Y instance and add it to the list of connections
* @param {number} clientID * @param {number} clientID
@@ -131,6 +136,7 @@ export class TestConnector {
createY (clientID) { createY (clientID) {
return new TestYInstance(this, clientID) return new TestYInstance(this, clientID)
} }
/** /**
* Choose random connection and flush a random message from a random sender. * Choose random connection and flush a random message from a random sender.
* *
@@ -162,6 +168,7 @@ export class TestConnector {
} }
return false return false
} }
/** /**
* @return {boolean} True iff this function actually flushed something * @return {boolean} True iff this function actually flushed something
*/ */
@@ -172,16 +179,20 @@ export class TestConnector {
} }
return didSomething return didSomething
} }
reconnectAll () { reconnectAll () {
this.allConns.forEach(conn => conn.connect()) this.allConns.forEach(conn => conn.connect())
} }
disconnectAll () { disconnectAll () {
this.allConns.forEach(conn => conn.disconnect()) this.allConns.forEach(conn => conn.disconnect())
} }
syncAll () { syncAll () {
this.reconnectAll() this.reconnectAll()
this.flushAllMessages() this.flushAllMessages()
} }
/** /**
* @return {boolean} Whether it was possible to disconnect a randon connection. * @return {boolean} Whether it was possible to disconnect a randon connection.
*/ */
@@ -192,6 +203,7 @@ export class TestConnector {
prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect() prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect()
return true return true
} }
/** /**
* @return {boolean} Whether it was possible to reconnect a random connection. * @return {boolean} Whether it was possible to reconnect a random connection.
*/ */
@@ -270,12 +282,12 @@ export const compare = users => {
// Test Map iterator // Test Map iterator
const ymapkeys = Array.from(users[0].getMap('map').keys()) const ymapkeys = Array.from(users[0].getMap('map').keys())
t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length) t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
ymapkeys.forEach(key => t.assert(userMapValues[0].hasOwnProperty(key))) ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key)))
/** /**
* @type {Object<string,any>} * @type {Object<string,any>}
*/ */
const mapRes = {} const mapRes = {}
for (let [k, v] of users[0].getMap('map')) { for (const [k, v] of users[0].getMap('map')) {
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
} }
t.compare(userMapValues[0], mapRes) t.compare(userMapValues[0], mapRes)

View File

@@ -13,6 +13,23 @@ import * as t from 'lib0/testing.js'
export const testUndoText = tc => { export const testUndoText = tc => {
const { testConnector, text0, text1 } = init(tc, { users: 3 }) const { testConnector, text0, text1 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0) const undoManager = new UndoManager(text0)
// items that are added & deleted in the same transaction won't be undo
text0.insert(0, 'test')
text0.delete(0, 4)
undoManager.undo()
t.assert(text0.toString() === '')
// follow redone items
text0.insert(0, 'a')
undoManager.stopCapturing()
text0.delete(0, 1)
undoManager.stopCapturing()
undoManager.undo()
t.assert(text0.toString() === 'a')
undoManager.undo()
t.assert(text0.toString() === '')
text0.insert(0, 'abc') text0.insert(0, 'abc')
text1.insert(0, 'xyz') text1.insert(0, 'xyz')
testConnector.syncAll() testConnector.syncAll()
@@ -52,11 +69,11 @@ export const testUndoMap = tc => {
const subType = new Y.Map() const subType = new Y.Map()
map0.set('a', subType) map0.set('a', subType)
subType.set('x', 42) subType.set('x', 42)
t.compare(map0.toJSON(), /** @type {any} */ ({ 'a': { x: 42 } })) t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
undoManager.undo() undoManager.undo()
t.assert(map0.get('a') === 1) t.assert(map0.get('a') === 1)
undoManager.redo() undoManager.redo()
t.compare(map0.toJSON(), /** @type {any} */ ({ 'a': { x: 42 } })) t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
testConnector.syncAll() testConnector.syncAll()
// if content is overwritten by another user, undo operations should be skipped // if content is overwritten by another user, undo operations should be skipped
map1.set('a', 44) map1.set('a', 44)
@@ -65,6 +82,15 @@ export const testUndoMap = tc => {
t.assert(map0.get('a') === 44) t.assert(map0.get('a') === 44)
undoManager.redo() undoManager.redo()
t.assert(map0.get('a') === 44) t.assert(map0.get('a') === 44)
// test setting value multiple times
map0.set('b', 'initial')
undoManager.stopCapturing()
map0.set('b', 'val1')
map0.set('b', 'val2')
undoManager.stopCapturing()
undoManager.undo()
t.assert(map0.get('b') === 'initial')
} }
/** /**

View File

@@ -3,6 +3,7 @@ import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint
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'
import * as prng from 'lib0/prng.js' import * as prng from 'lib0/prng.js'
import * as math from 'lib0/math.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
@@ -206,7 +207,7 @@ export const testChangeEvent = tc => {
const newArr = new Y.Array() const newArr = new Y.Array()
array0.insert(0, [newArr, 4, 'dtrn']) array0.insert(0, [newArr, 4, 'dtrn'])
t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0) t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0)
t.compare(changes.delta, [{insert: [newArr, 4, 'dtrn']}]) t.compare(changes.delta, [{ insert: [newArr, 4, 'dtrn'] }])
changes = null changes = null
array0.delete(0, 2) array0.delete(0, 2)
t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2) t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2)
@@ -226,7 +227,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
/** /**
* @type {Array<Object<string,any>>} * @type {Array<Object<string,any>>}
*/ */
let events = [] const events = []
array0.observe(e => { array0.observe(e => {
events.push(e) events.push(e)
}) })
@@ -317,7 +318,7 @@ export const testIteratingArrayContainingTypes = tc => {
arr.push([map]) arr.push([map])
} }
let cnt = 0 let cnt = 0
for (let item of arr) { for (const item of arr) {
t.assert(item.get('value') === cnt++, 'value is correct') t.assert(item.get('value') === cnt++, 'value is correct')
} }
y.destroy() y.destroy()
@@ -362,12 +363,12 @@ const arrayTransactions = [
var length = yarray.length var length = yarray.length
if (length > 0) { if (length > 0) {
var somePos = prng.int31(gen, 0, length - 1) var somePos = prng.int31(gen, 0, length - 1)
var delLength = prng.int31(gen, 1, Math.min(2, length - somePos)) var delLength = prng.int31(gen, 1, math.min(2, length - somePos))
if (prng.bool(gen)) { if (prng.bool(gen)) {
var type = yarray.get(somePos) var type = yarray.get(somePos)
if (type.length > 0) { if (type.length > 0) {
somePos = prng.int31(gen, 0, type.length - 1) somePos = prng.int31(gen, 0, type.length - 1)
delLength = prng.int31(gen, 0, Math.min(2, type.length - somePos)) delLength = prng.int31(gen, 0, math.min(2, type.length - somePos))
type.delete(somePos, delLength) type.delete(somePos, delLength)
} }
} else { } else {

View File

@@ -66,7 +66,7 @@ export const testGetAndSetOfMapProperty = tc => {
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
const u = user.getMap('map') const u = user.getMap('map')
t.compare(u.get('stuff'), 'stuffy') t.compare(u.get('stuff'), 'stuffy')
t.assert(u.get('undefined') === undefined, 'undefined') t.assert(u.get('undefined') === undefined, 'undefined')
@@ -108,7 +108,7 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
map0.set('stuff', 'stuffy') map0.set('stuff', 'stuffy')
t.compare(map0.get('stuff'), 'stuffy') t.compare(map0.get('stuff'), 'stuffy')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.compare(u.get('stuff'), 'stuffy') t.compare(u.get('stuff'), 'stuffy')
} }
@@ -123,7 +123,7 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
map0.set('stuff', 'c0') map0.set('stuff', 'c0')
map1.set('stuff', 'c1') map1.set('stuff', 'c1')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.compare(u.get('stuff'), 'c1') t.compare(u.get('stuff'), 'c1')
} }
@@ -139,7 +139,7 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
map1.set('stuff', 'c1') map1.set('stuff', 'c1')
map1.delete('stuff') map1.delete('stuff')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.assert(u.get('stuff') === undefined) t.assert(u.get('stuff') === undefined)
} }
@@ -156,7 +156,7 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
map1.set('stuff', 'c2') map1.set('stuff', 'c2')
map2.set('stuff', 'c3') map2.set('stuff', 'c3')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.compare(u.get('stuff'), 'c3') t.compare(u.get('stuff'), 'c3')
} }
@@ -179,7 +179,7 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
map3.set('stuff', 'c3') map3.set('stuff', 'c3')
map3.delete('stuff') map3.delete('stuff')
testConnector.flushAllMessages() testConnector.flushAllMessages()
for (let user of users) { for (const user of users) {
var u = user.getMap('map') var u = user.getMap('map')
t.assert(u.get('stuff') === undefined) t.assert(u.get('stuff') === undefined)
} }
@@ -340,6 +340,56 @@ export const testChangeEvent = tc => {
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testYmapEventExceptionsShouldCompleteTransaction = tc => {
const doc = new Y.Doc()
const map = doc.getMap('map')
let updateCalled = false
let throwingObserverCalled = false
let throwingDeepObserverCalled = false
doc.on('update', () => {
updateCalled = true
})
const throwingObserver = () => {
throwingObserverCalled = true
throw new Error('Failure')
}
const throwingDeepObserver = () => {
throwingDeepObserverCalled = true
throw new Error('Failure')
}
map.observe(throwingObserver)
map.observeDeep(throwingDeepObserver)
t.fails(() => {
map.set('y', '2')
})
t.assert(updateCalled)
t.assert(throwingObserverCalled)
t.assert(throwingDeepObserverCalled)
// check if it works again
updateCalled = false
throwingObserverCalled = false
throwingDeepObserverCalled = false
t.fails(() => {
map.set('z', '3')
})
t.assert(updateCalled)
t.assert(throwingObserverCalled)
t.assert(throwingDeepObserverCalled)
t.assert(map.get('z') === '3')
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -380,12 +430,12 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
*/ */
const mapTransactions = [ const mapTransactions = [
function set (user, gen) { function set (user, gen) {
let key = prng.oneOf(gen, ['one', 'two']) const key = prng.oneOf(gen, ['one', 'two'])
var value = prng.utf16String(gen) var value = prng.utf16String(gen)
user.getMap('map').set(key, value) user.getMap('map').set(key, value)
}, },
function setType (user, gen) { function setType (user, gen) {
let key = prng.oneOf(gen, ['one', 'two']) const key = prng.oneOf(gen, ['one', 'two'])
var type = prng.oneOf(gen, [new Y.Array(), new Y.Map()]) var type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
user.getMap('map').set(key, type) user.getMap('map').set(key, type)
if (type instanceof Y.Array) { if (type instanceof Y.Array) {
@@ -395,7 +445,7 @@ const mapTransactions = [
} }
}, },
function _delete (user, gen) { function _delete (user, gen) {
let key = prng.oneOf(gen, ['one', 'two']) const key = prng.oneOf(gen, ['one', 'two'])
user.getMap('map').delete(key) user.getMap('map').delete(key)
} }
] ]

View File

@@ -81,10 +81,10 @@ export const testBasicFormat = tc => {
export const testGetDeltaWithEmbeds = tc => { export const testGetDeltaWithEmbeds = tc => {
const { text0 } = init(tc, { users: 1 }) const { text0 } = init(tc, { users: 1 })
text0.applyDelta([{ text0.applyDelta([{
insert: {linebreak: 's'} insert: { linebreak: 's' }
}]) }])
t.compare(text0.toDelta(), [{ t.compare(text0.toDelta(), [{
insert: {linebreak: 's'} insert: { linebreak: 's' }
}]) }])
} }
@@ -127,7 +127,7 @@ export const testSnapshot = tc => {
delete v.attributes.ychange.user delete v.attributes.ychange.user
} }
}) })
t.compare(state2Diff, [{insert: 'a'}, {insert: 'x', attributes: {ychange: { state: 'added' }}}, {insert: 'b', attributes: {ychange: { state: 'removed' }}}, { insert: 'cd' }]) t.compare(state2Diff, [{ insert: 'a' }, { insert: 'x', attributes: { ychange: { type: 'added' } } }, { insert: 'b', attributes: { ychange: { type: 'removed' } } }, { insert: 'cd' }])
} }
/** /**
@@ -149,3 +149,12 @@ export const testSnapshotDeleteAfter = tc => {
const state1 = text0.toDelta(snapshot1) const state1 = text0.toDelta(snapshot1)
t.compare(state1, [{ insert: 'abcd' }]) t.compare(state1, [{ insert: 'abcd' }])
} }
/**
* @param {t.TestCase} tc
*/
export const testToJson = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'abc', { bold: true })
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
}

View File

@@ -60,13 +60,13 @@ export const testEvents = tc => {
*/ */
export const testTreewalker = tc => { export const testTreewalker = tc => {
const { users, xml0 } = init(tc, { users: 3 }) const { users, xml0 } = init(tc, { users: 3 })
let paragraph1 = new Y.XmlElement('p') const paragraph1 = new Y.XmlElement('p')
let paragraph2 = new Y.XmlElement('p') const paragraph2 = new Y.XmlElement('p')
let text1 = new Y.XmlText('init') const text1 = new Y.XmlText('init')
let text2 = new Y.XmlText('text') const text2 = new Y.XmlText('text')
paragraph1.insert(0, [text1, text2]) paragraph1.insert(0, [text1, text2])
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')]) xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
let allParagraphs = xml0.querySelectorAll('p') const allParagraphs = xml0.querySelectorAll('p')
t.assert(allParagraphs.length === 2, 'found exactly two paragraphs') t.assert(allParagraphs.length === 2, 'found exactly two paragraphs')
t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1') t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1')
t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2') t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2')

View File

@@ -6,15 +6,15 @@
"allowJs": true, /* Allow javascript files to be compiled. */ "allowJs": true, /* Allow javascript files to be compiled. */
"checkJs": true, /* Report errors in .js files. */ "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */ "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */ // "outFile": "./dist/yjs.js", /* Concatenate and emit output to single file. */
// "outDir": "./build", /* Redirect output structure to the directory. */ "outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */ // "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */ // "removeComments": true, /* Do not emit comments to output. */
"noEmit": true, /* Do not emit outputs. */ // "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
@@ -22,6 +22,7 @@
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */ "strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"emitDeclarationOnly": true,
// "strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
@@ -38,7 +39,10 @@
"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": { "paths": {
"yjs": ["./src/index.js"] "yjs": ["./src/index.js"],
"lib0/*": ["node_modules/lib0/*"],
"lib0/set.js": ["node_modules/lib0/set.js"],
"lib0/function.js": ["node_modules/lib0/function.js"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ }, /* 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. */
@@ -59,6 +63,6 @@
"maxNodeModuleJsDepth": 5, "maxNodeModuleJsDepth": 5,
// "types": ["./src/utils/typedefs.js"] // "types": ["./src/utils/typedefs.js"]
}, },
"include": ["./src/**/*", "./tests/**/*"], "include": ["./src/**/*.js", "./tests/**/*.js"],
"exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"] "exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"]
} }