Compare commits

...

97 Commits

Author SHA1 Message Date
Kevin Jahns
d6593412a2 13.0.0-84 2019-05-19 21:49:36 +02:00
Kevin Jahns
d31bf36531 use generated esm module by default 2019-05-19 21:48:09 +02:00
Kevin Jahns
a485f550db 13.0.0-83 2019-05-19 20:59:56 +02:00
Kevin Jahns
0610b16227 bump lib0 for webpack compatibility 2019-05-19 20:43:18 +02:00
Kevin Jahns
72e470c5f0 Fix ytext event.delta - items that are synced and deleted
When items are added and deleted in the same transaction, event.delta would recognize them as added (though they are actually deleted). Now it just ignores them.
2019-05-19 20:42:53 +02:00
Kevin Jahns
4d12a02e2f fix offset in state vector 2019-05-16 12:31:53 +02:00
Kevin Jahns
4a7d6f0a2d fix sorting bug that only affects older node versions (probably because old sorting algorithms are not stable) 2019-05-14 15:21:34 +02:00
Kevin Jahns
c80f446b5f README: update provider tutorial 2019-05-12 11:18:43 +02:00
Kevin Jahns
81a529d8dc update *getting started* yjs version 2019-05-07 15:43:09 +02:00
Kevin Jahns
4f0ab78914 13.0.0-82 2019-05-07 13:54:00 +02:00
Kevin Jahns
8c36f67f0b rework and document api 2019-05-07 13:44:23 +02:00
Kevin Jahns
77687d94e6 13.0.0-81 2019-04-28 17:32:05 +02:00
Kevin Jahns
4644511303 bump y-protocols dependency 2019-04-28 17:30:52 +02:00
Kevin Jahns
20005eecdb Merge deleted items more efficiently.
Previously deleted items were simply added to transaction._mergeStructs. But this inherently inefficient as it will splice the struct store for every item.

Now Yjs iterates over transaction.ds and tries to merge structs. It iterates from right to left so merging should be more efficient that before. But more work needs to be done.

For example we could set structs[i] = null and filter the structs after merging is done.
2019-04-28 17:20:35 +02:00
Kevin Jahns
c9dda245bf v13 api docs 2019-04-28 02:53:25 +02:00
Kevin Jahns
1417470156 update demos link 2019-04-27 03:44:48 +02:00
Kevin Jahns
584e5dfd40 Link to v13 docs from README 2019-04-27 03:35:44 +02:00
Kevin Jahns
805acbb9f5 13.0.0-80 2019-04-26 19:55:14 +02:00
Kevin Jahns
32c4c09072 update parent._map when splitting an item 2019-04-26 19:54:00 +02:00
Kevin Jahns
8c5a06bbf8 fix gc when item is deleted in observer call 2019-04-26 18:37:38 +02:00
Kevin Jahns
a336cc167c order observer and transaction cleanups after one another 2019-04-26 13:31:00 +02:00
Kevin Jahns
21d86cd2be Delete all children of ItemType when it is deleted 2019-04-26 12:29:28 +02:00
Kevin Jahns
1d0f9faa91 AbstractItem.mergeWith helper outsourced into separate function 2019-04-24 18:10:33 +02:00
Kevin Jahns
45237571b7 gc more efficiently 2019-04-23 20:51:32 +02:00
Kevin Jahns
bb6f6cd141 13.0.0-79 2019-04-20 00:03:30 +02:00
Kevin Jahns
729c1f16b8 fix test provider 2019-04-20 00:02:40 +02:00
Kevin Jahns
b6059704aa update dependencies 2019-04-20 00:00:09 +02:00
Kevin Jahns
fa3c92f44c change parameter order of transaction events 2019-04-19 23:36:00 +02:00
Kevin Jahns
cd82de7742 lint 2019-04-12 20:08:38 +02:00
Kevin Jahns
07a6a0044b simplify exposed APi 2019-04-12 20:04:07 +02:00
Kevin Jahns
4582832a71 rework intro 2019-04-12 14:24:31 +02:00
Kevin Jahns
07ac1d03e3 fix jsdoc 2019-04-11 23:34:56 +02:00
Kevin Jahns
cbcf1facb8 remove todo.md 2019-04-11 17:35:09 +02:00
Kevin Jahns
31ff7ac78c improve jsdoc comments 2019-04-11 13:22:59 +02:00
Kevin Jahns
ed3b31e58f jsdoc fixes 2019-04-11 00:49:07 +02:00
Kevin Jahns
759ecb21f7 rename transaction._replacedItems to transaction._mergeStructs 2019-04-11 00:31:43 +02:00
Kevin Jahns
9c29d820c8 rename AbstractRef to AbstractStructRef 2019-04-11 00:26:42 +02:00
Kevin Jahns
2ef11a5344 splitting an item must always happen inside a transaction, because we always need to check if we can merge it back 2019-04-11 00:23:08 +02:00
Kevin Jahns
9fe47e98d5 type._map points to the last element instead to enable merging of deletes in Map 2019-04-10 21:01:59 +02:00
Kevin Jahns
654510f3ff read struct refs as array 2019-04-10 18:52:22 +02:00
Kevin Jahns
52ec698635 implement some of the commented todos 2019-04-09 04:01:37 +02:00
Kevin Jahns
1b06f59d1c fixed remaining tests 2019-04-09 00:48:24 +02:00
Kevin Jahns
12bcc4d080 fix remaining random tests 2019-04-09 00:31:28 +02:00
Kevin Jahns
e1a9f314a7 fixed part of split/merge logic 2019-04-08 13:41:28 +02:00
Kevin Jahns
7a111de186 refactor read/write of structs 2019-04-07 23:08:08 +02:00
Kevin Jahns
90b3fa9dd9 fixed merging and adapted writeStructs to write end of message 2019-04-07 12:47:04 +02:00
Kevin Jahns
c635963747 Compare origin ids in item.integrate 2019-04-06 15:55:20 +02:00
Kevin Jahns
1b17b5e400 fixed 10 tests 2019-04-06 13:00:32 +02:00
Kevin Jahns
61d9d96d15 fix replace with delete 2019-04-05 21:06:43 +02:00
Kevin Jahns
7d0c048708 Items accept origins as IDs 2019-04-05 19:46:18 +02:00
Kevin Jahns
8a7416ad50 Create Structs based on offset, if necessary
implement offset parameter in Ref.toStruct
2019-04-05 12:38:02 +02:00
Kevin Jahns
e56899a02c after refactor - some tests are working again 2019-04-05 00:37:09 +02:00
Kevin Jahns
30bf3742c9 add internals file and use it to organize imports 2019-04-04 19:35:38 +02:00
Kevin Jahns
8dbd2c4696 restructure EventHandler 2019-04-04 13:50:00 +02:00
Kevin Jahns
6578727c9c fixed all type issues 2019-04-03 13:23:27 +02:00
Kevin Jahns
92ca001cdc implement getMap, getArray, getXml, .. 2019-04-03 03:08:10 +02:00
Kevin Jahns
415de1cc4c all YArray.tests type fixes 2019-04-03 02:30:44 +02:00
Kevin Jahns
e23582b1cd more type fixes and rethinking writeStructs 2019-04-02 23:08:58 +02:00
Kevin Jahns
73c28952c2 fix all types but yxmlelement 2019-03-30 11:00:54 +01:00
Kevin Jahns
1bc1e88d6a fix y-text 2019-03-30 01:08:09 +01:00
Kevin Jahns
c188f813a4 fixed YMap 2019-03-29 13:49:13 +01:00
Kevin Jahns
ff981a8697 fixed YArray 2019-03-29 01:03:02 +01:00
Kevin Jahns
d9ab593b07 prelim refactor commit 2019-03-26 01:14:15 +01:00
Kevin Jahns
293527e62b fix a few tsc errors (96 remaining) 2019-03-13 02:15:43 +01:00
Kevin Jahns
5a42a94cf4 add typescript to lint script 2019-03-13 01:49:51 +01:00
Kevin Jahns
040808300c clean up build script - no more warnings 2019-03-13 01:16:31 +01:00
Kevin Jahns
57975d409e cleanup dependencies 2019-03-13 00:22:38 +01:00
Kevin Jahns
306b2c64f3 Merge branch 'master' of https://github.com/y-js/yjs 2019-03-13 00:04:42 +01:00
Kevin Jahns
585265e9a5 refactor and remove dependency circles 2019-03-13 00:04:19 +01:00
Kevin Jahns
777ae9503a Merge pull request #142 from mtn/mtn-patch-1
Correct typo in README example
2019-03-12 03:36:51 +01:00
Kevin Jahns
4c1798e5fa fix all remaining tests (xml tests) 2019-03-12 01:42:51 +01:00
Kevin Jahns
f4d85e2a3e fix y-text tests 2019-03-12 01:22:06 +01:00
Kevin Jahns
a0f0c9c377 testing: use lib0.testing.compare to compare Maps and sets 2019-03-11 18:34:50 +01:00
Kevin Jahns
95ec2a435a fix remaining y-map tests 2019-03-11 17:52:51 +01:00
Kevin Jahns
da9836fe59 added all y-map tests 2019-03-11 12:31:37 +01:00
Kevin Jahns
3a7411f9e8 reworked some ymap tests (a few are running again) 2019-03-11 00:00:41 +01:00
Kevin Jahns
39cee7c6e7 refix array tests and switch to lib0 2019-03-10 23:26:53 +01:00
Kevin Jahns
0a5753c191 decode items before they are decoded. fixes lots of y-array tests 2019-03-07 18:57:39 +01:00
Kevin Jahns
76b7d0b651 fixed some issues in random tests 2019-03-06 13:29:16 +01:00
Kevin Jahns
99e3e95a00 added remaining y-array tests (random still failing) 2019-03-05 14:00:31 +01:00
Kevin Jahns
93ee4ee287 converted first y-array test to funlib/testing 2019-03-04 14:28:18 +01:00
Kevin Jahns
c5cc403a29 update test commands 2019-03-01 23:45:09 +01:00
Kevin Jahns
75f4a0a5f0 restructuring the project 2019-03-01 23:28:11 +01:00
Michael Noronha
591df5c00a Correct typo in README example
/bower_components -> ./bower_components
2019-02-23 17:30:05 -06:00
Kevin Jahns
f6b4819ae3 prosemirror: implement isChangeOrigin in state 2019-01-31 09:50:52 +01:00
Kevin Jahns
d483d9cc83 13.0.0-78 2019-01-29 01:38:40 +01:00
Kevin Jahns
453407b93d fix connection status and awareness info when disconnected (ws-provider) 2019-01-29 01:38:23 +01:00
Kevin Jahns
e699f92333 13.0.0-77 2019-01-29 00:56:15 +01:00
Kevin Jahns
6ff47719ef Merge branch 'master' of github.com:y-js/yjs 2019-01-29 00:55:22 +01:00
Kevin Jahns
3a0694c35c added utilities to make and recover snapshots 2019-01-29 00:54:58 +01:00
Kevin Jahns
74e5243742 Merge pull request #138 from calibr/yjs
updating YArray's iterator to iterate Types correctly
2019-01-23 11:00:37 +01:00
calibr
dcf43b9797 switch to the next item in YArray's iterator after processing a Type item 2019-01-16 03:12:58 +03:00
Kevin Jahns
77e479c03b working on snapshotting and version history 2019-01-09 23:54:36 +01:00
Kevin Jahns
ec58a99748 add clock vector to awareness protocol 2018-12-22 15:51:09 +01:00
Kevin Jahns
f1eb66655b implemented leveldb persistence for websocket server 2018-12-22 13:45:59 +01:00
Kevin Jahns
7f4ae9fe14 implemented codemirror binding with cursor support 2018-12-21 13:51:38 +01:00
Kevin Jahns
c0ba56a21f update v13 docs 2018-12-19 01:12:29 +01:00
190 changed files with 10854 additions and 17857 deletions

View File

@@ -1,11 +0,0 @@
{
"source": ".",
"destination": "./docs",
"excludes": ["build", "node_modules", "tests-lib", "test"],
"plugins": [{
"name": "esdoc-standard-plugin",
"option": {
"accessor": {"access": ["public"], "autoPrivate": true}
}
}]
}

8
.gitignore vendored
View File

@@ -1,8 +1,4 @@
node_modules
bower_components
docs
/y.*
/examples_all/*/index.dist.*
dist
.vscode
.yjsPersisted
build
docs

View File

@@ -5,7 +5,7 @@
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["./types", "./utils/UndoManager.js", "./utils/Y.js", "./provider", "./bindings"],
"include": ["./src"],
"includePattern": ".js$"
},
"plugins": [

View File

@@ -6,6 +6,8 @@ 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.v13.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
@@ -131,7 +133,7 @@ Here is a simple example of a shared textarea
// name: 'xmpp'
room: 'my-room' // clients connecting to the same room share data
},
sourceDir: '/bower_components', // location of the y-* modules (browser only)
sourceDir: './bower_components', // location of the y-* modules (browser only)
share: {
textarea: 'Text' // y.share.textarea is of type y-text
}
@@ -294,12 +296,5 @@ DEBUG_COLORS=0 DEBUG=y* node app.js > log
localStorage.debug = 'y*'
```
## Contribution
I created this framework during my bachelor thesis at the chair of computer
science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since
December 2014 I'm working on Yjs as a part of my student worker job at the i5.
## License
Yjs is licensed under the [MIT License](./LICENSE).
<yjs@dbis.rwth-aachen.de>

View File

@@ -1,21 +1,557 @@
# ![Yjs](https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png)
> A CRDT library with a powerful abstraction of shared data
> A CRDT framework with a powerful abstraction of shared data
Yjs v13 is a work in progress.
Yjs is a [CRDT implementation](#Yjs-CRDT-Algorithm) that exposes its internal data structure as *shared types*. Shared types are common data types like `Map` or `Array` with superpowers: changes are automatically distributed to other peers and merged without merge conflicts.
Yjs is **network agnostic** (p2p!), supports many existing **rich text editors**, **offline editing**, **version snapshots**, **undo/redo** and **shared cursors**. It scales well with an unlimited number of users and is well suited for even large documents.
* Chat: [https://gitter.im/y-js/yjs](https://gitter.im/y-js/yjs)
* Demos: [https://github.com/y-js/yjs-demos](https://github.com/y-js/yjs-demos)
* Benchmarks: [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
# Table of Contents
* [Overview](#Overview)
* [Bindings](#Bindings)
* [Providers](#Providers)
* [Getting Started](#Getting-Started)
* [API](#API)
* [Shared Types](#Shared-Types)
* [Y.Doc](#Y.Doc)
* [Document Updates](#Document-Updates)
* [Relative Positions](#Relative-Positions)
* [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
This repository contains a collection of shared types that can be observed for changes and manipulated concurrently. Network functionality and two-way-bindings are implemented in separate modules.
### Bindings
| Name &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; | Cursors | Binding | Demo |
|---|:-:|---|---|
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/y-js/y-prosemirror) | [link](https://yjs.website/tutorial-prosemirror.html) |
| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/y-js/y-quill) | [link](https://yjs.website/tutorial-quill.html) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/y-js/y-codemirror) | [link](https://yjs.website/tutorial-codemirror.html) |
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/y-js/y-ace) | [link]() |
| [Monaco](https://microsoft.github.io/monaco-editor/) | | [y-monaco](http://github.com/y-js/y-monaco) | [link]() |
### Providers
Setting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. **Providers** manage all that for you and are the perfect starting point for your collaborative app.
<dl>
<dt><a href="http://github.com/y-js/y-websocket">y-websocket</a></dt>
<dd>A module that contains a simple websocket backend and a websocket client that connects to that backend. The backend can be extended to persist updates in a leveldb database.</dd>
<dt><a href="http://github.com/y-js/y-mesh">y-mesh</a></dt>
<dd>[WIP] Creates a connected graph of webrtc connections with a high <a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It requires a signalling server that connects a client to the first peer. But after that the network manages itself. It is well suited for large and small networks.</dd>
<dt><a href="http://github.com/y-js/y-dat">y-dat</a></dt>
<dd>[WIP] Write document updates effinciently to the dat network using <a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has an append-only log of CRDT local updates (hypercore). Multifeed manages and sync hypercores and y-dat listens to changes and applies them to the Yjs document.</dd>
</dl>
## Getting Started
Install Yjs and a provider with your favorite package manager.
```sh
npm i yjs@13.0.0-82 y-websocket@1.0.0-3 y-textarea
```
**Start the y-websocket server**
```sh
PORT=1234 node ./node_modules/y-websocket/bin/server.js
```
**Example: Textarea Binding**
This is a complete example on how to create a connection to a [y-websocket](https://github.com/y-js/y-websocket) server instance, sync the shared document to all clients in a *room*, and bind a Y.Text type to a dom textarea. All changes to the textarea are automatically shared with everyone in the same room.
```js
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { TextareaBinding } from 'y-textarea'
const doc = Y.Doc()
const provider = new WebsocketProvider('http://localhost:1234', 'roomname')
// sync all document updates through the websocket connection
provider.sync('doc')
// Define a shared type on the document.
const ytext = doc.getText('my resume')
// use data bindings to bind types to editors
const binding = new TextareaBinding(ytext, document.querySelector('textarea'))
```
**Example: Observe types**
```js
const yarray = doc.getArray('my-array')
yarray.observe(event => {
console.log('yarray was modified')
})
// every time a local or remote client modifies yarray, the observer is called
yarray.insert(0, ['val']) // => "yarray was modified"
```
**Example: Nest types**
Remember, shared types are just plain old data types. The only limitation is that a shared type must exist only once in the shared document.
```js
const ymap = doc.getMap('map')
const foodArray = new Y.Array()
foodArray.insert(0, ['apple', 'banana'])
ymap.set('food', foodArray)
ymap.get('food') === foodArray // => true
ymap.set('fruit', foodArray) // => Error! foodArray is already defined on the shared document
```
Now you understand how types are defined on a shared document. Next you can jump to the [demo repository](https://github.com/y-js/yjs-demos) or continue reading the API docs.
## API
```js
import * as Y from 'yjs'
```
### Shared Types
<details>
<summary><b>Y.Array</b></summary>
<br>
<p>
A shareable Array-like type that supports efficient insert/delete of elements at any position. Internally it uses a linked list of Arrays that is split when necessary.
</p>
<pre>const yarray = new Y.Array()</pre>
<dl>
<b><code>insert(index:number, content:Array&lt;object|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd>
Insert content at <var>index</var>. Note that content is an array of elements. I.e. <code>array.insert(0, [1]</code> splices the list and inserts 1 at position 0.
</dd>
<b><code>push(Array&lt;Object|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd></dd>
<b><code>delete(index:number, length:number)</code></b>
<dd></dd>
<b><code>get(index:number)</code></b>
<dd></dd>
<b><code>length:number</code></b>
<dd></dd>
<b><code>map(function(T, number, YArray):M):Array&lt;M&gt;</code></b>
<dd></dd>
<b><code>toArray():Array&lt;Object|Array|string|number|Uint8Array|Y.Type&gt;</code></b>
<dd>Copies the content of this YArray to a new Array.</dd>
<b><code>toJSON():Array&lt;Object|Array|string|number&gt;</code></b>
<dd>Copies the content of this YArray to a new Array. It transforms all child types to JSON using their <code>toJSON</code> method.</dd>
<b><code>[Symbol.Iterator]</code></b>
<dd>
Returns an YArray Iterator that contains the values for each index in the array.
<pre>for (let value of yarray) { .. }</pre>
</dd>
<b><code>observe(function(YArrayEvent, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
</dd>
<b><code>unobserve(function(YArrayEvent, Transaction):void)</code></b>
<dd>
Removes an <code>observe</code> event listener from this type.
</dd>
<b><code>observeDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
</dd>
<b><code>unobserveDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Removes an <code>observeDeep</code> event listener from this type.
</dd>
</dl>
</details>
<details>
<summary><b>Y.Map</b></summary>
<br>
<p>
A shareable Map type.
</p>
<pre><code>const ymap = new Y.Map()</code></pre>
<dl>
<b><code>get(key:string):object|string|number|Uint8Array|Y.Type</code></b>
<dd></dd>
<b><code>set(key:string, value:object|string|number|Uint8Array|Y.Type)</code></b>
<dd></dd>
<b><code>delete(key:string)</code></b>
<dd></dd>
<b><code>has(key:string):boolean</code></b>
<dd></dd>
<b><code>get(index:number)</code></b>
<dd></dd>
<b><code>toJSON():Object&lt;string, Object|Array|string|number&gt;</code></b>
<dd>Copies the <code>[key,value]</code> pairs of this YMap to a new Object. It transforms all child types to JSON using their <code>toJSON</code> method.</dd>
<b><code>[Symbol.Iterator]</code></b>
<dd>
Returns an Iterator of <code>[key, value]</code> pairs.
<pre>for (let [key, value] of ymap) { .. }</pre>
</dd>
<b><code>entries()</code></b>
<dd>
Returns an Iterator of <code>[key, value]</code> pairs.
</dd>
<b><code>values()</code></b>
<dd>
Returns an Iterator of all values.
</dd>
<b><code>keys()</code></b>
<dd>
Returns an Iterator of all keys.
</dd>
<b><code>observe(function(YMapEvent, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
</dd>
<b><code>unobserve(function(YMapEvent, Transaction):void)</code></b>
<dd>
Removes an <code>observe</code> event listener from this type.
</dd>
<b><code>observeDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
</dd>
<b><code>unobserveDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Removes an <code>observeDeep</code> event listener from this type.
</dd>
</dl>
</details>
<details>
<summary><b>Y.Text</b></summary>
<br>
<p>
A shareable type that is optimized for shared editing on text. It allows to assign properties to ranges in the text. This makes it possible to implement rich-text bindings to this type.
</p>
<p>
This type can also be transformed to the <a href="https://quilljs.com/docs/delta">delta format</a>. Similarly the YTextEvents compute changes as deltas.
</p>
<pre>const ytext = new Y.Text()</pre>
<dl>
<b><code>insert(index:number, content:string, [formattingAttributes:Object&lt;string,string&gt;])</code></b>
<dd>
Insert a string at <var>index</var> and assign formatting attributes to it.
<pre>ytext.insert(0, 'bold text', { bold: true })</pre>
</dd>
<b><code>delete(index:number, length:number)</code></b>
<dd></dd>
<b><code>format(index:number, length:number, formattingAttributes:Object&lt;string,string&gt;)</code></b>
<dd>Assign formatting attributes to a range in the text</dd>
<b><code>applyDelta(delta)</code></b>
<dd>See <a href="https://quilljs.com/docs/delta/">Quill Delta</a></dd>
<b><code>length:number</code></b>
<dd></dd>
<b><code>toString():string</code></b>
<dd>Transforms this type, without formatting options, into a string.</dd>
<b><code>toJSON():string</code></b>
<dd>See <code>toString</code></dd>
<b><code>toDelta():Delta</code></b>
<dd>Transforms this type to a <a href="https://quilljs.com/docs/delta/">Quill Delta</a></dd>
<b><code>observe(function(YTextEvent, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
</dd>
<b><code>unobserve(function(YTextEvent, Transaction):void)</code></b>
<dd>
Removes an <code>observe</code> event listener from this type.
</dd>
<b><code>observeDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
</dd>
<b><code>unobserveDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Removes an <code>observeDeep</code> event listener from this type.
</dd>
</dl>
</details>
<details>
<summary><b>YXmlFragment</b></summary>
<br>
<p>
A container that holds an Array of Y.XmlElements.
</p>
<pre><code>const yxml = new Y.XmlFragment()</code></pre>
<dl>
<b><code>insert(index:number, content:Array&lt;Y.XmlElement|Y.XmlText&gt;)</code></b>
<dd></dd>
<b><code>delete(index:number, length:number)</code></b>
<dd></dd>
<b><code>get(index:number)</code></b>
<dd></dd>
<b><code>length:number</code></b>
<dd></dd>
<b><code>toArray():Array&lt;Y.XmlElement|Y.XmlText&gt;</code></b>
<dd>Copies the children to a new Array.</dd>
<b><code>toDOM():DocumentFragment</code></b>
<dd>Transforms this type and all children to new DOM elements.</dd>
<b><code>toString():string</code></b>
<dd>Get the XML serialization of all descendants.</dd>
<b><code>toJSON():string</code></b>
<dd>See <code>toString</code>.</dd>
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
</dd>
<b><code>unobserve(function(YXmlEvent, Transaction):void)</code></b>
<dd>
Removes an <code>observe</code> event listener from this type.
</dd>
<b><code>observeDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
</dd>
<b><code>unobserveDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Removes an <code>observeDeep</code> event listener from this type.
</dd>
</dl>
</details>
<details>
<summary><b>Y.XmlElement</b></summary>
<br>
<p>
A shareable type that represents an XML Element. It has a <code>nodeName</code>, attributes, and a list of children. But it makes no effort to validate its content and be actually XML compliant.
</p>
<pre><code>const yxml = new Y.XmlElement()</code></pre>
<dl>
<b><code>insert(index:number, content:Array&lt;Y.XmlElement|Y.XmlText&gt;)</code></b>
<dd></dd>
<b><code>delete(index:number, length:number)</code></b>
<dd></dd>
<b><code>get(index:number)</code></b>
<dd></dd>
<b><code>length:number</code></b>
<dd></dd>
<b><code>setAttribute(attributeName:string, attributeValue:string)</code></b>
<dd></dd>
<b><code>removeAttribute(attributeName:string)</code></b>
<dd></dd>
<b><code>getAttribute(attributeName:string):string</code></b>
<dd></dd>
<b><code>getAttributes(attributeName:string):Object&lt;string,string&gt;</code></b>
<dd></dd>
<b><code>toArray():Array&lt;Y.XmlElement|Y.XmlText&gt;</code></b>
<dd>Copies the children to a new Array.</dd>
<b><code>toDOM():Element</code></b>
<dd>Transforms this type and all children to a new DOM element.</dd>
<b><code>toString():string</code></b>
<dd>Get the XML serialization of all descendants.</dd>
<b><code>toJSON():string</code></b>
<dd>See <code>toString</code>.</dd>
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
</dd>
<b><code>unobserve(function(YXmlEvent, Transaction):void)</code></b>
<dd>
Removes an <code>observe</code> event listener from this type.
</dd>
<b><code>observeDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
</dd>
<b><code>unobserveDeep(function(Array&lt;YEvent&gt;, Transaction):void)</code></b>
<dd>
Removes an <code>observeDeep</code> event listener from this type.
</dd>
</dl>
</details>
### Y.Doc
```js
const doc = new Y.Doc()
```
<dl>
<b><code>clientID</code></b>
<dd>A unique id that identifies this client. (readonly)</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 the <code>update</code> event are called after each transaction. You should <i>bundle</i> changes into a single transaction to reduce the amount of event calls. I.e. <code>doc.transact(() => { yarray.insert(..); ymap.set(..) })</code> triggers a single change event. <br>You can specify an optional <code>origin</code> parameter that is stored on <code>transaction.origin</code> and <code>on('update', (update, origin) => ..)</code>.</dd>
<b><code>get(string, Y.[TypeClass]):[Type]</code></b>
<dd>Define a shared type.</dd>
<b><code>getArray(string):Y.Array</code></b>
<dd>Define a shared Y.Array type. Is equivalent to <code>y.get(string, Y.Array)</code>.</dd>
<b><code>getMap(string):Y.Map</code></b>
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
<b><code>getXmlFragment(string):Y.XmlFragment</code></b>
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
<b><code>on(string, function)</code></b>
<dd>Register an event listener on the shared type</dd>
<b><code>off(string, function)</code></b>
<dd>Unregister an event listener from the shared type</dd>
</dl>
#### Y.Doc Events
<dl>
<b><code>on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)</code></b>
<dd>Listen to document updates. Document updates must be transmitted to all other peers. You can apply document updates in any order and multiple times.</dd>
<b><code>on('beforeTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
<dd>Emitted before each transaction.</dd>
<b><code>on('afterTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
<dd>Emitted after each transaction.</dd>
</dl>
### Document Updates
Changes on the shared document are encoded into *document updates*. Document updates are *commutative* and *idempotent*. This means that they can be applied in any order and multiple times.
**Example: Listen to update events and apply them on remote client**
```js
const doc1 = new Y.Doc()
const doc2 = new Y.Doc()
doc1.on('update', update => {
Y.applyUpdate(doc2, update)
})
doc2.on('update', update => {
Y.applyUpdate(doc1, update)
})
// All changes are also applied to the other document
doc1.getArray('myarray').insert(0, ['Hello doc2, you got this?'])
doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?'
```
Yjs internally maintains a [state vector](#State-Vector) that denotes the next expected clock from each client. In a different interpretation it holds the number of structs created by each client. When two clients sync, you can either exchange the complete document structure or only the differences by sending the state vector to compute the differences.
**Example: Sync two clients by exchanging the complete document structure**
```js
const state1 = Y.encodeStateAsUpdate(ydoc1)
const state2 = Y.encodeStateAsUpdate(ydoc2)
Y.applyUpdate(ydoc1, state2)
Y.applyUpdate(ydoc2, state1)
```
**Example: Sync two clients by computing the differences**
This example shows how to sync two clients with the minimal amount of exchanged data by computing only the differences using the state vector of the remote client. Syncing clients using the state vector requires another roundtrip, but can safe a lot of bandwidth.
```js
const stateVector1 = Y.encodeStateVector(ydoc1)
const stateVector2 = Y.encodeStateVector(ydoc2)
const diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2)
const diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1)
Y.applyUpdate(ydoc1, diff2)
Y.applyUpdate(ydoc2, diff1)
```
<dl>
<b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b>
<dd>Apply a document update on the shared document. Optionally you can specify <code>transactionOrigin</code> that will be stored on <code>transaction.origin</code> and <code>ydoc.on('update', (update, origin) => ..)</code>.</dd>
<b><code>Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array</code></b>
<dd>Encode the document state as a single update message that can be applied on the remote document. Optionally specify the target state vector to only write the differences to the update message.</dd>
<b><code>Y.encodeStateVector(Y.Doc):Uint8Array</code></b>
<dd>Computes the state vector and encodes it into an Uint8Array.</dd>
</dl>
### Relative Positions
> This API is not stable yet
This feature is intended for managing selections / cursors. When working with other users that manipulate the shared document, you can't trust that an index position (an integer) will stay at the intended location. A *relative position* is fixated to an element in the shared document and is not affected by remote changes. I.e. given the document `"a|c"`, the relative position is attached to `c`. When a remote user modifies the document by inserting a character before the cursor, the cursor will stay attached to the character `c`. `insert(1, 'x')("a|c") = "ax|c"`. When the *relative position* is set to the end of the document, it will stay attached to the end of the document.
**Example: Transform to RelativePosition and back**
```js
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
const pos = Y.createAbsolutePositionFromRelativePosition(relPos, doc)
pos.type === ytext // => true
pos.index === 2 // => true
```
**Example: Send relative position to remote client (json)**
```js
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
const encodedRelPos = JSON.stringify(relPos)
// send encodedRelPos to remote client..
const parsedRelPos = JSON.parse(encodedRelPos)
const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)
pos.type === remoteytext // => true
pos.index === 2 // => true
```
**Example: Send relative position to remote client (Uint8Array)**
```js
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
const encodedRelPos = Y.encodeRelativePosition(relPos)
// send encodedRelPos to remote client..
const parsedRelPos = Y.decodeRelativePosition(encodedRelPos)
const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)
pos.type === remoteytext // => true
pos.index === 2 // => true
```
<dl>
<b><code>Y.createRelativePositionFromTypeIndex(Uint8Array|Y.Type, number)</code></b>
<dd></dd>
<b><code>Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc)</code></b>
<dd></dd>
<b><code>Y.encodeRelativePosition(RelativePosition):Uint8Array</code></b>
<dd></dd>
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
<dd></dd>
</dl>
## Miscellaneous
### Typescript Declarations
Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the only way to get type declarations is by adding Yjs to the list of checked files:
Yjs has type descriptions. But until [this ticket](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, this is how you can make use of Yjs type declarations.
```json
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
..
},
"include": [
"./node_modules/yjs/"
]
"maxNodeModuleJsDepth": 5
}
```
## Yjs CRDT Algorithm
*Conflict-free replicated data types* (CRDT) for collaborative editing are an alternative approach to *operational transformation* (OT). A very simple differenciation between the two approaches is that OT attempts to transform index positions to ensure convergence (all clients end up with the same content), while CRDTs use mathematical models that usually do not involve index transformations, like linked lists. OT is currently the de-facto standard for shared editing on text. OT approaches that support shared editing without a central source of truth (a central server) require too much bookkeeping to be viable in practice. CRDTs are better suited for distributed systems, provide additional guarantees that the document can be synced with remote clients, and do not require a central source of truth.
Yjs implements a modified version of the algorithm described in [this paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types). I will eventually publish a paper that describes why this approach works so well in practice. Note: Since operations make up the document structure, we prefer the term *struct* now.
CRDTs suitable for shared text editing suffer from the fact that they only grow in size. There are CRDTs that do not grow in size, but they do not have the characteristics that are benificial for shared text editing (like intention preservation). Yjs implements many improvements to the original algorithm that diminish the trade-off that the document only grows in size. We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs. But we can 1. merge preceeding structs into a single struct to reduce the amount of meta information, 2. we can delete content from the struct if it is deleted, and 3. we can garbage collect tombstones if we don't care about the order of the structs anymore (e.g. if the parent was deleted).
**Examples:**
1. If a user inserts elements in sequence, the struct will be merged into a single struct. E.g. `array.insert(0, ['a']), array.insert(0, ['b']);` is first represented as two structs (`[{id: {client, clock: 0}, content: 'a'}, {id: {client, clock: 1}, content: 'b'}`) and then merged into a single struct: `[{id: {client, clock: 0}, content: 'ab'}]`.
2. When a struct that contains content (e.g. `ItemString`) is deleted, the struct will be replaced with an `ItemDeleted` that does not contain content anymore.
3. When a type is deleted, all child elements are transformed to `GC` structs. A `GC` struct only denotes the existence of a struct and that it is deleted. `GC` structs can always be merged with other `GC` structs if the id's are adjacent.
Especially when working on structured content (e.g. shared editing on ProseMirror), these improvements yield very good results when [benchmarking](https://github.com/dmonad/crdt-benchmarks) random document edits. In practice they show even better results, because users usually edit text in sequence, resulting in structs that can easily be merged. The benchmarks show that even in the worst case scenario that a user edits text from right to left, Yjs achieves good performance even for huge documents.
#### State Vector
Yjs has the ability to exchange only the differences when syncing two clients. We use lamport timestamps to identify structs and to track in which order a client created them. Each struct has an `struct.id = { client: number, clock: number}` that uniquely identifies a struct. We define the next expected `clock` by each client as the *state vector*. This data structure is similar to the [version vectors](https://en.wikipedia.org/wiki/Version_vector) data structure. But we use state vectors only to describe the state of the local document, so we can compute the missing struct of the remote client. We do not use it to track causality.
## License and Author
Yjs and all related projects are [**MIT licensed**](./LICENSE).
Yjs is based on my research as a student at the [RWTH i5](http://dbis.rwth-aachen.de/). Now I am working on Yjs in my spare time.
Fund this project by donating on [Patreon](https://www.patreon.com/dmonad) or hiring [me](https://github.com/dmonad) for professional support.

View File

@@ -1 +0,0 @@
export * from './dom/DomBinding.js'

View File

@@ -1,248 +0,0 @@
/**
* @module bindings/dom
*/
/* global MutationObserver, getSelection */
import { fromRelativePosition } from '../../utils/relativePosition.js'
import { createMutex } from '../../lib/mutex.js'
import { createAssociation, removeAssociation } from './util.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
import { defaultFilter, applyFilterOnType } from './filter.js'
import { typeObserver } from './typeObserver.js'
import { domObserver } from './domObserver.js'
import { YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
/**
* @callback DomFilter
* @param {string} nodeName
* @param {Map<string, string>} attrs
* @return {Map | null}
*/
/**
* A binding that binds the children of a YXmlFragment to a DOM element.
*
* This binding is automatically destroyed when its parent is deleted.
*
* @example
* const div = document.createElement('div')
* const type = y.define('xml', Y.XmlFragment)
* const binding = new Y.QuillBinding(type, div)
*
* @class
*/
export class DomBinding {
/**
* @param {YXmlFragment} type The bind source. This is the ultimate source of
* truth.
* @param {Element} target The bind target. Mirrors the target.
* @param {Object} [opts] Optional configurations
* @param {DomFilter} [opts.filter=defaultFilter] The filter function to use.
* @param {Document} [opts.document=document] The filter function to use.
* @param {Object} [opts.hooks] The filter function to use.
* @param {Element} [opts.scrollingElement=null] The filter function to use.
*/
constructor (type, target, opts = {}) {
// Binding handles textType as this.type and domTextarea as this.target
/**
* The Yjs type that is bound to `target`
* @type {YXmlFragment}
*/
this.type = type
/**
* The target that `type` is bound to.
* @type {Element}
*/
this.target = target
/**
* @private
*/
this._mutualExclude = createMutex()
this.opts = opts
opts.document = opts.document || document
opts.hooks = opts.hooks || {}
this.scrollingElement = opts.scrollingElement || null
/**
* Maps each DOM element to the type that it is associated with.
* @type {Map}
*/
this.domToType = new Map()
/**
* Maps each YXml type to the DOM element that it is associated with.
* @type {Map}
*/
this.typeToDom = new Map()
/**
* Defines which DOM attributes and elements to filter out.
* Also filters remote changes.
* @type {DomFilter}
*/
this.filter = opts.filter || defaultFilter
// set initial value
target.innerHTML = ''
type.forEach(child => {
target.insertBefore(child.toDom(opts.document, opts.hooks, this), null)
})
this._typeObserver = typeObserver.bind(this)
this._domObserver = mutations => {
domObserver.call(this, mutations, opts.document)
}
type.observeDeep(this._typeObserver)
this._mutationObserver = new MutationObserver(this._domObserver)
this._mutationObserver.observe(target, {
childList: true,
attributes: true,
characterData: true,
subtree: true
})
this._currentSel = null
this._selectionchange = () => {
this._currentSel = getCurrentRelativeSelection(this)
}
document.addEventListener('selectionchange', this._selectionchange)
const y = type._y
this.y = y
// Force flush dom changes before Type changes are applied (they might
// modify the dom)
this._beforeTransactionHandler = y => {
this._domObserver(this._mutationObserver.takeRecords())
this._mutualExclude(() => {
beforeTransactionSelectionFixer(this)
})
}
y.on('beforeTransaction', this._beforeTransactionHandler)
this._afterTransactionHandler = (y, transaction) => {
this._mutualExclude(() => {
afterTransactionSelectionFixer(this)
})
// remove associations
// TODO: this could be done more efficiently
// e.g. Always delete using the following approach, or removeAssociation
// in dom/type-observer..
transaction.deletedStructs.forEach(type => {
const dom = this.typeToDom.get(type)
if (dom !== undefined) {
removeAssociation(this, dom, type)
}
})
}
y.on('afterTransaction', this._afterTransactionHandler)
// Before calling observers, apply dom filter to all changed and new types.
this._beforeObserverCallsHandler = (y, transaction) => {
// Apply dom filter to new and changed types
transaction.changedTypes.forEach((subs, type) => {
// Only check attributes. New types are filtered below.
if ((subs.size > 1 || (subs.size === 1 && subs.has(null) === false))) {
applyFilterOnType(y, this, type)
}
})
transaction.newTypes.forEach(type => {
applyFilterOnType(y, this, type)
})
}
y.on('beforeObserverCalls', this._beforeObserverCallsHandler)
createAssociation(this, target, type)
}
flushDomChanges () {
this._domObserver(this._mutationObserver.takeRecords())
}
/**
* NOTE:
* * does not apply filter to existing elements!
* * only guarantees that changes are filtered locally. Remote sites may see different content.
*
* @param {DomFilter} filter The filter function to use from now on.
*/
setFilter (filter) {
this.filter = filter
// TODO: apply filter to all elements
}
_getUndoStackInfo () {
return this.getSelection()
}
_restoreUndoStackInfo (info) {
this.restoreSelection(info)
}
getSelection () {
return this._currentSel
}
restoreSelection (selection) {
if (selection !== null) {
const { to, from } = selection
/**
* There is little information on the difference between anchor/focus and base/extent.
* MDN doesn't even mention base/extent anymore.. though you still have to call
* setBaseAndExtent to change the selection..
* I can observe that base/extend refer to notes higher up in the xml hierachy.
* Espesially for undo/redo this is preferred. If this becomes a problem in the future,
* we should probably go back to anchor/focus.
*/
const browserSelection = getSelection()
let { baseNode, baseOffset, extentNode, extentOffset } = browserSelection
if (from !== null) {
let sel = fromRelativePosition(this.y, from)
if (sel !== null) {
let node = this.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== baseNode || offset !== baseOffset) {
baseNode = node
baseOffset = offset
}
}
}
if (to !== null) {
let sel = fromRelativePosition(this.y, to)
if (sel !== null) {
let node = this.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== extentNode || offset !== extentOffset) {
extentNode = node
extentOffset = offset
}
}
}
browserSelection.setBaseAndExtent(
baseNode,
baseOffset,
extentNode,
extentOffset
)
}
}
/**
* Remove all properties that are handled by this class.
*/
destroy () {
this.domToType = null
this.typeToDom = null
this.type.unobserveDeep(this._typeObserver)
this._mutationObserver.disconnect()
const y = this.type._y
y.off('beforeTransaction', this._beforeTransactionHandler)
y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
y.off('afterTransaction', this._afterTransactionHandler)
document.removeEventListener('selectionchange', this._selectionchange)
this.type = null
this.target = null
}
}
/**
* A filter defines which elements and attributes to share.
* Return null if the node should be filtered. Otherwise return the Map of
* accepted attributes.
*
* @callback FilterFunction
* @param {string} nodeName
* @param {Map} attrs
* @return {Map|null}
*/

View File

@@ -1,150 +0,0 @@
/**
* @module bindings/dom
*/
import { YXmlHook } from '../../types/YXmlHook.js'
import {
iterateUntilUndeleted,
removeAssociation,
insertNodeHelper } from './util.js'
import { simpleDiff } from '../../lib/diff.js'
import { YXmlFragment } from '../../types/YXmlElement.js'
/**
* 1. Check if any of the nodes was deleted
* 2. Iterate over the children.
* 2.1 If a node exists that is not yet bound to a type, insert a new node
* 2.2 If _contents.length < dom.childNodes.length, fill the
* rest of _content with childNodes
* 2.3 If a node was moved, delete it and
* recreate a new yxml element that is bound to that node.
* You can detect that a node was moved because expectedId
* !== actualId in the list
*
* @function
* @private
*/
const applyChangesFromDom = (binding, dom, yxml, _document) => {
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
return
}
const y = yxml._y
const knownChildren = new Set()
for (let i = dom.childNodes.length - 1; i >= 0; i--) {
const type = binding.domToType.get(dom.childNodes[i])
if (type !== undefined && type !== false) {
knownChildren.add(type)
}
}
// 1. Check if any of the nodes was deleted
yxml.forEach(childType => {
if (knownChildren.has(childType) === false) {
childType._delete(y)
removeAssociation(binding, binding.typeToDom.get(childType), childType)
}
})
// 2. iterate
const childNodes = dom.childNodes
const len = childNodes.length
let prevExpectedType = null
let expectedType = iterateUntilUndeleted(yxml._start)
for (let domCnt = 0; domCnt < len; domCnt++) {
const childNode = childNodes[domCnt]
const childType = binding.domToType.get(childNode)
if (childType !== undefined) {
if (childType === false) {
// should be ignored or is going to be deleted
continue
}
if (expectedType !== null) {
if (expectedType !== childType) {
// 2.3 Not expected node
if (childType._parent !== yxml) {
// child was moved from another parent
// childType is going to be deleted by its previous parent
removeAssociation(binding, childNode, childType)
} else {
// child was moved to a different position.
removeAssociation(binding, childNode, childType)
childType._delete(y)
}
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
} else {
// Found expected node. Continue.
prevExpectedType = expectedType
expectedType = iterateUntilUndeleted(expectedType._right)
}
} else {
// 2.2 Fill _content with child nodes
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
}
} else {
// 2.1 A new node was found
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
}
}
}
/**
* @private
* @function
*/
export function domObserver (mutations, _document) {
this._mutualExclude(() => {
this.type._y.transact(() => {
let diffChildren = new Set()
mutations.forEach(mutation => {
const dom = mutation.target
const yxml = this.domToType.get(dom)
if (yxml === undefined) { // In case yxml is undefined, we double check if we forgot to bind the dom
let parent = dom
let yParent
do {
parent = parent.parentElement
yParent = this.domToType.get(parent)
} while (yParent === undefined && parent !== null)
if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) {
diffChildren.add(parent)
}
return
} else if (yxml === false || yxml.constructor === YXmlHook) {
// dom element is filtered / a dom hook
return
}
switch (mutation.type) {
case 'characterData':
var change = simpleDiff(yxml.toString(), dom.nodeValue)
yxml.delete(change.pos, change.remove)
yxml.insert(change.pos, change.insert)
break
case 'attributes':
if (yxml.constructor === YXmlFragment) {
break
}
let name = mutation.attributeName
let val = dom.getAttribute(name)
// check if filter accepts attribute
let attributes = new Map()
attributes.set(name, val)
if (yxml.constructor !== YXmlFragment && this.filter(dom.nodeName, attributes).size > 0) {
if (yxml.getAttribute(name) !== val) {
if (val == null) {
yxml.removeAttribute(name)
} else {
yxml.setAttribute(name, val)
}
}
}
break
case 'childList':
diffChildren.add(mutation.target)
break
}
})
for (let dom of diffChildren) {
const yxml = this.domToType.get(dom)
applyChangesFromDom(this, dom, yxml, _document)
}
})
})
}

View File

@@ -1,74 +0,0 @@
/**
* @module bindings/dom
*/
/* eslint-env browser */
import { YXmlText } from '../../types/YXmlText.js'
import { YXmlHook } from '../../types/YXmlHook.js'
import { YXmlElement } from '../../types/YXmlElement.js'
import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes, defaultFilter } from './filter.js'
import { DomBinding } from './DomBinding.js' // eslint-disable-line
/**
* @callback DomFilter
* @param {string} nodeName
* @param {Map<string, string>} attrs
* @return {Map | null}
*/
/**
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
*
* @function
* @param {Element|Text} element The DOM Element
* @param {?Document} _document Optional. Provide the global document object
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
* @param {DomFilter} [filter=defaultFilter] Optional. Dom element filter
* @param {?DomBinding} binding Warning: This property is for internal use only!
* @return {YXmlElement | YXmlText | false}
*/
export const domToType = (element, _document = document, hooks = {}, filter = defaultFilter, binding) => {
/**
* @type {any}
*/
let type = null
if (element instanceof Element) {
let hookName = null
let hook
// configure `hookName !== undefined` if element is a hook.
if (element.hasAttribute('data-yjs-hook')) {
hookName = element.getAttribute('data-yjs-hook')
hook = hooks[hookName]
if (hook === undefined) {
console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
element.removeAttribute('data-yjs-hook')
hookName = null
}
}
if (hookName === null) {
// Not a hook
const attrs = filterDomAttributes(element, filter)
if (attrs === null) {
type = false
} else {
type = new YXmlElement(element.nodeName)
attrs.forEach((val, key) => {
type.setAttribute(key, val)
})
type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
}
} else {
// Is a hook
type = new YXmlHook(hookName)
hook.fillType(element, type)
}
} else if (element instanceof Text) {
type = new YXmlText()
type.insert(0, element.nodeValue)
} else {
throw new Error('Can\'t transform this node type to a YXml type!')
}
createAssociation(binding, element, type)
return type
}

View File

@@ -1,71 +0,0 @@
/**
* @module bindings/dom
*/
import { Y } from '../../utils/Y.js' // eslint-disable-line
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
import { isParentOf } from '../../utils/isParentOf.js'
import { DomBinding } from './DomBinding.js' // eslint-disable-line
/**
* Default filter method (does nothing).
*
* @function
* @param {String} nodeName The nodeName of the element
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
* @return {Map | null} The allowed attributes or null, if the element should be
* filtered.
*/
export const defaultFilter = (nodeName, attrs) => {
// TODO: implement basic filter that filters out dangerous properties!
return attrs
}
/**
* @private
* @function
* @param {Element} dom
* @param {Function} filter
*/
export const filterDomAttributes = (dom, filter) => {
const attrs = new Map()
for (let i = dom.attributes.length - 1; i >= 0; i--) {
const attr = dom.attributes[i]
attrs.set(attr.name, attr.value)
}
return filter(dom.nodeName, attrs)
}
/**
* Applies a filter on a type.
*
* @private
* @function
* @param {Y} y The Yjs instance.
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
*/
export const applyFilterOnType = (y, binding, type) => {
if (isParentOf(binding.type, type) && type instanceof YXmlElement) {
const nodeName = type.nodeName
let attributes = new Map()
if (type.getAttributes !== undefined) {
let attrs = type.getAttributes()
for (let key in attrs) {
attributes.set(key, attrs[key])
}
}
const filteredAttributes = binding.filter(nodeName, new Map(attributes))
if (filteredAttributes === null) {
type._delete(y, true)
} else {
// iterate original attributes
attributes.forEach((value, key) => {
// delete all attributes that are not in filteredAttributes
if (filteredAttributes.has(key) === false) {
type.removeAttribute(key)
}
})
}
}
}

View File

@@ -1,49 +0,0 @@
/**
* @module bindings/dom
*/
/* globals getSelection */
import { getRelativePosition } from '../../utils/relativePosition.js'
let relativeSelection = null
/**
* @private
*/
const _getCurrentRelativeSelection = domBinding => {
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
const baseNodeType = domBinding.domToType.get(baseNode)
const extentNodeType = domBinding.domToType.get(extentNode)
if (baseNodeType !== undefined && extentNodeType !== undefined) {
return {
from: getRelativePosition(baseNodeType, baseOffset),
to: getRelativePosition(extentNodeType, extentOffset)
}
}
return null
}
/**
* @private
*/
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
/**
* @private
*/
export const beforeTransactionSelectionFixer = domBinding => {
relativeSelection = getCurrentRelativeSelection(domBinding)
}
/**
* Reset the browser range after every transaction.
* This prevents any collapsing issues with the local selection.
*
* @private
*/
export const afterTransactionSelectionFixer = domBinding => {
if (relativeSelection !== null) {
domBinding.restoreSelection(relativeSelection)
}
}

View File

@@ -1,110 +0,0 @@
/**
* @module bindings/dom
*/
/* eslint-env browser */
/* global getSelection */
import { YXmlText } from '../../types/YXmlText.js'
import { YXmlHook } from '../../types/YXmlHook.js'
import { removeDomChildrenUntilElementFound } from './util.js'
const findScrollReference = scrollingElement => {
if (scrollingElement !== null) {
let anchor = getSelection().anchorNode
if (anchor == null) {
let children = scrollingElement.children // only iterate through non-text nodes
for (let i = 0; i < children.length; i++) {
const elem = children[i]
const rect = elem.getBoundingClientRect()
if (rect.top >= 0) {
return { elem, top: rect.top }
}
}
} else {
/**
* @type {Element}
*/
let elem = anchor.parentElement
if (anchor instanceof Element) {
elem = anchor
}
return {
elem,
top: elem.getBoundingClientRect().top
}
}
}
return null
}
const fixScroll = (scrollingElement, ref) => {
if (ref !== null) {
const { elem, top } = ref
const currentTop = elem.getBoundingClientRect().top
const newScroll = scrollingElement.scrollTop + currentTop - top
if (newScroll >= 0) {
scrollingElement.scrollTop = newScroll
}
}
}
/**
* @private
*/
export const typeObserver = function (events) {
this._mutualExclude(() => {
const scrollRef = findScrollReference(this.scrollingElement)
events.forEach(event => {
const yxml = event.target
const dom = this.typeToDom.get(yxml)
if (dom !== undefined && dom !== false) {
if (yxml.constructor === YXmlText) {
dom.nodeValue = yxml.toString()
} else if (event.attributesChanged !== undefined) {
// update attributes
event.attributesChanged.forEach(attributeName => {
const value = yxml.getAttribute(attributeName)
if (value === undefined) {
dom.removeAttribute(attributeName)
} else {
dom.setAttribute(attributeName, value)
}
})
/*
* TODO: instead of hard-checking the types, it would be best to
* specify the type's features. E.g.
* - _yxmlHasAttributes
* - _yxmlHasChildren
* Furthermore, the features shouldn't be encoded in the types,
* only in the attributes (above)
*/
if (event.childListChanged && yxml.constructor !== YXmlHook) {
let currentChild = dom.firstChild
yxml.forEach(childType => {
const childNode = this.typeToDom.get(childType)
switch (childNode) {
case undefined:
// Does not exist. Create it.
const node = childType.toDom(this.opts.document, this.opts.hooks, this)
dom.insertBefore(node, currentChild)
break
case false:
// nop
break
default:
// Is already attached to the dom.
// Find it and remove all dom nodes in-between.
removeDomChildrenUntilElementFound(dom, currentChild, childNode)
currentChild = childNode.nextSibling
break
}
})
removeDomChildrenUntilElementFound(dom, currentChild, null)
}
}
}
})
fixScroll(this.scrollingElement, scrollRef)
})
}

View File

@@ -1,135 +0,0 @@
/**
* @module bindings/dom
*/
import { domToType } from './domToType.js'
import { DomBinding } from './DomBinding.js' // eslint-disable-line
/**
* Iterates items until an undeleted item is found.
*
* @private
*/
export const iterateUntilUndeleted = item => {
while (item !== null && item._deleted) {
item = item._right
}
return item
}
/**
* Removes an association (the information that a DOM element belongs to a
* type).
*
* @private
* @function
* @param {DomBinding} domBinding The binding object
* @param {Element} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
*
*/
export const removeAssociation = (domBinding, dom, type) => {
domBinding.domToType.delete(dom)
domBinding.typeToDom.delete(type)
}
/**
* Creates an association (the information that a DOM element belongs to a
* type).
*
* @private
* @function
* @param {DomBinding} domBinding The binding object
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
* @param {YXmlFragment|YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
*
*/
export const createAssociation = (domBinding, dom, type) => {
if (domBinding !== undefined) {
domBinding.domToType.set(dom, type)
domBinding.typeToDom.set(type, dom)
}
}
/**
* If oldDom is associated with a type, associate newDom with the type and
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
*
* @private
* @function
* @param {DomBinding} domBinding The binding object
* @param {Element} oldDom The existing dom
* @param {Element} newDom The new dom object
*/
export const switchAssociation = (domBinding, oldDom, newDom) => {
if (domBinding !== undefined) {
const type = domBinding.domToType.get(oldDom)
if (type !== undefined) {
removeAssociation(domBinding, oldDom, type)
createAssociation(domBinding, newDom, type)
}
}
}
/**
* Insert Dom Elements after one of the children of this YXmlFragment.
* The Dom elements will be bound to a new YXmlElement and inserted at the
* specified position.
*
* @private
* @function
* @param {YXmlElement} type The type in which to insert DOM elements.
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
* inserted after this node. Set null to insert at
* the beginning.
* @param {Array<Element>} doms The Dom elements to insert.
* @param {?Document} _document Optional. Provide the global document object.
* @param {DomBinding} binding The dom binding
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
*/
export const insertDomElementsAfter = (type, prev, doms, _document, binding) => {
const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding)
return type.insertAfter(prev, types)
}
export const domsToTypes = (doms, _document, hooks, filter, binding) => {
const types = []
for (let dom of doms) {
const t = domToType(dom, _document, hooks, filter, binding)
if (t !== false) {
types.push(t)
}
}
return types
}
/**
* @private
* @function
*/
export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, binding) => {
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
if (insertedNodes.length > 0) {
return insertedNodes[0]
} else {
return prevExpectedNode
}
}
/**
* Remove children until `elem` is found.
*
* @private
* @function
* @param {Element} parent The parent of `elem` and `currentChild`.
* @param {Node} currentChild Start removing elements with `currentChild`. If
* `currentChild` is `elem` it won't be removed.
* @param {Element|null} elem The elemnt to look for.
*/
export const removeDomChildrenUntilElementFound = (parent, currentChild, elem) => {
while (currentChild !== elem) {
const del = currentChild
currentChild = currentChild.nextSibling
parent.removeChild(del)
}
}

View File

@@ -1,633 +0,0 @@
/**
* @module bindings/prosemirror
*/
import { YText } from '../types/YText.js' // eslint-disable-line
import { YXmlElement, YXmlFragment } from '../types/YXmlElement.js' // eslint-disable-line
import { createMutex } from '../lib/mutex.js'
import * as PModel from 'prosemirror-model'
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state' // eslint-disable-line
import * as math from '../lib/math.js'
import * as object from '../lib/object.js'
import * as YPos from '../utils/relativePosition.js'
/**
* @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
*/
/**
* The unique prosemirror plugin key for prosemirrorPlugin.
*
* @public
*/
export const prosemirrorPluginKey = new PluginKey('yjs')
/**
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
*
* This plugin also keeps references to the type and the shared document so other plugins can access it.
* @param {YXmlFragment} yXmlFragment
* @return {Plugin} Returns a prosemirror plugin that binds to this type
*/
export const prosemirrorPlugin = yXmlFragment => {
const pluginState = {
type: yXmlFragment,
y: yXmlFragment._y,
binding: null
}
let changedInitialContent = false
const plugin = new Plugin({
key: prosemirrorPluginKey,
state: {
init: (initargs, state) => {
return pluginState
},
apply: (tr, pluginState) => {
// update Yjs state when apply is called. We need to do this here to compute the correct cursor decorations with the cursor plugin
if (pluginState.binding !== null && (changedInitialContent || tr.doc.content.size > 4)) {
changedInitialContent = true
pluginState.binding._prosemirrorChanged(tr.doc)
}
return pluginState
}
},
view: view => {
const binding = new ProsemirrorBinding(yXmlFragment, view)
pluginState.binding = binding
return {
update: () => {
if (changedInitialContent || view.state.doc.content.size > 4) {
changedInitialContent = true
binding._prosemirrorChanged(view.state.doc)
}
},
destroy: () => {
binding.destroy()
}
}
}
})
return plugin
}
/**
* The unique prosemirror plugin key for cursorPlugin.type
*
* @public
*/
export const cursorPluginKey = new PluginKey('yjs-cursor')
/**
* A prosemirror plugin that listens to awareness information on Yjs.
* This requires that a `prosemirrorPlugin` is also bound to the prosemirror.
*
* @public
*/
export const cursorPlugin = new Plugin({
key: cursorPluginKey,
props: {
decorations: state => {
const ystate = prosemirrorPluginKey.getState(state)
const y = ystate.y
const awareness = y.getAwarenessInfo()
const decorations = []
awareness.forEach((aw, userID) => {
if (aw.cursor != null) {
let user = aw.user || {}
if (user.color == null) {
user.color = '#ffa50070'
}
if (user.name == null) {
user.name = `User: ${userID}`
}
let anchor = relativePositionToAbsolutePosition(ystate.type, aw.cursor.anchor || null, ystate.binding.mapping)
let head = relativePositionToAbsolutePosition(ystate.type, aw.cursor.head || null, ystate.binding.mapping)
if (anchor !== null && head !== null) {
let maxsize = math.max(state.doc.content.size - 1, 0)
anchor = math.min(anchor, maxsize)
head = math.min(head, maxsize)
decorations.push(Decoration.widget(head, () => {
const cursor = document.createElement('span')
cursor.classList.add('ProseMirror-yjs-cursor')
cursor.setAttribute('style', `border-color: ${user.color}`)
const userDiv = document.createElement('div')
userDiv.setAttribute('style', `background-color: ${user.color}`)
userDiv.insertBefore(document.createTextNode(user.name), null)
cursor.insertBefore(userDiv, null)
return cursor
}, { key: userID + '' }))
const from = math.min(anchor, head)
const to = math.max(anchor, head)
decorations.push(Decoration.inline(from, to, { style: `background-color: ${user.color}` }))
}
}
})
return DecorationSet.create(state.doc, decorations)
}
},
view: view => {
const ystate = prosemirrorPluginKey.getState(view.state)
const y = ystate.y
const awarenessListener = () => {
view.updateState(view.state)
}
const updateCursorInfo = () => {
const current = y.getLocalAwarenessInfo()
if (view.hasFocus()) {
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
y.setAwarenessField('cursor', {
anchor, head
})
}
} else if (current.cursor !== null) {
y.setAwarenessField('cursor', null)
}
}
y.on('awareness', awarenessListener)
view.dom.addEventListener('focusin', updateCursorInfo)
view.dom.addEventListener('focusout', updateCursorInfo)
return {
update: updateCursorInfo,
destroy: () => {
const y = prosemirrorPluginKey.getState(view.state).y
y.setAwarenessField('cursor', null)
y.off('awareness', awarenessListener)
}
}
}
})
/**
* Transforms a Prosemirror based absolute position to a Yjs based relative position.
*
* @param {number} pos
* @param {YXmlFragment} type
* @param {ProsemirrorMapping} mapping
* @return {any} relative position
*/
export const absolutePositionToRelativePosition = (pos, type, mapping) => {
if (pos === 0) {
return YPos.getRelativePosition(type, 0)
}
let n = type._first
if (n !== null) {
while (type !== n) {
const pNodeSize = (mapping.get(n) || { nodeSize: 0 }).nodeSize
if (n.constructor === YText) {
if (n.length >= pos) {
return YPos.getRelativePosition(n, pos)
} else {
pos -= n.length
}
if (n._next !== null) {
n = n._next
} else {
do {
n = n._parent
pos--
} while (n._next === null && n !== type)
if (n !== type) {
n = n._next
}
}
} else if (n._first !== null && pos < pNodeSize) {
n = n._first
pos--
} else {
if (pos === 1 && n.length === 0 && pNodeSize > 1) {
// edge case, should end in this paragraph
return ['endof', n._id.user, n._id.clock, null, null]
}
pos -= pNodeSize
if (n._next !== null) {
n = n._next
} else {
if (pos === 0) {
n = n._parent
return ['endof', n._id.user, n._id.clock || null, n._id.name || null, n._id.type || null]
}
do {
n = n._parent
pos--
} while (n._next === null && n !== type)
if (n !== type) {
n = n._next
}
}
}
if (pos === 0 && n.constructor !== YText && n !== type) { // TODO: set to <= 0
return [n._id.user, n._id.clock]
}
}
}
return YPos.getRelativePosition(type, type.length)
}
/**
* @param {YXmlFragment} yDoc Top level type that is bound to pView
* @param {any} relPos Encoded Yjs based relative position
* @param {ProsemirrorMapping} mapping
*/
export const relativePositionToAbsolutePosition = (yDoc, relPos, mapping) => {
const decodedPos = YPos.fromRelativePosition(yDoc._y, relPos)
if (decodedPos === null) {
return null
}
let type = decodedPos.type
let pos = 0
if (type.constructor === YText) {
pos = decodedPos.offset
} else if (!type._deleted) {
let n = type._first
let i = 0
while (i < type.length && i < decodedPos.offset && n !== null) {
i++
pos += mapping.get(n).nodeSize
n = n._next
}
pos += 1 // increase because we go out of n
}
while (type !== yDoc) {
const parent = type._parent
if (!parent._deleted) {
pos += 1 // the start tag
let n = parent._first
// now iterate until we found type
while (n !== null) {
if (n === type) {
break
}
pos += mapping.get(n).nodeSize
n = n._next
}
}
type = parent
}
return pos - 1 // we don't count the most outer tag, because it is a fragment
}
/**
* Binding for prosemirror.
*
* @protected
*/
export class ProsemirrorBinding {
/**
* @param {YXmlFragment} yXmlFragment The bind source
* @param {EditorView} prosemirrorView The target binding
*/
constructor (yXmlFragment, prosemirrorView) {
this.type = yXmlFragment
this.prosemirrorView = prosemirrorView
this.mux = createMutex()
/**
* @type {ProsemirrorMapping}
*/
this.mapping = new Map()
this._observeFunction = this._typeChanged.bind(this)
this.y = yXmlFragment._y
/**
* current selection as relative positions in the Yjs model
*/
this._relSelection = null
this.y.on('beforeTransaction', e => {
this._relSelection = {
anchor: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.anchor, yXmlFragment, this.mapping),
head: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.head, yXmlFragment, this.mapping)
}
})
yXmlFragment.observeDeep(this._observeFunction)
}
_typeChanged (events, transaction) {
if (events.length === 0) {
return
}
console.info('new types:', transaction.newTypes.size, 'deleted types:', transaction.deletedStructs.size, transaction.newTypes, transaction.deletedStructs)
this.mux(() => {
const delStruct = (_, struct) => this.mapping.delete(struct)
transaction.deletedStructs.forEach(struct => this.mapping.delete(struct))
transaction.changedTypes.forEach(delStruct)
transaction.changedParentTypes.forEach(delStruct)
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null)
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
const relSel = this._relSelection
if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
const anchor = relativePositionToAbsolutePosition(this.type, relSel.anchor, this.mapping)
const head = relativePositionToAbsolutePosition(this.type, relSel.head, this.mapping)
if (anchor !== null && head !== null) {
tr.setSelection(TextSelection.create(tr.doc, anchor, head))
}
}
this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr))
})
}
_prosemirrorChanged (doc) {
this.mux(() => {
updateYFragment(this.type, doc.content, this.mapping)
})
}
destroy () {
this.type.unobserveDeep(this._observeFunction)
}
}
/**
* @privateMapping
* @param {YXmlElement} el
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @return {PModel.Node}
*/
export const createNodeIfNotExists = (el, schema, mapping) => {
const node = mapping.get(el)
if (node === undefined) {
return createNodeFromYElement(el, schema, mapping)
}
return node
}
/**
* @private
* @param {YXmlElement} el
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
*/
export const createNodeFromYElement = (el, schema, mapping) => {
const children = []
el.toArray().forEach(type => {
if (type.constructor === YXmlElement) {
const n = createNodeIfNotExists(type, schema, mapping)
if (n !== null) {
children.push(n)
}
} else {
const ns = createTextNodesFromYText(type, schema, mapping)
if (ns !== null) {
ns.forEach(textchild => {
if (textchild !== null) {
children.push(textchild)
}
})
}
}
})
let node
try {
node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), children)
} catch (e) {
// an error occured while creating the node. This is probably a result because of a concurrent action.
// delete the node and do not push to children
el._y.transact(() => {
el._delete(el._y, true)
})
return null
}
mapping.set(el, node)
return node
}
/**
* @private
* @param {YText} text
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @return {Array<PModel.Node>}
*/
export const createTextNodesFromYText = (text, schema, mapping) => {
const nodes = []
const deltas = text.toDelta()
try {
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i]
const marks = []
for (let markName in delta.attributes) {
marks.push(schema.mark(markName, delta.attributes[markName]))
}
nodes.push(schema.text(delta.insert, marks))
}
if (nodes.length > 0) {
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
}
} catch (e) {
text._y.transact(() => {
text._delete(text._y, true)
})
return null
}
return nodes
}
/**
* @private
* @param {PModel.Node} node
* @param {ProsemirrorMapping} mapping
* @return {YXmlElement | YText}
*/
export const createTypeFromNode = (node, mapping) => {
let type
if (node.isText) {
type = new YText()
const attrs = {}
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
type.insert(0, node.text, attrs)
} else {
type = new YXmlElement(node.type.name)
for (let key in node.attrs) {
const val = node.attrs[key]
if (val !== null) {
type.setAttribute(key, val)
}
}
const ins = []
for (let i = 0; i < node.childCount; i++) {
ins.push(createTypeFromNode(node.child(i), mapping))
}
type.insert(0, ins)
}
mapping.set(type, node)
return type
}
const equalAttrs = (pattrs, yattrs) => {
const keys = Object.keys(pattrs).filter(key => pattrs[key] === null)
let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
for (let i = 0; i < keys.length && eq; i++) {
const key = keys[i]
eq = pattrs[key] === yattrs[key]
}
return eq
}
const equalYTextPText = (ytext, ptext) => {
const d = ytext.toDelta()[0]
return d.insert === ptext.text && object.keys(d.attributes || {}).length === ptext.marks.length && ptext.marks.every(mark => equalAttrs(d.attributes[mark.type.name], mark.attrs))
}
const equalYTypePNode = (ytype, pnode) =>
ytype.constructor === YText
? equalYTextPText(ytype, pnode)
: (matchNodeName(ytype, pnode) && ytype.length === pnode.childCount && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, pnode.child(i))))
const computeChildEqualityFactor = (ytype, pnode, mapping) => {
const yChildren = ytype.toArray()
const pChildCnt = pnode.childCount
const yChildCnt = yChildren.length
const minCnt = math.min(yChildCnt, pChildCnt)
let left = 0
let right = 0
let foundMappedChild = false
for (; left < minCnt; left++) {
const leftY = yChildren[left]
const leftP = pnode.child(left)
if (mapping.get(leftY) === leftP) {
foundMappedChild = true// definite (good) match!
} else if (!equalYTypePNode(leftY, leftP)) {
break
}
}
for (; left + right < minCnt; right++) {
const rightY = yChildren[yChildCnt - right - 1]
const rightP = pnode.child(pChildCnt - right - 1)
if (mapping.get(rightY) !== rightP) {
foundMappedChild = true
} else if (!equalYTypePNode(rightP, rightP)) {
break
}
}
return {
equalityFactor: left + right,
foundMappedChild
}
}
/**
* @private
* @param {YXmlFragment} yDomFragment
* @param {PModel.Node} pContent
* @param {ProsemirrorMapping} mapping
*/
const updateYFragment = (yDomFragment, pContent, mapping) => {
if (yDomFragment instanceof YXmlElement && yDomFragment.nodeName.toLowerCase() !== pContent.type.name) {
throw new Error('node name mismatch!')
}
mapping.set(yDomFragment, pContent)
// update attributes
if (yDomFragment instanceof YXmlElement) {
const yDomAttrs = yDomFragment.getAttributes()
const pAttrs = pContent.attrs
for (let key in pAttrs) {
if (pAttrs[key] !== null) {
if (yDomAttrs[key] !== pAttrs[key]) {
yDomFragment.setAttribute(key, pAttrs[key])
}
} else {
yDomFragment.removeAttribute(key)
}
}
// remove all keys that are no longer in pAttrs
for (let key in yDomAttrs) {
if (pAttrs[key] === undefined) {
yDomFragment.removeAttribute(key)
}
}
}
// update children
const pChildCnt = pContent.childCount
const yChildren = yDomFragment.toArray()
const yChildCnt = yChildren.length
const minCnt = math.min(pChildCnt, yChildCnt)
let left = 0
let right = 0
// find number of matching elements from left
for (;left < minCnt; left++) {
const leftY = yChildren[left]
const leftP = pContent.child(left)
if (mapping.get(leftY) !== leftP) {
if (equalYTypePNode(leftY, leftP)) {
// update mapping
mapping.set(leftY, leftP)
} else {
break
}
}
}
// find number of matching elements from right
for (;right + left < minCnt; right++) {
const rightY = yChildren[yChildCnt - right - 1]
const rightP = pContent.child(pChildCnt - right - 1)
if (mapping.get(rightY) !== rightP) {
if (equalYTypePNode(rightY, rightP)) {
// update mapping
mapping.set(rightY, rightP)
} else {
break
}
}
}
yDomFragment._y.transact(() => {
// try to compare and update
while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
const leftY = yChildren[left]
const leftP = pContent.child(left)
const rightY = yChildren[yChildCnt - right - 1]
const rightP = pContent.child(pChildCnt - right - 1)
if (leftY.constructor === YText && leftP.isText) {
if (!equalYTextPText(leftY, leftP)) {
yDomFragment.delete(left, 1)
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
}
left += 1
} else {
let updateLeft = matchNodeName(leftY, leftP)
let updateRight = matchNodeName(rightY, rightP)
if (updateLeft && updateRight) {
// decide which which element to update
const equalityLeft = computeChildEqualityFactor(leftY, leftP, mapping)
const equalityRight = computeChildEqualityFactor(rightY, rightP, mapping)
if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
updateRight = false
} else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
updateLeft = false
} else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
updateLeft = false
} else {
updateRight = false
}
}
if (updateLeft) {
updateYFragment(leftY, leftP, mapping)
left += 1
} else if (updateRight) {
updateYFragment(rightY, rightP, mapping)
right += 1
} else {
yDomFragment.delete(left, 1)
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
left += 1
}
}
}
const yDelLen = yChildCnt - left - right
if (yDelLen > 0) {
yDomFragment.delete(left, yDelLen)
}
if (left + right < pChildCnt) {
const ins = []
for (let i = left; i < pChildCnt - right; i++) {
ins.push(createTypeFromNode(pContent.child(i), mapping))
}
yDomFragment.insert(left, ins)
}
})
}
/**
* @function
* @param {YXmlElement} yElement
* @param {any} pNode Prosemirror Node
*/
const matchNodeName = (yElement, pNode) => yElement.nodeName === pNode.type.name.toUpperCase()

View File

@@ -1,71 +0,0 @@
/**
* @module bindings/quill
*/
import { createMutex } from '../lib/mutex.js'
const typeObserver = function (event) {
const quill = this.target
// Force flush Quill changes.
quill.update('yjs')
this._mutualExclude(() => {
// Apply computed delta.
quill.updateContents(event.delta, 'yjs')
// Force flush Quill changes. Ignore applied changes.
quill.update('yjs')
})
}
const quillObserver = function (delta) {
this._mutualExclude(() => {
this.type.applyDelta(delta.ops)
})
}
/**
* A Binding that binds a YText type to a Quill editor.
*
* @example
* const quill = new Quill(document.createElement('div'))
* const type = y.define('quill', Y.Text)
* const binding = new Y.QuillBinding(quill, type)
* // Now modifications on the DOM will be reflected in the Type, and the other
* // way around!
*/
export class QuillBinding {
/**
* @param {YText} textType
* @param {Quill} quill
*/
constructor (textType, quill) {
// Binding handles textType as this.type and quill as this.target.
/**
* The Yjs type that is bound to `target`
* @type {YText}
*/
this.type = textType
/**
* The target that `type` is bound to.
* @type {Quill}
*/
this.target = quill
/**
* @private
*/
this._mutualExclude = createMutex()
// Set initial value.
quill.setContents(textType.toDelta(), 'yjs')
// Observers are handled by this class.
this._typeObserver = typeObserver.bind(this)
this._quillObserver = quillObserver.bind(this)
textType.observe(this._typeObserver)
quill.on('text-change', this._quillObserver)
}
destroy () {
// Remove everything that is handled by this class.
this.type.unobserve(this._typeObserver)
this.target.off('text-change', this._quillObserver)
this.type = null
this.target = null
}
}

View File

@@ -1,72 +0,0 @@
/**
* @module bindings/textarea
*/
import { simpleDiff } from '../lib/diff.js'
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.js'
import { createMutex } from '../lib/mutex.js'
function typeObserver () {
this._mutualExclude(() => {
const textarea = this.target
const textType = this.type
const relativeStart = getRelativePosition(textType, textarea.selectionStart)
const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
textarea.value = textType.toString()
const start = fromRelativePosition(textType._y, relativeStart)
const end = fromRelativePosition(textType._y, relativeEnd)
textarea.setSelectionRange(start, end)
})
}
function domObserver () {
this._mutualExclude(() => {
let diff = simpleDiff(this.type.toString(), this.target.value)
this.type.delete(diff.pos, diff.remove)
this.type.insert(diff.pos, diff.insert)
})
}
/**
* A binding that binds a YText to a dom textarea.
*
* This binding is automatically destroyed when its parent is deleted.
*
* @example
* const textare = document.createElement('textarea')
* const type = y.define('textarea', Y.Text)
* const binding = new Y.QuillBinding(type, textarea)
*
*/
export class TextareaBinding {
constructor (textType, domTextarea) {
/**
* The Yjs type that is bound to `target`
* @type {Type}
*/
this.type = textType
/**
* The target that `type` is bound to.
* @type {*}
*/
this.target = domTextarea
/**
* @private
*/
this._mutualExclude = createMutex()
// set initial value
domTextarea.value = textType.toString()
// Observers are handled by this class
this._typeObserver = typeObserver.bind(this)
this._domObserver = domObserver.bind(this)
textType.observe(this._typeObserver)
domTextarea.addEventListener('input', this._domObserver)
}
destroy () {
// Remove everything that is handled by this class
this.type.unobserve(this._typeObserver)
this.target.unobserve(this._domObserver)
this.type = null
this.target = null
}
}

70
examples/codemirror.html Normal file
View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs CodeMirror Example</title>
<link rel=stylesheet href="https://codemirror.net/lib/codemirror.css">
<style>
#container {
border: grey;
border-style: solid;
border-width: thin;
}
</style>
</head>
<body>
<p>This example shows how to bind a YText type to <a href="https://codemirror.net/">CodeMirror</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<div class="code-html">
<style>
.remote-caret {
position: absolute;
border-left: black;
border-left-style: solid;
border-left-width: 2px;
height: 1em;
}
.remote-caret > div {
position: relative;
top: -1.05em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-family: serif;
font-style: normal;
font-weight: normal;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
}
</style>
<div id="container"></div>
</div>
<!-- The actual source file for the following code is found in ./codemirror.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/codemirror.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { CodeMirrorBinding } from 'yjs/bindings/codemirror.js'
import * as conf from './exampleConfig.js'
import CodeMirror from 'codemirror'
import 'codemirror/mode/javascript/javascript.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('codemirror')
const ytext = ydocument.define('codemirror', Y.Text)
const editor = new CodeMirror(document.querySelector('#container'), {
mode: 'javascript',
lineNumbers: true
})
const binding = new CodeMirrorBinding(ytext, editor)
window.codemirrorExample = {
binding, editor, ytext, ydocument
}
</script>
</body>
</html>

22
examples/codemirror.js Normal file
View File

@@ -0,0 +1,22 @@
import { WebsocketProvider } from 'y-websocket'
import { CodeMirrorBinding } from 'y-codemirror'
import * as conf from './exampleConfig.js'
import CodeMirror from 'codemirror'
import 'codemirror/mode/javascript/javascript.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('codemirror')
const ytext = ydocument.getText('codemirror')
const editor = new CodeMirror(document.querySelector('#container'), {
mode: 'javascript',
lineNumbers: true
})
const binding = new CodeMirrorBinding(ytext, editor)
window.codemirrorExample = {
binding, editor, ytext, ydocument
}

View File

@@ -18,12 +18,13 @@
<div id="content" contenteditable=""></div>
</div>
<!-- The actual source file for the following code is found in ./dom.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/dom.js">
<script class="code-js" src="./build/dom.js" type="module">
import * as Y from 'yjs/index.js'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { DomBinding } from 'yjs/bindings/dom.js'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider('wss://api.yjs.website')
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('dom')
const type = ydocument.define('xml', Y.XmlFragment)
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })

View File

@@ -1,7 +1,6 @@
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { DomBinding } from '../bindings/dom.js'
import * as Y from '../src/index.js'
import { WebsocketProvider } from 'y-websocket'
import { DomBinding } from 'y-dom'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)

View File

@@ -1,6 +1,9 @@
{
"codemirror": {
"title": "CodeMirror Binding"
},
"prosemirror": {
"title": "Prosemirror Binding"
"title": "ProseMirror Binding"
},
"textarea": {
"title": "Textarea Binding"

View File

@@ -0,0 +1,159 @@
import { Plugin } from 'prosemirror-state'
import crel from 'crel'
import * as Y from '../src/index.js'
import { prosemirrorPluginKey } from 'y-prosemirror'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as historyProtocol from 'y-protocols/history.js'
const niceColors = ['#3cb44b', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#008080', '#9a6324', '#800000', '#808000', '#000075', '#808080']
const createUserCSS = (userid, username, color = 'rgb(250, 129, 0)', color2 = 'rgba(250, 129, 0, .41)') => `
[ychange_state][ychange_user="${userid}"]:hover::before {
content: "${username}" !important;
background-color: ${color} !important;
}
[ychange_state="added"][ychange_user="${userid}"] {
background-color: ${color2} !important;
}
[ychange_state="removed"][ychange_user="${userid}"] {
color: ${color} !important;
}
`
export const noteHistoryPlugin = new Plugin({
state: {
init (initargs, state) {
return new NoteHistoryPlugin()
},
apply (tr, pluginState) {
return pluginState
}
},
view (editorView) {
const hstate = noteHistoryPlugin.getState(editorView.state)
hstate.init(editorView)
return {
destroy: hstate.destroy.bind(hstate)
}
}
})
const createWrapper = () => {
const wrapper = crel('div', { style: 'display: flex;' })
const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
wrapper.insertBefore(historyContainer, null)
const userStyleContainer = crel('style')
wrapper.insertBefore(userStyleContainer, null)
return { wrapper, historyContainer, userStyleContainer }
}
class NoteHistoryPlugin {
init (editorView) {
this.editorView = editorView
const { historyContainer, wrapper, userStyleContainer } = createWrapper()
this.userStyleContainer = userStyleContainer
this.wrapper = wrapper
this.historyContainer = historyContainer
const n = editorView.dom.parentNode.parentNode
n.parentNode.replaceChild(this.wrapper, n)
n.style['flex-grow'] = '1'
wrapper.insertBefore(n, this.wrapper.firstChild)
this.render()
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
history.observe(this.render.bind(this))
}
destroy () {
this.wrapper.parentNode.replaceChild(this.wrapper.firstChild, this.wrapper)
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
history.unobserve(this.render)
}
render () {
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array).toArray()
const fragment = document.createDocumentFragment()
const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
fragment.insertBefore(snapshotBtn, null)
let _prevSnap = null // empty
snapshotBtn.addEventListener('click', () => {
const awareness = y.getAwarenessInfo()
const userMap = new Map()
const aw = y.getLocalAwarenessInfo()
userMap.set(y.userID, aw.name || 'unknown')
awareness.forEach((a, userID) => {
userMap.set(userID, a.name || 'Unknown')
})
this.snapshot(userMap)
})
history.forEach(buf => {
const decoder = decoding.createDecoder(buf)
const snapshot = historyProtocol.readHistorySnapshot(decoder)
const date = new Date(decoding.readUint32(decoder) * 1000)
const restoreBtn = crel('button', { type: 'button' }, ['restore'])
const a = crel('a', [
'• ' + date.toUTCString(), restoreBtn
])
const el = crel('div', [ a ])
let prevSnapshot = _prevSnap // rebind to new variable
restoreBtn.addEventListener('click', event => {
if (prevSnapshot === null) {
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
}
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot, restore: true }))
event.stopPropagation()
})
a.addEventListener('click', () => {
console.log('setting snapshot')
if (prevSnapshot === null) {
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
}
this.renderSnapshot(snapshot, prevSnapshot)
})
fragment.insertBefore(el, null)
_prevSnap = snapshot
})
this.historyContainer.innerHTML = ''
this.historyContainer.insertBefore(fragment, null)
}
renderSnapshot (snapshot, prevSnapshot) {
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot }))
/**
* @type {Array<string|null>}
*/
let colors = niceColors.slice()
let style = ''
snapshot.userMap.forEach((name, userid) => {
/**
* @type {any}
*/
const randInt = name.split('').map(s => s.charCodeAt(0)).reduce((a, b) => a + b)
let color = null
let i = 0
for (; i < colors.length && color === null; i++) {
color = colors[(randInt + i) % colors.length]
}
if (color === null) {
colors = niceColors.slice()
i = 0
color = colors[randInt % colors.length]
}
colors[randInt % colors.length] = null
style += createUserCSS(userid, name, color, color + '69')
})
this.userStyleContainer.innerHTML = style
}
/**
* @param {Map<number, string>} [updatedUserMap] Maps from userid (yjs model) to account name (e.g. mail address)
*/
snapshot (updatedUserMap = new Map()) {
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
const encoder = encoding.createEncoder()
historyProtocol.writeHistorySnapshot(encoder, y, updatedUserMap)
encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
history.push([encoding.toUint8Array(encoder)])
}
}

View File

@@ -0,0 +1,197 @@
import { Schema } from 'prosemirror-model'
const brDOM = ['br']
const calcYchangeDomAttrs = (attrs, domAttrs = {}) => {
domAttrs = Object.assign({}, domAttrs)
if (attrs.ychange !== null) {
domAttrs.ychange_user = attrs.ychange.user
domAttrs.ychange_state = attrs.ychange.state
}
return domAttrs
}
// :: Object
// [Specs](#model.NodeSpec) for the nodes defined in this schema.
export const nodes = {
// :: NodeSpec The top level document node.
doc: {
content: 'block+'
},
// :: NodeSpec A plain paragraph textblock. Represented in the DOM
// as a `<p>` element.
paragraph: {
attrs: { ychange: { default: null } },
content: 'inline*',
group: 'block',
parseDOM: [{ tag: 'p' }],
toDOM (node) { return ['p', calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
attrs: { ychange: { default: null } },
content: 'block+',
group: 'block',
defining: true,
parseDOM: [{ tag: 'blockquote' }],
toDOM (node) { return ['blockquote', calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A horizontal rule (`<hr>`).
horizontal_rule: {
attrs: { ychange: { default: null } },
group: 'block',
parseDOM: [{ tag: 'hr' }],
toDOM (node) {
return ['hr', calcYchangeDomAttrs(node.attrs)]
}
},
// :: NodeSpec A heading textblock, with a `level` attribute that
// should hold the number 1 to 6. Parsed and serialized as `<h1>` to
// `<h6>` elements.
heading: {
attrs: {
level: { default: 1 },
ychange: { default: null }
},
content: 'inline*',
group: 'block',
defining: true,
parseDOM: [{ tag: 'h1', attrs: { level: 1 } },
{ tag: 'h2', attrs: { level: 2 } },
{ tag: 'h3', attrs: { level: 3 } },
{ tag: 'h4', attrs: { level: 4 } },
{ tag: 'h5', attrs: { level: 5 } },
{ tag: 'h6', attrs: { level: 6 } }],
toDOM (node) { return ['h' + node.attrs.level, calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A code listing. Disallows marks or non-text inline
// nodes by default. Represented as a `<pre>` element with a
// `<code>` element inside of it.
code_block: {
attrs: { ychange: { default: null } },
content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
toDOM (node) { return ['pre', calcYchangeDomAttrs(node.attrs), ['code', 0]] }
},
// :: NodeSpec The text node.
text: {
group: 'inline'
},
// :: NodeSpec An inline image (`<img>`) node. Supports `src`,
// `alt`, and `href` attributes. The latter two default to the empty
// string.
image: {
inline: true,
attrs: {
ychange: { default: null },
src: {},
alt: { default: null },
title: { default: null }
},
group: 'inline',
draggable: true,
parseDOM: [{ tag: 'img[src]',
getAttrs (dom) {
return {
src: dom.getAttribute('src'),
title: dom.getAttribute('title'),
alt: dom.getAttribute('alt')
}
} }],
toDOM (node) {
const domAttrs = {
src: node.attrs.src,
title: node.attrs.title,
alt: node.attrs.alt
}
return ['img', calcYchangeDomAttrs(node.attrs, domAttrs)]
}
},
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
hard_break: {
inline: true,
group: 'inline',
selectable: false,
parseDOM: [{ tag: 'br' }],
toDOM () { return brDOM }
}
}
const emDOM = ['em', 0]; const strongDOM = ['strong', 0]; const codeDOM = ['code', 0]
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
export const marks = {
// :: MarkSpec A link. Has `href` and `title` attributes. `title`
// defaults to the empty string. Rendered and parsed as an `<a>`
// element.
link: {
attrs: {
href: {},
title: { default: null }
},
inclusive: false,
parseDOM: [{ tag: 'a[href]',
getAttrs (dom) {
return { href: dom.getAttribute('href'), title: dom.getAttribute('title') }
} }],
toDOM (node) { return ['a', node.attrs, 0] }
},
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`.
em: {
parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }],
toDOM () { return emDOM }
},
// :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
// also match `<b>` and `font-weight: bold`.
strong: {
parseDOM: [{ tag: 'strong' },
// This works around a Google Docs misbehavior where
// pasted content will be inexplicably wrapped in `<b>`
// tags with a font-weight normal.
{ tag: 'b', getAttrs: node => node.style.fontWeight !== 'normal' && null },
{ style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null }],
toDOM () { return strongDOM }
},
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
parseDOM: [{ tag: 'code' }],
toDOM () { return codeDOM }
},
ychange: {
attrs: {
user: { default: null },
state: { default: null }
},
inclusive: false,
parseDOM: [{ tag: 'ychange' }],
toDOM (node) {
return ['ychange', { ychange_user: node.attrs.user, ychange_state: node.attrs.state }, 0]
}
}
}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
// which are defined in the [`prosemirror-schema-list`](#schema-list)
// module.
//
// To reuse elements from this schema, extend or read from its
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
export const schema = new Schema({ nodes, marks })

330
examples/prosemirror.css Normal file
View File

@@ -0,0 +1,330 @@
.ProseMirror {
position: relative;
}
.ProseMirror {
word-wrap: break-word;
white-space: pre-wrap;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror li {
position: relative;
}
.ProseMirror-hideselection *::selection { background: transparent; }
.ProseMirror-hideselection *::-moz-selection { background: transparent; }
.ProseMirror-hideselection { caret-color: transparent; }
.ProseMirror-selectednode {
outline: 2px solid #8cf;
}
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
outline: none;
}
li.ProseMirror-selectednode:after {
content: "";
position: absolute;
left: -32px;
right: -2px; top: -2px; bottom: -2px;
border: 2px solid #8cf;
pointer-events: none;
}
.ProseMirror-textblock-dropdown {
min-width: 3em;
}
.ProseMirror-menu {
margin: 0 -4px;
line-height: 1;
}
.ProseMirror-tooltip .ProseMirror-menu {
width: -webkit-fit-content;
width: fit-content;
white-space: pre;
}
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
}
.ProseMirror-menuseparator {
border-right: 1px solid #ddd;
margin-right: 3px;
}
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
font-size: 90%;
white-space: nowrap;
}
.ProseMirror-menu-dropdown {
vertical-align: 1px;
cursor: pointer;
position: relative;
padding-right: 15px;
}
.ProseMirror-menu-dropdown-wrap {
padding: 1px 0 1px 4px;
display: inline-block;
position: relative;
}
.ProseMirror-menu-dropdown:after {
content: "";
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 2px);
}
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
position: absolute;
background: white;
color: #666;
border: 1px solid #aaa;
padding: 2px;
}
.ProseMirror-menu-dropdown-menu {
z-index: 15;
min-width: 6em;
}
.ProseMirror-menu-dropdown-item {
cursor: pointer;
padding: 2px 8px 2px 4px;
}
.ProseMirror-menu-dropdown-item:hover {
background: #f2f2f2;
}
.ProseMirror-menu-submenu-wrap {
position: relative;
margin-right: -4px;
}
.ProseMirror-menu-submenu-label:after {
content: "";
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 4px);
}
.ProseMirror-menu-submenu {
display: none;
min-width: 4em;
left: 100%;
top: -3px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-disabled {
opacity: .3;
}
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
display: block;
}
.ProseMirror-menubar {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
position: relative;
min-height: 1em;
color: #666;
padding: 1px 6px;
top: 0; left: 0; right: 0;
border-bottom: 1px solid silver;
background: white;
z-index: 10;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: visible;
}
.ProseMirror-icon {
display: inline-block;
line-height: .8;
vertical-align: -2px; /* Compensate for padding */
padding: 2px 8px;
cursor: pointer;
}
.ProseMirror-menu-disabled.ProseMirror-icon {
cursor: default;
}
.ProseMirror-icon svg {
fill: currentColor;
height: 1em;
}
.ProseMirror-icon span {
vertical-align: text-top;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
/* Add space around the hr to make clicking it easier */
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
border: none;
margin: 1em 0;
}
.ProseMirror-example-setup-style hr:after {
content: "";
display: block;
height: 1px;
background-color: silver;
line-height: 2px;
}
.ProseMirror ul, .ProseMirror ol {
padding-left: 30px;
}
.ProseMirror blockquote {
padding-left: 1em;
border-left: 3px solid #eee;
margin-left: 0; margin-right: 0;
}
.ProseMirror-example-setup-style img {
cursor: default;
}
.ProseMirror-prompt {
background: white;
padding: 5px 10px 5px 15px;
border: 1px solid silver;
position: fixed;
border-radius: 3px;
z-index: 11;
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
}
.ProseMirror-prompt h5 {
margin: 0;
font-weight: normal;
font-size: 100%;
color: #444;
}
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
background: #eee;
border: none;
outline: none;
}
.ProseMirror-prompt input[type="text"] {
padding: 0 4px;
}
.ProseMirror-prompt-close {
position: absolute;
left: 2px; top: 1px;
color: #666;
border: none; background: transparent; padding: 0;
}
.ProseMirror-prompt-close:after {
content: "✕";
font-size: 12px;
}
.ProseMirror-invalid {
background: #ffc;
border: 1px solid #cc7;
border-radius: 4px;
padding: 5px 10px;
position: absolute;
min-width: 10em;
}
.ProseMirror-prompt-buttons {
margin-top: 5px;
display: none;
}
#editor, .editor {
background: white;
color: black;
background-clip: padding-box;
border-radius: 4px;
border: 2px solid rgba(0, 0, 0, 0.2);
padding: 5px 0;
margin-bottom: 23px;
}
.ProseMirror p:first-child,
.ProseMirror h1:first-child,
.ProseMirror h2:first-child,
.ProseMirror h3:first-child,
.ProseMirror h4:first-child,
.ProseMirror h5:first-child,
.ProseMirror h6:first-child {
margin-top: 10px;
}
.ProseMirror {
padding: 4px 8px 4px 14px;
line-height: 1.2;
outline: none;
}
.ProseMirror p { margin-bottom: 1em }

View File

@@ -2,7 +2,7 @@
<html>
<head>
<title>Yjs Prosemirror Example</title>
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
<link rel=stylesheet href="./prosemirror.css">
<style>
placeholder {
display: inline;
@@ -30,6 +30,7 @@
border-left-width: 2px;
border-color: orange;
height: 1em;
word-break: normal;
}
.ProseMirror-yjs-cursor > div {
position: relative;
@@ -45,21 +46,59 @@
padding-left: 2px;
padding-right: 2px;
}
[ychange_state] {
position: relative;
}
[ychange_state]:hover::before {
content: attr(ychange_user);
background-color: #fa8100;
position: absolute;
top: -14px;
right: 0;
font-size: 12px;
padding: 0 2px;
border-radius: 3px 3px 0 0;
color: #fdfdfe;
user-select: none;
word-break: normal;
}
*[ychange_state='added'] {
background-color: #fa810069;
}
ychange[ychange_state='removed'] {
color: rgb(250, 129, 0);
text-decoration: line-through;
}
*:not(ychange)[ychange_state='removed'] {
background-color: #ff9494c9;
text-decoration: line-through;
}
img[ychange_state='removed'] {
padding: 2px;
}
.y-connect-btn {
position: absolute;
top: 20px;
right: 20px;
}
</style>
</head>
<body>
<p>This example shows how to bind a YXmlFragment type to a <a href="http://prosemirror.net">Prosemirror</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<button type="button" class="y-connect-btn">Disconnect</button>
<p>This example shows how to bind a YXmlFragment to a <a href="http://prosemirror.net">Prosemirror</a> editor using <a href="https://github.com/y-js/y-prosemirror">y-prosemirror</a>.</p>
<p>The content of this editor is shared with every client that visits this domain.</p>
<div class="code-html">
<div id="editor" style="margin-bottom: 23px"></div>
<div style="display: none" id="content"></div>
</div>
<!-- The actual source file for the following code is found in ./prosemirror.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/prosemirror.js">
<script class="code-js" src="./build/prosemirror.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from '../provider/websocket.js'
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { prosemirrorPlugin, cursorPlugin } from 'yjs/bindings/prosemirror'
import * as conf from './exampleConfig.js'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
@@ -67,7 +106,7 @@ import { DOMParser } from 'prosemirror-model'
import { schema } from 'prosemirror-schema-basic'
import { exampleSetup } from 'prosemirror-example-setup'
const provider = new WebsocketProvider('wss://api.yjs.website')
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror')
const type = ydocument.define('prosemirror', Y.XmlFragment)

View File

@@ -1,24 +1,36 @@
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror.js'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { prosemirrorPlugin, cursorPlugin } from 'y-prosemirror'
import * as conf from './exampleConfig.js'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { DOMParser } from 'prosemirror-model'
import { schema } from 'prosemirror-schema-basic'
import { schema } from './prosemirror-schema.js'
import { exampleSetup } from 'prosemirror-example-setup'
// import { noteHistoryPlugin } from './prosemirror-history.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror')
const type = ydocument.define('prosemirror', Y.XmlFragment)
const ydocument = provider.get('prosemirror' /*, { gc: false } */)
const type = ydocument.get('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin])
plugins: exampleSetup({ schema }).concat([prosemirrorPlugin(type), cursorPlugin])
})
})
const connectBtn = document.querySelector('.y-connect-btn')
connectBtn.addEventListener('click', () => {
if (ydocument.wsconnected) {
ydocument.disconnect()
connectBtn.textContent = 'Connect'
} else {
ydocument.connect()
connectBtn.textContent = 'Disconnect'
}
})
window.example = { provider, ydocument, type, prosemirrorView }

View File

@@ -14,14 +14,16 @@
</div>
</div>
<!-- The actual source file for the following code is found in ./quill.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/quill.js">
<script class="code-js" src="./build/quill.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { QuillBinding } from 'yjs/bindings/quill.js'
import * as conf from './exampleConfig.js'
import Quill from 'quill'
const provider = new WebsocketProvider('wss://api.yjs.website')
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('quill')
const ytext = ydocument.define('quill', Y.Text)

View File

@@ -1,6 +1,6 @@
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { QuillBinding } from '../bindings/quill.js'
import * as Y from '../src/index.js'
import { WebsocketProvider } from 'y-websocket'
import { QuillBinding } from 'y-quill'
import * as conf from './exampleConfig.js'

View File

@@ -11,12 +11,14 @@
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<!-- The actual source file for the following code is found in ./textarea.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/textarea.js">
<script class="code-js" src="./build/textarea.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { TextareaBinding } from 'yjs/bindings/textarea.js'
const provider = new WebsocketProvider('wss://api.yjs.website')
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('textarea')
const type = ydocument.define('textarea', Y.Text)
const textarea = document.querySelector('textarea')

View File

@@ -1,12 +1,11 @@
import * as Y from '../index.js'
import { WebsocketProvider } from '../provider/websocket.js'
import { TextareaBinding } from '../bindings/textarea.js'
import { WebsocketProvider } from 'y-websocket'
import { TextareaBinding } from 'y-textarea'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('textarea')
const type = ydocument.define('textarea', Y.Text)
const type = ydocument.getText('textarea')
const textarea = document.querySelector('textarea')
const binding = new TextareaBinding(type, textarea)

View File

@@ -1,33 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style type="text/css" media="screen">
#aceContainer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.inserted {
position:absolute;
z-index:20;
background-color: #FFC107;
}
.deleted {
position:absolute;
z-index:20;
background-color: #FFC107;
}
</style>
</head>
<body>
<div id="aceContainer"></div>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/ace-builds/src/ace.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,17 +0,0 @@
/* global Y, ace */
let y = new Y('ace-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yAce = y
// bind the textarea to a shared text element
var editor = ace.edit('aceContainer')
editor.setTheme('ace/theme/chrome')
editor.getSession().setMode('ace/mode/javascript')
y.define('ace', Y.Text).bindAce(editor)

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<style>
#chat p span {
color: blue;
}
</style>
<div id="chat"></div>
<form id="chatform">
<input name="username" type="text" style="width:15%;">
<input name="message" type="text" style="width:60%;">
<input type="submit" value="Send">
</form>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,65 +0,0 @@
/* global Y */
let y = new Y('chat-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yChat = y
let chatprotocol = y.define('chatprotocol', Y.Array)
let chatcontainer = document.querySelector('#chat')
// This functions inserts a message at the specified position in the DOM
const appendMessage = (message, position) => {
var p = document.createElement('p')
var uname = document.createElement('span')
uname.appendChild(document.createTextNode(message.username + ': '))
p.appendChild(uname)
p.appendChild(document.createTextNode(message.message))
chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
}
// This function makes sure that only 7 messages exist in the chat history.
// The rest is deleted
const cleanupChat = () => {
if (chatprotocol.length > 7) {
chatprotocol.delete(0, chatprotocol.length - 7)
}
}
cleanupChat()
// Insert the initial content
chatprotocol.toArray().forEach(appendMessage)
// whenever content changes, make sure to reflect the changes in the DOM
chatprotocol.observe(event => {
// concurrent insertions may result in a history > 7, so cleanup here
cleanupChat()
chatcontainer.innerHTML = ''
chatprotocol.toArray().forEach(appendMessage)
})
document.querySelector('#chatform').onsubmit = function (event) {
// the form is submitted
var message = {
username: this.querySelector('[name=username]').value,
message: this.querySelector('[name=message]').value
}
if (message.username.length > 0 && message.message.length > 0) {
if (chatprotocol.length > 6) {
// If we are goint to insert the 8th element, make sure to delete first.
chatprotocol.delete(0)
}
// Here we insert a message in the shared chat type.
// This will call the observe function (see line 40)
// and reflect the change in the DOM
chatprotocol.push([message])
this.querySelector('[name=message]').value = ''
}
// Do not send this form!
event.preventDefault()
return false
}

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="codeMirrorContainer"></div>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
<style>
.CodeMirror {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
</style>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,16 +0,0 @@
/* global Y, CodeMirror */
let y = new Y('codemirror-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yCodeMirror = y
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
mode: 'javascript',
lineNumbers: true
})
y.define('codemirror', Y.Text).bindCodeMirror(editor)

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<style>
path {
fill: none;
stroke: blue;
stroke-width: 1px;
stroke-linejoin: round;
stroke-linecap: round;
}
</style>
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,74 +0,0 @@
/* globals Y, d3 */
let y = new Y('drawing-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yDrawing = y
var drawing = y.define('drawing', Y.Array)
var renderPath = d3.svg.line()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.interpolate('basic')
var svg = d3.select('#drawingCanvas')
.call(d3.behavior.drag()
.on('dragstart', dragstart)
.on('drag', drag)
.on('dragend', dragend))
// create line from a shared array object and update the line when the array changes
function drawLine (yarray) {
var line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
yarray.observe(function (event) {
line.remove()
line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
})
}
// call drawLine every time an array is appended
drawing.observe(function (event) {
event.removedElements.forEach(function () {
// if one is deleted, all will be deleted!!
svg.selectAll('path').remove()
})
event.addedElements.forEach(function (path) {
drawLine(path)
})
})
// draw all existing content
for (var i = 0; i < drawing.length; i++) {
drawLine(drawing.get(i))
}
// clear canvas on request
document.querySelector('#clearDrawingCanvas').onclick = function () {
drawing.delete(0, drawing.length)
}
var sharedLine = null
function dragstart () {
drawing.insert(drawing.length, [Y.Array])
sharedLine = drawing.get(drawing.length - 1)
}
// After one dragged event is recognized, we ignore them for 33ms.
var ignoreDrag = null
function drag () {
if (sharedLine != null && ignoreDrag == null) {
ignoreDrag = window.setTimeout(function () {
ignoreDrag = null
}, 10)
sharedLine.push([d3.mouse(this)])
}
}
function dragend () {
sharedLine = null
window.clearTimeout(ignoreDrag)
ignoreDrag = null
}

View File

@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="./index.js"></script>
<style>
magic-drawing .drawingCanvas path {
fill: none;
stroke: blue;
stroke-width: 2px;
stroke-linejoin: round;
stroke-linecap: round;
}
magic-drawing .drawingCanvas {
width: 500px;
height: 500px;
cursor: default;
padding:1px;
border:1px solid #021a40;
}
magic-drawing .clearDrawingButton {
position: absolute;
top: 0;
left: 0;
}
magic-drawing {
position: relative;
display: block;
}
</style>
</head>
<body contenteditable="true">
</body>
</html>

View File

@@ -1,134 +0,0 @@
/* global Y, d3 */
const hooks = {
'magic-drawing': {
fillType: function (dom, type) {
initDrawingBindings(type, dom)
},
createDom: function (type) {
const dom = document.createElement('magic-drawing')
initDrawingBindings(type, dom)
return dom
}
}
}
window.onload = function () {
window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks })
}
window.addMagicDrawing = function addMagicDrawing () {
let mt = document.createElement('magic-drawing')
mt.setAttribute('data-yjs-hook', 'magic-drawing')
document.body.append(mt)
}
var renderPath = d3.svg.line()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.interpolate('basic')
function initDrawingBindings (type, dom) {
dom.contentEditable = 'false'
dom.setAttribute('data-yjs-hook', 'magic-drawing')
var drawing = type.get('drawing')
if (drawing === undefined) {
drawing = type.set('drawing', new Y.Array())
}
var canvas = dom.querySelector('.drawingCanvas')
if (canvas == null) {
canvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
canvas.setAttribute('class', 'drawingCanvas')
canvas.setAttribute('viewbox', '0 0 100 100')
dom.insertBefore(canvas, null)
}
var clearDrawingButton = dom.querySelector('.clearDrawingButton')
if (clearDrawingButton == null) {
clearDrawingButton = document.createElement('button')
clearDrawingButton.setAttribute('type', 'button')
clearDrawingButton.setAttribute('class', 'clearDrawingButton')
clearDrawingButton.innerText = 'Clear Drawing'
dom.insertBefore(clearDrawingButton, null)
}
var svg = d3.select(canvas)
.call(d3.behavior.drag()
.on('dragstart', dragstart)
.on('drag', drag)
.on('dragend', dragend))
// create line from a shared array object and update the line when the array changes
function drawLine (yarray, svg) {
var line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
yarray.observe(function (event) {
line.remove()
line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
})
}
// call drawLine every time an array is appended
drawing.observe(function (event) {
event.removedElements.forEach(function () {
// if one is deleted, all will be deleted!!
svg.selectAll('path').remove()
})
event.addedElements.forEach(function (path) {
drawLine(path, svg)
})
})
// draw all existing content
for (var i = 0; i < drawing.length; i++) {
drawLine(drawing.get(i), svg)
}
// clear canvas on request
clearDrawingButton.onclick = function () {
drawing.delete(0, drawing.length)
}
var sharedLine = null
function dragstart () {
drawing.insert(drawing.length, [Y.Array])
sharedLine = drawing.get(drawing.length - 1)
}
// After one dragged event is recognized, we ignore them for 33ms.
var ignoreDrag = null
function drag () {
if (sharedLine != null && ignoreDrag == null) {
ignoreDrag = window.setTimeout(function () {
ignoreDrag = null
}, 10)
sharedLine.push([d3.mouse(this)])
}
}
function dragend () {
sharedLine = null
window.clearTimeout(ignoreDrag)
ignoreDrag = null
}
}
let y = new Y('html-editor-drawing-hook-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yXml = y
window.yXmlType = y.define('xml', Y.XmlFragment)
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
captureTimeout: 500
})
document.onkeydown = function interceptUndoRedo (e) {
if (e.keyCode === 90 && e.metaKey) {
if (!e.shiftKey) {
window.undoManager.undo()
} else {
window.undoManager.redo()
}
e.preventDefault()
}
}

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<script src="./index.js" type="module"></script>
</head>
<body>
<label for="room">Room: </label>
<input type="text" id="room" name="room">
<div id="content" contenteditable style="position:absolute;top:35px;left:0;right:0;bottom:0;outline: 0px solid transparent;"></div>
</body>
</html>

View File

@@ -1,77 +0,0 @@
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
import Y from '../../src/Y.js'
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
import UndoManager from '../../src/Util/UndoManager.js'
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
import YXmlText from '../../src/Types/YXml/YXmlText.js'
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.js'
const connector = new YWebsocketsConnector()
const persistence = new YIndexdDBPersistence()
const roomInput = document.querySelector('#room')
let currentRoomName = null
let y = null
let domBinding = null
function setRoomName (roomName) {
if (currentRoomName !== roomName) {
console.log(`change room: "${roomName}"`)
roomInput.value = roomName
currentRoomName = roomName
location.hash = '#' + roomName
if (y !== null) {
domBinding.destroy()
}
const room = connector._rooms.get(roomName)
if (room !== undefined) {
y = room.y
} else {
y = new Y(roomName, null, null, { gc: true })
persistence.connectY(roomName, y).then(() => {
// connect after persisted content was applied to y
// If we don't wait for persistence, the other peer will send all data, waisting
// network bandwidth..
connector.connectY(roomName, y)
})
window.y = y
}
window.y = y
window.yXmlType = y.define('xml', YXmlFragment)
domBinding = new DomBinding(window.yXmlType, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
}
}
window.setRoomName = setRoomName
window.createRooms = function (i = 0) {
setInterval(function () {
setRoomName(i + '')
i++
const nodes = []
for (let j = 0; j < 100; j++) {
const node = new YXmlElement('p')
node.insert(0, [new YXmlText(`This is the ${i}th paragraph of room ${i}`)])
nodes.push(node)
}
y.share.xml.insert(0, nodes)
}, 100)
}
connector.syncPersistence(persistence)
window.connector = connector
window.persistence = persistence
window.onload = function () {
setRoomName((location.hash || '#default').slice(1))
roomInput.addEventListener('input', e => {
const roomName = e.target.value
setRoomName(roomName)
})
}

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="codeMirrorContainer"></div>
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
<style>
.CodeMirror {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
</style>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,19 +0,0 @@
/* global Y, CodeMirror */
const persistence = new Y.IndexedDB()
const connector = {
connector: {
name: 'websockets-client',
room: 'codemirror-example'
}
}
const y = new Y('codemirror-example', connector, persistence)
window.yCodeMirror = y
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
mode: 'javascript',
lineNumbers: true
})
y.define('codemirror', Y.Text).bindCodeMirror(editor)

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.draggable {
cursor: move;
}
</style>
</head>
<body>
<svg id="puzzle-example" width="100%" viewBox="0 0 800 800">
<g>
<path d="M 311.76636,154.23389 C 312.14136,171.85693 318.14087,184.97998 336.13843,184.23047 C 354.13647,183.48047 351.88647,180.48096 354.88599,178.98096 C 357.8855,177.48096 368.38452,170.35693 380.00806,169.98193 C 424.61841,168.54297 419.78296,223.6001 382.25757,223.6001 C 377.75806,223.6001 363.51001,219.10107 356.38599,211.97656 C 349.26196,204.85254 310.64185,207.10254 314.76636,236.34863 C 316.34888,247.5708 324.08374,267.90723 324.84595,286.23486 C 325.29321,296.99414 323.17603,307.00635 321.58911,315.6377 C 360.11353,305.4585 367.73462,304.30518 404.00513,312.83936 C 410.37915,314.33887 436.62573,310.21436 421.25269,290.3418 C 405.87964,270.46924 406.25464,248.34717 417.12817,240.84814 C 428.00171,233.34912 446.74976,228.84961 457.99829,234.09912 C 469.24683,239.34814 484.61987,255.84619 475.24585,271.59424 C 465.87231,287.34229 452.74878,290.7168 456.49829,303.84033 C 460.2478,316.96387 479.74536,320.33838 500.74292,321.83789 C 509.70142,322.47803 527.97192,323.28467 542.10864,320.12939 C 549.91821,318.38672 556.92212,315.89502 562.46753,313.56396 C 561.40796,277.80664 560.84888,245.71729 560.3606,241.97314 C 558.85278,230.41455 542.49536,217.28564 525.86499,223.2251 C 520.61548,225.1001 519.86548,231.84912 505.24243,232.59912 C 444.92798,235.69238 462.06958,143.26709 525.86499,180.48096 C 539.52759,188.45068 575.19409,190.7583 570.10913,156.85889 C 567.85962,141.86035 553.98608,102.86523 553.98608,102.86523 C 553.98608,102.86523 477.23755,111.82227 451.99878,91.991699 C 441.50024,83.74292 444.87476,69.494629 449.37427,61.245605 C 453.87378,52.996582 465.12231,46.622559 464.74731,36.123779 C 463.02563,-12.086426 392.96704,-10.902832 396.5061,36.873535 C 397.25562,46.997314 406.62964,52.621582 410.75415,60.495605 C 420.00757,78.161377 405.50024,96.073486 384.50757,99.490723 C 377.36206,100.65381 349.17505,102.65332 320.39429,102.23486 C 319.677,102.22461 318.95923,102.21143 318.24194,102.19775 C 315.08423,120.9751 311.55688,144.39697 311.76636,154.23389 z " style="fill:#f2c569;stroke:#000000" id="path2502"/>
<path d="M 500.74292,321.83789 C 479.74536,320.33838 460.2478,316.96387 456.49829,303.84033 C 452.74878,290.7168 465.87231,287.34229 475.24585,271.59424 C 484.61987,255.84619 469.24683,239.34814 457.99829,234.09912 C 446.74976,228.84961 428.00171,233.34912 417.12817,240.84814 C 406.25464,248.34717 405.87964,270.46924 421.25269,290.3418 C 436.62573,310.21436 410.37915,314.33887 404.00513,312.83936 C 367.73462,304.30518 360.11353,305.4585 321.58911,315.6377 C 320.56372,321.21484 319.75854,326.2207 320.01538,330.46191 C 320.76538,342.83545 329.3894,385.95508 327.8894,392.7041 C 326.3894,399.45312 313.64136,418.20117 297.89331,407.32715 C 282.14526,396.45361 276.52075,393.4541 265.27222,394.5791 C 254.02368,395.70361 239.77563,402.07812 239.77563,419.32568 C 239.77563,436.57373 250.27417,449.69727 268.64673,447.82227 C 287.36353,445.9126 317.92163,423.11035 325.63989,452.69678 C 330.1394,469.94434 330.51392,487.19238 330.1394,498.44092 C 329.95825,503.87646 326.09985,518.06592 322.16089,531.28125 C 353.2854,532.73682 386.47095,531.26611 394.2561,529.93701 C 430.30933,523.78174 429.31909,496.09766 412.62866,477.44385 C 406.25464,470.31934 401.75513,455.32129 405.87964,444.82275 C 414.07056,423.97314 458.8064,422.17773 473.37134,438.82324 C 483.86987,450.82178 475.99585,477.44385 468.49683,482.69287 C 453.52222,493.17529 457.22485,516.83008 473.37134,528.06201 C 504.79126,549.91943 572.35913,535.56152 572.35913,535.56152 C 572.35913,535.56152 567.85962,498.06592 567.48462,471.81934 C 567.10962,445.57275 589.60669,450.07227 593.3562,450.07227 C 597.10571,450.07227 604.22974,455.32129 609.47925,459.4458 C 614.72876,463.57031 618.85327,469.94434 630.85181,470.69434 C 677.43726,473.60596 674.58813,420.7373 631.97632,413.32666 C 623.35229,411.82666 614.72876,416.32617 603.10522,424.57519 C 591.48169,432.82422 577.23315,425.32519 570.10913,417.45117 C 566.07788,412.99561 563.8479,360.16406 562.46753,313.56396 C 556.92212,315.89502 549.91821,318.38672 542.10864,320.12939 C 527.97192,323.28467 509.70142,322.47803 500.74292,321.83789 z " style="fill:#f3f3d6;stroke:#000000" id="path2504"/>
<path d="M 240.52563,141.86035 C 257.60327,159.6499 243.94507,188.68799 214.65356,190.22949 C 185.09448,191.78516 164.66675,157.17822 190.28589,136.61621 C 200.49585,128.42139 198.05786,114.12158 179.78296,106.98975 C 154.4187,97.091553 90.54419,107.73975 90.54419,107.73975 C 90.54419,107.73975 100.88794,135.11328 101.41772,168.48242 C 101.79272,192.104 68.796875,189.47949 63.172607,186.85498 C 57.54834,184.23047 45.924805,173.73145 37.675781,173.73145 C -14.411865,173.73145 -10.013184,245.84375 39.925537,232.22412 C 48.174316,229.97461 56.42334,220.97559 68.796875,222.47559 C 81.17041,223.9751 87.544434,232.59912 87.544434,246.09766 C 87.544434,252.51709 87.0354,281.24268 86.340576,312.87012 C 119.15894,313.67676 160.60962,314.46582 170.03442,313.58887 C 186.15698,312.08936 195.90601,301.59033 188.40698,293.3418 C 180.90796,285.09277 156.16089,256.59619 179.03296,239.34814 C 201.90503,222.10059 235.65112,231.84912 239.77563,247.22217 C 243.90015,262.59521 240.52563,273.46924 234.90112,279.09326 C 229.27661,284.71777 210.52905,298.96582 221.40259,308.71484 C 232.27661,318.46338 263.77222,330.83691 302.39282,320.71338 C 309.58862,318.82715 315.92114,317.13525 321.58911,315.6377 C 323.17603,307.00635 325.29321,296.99414 324.84595,286.23486 C 324.08374,267.90723 316.34888,247.5708 314.76636,236.34863 C 310.64185,207.10254 349.26196,204.85254 356.38599,211.97656 C 363.51001,219.10107 377.75806,223.6001 382.25757,223.6001 C 419.78296,223.6001 424.61841,168.54297 380.00806,169.98193 C 368.38452,170.35693 357.8855,177.48096 354.88599,178.98096 C 351.88647,180.48096 354.13647,183.48047 336.13843,184.23047 C 318.14087,184.97998 312.14136,171.85693 311.76636,154.23389 C 311.55688,144.39697 315.08423,120.9751 318.24194,102.19775 C 290.37524,101.67725 262.46069,98.968262 254.39868,97.991211 C 233.38013,95.443359 217.17456,117.53662 240.52563,141.86035 z " style="fill:#bebcdb;stroke:#000000" id="path2506"/>
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
</g>
</svg>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,67 +0,0 @@
/* global Y, d3 */
let y = new Y('jigsaw-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
let jigsaw = y.define('jigsaw', Y.Map)
window.yJigsaw = y
var origin // mouse start position - translation of piece
var drag = d3.behavior.drag()
.on('dragstart', function (params) {
// get the translation of the element
var translation = d3
.select(this)
.attr('transform')
.slice(10, -1)
.split(',')
.map(Number)
// mouse coordinates
var mouse = d3.mouse(this.parentNode)
origin = {
x: mouse[0] - translation[0],
y: mouse[1] - translation[1]
}
})
.on('drag', function () {
var mouse = d3.mouse(this.parentNode)
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
var y = mouse[1] - origin.y
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
})
.on('dragend', function (piece, i) {
// save the current translation of the puzzle piece
var mouse = d3.mouse(this.parentNode)
var x = mouse[0] - origin.x
var y = mouse[1] - origin.y
jigsaw.set(piece, {x: x, y: y})
})
var data = ['piece1', 'piece2', 'piece3', 'piece4']
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
pieces
.classed('draggable', true)
.attr('transform', function (piece) {
var translation = piece.get('translation') || {x: 0, y: 0}
return 'translate(' + translation.x + ',' + translation.y + ')'
}).call(drag)
data.forEach(function (piece) {
jigsaw.observe(function () {
// whenever a property of a piece changes, update the translation of the pieces
pieces
.transition()
.attr('transform', function (piece) {
var translation = piece.get(piece)
if (translation == null || typeof translation.x !== 'number' || typeof translation.y !== 'number') {
translation = { x: 0, y: 0 }
}
return 'translate(' + translation.x + ',' + translation.y + ')'
})
})
})

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="monacoContainer"></div>
<style>
#monacoContainer {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
</style>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
/* global Y, monaco */
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
let y = new Y('monaco-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
require(['vs/editor/editor.main'], function () {
window.yMonaco = y
// Create Monaco editor
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
language: 'javascript'
})
// Bind to y.share.monaco
y.define('monaco', Y.Text).bindMonaco(editor)
})

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<script src="./index.js" type="module"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="sidebar">
<h3 id="createNoteButton">+ Create Note</h3>
<div class="notelist"></div>
</div>
<div class="main">
<h1 id="headline"></h1>
<div id="editor" contenteditable="true"></div>
</div>
</body>
</html>

View File

@@ -1,132 +0,0 @@
/* eslint-env browser */
import { createYdbClient } from '../../YdbClient/index.js'
import Y from '../../src/Y.dist.js'
import * as ydb from '../../YdbClient/YdbClient.js'
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
createYdbClient('ws://localhost:8899/ws').then(ydbclient => {
const y = ydbclient.getY('notelist')
let ynotelist = y.define('notelist', Y.Array)
window.ynotelist = ynotelist
const domNoteList = document.querySelector('.notelist')
// utils
const addEventListener = (element, eventname, f) => element.addEventListener(eventname, f)
// create note button
const createNoteButton = event => {
ynotelist.insert(0, [{
guid: uuidv4(),
title: 'Note #' + ynotelist.length
}])
}
addEventListener(document.querySelector('#createNoteButton'), 'click', createNoteButton)
window.createNote = createNoteButton
window.createNotes = n => {
y.transact(() => {
for (let i = 0; i < n; i++) {
createNoteButton()
}
})
}
// clear note list function
window.clearNotes = () => ynotelist.delete(0, ynotelist.length)
// update editor and editor title
let domBinding = null
const updateEditor = () => {
domNoteList.querySelectorAll('a').forEach(a => a.classList.remove('selected'))
const domNote = document.querySelector('.notelist').querySelector(`[href="${location.hash}"]`)
if (domNote !== null) {
domNote.classList.add('selected')
const note = ynotelist.toArray().find(note => note.guid === location.hash.slice(1))
if (note !== undefined) {
const ydoc = ydbclient.getY(note.guid)
const ycontent = ydoc.define('content', Y.XmlFragment)
if (domBinding !== null) {
domBinding.destroy()
}
domBinding = new DomBinding(ycontent, document.querySelector('#editor'))
document.querySelector('#headline').innerText = note.title
document.querySelector('#editor').focus()
}
}
}
// listen to url-hash changes
addEventListener(window, 'hashchange', updateEditor)
updateEditor()
const styleSyncedState = (div, noteSyncedState) => {
let classes = []
if (noteSyncedState.persisted) {
classes.push('persisted')
} else {
if (noteSyncedState.upsynced) {
classes.push('upsynced')
} else {
classes.push('noupsynced')
}
if (noteSyncedState.downsynced) {
classes.push('downsynced')
} else {
classes.push('nodownsynced')
}
}
div.setAttribute('class', classes.join(' '))
}
ydbclient.on('syncstate', event => event.updated.forEach((state, room) => {
const a = document.querySelector(`[href="#${room}"]`)
if (a !== null) {
styleSyncedState(a.firstChild, state)
}
}))
// render note list
const renderNoteList = (elementList, insertRef = domNoteList.firstChild) => {
const fragment = document.createDocumentFragment()
const addNow = elementList.splice(0, 100)
addNow.forEach(note => {
const a = document.createElement('a')
const div = document.createElement('div')
a.insertBefore(div, null)
a.setAttribute('href', '#' + note.guid)
div.innerText = note.title
styleSyncedState(div, ydbclient.getRoomState(note.guid))
fragment.insertBefore(a, null)
})
if (domBinding == null) {
updateEditor()
}
domNoteList.insertBefore(fragment, insertRef)
if (elementList.length > 0) {
setTimeout(() => renderNoteList(elementList, insertRef), 100)
}
}
{
const notelist = ynotelist.toArray()
if (notelist.length > 0) {
renderNoteList(notelist)
ydb.subscribeRooms(ydbclient, notelist.map(note => note.guid))
}
}
ynotelist.observe(event => {
const addedNotes = []
event.addedElements.forEach(itemJson => itemJson._content.forEach(json => addedNotes.push(json)))
renderNoteList(addedNotes.slice().reverse()) // renderNoteList modifies addedNotes, so first make a copy of it
setTimeout(() => {
ydb.subscribeRooms(ydbclient, addedNotes.map(note => note.guid))
}, 200)
if (domBinding === null) {
updateEditor()
}
})
})

View File

@@ -1,100 +0,0 @@
.sidebar {
height: 100%; /* Full-height: remove this if you want "auto" height */
width: 180px; /* Set the width of the sidebar */
position: fixed; /* Fixed Sidebar (stay in place on scroll) */
z-index: 1; /* Stay on top */
top: 0; /* Stay at the top */
left: 0;
background-color: #111; /* Black */
overflow-x: hidden; /* Disable horizontal scroll */
padding-top: 20px;
color: #50abff;
}
#createNoteButton {
padding-left: .5em;
padding-top: .5em;
padding-bottom: .7em;
margin: 0;
cursor: pointer;
}
.notelist > a {
padding: 6px 8px 6px 16px;
text-decoration: none;
font-size: 13px;
color: #818181;
display: block;
}
.notelist > a.selected {
border-style: outset;
}
.notelist > a > div {
position: relative;
display: inline;
}
/* When you mouse over the navigation links, change their color */
.sidebar a:hover {
color: #f1f1f1;
}
/* Style page content */
.main {
margin-left: 180px; /* Same as the width of the sidebar */
padding: 0px 10px;
}
/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */
@media screen and (max-height: 450px) {
.sidebar {padding-top: 15px;}
.sidebar a {font-size: 18px;}
}
#editor {
min-height: 400px;
}
[contenteditable]:focus {
outline: 0px solid transparent;
}
.persisted::before {
content: "✔";
color: green;
position: absolute;
right: -14px;
top: 0px;
}
.upsynced::before {
content: "↑";
color: green;
position: absolute;
right: -14px;
top: 0px;
}
.noupsynced::before {
content: "↑";
color: red;
position: absolute;
right: -14px;
top: 0px;
}
.downsynced::after {
content: "↓";
color: green;
position: absolute;
right: -22px;
top: 0px;
}
.nodownsynced::after {
content: "↓";
color: red;
position: absolute;
right: -22px;
top: 0px;
}

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- Main quill library -->
<script src="../../node_modules/quill/dist/quill.min.js"></script>
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
<!-- Quill cursors module -->
<script src="../../node_modules/quill-cursors/dist/quill-cursors.min.js"></script>
<link href="../../node_modules/quill-cursors/dist/quill-cursors.css" rel="stylesheet">
<!-- Yjs Library and connector -->
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
</head>
<body>
<div id="quill-container">
<div id="quill">
</div>
</div>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,78 +0,0 @@
/* global Y, Quill, QuillCursors */
Quill.register('modules/cursors', QuillCursors)
let y = new Y('quill-0', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
let users = y.define('users', Y.Array)
let myUserInfo = new Y.Map()
myUserInfo.set('name', 'dada')
myUserInfo.set('color', 'red')
users.push([myUserInfo])
let quill = new Quill('#quill-container', {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
],
cursors: {
hideDelay: 500
}
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
let cursors = quill.getModule('cursors')
const drawCursors = () => {
cursors.clearCursors()
users.map((user, userId) => {
if (user !== myUserInfo) {
let relativeRange = user.get('range')
let lastUpdated = new Date(user.get('last updated')).getTime()
if (lastUpdated != null && new Date().getTime() - lastUpdated < 20000 && relativeRange != null) {
let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
let range = { index: start, length: end - start }
cursors.setCursor(userId + '', range, user.get('name'), user.get('color'))
}
}
})
}
users.observeDeep(drawCursors)
drawCursors()
quill.on('selection-change', function (range) {
if (range != null) {
myUserInfo.set('range', {
start: Y.utils.getRelativePosition(yText, range.index),
end: Y.utils.getRelativePosition(yText, range.index + range.length)
})
} else {
myUserInfo.delete('range')
}
myUserInfo.set('last updated', new Date().toString())
})
let yText = y.define('quill', Y.Text)
let quillBinding = new Y.QuillBinding(yText, quill)
window.quillBinding = quillBinding
window.yText = yText
window.y = y
window.quill = quill
window.users = users
window.cursors = cursors

View File

@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- quill does not include dist files! We are using the hosted version instead -->
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
<style>
#quill-container {
border: 1px solid gray;
box-shadow: 0px 0px 10px gray;
}
</style>
</head>
<body>
<div id="quill-container">
<div id="quill">
</div>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
<!-- quill does not include dist files! We are using the hosted version instead (see above)
<script src="../bower_components/quill/dist/quill.js"></script>
-->
<script src="../bower_components/yjs/y.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,49 +0,0 @@
/* global Y, Quill */
// register yjs service worker
if ('serviceWorker' in navigator) {
// Register service worker
// it is important to copy yjs-sw-template to the root directory!
navigator.serviceWorker.register('./yjs-sw-template.js').then(function (reg) {
console.log('Yjs service worker registration succeeded. Scope is ' + reg.scope)
}).catch(function (err) {
console.error('Yjs service worker registration failed with error ' + err)
})
}
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'serviceworker',
room: 'ServiceWorkerExample2'
},
sourceDir: '/bower_components',
share: {
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
}
}).then(function (y) {
window.yServiceWorker = y
// create quill element
window.quill = new Quill('#quill', {
modules: {
formula: true,
syntax: true,
toolbar: [
[{ size: ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }]
]
},
theme: 'snow'
})
// bind quill to richtext type
y.share.richtext.bind(window.quill)
})

View File

@@ -1,22 +0,0 @@
/* eslint-env worker */
// copy and modify this file
self.DBConfig = {
name: 'indexeddb'
}
self.ConnectorConfig = {
name: 'websockets-client',
// url: '..',
options: {
jsonp: false
}
}
importScripts(
'/bower_components/yjs/y.js',
'/bower_components/y-memory/y-memory.js',
'/bower_components/y-indexeddb/y-indexeddb.js',
'/bower_components/y-websockets-client/y-websockets-client.js',
'/bower_components/y-serviceworker/yjs-sw-include.js'
)

View File

@@ -1,55 +0,0 @@
import './structs/Item.js'
import { Delete } from './structs/Delete.js'
import { ItemJSON } from './structs/ItemJSON.js'
import { ItemString } from './structs/ItemString.js'
import { ItemFormat } from './structs/ItemFormat.js'
import { ItemEmbed } from './structs/ItemEmbed.js'
import { GC } from './structs/GC.js'
import { YArray } from './types/YArray.js'
import { YMap } from './types/YMap.js'
import { YText } from './types/YText.js'
import { YXmlText } from './types/YXmlText.js'
import { YXmlHook } from './types/YXmlHook.js'
import { YXmlElement, YXmlFragment } from './types/YXmlElement.js'
import { registerStruct } from './utils/structReferences.js'
import * as decoding from './lib/decoding.js'
import * as encoding from './lib/encoding.js'
import * as awarenessProtocol from './protocols/awareness.js'
import * as syncProtocol from './protocols/sync.js'
import * as authProtocol from './protocols/auth.js'
export { decoding, encoding, awarenessProtocol, syncProtocol, authProtocol }
export { Y } from './utils/Y.js'
export { UndoManager } from './utils/UndoManager.js'
export { Transaction } from './utils/Transaction.js'
export { YArray as Array } from './types/YArray.js'
export { YMap as Map } from './types/YMap.js'
export { YText as Text } from './types/YText.js'
export { YXmlText as XmlText } from './types/YXmlText.js'
export { YXmlHook as XmlHook } from './types/YXmlHook.js'
export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.js'
export { getRelativePosition, fromRelativePosition } from './utils/relativePosition.js'
export { registerStruct } from './utils/structReferences.js'
export * from './lib/mutex.js'
registerStruct(0, GC)
registerStruct(1, ItemJSON)
registerStruct(2, ItemString)
registerStruct(3, ItemFormat)
registerStruct(4, Delete)
registerStruct(5, YArray)
registerStruct(6, YMap)
registerStruct(7, YText)
registerStruct(8, YXmlFragment)
registerStruct(9, YXmlElement)
registerStruct(10, YXmlText)
registerStruct(11, YXmlHook)
registerStruct(12, ItemEmbed)

View File

@@ -1,113 +0,0 @@
/**
* Handles named events.
*/
export class NamedEventHandler {
constructor () {
this._eventListener = new Map()
this._stateListener = new Map()
}
/**
* @private
* Returns all listeners that listen to a specified name.
*
* @param {String} name The query event name.
*/
_getListener (name) {
let listeners = this._eventListener.get(name)
if (listeners === undefined) {
listeners = {
once: new Set(),
on: new Set()
}
this._eventListener.set(name, listeners)
}
return listeners
}
/**
* Adds a named event listener. The listener is removed after it has been
* called once.
*
* @param {String} name The event name to listen to.
* @param {Function} f The function that is executed when the event is fired.
*/
once (name, f) {
let listeners = this._getListener(name)
listeners.once.add(f)
}
/**
* Adds a named event listener.
*
* @param {String} name The event name to listen to.
* @param {Function} f The function that is executed when the event is fired.
*/
on (name, f) {
let listeners = this._getListener(name)
listeners.on.add(f)
}
/**
* @private
* Init the saved state for an event name.
*/
_initStateListener (name) {
let state = this._stateListener.get(name)
if (state === undefined) {
state = {}
state.promise = new Promise(resolve => {
state.resolve = resolve
})
this._stateListener.set(name, state)
}
return state
}
/**
* Returns a Promise that is resolved when the event name is called.
* The Promise is immediately resolved when the event name was called in the
* past.
*/
when (name) {
return this._initStateListener(name).promise
}
/**
* Remove an event listener that was registered with either
* {@link EventHandler#on} or {@link EventHandler#once}.
*/
off (name, f) {
if (name == null || f == null) {
throw new Error('You must specify event name and function!')
}
const listener = this._eventListener.get(name)
if (listener !== undefined) {
listener.on.delete(f)
listener.once.delete(f)
}
}
/**
* Emit a named event. All registered event listeners that listen to the
* specified name will receive the event.
*
* @param {String} name The event name.
* @param {Array} args The arguments that are applied to the event listener.
*/
emit (name, ...args) {
this._initStateListener(name).resolve()
const listener = this._eventListener.get(name)
if (listener !== undefined) {
listener.on.forEach(f => f.apply(null, args))
listener.once.forEach(f => f.apply(null, args))
listener.once = new Set()
} else if (name === 'error') {
console.error(args[0])
}
}
destroy () {
this._eventListener = null
}
}

View File

@@ -1,468 +0,0 @@
/**
* @module tree
*/
const rotate = (tree, parent, newParent, n) => {
if (parent === null) {
tree.root = newParent
newParent._parent = null
} else if (parent.left === n) {
parent.left = newParent
} else if (parent.right === n) {
parent.right = newParent
} else {
throw new Error('The elements are wrongly connected!')
}
}
class N {
// A created node is always red!
constructor (val) {
this.val = val
this.color = true
this._left = null
this._right = null
this._parent = null
}
isRed () { return this.color }
isBlack () { return !this.color }
redden () { this.color = true; return this }
blacken () { this.color = false; return this }
get grandparent () {
return this.parent.parent
}
get parent () {
return this._parent
}
get sibling () {
return (this === this.parent.left)
? this.parent.right : this.parent.left
}
get left () {
return this._left
}
get right () {
return this._right
}
set left (n) {
if (n !== null) {
n._parent = this
}
this._left = n
}
set right (n) {
if (n !== null) {
n._parent = this
}
this._right = n
}
rotateLeft (tree) {
const parent = this.parent
const newParent = this.right
const newRight = this.right.left
newParent.left = this
this.right = newRight
rotate(tree, parent, newParent, this)
}
next () {
if (this.right !== null) {
// search the most left node in the right tree
var o = this.right
while (o.left !== null) {
o = o.left
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.left) {
p = p.parent
}
return p.parent
}
}
prev () {
if (this.left !== null) {
// search the most right node in the left tree
var o = this.left
while (o.right !== null) {
o = o.right
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.right) {
p = p.parent
}
return p.parent
}
}
rotateRight (tree) {
const parent = this.parent
const newParent = this.left
const newLeft = this.left.right
newParent.right = this
this.left = newLeft
rotate(tree, parent, newParent, this)
}
getUncle () {
// we can assume that grandparent exists when this is called!
if (this.parent === this.parent.parent.left) {
return this.parent.parent.right
} else {
return this.parent.parent.left
}
}
}
const isBlack = node =>
node !== null ? node.isBlack() : true
const isRed = (node) =>
node !== null ? node.isRed() : false
/*
* This is a Red Black Tree implementation
*/
export class Tree {
constructor () {
this.root = null
this.length = 0
}
findNext (id) {
var nextID = id.clone()
nextID.clock += 1
return this.findWithLowerBound(nextID)
}
findPrev (id) {
let prevID = id.clone()
prevID.clock -= 1
return this.findWithUpperBound(prevID)
}
findNodeWithLowerBound (from) {
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if (from === null || (from.lessThan(o.val._id) && o.left !== null)) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.left
} else if (from !== null && o.val._id.lessThan(from)) {
// o is not within the bound, maybe one of the right elements is..
if (o.right !== null) {
o = o.right
} else {
// there is no right element. Search for the next bigger element,
// this should be within the bounds
return o.next()
}
} else {
return o
}
}
}
}
findNodeWithUpperBound (to) {
if (to === void 0) {
throw new Error('You must define from!')
}
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if ((to === null || o.val._id.lessThan(to)) && o.right !== null) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.right
} else if (to !== null && to.lessThan(o.val._id)) {
// o is not within the bound, maybe one of the left elements is..
if (o.left !== null) {
o = o.left
} else {
// there is no left element. Search for the prev smaller element,
// this should be within the bounds
return o.prev()
}
} else {
return o
}
}
}
}
findSmallestNode () {
var o = this.root
while (o != null && o.left != null) {
o = o.left
}
return o
}
findWithLowerBound (from) {
var n = this.findNodeWithLowerBound(from)
return n == null ? null : n.val
}
findWithUpperBound (to) {
var n = this.findNodeWithUpperBound(to)
return n == null ? null : n.val
}
iterate (from, to, f) {
var o
if (from === null) {
o = this.findSmallestNode()
} else {
o = this.findNodeWithLowerBound(from)
}
while (
o !== null &&
(
to === null || // eslint-disable-line no-unmodified-loop-condition
o.val._id.lessThan(to) ||
o.val._id.equals(to)
)
) {
f(o.val)
o = o.next()
}
}
find (id) {
let n = this.findNode(id)
if (n !== null) {
return n.val
} else {
return null
}
}
findNode (id) {
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if (o === null) {
return null
}
if (id.lessThan(o.val._id)) {
o = o.left
} else if (o.val._id.lessThan(id)) {
o = o.right
} else {
return o
}
}
}
}
delete (id) {
var d = this.findNode(id)
if (d == null) {
// throw new Error('Element does not exist!')
return
}
this.length--
if (d.left !== null && d.right !== null) {
// switch d with the greates element in the left subtree.
// o should have at most one child.
var o = d.left
// find
while (o.right !== null) {
o = o.right
}
// switch
d.val = o.val
d = o
}
// d has at most one child
// let n be the node that replaces d
var isFakeChild
var child = d.left || d.right
if (child === null) {
isFakeChild = true
child = new N(null)
child.blacken()
d.right = child
} else {
isFakeChild = false
}
if (d.parent === null) {
if (!isFakeChild) {
this.root = child
child.blacken()
child._parent = null
} else {
this.root = null
}
return
} else if (d.parent.left === d) {
d.parent.left = child
} else if (d.parent.right === d) {
d.parent.right = child
} else {
throw new Error('Impossible!')
}
if (d.isBlack()) {
if (child.isRed()) {
child.blacken()
} else {
this._fixDelete(child)
}
}
this.root.blacken()
if (isFakeChild) {
if (child.parent.left === child) {
child.parent.left = null
} else if (child.parent.right === child) {
child.parent.right = null
} else {
throw new Error('Impossible #3')
}
}
}
_fixDelete (n) {
if (n.parent === null) {
// this can only be called after the first iteration of fixDelete.
return
}
// d was already replaced by the child
// d is not the root
// d and child are black
var sibling = n.sibling
if (isRed(sibling)) {
// make sibling the grandfather
n.parent.redden()
sibling.blacken()
if (n === n.parent.left) {
n.parent.rotateLeft(this)
} else if (n === n.parent.right) {
n.parent.rotateRight(this)
} else {
throw new Error('Impossible #2')
}
sibling = n.sibling
}
// parent, sibling, and children of n are black
if (n.parent.isBlack() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
this._fixDelete(n.parent)
} else if (n.parent.isRed() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
n.parent.blacken()
} else {
if (n === n.parent.left &&
sibling.isBlack() &&
isRed(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
sibling.left.blacken()
sibling.rotateRight(this)
sibling = n.sibling
} else if (n === n.parent.right &&
sibling.isBlack() &&
isRed(sibling.right) &&
isBlack(sibling.left)
) {
sibling.redden()
sibling.right.blacken()
sibling.rotateLeft(this)
sibling = n.sibling
}
sibling.color = n.parent.color
n.parent.blacken()
if (n === n.parent.left) {
sibling.right.blacken()
n.parent.rotateLeft(this)
} else {
sibling.left.blacken()
n.parent.rotateRight(this)
}
}
}
put (v) {
var node = new N(v)
if (this.root !== null) {
var p = this.root // p abbrev. parent
while (true) {
if (node.val._id.lessThan(p.val._id)) {
if (p.left === null) {
p.left = node
break
} else {
p = p.left
}
} else if (p.val._id.lessThan(node.val._id)) {
if (p.right === null) {
p.right = node
break
} else {
p = p.right
}
} else {
p.val = node.val
return p
}
}
this._fixInsert(node)
} else {
this.root = node
}
this.length++
this.root.blacken()
return node
}
_fixInsert (n) {
if (n.parent === null) {
n.blacken()
return
} else if (n.parent.isBlack()) {
return
}
var uncle = n.getUncle()
if (uncle !== null && uncle.isRed()) {
// Note: parent: red, uncle: red
n.parent.blacken()
uncle.blacken()
n.grandparent.redden()
this._fixInsert(n.grandparent)
} else {
// Note: parent: red, uncle: black or null
// Now we transform the tree in such a way that
// either of these holds:
// 1) grandparent.left.isRed
// and grandparent.left.left.isRed
// 2) grandparent.right.isRed
// and grandparent.right.right.isRed
if (n === n.parent.right && n.parent === n.grandparent.left) {
n.parent.rotateLeft(this)
// Since we rotated and want to use the previous
// cases, we need to set n in such a way that
// n.parent.isRed again
n = n.left
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
n.parent.rotateRight(this)
// see above
n = n.right
}
// Case 1) or 2) hold from here on.
// Now traverse grandparent, make parent a black node
// on the highest level which holds two red nodes.
n.parent.blacken()
n.grandparent.redden()
if (n === n.parent.left) {
// Case 1
n.grandparent.rotateRight(this)
} else {
// Case 2
n.grandparent.rotateLeft(this)
}
}
}
}

View File

@@ -1,40 +0,0 @@
/* eslint-env browser */
/**
* @module binary
*/
import * as string from './string.js'
import * as globals from './globals.js'
export const BITS32 = 0xFFFFFFFF
export const BITS21 = (1 << 21) - 1
export const BITS16 = (1 << 16) - 1
export const BIT26 = 1 << 26
export const BIT32 = 1 << 32
/**
* @param {Uint8Array} bytes
* @return {string}
*/
export const toBase64 = bytes => {
let s = ''
for (let i = 0; i < bytes.byteLength; i++) {
s += string.fromCharCode(bytes[i])
}
return btoa(s)
}
/**
* @param {string} s
* @return {Uint8Array}
*/
export const fromBase64 = s => {
const a = atob(s)
const bytes = globals.createUint8ArrayFromLen(a.length)
for (let i = 0; i < a.length; i++) {
bytes[i] = a.charCodeAt(i)
}
return bytes
}

View File

@@ -1,72 +0,0 @@
/* eslint-env browser */
import * as binary from './binary.js'
import * as globals from './globals.js'
/**
* @typedef {Object} Channel
* @property {Set<Function>} Channel.subs
* @property {BC} Channel.bc
*/
/**
* @type {Map<string, Channel>}
*/
const channels = new Map()
class LocalStoragePolyfill {
constructor (room) {
this.room = room
this.onmessage = null
addEventListener('storage', e => e.key === room && this.onmessage !== null && this.onmessage({ data: binary.fromBase64(e.newValue) }))
}
/**
* @param {ArrayBuffer} data
*/
postMessage (buf) {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(this.room, binary.toBase64(globals.createUint8ArrayFromArrayBuffer(buf)))
}
}
}
// Use BroadcastChannel or Polyfill
const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel
/**
* @param {string} room
* @return {Channel}
*/
const getChannel = room => {
let c = channels.get(room)
if (c === undefined) {
const subs = new Set()
const bc = new BC(room)
bc.onmessage = e => subs.forEach(sub => sub(e.data))
c = {
bc, subs
}
channels.set(room, c)
}
return c
}
/**
* @function
* @param {string} room
* @param {Function} f
*/
export const subscribe = (room, f) => getChannel(room).subs.add(f)
/**
* Publish data to all subscribers (including subscribers on this tab)
*
* @function
* @param {string} room
* @param {ArrayBuffer} data
*/
export const publish = (room, data) => {
const c = getChannel(room)
c.bc.postMessage(data)
c.subs.forEach(sub => sub(data))
}

View File

@@ -1,207 +0,0 @@
/**
* @module decoding
*/
/* global Buffer */
import * as globals from './globals.js'
/**
* A Decoder handles the decoding of an ArrayBuffer.
*/
export class Decoder {
/**
* @param {ArrayBuffer} buffer Binary data to decode
*/
constructor (buffer) {
this.arr = new Uint8Array(buffer)
this.pos = 0
}
}
/**
* @function
* @param {ArrayBuffer} buffer
* @return {Decoder}
*/
export const createDecoder = buffer => new Decoder(buffer)
/**
* @function
* @param {Decoder} decoder
* @return {boolean}
*/
export const hasContent = decoder => decoder.pos !== decoder.arr.length
/**
* Clone a decoder instance.
* Optionally set a new position parameter.
*
* @function
* @param {Decoder} decoder The decoder instance
* @param {number} [newPos] Defaults to current position
* @return {Decoder} A clone of `decoder`
*/
export const clone = (decoder, newPos = decoder.pos) => {
let _decoder = createDecoder(decoder.arr.buffer)
_decoder.pos = newPos
return _decoder
}
/**
* Read `len` bytes as an ArrayBuffer.
* @function
* @param {Decoder} decoder The decoder instance
* @param {number} len The length of bytes to read
* @return {ArrayBuffer}
*/
export const readArrayBuffer = (decoder, len) => {
const arrayBuffer = globals.createUint8ArrayFromLen(len)
const view = globals.createUint8ArrayFromBuffer(decoder.arr.buffer, decoder.pos, len)
arrayBuffer.set(view)
decoder.pos += len
return arrayBuffer.buffer
}
/**
* Read variable length payload as ArrayBuffer
* @function
* @param {Decoder} decoder
* @return {ArrayBuffer}
*/
export const readPayload = decoder => readArrayBuffer(decoder, readVarUint(decoder))
/**
* Read the rest of the content as an ArrayBuffer
* @function
* @param {Decoder} decoder
* @return {ArrayBuffer}
*/
export const readTail = decoder => readArrayBuffer(decoder, decoder.arr.length - decoder.pos)
/**
* Skip one byte, jump to the next position.
* @function
* @param {Decoder} decoder The decoder instance
* @return {number} The next position
*/
export const skip8 = decoder => decoder.pos++
/**
* Read one byte as unsigned integer.
* @function
* @param {Decoder} decoder The decoder instance
* @return {number} Unsigned 8-bit integer
*/
export const readUint8 = decoder => decoder.arr[decoder.pos++]
/**
* Read 4 bytes as unsigned integer.
*
* @function
* @param {Decoder} decoder
* @return {number} An unsigned integer.
*/
export const readUint32 = decoder => {
let uint =
decoder.arr[decoder.pos] +
(decoder.arr[decoder.pos + 1] << 8) +
(decoder.arr[decoder.pos + 2] << 16) +
(decoder.arr[decoder.pos + 3] << 24)
decoder.pos += 4
return uint
}
/**
* Look ahead without incrementing position.
* to the next byte and read it as unsigned integer.
*
* @function
* @param {Decoder} decoder
* @return {number} An unsigned integer.
*/
export const peekUint8 = decoder => decoder.arr[decoder.pos]
/**
* Read unsigned integer (32bit) with variable length.
* 1/8th of the storage is used as encoding overhead.
* * numbers < 2^7 is stored in one bytlength
* * numbers < 2^14 is stored in two bylength
*
* @function
* @param {Decoder} decoder
* @return {number} An unsigned integer.length
*/
export const readVarUint = decoder => {
let num = 0
let len = 0
while (true) {
let r = decoder.arr[decoder.pos++]
num = num | ((r & 0b1111111) << len)
len += 7
if (r < 1 << 7) {
return num >>> 0 // return unsigned number!
}
if (len > 35) {
throw new Error('Integer out of range!')
}
}
}
/**
* Look ahead and read varUint without incrementing position
*
* @function
* @param {Decoder} decoder
* @return {number}
*/
export const peekVarUint = decoder => {
let pos = decoder.pos
let s = readVarUint(decoder)
decoder.pos = pos
return s
}
/**
* Read string of variable length
* * varUint is used to store the length of the string
*
* Transforming utf8 to a string is pretty expensive. The code performs 10x better
* when String.fromCodePoint is fed with all characters as arguments.
* But most environments have a maximum number of arguments per functions.
* For effiency reasons we apply a maximum of 10000 characters at once.
*
* @function
* @param {Decoder} decoder
* @return {String} The read String.
*/
export const readVarString = decoder => {
let remainingLen = readVarUint(decoder)
let encodedString = ''
while (remainingLen > 0) {
const nextLen = remainingLen < 10000 ? remainingLen : 10000
const bytes = new Array(nextLen)
for (let i = 0; i < nextLen; i++) {
bytes[i] = decoder.arr[decoder.pos++]
}
encodedString += String.fromCodePoint.apply(null, bytes)
remainingLen -= nextLen
}
return decodeURIComponent(escape(encodedString))
}
/**
* Look ahead and read varString without incrementing position
*
* @function
* @param {Decoder} decoder
* @return {string}
*/
export const peekVarString = decoder => {
let pos = decoder.pos
let s = readVarString(decoder)
decoder.pos = pos
return s
}

View File

@@ -1,50 +0,0 @@
/**
* @module diff
*/
/**
* A SimpleDiff describes a change on a String.
*
* @example
* console.log(a) // the old value
* console.log(b) // the updated value
* // Apply changes of diff (pseudocode)
* a.remove(diff.pos, diff.remove) // Remove `diff.remove` characters
* a.insert(diff.pos, diff.insert) // Insert `diff.insert`
* a === b // values match
*
* @typedef {Object} SimpleDiff
* @property {Number} pos The index where changes were applied
* @property {Number} remove The number of characters to delete starting
* at `index`.
* @property {String} insert The new text to insert at `index` after applying
* `delete`
*/
/**
* Create a diff between two strings. This diff implementation is highly
* efficient, but not very sophisticated.
*
* @public
* @param {String} a The old version of the string
* @param {String} b The updated version of the string
* @return {SimpleDiff} The diff description.
*/
export const simpleDiff = (a, b) => {
let left = 0 // number of same characters counting from left
let right = 0 // number of same characters counting from right
while (left < a.length && left < b.length && a[left] === b[left]) {
left++
}
if (left !== a.length || left !== b.length) {
// Only check right if a !== b
while (right + left < a.length && right + left < b.length && a[a.length - right - 1] === b[b.length - right - 1]) {
right++
}
}
return {
pos: left, // TODO: rename to index (also in type above)
remove: a.length - left - right,
insert: b.slice(left, b.length - right)
}
}

View File

@@ -1,243 +0,0 @@
/**
* @module encoding
*/
import * as globals from './globals.js'
const bits7 = 0b1111111
const bits8 = 0b11111111
/**
* A BinaryEncoder handles the encoding to an ArrayBuffer.
*/
export class Encoder {
constructor () {
this.cpos = 0
this.cbuf = globals.createUint8ArrayFromLen(1000)
this.bufs = []
}
}
/**
* @function
* @return {Encoder}
*/
export const createEncoder = () => new Encoder()
/**
* The current length of the encoded data.
*
* @function
* @param {Encoder} encoder
* @return {number}
*/
export const length = encoder => {
let len = encoder.cpos
for (let i = 0; i < encoder.bufs.length; i++) {
len += encoder.bufs[i].length
}
return len
}
/**
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
*
* @function
* @param {Encoder} encoder
* @return {ArrayBuffer} The created ArrayBuffer.
*/
export const toBuffer = encoder => {
const uint8arr = globals.createUint8ArrayFromLen(length(encoder))
let curPos = 0
for (let i = 0; i < encoder.bufs.length; i++) {
let d = encoder.bufs[i]
uint8arr.set(d, curPos)
curPos += d.length
}
uint8arr.set(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos), curPos)
return uint8arr.buffer
}
/**
* Write one byte to the encoder.
*
* @function
* @param {Encoder} encoder
* @param {number} num The byte that is to be encoded.
*/
export const write = (encoder, num) => {
if (encoder.cpos === encoder.cbuf.length) {
encoder.bufs.push(encoder.cbuf)
encoder.cbuf = globals.createUint8ArrayFromLen(encoder.cbuf.length * 2)
encoder.cpos = 0
}
encoder.cbuf[encoder.cpos++] = num
}
/**
* Write one byte at a specific position.
* Position must already be written (i.e. encoder.length > pos)
*
* @function
* @param {Encoder} encoder
* @param {number} pos Position to which to write data
* @param {number} num Unsigned 8-bit integer
*/
export const set = (encoder, pos, num) => {
let buffer = null
// iterate all buffers and adjust position
for (let i = 0; i < encoder.bufs.length && buffer === null; i++) {
const b = encoder.bufs[i]
if (pos < b.length) {
buffer = b // found buffer
} else {
pos -= b.length
}
}
if (buffer === null) {
// use current buffer
buffer = encoder.cbuf
}
buffer[pos] = num
}
/**
* Write one byte as an unsigned integer.
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
export const writeUint8 = (encoder, num) => write(encoder, num & bits8)
/**
* Write one byte as an unsigned Integer at a specific location.
*
* @function
* @param {Encoder} encoder
* @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded.
*/
export const setUint8 = (encoder, pos, num) => set(encoder, pos, num & bits8)
/**
* Write two bytes as an unsigned integer.
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
export const writeUint16 = (encoder, num) => {
write(encoder, num & bits8)
write(encoder, (num >>> 8) & bits8)
}
/**
* Write two bytes as an unsigned integer at a specific location.
*
* @function
* @param {Encoder} encoder
* @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded.
*/
export const setUint16 = (encoder, pos, num) => {
set(encoder, pos, num & bits8)
set(encoder, pos + 1, (num >>> 8) & bits8)
}
/**
* Write two bytes as an unsigned integer
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
export const writeUint32 = (encoder, num) => {
for (let i = 0; i < 4; i++) {
write(encoder, num & bits8)
num >>>= 8
}
}
/**
* Write two bytes as an unsigned integer at a specific location.
*
* @function
* @param {Encoder} encoder
* @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded.
*/
export const setUint32 = (encoder, pos, num) => {
for (let i = 0; i < 4; i++) {
set(encoder, pos + i, num & bits8)
num >>>= 8
}
}
/**
* Write a variable length unsigned integer.
*
* Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
export const writeVarUint = (encoder, num) => {
while (num >= 0b10000000) {
write(encoder, 0b10000000 | (bits7 & num))
num >>>= 7
}
write(encoder, bits7 & num)
}
/**
* Write a variable length string.
*
* @function
* @param {Encoder} encoder
* @param {String} str The string that is to be encoded.
*/
export const writeVarString = (encoder, str) => {
const encodedString = unescape(encodeURIComponent(str))
const len = encodedString.length
writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
write(encoder, encodedString.codePointAt(i))
}
}
/**
* Write the content of another Encoder.
*
* TODO: can be improved!
*
* @function
* @param {Encoder} encoder The enUint8Arr
* @param {Encoder} append The BinaryEncoder to be written.
*/
export const writeBinaryEncoder = (encoder, append) => writeArrayBuffer(encoder, toBuffer(append))
/**
* Append an arrayBuffer to the encoder.
*
* @function
* @param {Encoder} encoder
* @param {ArrayBuffer} arrayBuffer
*/
export const writeArrayBuffer = (encoder, arrayBuffer) => {
const prevBufferLen = encoder.cbuf.length
// TODO: Append to cbuf if possible
encoder.bufs.push(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos))
encoder.bufs.push(globals.createUint8ArrayFromArrayBuffer(arrayBuffer))
encoder.cbuf = globals.createUint8ArrayFromLen(prevBufferLen)
encoder.cpos = 0
}
/**
* @function
* @param {Encoder} encoder
* @param {ArrayBuffer} arrayBuffer
*/
export const writePayload = (encoder, arrayBuffer) => {
writeVarUint(encoder, arrayBuffer.byteLength)
writeArrayBuffer(encoder, arrayBuffer)
}

View File

@@ -1,49 +0,0 @@
import * as encoding from './encoding.js'
/**
* Check if binary encoding is compatible with golang binary encoding - binary.PutVarUint.
*
* Result: is compatible up to 32 bit: [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
*/
let err = null
try {
const tests = [
{ in: 0, out: [0] },
{ in: 1, out: [1] },
{ in: 128, out: [128, 1] },
{ in: 200, out: [200, 1] },
{ in: 32, out: [32] },
{ in: 500, out: [244, 3] },
{ in: 256, out: [128, 2] },
{ in: 700, out: [188, 5] },
{ in: 1024, out: [128, 8] },
{ in: 1025, out: [129, 8] },
{ in: 4048, out: [208, 31] },
{ in: 5050, out: [186, 39] },
{ in: 1000000, out: [192, 132, 61] },
{ in: 34951959, out: [151, 166, 213, 16] },
{ in: 2147483646, out: [254, 255, 255, 255, 7] },
{ in: 2147483647, out: [255, 255, 255, 255, 7] },
{ in: 2147483648, out: [128, 128, 128, 128, 8] },
{ in: 2147483700, out: [180, 128, 128, 128, 8] },
{ in: 4294967294, out: [254, 255, 255, 255, 15] },
{ in: 4294967295, out: [255, 255, 255, 255, 15] }
]
tests.forEach(test => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, test.in)
const buffer = new Uint8Array(encoding.toBuffer(encoder))
if (buffer.byteLength !== test.out.length) {
throw new Error('Length don\'t match!')
}
for (let j = 0; j < buffer.length; j++) {
if (buffer[j] !== test[1][j]) {
throw new Error('values don\'t match!')
}
}
})
} catch (error) {
err = error
} finally {
console.log('YDB Client: Encoding varUint compatiblity test: ', err || 'success!')
}

View File

@@ -1,65 +0,0 @@
/**
* @module globals
*/
/* eslint-env browser */
export const Uint8Array_ = Uint8Array
/**
* @param {Array<number>} arr
* @return {ArrayBuffer}
*/
export const createArrayBufferFromArray = arr => new Uint8Array_(arr).buffer
export const createUint8ArrayFromLen = len => new Uint8Array_(len)
/**
* Create Uint8Array with initial content from buffer
*/
export const createUint8ArrayFromBuffer = (buffer, byteOffset, length) => new Uint8Array_(buffer, byteOffset, length)
/**
* Create Uint8Array with initial content from buffer
*/
export const createUint8ArrayFromArrayBuffer = arraybuffer => new Uint8Array_(arraybuffer)
export const createArrayFromArrayBuffer = arraybuffer => Array.from(createUint8ArrayFromArrayBuffer(arraybuffer))
export const createPromise = f => new Promise(f)
export const createMap = () => new Map()
export const createSet = () => new Set()
/**
* `Promise.all` wait for all promises in the array to resolve and return the result
* @param {Array<Promise<any>>} arrp
* @return {any}
*/
export const pall = arrp => Promise.all(arrp)
export const preject = reason => Promise.reject(reason)
export const presolve = res => Promise.resolve(res)
export const until = (timeout, check) => createPromise((resolve, reject) => {
const hasTimeout = timeout > 0
const untilInterval = () => {
if (check()) {
clearInterval(intervalHandle)
resolve()
} else if (hasTimeout) {
timeout -= 10
if (timeout < 0) {
clearInterval(intervalHandle)
reject(error('Timeout'))
}
}
}
const intervalHandle = setInterval(untilInterval, 10)
})
export const error = description => new Error(description)
/**
* @param {number} t Time to wait
* @return {Promise} Promise that is resolved after t ms
*/
export const wait = t => createPromise(r => setTimeout(r, t))

View File

@@ -1,166 +0,0 @@
/**
* @module lib/idb
*/
/* eslint-env browser */
import * as globals from './globals.js'
/*
* IDB Request to Promise transformer
*/
export const rtop = request => globals.createPromise((resolve, reject) => {
request.onerror = event => reject(new Error(event.target.error))
request.onblocked = () => location.reload()
request.onsuccess = event => resolve(event.target.result)
})
/**
* @param {string} name
* @param {Function} initDB Called when the database is first created
* @return {Promise<IDBDatabase>}
*/
export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => {
let request = indexedDB.open(name)
/**
* @param {any} event
*/
request.onupgradeneeded = event => initDB(event.target.result)
/**
* @param {any} event
*/
request.onerror = event => reject(new Error(event.target.error))
request.onblocked = () => location.reload()
/**
* @param {any} event
*/
request.onsuccess = event => {
const db = event.target.result
db.onversionchange = () => { db.close() }
addEventListener('unload', () => db.close())
resolve(db)
}
})
export const deleteDB = name => rtop(indexedDB.deleteDatabase(name))
export const createStores = (db, definitions) => definitions.forEach(d =>
db.createObjectStore.apply(db, d)
)
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date | Array } key
* @return {Promise<ArrayBuffer>}
*/
export const get = (store, key) =>
rtop(store.get(key))
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date | IDBKeyRange | Array } key
*/
export const del = (store, key) =>
rtop(store.delete(key))
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date | boolean} item
* @param {String | number | ArrayBuffer | Date | Array} [key]
*/
export const put = (store, item, key) =>
rtop(store.put(item, key))
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date | boolean} item
* @param {String | number | ArrayBuffer | Date | Array} [key]
* @return {Promise<ArrayBuffer>}
*/
export const add = (store, item, key) =>
rtop(store.add(item, key))
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date} item
* @return {Promise<number>}
*/
export const addAutoKey = (store, item) =>
rtop(store.add(item))
/**
* @param {IDBObjectStore} store
* @param {IDBKeyRange} [range]
*/
export const getAll = (store, range) =>
rtop(store.getAll(range))
/**
* @param {IDBObjectStore} store
* @param {IDBKeyRange} [range]
*/
export const getAllKeys = (store, range) =>
rtop(store.getAllKeys(range))
/**
* @typedef KeyValuePair
* @type {Object}
* @property {any} k key
* @property {any} v Value
*/
/**
* @param {IDBObjectStore} store
* @param {IDBKeyRange} [range]
* @return {Promise<Array<KeyValuePair>>}
*/
export const getAllKeysValues = (store, range) =>
globals.pall([getAllKeys(store, range), getAll(store, range)]).then(([ks, vs]) => ks.map((k, i) => ({ k, v: vs[i] })))
/**
* Iterate on keys and values
* @param {IDBObjectStore} store
* @param {IDBKeyRange?} keyrange
* @param {Function} f Return true in order to continue the cursor
*/
export const iterate = (store, keyrange, f) => globals.createPromise((resolve, reject) => {
const request = store.openCursor(keyrange)
request.onerror = reject
/**
* @param {any} event
*/
request.onsuccess = event => {
const cursor = event.target.result
if (cursor === null) {
return resolve()
}
f(cursor.value, cursor.key)
cursor.continue()
}
})
/**
* Iterate on the keys (no values)
*
* @param {IDBObjectStore} store
* @param {IDBKeyRange} keyrange
* @param {function} f Call `idbcursor.continue()` to iterate further
*/
export const iterateKeys = (store, keyrange, f) => {
/**
* @param {any} event
*/
store.openKeyCursor(keyrange).onsuccess = event => f(event.target.result)
}
/**
* Open store from transaction
* @param {IDBTransaction} t
* @param {String} store
* @returns {IDBObjectStore}
*/
export const getStore = (t, store) => t.objectStore(store)
export const createIDBKeyRangeBound = (lower, upper, lowerOpen, upperOpen) => IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)
export const createIDBKeyRangeUpperBound = (upper, upperOpen) => IDBKeyRange.upperBound(upper, upperOpen)
export const createIDBKeyRangeLowerBound = (lower, lowerOpen) => IDBKeyRange.lowerBound(lower, lowerOpen)

View File

@@ -1,34 +0,0 @@
import * as test from './testing.js'
import * as idb from './idb.js'
import * as logging from './logging.js'
const initTestDB = db => idb.createStores(db, [['test']])
const testDBName = 'idb-test'
const createTransaction = db => db.transaction(['test'], 'readwrite')
/**
* @param {IDBTransaction} t
* @return {IDBObjectStore}
*/
const getStore = t => idb.getStore(t, 'test')
idb.deleteDB(testDBName).then(() => idb.openDB(testDBName, initTestDB)).then(db => {
test.run('idb iteration', async testname => {
const t = createTransaction(db)
await idb.put(getStore(t), 0, ['t', 0])
await idb.put(getStore(t), 1, ['t', 1])
const valsGetAll = await idb.getAll(getStore(t))
if (valsGetAll.length !== 2) {
logging.fail('getAll does not return two values')
}
const valsIterate = []
const keyrange = idb.createIDBKeyRangeBound(['t', 0], ['t', 1], false, false)
await idb.put(getStore(t), 2, ['t', 2])
await idb.iterate(getStore(t), keyrange, (val, key) => {
valsIterate.push(val)
})
if (valsIterate.length !== 2) {
logging.fail('iterate does not return two values')
}
})
})

View File

@@ -1,26 +0,0 @@
/**
* @module logging
*/
import * as globals from './globals.js'
let date = new Date().getTime()
const writeDate = () => {
const oldDate = date
date = new Date().getTime()
return date - oldDate
}
export const print = (...args) => console.log(...args)
export const log = m => print(`%cydb-client: %c${m} %c+${writeDate()}ms`, 'color: blue;', '', 'color: blue')
export const fail = m => {
throw new Error(m)
}
/**
* @param {ArrayBuffer} buffer
* @return {string}
*/
export const arrayBufferToString = buffer => JSON.stringify(Array.from(globals.createUint8ArrayFromBuffer(buffer)))

View File

@@ -1,28 +0,0 @@
/**
* @module math
*/
export const floor = Math.floor
/**
* @function
* @param {number} a
* @param {number} b
* @return {number} The sum of a and b
*/
export const add = (a, b) => a + b
/**
* @function
* @param {number} a
* @param {number} b
* @return {number} The smaller element of a and b
*/
export const min = (a, b) => a < b ? a : b
/**
* @function
* @param {number} a
* @param {number} b
* @return {number} The bigger element of a and b
*/
export const max = (a, b) => a > b ? a : b

View File

@@ -1,31 +0,0 @@
/**
* Creates a mutual exclude function with the following property:
*
* @example
* const mutex = createMutex()
* mutex(() => {
* // This function is immediately executed
* mutex(() => {
* // This function is not executed, as the mutex is already active.
* })
* })
*
* @return {Function} A mutual exclude function
* @public
*/
export const createMutex = () => {
let token = true
return (f, g) => {
if (token) {
token = false
try {
f()
} finally {
token = true
}
} else if (g !== undefined) {
g()
}
}
}

View File

@@ -1,6 +0,0 @@
/**
* @module number
*/
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER

View File

@@ -1,14 +0,0 @@
export const create = Object.create(null)
export const keys = Object.keys
export const equalFlat = (a, b) => {
const keys = Object.keys(a)
let eq = keys.length === Object.keys(b).length
for (let i = 0; i < keys.length && eq; i++) {
const key = keys[i]
eq = a[key] === b[key]
}
return eq
}

View File

@@ -1,67 +0,0 @@
/**
* @module prng
*/
const N = 624
const M = 397
const twist = (u, v) => ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
const nextState = (state) => {
let p = 0
let j
for (j = N - M + 1; --j; p++) {
state[p] = state[p + M] ^ twist(state[p], state[p + 1])
}
for (j = M; --j; p++) {
state[p] = state[p + M - N] ^ twist(state[p], state[p + 1])
}
state[p] = state[p + M - N] ^ twist(state[p], state[0])
}
/**
* This is a port of Shawn Cokus's implementation of the original Mersenne Twister algorithm (http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/CODES/MTARCOK/mt19937ar-cok.c).
* MT has a very high period of 2^19937. Though the authors of xorshift describe that a high period is not
* very relevant (http://vigna.di.unimi.it/xorshift/). It is four times slower than xoroshiro128plus and
* needs to recompute its state after generating 624 numbers.
*
* @example
* const gen = new Mt19937(new Date().getTime())
* console.log(gen.next())
*
* @public
*/
export class Mt19937 {
/**
* @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
*/
constructor (seed) {
this.seed = seed
const state = new Uint32Array(N)
state[0] = seed
for (let i = 1; i < N; i++) {
state[i] = (Math.imul(1812433253, (state[i - 1] ^ (state[i - 1] >>> 30))) + i) & 0xFFFFFFFF
}
this._state = state
this._i = 0
nextState(this._state)
}
/**
* Generate a random signed integer.
*
* @return {Number} A 32 bit signed integer.
*/
next () {
if (this._i === N) {
// need to compute a new state
nextState(this._state)
this._i = 0
}
let y = this._state[this._i++]
y ^= (y >>> 11)
y ^= (y << 7) & 0x9d2c5680
y ^= (y << 15) & 0xefc60000
y ^= (y >>> 18)
return y
}
}

View File

@@ -1,51 +0,0 @@
/**
* @module prng
*/
import { Mt19937 } from './Mt19937.js'
import { Xoroshiro128plus } from './Xoroshiro128plus.js'
import { Xorshift32 } from './Xorshift32.js'
import * as time from '../../time.js'
const DIAMETER = 300
const NUMBERS = 10000
const runPRNG = (name, Gen) => {
console.log('== ' + name + ' ==')
const gen = new Gen(1234)
let head = 0
let tails = 0
const date = time.getUnixTime()
const canvas = document.createElement('canvas')
canvas.height = DIAMETER
canvas.width = DIAMETER
const ctx = canvas.getContext('2d')
const vals = new Set()
ctx.fillStyle = 'blue'
for (let i = 0; i < NUMBERS; i++) {
const n = gen.next() & 0xFFFFFF
const x = (gen.next() >>> 0) % DIAMETER
const y = (gen.next() >>> 0) % DIAMETER
ctx.fillRect(x, y, 1, 2)
if ((n & 1) === 1) {
head++
} else {
tails++
}
if (vals.has(n)) {
console.warn(`The generator generated a duplicate`)
}
vals.add(n)
}
console.log('time: ', time.getUnixTime() - date)
console.log('head:', head, 'tails:', tails)
console.log('%c ', `font-size: 200px; background: url(${canvas.toDataURL()}) no-repeat;`)
const h1 = document.createElement('h1')
h1.insertBefore(document.createTextNode(name), null)
document.body.insertBefore(h1, null)
document.body.appendChild(canvas)
}
runPRNG('mt19937', Mt19937)
runPRNG('xoroshiro128plus', Xoroshiro128plus)
runPRNG('xorshift32', Xorshift32)

View File

@@ -1,5 +0,0 @@
# Pseudo Random Number Generators (PRNG)
Given a seed a PRNG generates a sequence of numbers that cannot be reasonably predicted. Two PRNGs must generate the same random sequence of numbers if given the same seed.
TODO: explain what POINT is

View File

@@ -1,101 +0,0 @@
/**
* @module prng
*/
import { Xorshift32 } from './Xorshift32.js'
/**
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
*
* This implementation follows the idea of the original xoroshiro128plus implementation,
* but is optimized for the JavaScript runtime. I.e.
* * The operations are performed on 32bit integers (the original implementation works with 64bit values).
* * The initial 128bit state is computed based on a 32bit seed and Xorshift32.
* * This implementation returns two 32bit values based on the 64bit value that is computed by xoroshiro128plus.
* Caution: The last addition step works slightly different than in the original implementation - the add carry of the
* first 32bit addition is not carried over to the last 32bit.
*
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
*/
export class Xoroshiro128plus {
constructor (seed) {
this.seed = seed
// This is a variant of Xoroshiro128plus to fill the initial state
const xorshift32 = new Xorshift32(seed)
this.state = new Uint32Array(4)
for (let i = 0; i < 4; i++) {
this.state[i] = xorshift32.next()
}
this._fresh = true
}
next () {
const state = this.state
if (this._fresh) {
this._fresh = false
return (state[0] + state[2]) & 0xFFFFFFFF
} else {
this._fresh = true
const s0 = state[0]
const s1 = state[1]
const s2 = state[2] ^ s0
const s3 = state[3] ^ s1
// function js_rotl (x, k) {
// k = k - 32
// const x1 = x[0]
// const x2 = x[1]
// x[0] = x2 << k | x1 >>> (32 - k)
// x[1] = x1 << k | x2 >>> (32 - k)
// }
// rotl(s0, 55) // k = 23 = 55 - 32; j = 9 = 32 - 23
state[0] = (s1 << 23 | s0 >>> 9) ^ s2 ^ (s2 << 14 | s3 >>> 18)
state[1] = (s0 << 23 | s1 >>> 9) ^ s3 ^ (s3 << 14)
// rol(s1, 36) // k = 4 = 36 - 32; j = 23 = 32 - 9
state[2] = s3 << 4 | s2 >>> 28
state[3] = s2 << 4 | s3 >>> 28
return (state[1] + state[3]) & 0xFFFFFFFF
}
}
}
/*
// reference implementation
#include <stdint.h>
#include <stdio.h>
uint64_t s[2];
static inline uint64_t rotl(const uint64_t x, int k) {
return (x << k) | (x >> (64 - k));
}
uint64_t next(void) {
const uint64_t s0 = s[0];
uint64_t s1 = s[1];
s1 ^= s0;
s[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14); // a, b
s[1] = rotl(s1, 36); // c
return (s[0] + s[1]) & 0xFFFFFFFF;
}
int main(void)
{
int i;
s[0] = 1111 | (1337ul << 32);
s[1] = 1234 | (9999ul << 32);
printf("1000 outputs of genrand_int31()\n");
for (i=0; i<100; i++) {
printf("%10lu ", i);
printf("%10lu ", next());
printf("- %10lu ", s[0] >> 32);
printf("%10lu ", (s[0] << 32) >> 32);
printf("%10lu ", s[1] >> 32);
printf("%10lu ", (s[1] << 32) >> 32);
printf("\n");
// if (i%5==4) printf("\n");
}
return 0;
}
*/

View File

@@ -1,29 +0,0 @@
/**
* @module prng
*/
/**
* Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`.
*/
export class Xorshift32 {
/**
* @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
*/
constructor (seed) {
this.seed = seed
this._state = seed
}
/**
* Generate a random signed integer.
*
* @return {Number} A 32 bit signed integer.
*/
next () {
let x = this._state
x ^= x << 13
x ^= x >> 17
x ^= x << 5
this._state = x
return x
}
}

View File

@@ -1,134 +0,0 @@
/**
* @module prng
*/
import * as binary from '../binary.js'
import { fromCharCode, fromCodePoint } from '../string.js'
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
import * as math from '../math.js'
import { Xoroshiro128plus as DefaultPRNG } from './PRNG/Xoroshiro128plus.js'
/**
* Description of the function
* @callback generatorNext
* @return {number} A 32bit integer
*/
/**
* A random type generator.
*
* @typedef {Object} PRNG
* @property {generatorNext} next Generate new number
*/
/**
* Create a Xoroshiro128plus Pseudo-Random-Number-Generator.
* This is the fastest full-period generator passing BigCrush without systematic failures.
* But there are more PRNGs available in ./PRNG/.
*
* @param {number} seed A positive 32bit integer. Do not use negative numbers.
* @return {PRNG}
*/
export const createPRNG = seed => new DefaultPRNG(Math.floor(seed < 1 ? seed * binary.BITS32 : seed))
/**
* Generates a single random bool.
*
* @param {PRNG} gen A random number generator.
* @return {Boolean} A random boolean
*/
export const bool = gen => (gen.next() & 2) === 2 // brackets are non-optional!
/**
* Generates a random integer with 53 bit resolution.
*
* @param {PRNG} gen A random number generator.
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
* @return {Number} A random integer on [min, max]
*/
export const int53 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => math.floor(real53(gen) * (max + 1 - min) + min)
/**
* Generates a random integer with 32 bit resolution.
*
* @param {PRNG} gen A random number generator.
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
* @return {Number} A random integer on [min, max]
*/
export const int32 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => min + ((gen.next() >>> 0) % (max + 1 - min))
/**
* Generates a random real on [0, 1) with 32 bit resolution.
*
* @param {PRNG} gen A random number generator.
* @return {Number} A random real number on [0, 1).
*/
export const real32 = gen => (gen.next() >>> 0) / binary.BITS32
/**
* Generates a random real on [0, 1) with 53 bit resolution.
*
* @param {PRNG} gen A random number generator.
* @return {Number} A random real number on [0, 1).
*/
export const real53 = gen => (((gen.next() >>> 5) * binary.BIT26) + (gen.next() >>> 6)) / MAX_SAFE_INTEGER
/**
* Generates a random character from char code 32 - 126. I.e. Characters, Numbers, special characters, and Space:
*
* (Space)!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~
*/
export const char = gen => fromCharCode(int32(gen, 32, 126))
/**
* @param {PRNG} gen
* @return {string} A single letter (a-z)
*/
export const letter = gen => fromCharCode(int32(gen, 97, 122))
/**
* @param {PRNG} gen
* @return {string} A random word without spaces consisting of letters (a-z)
*/
export const word = gen => {
const len = int32(gen, 0, 20)
let str = ''
for (let i = 0; i < len; i++) {
str += letter(gen)
}
return str
}
/**
* TODO: this function produces invalid runes. Does not cover all of utf16!!
*/
export const utf16Rune = gen => {
const codepoint = int32(gen, 0, 256)
return fromCodePoint(codepoint)
}
/**
* @param {PRNG} gen
* @param {number} [maxlen = 20]
*/
export const utf16String = (gen, maxlen = 20) => {
const len = int32(gen, 0, maxlen)
let str = ''
for (let i = 0; i < len; i++) {
str += utf16Rune(gen)
}
return str
}
/**
* Returns one element of a given array.
*
* @param {PRNG} gen A random number generator.
* @param {Array<T>} array Non empty Array of possible values.
* @return {T} One of the values of the supplied Array.
* @template T
*/
export const oneOf = (gen, array) => array[int32(gen, 0, array.length - 1)]

View File

@@ -1,114 +0,0 @@
/**
* @module prng
*/
/**
*TODO: enable tests
import * as rt from '../rich-text/formatters.js''
import { test } from '../test/test.js''
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.js''
import Xorshift32 from './PRNG/Xorshift32.js''
import MT19937 from './PRNG/Mt19937.js''
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.js''
import { MAX_SAFE_INTEGER } from '../number/constants.js''
import { BIT32 } from '../binary/constants.js''
function init (Gen) {
return {
gen: new Gen(1234)
}
}
const PRNGs = [
{ name: 'Xoroshiro128plus', Gen: Xoroshiro128plus },
{ name: 'Xorshift32', Gen: Xorshift32 },
{ name: 'MT19937', Gen: MT19937 }
]
const ITERATONS = 1000000
for (const PRNG of PRNGs) {
const prefix = rt.orange`${PRNG.name}:`
test(rt.plain`${prefix} generateBool`, function generateBoolTest (t) {
const { gen } = init(PRNG.Gen)
let head = 0
let tail = 0
let b
let i
for (i = 0; i < ITERATONS; i++) {
b = generateBool(gen)
if (b) {
head++
} else {
tail++
}
}
t.log(`Generated ${head} heads and ${tail} tails.`)
t.assert(tail >= Math.floor(ITERATONS * 0.49), 'Generated enough tails.')
t.assert(head >= Math.floor(ITERATONS * 0.49), 'Generated enough heads.')
})
test(rt.plain`${prefix} generateInt integers average correctly`, function averageIntTest (t) {
const { gen } = init(PRNG.Gen)
let count = 0
let i
for (i = 0; i < ITERATONS; i++) {
count += generateInt(gen, 0, 100)
}
const average = count / ITERATONS
const expectedAverage = 100 / 2
t.log(`Average is: ${average}. Expected average is ${expectedAverage}.`)
t.assert(Math.abs(average - expectedAverage) <= 1, 'Expected average is at most 1 off.')
})
test(rt.plain`${prefix} generateInt32 generates integer with 32 bits`, function generateLargeIntegers (t) {
const { gen } = init(PRNG.Gen)
let num = 0
let i
let newNum
for (i = 0; i < ITERATONS; i++) {
newNum = generateInt32(gen, 0, MAX_SAFE_INTEGER)
if (newNum > num) {
num = newNum
}
}
t.log(`Largest number generated is ${num} (0b${num.toString(2)})`)
t.assert(num > (BIT32 >>> 0), 'Largest number is 32 bits long.')
})
test(rt.plain`${prefix} generateReal has 53 bit resolution`, function real53bitResolution (t) {
const { gen } = init(PRNG.Gen)
let num = 0
let i
let newNum
for (i = 0; i < ITERATONS; i++) {
newNum = generateReal(gen) * MAX_SAFE_INTEGER
if (newNum > num) {
num = newNum
}
}
t.log(`Largest number generated is ${num}.`)
t.assert((MAX_SAFE_INTEGER - num) / MAX_SAFE_INTEGER < 0.01, 'Largest number is close to MAX_SAFE_INTEGER (at most 1% off).')
})
test(rt.plain`${prefix} generateChar generates all described characters`, function real53bitResolution (t) {
const { gen } = init(PRNG.Gen)
const charSet = new Set()
const chars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~"'
let i
let char
for (i = chars.length - 1; i >= 0; i--) {
charSet.add(chars[i])
}
for (i = 0; i < ITERATONS; i++) {
char = generateChar(gen)
charSet.delete(char)
}
t.log(`Charactes missing: ${charSet.size} - generating all of "${chars}"`)
t.assert(charSet.size === 0, 'Generated all documented characters.')
})
}
*/

View File

@@ -1,3 +0,0 @@
/**
* @module random
*/

View File

@@ -1,6 +0,0 @@
/**
* @module string
*/
export const fromCharCode = String.fromCharCode
export const fromCodePoint = String.fromCodePoint

View File

@@ -1,37 +0,0 @@
/**
* @module testing
*/
import * as logging from './logging.js'
import { simpleDiff } from './diff.js'
export const run = async (name, f) => {
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
const start = new Date()
try {
await f(name)
} catch (e) {
logging.print(`%cFailure:%c ${name} in %c${new Date().getTime() - start.getTime()}ms`, 'color:red;font-weight:bold', '', 'color:grey')
throw e
}
logging.print(`%cSuccess:%c ${name} in %c${new Date().getTime() - start.getTime()}ms`, 'color:green;font-weight:bold', '', 'color:grey')
}
export const compareArrays = (as, bs) => {
if (as.length !== bs.length) {
return false
}
for (let i = 0; i < as.length; i++) {
if (as[i] !== bs[i]) {
return false
}
}
return true
}
export const compareStrings = (a, b) => {
if (a !== b) {
const diff = simpleDiff(a, b)
logging.print(`%c${a.slice(0, diff.pos)}%c${a.slice(diff.pos, diff.remove)}%c${diff.insert}%c${a.slice(diff.pos + diff.remove)}`, 'color:grey', 'color:red', 'color:green', 'color:grey')
}
}

View File

@@ -1,3 +0,0 @@
export const getDate = () => new Date()
export const getUnixTime = () => getDate().getTime()

5208
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +1,42 @@
{
"name": "yjs",
"version": "13.0.0-76",
"description": "A ",
"main": "./build/yjs.js",
"module": "./index.js'",
"version": "13.0.0-84",
"description": "Shared Editing Library",
"main": "./dist/yjs.js",
"module": "./dist/yjs.mjs",
"sideEffects": false,
"scripts": {
"test": "npm run lint",
"build": "rm -rf build examples/build && PRODUCTION=1 rollup -c",
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production",
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000",
"dist": "rm -rf dist examples/build && rollup -c",
"serve-examples": "concurrently 'npm run watch' 'serve examples'",
"watch": "rollup -wc",
"debug": "concurrently 'npm run watch' 'cutest-serve build/y.test.js -o'",
"lint": "standard **/*.js",
"lint": "standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
"serve-docs": "npm run docs && serve ./docs/",
"postversion": "npm run build",
"websocket-server": "node ./provider/websocket/server.js",
"now-start": "npm run websocket-server"
"preversion": "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"
},
"files": [
"build/*",
"bindings/*",
"docs/*",
"examples/*",
"lib/*",
"persistences/*",
"protocols/*",
"provider/*",
"bindings/*",
"structs/*",
"dist/*",
"src/*",
"tests/*",
"types/*",
"utils/*",
"index.js",
"README.md",
"LICENSE"
"docs/*"
],
"dictionaries": {
"doc": "docs",
"example": "examples",
"test": "tests",
"lib": "./"
},
"bin": {
"y-websockets": "provider/websocket/server.js"
"test": "tests"
},
"standard": {
"ignore": [
"/build",
"/dist",
"/node_modules",
"/rollup.test.js",
"/rollup.test.js"
"/docs",
"/examples/build"
]
},
"repository": {
@@ -66,39 +53,28 @@
"url": "https://github.com/y-js/yjs/issues"
},
"homepage": "http://y-js.org",
"dependencies": {
"lib0": "0.0.5"
},
"devDependencies": {
"@types/ws": "^6.0.1",
"babel-cli": "^6.26.0",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-transform-regenerator": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-latest": "^6.24.1",
"codemirror": "^5.46.0",
"concurrently": "^3.6.1",
"cutest": "^0.1.9",
"esdoc": "^1.1.0",
"esdoc-standard-plugin": "^1.0.0",
"jsdoc": "^3.5.5",
"jsdoc": "^3.6.2",
"live-server": "^1.2.1",
"prosemirror-example-setup": "^1.0.1",
"prosemirror-schema-basic": "^1.0.0",
"prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.6.5",
"prosemirror-schema-basic": "^1.0.1",
"prosemirror-state": "^1.2.3",
"prosemirror-view": "^1.9.5",
"quill": "^1.3.6",
"quill-cursors": "^1.0.3",
"rollup": "^0.58.2",
"rollup": "^1.11.3",
"rollup-cli": "^1.0.9",
"rollup-plugin-babel": "^2.7.1",
"rollup-plugin-commonjs": "^8.4.1",
"rollup-plugin-inject": "^2.2.0",
"rollup-plugin-multi-entry": "^2.0.2",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-uglify": "^6.0.0",
"rollup-plugin-uglify-es": "0.0.1",
"rollup-regenerator-runtime": "^6.23.1",
"rollup-watch": "^3.2.2",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-node-resolve": "^4.2.4",
"rollup-plugin-terser": "^4.0.4",
"standard": "^11.0.1",
"tui-jsdoc-template": "^1.2.2"
},
"dependencies": {
"ws": "^6.1.0"
"tui-jsdoc-template": "^1.2.2",
"typescript": "^3.4.5",
"y-protocols": "0.0.4"
}
}

View File

@@ -1,74 +0,0 @@
/*
import fs from 'fs'
import path from 'path'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { createMutex } from '../lib/mutex.js'
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
function createFilePath (persistence, roomName) {
// TODO: filename checking!
return path.join(persistence.dir, roomName)
}
export class FilePersistence {
constructor (dir) {
this.dir = dir
this._mutex = createMutex()
}
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
// TODO: implement
// nop
}
saveUpdate (room, y, encodedStructs) {
return new Promise((resolve, reject) => {
this._mutex(() => {
const filePath = createFilePath(this, room)
const updateMessage = encoding.createEncoder()
encodeUpdate(y, encodedStructs, updateMessage)
fs.appendFile(filePath, Buffer.from(encoding.toBuffer(updateMessage)), (err) => {
if (err !== null) {
reject(err)
} else {
resolve()
}
})
}, resolve)
})
}
saveState (roomName, y) {
return new Promise((resolve, reject) => {
const encoder = encoding.createEncoder()
encodeStructsDS(y, encoder)
const filePath = createFilePath(this, roomName)
fs.writeFile(filePath, Buffer.from(encoding.toBuffer(encoder)), (err) => {
if (err !== null) {
reject(err)
} else {
resolve()
}
})
})
}
readState (roomName, y) {
// Check if the file exists in the current directory.
return new Promise((resolve, reject) => {
const filePath = path.join(this.dir, roomName)
fs.readFile(filePath, (err, data) => {
if (err !== null) {
resolve()
// reject(err)
} else {
this._mutex(() => {
console.info(`unpacking data (${data.length})`)
console.time('unpacking')
decodePersisted(y, decoding.createDecoder(data.buffer))
console.timeEnd('unpacking')
})
resolve()
}
})
})
}
}
*/

View File

@@ -1,553 +0,0 @@
/*
import { Y } from '../utils/Y.js'
import { createMutex } from '../lib/mutex.js'
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
function rtop (request) {
return new Promise(function (resolve, reject) {
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
resolve(event.target.result)
}
})
}
function openDB (room) {
return new Promise(function (resolve, reject) {
let request = indexedDB.open(room)
request.onupgradeneeded = function (event) {
const db = event.target.result
if (db.objectStoreNames.contains('updates')) {
db.deleteObjectStore('updates')
}
db.createObjectStore('updates', {autoIncrement: true})
}
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
const db = event.target.result
db.onversionchange = function () { db.close() }
resolve(db)
}
})
}
function persist (room) {
let t = room.db.transaction(['updates'], 'readwrite')
let updatesStore = t.objectStore('updates')
return rtop(updatesStore.getAll())
.then(updates => {
// apply all previous updates before deleting them
room.mutex(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
})
const encoder = new BinaryEncoder()
encodeStructsDS(y, encoder)
// delete all pending updates
rtop(updatesStore.clear()).then(() => {
// write current model
updatesStore.put(encoder.createBuffer())
})
})
}
function saveUpdate (room, updateBuffer) {
const db = room.db
if (db !== null) {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
const updatePut = rtop(updatesStore.put(updateBuffer))
rtop(updatesStore.count()).then(cnt => {
if (cnt >= PREFERRED_TRIM_SIZE) {
persist(room)
}
})
return updatePut
}
}
function registerRoomInPersistence (documentsDB, roomName) {
return documentsDB.then(
db => Promise.all([
db,
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
])
).then(
([db, doc]) => {
if (doc === undefined) {
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
}
}
)
}
const PREFERRED_TRIM_SIZE = 400
export class IndexedDBPersistence {
constructor () {
this._rooms = new Map()
this._documentsDB = new Promise(function (resolve, reject) {
let request = indexedDB.open('_yjs_documents')
request.onupgradeneeded = function (event) {
const db = event.target.result
if (db.objectStoreNames.contains('documents')) {
db.deleteObjectStore('documents')
}
db.createObjectStore('documents', { keyPath: "roomName" })
}
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
const db = event.target.result
db.onversionchange = function () { db.close() }
resolve(db)
}
})
addEventListener('unload', () => {
// close everything when page unloads
this._rooms.forEach(room => {
if (room.db !== null) {
room.db.close()
} else {
room.dbPromise.then(db => db.close())
}
})
this._documentsDB.then(db => db.close())
})
}
getAllDocuments () {
return this._documentsDB.then(
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
)
}
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
this._documentsDB.then(
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
)
}
_createYInstance (roomName) {
const room = this._rooms.get(roomName)
if (room !== undefined) {
return room.y
}
const y = new Y()
return openDB(roomName).then(
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
).then(
updates =>
y.transact(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
}, true)
).then(() => Promise.resolve(y))
}
_persistStructsDS (roomName, structsDS) {
const encoder = new BinaryEncoder()
encoder.writeVarUint(PERSIST_STRUCTS_DS)
encoder.writeArrayBuffer(structsDS)
return openDB(roomName).then(db => {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
return rtop(updatesStore.put(encoder.createBuffer()))
})
}
_persistStructs (roomName, structs) {
const encoder = new BinaryEncoder()
encoder.writeVarUint(PERSIST_UPDATE)
encoder.writeArrayBuffer(structs)
return openDB(roomName).then(db => {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
return rtop(updatesStore.put(encoder.createBuffer()))
})
}
connectY (roomName, y) {
if (this._rooms.has(roomName)) {
throw new Error('A Y instance is already bound to this room!')
}
let room = {
db: null,
dbPromise: null,
channel: null,
mutex: createMutex(),
y
}
if (typeof BroadcastChannel !== 'undefined') {
room.channel = new BroadcastChannel('__yjs__' + roomName)
room.channel.addEventListener('message', e => {
room.mutex(function () {
decodePersisted(y, new BinaryDecoder(e.data))
})
})
}
y.on('destroyed', () => {
this.disconnectY(roomName, y)
})
y.on('afterTransaction', (y, transaction) => {
room.mutex(() => {
if (transaction.encodedStructsLen > 0) {
const encoder = new BinaryEncoder()
const update = new BinaryEncoder()
encodeUpdate(y, transaction.encodedStructs, update)
const updateBuffer = update.createBuffer()
if (room.channel !== null) {
room.channel.postMessage(updateBuffer)
}
if (transaction.encodedStructsLen > 0
import { Y } from '../utils/Y.js'
import { createMutex } from '../lib/mutex.js'
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
function rtop (request) {
return new Promise(function (resolve, reject) {
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
resolve(event.target.result)
}
})
}
function openDB (room) {
return new Promise(function (resolve, reject) {
let request = indexedDB.open(room)
request.onupgradeneeded = function (event) {
const db = event.target.result
if (db.objectStoreNames.contains('updates')) {
db.deleteObjectStore('updates')
}
db.createObjectStore('updates', {autoIncrement: true})
}
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
const db = event.target.result
db.onversionchange = function () { db.close() }
resolve(db)
}
})
}
function persist (room) {
let t = room.db.transaction(['updates'], 'readwrite')
let updatesStore = t.objectStore('updates')
return rtop(updatesStore.getAll())
.then(updates => {
// apply all previous updates before deleting them
room.mutex(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
})
const encoder = new BinaryEncoder()
encodeStructsDS(y, encoder)
// delete all pending updates
rtop(updatesStore.clear()).then(() => {
// write current model
updatesStore.put(encoder.createBuffer())
})
})
}
function saveUpdate (room, updateBuffer) {
const db = room.db
if (db !== null) {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
const updatePut = rtop(updatesStore.put(updateBuffer))
rtop(updatesStore.count()).then(cnt => {
if (cnt >= PREFERRED_TRIM_SIZE) {
persist(room)
}
})
return updatePut
}
}
function registerRoomInPersistence (documentsDB, roomName) {
return documentsDB.then(
db => Promise.all([
db,
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
])
).then(
([db, doc]) => {
if (doc === undefined) {
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
}
}
)
}
const PREFERRED_TRIM_SIZE = 400
export class IndexedDBPersistence {
constructor () {
this._rooms = new Map()
this._documentsDB = new Promise(function (resolve, reject) {
let request = indexedDB.open('_yjs_documents')
request.onupgradeneeded = function (event) {
const db = event.target.result
if (db.objectStoreNames.contains('documents')) {
db.deleteObjectStore('documents')
}
db.createObjectStore('documents', { keyPath: "roomName" })
}
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
const db = event.target.result
db.onversionchange = function () { db.close() }
resolve(db)
}
})
addEventListener('unload', () => {
// close everything when page unloads
this._rooms.forEach(room => {
if (room.db !== null) {
room.db.close()
} else {
room.dbPromise.then(db => db.close())
}
})
this._documentsDB.then(db => db.close())
})
}
getAllDocuments () {
return this._documentsDB.then(
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
)
}
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
this._documentsDB.then(
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
)
}
_createYInstance (roomName) {
const room = this._rooms.get(roomName)
if (room !== undefined) {
return room.y
}
const y = new Y()
return openDB(roomName).then(
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
).then(
updates =>
y.transact(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
}, true)
).then(() => Promise.resolve(y))
}
_persistStructsDS (roomName, structsDS) {
const encoder = new BinaryEncoder()
encoder.writeVarUint(PERSIST_STRUCTS_DS)
encoder.writeArrayBuffer(structsDS)
return openDB(roomName).then(db => {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
return rtop(updatesStore.put(encoder.createBuffer()))
})
}
_persistStructs (roomName, structs) {
const encoder = new BinaryEncoder()
encoder.writeVarUint(PERSIST_UPDATE)
encoder.writeArrayBuffer(structs)
return openDB(roomName).then(db => {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
return rtop(updatesStore.put(encoder.createBuffer()))
})
}
connectY (roomName, y) {
if (this._rooms.has(roomName)) {
throw new Error('A Y instance is already bound to this room!')
}
let room = {
db: null,
dbPromise: null,
channel: null,
mutex: createMutex(),
y
}
if (typeof BroadcastChannel !== 'undefined') {
room.channel = new BroadcastChannel('__yjs__' + roomName)
room.channel.addEventListener('message', e => {
room.mutex(function () {
decodePersisted(y, new BinaryDecoder(e.data))
})
})
}
y.on('destroyed', () => {
this.disconnectY(roomName, y)
})
y.on('afterTransaction', (y, transaction) => {
room.mutex(() => {
if (transaction.encodedStructsLen > 0) {
const encoder = new BinaryEncoder()
const update = new BinaryEncoder()
encodeUpdate(y, transaction.encodedStructs, update)
const updateBuffer = update.createBuffer()
if (room.channel !== null) {
room.channel.postMessage(updateBuffer)
}
if (transaction.encodedStructsLen > 0) {
if (room.db !== null) {
saveUpdate(room, updateBuffer)
}
}
}
})
})
// register document in documentsDB
this._documentsDB.then(
db =>
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
.then(
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
)
)
// open room db and read existing data
return room.dbPromise = openDB(roomName)
.then(db => {
room.db = db
const t = room.db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
// write current state as update
const encoder = new BinaryEncoder()
encodeStructsDS(y, encoder)
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
// read persisted state
return rtop(updatesStore.getAll()).then(updates => {
room.mutex(() => {
y.transact(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
}, true)
})
})
})
})
}
disconnectY (roomName) {
const {
db, channel
} = this._rooms.get(roomName)
db.close()
if (channel !== null) {
channel.close()
}
this._rooms.delete(roomName)
}
/**
* Remove all persisted data that belongs to a room.
* Automatically destroys all Yjs all Yjs instances that persist to
* the room. If `destroyYjsInstances = false` the persistence functionality
* will be removed from the Yjs instances.
*
removePersistedData (roomName, destroyYjsInstances = true) {
this.disconnectY(roomName)
return rtop(indexedDB.deleteDatabase(roomName))
}
}
{
if (room.db !== null) {
saveUpdate(room, updateBuffer)
}
}
}
})
})
// register document in documentsDB
this._documentsDB.then(
db =>
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
.then(
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
)
)
// open room db and read existing data
return room.dbPromise = openDB(roomName)
.then(db => {
room.db = db
const t = room.db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
// write current state as update
const encoder = new BinaryEncoder()
encodeStructsDS(y, encoder)
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
// read persisted state
return rtop(updatesStore.getAll()).then(updates => {
room.mutex(() => {
y.transact(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
}, true)
})
})
})
})
}
disconnectY (roomName) {
const {
db, channel
} = this._rooms.get(roomName)
db.close()
if (channel !== null) {
channel.close()
}
this._rooms.delete(roomName)
}
/**
* Remove all persisted data that belongs to a room.
* Automatically destroys all Yjs all Yjs instances that persist to
* the room. If `destroyYjsInstances = false` the persistence functionality
* will be removed from the Yjs instances.
*
removePersistedData (roomName, destroyYjsInstances = true) {
this.disconnectY(roomName)
return rtop(indexedDB.deleteDatabase(roomName))
}
}
*/

View File

@@ -1,53 +0,0 @@
/*
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
import { writeStructs } from '../MessageHandler/syncStep1.js'
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
export const PERSIST_UPDATE = 0
/**
* Write an update to an encoder.
*
* @param {Y} y A Yjs instance
* @param {Encoder} updateEncoder I.e. transaction.encodedStructs
*
export const encodeUpdate = (y, updateEncoder, encoder) => {
encoder.writeVarUint(PERSIST_UPDATE)
encoder.writeBinaryEncoder(updateEncoder)
}
export const PERSIST_STRUCTS_DS = 1
/**
* Write the current Yjs data model to an encoder.
*
* @param {Y} y A Yjs instance
* @param {Encoder} encoder An encoder to write to
*
export const encodeStructsDS = (y, encoder) => {
encoder.writeVarUint(PERSIST_STRUCTS_DS)
writeStructs(y, encoder, new Map())
writeDeleteSet(y, encoder)
}
/**
* Feed the Yjs instance with the persisted state
* @param {Y} y A Yjs instance.
* @param {Decoder} decoder A Decoder instance that holds the file content.
*
export const decodePersisted = (y, decoder) => {
y.transact(() => {
while (decoder.hasContent()) {
const contentType = decoder.readVarUint()
switch (contentType) {
case PERSIST_UPDATE:
integrateRemoteStructs(decoder, y)
break
case PERSIST_STRUCTS_DS:
integrateRemoteStructs(decoder, y)
readDeleteSet(y, decoder)
break
}
}
}, true)
}
*/

View File

@@ -1,5 +0,0 @@
import * as idb from '../lib/idb.js'
const bc = new BroadcastChannel('ydb-client')
idb.openDB()

View File

@@ -1,33 +0,0 @@
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { Y } from '../utils/Y.js';
export const messagePermissionDenied = 0
/**
* @param {encoding.Encoder} encoder
* @param {string} reason
*/
export const writePermissionDenied = (encoder, reason) => {
encoding.writeVarUint(encoder, messagePermissionDenied)
encoding.writeVarString(encoder, reason)
}
/**
* @callback PermissionDeniedHandler
* @param {any} y
* @param {string} reason
*/
/**
*
* @param {decoding.Decoder} decoder
* @param {Y} y
* @param {PermissionDeniedHandler} permissionDeniedHandler
*/
export const readAuthMessage = (decoder, y, permissionDeniedHandler) => {
switch (decoding.readVarUint(decoder)) {
case messagePermissionDenied: permissionDeniedHandler(y, decoding.readVarString(decoder))
}
}

View File

@@ -1,112 +0,0 @@
/**
* @module awareness-protocol
*/
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
const messageUsersStateChanged = 0
/**
* @typedef {Object} UserStateUpdate
* @property {number} UserStateUpdate.userID
* @property {Object} UserStateUpdate.state
*/
/**
* @param {encoding.Encoder} encoder
* @param {Array<UserStateUpdate>} stateUpdates
*/
export const writeUsersStateChange = (encoder, stateUpdates) => {
const len = stateUpdates.length
encoding.writeVarUint(encoder, messageUsersStateChanged)
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
const {userID, state} = stateUpdates[i]
encoding.writeVarUint(encoder, userID)
encoding.writeVarString(encoder, JSON.stringify(state))
}
}
export const readUsersStateChange = (decoder, y) => {
const added = []
const updated = []
const removed = []
const len = decoding.readVarUint(decoder)
for (let i = 0; i < len; i++) {
const userID = decoding.readVarUint(decoder)
const state = JSON.parse(decoding.readVarString(decoder))
if (userID !== y.userID) {
if (state === null) {
if (y.awareness.has(userID)) {
y.awareness.delete(userID)
removed.push(userID)
}
} else {
if (y.awareness.has(userID)) {
updated.push(userID)
} else {
added.push(userID)
}
y.awareness.set(userID, state)
}
}
}
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
y.emit('awareness', {
added, updated, removed
})
}
}
/**
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
*/
export const forwardUsersStateChange = (decoder, encoder) => {
const len = decoding.readVarUint(decoder)
const updates = []
encoding.writeVarUint(encoder, messageUsersStateChanged)
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
const userID = decoding.readVarUint(decoder)
const state = decoding.readVarString(decoder)
encoding.writeVarUint(encoder, userID)
encoding.writeVarString(encoder, state)
updates.push({userID, state: JSON.parse(state)})
}
return updates
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const readAwarenessMessage = (decoder, y) => {
switch (decoding.readVarUint(decoder)) {
case messageUsersStateChanged:
readUsersStateChange(decoder, y)
break
}
}
/**
* @typedef {Object} UserState
* @property {number} UserState.userID
* @property {any} UserState.state
*/
/**
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @return {Array<UserState>} Array of state updates
*/
export const forwardAwarenessMessage = (decoder, encoder) => {
let s = []
switch (decoding.readVarUint(decoder)) {
case messageUsersStateChanged:
s = forwardUsersStateChange(decoder, encoder)
}
return s
}

View File

@@ -1,448 +0,0 @@
/**
* @module sync-protocol
*/
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import * as ID from '../utils/ID.js'
import { getStruct } from '../utils/structReferences.js'
import { deleteItemRange } from '../utils/structManipulation.js'
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
import { Item } from '../structs/Item.js'
import * as stringify from '../utils/structStringify.js'
/**
* @typedef {Map<number, number>} StateSet
*/
/**
* Core Yjs only defines three message types:
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the the client is assured that
* it received all information from the remote client.
*
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
*
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
* Therefore it is necesarry that the client initiates the sync.
*
* Construction of a message:
* [messageType : varUint, message definition..]
*
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
*
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
*/
export const messageYjsSyncStep1 = 0
export const messageYjsSyncStep2 = 1
export const messageYjsUpdate = 2
/**
* Stringifies a message-encoded Delete Set.
*
* @param {decoding.Decoder} decoder
* @return {string}
*/
export const stringifyDeleteSet = (decoder) => {
let str = ''
const dsLength = decoding.readUint32(decoder)
for (let i = 0; i < dsLength; i++) {
str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user
const dvLength = decoding.readUint32(decoder)
for (let j = 0; j < dvLength; j++) {
str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n`
}
}
return str
}
/**
* Write the DeleteSet of a shared document to an Encoder.
*
* @param {encoding.Encoder} encoder
* @param {Y} y
*/
export const writeDeleteSet = (encoder, y) => {
let currentUser = null
let currentLength
let lastLenPos
let numberOfUsers = 0
const laterDSLenPus = encoding.length(encoder)
encoding.writeUint32(encoder, 0)
y.ds.iterate(null, null, n => {
const user = n._id.user
const clock = n._id.clock
const len = n.len
const gc = n.gc
if (currentUser !== user) {
numberOfUsers++
// a new user was foundimport { StateSet } from '../Store/StateStore.js' // eslint-disable-line
if (currentUser !== null) { // happens on first iteration
encoding.setUint32(encoder, lastLenPos, currentLength)
}
currentUser = user
encoding.writeVarUint(encoder, user)
// pseudo-fill pos
lastLenPos = encoding.length(encoder)
encoding.writeUint32(encoder, 0)
currentLength = 0
}
encoding.writeVarUint(encoder, clock)
encoding.writeVarUint(encoder, len)
encoding.writeUint8(encoder, gc ? 1 : 0)
currentLength++
})
if (currentUser !== null) { // happens on first iteration
encoding.setUint32(encoder, lastLenPos, currentLength)
}
encoding.setUint32(encoder, laterDSLenPus, numberOfUsers)
}
/**
* Read delete set from Decoder and apply it to a shared document.
*
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const readDeleteSet = (decoder, y) => {
const dsLength = decoding.readUint32(decoder)
for (let i = 0; i < dsLength; i++) {
const user = decoding.readVarUint(decoder)
const dv = []
const dvLength = decoding.readUint32(decoder)
for (let j = 0; j < dvLength; j++) {
const from = decoding.readVarUint(decoder)
const len = decoding.readVarUint(decoder)
const gc = decoding.readUint8(decoder) === 1
dv.push({from, len, gc})
}
if (dvLength > 0) {
const deletions = []
let pos = 0
let d = dv[pos]
y.ds.iterate(ID.createID(user, 0), ID.createID(user, Number.MAX_VALUE), n => {
// cases:
// 1. d deletes something to the right of n
// => go to next n (break)
// 2. d deletes something to the left of n
// => create deletions
// => reset d accordingly
// *)=> if d doesn't delete anything anymore, go to next d (continue)
// 3. not 2) and d deletes something that also n deletes
// => reset d so that it doesn't contain n's deletion
// *)=> if d does not delete anything anymore, go to next d (continue)
while (d != null) {
var diff = 0 // describe the diff of length in 1) and 2)
if (n._id.clock + n.len <= d.from) {
// 1)
break
} else if (d.from < n._id.clock) {
// 2)
// delete maximum the len of d
// else delete as much as possible
diff = Math.min(n._id.clock - d.from, d.len)
// deleteItemRange(y, user, d.from, diff, true)
deletions.push([user, d.from, diff])
} else {
// 3)
diff = n._id.clock + n.len - d.from // never null (see 1)
if (d.gc && !n.gc) {
// d marks as gc'd but n does not
// then delete either way
// deleteItemRange(y, user, d.from, Math.min(diff, d.len), true)
deletions.push([user, d.from, Math.min(diff, d.len)])
}
}
if (d.len <= diff) {
// d doesn't delete anything anymore
d = dv[++pos]
} else {
d.from = d.from + diff // reset pos
d.len = d.len - diff // reset length
}
}
})
// TODO: It would be more performant to apply the deletes in the above loop
// Adapt the Tree implementation to support delete while iterating
for (let i = deletions.length - 1; i >= 0; i--) {
const del = deletions[i]
deleteItemRange(y, del[0], del[1], del[2], true)
}
// for the rest.. just apply it
for (; pos < dv.length; pos++) {
d = dv[pos]
deleteItemRange(y, user, d.from, d.len, true)
// deletions.push([user, d.from, d.len, d.gc)
}
}
}
}
/**
* Read a StateSet from Decoder and return it as string.
*
* @param {decoding.Decoder} decoder
* @return {string}
*/
export const stringifyStateSet = decoder => {
let s = 'State Set: '
readStateSet(decoder).forEach((clock, user) => {
s += `(${user}: ${clock}), `
})
return s
}
/**
* Write StateSet to Encoder
*
* @param {encoding.Encoder} encoder
* @param {Y} y
*/
export const writeStateSet = (encoder, y) => {
const state = y.ss.state
// write as fixed-size number to stay consistent with the other encode functions.
// => anytime we write the number of objects that follow, encode as fixed-size number.
encoding.writeUint32(encoder, state.size)
state.forEach((clock, user) => {
encoding.writeVarUint(encoder, user)
encoding.writeVarUint(encoder, clock)
})
}
/**
* Read StateSet from Decoder and return as Map
*
* @param {decoding.Decoder} decoder
* @return {StateSet}
*/
export const readStateSet = decoder => {
const ss = new Map()
const ssLength = decoding.readUint32(decoder)
for (let i = 0; i < ssLength; i++) {
const user = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
ss.set(user, clock)
}
return ss
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @return {string}
*/
export const stringifyStructs = (decoder, y) => {
let str = ''
const len = decoding.readUint32(decoder)
for (let i = 0; i < len; i++) {
let reference = decoding.readVarUint(decoder)
let Constr = getStruct(reference)
let struct = new Constr()
let missing = struct._fromBinary(y, decoder)
let logMessage = ' ' + struct._logString()
if (missing.length > 0) {
logMessage += ' .. missing: ' + missing.map(stringify.stringifyItemID).join(', ')
}
str += logMessage + '\n'
}
return str
}
/**
* Write all Items that are not not included in ss to
* the encoder object.
*
* @param {encoding.Encoder} encoder
* @param {Y} y
* @param {StateSet} ss State Set received from a remote client. Maps from client id to number of created operations by client id.
*/
export const writeStructs = (encoder, y, ss) => {
const lenPos = encoding.length(encoder)
encoding.writeUint32(encoder, 0)
let len = 0
for (let user of y.ss.state.keys()) {
let clock = ss.get(user) || 0
if (user !== ID.RootFakeUserID) {
const minBound = ID.createID(user, clock)
const overlappingLeft = y.os.findPrev(minBound)
const rightID = overlappingLeft === null ? null : overlappingLeft._id
if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) {
// TODO: only write partial content (only missing content)
// const struct = overlappingLeft._clonePartial(clock - rightID.clock)
const struct = overlappingLeft
struct._toBinary(encoder)
len++
}
y.os.iterate(minBound, ID.createID(user, Number.MAX_VALUE), struct => {
struct._toBinary(encoder)
len++
})
}
}
encoding.setUint32(encoder, lenPos, len)
}
/**
* Read structs and delete operations from decoder and apply them on a shared document.
*
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const readStructs = (decoder, y) => {
const len = decoding.readUint32(decoder)
for (let i = 0; i < len; i++) {
integrateRemoteStruct(decoder, y)
}
}
/**
* Read SyncStep1 and return it as a readable string.
*
* @param {decoding.Decoder} decoder
* @return {string}
*/
export const stringifySyncStep1 = (decoder) => {
let s = 'SyncStep1: '
const len = decoding.readUint32(decoder)
for (let i = 0; i < len; i++) {
const user = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
s += `(${user}:${clock})`
}
return s
}
/**
* Create a sync step 1 message based on the state of the current shared document.
*
* @param {encoding.Encoder} encoder
* @param {Y} y
*/
export const writeSyncStep1 = (encoder, y) => {
encoding.writeVarUint(encoder, messageYjsSyncStep1)
writeStateSet(encoder, y)
}
/**
* Read SyncStep1 message and reply with SyncStep2.
*
* @param {decoding.Decoder} decoder The reply to the received message
* @param {encoding.Encoder} encoder The received message
* @param {Y} y
*/
export const readSyncStep1 = (decoder, encoder, y) => {
// read sync step 1 message
const ss = readStateSet(decoder)
// write sync step 2
encoding.writeVarUint(encoder, messageYjsSyncStep2)
writeStructs(encoder, y, ss)
writeDeleteSet(encoder, y)
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @return {string}
*/
export const stringifySyncStep2 = (decoder, y) => {
let str = ' == Sync step 2:\n'
str += ' + Structs:\n'
str += stringifyStructs(decoder, y)
// write DS to string
str += ' + Delete Set:\n'
str += stringifyDeleteSet(decoder)
return str
}
/**
* Read and apply Structs and then DeleteSet to a y instance.
*
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const readSyncStep2 = (decoder, y) => {
readStructs(decoder, y)
readDeleteSet(decoder, y)
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @return {string}
*/
export const stringifyUpdate = (decoder, y) =>
' == Update:\n' + stringifyStructs(decoder, y)
/**
* @param {encoding.Encoder} encoder
* @param {number} numOfStructs
* @param {encoding.Encoder} updates
*/
export const writeUpdate = (encoder, numOfStructs, updates) => {
encoding.writeVarUint(encoder, messageYjsUpdate)
encoding.writeUint32(encoder, numOfStructs)
encoding.writeBinaryEncoder(encoder, updates)
}
export const readUpdate = readStructs
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @return {string} The message converted to string
*/
export const stringifySyncMessage = (decoder, y) => {
const messageType = decoding.readVarUint(decoder)
let stringifiedMessage
let stringifiedMessageType
switch (messageType) {
case messageYjsSyncStep1:
stringifiedMessageType = 'YjsSyncStep1'
stringifiedMessage = stringifySyncStep1(decoder)
break
case messageYjsSyncStep2:
stringifiedMessageType = 'YjsSyncStep2'
stringifiedMessage = stringifySyncStep2(decoder, y)
break
case messageYjsUpdate:
stringifiedMessageType = 'YjsUpdate'
stringifiedMessage = stringifyStructs(decoder, y)
break
default:
stringifiedMessageType = 'Unknown'
stringifiedMessage = 'Unknown'
}
return `Message ${stringifiedMessageType}:\n${stringifiedMessage}`
}
/**
* @param {decoding.Decoder} decoder A message received from another client
* @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
* @param {Y} y
*/
export const readSyncMessage = (decoder, encoder, y) => {
const messageType = decoding.readVarUint(decoder)
switch (messageType) {
case messageYjsSyncStep1:
readSyncStep1(decoder, encoder, y)
break
case messageYjsSyncStep2:
y.transact(() => readSyncStep2(decoder, y), true)
break
case messageYjsUpdate:
y.transact(() => readUpdate(decoder, y), true)
break
default:
throw new Error('Unknown message type')
}
return messageType
}

View File

@@ -1,5 +0,0 @@
/**
* @module provider/websocket
*/
export * from './websocket/WebSocketProvider.js'

View File

@@ -1,166 +0,0 @@
/**
* @module provider/websocket
*/
/* eslint-env browser */
import * as Y from '../../index.js'
import * as bc from '../../lib/broadcastchannel.js'
const messageSync = 0
const messageAwareness = 1
const messageAuth = 2
const reconnectTimeout = 3000
/**
* @param {WebsocketsSharedDocument} doc
* @param {string} reason
*/
const permissionDeniedHandler = (doc, reason) => console.warn(`Permission denied to access ${doc.url}.\n${reason}`)
/**
* @param {WebsocketsSharedDocument} doc
* @param {ArrayBuffer} buf
* @return {Y.encoding.Encoder}
*/
const readMessage = (doc, buf) => {
const decoder = Y.decoding.createDecoder(buf)
const encoder = Y.encoding.createEncoder()
const messageType = Y.decoding.readVarUint(decoder)
switch (messageType) {
case messageSync:
Y.encoding.writeVarUint(encoder, messageSync)
doc.mux(() =>
Y.syncProtocol.readSyncMessage(decoder, encoder, doc)
)
break
case messageAwareness:
Y.awarenessProtocol.readAwarenessMessage(decoder, doc)
break
case messageAuth:
Y.authProtocol.readAuthMessage(decoder, doc, permissionDeniedHandler)
}
return encoder
}
const setupWS = (doc, url) => {
const websocket = new WebSocket(url)
websocket.binaryType = 'arraybuffer'
doc.ws = websocket
websocket.onmessage = event => {
const encoder = readMessage(doc, event.data)
if (Y.encoding.length(encoder) > 1) {
websocket.send(Y.encoding.toBuffer(encoder))
}
}
websocket.onclose = () => {
doc.ws = null
doc.wsconnected = false
doc.emit('status', {
status: 'connected'
})
setTimeout(setupWS, reconnectTimeout, doc, url)
}
websocket.onopen = () => {
doc.wsconnected = true
doc.emit('status', {
status: 'disconnected'
})
// always send sync step 1 when connected
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeSyncStep1(encoder, doc)
websocket.send(Y.encoding.toBuffer(encoder))
// force send stored awareness info
doc.setAwarenessField(null, null)
}
}
const broadcastUpdate = (y, transaction) => {
if (transaction.encodedStructsLen > 0) {
y.mux(() => {
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
const buf = Y.encoding.toBuffer(encoder)
if (y.wsconnected) {
y.ws.send(buf)
}
bc.publish(y.url, buf)
})
}
}
class WebsocketsSharedDocument extends Y.Y {
constructor (url) {
super()
this.url = url
this.wsconnected = false
this.mux = Y.createMutex()
this.ws = null
this._localAwarenessState = {}
this.awareness = new Map()
setupWS(this, url)
this.on('afterTransaction', broadcastUpdate)
this._bcSubscriber = data => {
const encoder = readMessage(this, data) // already muxed
this.mux(() => {
if (Y.encoding.length(encoder) > 1) {
bc.publish(url, Y.encoding.toBuffer(encoder))
}
})
}
bc.subscribe(url, this._bcSubscriber)
// send sync step1 to bc
this.mux(() => {
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeSyncStep1(encoder, this)
bc.publish(url, Y.encoding.toBuffer(encoder))
})
}
getLocalAwarenessInfo () {
return this._localAwarenessState
}
getAwarenessInfo () {
return this.awareness
}
setAwarenessField (field, value) {
if (field !== null) {
this._localAwarenessState[field] = value
}
if (this.wsconnected) {
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageAwareness)
Y.awarenessProtocol.writeUsersStateChange(encoder, [{ userID: this.userID, state: this._localAwarenessState }])
const buf = Y.encoding.toBuffer(encoder)
this.ws.send(buf)
}
}
}
export class WebsocketProvider {
constructor (url) {
// ensure that url is always ends with /
while (url[url.length - 1] === '/') {
url = url.slice(0, url.length - 1)
}
this.url = url + '/'
/**
* @type {Map<string, WebsocketsSharedDocument>}
*/
this.docs = new Map()
}
/**
* @param {string} name
* @return {WebsocketsSharedDocument}
*/
get (name) {
let doc = this.docs.get(name)
if (doc === undefined) {
doc = new WebsocketsSharedDocument(this.url + name)
}
return doc
}
}

View File

@@ -1,127 +0,0 @@
/**
* @module provider/websocket/server
*/
const Y = require('../../build/yjs.js')
const WebSocket = require('ws')
const http = require('http')
const port = process.env.PORT || 1234
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('okay')
})
const wss = new WebSocket.Server({ noServer: true })
const docs = new Map()
const messageSync = 0
const messageAwareness = 1
const messageAuth = 2
const afterTransaction = (doc, transaction) => {
if (transaction.encodedStructsLen > 0) {
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
const message = Y.encoding.toBuffer(encoder)
doc.conns.forEach((_, conn) => conn.send(message))
}
}
class WSSharedDoc extends Y.Y {
constructor () {
super({ gc: true })
this.mux = Y.createMutex()
/**
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
* @type {Map<Object, Set<number>>}
*/
this.conns = new Map()
this.awareness = new Map()
this.on('afterTransaction', afterTransaction)
}
}
const messageListener = (conn, doc, message) => {
const encoder = Y.encoding.createEncoder()
const decoder = Y.decoding.createDecoder(message)
const messageType = Y.decoding.readVarUint(decoder)
switch (messageType) {
case messageSync:
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.readSyncMessage(decoder, encoder, doc)
if (Y.encoding.length(encoder) > 1) {
conn.send(Y.encoding.toBuffer(encoder))
}
break
case messageAwareness: {
Y.encoding.writeVarUint(encoder, messageAwareness)
const updates = Y.awarenessProtocol.forwardAwarenessMessage(decoder, encoder)
updates.forEach(update => {
doc.awareness.set(update.userID, update.state)
doc.conns.get(conn).add(update.userID)
})
const buff = Y.encoding.toBuffer(encoder)
doc.conns.forEach((_, c) => {
c.send(buff)
})
break
}
}
}
const setupConnection = (conn, req) => {
conn.binaryType = 'arraybuffer'
// get doc, create if it does not exist yet
let doc = docs.get(req.url.slice(1))
if (doc === undefined) {
doc = new WSSharedDoc()
docs.set(req.url.slice(1), doc)
}
doc.conns.set(conn, new Set())
// listen and reply to events
conn.on('message', message => messageListener(conn, doc, message))
conn.on('close', () => {
const controlledIds = doc.conns.get(conn)
doc.conns.delete(conn)
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageAwareness)
Y.awarenessProtocol.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => {
doc.awareness.delete(userID)
return { userID, state: null }
}))
const buf = Y.encoding.toBuffer(encoder)
doc.conns.forEach((_, conn) => conn.send(buf))
})
// send sync step 1
const encoder = Y.encoding.createEncoder()
Y.encoding.writeVarUint(encoder, messageSync)
Y.syncProtocol.writeSyncStep1(encoder, doc)
conn.send(Y.encoding.toBuffer(encoder))
if (doc.awareness.size > 0) {
const encoder = Y.encoding.createEncoder()
const userStates = []
doc.awareness.forEach((state, userID) => {
userStates.push({ state, userID })
})
Y.encoding.writeVarUint(encoder, messageAwareness)
Y.awarenessProtocol.writeUsersStateChange(encoder, userStates)
conn.send(Y.encoding.toBuffer(encoder))
}
}
wss.on('connection', setupConnection)
server.on('upgrade', (request, socket, head) => {
// You may check auth of request here..
wss.handleUpgrade(request, socket, head, ws => {
wss.emit('connection', ws, request)
})
})
server.listen(port)
console.log('running on port', port)

View File

@@ -1,23 +0,0 @@
/**
* @module provider/ydb
*/
import * as globals from './globals.js'
export const Class = class NamedEventHandler {
constructor () {
this.l = globals.createMap()
}
on (eventname, f) {
const l = this.l
let h = l.get(eventname)
if (h === undefined) {
h = globals.createSet()
l.set(eventname, h)
}
h.add(f)
}
}
export const fire = (handler, eventname, event) =>
handler.l.get(eventname).forEach(f => f(event))

View File

@@ -1 +0,0 @@
* Host should discard message when confNumber is older than expected

View File

@@ -1,56 +0,0 @@
Implement default dom filter..
But requires more explicit filtering of src attributes
e.g. src="java\nscript:alert(0)"
function domFilter (nodeName, attributes) {
// Filter all attributes that start with on*. E.g. onclick does execute code
// If key is 'href' or 'src', filter everything but 'http*', 'blob*', or 'data:image*' urls
attributes.forEach(function (value, key) {
key = key.toLowerCase();
value = value.toLowerCase();
if (key != null && (
// filter all attributes starting with 'on'
key.substr(0, 2) === 'on' ||
// if key is 'href' or 'src', filter everything but http, blob, or data:image
(
(key === 'href' || key === 'src') &&
value.substr(0, 4) !== 'http' &&
value.substr(0, 4) !== 'blob' &&
value.substr(0, 10) !== 'data:image'
)
)) {
attributes.delete(key);
}
});
switch (nodeName) {
case 'SCRIPT':
return null;
case 'EN-ADORNMENTS':
// TODO: Remove EN-ADORNMENTS check when merged into master branch!
return null;
case 'EN-TABLE':
attributes.delete('class');
return attributes;
case 'EN-COMMENT':
attributes.delete('style');
attributes.delete('class');
return attributes;
case 'SPAN':
return (attributes.get('id') || '').substr(0, 5) === 'goog_' ? null : attributes;
case 'TD':
attributes.delete('class');
return attributes;
case 'EMBED':
attributes.delete('src');
attributes.delete('style');
attributes.delete('data-reference');
return attributes;
case 'FORM':
attributes.delete('action');
return attributes;
default:
return (nodeName || '').substr(0, 3) === 'UI-' ? null : attributes;
}
}

View File

@@ -1,218 +0,0 @@
/**
* @module provider/ydb
*/
/* eslint-env browser */
import * as idbactions from './idbactions.js'
import * as globals from '../../lib/globals.js'
import * as message from './message.js'
import * as bc from './broadcastchannel.js'
import * as encoding from '../../lib/encoding.js'
import * as logging from '../../lib/logging.js'
import * as idb from '../../lib/idb.js'
import * as decoding from '../../lib/decoding.js'
import { Y } from '../../utils/Y.js'
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.js'
import { createMutualExclude } from '../../lib/mutualExclude.js'
import * as NamedEventHandler from './NamedEventHandler.js'
/**
* @typedef RoomState
* @type {Object}
* @property {number} rsid room session id, -1 if unknown (created locally)
* @property {number} offset By server, -1 if unknown
* @property {number} cOffset current offset by client
*/
/**
* @typedef SyncState
* @type {Object}
* @property {boolean} upsynced True if all local updates have been sent to the server and the server confirmed that it received the update
* @property {boolean} downsynced True if the current session subscribed to the room, the server confirmed the subscription, and the initial data was received
* @property {boolean} persisted True if the server confirmed that it persisted all published data
*/
/**
*
*/
export class YdbClient extends NamedEventHandler.Class {
constructor (url, db) {
super()
this.url = url
this.ws = new WebSocket(url)
this.rooms = globals.createMap()
this.db = db
this.connected = false
/**
* Set of room states. We try to keep it up in sync with idb, but this may fail due to concurrency with other windows.
* TODO: implement tests for this
* @type Map<string, RoomState>
*/
this.roomStates = globals.createMap()
/**
* Meta information about unconfirmed updates created by this client.
* Maps from confid to roomname
* @type Map<number, string>
*/
this.clientUnconfirmedStates = globals.createMap()
bc.subscribeYdbEvents(this)
initWS(this, this.ws)
}
/**
* Open a Yjs instance that connects to `roomname`.
* @param {string} roomname
* @return {Y}
*/
getY (roomname) {
const y = new Y(roomname)
const mutex = createMutualExclude()
y.on('afterTransaction', (y, transaction) => mutex(() => {
if (transaction.encodedStructsLen > 0) {
update(this, roomname, transaction.encodedStructs.createBuffer())
}
}))
subscribe(this, roomname, update => mutex(() => {
y.transact(() => {
const decoder = decoding.createDecoder(update)
while (decoding.hasContent(decoder)) {
integrateRemoteStruct(y, decoder)
}
}, true)
}))
return y
}
getRoomState (roomname) {
return bc.computeRoomState(this, bc.getUnconfirmedRooms(this), roomname)
}
getRoomStates () {
const unconfirmedRooms = bc.getUnconfirmedRooms(this)
const states = globals.createMap()
this.roomStates.forEach((rstate, roomname) => states.set(roomname, bc.computeRoomState(this, unconfirmedRooms, roomname)))
return states
}
}
/**
* Initialize WebSocket connection. Try to reconnect on error/disconnect.
* @param {YdbClient} ydb
* @param {WebSocket} ws
*/
const initWS = (ydb, ws) => {
ws.binaryType = 'arraybuffer'
ws.onclose = () => {
ydb.connected = false
logging.log('Disconnected from ydb. Reconnecting..')
ydb.ws = new WebSocket(ydb.url)
initWS(ydb, ws)
}
ws.onopen = () => {
const t = idbactions.createTransaction(ydb.db)
globals.pall([idbactions.getRoomMetas(t), idbactions.getUnconfirmedSubscriptions(t), idbactions.getUnconfirmedUpdates(t)]).then(([metas, us, unconfirmedUpdates]) => {
let subs = []
metas.forEach(meta => {
subs.push({
room: meta.room,
offset: meta.offset,
rsid: meta.rsid
})
})
us.forEach(room => {
subs.push({
room, offset: 0, rsid: 0
})
})
subs = subs.filter(subdev => !ydb.roomStates.has(subdev.room)) // filter already subbed rooms
ydb.connected = true
const encoder = encoding.createEncoder()
if (subs.length > 0) {
encoding.writeArrayBuffer(encoder, message.createSub(subs))
bc._broadcastYdbSyncingRoomsToServer(subs.map(subdev => subdev.room))
}
encoding.writeArrayBuffer(encoder, unconfirmedUpdates)
send(ydb, encoding.toBuffer(encoder))
})
}
ws.onmessage = event => message.readMessage(ydb, event.data)
}
// maps from dbNamespace to db
const dbPromises = new Map()
/**
* Factory function. Get a ydb instance that connects to url, and uses dbNamespace as indexeddb namespace.
* Create if it does not exist yet.
*
* @param {string} url
* @param {string} dbNamespace
* @return {Promise<YdbClient>}
*/
export const get = (url, dbNamespace = 'ydb') => {
if (!dbPromises.has(dbNamespace)) {
dbPromises.set(dbNamespace, idbactions.openDB(dbNamespace))
}
return dbPromises.get(dbNamespace).then(db => globals.presolve(new YdbClient(url, db)))
}
/**
* Remove a db namespace. Call this to remove any persisted data. Make sure to close active sessions.
* TODO: destroy active ydbClient sessions / throw if a session is still active
* @param {string} dbNamespace
* @return {Promise}
*/
export const clear = (dbNamespace = 'ydb') => idb.deleteDB(dbNamespace)
/**
* @param {YdbClient} ydb
* @param {ArrayBuffer} m
*/
export const send = (ydb, m) => ydb.connected && m.byteLength !== 0 && ydb.ws.send(m)
/**
* @param {YdbClient} ydb
* @param {string} room
* @param {ArrayBuffer} update
*/
export const update = (ydb, room, update) => {
bc.publishRoomData(room, update)
const t = idbactions.createTransaction(ydb.db)
logging.log(`Write Unconfirmed Update. room "${room}", ${JSON.stringify(update)}`)
return idbactions.writeClientUnconfirmed(t, room, update).then(clientConf => {
logging.log(`Send Unconfirmed Update. connected ${ydb.connected} room "${room}", clientConf ${clientConf}`)
bc._broadcastYdbCUConfCreated(clientConf, room)
send(ydb, message.createUpdate(room, update, clientConf))
})
}
export const subscribe = (ydb, room, f) => {
bc.subscribeRoomData(room, f)
const t = idbactions.createTransaction(ydb.db)
if (!ydb.roomStates.has(room)) {
subscribeRooms(ydb, [room])
}
idbactions.getRoomData(t, room).then(data => {
if (data.byteLength > 0) {
f(data)
}
})
}
export const subscribeRooms = (ydb, rooms) => {
const t = idbactions.createTransaction(ydb.db)
let subs = []
// TODO: try not to do too many single calls. Implement getRoomMetas(t, rooms) or retrieve all metas once and store them on ydb
// TODO: find out performance of getRoomMetas with all metas
return globals.pall(rooms.map(room => idbactions.getRoomMeta(t, room).then(meta => {
if (meta === undefined) {
subs.push(room)
return idbactions.writeUnconfirmedSubscription(t, room)
}
}))).then(() => {
subs = subs.filter(room => !ydb.roomStates.has(room))
// write all sub messages when all unconfirmed subs are writted to idb
if (subs.length > 0) {
send(ydb, message.createSub(subs.map(room => ({room, offset: 0, rsid: 0}))))
bc._broadcastYdbSyncingRoomsToServer(subs)
}
})
}

Some files were not shown because too many files have changed in this diff Show More