Compare commits

...

59 Commits

Author SHA1 Message Date
Kevin Jahns
4c87f9a021 13.0.6 2020-05-08 14:50:53 +02:00
Kevin Jahns
4b08c67e06 bump lib0 to fix critical encoding issue in safari 2020-05-08 14:49:50 +02:00
Kevin Jahns
9f5bc9ddfe change client id when duplicate content is detected 2020-05-03 16:10:58 +02:00
Kevin Jahns
b399ffa765 add gc information to API docs 2020-04-26 13:24:18 +02:00
Kevin Jahns
180f4667c1 Readme correction: UndoManager accepts options 2020-04-17 02:02:09 +02:00
Kevin Jahns
9455373611 Merge branch 'master' of github.com:yjs/yjs 2020-04-15 20:50:29 +02:00
Kevin Jahns
aa804d89c0 update now.sh links 2020-04-15 19:52:34 +02:00
Kevin Jahns
3ef51a5d1a run test-exhaustive 2020-04-03 12:11:25 +02:00
Kevin Jahns
e61089c659 npm ci before workflow start 2020-04-03 12:09:13 +02:00
Kevin Jahns
97625cf29b fix workflow 2020-04-03 12:05:43 +02:00
Kevin Jahns
a5dc6c27aa Setup github workflow 2020-04-03 12:02:37 +02:00
Kevin Jahns
26a51bafc9 13.0.5 2020-04-02 01:05:04 +02:00
Kevin Jahns
f40e09d156 type fixes for breaking typescript@3.8.* release 2020-04-02 01:03:30 +02:00
Kevin Jahns
81650bc8f6 Merge branch 'gived-ISNIT0/187' 2020-04-01 23:44:40 +02:00
Kevin Jahns
c87caafeb6 lint & refactor PR #187 2020-04-01 23:39:27 +02:00
Kevin Jahns
195b26d90f Merge branch 'ISNIT0/187' of https://github.com/gived/yjs into gived-ISNIT0/187 2020-04-01 14:05:18 +02:00
Kevin Jahns
7e0189ca84 Merge branch 'master' of github.com:yjs/yjs 2020-04-01 14:04:45 +02:00
Kevin Jahns
192706f2a8 update readme 2020-04-01 14:04:41 +02:00
Joe Reeve
a4ce8ae07d 🐛 fix for #187 2020-03-31 16:06:28 +01:00
Kevin Jahns
e04a980af1 Merge pull request #184 from yjs/readme-cleanup
remove deadlinks
2020-03-21 21:50:43 +01:00
Nik Graf
47d40eb6b0 remove deadlinks 2020-03-21 15:51:39 +01:00
Kevin Jahns
fc4a39cc7d Merge pull request #182 from LucasGenoud/patch-1
Update lib0 to latest version
2020-02-27 18:13:22 +01:00
LucasGenoud
44e1fd9f14 Update lib0 to latest version 2020-02-27 10:51:21 +01:00
Kevin Jahns
02cc5a215f bump lib0 2020-02-19 09:49:54 -06:00
Kevin Jahns
d1e8d50c43 13.0.4 2020-02-12 10:53:56 +01:00
Kevin Jahns
18bb2d0719 fix imports in esm bundle 2020-02-12 10:52:51 +01:00
Kevin Jahns
45df311dd7 13.0.3 2020-02-12 10:38:28 +01:00
Kevin Jahns
62888b4004 bundle yjs as a module to prevent declaration issues from circular dependencies 2020-02-12 10:37:22 +01:00
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
45 changed files with 1452 additions and 3206 deletions

31
.github/workflows/nodejs.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 13.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run lint
- run: npm run test-extensive
env:
CI: true

View File

@@ -15,11 +15,15 @@ suited for even large documents.
* Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos)
* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev)
* Benchmarks:
* Benchmark Yjs vs. Automerge:
[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
release checkout the [v12 docs](./README.v12.md) :warning:
:construction_worker_woman: If you are looking for professional (paid) support to
build collaborative or distributed applications ping us at
<yjs@tag1consulting.com>. Otherwise you can find help on our
[discussion board](https://discuss.yjs.dev).
## Table of Contents
@@ -36,12 +40,6 @@ release checkout the [v12 docs](./README.v12.md) :warning:
* [Miscellaneous](#Miscellaneous)
* [Typescript Declarations](#Typescript-Declarations)
* [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm)
* [Evaluation](#Evaluation)
* [Existing shared editing libraries](#Exisisting-Javascript-Libraries)
* [CRDT Algorithms](#CRDT-Algorithms)
* [Comparison of CRDT with OT](#Comparing-CRDT-with-OT)
* [Comparison of CRDT Algorithms](#Comparing-CRDT-Algorithms)
* [Comparison of Yjs with other Implementations](#Comparing-Yjs-with-other-Implementations)
* [License and Author](#License-and-Author)
## Overview
@@ -54,13 +52,10 @@ are implemented in separate modules.
| 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/) |
| [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/) |
| [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/) |
| [Textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) | | [y-textarea](http://github.com/yjs/y-textarea) | [demo](https://yjs-demos.now.sh/textarea/) |
| [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) | | [y-dom](http://github.com/yjs/y-dom) | [demo](https://yjs-demos.now.sh/dom/) |
| [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://demos.yjs.dev/prosemirror/prosemirror.html) |
| [Quill](https://quilljs.com/) | ✔ | [y-quill](http://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
### Providers
@@ -70,18 +65,25 @@ manage all that for you and are the perfect starting point for your
collaborative app.
<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>
<dd>
A module that contains a simple websocket backend and a websocket client that
connects to that backend. The backend can be extended to persist updates in a
leveldb database.
</dd>
<dt><a href="http://github.com/yjs/y-mesh">y-mesh</a></dt>
<dt><a href="http://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
<dd>
[WIP] Creates a connected graph of webrtc connections with a high
<a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It
requires a signaling server that connects a client to the first peer. But after
that the network manages itself. It is well suited for large and small networks.
Efficiently persists document updates to the browsers indexeddb database.
The document is immediately available and only diffs need to be synced through the
network provider.
</dd>
<dt><a href="http://github.com/yjs/y-dat">y-dat</a></dt>
<dd>
@@ -97,7 +99,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:
```sh
npm i yjs@13.0.0-97 y-websocket@1.0.0-6
npm i yjs y-websocket
```
Start the y-websocket server:
@@ -461,6 +463,12 @@ const doc = new Y.Doc()
<dl>
<b><code>clientID</code></b>
<dd>A unique id that identifies this client. (readonly)</dd>
<b><code>gc</code></b>
<dd>
Whether garbage collection is enabled on this doc instance. Set `doc.gc = false`
in order to disable gc and be able to restore old content. See https://github.com/yjs/yjs#yjs-crdt-algorithm
for more information about gc in Yjs.
</dd>
<b><code>transact(function(Transaction):void [, origin:any])</code></b>
<dd>
Every change on the shared document happens in a transaction. Observer calls and
@@ -648,8 +656,8 @@ ytext.toString() // => 'abc'
```
<dl>
<b><code>constructor(scope:Y.AbstractType|Array&lt;Y.AbstractType&gt;,
[[{captureTimeout:number,trackedOrigins:Set&lt;any&gt;,deleteFilter:function(item):boolean}]])</code></b>
<b><code>constructor(scope:Y.AbstractType|Array&lt;Y.AbstractType&gt;
[, {captureTimeout:number,trackedOrigins:Set&lt;any&gt;,deleteFilter:function(item):boolean}])</code></b>
<dd>Accepts either single type as scope or an array of types.</dd>
<b><code>undo()</code></b>
<dd></dd>
@@ -671,7 +679,7 @@ undo- or the redo-stack.
<code>
on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
| 'redo' })
</code>
</code>
</b>
<dd>
Register an event that is called when a <code>StackItem</code> is popped from
@@ -689,28 +697,30 @@ StackItem won't be merged.
// without stopCapturing
ytext.insert(0, 'a')
ytext.insert(1, 'b')
um.undo()
undoManager.undo()
ytext.toString() // => '' (note that 'ab' was removed)
// with stopCapturing
ytext.insert(0, 'a')
um.stopCapturing()
undoManager.stopCapturing()
ytext.insert(0, 'b')
um.undo()
undoManager.undo()
ytext.toString() // => 'a' (note that only 'b' was removed)
```
#### Example: Specify tracked origins
Every change on the shared document has an origin. If no origin was specified,
it defaults to `null`. By specifying `trackedTransactionOrigins` you can
it defaults to `null`. By specifying `trackedOrigins` you can
selectively specify which changes should be tracked by `UndoManager`. The
UndoManager instance is always added to `trackedTransactionOrigins`.
UndoManager instance is always added to `trackedOrigins`.
```js
class CustomBinding {}
const ytext = doc.getArray('array')
const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding]))
const undoManager = new Y.UndoManager(ytext, {
trackedOrigins: new Set([42, CustomBinding])
})
ytext.insert(0, 'abc')
undoManager.undo()
@@ -748,7 +758,9 @@ document. You can assign meta-information to Undo-/Redo-StackItems.
```js
const ytext = doc.getArray('array')
const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding]))
const undoManager = new Y.UndoManager(ytext, {
trackedOrigins: new Set([42, CustomBinding])
})
undoManager.on('stack-item-added', event => {
// save the current cursor location on the stack-item

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

3518
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

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
@@ -37,23 +38,27 @@ const debugResolve = {
export default [{
input: './src/index.js',
output: [{
output: {
name: 'Y',
file: 'dist/yjs.js',
file: 'dist/yjs.cjs',
format: 'cjs',
sourcemap: true,
paths: path => {
if (/^lib0\//.test(path)) {
return `lib0/dist/${path.slice(5)}`
return `lib0/dist/${path.slice(5, -3)}.cjs`
}
return path
}
}, {
},
external: id => /^lib0\//.test(id)
}, {
input: './src/index.js',
output: {
name: 'Y',
file: 'dist/yjs.mjs',
format: 'es',
format: 'esm',
sourcemap: true
}],
},
external: id => /^lib0\//.test(id)
}, {
input: './tests/index.js',
@@ -66,8 +71,24 @@ export default [{
plugins: [
debugResolve,
nodeResolve({
sourcemap: true,
mainFields: ['module', 'browser', 'main']
})
}),
commonjs()
]
}, {
input: './tests/index.js',
output: {
name: 'test',
file: 'dist/tests.cjs',
format: 'cjs',
sourcemap: true
},
plugins: [
debugResolve,
nodeResolve({
mainFields: ['module', 'main']
}),
commonjs()
],
external: ['isomorphic.js']
}]

View File

@@ -47,12 +47,16 @@ export {
typeMapGetSnapshot,
iterateDeletedStructs,
applyUpdate,
readUpdate,
encodeStateAsUpdate,
encodeStateVector,
UndoManager,
decodeSnapshot,
encodeSnapshot,
isDeleted,
isParentOf,
equalSnapshots,
PermanentUserData // @TODO experimental
PermanentUserData, // @TODO experimental
tryGc,
transact
} from './internals.js'

View File

@@ -6,9 +6,6 @@ import {
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
import * as error from 'lib0/error.js'
/**
* @private
*/
export class AbstractStruct {
/**
* @param {ID} id
@@ -24,6 +21,7 @@ export class AbstractStruct {
this.length = length
this.deleted = false
}
/**
* Merge this struct with the item to the right.
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
@@ -34,15 +32,16 @@ export class AbstractStruct {
mergeWith (right) {
return false
}
/**
* @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset
* @param {number} encodingRef
* @private
*/
write (encoder, offset, encodingRef) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
*/
@@ -51,9 +50,6 @@ export class AbstractStruct {
}
}
/**
* @private
*/
export class AbstractStructRef {
/**
* @param {ID} id
@@ -69,6 +65,7 @@ export class AbstractStructRef {
*/
this.id = id
}
/**
* @param {Transaction} transaction
* @return {Array<ID|null>}
@@ -76,6 +73,7 @@ export class AbstractStructRef {
getMissing (transaction) {
return this._missing
}
/**
* @param {Transaction} transaction
* @param {StructStore} store

View File

@@ -5,9 +5,6 @@ import {
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export class ContentAny {
/**
* @param {Array<any>} arr
@@ -18,30 +15,35 @@ export class ContentAny {
*/
this.arr = arr
}
/**
* @return {number}
*/
getLength () {
return this.arr.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.arr
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentAny}
*/
copy () {
return new ContentAny(this.arr)
}
/**
* @param {number} offset
* @return {ContentAny}
@@ -51,6 +53,7 @@ export class ContentAny {
this.arr = this.arr.slice(0, offset)
return right
}
/**
* @param {ContentAny} right
* @return {boolean}
@@ -59,6 +62,7 @@ export class ContentAny {
this.arr = this.arr.concat(right.arr)
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
@@ -84,6 +88,7 @@ export class ContentAny {
encoding.writeAny(encoder, c)
}
}
/**
* @return {number}
*/
@@ -93,8 +98,6 @@ export class ContentAny {
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentAny}
*/

View File

@@ -7,9 +7,6 @@ import * as decoding from 'lib0/decoding.js'
import * as buffer from 'lib0/buffer.js'
import * as error from 'lib0/error.js'
/**
* @private
*/
export class ContentBinary {
/**
* @param {Uint8Array} content
@@ -17,30 +14,35 @@ export class ContentBinary {
constructor (content) {
this.content = content
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.content]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentBinary}
*/
copy () {
return new ContentBinary(this.content)
}
/**
* @param {number} offset
* @return {ContentBinary}
@@ -48,6 +50,7 @@ export class ContentBinary {
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentBinary} right
* @return {boolean}
@@ -55,6 +58,7 @@ export class ContentBinary {
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
@@ -75,6 +79,7 @@ export class ContentBinary {
write (encoder, offset) {
encoding.writeVarUint8Array(encoder, this.content)
}
/**
* @return {number}
*/
@@ -84,8 +89,6 @@ export class ContentBinary {
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentBinary}
*/

View File

@@ -7,9 +7,6 @@ import {
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export class ContentDeleted {
/**
* @param {number} len
@@ -17,30 +14,35 @@ export class ContentDeleted {
constructor (len) {
this.len = len
}
/**
* @return {number}
*/
getLength () {
return this.len
}
/**
* @return {Array<any>}
*/
getContent () {
return []
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentDeleted}
*/
copy () {
return new ContentDeleted(this.len)
}
/**
* @param {number} offset
* @return {ContentDeleted}
@@ -50,6 +52,7 @@ export class ContentDeleted {
this.len = offset
return right
}
/**
* @param {ContentDeleted} right
* @return {boolean}
@@ -58,6 +61,7 @@ export class ContentDeleted {
this.len += right.len
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
@@ -66,6 +70,7 @@ export class ContentDeleted {
addToDeleteSet(transaction.deleteSet, item.id, this.len)
item.deleted = true
}
/**
* @param {Transaction} transaction
*/
@@ -81,6 +86,7 @@ export class ContentDeleted {
write (encoder, offset) {
encoding.writeVarUint(encoder, this.len - offset)
}
/**
* @return {number}
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,10 +68,11 @@ export const followRedone = (store, id) => {
* sending it to other peers
*
* @param {Item|null} item
* @param {boolean} keep
*/
export const keepItem = item => {
while (item !== null && !item.keep) {
item.keep = true
export const keepItem = (item, keep) => {
while (item !== null && item.keep !== keep) {
item.keep = keep
item = item.parent._item
}
}
@@ -220,7 +221,7 @@ export const redoItem = (transaction, item, redoitems) => {
item.content.copy()
)
item.redone = redoneItem.id
keepItem(redoneItem)
keepItem(redoneItem, true)
redoneItem.integrate(transaction)
return redoneItem
}
@@ -303,7 +304,6 @@ export class Item extends AbstractStruct {
/**
* @param {Transaction} transaction
* @private
*/
integrate (transaction) {
const store = transaction.doc.store
@@ -402,7 +402,6 @@ export class Item extends AbstractStruct {
/**
* Returns the next non-deleted item
* @private
*/
get next () {
let n = this.right
@@ -414,7 +413,6 @@ export class Item extends AbstractStruct {
/**
* Returns the previous non-deleted item
* @private
*/
get prev () {
let n = this.left
@@ -430,6 +428,7 @@ export class Item extends AbstractStruct {
get lastId () {
return createID(this.id.client, this.id.clock + this.length - 1)
}
/**
* Try to merge two items
*
@@ -484,8 +483,6 @@ export class Item extends AbstractStruct {
/**
* @param {StructStore} store
* @param {boolean} parentGCd
*
* @private
*/
gc (store, parentGCd) {
if (!this.deleted) {
@@ -507,8 +504,6 @@ export class Item extends AbstractStruct {
*
* @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset
*
* @private
*/
write (encoder, offset) {
const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin
@@ -578,12 +573,14 @@ export class AbstractContent {
getLength () {
throw error.methodUnimplemented()
}
/**
* @return {Array<any>}
*/
getContent () {
throw error.methodUnimplemented()
}
/**
* Should return false if this Item is some kind of meta information
* (e.g. format information).
@@ -596,12 +593,14 @@ export class AbstractContent {
isCountable () {
throw error.methodUnimplemented()
}
/**
* @return {AbstractContent}
*/
copy () {
throw error.methodUnimplemented()
}
/**
* @param {number} offset
* @return {AbstractContent}
@@ -609,6 +608,7 @@ export class AbstractContent {
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {AbstractContent} right
* @return {boolean}
@@ -616,6 +616,7 @@ export class AbstractContent {
mergeWith (right) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
* @param {Item} item
@@ -623,18 +624,21 @@ export class AbstractContent {
integrate (transaction, item) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {
throw error.methodUnimplemented()
}
/**
* @param {StructStore} store
*/
gc (store) {
throw error.methodUnimplemented()
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
@@ -642,6 +646,7 @@ export class AbstractContent {
write (encoder, offset) {
throw error.methodUnimplemented()
}
/**
* @return {number}
*/
@@ -709,6 +714,7 @@ export class ItemRef extends AbstractStructRef {
this.content = readItemContent(decoder, info)
this.length = this.content.getLength()
}
/**
* @param {Transaction} transaction
* @param {StructStore} store

View File

@@ -22,7 +22,6 @@ import * as encoding from 'lib0/encoding.js' // eslint-disable-line
/**
* Call event listeners with an event. This will also add an event to all
* parents (for `.observeDeep` handlers).
* @private
*
* @template EventType
* @param {AbstractType<EventType>} type
@@ -54,17 +53,14 @@ export class AbstractType {
*/
this._item = null
/**
* @private
* @type {Map<string,Item>}
*/
this._map = new Map()
/**
* @private
* @type {Item|null}
*/
this._start = null
/**
* @private
* @type {Doc|null}
*/
this.doc = null
@@ -90,7 +86,6 @@ export class AbstractType {
*
* @param {Doc} y The Yjs instance
* @param {Item|null} item
* @private
*/
_integrate (y, item) {
this.doc = y
@@ -99,7 +94,6 @@ export class AbstractType {
/**
* @return {AbstractType<EventType>}
* @private
*/
_copy () {
throw error.methodUnimplemented()
@@ -107,7 +101,6 @@ export class AbstractType {
/**
* @param {encoding.Encoder} encoder
* @private
*/
_write (encoder) { }
@@ -128,8 +121,6 @@ export class AbstractType {
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*
* @private
*/
_callObserver (transaction, parentSubs) { /* skip if no type is specified */ }
@@ -171,7 +162,7 @@ export class AbstractType {
/**
* @abstract
* @return {Object | Array | number | string}
* @return {any}
*/
toJSON () {}
}
@@ -370,7 +361,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
let left = referenceItem
const right = referenceItem === null ? parent._start : referenceItem.right
/**
* @type {Array<Object|Array|number>}
* @type {Array<Object|Array<any>|number>}
*/
let jsonContent = []
const packJsonContent = () => {
@@ -515,7 +506,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
content = new ContentAny([value])
break
case Uint8Array:
content = new ContentBinary(value)
content = new ContentBinary(/** @type {Uint8Array} */ (value))
break
default:
if (value instanceof AbstractType) {
@@ -552,7 +543,7 @@ export const typeMapGetAll = (parent) => {
/**
* @type {Object<string,any>}
*/
let res = {}
const res = {}
for (const [key, value] of parent._map) {
if (!value.deleted) {
res[key] = value.content.getContent()[value.length - 1]

View File

@@ -51,6 +51,7 @@ export class YArray extends AbstractType {
*/
this._prelimContent = []
}
/**
* Integrate this type into the Yjs instance.
*
@@ -60,12 +61,10 @@ export class YArray extends AbstractType {
*
* @param {Doc} y The Yjs instance
* @param {Item} item
*
* @private
*/
_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
}
@@ -76,13 +75,12 @@ export class YArray extends AbstractType {
get length () {
return this._prelimContent === null ? this._length : this._prelimContent.length
}
/**
* Creates YArrayEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*
* @private
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YArrayEvent(this, transaction))
@@ -110,7 +108,7 @@ export class YArray extends AbstractType {
typeListInsertGenerics(transaction, this, index, content)
})
} else {
/** @type {Array} */ (this._prelimContent).splice(index, 0, ...content)
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
}
}
@@ -135,7 +133,7 @@ export class YArray extends AbstractType {
typeListDelete(transaction, this, index, length)
})
} else {
/** @type {Array} */ (this._prelimContent).splice(index, length)
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
}
}
@@ -198,7 +196,6 @@ export class YArray extends AbstractType {
/**
* @param {encoding.Encoder} encoder
* @private
*/
_write (encoder) {
encoding.writeVarUint(encoder, YArrayRefID)

View File

@@ -53,6 +53,7 @@ export class YMap extends AbstractType {
*/
this._prelimContent = new Map()
}
/**
* Integrate this type into the Yjs instance.
*
@@ -62,12 +63,10 @@ export class YMap extends AbstractType {
*
* @param {Doc} y The Yjs instance
* @param {Item} item
*
* @private
*/
_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._prelimContent = null
@@ -82,8 +81,6 @@ export class YMap extends AbstractType {
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*
* @private
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs))
@@ -99,7 +96,7 @@ export class YMap extends AbstractType {
* @type {Object<string,T>}
*/
const map = {}
for (let [key, item] of this._map) {
for (const [key, item] of this._map) {
if (!item.deleted) {
const v = item.content.getContent()[item.length - 1]
map[key] = v instanceof AbstractType ? v.toJSON() : v
@@ -145,7 +142,7 @@ export class YMap extends AbstractType {
* @type {Object<string,T>}
*/
const map = {}
for (let [key, item] of this._map) {
for (const [key, item] of this._map) {
if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this)
}
@@ -214,8 +211,6 @@ export class YMap extends AbstractType {
/**
* @param {encoding.Encoder} encoder
*
* @private
*/
_write (encoder) {
encoding.writeVarUint(encoder, YMapRefID)

View File

@@ -112,10 +112,9 @@ const findNextPosition = (transaction, currentAttributes, left, right, count) =>
* @function
*/
const findPosition = (transaction, parent, index) => {
let currentAttributes = new Map()
let left = null
let right = parent._start
return findNextPosition(transaction, currentAttributes, left, right, index)
const currentAttributes = new Map()
const right = parent._start
return findNextPosition(transaction, currentAttributes, null, right, index)
}
/**
@@ -147,7 +146,7 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
left = 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.integrate(transaction)
}
@@ -214,7 +213,7 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
const negatedAttributes = new Map()
// insert format-start items
for (let key in attributes) {
for (const key in attributes) {
const val = attributes[key]
const currentVal = currentAttributes.get(key) || null
if (!equalAttrs(currentVal, val)) {
@@ -241,7 +240,7 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
* @function
**/
const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => {
for (let [key] of currentAttributes) {
for (const [key] of currentAttributes) {
if (attributes[key] === undefined) {
attributes[key] = null
}
@@ -251,7 +250,7 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
left = insertPos.left
right = insertPos.right
// insert content
const content = text.constructor === String ? new ContentString(text) : new ContentEmbed(text)
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, content)
left.integrate(transaction)
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
@@ -281,7 +280,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
while (length > 0 && right !== null) {
if (!right.deleted) {
switch (right.content.constructor) {
case ContentFormat:
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (right.content)
const attr = attributes[key]
if (attr !== undefined) {
@@ -294,6 +293,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
}
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
break
}
case ContentEmbed:
case ContentString:
if (length < right.length) {
@@ -400,11 +400,11 @@ export class YTextEvent extends YEvent {
constructor (ytext, transaction) {
super(ytext, transaction)
/**
* @private
* @type {Array<DeltaItem>|null}
*/
this._delta = null
}
/**
* Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
@@ -429,7 +429,10 @@ export class YTextEvent extends YEvent {
/**
* @type {Object<string,any>}
*/
let attributes = {} // counts added or removed new attributes for retain
const attributes = {} // counts added or removed new attributes for retain
/**
* @type {string|object}
*/
let insert = ''
let retain = 0
let deleteLen = 0
@@ -448,7 +451,7 @@ export class YTextEvent extends YEvent {
op = { insert }
if (currentAttributes.size > 0) {
op.attributes = {}
for (let [key, value] of currentAttributes) {
for (const [key, value] of currentAttributes) {
if (value !== null) {
op.attributes[key] = value
}
@@ -460,7 +463,7 @@ export class YTextEvent extends YEvent {
op = { retain }
if (Object.keys(attributes).length > 0) {
op.attributes = {}
for (let key in attributes) {
for (const key in attributes) {
op.attributes[key] = attributes[key]
}
}
@@ -518,7 +521,7 @@ export class YTextEvent extends YEvent {
retain += item.length
}
break
case ContentFormat:
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) {
if (!this.deletes(item)) {
@@ -570,12 +573,13 @@ export class YTextEvent extends YEvent {
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content))
}
break
}
}
item = item.right
}
addOp()
while (delta.length > 0) {
let lastOp = delta[delta.length - 1]
const lastOp = delta[delta.length - 1]
if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
// retain delta's if they don't assign attributes
delta.pop()
@@ -607,11 +611,15 @@ export class YText extends AbstractType {
/**
* Array of pending operations on this type
* @type {Array<function():void>?}
* @private
*/
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
}
/**
* Number of characters of this text type.
*
* @type {number}
*/
get length () {
return this._length
}
@@ -619,8 +627,6 @@ export class YText extends AbstractType {
/**
* @param {Doc} y
* @param {Item} item
*
* @private
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -641,8 +647,6 @@ export class YText extends AbstractType {
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*
* @private
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YTextEvent(this, transaction))
@@ -668,6 +672,16 @@ export class YText extends AbstractType {
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.
*
@@ -734,7 +748,7 @@ export class YText extends AbstractType {
*/
const attributes = {}
let addAttributes = false
for (let [key, value] of currentAttributes) {
for (const [key, value] of currentAttributes) {
addAttributes = true
attributes[key] = value
}
@@ -761,7 +775,7 @@ export class YText extends AbstractType {
while (n !== null) {
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
switch (n.content.constructor) {
case ContentString:
case ContentString: {
const cur = currentAttributes.get('ychange')
if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
@@ -779,12 +793,25 @@ export class YText extends AbstractType {
}
str += /** @type {ContentString} */ (n.content).str
break
case ContentEmbed:
}
case ContentEmbed: {
packStr()
ops.push({
/**
* @type {Object<string,any>}
*/
const op = {
insert: /** @type {ContentEmbed} */ (n.content).embed
})
}
if (currentAttributes.size > 0) {
const attrs = /** @type {Object<string,any>} */ ({})
op.attributes = attrs
for (const [key, value] of currentAttributes) {
attrs[key] = value
}
}
ops.push(op)
break
}
case ContentFormat:
if (isVisible(n, snapshot)) {
packStr()
@@ -820,6 +847,7 @@ export class YText extends AbstractType {
const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (!attributes) {
attributes = {}
// @ts-ignore
currentAttributes.forEach((v, k) => { attributes[k] = v })
}
insertText(transaction, this, left, right, currentAttributes, text, attributes)
@@ -891,7 +919,7 @@ export class YText extends AbstractType {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
let { left, right, currentAttributes } = findPosition(transaction, this, index)
const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (right === null) {
return
}
@@ -904,8 +932,6 @@ export class YText extends AbstractType {
/**
* @param {encoding.Encoder} encoder
*
* @private
*/
_write (encoder) {
encoding.writeVarUint(encoder, YTextRefID)

View File

@@ -27,7 +27,6 @@ export class YXmlElement extends YXmlFragment {
this.nodeName = nodeName
/**
* @type {Map<string, any>|null}
* @private
*/
this._prelimAttrs = new Map()
}
@@ -41,7 +40,6 @@ export class YXmlElement extends YXmlFragment {
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @private
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -55,7 +53,6 @@ export class YXmlElement extends YXmlFragment {
* Creates an Item with the same effect as this Item (without position effect)
*
* @return {YXmlElement}
* @private
*/
_copy () {
return new YXmlElement(this.nodeName)
@@ -74,7 +71,7 @@ export class YXmlElement extends YXmlFragment {
const attrs = this.getAttributes()
const stringBuilder = []
const keys = []
for (let key in attrs) {
for (const key in attrs) {
keys.push(key)
}
keys.sort()
@@ -140,7 +137,7 @@ export class YXmlElement extends YXmlFragment {
* Returns all attribute name/value pairs in a JSON Object.
*
* @param {Snapshot} [snapshot]
* @return {Object} A JSON Object that describes the attributes.
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
@@ -165,8 +162,8 @@ export class YXmlElement extends YXmlFragment {
*/
toDOM (_document = document, hooks = {}, binding) {
const dom = _document.createElement(this.nodeName)
let attrs = this.getAttributes()
for (let key in attrs) {
const attrs = this.getAttributes()
for (const key in attrs) {
dom.setAttribute(key, attrs[key])
}
typeListForEach(this, yxml => {
@@ -184,7 +181,6 @@ export class YXmlElement extends YXmlFragment {
*
* This is called when this Item is sent to a remote peer.
*
* @private
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_write (encoder) {
@@ -197,7 +193,6 @@ export class YXmlElement extends YXmlFragment {
* @param {decoding.Decoder} decoder
* @return {YXmlElement}
*
* @private
* @function
*/
export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder))

View File

@@ -68,6 +68,7 @@ export class YXmlTreeWalker {
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
@@ -126,10 +127,10 @@ export class YXmlFragment extends AbstractType {
super()
/**
* @type {Array<any>|null}
* @private
*/
this._prelimContent = []
}
/**
* Integrate this type into the Yjs instance.
*
@@ -139,11 +140,10 @@ export class YXmlFragment extends AbstractType {
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @private
*/
_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
}
@@ -222,7 +222,6 @@ export class YXmlFragment extends AbstractType {
/**
* Creates YXmlEvent and calls observers.
* @private
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
@@ -240,6 +239,9 @@ export class YXmlFragment extends AbstractType {
return typeListMap(this, xml => xml.toString()).join('')
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
@@ -307,6 +309,7 @@ export class YXmlFragment extends AbstractType {
this._prelimContent.splice(index, length)
}
}
/**
* Transforms this YArray to a JavaScript Array.
*
@@ -315,13 +318,13 @@ export class YXmlFragment extends AbstractType {
toArray () {
return typeListToArray(this)
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @private
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_write (encoder) {

View File

@@ -25,8 +25,6 @@ export class YXmlHook extends YMap {
/**
* Creates an Item with the same effect as this Item (without position effect)
*
* @private
*/
_copy () {
return new YXmlHook(this.hookName)
@@ -69,8 +67,6 @@ export class YXmlHook extends YMap {
* This is called when this Item is sent to a remote peer.
*
* @param {encoding.Encoder} encoder The encoder to write data to.
*
* @private
*/
_write (encoder) {
super._write(encoder)

View File

@@ -12,6 +12,7 @@ export class YXmlText extends YText {
_copy () {
return new YXmlText()
}
/**
* Creates a Dom Element that mirrors this YXmlText.
*
@@ -39,9 +40,9 @@ export class YXmlText extends YText {
// @ts-ignore
return this.toDelta().map(delta => {
const nestedNodes = []
for (let nodeName in delta.attributes) {
for (const nodeName in delta.attributes) {
const attrs = []
for (let key in delta.attributes[nodeName]) {
for (const key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] })
}
// sort attributes to get a unique order
@@ -69,14 +70,15 @@ export class YXmlText extends YText {
}).join('')
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
/**
* @param {encoding.Encoder} encoder
*
* @private
*/
_write (encoder) {
encoding.writeVarUint(encoder, YXmlTextRefID)

View File

@@ -42,7 +42,6 @@ export class DeleteSet {
constructor () {
/**
* @type {Map<number,Array<DeleteItem>>}
* @private
*/
this.clients = new Map()
}

View File

@@ -17,6 +17,8 @@ import { Observable } from 'lib0/observable.js'
import * as random from 'lib0/random.js'
import * as map from 'lib0/map.js'
export const generateNewClientId = random.uint32
/**
* A Yjs instance handles the state of shared data.
* @extends Observable<string>
@@ -25,11 +27,13 @@ export class Doc extends Observable {
/**
* @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 ({ gc = true } = {}) {
constructor ({ gc = true, gcFilter = () => true } = {}) {
super()
this.gc = gc
this.clientID = random.uint32()
this.gcFilter = gcFilter
this.clientID = generateNewClientId()
/**
* @type {Map<string, AbstractType<YEvent>>}
*/
@@ -37,15 +41,14 @@ export class Doc extends Observable {
this.store = new StructStore()
/**
* @type {Transaction | null}
* @private
*/
this._transaction = null
/**
* @type {Array<Transaction>}
* @private
*/
this._transactionCleanups = []
}
/**
* Changes that happen inside of a transaction are bundled. This means that
* the observer fires _after_ the transaction is finished and that all changes
@@ -60,6 +63,7 @@ export class Doc extends Observable {
transact (f, origin = null) {
transact(this, f, origin)
}
/**
* Define a shared data type.
*
@@ -101,6 +105,7 @@ export class Doc extends Observable {
t._map = type._map
type._map.forEach(/** @param {Item?} n */ n => {
for (; n !== null; n = n.left) {
// @ts-ignore
n.parent = t
}
})
@@ -118,6 +123,7 @@ export class Doc extends Observable {
}
return type
}
/**
* @template T
* @param {string} name
@@ -129,6 +135,7 @@ export class Doc extends Observable {
// @ts-ignore
return this.get(name, YArray)
}
/**
* @param {string} name
* @return {YText}
@@ -139,6 +146,7 @@ export class Doc extends Observable {
// @ts-ignore
return this.get(name, YText)
}
/**
* @param {string} name
* @return {YMap<any>}
@@ -149,6 +157,7 @@ export class Doc extends Observable {
// @ts-ignore
return this.get(name, YMap)
}
/**
* @param {string} name
* @return {YXmlFragment}
@@ -159,15 +168,15 @@ export class Doc extends Observable {
// @ts-ignore
return this.get(name, YXmlFragment)
}
/**
* Emit `destroy` event and unregister all event handlers.
*
* @protected
*/
destroy () {
this.emit('destroyed', [true])
super.destroy()
}
/**
* @param {string} eventName
* @param {function} f
@@ -175,6 +184,7 @@ export class Doc extends Observable {
on (eventName, f) {
super.on(eventName, f)
}
/**
* @param {string} eventName
* @param {function} f

View File

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

View File

@@ -15,15 +15,14 @@ import { mergeDeleteSets, isDeleted } from './DeleteSet.js'
export class PermanentUserData {
/**
* @param {Doc} doc
* @param {string} key
* @param {YMap<any>} [storeType]
*/
constructor (doc, key = 'users') {
const users = doc.getMap(key)
constructor (doc, storeType = doc.getMap('users')) {
/**
* @type {Map<string,DeleteSet>}
*/
const dss = new Map()
this.yusers = users
this.yusers = storeType
this.doc = doc
/**
* Maps from clientid to userDescription
@@ -59,20 +58,23 @@ export class PermanentUserData {
ids.forEach(addClientId)
}
// observe users
users.observe(event => {
storeType.observe(event => {
event.keysChanged.forEach(userDescription =>
initUser(users.get(userDescription), userDescription)
initUser(storeType.get(userDescription), userDescription)
)
})
// add intial data
users.forEach(initUser)
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) {
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
const users = this.yusers
let user = users.get(userDescription)
if (!user) {
@@ -108,7 +110,7 @@ export class PermanentUserData {
setTimeout(() => {
const yds = user.get('ds')
const ds = transaction.deleteSet
if (transaction.local && ds.clients.size > 0) {
if (transaction.local && ds.clients.size > 0 && filter(transaction, ds)) {
const encoder = encoding.createEncoder()
writeDeleteSet(encoder, ds)
yds.push([encoding.toUint8Array(encoder)])
@@ -116,6 +118,7 @@ export class PermanentUserData {
})
})
}
/**
* @param {number} clientid
* @return {any}
@@ -123,6 +126,7 @@ export class PermanentUserData {
getUserByClientId (clientid) {
return this.clients.get(clientid) || null
}
/**
* @param {ID} id
* @return {string | null}

View File

@@ -63,7 +63,7 @@ export class RelativePosition {
}
/**
* @param {Object} json
* @param {any} json
* @return {RelativePosition}
*
* @function

View File

@@ -28,13 +28,11 @@ export class Snapshot {
constructor (ds, sv) {
/**
* @type {DeleteSet}
* @private
*/
this.ds = ds
/**
* State Map
* @type {Map<number,number>}
* @private
*/
this.sv = sv
}

View File

@@ -13,7 +13,6 @@ export class StructStore {
constructor () {
/**
* @type {Map<number,Array<GC|Item>>}
* @private
*/
this.clients = new Map()
/**
@@ -23,19 +22,16 @@ export class StructStore {
* slow in Chrome for arrays with more than 100k elements
* @see tryResumePendingStructRefs
* @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
* @private
*/
this.pendingClientsStructRefs = new Map()
/**
* Stack of pending structs waiting for struct dependencies
* Maximum length of stack is structReaders.size
* @type {Array<GCRef|ItemRef>}
* @private
*/
this.pendingStack = []
/**
* @type {Array<decoding.Decoder>}
* @private
*/
this.pendingDeleteReaders = []
}
@@ -185,7 +181,7 @@ export const getItem = (store, id) => find(store, id)
*/
export const findIndexCleanStart = (transaction, structs, clock) => {
const index = findIndexSS(structs, clock)
let struct = structs[index]
const struct = structs[index]
if (struct.id.clock < clock && struct instanceof Item) {
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
return index + 1

View File

@@ -10,13 +10,15 @@ import {
findIndexSS,
callEventHandlerListeners,
Item,
ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
generateNewClientId,
StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as map from 'lib0/map.js'
import * as math from 'lib0/math.js'
import * as set from 'lib0/set.js'
import * as logging from 'lib0/logging.js'
import { callAll } from 'lib0/function.js'
/**
@@ -85,7 +87,6 @@ export class Transaction {
this.changedParentTypes = new Map()
/**
* @type {Set<ID>}
* @private
*/
this._mergeStructs = new Set()
/**
@@ -145,6 +146,85 @@ 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
@@ -201,63 +281,12 @@ const cleanupTransactions = (transactionCleanups, i) => {
})
callAll(fs, [])
} finally {
/**
* @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)
}
}
tryGcDeleteSet(ds, store, doc.gcFilter)
}
tryMergeDeleteSet(ds, store)
// on all affected store.clients props, try to merge
for (const [client, clock] of transaction.afterState) {
@@ -286,6 +315,10 @@ const cleanupTransactions = (transactionCleanups, i) => {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
doc.clientID = generateNewClientId()
logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
}
// @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')) {
@@ -310,7 +343,6 @@ const cleanupTransactions = (transactionCleanups, i) => {
* @param {function(Transaction):void} f
* @param {any} [origin=true]
*
* @private
* @function
*/
export const transact = (doc, f, origin = null, local = true) => {

View File

@@ -199,13 +199,32 @@ export class UndoManager extends Observable {
// make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item)
keepItem(item, true)
}
})
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
* smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next

View File

@@ -181,7 +181,7 @@ export class YEvent {
})
this._changes = changes
}
return changes
return /** @type {any} */ (changes)
}
}

View File

@@ -0,0 +1,19 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js'
/**
* Client id should be changed when an instance receives updates from another client using the same client id.
*
* @param {t.TestCase} tc
*/
export const testClientIdDuplicateChange = tc => {
const doc1 = new Y.Doc()
doc1.clientID = 0
const doc2 = new Y.Doc()
doc2.clientID = 0
t.assert(doc2.clientID === doc1.clientID)
doc1.getArray('a').insert(0, [1, 2])
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))
t.assert(doc2.clientID !== doc1.clientID)
}

View File

@@ -5,6 +5,7 @@ import * as text from './y-text.tests.js'
import * as xml from './y-xml.tests.js'
import * as encoding from './encoding.tests.js'
import * as undoredo from './undo-redo.tests.js'
import * as consistency from './consistency.tests.js'
import { runTests } from 'lib0/testing.js'
import { isBrowser, isNode } from 'lib0/environment.js'
@@ -14,7 +15,7 @@ if (isBrowser) {
log.createVConsole(document.body)
}
runTests({
map, array, text, xml, encoding, undoredo
map, array, text, xml, consistency, encoding, undoredo
}).then(success => {
/* istanbul ignore next */
if (isNode) {

View File

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

View File

@@ -69,11 +69,11 @@ export const testUndoMap = tc => {
const subType = new Y.Map()
map0.set('a', subType)
subType.set('x', 42)
t.compare(map0.toJSON(), /** @type {any} */ ({ 'a': { x: 42 } }))
t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
undoManager.undo()
t.assert(map0.get('a') === 1)
undoManager.redo()
t.compare(map0.toJSON(), /** @type {any} */ ({ 'a': { x: 42 } }))
t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
testConnector.syncAll()
// if content is overwritten by another user, undo operations should be skipped
map1.set('a', 44)

View File

@@ -227,7 +227,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
/**
* @type {Array<Object<string,any>>}
*/
let events = []
const events = []
array0.observe(e => {
events.push(e)
})
@@ -318,7 +318,7 @@ export const testIteratingArrayContainingTypes = tc => {
arr.push([map])
}
let cnt = 0
for (let item of arr) {
for (const item of arr) {
t.assert(item.get('value') === cnt++, 'value is correct')
}
y.destroy()

View File

@@ -66,7 +66,7 @@ export const testGetAndSetOfMapProperty = tc => {
testConnector.flushAllMessages()
for (let user of users) {
for (const user of users) {
const u = user.getMap('map')
t.compare(u.get('stuff'), 'stuffy')
t.assert(u.get('undefined') === undefined, 'undefined')
@@ -108,7 +108,7 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
map0.set('stuff', 'stuffy')
t.compare(map0.get('stuff'), 'stuffy')
testConnector.flushAllMessages()
for (let user of users) {
for (const user of users) {
var u = user.getMap('map')
t.compare(u.get('stuff'), 'stuffy')
}
@@ -123,7 +123,7 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
testConnector.flushAllMessages()
for (let user of users) {
for (const user of users) {
var u = user.getMap('map')
t.compare(u.get('stuff'), 'c1')
}
@@ -139,7 +139,7 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
map1.set('stuff', 'c1')
map1.delete('stuff')
testConnector.flushAllMessages()
for (let user of users) {
for (const user of users) {
var u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
}
@@ -156,7 +156,7 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
testConnector.flushAllMessages()
for (let user of users) {
for (const user of users) {
var u = user.getMap('map')
t.compare(u.get('stuff'), 'c3')
}
@@ -179,7 +179,7 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
map3.set('stuff', 'c3')
map3.delete('stuff')
testConnector.flushAllMessages()
for (let user of users) {
for (const user of users) {
var u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
}
@@ -430,12 +430,12 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
*/
const mapTransactions = [
function set (user, gen) {
let key = prng.oneOf(gen, ['one', 'two'])
const key = prng.oneOf(gen, ['one', 'two'])
var value = prng.utf16String(gen)
user.getMap('map').set(key, value)
},
function setType (user, gen) {
let key = prng.oneOf(gen, ['one', 'two'])
const key = prng.oneOf(gen, ['one', 'two'])
var type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
user.getMap('map').set(key, type)
if (type instanceof Y.Array) {
@@ -445,7 +445,7 @@ const mapTransactions = [
}
},
function _delete (user, gen) {
let key = prng.oneOf(gen, ['one', 'two'])
const key = prng.oneOf(gen, ['one', 'two'])
user.getMap('map').delete(key)
}
]

View File

@@ -149,3 +149,34 @@ export const testSnapshotDeleteAfter = tc => {
const state1 = text0.toDelta(snapshot1)
t.compare(state1, [{ insert: 'abcd' }])
}
/**
* @param {t.TestCase} tc
*/
export const testToJson = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'abc', { bold: true })
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
}
/**
* @param {t.TestCase} tc
*/
export const testToDeltaEmbedAttributes = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 })
const delta0 = text0.toDelta()
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' }, attributes: { width: 100 } }, { insert: 'b', attributes: { bold: true } }])
}
/**
* @param {t.TestCase} tc
*/
export const testToDeltaEmbedNoAttributes = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
text0.insertEmbed(1, { image: 'imageSrc.png' })
const delta0 = text0.toDelta()
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' } }, { insert: 'b', attributes: { bold: true } }], 'toDelta does not set attributes key when no attributes are present')
}

View File

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

View File

@@ -6,15 +6,15 @@
"allowJs": true, /* Allow javascript files to be compiled. */
"checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./build", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "outFile": "./dist/yjs.js", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": true, /* Do not emit outputs. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
@@ -22,6 +22,7 @@
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"emitDeclarationOnly": true,
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
@@ -38,10 +39,7 @@
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": {
"yjs": ["./src/index.js"],
"lib0/*": ["node_modules/lib0/*"],
"lib0/set.js": ["node_modules/lib0/set.js"],
"lib0/function.js": ["node_modules/lib0/function.js"]
"yjs": ["./src/index.js"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
@@ -59,9 +57,8 @@
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"maxNodeModuleJsDepth": 5,
// "maxNodeModuleJsDepth": 0,
// "types": ["./src/utils/typedefs.js"]
},
"include": ["./src/**/*", "./tests/**/*"],
"exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"]
"include": ["./src/**/*.js", "./tests/**/*.js"]
}