From 6cb64b37078ab198354c6166724b7b2f795dcc44 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Wed, 4 Sep 2019 13:05:03 +0200 Subject: [PATCH] move repository to yjs org --- .jsdoc.json | 2 +- README.md | 1106 ++++++++++++++++++++++++++++++---------- README.v12.md | 305 +++++++++++ README.v13.md | 874 ------------------------------- package.json | 14 +- tests/y-array.tests.js | 2 +- 6 files changed, 1142 insertions(+), 1161 deletions(-) create mode 100644 README.v12.md delete mode 100644 README.v13.md diff --git a/.jsdoc.json b/.jsdoc.json index 1cca0d93..86397dac 100644 --- a/.jsdoc.json +++ b/.jsdoc.json @@ -17,7 +17,7 @@ "useCollapsibles": true, "collapse": true, "resources": { - "y-js.org": "yjs.website" + "yjs.dev": "Yjs website" }, "logo": { "url": "https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png", diff --git a/README.md b/README.md index 42e54230..be5c2aee 100644 --- a/README.md +++ b/README.md @@ -1,305 +1,855 @@ -# ![Yjs](https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png) +# ![Yjs](https://yjs.dev/images/logo/yjs-120x120.png) -Yjs is a framework for offline-first p2p shared editing on structured data like -text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides -most of the complexity of concurrent editing. For additional information, demos, -and tutorials visit [y-js.org](http://y-js.org/). +> A CRDT framework with a powerful abstraction of shared data -:warning: Checkout the [v13 docs](./README.v13.md) for the upcoming release :warning: +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. -### Extensions -Yjs only knows how to resolve conflicts on shared data. You have to choose a .. +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. -* *Connector* - a communication protocol that propagates changes to the clients -* *Database* - a database to store your changes -* one or more *Types* - that represent the shared data +* Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos) +* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev) +* Benchmarks: + [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks) -Connectors, Databases, and Types are available as modules that extend Yjs. Here -is a list of the modules we know of: +:warning: This is the documentation for v13 (still in alpha). For the stable v12 +release checkout the [v12 docs](./README.v12.md) :warning: -##### Connectors +## Table of Contents -|Name | Description | -|----------------|-----------------------------------| -|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC| -|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets | -|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))| -|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!| -|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios| +* [Overview](#Overview) + * [Bindings](#Bindings) + * [Providers](#Providers) +* [Getting Started](#Getting-Started) +* [API](#API) + * [Shared Types](#Shared-Types) + * [Y.Doc](#YDoc) + * [Document Updates](#Document-Updates) + * [Relative Positions](#Relative-Positions) + * [Y.UndoManager](#YUndoManager) +* [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) -##### Database adapters +## Overview -|Name | Description | -|----------------|-----------------------------------| -|[memory](https://github.com/y-js/y-memory) | In-memory storage. | -|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser | -|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps | +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. -##### Types +### Bindings -| Name | Description | -|----------|-------------------| -|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object | -|[array](https://github.com/y-js/y-array) | A shared Array implementation | -|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects | -|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to the [Ace Editor](https://ace.c9.io), [CodeMirror](https://codemirror.net/), [Monaco](https://github.com/Microsoft/monaco-editor), textareas, input elements, and HTML elements (e.g. <*h1*>, or <*p*>) | -|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)| +| Name | Cursors | Binding | Demo | +|---|:-:|---|---| +| [ProseMirror](https://prosemirror.net/)                                                   | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) | +| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) | +| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) | +| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) | +| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) | +| [Textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) | | [y-textarea](http://github.com/yjs/y-textarea) | [demo](https://yjs-demos.now.sh/textarea/) | +| [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) | | [y-dom](http://github.com/yjs/y-dom) | [demo](https://yjs-demos.now.sh/dom/) | -##### Other +### Providers -| Name | Description | -|-----------|-------------------| -|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element | +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. -## Use it! -Install Yjs, and its modules with [bower](http://bower.io/), or -[npm](https://www.npmjs.org/package/yjs). +
+
y-websocket
+
+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. +
+
y-mesh
+
+[WIP] Creates a connected graph of webrtc connections with a high +strength. 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. +
+
y-dat
+
+[WIP] Write document updates effinciently to the dat network using +multifeed. 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. +
+
-### Bower +## Getting Started -``` -bower install --save yjs y-array % add all y-* modules you want to use -``` -You only need to include the `y.js` file. Yjs is able to automatically require -missing modules. -``` - -``` +Install Yjs and a provider with your favorite package manager: -### CDN - -``` - - - - - - -// .. -// do the same for all modules you want to use -``` - -### Npm - -``` -npm install --save yjs % add all y-* modules you want to use -``` - -If you don't include via script tag, you have to explicitly include all modules! -(Same goes for other module systems) -``` -var Y = require('yjs') -require('y-array')(Y) // add the y-array type to Yjs -require('y-websockets-client')(Y) -require('y-memory')(Y) -require('y-map')(Y) -require('y-text')(Y) -// .. -// do the same for all modules you want to use -``` - -### ES6 Syntax - -``` -import Y from 'yjs' -import yArray from 'y-array' -import yWebsocketsClient from 'y-webrtc' -import yMemory from 'y-memory' -import yMap from 'y-map' -import yText from 'y-text' -// .. -Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */) -``` - -# Text editing example - -Install dependencies -``` -bower i yjs y-memory y-webrtc y-array y-text -``` - -Here is a simple example of a shared textarea -```HTML - - - - - - - - - -``` - -## Get Help & Give Help -There are some friendly people on [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join! - -Report _any_ issues to the -[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very -soon, if possible. - -# API - -### Y(options) -* Y.extend(module1, module2, ..) - * Add extensions to Y - * `Y.extend(require('y-webrtc'))` has the same semantics as - `require('y-webrtc')(Y)` -* options.db - * Will be forwarded to the database adapter. Specify the database adaper on - `options.db.name`. - * Have a look at the used database adapter repository to see all available - options. -* options.connector - * Will be forwarded to the connector adapter. Specify the connector adaper on - `options.connector.name`. - * All our connectors implement a `room` property. Clients that specify the - same room share the same data. - * All of our connectors specify an `url` property that defines the connection - endpoint of the used connector. - * All of our connectors also have a default connection endpoint that you can - use for development. - * Set `options.connector.generateUserId = true` in order to genenerate a - userid, instead of receiving one from the server. This way the `Y(..)` is - immediately going to be resolved, without waiting for any confirmation from - the server. Use with caution. - * Have a look at the used connector repository to see all available options. - * *Only if you know what you are doing:* Set - `options.connector.preferUntransformed = true` in order receive the shared - data untransformed. This is very efficient as the database content is simply - copied to this client. This does only work if this client receives content - from only one client. -* options.sourceDir (browser only) - * Path where all y-* modules are stored - * Defaults to `/bower_components` - * Not required when running on `nodejs` / `iojs` - * When using nodejs you need to manually extend Yjs: -``` -var Y = require('yjs') -// you have to require a db, connector, and *all* types you use! -require('y-memory')(Y) -require('y-webrtc')(Y) -require('y-map')(Y) -// .. -``` -* options.share - * Specify on `options.share[arbitraryName]` types that are shared among all - users. - * E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and - create an y-array type on `y.share[arbitraryName]`. - * If userA doesn't specify `options.share[arbitraryName]`, it won't be - available for userA. - * If userB specifies `options.share[arbitraryName]`, it still won't be - available for userA. But all the updates are send from userB to userA. - * In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted. - Instead, they are merged among all users. This feature is only available on - `y.share.*` - * Weird behavior: It is supported that two users specify different types with - the same property name. - E.g. userA specifies `options.share.x = 'Array'`, and userB specifies - `options.share.x = 'Text'`. But they only share data if they specified the - same type with the same property name -* options.type (browser only) - * Array of modules that Yjs needs to require, before instantiating a shared - type. - * By default Yjs requires the specified database adapter, the specified - connector, and all modules that are used in `options.share.*` - * Put all types here that you intend to use, but are not used in y.share.* - -### Instantiated Y object (y) -`Y(options)` returns a promise that is fulfilled when.. - -* All modules are loaded - * The specified database adapter is loaded - * The specified connector is loaded - * All types are included -* The connector is initialized, and a unique user id is set (received from the - server) - * Note: When using y-indexeddb, a retrieved user id is stored on `localStorage` - -The promise returns an instance of Y. We denote it with a lower case `y`. - -* y.share.* - * Instances of the types you specified on options.share.* - * y.share.* can only be defined once when you instantiate Y! -* y.connector is an instance of Y.AbstractConnector -* y.connector.onUserEvent(function (event) {..}) - * Observe user events (event.action is either 'userLeft' or 'userJoined') -* y.connector.whenSynced(listener) - * `listener` is executed when y synced with at least one user. - * `listener` is not called when no other user is in the same room. - * y-websockets-client aways waits to sync with the server -* y.connector.disconnect() - * Force to disconnect this instance from the other instances -* y.connector.connect() - * Try to reconnect to the other instances (needs to be supported by the - connector) - * Not supported by y-xmpp -* y.close() - * Destroy this object. - * Destroys all types (they will throw weird errors if you still use them) - * Disconnects from the other instances (via connector) - * Returns a promise -* y.destroy() - * calls y.close() - * Removes all data from the database - * Returns a promise -* y.db.stopGarbageCollector() - * Stop the garbage collector. Call y.db.garbageCollect() to continue garbage - collection -* y.db.gc :: Boolean - * Whether gc is turned on -* y.db.gcTimeout :: Number (defaults to 50000 ms) - * Time interval between two garbage collect cycles - * It is required that all instances exchanged all messages after two garbage - collect cycles (after 100000 ms per default) -* y.db.userId :: String - * The used user id for this client. **Never overwrite this** - -### Logging -Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag -`y*` enables logging for all y-* components. You can selectively remove -components you are not interested in: E.g. The flag `y*,-y:connector-message` -will not log the long `y:connector-message` messages. - -##### Enable logging in Node.js ```sh -DEBUG=y* node app.js +npm i yjs@13.0.0-97 y-websocket@1.0.0-6 ``` -Remove the colors in order to log to a file: +Start the y-websocket server: + ```sh -DEBUG_COLORS=0 DEBUG=y* node app.js > log +PORT=1234 node ./node_modules/y-websocket/bin/server.js ``` -##### Enable logging in the browser +### Example: Observe types + ```js -localStorage.debug = 'y*' +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" ``` -## License -Yjs is licensed under the [MIT License](./LICENSE). +### 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 +``` + +Now you understand how types are defined on a shared document. Next you can jump +to the [demo repository](https://github.com/yjs/yjs-demos) or continue reading +the API docs. + +## API + +```js +import * as Y from 'yjs' +``` + +### Shared Types + +
+ Y.Array +
+

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

+
const yarray = new Y.Array()
+
+ insert(index:number, content:Array<object|boolean|Array|string|number|Uint8Array|Y.Type>) +
+Insert content at index. Note that content is an array of elements. +I.e. array.insert(0, [1] splices the list and inserts 1 at +position 0. +
+ push(Array<Object|boolean|Array|string|number|Uint8Array|Y.Type>) +
+ delete(index:number, length:number) +
+ get(index:number) +
+ length:number +
+ + +forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type, + index:number, array: Y.Array)) + + +
+ map(function(T, number, YArray):M):Array<M> +
+ toArray():Array<object|boolean|Array|string|number|Uint8Array|Y.Type> +
Copies the content of this YArray to a new Array.
+ toJSON():Array<Object|boolean|Array|string|number> +
+Copies the content of this YArray to a new Array. It transforms all child types +to JSON using their toJSON method. +
+ [Symbol.Iterator] +
+ Returns an YArray Iterator that contains the values for each index in the array. +
for (let value of yarray) { .. }
+
+ observe(function(YArrayEvent, Transaction):void) +
+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. +
+ unobserve(function(YArrayEvent, Transaction):void) +
+ Removes an observe event listener from this type. +
+ observeDeep(function(Array<YEvent>, Transaction):void) +
+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. +
+ unobserveDeep(function(Array<YEvent>, Transaction):void) +
+ Removes an observeDeep event listener from this type. +
+
+
+
+ Y.Map +
+

+ A shareable Map type. +

+
const ymap = new Y.Map()
+
+ get(key:string):object|boolean|string|number|Uint8Array|Y.Type +
+ set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type) +
+ delete(key:string) +
+ has(key:string):boolean +
+ get(index:number) +
+ toJSON():Object<string, Object|boolean|Array|string|number|Uint8Array> +
+Copies the [key,value] pairs of this YMap to a new Object.It +transforms all child types to JSON using their toJSON method. +
+ forEach(function(key:string,value:object|boolean|Array|string|number|Uint8Array|Y.Type)) +
+ Execute the provided function once for every key-value pair. +
+ [Symbol.Iterator] +
+ Returns an Iterator of [key, value] pairs. +
for (let [key, value] of ymap) { .. }
+
+ entries() +
+ Returns an Iterator of [key, value] pairs. +
+ values() +
+ Returns an Iterator of all values. +
+ keys() +
+ Returns an Iterator of all keys. +
+ observe(function(YMapEvent, Transaction):void) +
+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. +
+ unobserve(function(YMapEvent, Transaction):void) +
+ Removes an observe event listener from this type. +
+ observeDeep(function(Array<YEvent>, Transaction):void) +
+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. +
+ unobserveDeep(function(Array<YEvent>, Transaction):void) +
+ Removes an observeDeep event listener from this type. +
+
+
+ +
+ Y.Text +
+

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

+

+This type can also be transformed to the +delta format. Similarly the +YTextEvents compute changes as deltas. +

+
const ytext = new Y.Text()
+
+ insert(index:number, content:string, [formattingAttributes:Object<string,string>]) +
+ Insert a string at index and assign formatting attributes to it. +
ytext.insert(0, 'bold text', { bold: true })
+
+ delete(index:number, length:number) +
+ format(index:number, length:number, formattingAttributes:Object<string,string>) +
Assign formatting attributes to a range in the text
+ applyDelta(delta) +
See Quill Delta
+ length:number +
+ toString():string +
Transforms this type, without formatting options, into a string.
+ toJSON():string +
See toString
+ toDelta():Delta +
+Transforms this type to a Quill Delta +
+ observe(function(YTextEvent, Transaction):void) +
+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. +
+ unobserve(function(YTextEvent, Transaction):void) +
+ Removes an observe event listener from this type. +
+ observeDeep(function(Array<YEvent>, Transaction):void) +
+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. +
+ unobserveDeep(function(Array<YEvent>, Transaction):void) +
+ Removes an observeDeep event listener from this type. +
+
+
+ +
+ YXmlFragment +
+

+ A container that holds an Array of Y.XmlElements. +

+
const yxml = new Y.XmlFragment()
+
+ insert(index:number, content:Array<Y.XmlElement|Y.XmlText>) +
+ delete(index:number, length:number) +
+ get(index:number) +
+ length:number +
+ toArray():Array<Y.XmlElement|Y.XmlText> +
Copies the children to a new Array.
+ toDOM():DocumentFragment +
Transforms this type and all children to new DOM elements.
+ toString():string +
Get the XML serialization of all descendants.
+ toJSON():string +
See toString.
+ observe(function(YXmlEvent, Transaction):void) +
+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. +
+ unobserve(function(YXmlEvent, Transaction):void) +
+ Removes an observe event listener from this type. +
+ observeDeep(function(Array<YEvent>, Transaction):void) +
+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. +
+ unobserveDeep(function(Array<YEvent>, Transaction):void) +
+ Removes an observeDeep event listener from this type. +
+
+
+ +
+ Y.XmlElement +
+

+A shareable type that represents an XML Element. It has a nodeName, +attributes, and a list of children. But it makes no effort to validate its +content and be actually XML compliant. +

+
const yxml = new Y.XmlElement()
+
+ insert(index:number, content:Array<Y.XmlElement|Y.XmlText>) +
+ delete(index:number, length:number) +
+ get(index:number) +
+ length:number +
+ setAttribute(attributeName:string, attributeValue:string) +
+ removeAttribute(attributeName:string) +
+ getAttribute(attributeName:string):string +
+ getAttributes(attributeName:string):Object<string,string> +
+ toArray():Array<Y.XmlElement|Y.XmlText> +
Copies the children to a new Array.
+ toDOM():Element +
Transforms this type and all children to a new DOM element.
+ toString():string +
Get the XML serialization of all descendants.
+ toJSON():string +
See toString.
+ observe(function(YXmlEvent, Transaction):void) +
+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. +
+ unobserve(function(YXmlEvent, Transaction):void) +
+ Removes an observe event listener from this type. +
+ observeDeep(function(Array<YEvent>, Transaction):void) +
+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. +
+ unobserveDeep(function(Array<YEvent>, Transaction):void) +
+ Removes an observeDeep event listener from this type. +
+
+
+ +### Y.Doc + +```js +const doc = new Y.Doc() +``` + +
+ clientID +
A unique id that identifies this client. (readonly)
+ transact(function(Transaction):void [, origin:any]) +
+Every change on the shared document happens in a transaction. Observer calls and +the update event are called after each transaction. You should +bundle changes into a single transaction to reduce the amount of event +calls. I.e. doc.transact(() => { yarray.insert(..); ymap.set(..) }) +triggers a single change event.
You can specify an optional origin +parameter that is stored on transaction.origin and +on('update', (update, origin) => ..). +
+ get(string, Y.[TypeClass]):[Type] +
Define a shared type.
+ getArray(string):Y.Array +
Define a shared Y.Array type. Is equivalent to y.get(string, Y.Array).
+ getMap(string):Y.Map +
Define a shared Y.Map type. Is equivalent to y.get(string, Y.Map).
+ getXmlFragment(string):Y.XmlFragment +
Define a shared Y.XmlFragment type. Is equivalent to y.get(string, Y.XmlFragment).
+ on(string, function) +
Register an event listener on the shared type
+ off(string, function) +
Unregister an event listener from the shared type
+
+ +#### Y.Doc Events + +
+ on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void) +
+Listen to document updates. Document updates must be transmitted to all other +peers. You can apply document updates in any order and multiple times. +
+ on('beforeTransaction', function(Y.Transaction, Y.Doc):void) +
Emitted before each transaction.
+ on('afterTransaction', function(Y.Transaction, Y.Doc):void) +
Emitted after each transaction.
+
+ +### 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) +``` + +
+ Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any]) +
+Apply a document update on the shared document. Optionally you can specify +transactionOrigin that will be stored on +transaction.origin +and ydoc.on('update', (update, origin) => ..). +
+ Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array +
+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. +
+ Y.encodeStateVector(Y.Doc):Uint8Array +
Computes the state vector and encodes it into an Uint8Array.
+
+ +### 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 +``` + +
+ Y.createRelativePositionFromTypeIndex(Uint8Array|Y.Type, number) +
+ Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc) +
+ Y.encodeRelativePosition(RelativePosition):Uint8Array +
+ Y.decodeRelativePosition(Uint8Array):RelativePosition +
+
+ +### Y.UndoManager + +Yjs ships with an Undo/Redo manager for selective undo/redo of of changes on a +Yjs type. The changes can be optionally scoped to transaction origins. + +```js +const ytext = doc.getArray('array') +const undoManager = new Y.UndoManager(ytext) + +ytext.insert(0, 'abc') +undoManager.undo() +ytext.toString() // => '' +undoManager.redo() +ytext.toString() // => 'abc' +``` + +
+ constructor(scope:Y.AbstractType|Array<Y.AbstractType>, + [[{captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}]]) +
Accepts either single type as scope or an array of types.
+ undo() +
+ redo() +
+ stopCapturing() +
+ + +on('stack-item-added', { stackItem: { meta: Map<any,any> }, type: 'undo' +| 'redo' }) + + +
+Register an event that is called when a StackItem is added to the +undo- or the redo-stack. +
+ + +on('stack-item-popped', { stackItem: { meta: Map<any,any> }, type: 'undo' +| 'redo' }) + + +
+Register an event that is called when a StackItem is popped from +the undo- or the redo-stack. +
+
+ +#### Example: Stop Capturing + +UndoManager merges Undo-StackItems if they are created within time-gap +smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next +StackItem won't be merged. + +```js +// without stopCapturing +ytext.insert(0, 'a') +ytext.insert(1, 'b') +um.undo() +ytext.toString() // => '' (note that 'ab' was removed) +// with stopCapturing +ytext.insert(0, 'a') +um.stopCapturing() +ytext.insert(0, 'b') +um.undo() +ytext.toString() // => 'a' (note that only 'b' was removed) +``` + +#### Example: Specify tracked origins + +Every change on the shared document has an origin. If no origin was specified, +it defaults to `null`. By specifying `trackedTransactionOrigins` you can +selectively specify which changes should be tracked by `UndoManager`. The +UndoManager instance is always added to `trackedTransactionOrigins`. + +```js +class CustomBinding {} + +const ytext = doc.getArray('array') +const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding])) + +ytext.insert(0, 'abc') +undoManager.undo() +ytext.toString() // => 'abc' (does not track because origin `null` and not part + // of `trackedTransactionOrigins`) +ytext.delete(0, 3) // revert change + +doc.transact(() => { + ytext.insert(0, 'abc') +}, 42) +undoManager.undo() +ytext.toString() // => '' (tracked because origin is an instance of `trackedTransactionorigins`) + +doc.transact(() => { + ytext.insert(0, 'abc') +}, 41) +undoManager.undo() +ytext.toString() // => '' (not tracked because 41 is not an instance of + // `trackedTransactionorigins`) +ytext.delete(0, 3) // revert change + +doc.transact(() => { + ytext.insert(0, 'abc') +}, new CustomBinding()) +undoManager.undo() +ytext.toString() // => '' (tracked because origin is a `CustomBinding` and + // `CustomBinding` is in `trackedTransactionorigins`) +``` + +#### Example: Add additional information to the StackItems + +When undoing or redoing a previous action, it is often expected to restore +additional meta information like the cursor location or the view on the +document. You can assign meta-information to Undo-/Redo-StackItems. + +```js +const ytext = doc.getArray('array') +const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding])) + +undoManager.on('stack-item-added', event => { + // save the current cursor location on the stack-item + event.stackItem.meta.set('cursor-location', getRelativeCursorLocation()) +}) + +undoManager.on('stack-item-popped', event => { + // restore the current cursor location on the stack-item + restoreCursorLocation(event.stackItem.meta.get('cursor-location')) +}) +``` + +## Miscellaneous + +### Typescript Declarations + +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, + }, + "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. diff --git a/README.v12.md b/README.v12.md new file mode 100644 index 00000000..f750f721 --- /dev/null +++ b/README.v12.md @@ -0,0 +1,305 @@ + +# ![Yjs](https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png) + +Yjs is a framework for offline-first p2p shared editing on structured data like +text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides +most of the complexity of concurrent editing. For additional information, demos, +and tutorials visit [y-js.org](http://y-js.org/). + +:warning: Checkout the [v13 docs](./README.md) for the upcoming release :warning: + +### Extensions +Yjs only knows how to resolve conflicts on shared data. You have to choose a .. + +* *Connector* - a communication protocol that propagates changes to the clients +* *Database* - a database to store your changes +* one or more *Types* - that represent the shared data + +Connectors, Databases, and Types are available as modules that extend Yjs. Here +is a list of the modules we know of: + +##### Connectors + +|Name | Description | +|----------------|-----------------------------------| +|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC| +|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets | +|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))| +|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!| +|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios| + +##### Database adapters + +|Name | Description | +|----------------|-----------------------------------| +|[memory](https://github.com/y-js/y-memory) | In-memory storage. | +|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser | +|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps | + +##### Types + +| Name | Description | +|----------|-------------------| +|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object | +|[array](https://github.com/y-js/y-array) | A shared Array implementation | +|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects | +|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to the [Ace Editor](https://ace.c9.io), [CodeMirror](https://codemirror.net/), [Monaco](https://github.com/Microsoft/monaco-editor), textareas, input elements, and HTML elements (e.g. <*h1*>, or <*p*>) | +|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)| + +##### Other + +| Name | Description | +|-----------|-------------------| +|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element | + +## Use it! +Install Yjs, and its modules with [bower](http://bower.io/), or +[npm](https://www.npmjs.org/package/yjs). + +### Bower + +``` +bower install --save yjs y-array % add all y-* modules you want to use +``` +You only need to include the `y.js` file. Yjs is able to automatically require +missing modules. +``` + +``` + +### CDN + +``` + + + + + + +// .. +// do the same for all modules you want to use +``` + +### Npm + +``` +npm install --save yjs % add all y-* modules you want to use +``` + +If you don't include via script tag, you have to explicitly include all modules! +(Same goes for other module systems) +``` +var Y = require('yjs') +require('y-array')(Y) // add the y-array type to Yjs +require('y-websockets-client')(Y) +require('y-memory')(Y) +require('y-map')(Y) +require('y-text')(Y) +// .. +// do the same for all modules you want to use +``` + +### ES6 Syntax + +``` +import Y from 'yjs' +import yArray from 'y-array' +import yWebsocketsClient from 'y-webrtc' +import yMemory from 'y-memory' +import yMap from 'y-map' +import yText from 'y-text' +// .. +Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */) +``` + +# Text editing example + +Install dependencies +``` +bower i yjs y-memory y-webrtc y-array y-text +``` + +Here is a simple example of a shared textarea +```HTML + + + + + + + + + +``` + +## Get Help & Give Help +There are some friendly people on [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join! + +Report _any_ issues to the +[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very +soon, if possible. + +# API + +### Y(options) +* Y.extend(module1, module2, ..) + * Add extensions to Y + * `Y.extend(require('y-webrtc'))` has the same semantics as + `require('y-webrtc')(Y)` +* options.db + * Will be forwarded to the database adapter. Specify the database adaper on + `options.db.name`. + * Have a look at the used database adapter repository to see all available + options. +* options.connector + * Will be forwarded to the connector adapter. Specify the connector adaper on + `options.connector.name`. + * All our connectors implement a `room` property. Clients that specify the + same room share the same data. + * All of our connectors specify an `url` property that defines the connection + endpoint of the used connector. + * All of our connectors also have a default connection endpoint that you can + use for development. + * Set `options.connector.generateUserId = true` in order to genenerate a + userid, instead of receiving one from the server. This way the `Y(..)` is + immediately going to be resolved, without waiting for any confirmation from + the server. Use with caution. + * Have a look at the used connector repository to see all available options. + * *Only if you know what you are doing:* Set + `options.connector.preferUntransformed = true` in order receive the shared + data untransformed. This is very efficient as the database content is simply + copied to this client. This does only work if this client receives content + from only one client. +* options.sourceDir (browser only) + * Path where all y-* modules are stored + * Defaults to `/bower_components` + * Not required when running on `nodejs` / `iojs` + * When using nodejs you need to manually extend Yjs: +``` +var Y = require('yjs') +// you have to require a db, connector, and *all* types you use! +require('y-memory')(Y) +require('y-webrtc')(Y) +require('y-map')(Y) +// .. +``` +* options.share + * Specify on `options.share[arbitraryName]` types that are shared among all + users. + * E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and + create an y-array type on `y.share[arbitraryName]`. + * If userA doesn't specify `options.share[arbitraryName]`, it won't be + available for userA. + * If userB specifies `options.share[arbitraryName]`, it still won't be + available for userA. But all the updates are send from userB to userA. + * In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted. + Instead, they are merged among all users. This feature is only available on + `y.share.*` + * Weird behavior: It is supported that two users specify different types with + the same property name. + E.g. userA specifies `options.share.x = 'Array'`, and userB specifies + `options.share.x = 'Text'`. But they only share data if they specified the + same type with the same property name +* options.type (browser only) + * Array of modules that Yjs needs to require, before instantiating a shared + type. + * By default Yjs requires the specified database adapter, the specified + connector, and all modules that are used in `options.share.*` + * Put all types here that you intend to use, but are not used in y.share.* + +### Instantiated Y object (y) +`Y(options)` returns a promise that is fulfilled when.. + +* All modules are loaded + * The specified database adapter is loaded + * The specified connector is loaded + * All types are included +* The connector is initialized, and a unique user id is set (received from the + server) + * Note: When using y-indexeddb, a retrieved user id is stored on `localStorage` + +The promise returns an instance of Y. We denote it with a lower case `y`. + +* y.share.* + * Instances of the types you specified on options.share.* + * y.share.* can only be defined once when you instantiate Y! +* y.connector is an instance of Y.AbstractConnector +* y.connector.onUserEvent(function (event) {..}) + * Observe user events (event.action is either 'userLeft' or 'userJoined') +* y.connector.whenSynced(listener) + * `listener` is executed when y synced with at least one user. + * `listener` is not called when no other user is in the same room. + * y-websockets-client aways waits to sync with the server +* y.connector.disconnect() + * Force to disconnect this instance from the other instances +* y.connector.connect() + * Try to reconnect to the other instances (needs to be supported by the + connector) + * Not supported by y-xmpp +* y.close() + * Destroy this object. + * Destroys all types (they will throw weird errors if you still use them) + * Disconnects from the other instances (via connector) + * Returns a promise +* y.destroy() + * calls y.close() + * Removes all data from the database + * Returns a promise +* y.db.stopGarbageCollector() + * Stop the garbage collector. Call y.db.garbageCollect() to continue garbage + collection +* y.db.gc :: Boolean + * Whether gc is turned on +* y.db.gcTimeout :: Number (defaults to 50000 ms) + * Time interval between two garbage collect cycles + * It is required that all instances exchanged all messages after two garbage + collect cycles (after 100000 ms per default) +* y.db.userId :: String + * The used user id for this client. **Never overwrite this** + +### Logging +Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag +`y*` enables logging for all y-* components. You can selectively remove +components you are not interested in: E.g. The flag `y*,-y:connector-message` +will not log the long `y:connector-message` messages. + +##### Enable logging in Node.js +```sh +DEBUG=y* node app.js +``` + +Remove the colors in order to log to a file: +```sh +DEBUG_COLORS=0 DEBUG=y* node app.js > log +``` + +##### Enable logging in the browser +```js +localStorage.debug = 'y*' +``` + +## License +Yjs is licensed under the [MIT License](./LICENSE). diff --git a/README.v13.md b/README.v13.md deleted file mode 100644 index ad5becef..00000000 --- a/README.v13.md +++ /dev/null @@ -1,874 +0,0 @@ - -# ![Yjs](https://yjs.dev/images/logo/yjs-120x120.png) - -> A CRDT framework with a powerful abstraction of shared data - -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](#YDoc) - * [Document Updates](#Document-Updates) - * [Relative Positions](#Relative-Positions) - * [Y.UndoManager](#YUndoManager) -* [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 | Cursors | Binding | Demo | -|---|:-:|---|---| -| [ProseMirror](https://prosemirror.net/)                                                   | ✔ | [y-prosemirror](http://github.com/y-js/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) | -| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/y-js/y-quill) | [demo](https://yjs-demos.now.sh/quill/) | -| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/y-js/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) | -| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/y-js/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) | -| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/y-js/y-ace) | [demo](https://yjs-demos.now.sh/ace/) | -| [Textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) | | [y-textarea](http://github.com/y-js/y-textarea) | [demo](https://yjs-demos.now.sh/textarea/) | -| [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) | | [y-dom](http://github.com/y-js/y-dom) | [demo](https://yjs-demos.now.sh/dom/) | - -### Providers - -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. - -
-
y-websocket
-
-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. -
-
y-mesh
-
-[WIP] Creates a connected graph of webrtc connections with a high -strength. 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. -
-
y-dat
-
-[WIP] Write document updates effinciently to the dat network using -multifeed. 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. -
-
- -## 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('ws://localhost:1234', 'roomname', 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 -``` - -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 - -
- Y.Array -
-

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

-
const yarray = new Y.Array()
-
- insert(index:number, content:Array<object|boolean|Array|string|number|Uint8Array|Y.Type>) -
-Insert content at index. Note that content is an array of elements. -I.e. array.insert(0, [1] splices the list and inserts 1 at -position 0. -
- push(Array<Object|boolean|Array|string|number|Uint8Array|Y.Type>) -
- delete(index:number, length:number) -
- get(index:number) -
- length:number -
- - -forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type, - index:number, array: Y.Array)) - - -
- map(function(T, number, YArray):M):Array<M> -
- toArray():Array<object|boolean|Array|string|number|Uint8Array|Y.Type> -
Copies the content of this YArray to a new Array.
- toJSON():Array<Object|boolean|Array|string|number> -
-Copies the content of this YArray to a new Array. It transforms all child types -to JSON using their toJSON method. -
- [Symbol.Iterator] -
- Returns an YArray Iterator that contains the values for each index in the array. -
for (let value of yarray) { .. }
-
- observe(function(YArrayEvent, Transaction):void) -
-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. -
- unobserve(function(YArrayEvent, Transaction):void) -
- Removes an observe event listener from this type. -
- observeDeep(function(Array<YEvent>, Transaction):void) -
-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. -
- unobserveDeep(function(Array<YEvent>, Transaction):void) -
- Removes an observeDeep event listener from this type. -
-
-
-
- Y.Map -
-

- A shareable Map type. -

-
const ymap = new Y.Map()
-
- get(key:string):object|boolean|string|number|Uint8Array|Y.Type -
- set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type) -
- delete(key:string) -
- has(key:string):boolean -
- get(index:number) -
- toJSON():Object<string, Object|boolean|Array|string|number|Uint8Array> -
-Copies the [key,value] pairs of this YMap to a new Object.It -transforms all child types to JSON using their toJSON method. -
- forEach(function(key:string,value:object|boolean|Array|string|number|Uint8Array|Y.Type)) -
- Execute the provided function once for every key-value pair. -
- [Symbol.Iterator] -
- Returns an Iterator of [key, value] pairs. -
for (let [key, value] of ymap) { .. }
-
- entries() -
- Returns an Iterator of [key, value] pairs. -
- values() -
- Returns an Iterator of all values. -
- keys() -
- Returns an Iterator of all keys. -
- observe(function(YMapEvent, Transaction):void) -
-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. -
- unobserve(function(YMapEvent, Transaction):void) -
- Removes an observe event listener from this type. -
- observeDeep(function(Array<YEvent>, Transaction):void) -
-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. -
- unobserveDeep(function(Array<YEvent>, Transaction):void) -
- Removes an observeDeep event listener from this type. -
-
-
- -
- Y.Text -
-

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

-

-This type can also be transformed to the -delta format. Similarly the -YTextEvents compute changes as deltas. -

-
const ytext = new Y.Text()
-
- insert(index:number, content:string, [formattingAttributes:Object<string,string>]) -
- Insert a string at index and assign formatting attributes to it. -
ytext.insert(0, 'bold text', { bold: true })
-
- delete(index:number, length:number) -
- format(index:number, length:number, formattingAttributes:Object<string,string>) -
Assign formatting attributes to a range in the text
- applyDelta(delta) -
See Quill Delta
- length:number -
- toString():string -
Transforms this type, without formatting options, into a string.
- toJSON():string -
See toString
- toDelta():Delta -
-Transforms this type to a Quill Delta -
- observe(function(YTextEvent, Transaction):void) -
-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. -
- unobserve(function(YTextEvent, Transaction):void) -
- Removes an observe event listener from this type. -
- observeDeep(function(Array<YEvent>, Transaction):void) -
-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. -
- unobserveDeep(function(Array<YEvent>, Transaction):void) -
- Removes an observeDeep event listener from this type. -
-
-
- -
- YXmlFragment -
-

- A container that holds an Array of Y.XmlElements. -

-
const yxml = new Y.XmlFragment()
-
- insert(index:number, content:Array<Y.XmlElement|Y.XmlText>) -
- delete(index:number, length:number) -
- get(index:number) -
- length:number -
- toArray():Array<Y.XmlElement|Y.XmlText> -
Copies the children to a new Array.
- toDOM():DocumentFragment -
Transforms this type and all children to new DOM elements.
- toString():string -
Get the XML serialization of all descendants.
- toJSON():string -
See toString.
- observe(function(YXmlEvent, Transaction):void) -
-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. -
- unobserve(function(YXmlEvent, Transaction):void) -
- Removes an observe event listener from this type. -
- observeDeep(function(Array<YEvent>, Transaction):void) -
-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. -
- unobserveDeep(function(Array<YEvent>, Transaction):void) -
- Removes an observeDeep event listener from this type. -
-
-
- -
- Y.XmlElement -
-

-A shareable type that represents an XML Element. It has a nodeName, -attributes, and a list of children. But it makes no effort to validate its -content and be actually XML compliant. -

-
const yxml = new Y.XmlElement()
-
- insert(index:number, content:Array<Y.XmlElement|Y.XmlText>) -
- delete(index:number, length:number) -
- get(index:number) -
- length:number -
- setAttribute(attributeName:string, attributeValue:string) -
- removeAttribute(attributeName:string) -
- getAttribute(attributeName:string):string -
- getAttributes(attributeName:string):Object<string,string> -
- toArray():Array<Y.XmlElement|Y.XmlText> -
Copies the children to a new Array.
- toDOM():Element -
Transforms this type and all children to a new DOM element.
- toString():string -
Get the XML serialization of all descendants.
- toJSON():string -
See toString.
- observe(function(YXmlEvent, Transaction):void) -
-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. -
- unobserve(function(YXmlEvent, Transaction):void) -
- Removes an observe event listener from this type. -
- observeDeep(function(Array<YEvent>, Transaction):void) -
-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. -
- unobserveDeep(function(Array<YEvent>, Transaction):void) -
- Removes an observeDeep event listener from this type. -
-
-
- -### Y.Doc - -```js -const doc = new Y.Doc() -``` - -
- clientID -
A unique id that identifies this client. (readonly)
- transact(function(Transaction):void [, origin:any]) -
-Every change on the shared document happens in a transaction. Observer calls and -the update event are called after each transaction. You should -bundle changes into a single transaction to reduce the amount of event -calls. I.e. doc.transact(() => { yarray.insert(..); ymap.set(..) }) -triggers a single change event.
You can specify an optional origin -parameter that is stored on transaction.origin and -on('update', (update, origin) => ..). -
- get(string, Y.[TypeClass]):[Type] -
Define a shared type.
- getArray(string):Y.Array -
Define a shared Y.Array type. Is equivalent to y.get(string, Y.Array).
- getMap(string):Y.Map -
Define a shared Y.Map type. Is equivalent to y.get(string, Y.Map).
- getXmlFragment(string):Y.XmlFragment -
Define a shared Y.XmlFragment type. Is equivalent to y.get(string, Y.XmlFragment).
- on(string, function) -
Register an event listener on the shared type
- off(string, function) -
Unregister an event listener from the shared type
-
- -#### Y.Doc Events - -
- on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void) -
-Listen to document updates. Document updates must be transmitted to all other -peers. You can apply document updates in any order and multiple times. -
- on('beforeTransaction', function(Y.Transaction, Y.Doc):void) -
Emitted before each transaction.
- on('afterTransaction', function(Y.Transaction, Y.Doc):void) -
Emitted after each transaction.
-
- -### 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) -``` - -
- Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any]) -
-Apply a document update on the shared document. Optionally you can specify -transactionOrigin that will be stored on -transaction.origin -and ydoc.on('update', (update, origin) => ..). -
- Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array -
-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. -
- Y.encodeStateVector(Y.Doc):Uint8Array -
Computes the state vector and encodes it into an Uint8Array.
-
- -### 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 -``` - -
- Y.createRelativePositionFromTypeIndex(Uint8Array|Y.Type, number) -
- Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc) -
- Y.encodeRelativePosition(RelativePosition):Uint8Array -
- Y.decodeRelativePosition(Uint8Array):RelativePosition -
-
- -### Y.UndoManager - -Yjs ships with an Undo/Redo manager for selective undo/redo of of changes on a -Yjs type. The changes can be optionally scoped to transaction origins. - -```js -const ytext = doc.getArray('array') -const undoManager = new Y.UndoManager(ytext) - -ytext.insert(0, 'abc') -undoManager.undo() -ytext.toString() // => '' -undoManager.redo() -ytext.toString() // => 'abc' -``` - -
- constructor(scope:Y.AbstractType|Array<Y.AbstractType>, - [[{captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}]]) -
Accepts either single type as scope or an array of types.
- undo() -
- redo() -
- stopCapturing() -
- - -on('stack-item-added', { stackItem: { meta: Map<any,any> }, type: 'undo' -| 'redo' }) - - -
-Register an event that is called when a StackItem is added to the -undo- or the redo-stack. -
- - -on('stack-item-popped', { stackItem: { meta: Map<any,any> }, type: 'undo' -| 'redo' }) - - -
-Register an event that is called when a StackItem is popped from -the undo- or the redo-stack. -
-
- -#### Example: Stop Capturing - -UndoManager merges Undo-StackItems if they are created within time-gap -smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next -StackItem won't be merged. - -```js -// without stopCapturing -ytext.insert(0, 'a') -ytext.insert(1, 'b') -um.undo() -ytext.toString() // => '' (note that 'ab' was removed) -// with stopCapturing -ytext.insert(0, 'a') -um.stopCapturing() -ytext.insert(0, 'b') -um.undo() -ytext.toString() // => 'a' (note that only 'b' was removed) -``` - -#### Example: Specify tracked origins - -Every change on the shared document has an origin. If no origin was specified, -it defaults to `null`. By specifying `trackedTransactionOrigins` you can -selectively specify which changes should be tracked by `UndoManager`. The -UndoManager instance is always added to `trackedTransactionOrigins`. - -```js -class CustomBinding {} - -const ytext = doc.getArray('array') -const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding])) - -ytext.insert(0, 'abc') -undoManager.undo() -ytext.toString() // => 'abc' (does not track because origin `null` and not part - // of `trackedTransactionOrigins`) -ytext.delete(0, 3) // revert change - -doc.transact(() => { - ytext.insert(0, 'abc') -}, 42) -undoManager.undo() -ytext.toString() // => '' (tracked because origin is an instance of `trackedTransactionorigins`) - -doc.transact(() => { - ytext.insert(0, 'abc') -}, 41) -undoManager.undo() -ytext.toString() // => '' (not tracked because 41 is not an instance of - // `trackedTransactionorigins`) -ytext.delete(0, 3) // revert change - -doc.transact(() => { - ytext.insert(0, 'abc') -}, new CustomBinding()) -undoManager.undo() -ytext.toString() // => '' (tracked because origin is a `CustomBinding` and - // `CustomBinding` is in `trackedTransactionorigins`) -``` - -#### Example: Add additional information to the StackItems - -When undoing or redoing a previous action, it is often expected to restore -additional meta information like the cursor location or the view on the -document. You can assign meta-information to Undo-/Redo-StackItems. - -```js -const ytext = doc.getArray('array') -const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding])) - -undoManager.on('stack-item-added', event => { - // save the current cursor location on the stack-item - event.stackItem.meta.set('cursor-location', getRelativeCursorLocation()) -}) - -undoManager.on('stack-item-popped', event => { - // restore the current cursor location on the stack-item - restoreCursorLocation(event.stackItem.meta.get('cursor-location')) -}) -``` - -## Miscellaneous - -### Typescript Declarations - -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, - }, - "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. diff --git a/package.json b/package.json index 0f318536..941d2f9a 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000", "dist": "rm -rf dist && rollup -c", "watch": "rollup -wc", - "lint": "markdownlint README.v13.md && standard && tsc", - "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true", + "lint": "markdownlint README.md && standard && tsc", + "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true", "serve-docs": "npm run docs && serve ./docs/", "preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.js --repitition-time 1000", "postversion": "git push && git push --tags", @@ -38,20 +38,20 @@ }, "repository": { "type": "git", - "url": "https://github.com/y-js/yjs.git" + "url": "https://github.com/yjs/yjs.git" }, "keywords": [ "crdt" ], "author": "Kevin Jahns", - "email": "kevin.jahns@rwth-aachen.de", + "email": "kevin.jahns@protonmail.com", "license": "MIT", "bugs": { - "url": "https://github.com/y-js/yjs/issues" + "url": "https://github.com/yjs/yjs/issues" }, - "homepage": "http://y-js.org", + "homepage": "https://yjs.dev", "dependencies": { - "lib0": "0.0.6" + "lib0": "^0.1.0" }, "devDependencies": { "concurrently": "^3.6.1", diff --git a/tests/y-array.tests.js b/tests/y-array.tests.js index 953e862f..36804780 100644 --- a/tests/y-array.tests.js +++ b/tests/y-array.tests.js @@ -238,7 +238,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => { } /** - * This issue has been reported here https://github.com/y-js/yjs/issues/155 + * This issue has been reported here https://github.com/yjs/yjs/issues/155 * @param {t.TestCase} tc */ export const testNewChildDoesNotEmitEventInTransaction = tc => {