Compare commits

..

9 Commits

Author SHA1 Message Date
Kevin Jahns
7744993dde Deploy 12.1.0 2016-10-29 21:45:33 +02:00
Kevin Jahns
cf3969dff6 fix richtext example 2016-10-13 17:28:22 +02:00
Kevin Jahns
b8dd7d1862 Deploy 12.0.3 2016-10-07 21:00:42 +02:00
Kevin Jahns
9e9f238b12 Deploy 12.0.2 2016-10-05 00:59:56 +02:00
Kevin Jahns
723fc77627 Deploy 12.0.1 2016-09-27 16:17:27 +02:00
Kevin Jahns
9eac8d9398 Deploy 12.0.0 2016-09-20 19:19:14 +02:00
Kevin Jahns
0b14e90585 update examples 2016-09-20 18:29:19 +02:00
Kevin Jahns
4726c71dd0 update quill example 2016-08-29 17:47:38 +02:00
Kevin Jahns
59d859b38b updated quill example 2016-08-22 12:57:04 +02:00
11 changed files with 409 additions and 293 deletions

View File

@@ -43,9 +43,7 @@ Y({
// call drawLine every time an array is appended // call drawLine every time an array is appended
y.share.drawing.observe(function (event) { y.share.drawing.observe(function (event) {
if (event.type === 'insert') { if (event.type === 'insert') {
event.values().then(function (values) { event.values.forEach(drawLine)
values.forEach(drawLine)
})
} else { } else {
// just remove all elements (thats what we do anyway) // just remove all elements (thats what we do anyway)
svg.selectAll('path').remove() svg.selectAll('path').remove()
@@ -53,7 +51,7 @@ Y({
}) })
// draw all existing content // draw all existing content
for (var i = 0; i < drawing.length; i++) { for (var i = 0; i < drawing.length; i++) {
drawing.get(i).then(drawLine) drawLine(drawing.get(i))
} }
// clear canvas on request // clear canvas on request
@@ -64,9 +62,7 @@ Y({
var sharedLine = null var sharedLine = null
function dragstart () { function dragstart () {
drawing.insert(drawing.length, [Y.Array]) drawing.insert(drawing.length, [Y.Array])
drawing.get(drawing.length - 1).then(function (array) { sharedLine = drawing.get(drawing.length - 1)
sharedLine = array
})
} }
// After one dragged event is recognized, we ignore them for 33ms. // After one dragged event is recognized, we ignore them for 33ms.

View File

@@ -1,7 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /> <!-- 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/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"> <link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
<style> <style>
@@ -17,10 +19,12 @@
</div> </div>
</div> </div>
<!-- Include the Quill library -->
<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/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="//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/quill/dist/quill.js"></script>
-->
<script src="../bower_components/yjs/y.es6"></script> <script src="../bower_components/yjs/y.es6"></script>
<script src="./index.js"></script> <script src="./index.js"></script>
</body> </body>

View File

@@ -7,8 +7,8 @@ Y({
name: 'memory' name: 'memory'
}, },
connector: { connector: {
name: 'webrtc', name: 'websockets-client',
room: 'richtext-example-quill-beta' room: 'richtext-example-quill-1.0-test'
}, },
sourceDir: '/bower_components', sourceDir: '/bower_components',
share: { share: {
@@ -36,5 +36,4 @@ Y({
}); });
// bind quill to richtext type // bind quill to richtext type
y.share.richtext.bind(window.quill) y.share.richtext.bind(window.quill)
}) })

View File

@@ -16,8 +16,6 @@ Y({
}).then(function (y) { }).then(function (y) {
window.yXml = y window.yXml = y
// bind xml type to a dom, and put it in body // bind xml type to a dom, and put it in body
y.share.xml.getDom().then(function (dom) { window.sharedDom = y.share.xml.getDom()
window.sharedDom = dom document.body.appendChild(window.sharedDom)
document.body.appendChild(dom)
})
}) })

View File

@@ -19,7 +19,7 @@
"y-text": "latest", "y-text": "latest",
"y-indexeddb": "latest", "y-indexeddb": "latest",
"y-xml": "latest", "y-xml": "latest",
"quill": "~0.20.1", "quill": "^1.0.0-rc.2",
"ace": "~1.2.3", "ace": "~1.2.3",
"ace-builds": "~1.2.3", "ace-builds": "~1.2.3",
"jquery": "~2.2.2", "jquery": "~2.2.2",

210
README.md
View File

@@ -1,100 +1,142 @@
# ![Yjs](http://y-js.org/images/yjs.png) # ![Yjs](http://y-js.org/images/yjs.png)
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on shared data. Yjs is a framework for offline-first p2p shared editing on structured data like text, richtext, json, or XML.
The framework provides similar functionality as [ShareJs] and [OpenCoweb], but supports peer-to-peer It is fairly easy to get started, as Yjs hides most of the complexity of concurrent editing.
communication protocols by default. Yjs was designed to handle concurrent actions on arbitrary data For additional information, demos, and tutorials visit [y-js.org](http://y-js.org/).
like Text, Json, and XML. We also provide support for storing and manipulating your shared data offline.
For more information and demo applications visit our [homepage](http://y-js.org/).
You can create you own shared types easily. ### Extensions
Therefore, you can design the structure of your custom type, Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
and ensure data validity, while Yjs ensures data consistency (everyone will eventually end up with the same data). * *Connector* - a communication protocol that propagates changes to the clients
We already provide abstract data types for * *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))|
|[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 | | Name | Description |
|----------|-------------------| |----------|-------------------|
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object | |[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 | |[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 | |[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 textareas, input elements, or HTML elements (e.g. <*h1*>, or <*p*>). Also supports the [Ace Editor](https://ace.c9.io) | |[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to the [Ace Editor](https://ace.c9.io), 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/)| |[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/)|
Yjs supports P2P message propagation, and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios. ## Use it!
Install Yjs, and its modules with [bower](http://bower.io/), or [npm](https://www.npmjs.org/package/yjs).
We support several communication protocols as so called *Connectors*.
You can create your own connector too - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors).
Currently, we support the following communication protocols:
|Name | Description |
|----------------|-----------------------------------|
|[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))|
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|[websockets](https://github.com/y-js/y-websockets-client) | Exchange updates efficiently in the classical client-server model |
|[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|
You are not limited to use a specific database to store the shared data. We provide the following 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 |
The advantages over similar frameworks are support for
* .. P2P message propagation and arbitrary communication protocols
* .. share any type of data. The types provide a convenient interface
* .. offline support: Changes are stored persistently and only relevant changes are propagated on rejoin
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline. Every type has a notion on how we define Intention Preservation on it.
## Use it!
Install yjs and its modules with [bower](http://bower.io/), or with [npm](https://www.npmjs.org/package/yjs).
### Bower ### Bower
``` ```
bower install yjs --save bower install --save yjs y-array % add all y-* modules you want to use
``` ```
Then you include the libraries directly from the installation folder. You only need to include the `y.js` file. Yjs is able to automatically require missing modules.
``` ```
<script src="./bower_components/yjs/y.js"></script> <script src="./bower_components/yjs/y.js"></script>
``` ```
### Npm ### Npm
``` ```
npm install yjs --save npm install --save yjs % add all y-* modules you want to use
``` ```
And use it like this with *npm*: If you don't include via script tag, you have to explicitly include all modules! (Same goes for other module systems)
``` ```
Y = require("yjs"); 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-array')(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 yArray from 'y-array'
import yMap from 'y-map'
import yText from 'y-text'
// ..
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
``` ```
# Text editing example # Text editing example
Install dependencies
``` ```
Y({ bower i yjs y-memory y-webrtc y-array y-text
db: {
name: 'memory' // store in memory.
// name: 'indexeddb'
},
connector: {
name: 'websockets-client', // choose the websockets connector
// name: 'webrtc'
// name: 'xmpp'
room: 'Textarea-example-dev'
},
sourceDir: '/bower_components', // location of the y-* modules
share: {
textarea: 'Text' // y.share.textarea is of type Y.Text
}
// types: ['Richtext', 'Array'] // optional list of types you want to import
}).then(function (y) {
// bind the textarea to a shared text element
y.share.textarea.bind(document.getElementById('textfield'))
}
``` ```
# Api Here is a simple example of a shared textarea
```HTML
<!DOCTYPE html>
<html>
<body>
<script src="./bower_components/yjs/y.js"></script>
<!-- Yjs automatically includes all missing dependencies (browser only) -->
<script>
Y({
db: {
name: 'memory' // use memory database adapter.
// name: 'indexeddb' // use indexeddb database adapter instead for offline apps
},
connector: {
name: 'webrtc', // use webrtc connector
// name: 'websockets-client'
// name: 'xmpp'
room: 'my-room' // clients connecting to the same room share data
},
sourceDir: '/bower_components', // location of the y-* modules (browser only)
share: {
textarea: 'Text' // y.share.textarea is of type y-text
}
}).then(function (y) {
// The Yjs instance `y` is available
// y.share.* contains the shared types
// Bind `y.share.textarea` to `<textarea/>`
y.share.textarea.bind(document.querySelector('textarea'))
})
</script>
<textarea></textarea>
</body>
</html>
```
## Get Help & Give Help
There are some friendly people on [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join!
Report _any_ issues to the [Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very soon, if possible.
# API
### Y(options) ### Y(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 * options.db
* Will be forwarded to the database adapter. Specify the database adaper on `options.db.name`. * 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. * Have a look at the used database adapter repository to see all available options.
@@ -104,14 +146,14 @@ Y({
* All of our connectors specify an `url` property that defines the connection endpoint of the used connector. * 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. * All of our connectors also have a default connection endpoint that you can use for development.
* Have a look at the used connector repository to see all available options. * Have a look at the used connector repository to see all available options.
* options.sourceDir * options.sourceDir (browser only)
* Path where all y-* modules are stored. * Path where all y-* modules are stored
* Defaults to `/bower_components` * Defaults to `/bower_components`
* Not required when running on `nodejs` / `iojs` * Not required when running on `nodejs` / `iojs`
* When using browserify you can specify all used modules like this: * When using nodejs you need to manually extend Yjs:
``` ```
var Y = require('yjs') var Y = require('yjs')
// you need to require the db, connector, and *all* types you use! // you have to require a db, connector, and *all* types you use!
require('y-memory')(Y) require('y-memory')(Y)
require('y-webrtc')(Y) require('y-webrtc')(Y)
require('y-map')(Y) require('y-map')(Y)
@@ -119,13 +161,13 @@ require('y-map')(Y)
``` ```
* options.share * options.share
* Specify on `options.share[arbitraryName]` types that are shared among all users. * 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]`. * 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 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. * 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.*` * 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. * 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'll only share data if they specified the same type 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 * options.type (browser only)
* Array of modules that Yjs needs to require, before instantiating a shared type. * 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.*` * 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.* * Put all types here that you intend to use, but are not used in y.share.*
@@ -170,18 +212,19 @@ The promise returns an instance of Y. We denote it with a lower case `y`.
* y.db.userId :: String * y.db.userId :: String
* The used user id for this client. **Never overwrite this** * The used user id for this client. **Never overwrite this**
## Get 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 may help you with your problem, and answer your questions.
Please report _any_ issues to the [Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very soon, if possible.
If you want to see an issue fixed, please subscribe to the thread (or remind me via gitter).
## Changelog ## Changelog
### 12.0.0
* **Types are synchronous and never return a promise (except explicitly stated)**
* `y.share.map.get('map type') // => returns a y-map instead of a promise`
* The event property `oldValues`, and `values` contain a list of values (without wrapper)
* Support for the [y-leveldb](https://github.com/y-js/y-leveldb) database adapter
* [y-richtext](https://github.com/y-js/y-richtext) supports Quill@1.0.0-rc.2
* Only the types are affected by this release. You have to upgrade y-array@10.0.0, y-map@10.0.0, y-richtext@9.0.0, and y-xml@10.0.0
### 11.0.0 ### 11.0.0
* **All types now return a single event instead of list of events** * **All types return a single event instead of list of events**
* Insert events contain a list of values * Insert events contain a list of values
* Improved performance for large insertions & deletions * Improved performance for large insertions & deletions
* Several bugfixes (offline editing related) * Several bugfixes (offline editing related)
@@ -195,7 +238,7 @@ If you want to see an issue fixed, please subscribe to the thread (or remind me
### 9.0.0 ### 9.0.0
There were several rolling updates from 0.6 to 0.8. We consider Yjs stable since a long time, There were several rolling updates from 0.6 to 0.8. We consider Yjs stable since a long time,
and intend to continue stable releases. From this release forward y-* modules will implement peer-dependencies for npm, and dependencies for bower. and intend to continue stable releases. From this release forward y-* modules will implement peer-dependencies for npm, and dependencies for bower.
Furthermore, incompatible yjs instances will now throw errors when syncing - this feature was influenced by #48. The versioning jump was influenced by react (see [here](https://facebook.github.io/react/blog/2016/02/19/new-versioning-scheme.html)) Furthermore, incompatible yjs instances throw errors now when syncing - this feature was influenced by #48. The versioning jump was influenced by react (see [here](https://facebook.github.io/react/blog/2016/02/19/new-versioning-scheme.html))
### 0.6.0 ### 0.6.0
@@ -215,10 +258,7 @@ This is a complete rewrite of the 0.5 version of Yjs. Since Yjs 0.6.0 it is poss
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. 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 ## License
Yjs is licensed under the [MIT License](./LICENSE.txt). Yjs is licensed under the [MIT License](./LICENSE).
<yjs@dbis.rwth-aachen.de> <yjs@dbis.rwth-aachen.de>
[ShareJs]: https://github.com/share/ShareJS
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "11.2.6", "version": "12.1.0",
"homepage": "y-js.org", "homepage": "y-js.org",
"authors": [ "authors": [
"Kevin Jahns <kevin.jahns@rwth-aachen.de>" "Kevin Jahns <kevin.jahns@rwth-aachen.de>"

447
y.es6
View File

@@ -2,6 +2,9 @@
/* @flow */ /* @flow */
'use strict' 'use strict'
function canRead (auth) { return auth === 'read' || auth === 'write' }
function canWrite (auth) { return auth === 'write' }
module.exports = function (Y/* :any */) { module.exports = function (Y/* :any */) {
class AbstractConnector { class AbstractConnector {
/* :: /* ::
@@ -55,6 +58,8 @@ module.exports = function (Y/* :any */) {
this.syncStep2 = Promise.resolve() this.syncStep2 = Promise.resolve()
this.broadcastOpBuffer = [] this.broadcastOpBuffer = []
this.protocolVersion = 11 this.protocolVersion = 11
this.authInfo = opts.auth || null
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
} }
reconnect () { reconnect () {
} }
@@ -88,6 +93,9 @@ module.exports = function (Y/* :any */) {
onUserEvent (f) { onUserEvent (f) {
this.userEventListeners.push(f) this.userEventListeners.push(f)
} }
removeUserEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(g => { f !== g })
}
userLeft (user) { userLeft (user) {
if (this.connections[user] != null) { if (this.connections[user] != null) {
delete this.connections[user] delete this.connections[user]
@@ -164,7 +172,8 @@ module.exports = function (Y/* :any */) {
type: 'sync step 1', type: 'sync step 1',
stateSet: stateSet, stateSet: stateSet,
deleteSet: deleteSet, deleteSet: deleteSet,
protocolVersion: conn.protocolVersion protocolVersion: conn.protocolVersion,
auth: conn.authInfo
}) })
}) })
} else { } else {
@@ -218,7 +227,7 @@ module.exports = function (Y/* :any */) {
*/ */
receiveMessage (sender/* :UserId */, message/* :Message */) { receiveMessage (sender/* :UserId */, message/* :Message */) {
if (sender === this.userId) { if (sender === this.userId) {
return return Promise.resolve()
} }
if (this.debug) { if (this.debug) {
console.log(`receive ${sender} -> ${this.userId}: ${message.type}`, JSON.parse(JSON.stringify(message))) // eslint-disable-line console.log(`receive ${sender} -> ${this.userId}: ${message.type}`, JSON.parse(JSON.stringify(message))) // eslint-disable-line
@@ -233,91 +242,118 @@ module.exports = function (Y/* :any */) {
type: 'sync stop', type: 'sync stop',
protocolVersion: this.protocolVersion protocolVersion: this.protocolVersion
}) })
return return Promise.reject('Incompatible protocol version')
} }
if (message.type === 'sync step 1') { if (message.auth != null && this.connections[sender] != null) {
let conn = this // authenticate using auth in message
let m = message var auth = this.checkAuth(message.auth, this.y)
this.y.db.requestTransaction(function *() { this.connections[sender].auth = auth
var currentStateSet = yield* this.getStateSet() auth.then(auth => {
yield* this.applyDeleteSet(m.deleteSet) for (var f of this.userEventListeners) {
f({
var ds = yield* this.getDeleteSet() action: 'userAuthenticated',
var ops = yield* this.getOperations(m.stateSet) user: sender,
conn.send(sender, { auth: auth
type: 'sync step 2',
os: ops,
stateSet: currentStateSet,
deleteSet: ds,
protocolVersion: this.protocolVersion
})
if (this.forwardToSyncingClients) {
conn.syncingClients.push(sender)
setTimeout(function () {
conn.syncingClients = conn.syncingClients.filter(function (cli) {
return cli !== sender
})
conn.send(sender, {
type: 'sync done'
})
}, 5000) // TODO: conn.syncingClientDuration)
} else {
conn.send(sender, {
type: 'sync done'
}) })
} }
conn._setSyncedWith(sender)
}) })
} else if (message.type === 'sync step 2') { } else if (this.connections[sender] != null && this.connections[sender].auth == null) {
let conn = this // authenticate without otherwise
var broadcastHB = !this.broadcastedHB this.connections[sender].auth = this.checkAuth(null, this.y)
this.broadcastedHB = true }
var db = this.y.db if (this.connections[sender] != null && this.connections[sender].auth != null) {
var defer = {} return this.connections[sender].auth.then((auth) => {
defer.promise = new Promise(function (resolve) { if (message.type === 'sync step 1' && canRead(auth)) {
defer.resolve = resolve let conn = this
}) let m = message
this.syncStep2 = defer.promise
let m /* :MessageSyncStep2 */ = message this.y.db.requestTransaction(function *() {
db.requestTransaction(function * () { var currentStateSet = yield* this.getStateSet()
yield* this.applyDeleteSet(m.deleteSet) if (canWrite(auth)) {
this.store.apply(m.os) yield* this.applyDeleteSet(m.deleteSet)
db.requestTransaction(function * () { }
var ops = yield* this.getOperations(m.stateSet)
if (ops.length > 0) { var ds = yield* this.getDeleteSet()
if (!broadcastHB) { // TODO: consider to broadcast here.. var ops = yield* this.getOperations(m.stateSet)
conn.send(sender, { conn.send(sender, {
type: 'update', type: 'sync step 2',
ops: ops os: ops,
}) stateSet: currentStateSet,
deleteSet: ds,
protocolVersion: this.protocolVersion,
auth: this.authInfo
})
if (this.forwardToSyncingClients) {
conn.syncingClients.push(sender)
setTimeout(function () {
conn.syncingClients = conn.syncingClients.filter(function (cli) {
return cli !== sender
})
conn.send(sender, {
type: 'sync done'
})
}, 5000) // TODO: conn.syncingClientDuration)
} else { } else {
// broadcast only once! conn.send(sender, {
conn.broadcastOps(ops) type: 'sync done'
})
}
conn._setSyncedWith(sender)
})
} else if (message.type === 'sync step 2' && canWrite(auth)) {
let conn = this
var broadcastHB = !this.broadcastedHB
this.broadcastedHB = true
var db = this.y.db
var defer = {}
defer.promise = new Promise(function (resolve) {
defer.resolve = resolve
})
this.syncStep2 = defer.promise
let m /* :MessageSyncStep2 */ = message
db.requestTransaction(function * () {
yield* this.applyDeleteSet(m.deleteSet)
this.store.apply(m.os)
db.requestTransaction(function * () {
var ops = yield* this.getOperations(m.stateSet)
if (ops.length > 0) {
if (!broadcastHB) { // TODO: consider to broadcast here..
conn.send(sender, {
type: 'update',
ops: ops
})
} else {
// broadcast only once!
conn.broadcastOps(ops)
}
}
defer.resolve()
})
})
} else if (message.type === 'sync done') {
var self = this
this.syncStep2.then(function () {
self._setSyncedWith(sender)
})
} else if (message.type === 'update' && canWrite(auth)) {
if (this.forwardToSyncingClients) {
for (var client of this.syncingClients) {
this.send(client, message)
} }
} }
defer.resolve() if (this.y.db.forwardAppliedOperations) {
}) var delops = message.ops.filter(function (o) {
}) return o.struct === 'Delete'
} else if (message.type === 'sync done') { })
var self = this if (delops.length > 0) {
this.syncStep2.then(function () { this.broadcastOps(delops)
self._setSyncedWith(sender) }
}) }
} else if (message.type === 'update') { this.y.db.apply(message.ops)
if (this.forwardToSyncingClients) {
for (var client of this.syncingClients) {
this.send(client, message)
} }
} })
if (this.y.db.forwardAppliedOperations) { } else {
var delops = message.ops.filter(function (o) { return Promise.reject('Unable to deliver message')
return o.struct === 'Delete'
})
if (delops.length > 0) {
this.broadcastOps(delops)
}
}
this.y.db.apply(message.ops)
} }
} }
_setSyncedWith (user) { _setSyncedWith (user) {
@@ -451,11 +487,21 @@ module.exports = function (Y) {
} }
}, },
whenTransactionsFinished: function () { whenTransactionsFinished: function () {
var ps = [] var self = this
for (var name in this.users) { return new Promise(function (resolve, reject) {
ps.push(this.users[name].y.db.whenTransactionsFinished()) // The connector first has to send the messages to the db.
} // Wait for the checkAuth-function to resolve
return Promise.all(ps) // The test lib only has a simple checkAuth function: `() => Promise.resolve()`
// Just add a function to the event-queue, in order to wait for the event.
// TODO: this may be buggy in test applications (but it isn't be for real-life apps)
setTimeout(function () {
var ps = []
for (var name in self.users) {
ps.push(self.users[name].y.db.whenTransactionsFinished())
}
Promise.all(ps).then(resolve, reject)
}, 0)
})
}, },
flushOne: function flushOne () { flushOne: function flushOne () {
var bufs = [] var bufs = []
@@ -481,8 +527,9 @@ module.exports = function (Y) {
delete buff[sender] delete buff[sender]
} }
var user = globalRoom.users[userId] var user = globalRoom.users[userId]
user.receiveMessage(m[0], m[1]) return user.receiveMessage(m[0], m[1]).then(function () {
return user.y.db.whenTransactionsFinished() return user.y.db.whenTransactionsFinished()
}, function () {})
} else { } else {
return false return false
} }
@@ -499,16 +546,14 @@ module.exports = function (Y) {
} }
globalRoom.whenTransactionsFinished().then(nextFlush) globalRoom.whenTransactionsFinished().then(nextFlush)
} else { } else {
setTimeout(function () { c = globalRoom.flushOne()
var c = globalRoom.flushOne() if (c) {
if (c) { c.then(function () {
c.then(function () { globalRoom.whenTransactionsFinished().then(nextFlush)
globalRoom.whenTransactionsFinished().then(nextFlush) })
}) } else {
} else { resolve()
resolve() }
}
}, 0)
} }
} }
globalRoom.whenTransactionsFinished().then(nextFlush) globalRoom.whenTransactionsFinished().then(nextFlush)
@@ -534,7 +579,7 @@ module.exports = function (Y) {
this.syncingClientDuration = 0 this.syncingClientDuration = 0
} }
receiveMessage (sender, m) { receiveMessage (sender, m) {
super.receiveMessage(sender, JSON.parse(JSON.stringify(m))) return super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
} }
send (userId, message) { send (userId, message) {
var buffer = globalRoom.buffers[userId] var buffer = globalRoom.buffers[userId]
@@ -581,7 +626,7 @@ module.exports = function (Y) {
if (buff[sender].length === 0) { if (buff[sender].length === 0) {
delete buff[sender] delete buff[sender]
} }
this.receiveMessage(m[0], m[1]) yield this.receiveMessage(m[0], m[1])
} }
yield self.whenTransactionsFinished() yield self.whenTransactionsFinished()
}) })
@@ -1137,6 +1182,48 @@ module.exports = function (Y /* :any */) {
}, 0) }, 0)
} }
} }
/*
Get a created/initialized type.
*/
getType (id) {
return this.initializedTypes[JSON.stringify(id)]
}
/*
Init type. This is called when a remote operation is retrieved, and transformed to a type
TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
*/
* initType (id, args) {
var sid = JSON.stringify(id)
var t = this.store.initializedTypes[sid]
if (t == null) {
var op/* :MapStruct | ListStruct */ = yield* this.getOperation(id)
if (op != null) {
t = yield* Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
this.store.initializedTypes[sid] = t
}
}
return t
}
/*
Create type. This is called when the local user creates a type (which is a synchronous action)
*/
createType (typedefinition, id) {
var structname = typedefinition[0].struct
id = id || this.getNextOpId(1)
var op = Y.Struct[structname].create(id)
op.type = typedefinition[0].name
this.requestTransaction(function * () {
if (op.id[0] === '_') {
yield* this.setOperation(op)
} else {
yield* this.applyCreatedOperations([op])
}
})
var t = Y[op.type].typeDefinition.createType(this, op, typedefinition[1])
this.initializedTypes[JSON.stringify(op.id)] = t
return t
}
} }
Y.AbstractDatabase = AbstractDatabase Y.AbstractDatabase = AbstractDatabase
} }
@@ -1638,57 +1725,6 @@ module.exports = function (Y/* :any */) {
os: Store; os: Store;
ss: Store; ss: Store;
*/ */
/*
Get a type based on the id of its model.
If it does not exist yes, create it.
TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
*/
* getType (id, args) {
var sid = JSON.stringify(id)
var t = this.store.initializedTypes[sid]
if (t == null) {
var op/* :MapStruct | ListStruct */ = yield* this.getOperation(id)
if (op != null) {
t = yield* Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
this.store.initializedTypes[sid] = t
}
}
return t
}
* createType (typedefinition, id) {
var structname = typedefinition[0].struct
id = id || this.store.getNextOpId(1)
var op
if (id[0] === '_') {
op = yield* this.getOperation(id)
} else {
op = Y.Struct[structname].create(id)
op.type = typedefinition[0].name
}
if (typedefinition[0].appendAdditionalInfo != null) {
yield* typedefinition[0].appendAdditionalInfo.call(this, op, typedefinition[1])
}
if (op[0] === '_') {
yield* this.setOperation(op)
} else {
yield* this.applyCreatedOperations([op])
}
return yield* this.getType(id, typedefinition[1])
}
/* createType (typedefinition, id) {
var structname = typedefinition[0].struct
id = id || this.store.getNextOpId(1)
var op = Y.Struct[structname].create(id)
op.type = typedefinition[0].name
if (typedefinition[0].appendAdditionalInfo != null) {
yield* typedefinition[0].appendAdditionalInfo.call(this, op, typedefinition[1])
}
// yield* this.applyCreatedOperations([op])
yield* Y.Struct[op.struct].execute.call(this, op)
yield* this.addOperation(op)
yield* this.store.operationAdded(this, op)
return yield* this.getType(id, typedefinition[1])
}*/
/* /*
Apply operations that this user created (no remote ones!) Apply operations that this user created (no remote ones!)
* does not check for Struct.*.requiredOps() * does not check for Struct.*.requiredOps()
@@ -2282,7 +2318,7 @@ module.exports = function (Y/* :any */) {
} }
if (this.store.forwardAppliedOperations) { if (this.store.forwardAppliedOperations) {
var ops = [] var ops = []
ops.push({struct: 'Delete', target: [d[0], d[1]], length: del[2]}) ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
this.store.y.connector.broadcastOps(ops) this.store.y.connector.broadcastOps(ops)
} }
} }
@@ -2705,7 +2741,7 @@ module.exports = function (Y /* : any*/) {
try { try {
this.eventListeners[i](event) this.eventListeners[i](event)
} catch (e) { } catch (e) {
console.error('User events must not throw Errors!') console.error('Your observer threw an error. This error was caught so that Yjs still can ensure data consistency! In order to debug this error you have to check "Pause On Caught Exceptions"', e)
} }
} }
} }
@@ -2891,7 +2927,13 @@ module.exports = function (Y /* : any*/) {
// finished with remaining operations // finished with remaining operations
self.waiting.push(d) self.waiting.push(d)
} }
checkDelete(op) if (op.key == null) {
// deletes in list
checkDelete(op)
} else {
// deletes in map
this.waiting.push(op)
}
} else { } else {
this.waiting.push(op) this.waiting.push(op)
} }
@@ -2940,7 +2982,11 @@ module.exports = function (Y /* : any*/) {
var o = this.waiting[i] var o = this.waiting[i]
if (o.struct === 'Insert') { if (o.struct === 'Insert') {
var _o = yield* transaction.getInsertion(o.id) var _o = yield* transaction.getInsertion(o.id)
if (!Y.utils.compareIds(_o.id, o.id)) { if (_o.parentSub != null && _o.left != null) {
// if o is an insertion of a map struc (parentSub is defined), then it shouldn't be necessary to compute left
this.waiting.splice(i, 1)
i-- // update index
} else if (!Y.utils.compareIds(_o.id, o.id)) {
// o got extended // o got extended
o.left = [o.id[0], o.id[1] - 1] o.left = [o.id[0], o.id[1] - 1]
} else if (_o.left == null) { } else if (_o.left == null) {
@@ -3100,6 +3146,14 @@ module.exports = function (Y /* : any*/) {
} }
Y.utils.EventHandler = EventHandler Y.utils.EventHandler = EventHandler
/*
Default class of custom types!
*/
class CustomType {
}
Y.utils.CustomType = CustomType
/* /*
A wrapper for the definition of a custom type. A wrapper for the definition of a custom type.
Every custom type must have three properties: Every custom type must have three properties:
@@ -3111,7 +3165,7 @@ module.exports = function (Y /* : any*/) {
* class * class
- the constructor of the custom type (e.g. in order to inherit from a type) - the constructor of the custom type (e.g. in order to inherit from a type)
*/ */
class CustomType { // eslint-disable-line class CustomTypeDefinition { // eslint-disable-line
/* :: /* ::
struct: any; struct: any;
initType: any; initType: any;
@@ -3122,12 +3176,14 @@ module.exports = function (Y /* : any*/) {
if (def.struct == null || if (def.struct == null ||
def.initType == null || def.initType == null ||
def.class == null || def.class == null ||
def.name == null def.name == null ||
def.createType == null
) { ) {
throw new Error('Custom type was not initialized correctly!') throw new Error('Custom type was not initialized correctly!')
} }
this.struct = def.struct this.struct = def.struct
this.initType = def.initType this.initType = def.initType
this.createType = def.createType
this.class = def.class this.class = def.class
this.name = def.name this.name = def.name
if (def.appendAdditionalInfo != null) { if (def.appendAdditionalInfo != null) {
@@ -3139,13 +3195,13 @@ module.exports = function (Y /* : any*/) {
this.parseArguments.typeDefinition = this this.parseArguments.typeDefinition = this
} }
} }
Y.utils.CustomType = CustomType Y.utils.CustomTypeDefinition = CustomTypeDefinition
Y.utils.isTypeDefinition = function isTypeDefinition (v) { Y.utils.isTypeDefinition = function isTypeDefinition (v) {
if (v != null) { if (v != null) {
if (v instanceof Y.utils.CustomType) return [v] if (v instanceof Y.utils.CustomTypeDefinition) return [v]
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomType) return v else if (v.constructor === Array && v[0] instanceof Y.utils.CustomTypeDefinition) return v
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomType) return [v.typeDefinition] else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomTypeDefinition) return [v.typeDefinition]
} }
return false return false
} }
@@ -3405,19 +3461,31 @@ module.exports = Y
Y.requiringModules = requiringModules Y.requiringModules = requiringModules
Y.extend = function (name, value) { Y.extend = function (name, value) {
if (value instanceof Y.utils.CustomType) { if (arguments.length === 2 && typeof name === 'string') {
Y[name] = value.parseArguments if (value instanceof Y.utils.CustomTypeDefinition) {
Y[name] = value.parseArguments
} else {
Y[name] = value
}
if (requiringModules[name] != null) {
requiringModules[name].resolve()
delete requiringModules[name]
}
} else { } else {
Y[name] = value for (var i = 0; i < arguments.length; i++) {
} var f = arguments[i]
if (requiringModules[name] != null) { if (typeof f === 'function') {
requiringModules[name].resolve() f(Y)
delete requiringModules[name] } else {
throw new Error('Expected function!')
}
}
} }
} }
Y.requestModules = requestModules Y.requestModules = requestModules
function requestModules (modules) { function requestModules (modules) {
var sourceDir = Y.sourceDir || '/bower_components'
// determine if this module was compiled for es5 or es6 (y.js vs. y.es6) // determine if this module was compiled for es5 or es6 (y.js vs. y.es6)
// if Insert.execute is a Function, then it isnt a generator.. // if Insert.execute is a Function, then it isnt a generator..
// then load the es5(.js) files.. // then load the es5(.js) files..
@@ -3431,7 +3499,7 @@ function requestModules (modules) {
// module does not exist // module does not exist
if (typeof window !== 'undefined' && window.Y !== 'undefined') { if (typeof window !== 'undefined' && window.Y !== 'undefined') {
var imported = document.createElement('script') var imported = document.createElement('script')
imported.src = Y.sourceDir + '/' + modulename + '/' + modulename + extention imported.src = sourceDir + '/' + modulename + '/' + modulename + extention
document.head.appendChild(imported) document.head.appendChild(imported)
let requireModule = {} let requireModule = {}
@@ -3482,31 +3550,37 @@ type YOptions = {
*/ */
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ { function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
if (opts.sourceDir != null) {
Y.sourceDir = opts.sourceDir
}
opts.types = opts.types != null ? opts.types : [] opts.types = opts.types != null ? opts.types : []
var modules = [opts.db.name, opts.connector.name].concat(opts.types) var modules = [opts.db.name, opts.connector.name].concat(opts.types)
for (var name in opts.share) { for (var name in opts.share) {
modules.push(opts.share[name]) modules.push(opts.share[name])
} }
Y.sourceDir = opts.sourceDir
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
setTimeout(function () { if (opts == null) reject('An options object is expected! ')
Y.requestModules(modules).then(function () { else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
if (opts == null) reject('An options object is expected! ') else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)') else if (opts.db == null) reject('You must specify a database! (missing db property)')
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)') else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
else if (opts.db == null) reject('You must specify a database! (missing db property)') else if (opts.share == null) reject('You must specify a set of shared types!')
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)') else {
else if (opts.share == null) reject('You must specify a set of shared types!') opts = Y.utils.copyObject(opts)
else { opts.connector = Y.utils.copyObject(opts.connector)
opts.db = Y.utils.copyObject(opts.db)
opts.share = Y.utils.copyObject(opts.share)
setTimeout(function () {
Y.requestModules(modules).then(function () {
var yconfig = new YConfig(opts) var yconfig = new YConfig(opts)
yconfig.db.whenUserIdSet(function () { yconfig.db.whenUserIdSet(function () {
yconfig.init(function () { yconfig.init(function () {
resolve(yconfig) resolve(yconfig)
}) })
}) })
} }).catch(reject)
}).catch(reject) }, 0)
}, 0) }
}) })
} }
@@ -3531,6 +3605,9 @@ class YConfig {
for (var propertyname in opts.share) { for (var propertyname in opts.share) {
var typeConstructor = opts.share[propertyname].split('(') var typeConstructor = opts.share[propertyname].split('(')
var typeName = typeConstructor.splice(0, 1) var typeName = typeConstructor.splice(0, 1)
var type = Y[typeName]
var typedef = type.typeDefinition
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
var args = [] var args = []
if (typeConstructor.length === 1) { if (typeConstructor.length === 1) {
try { try {
@@ -3538,11 +3615,13 @@ class YConfig {
} catch (e) { } catch (e) {
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')') throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
} }
if (type.typeDefinition.parseArguments == null) {
throw new Error(typeName + ' does not expect arguments!')
} else {
args = typedef.parseArguments(args[0])[1]
}
} }
var type = Y[typeName] share[propertyname] = yield* this.store.initType.call(this, id, args)
var typedef = type.typeDefinition
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
share[propertyname] = yield* this.createType(type.apply(typedef, args), id)
} }
this.store.whenTransactionsFinished() this.store.whenTransactionsFinished()
.then(callback) .then(callback)

File diff suppressed because one or more lines are too long

6
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long